add project_supervisors endpoints for coordinator and add Leaders card in frontend

This commit is contained in:
Patryk Drzewiński 2022-06-12 23:09:34 +02:00
parent a4a2807fd2
commit 9dd72aeb20
9 changed files with 501 additions and 15 deletions

View File

@ -0,0 +1,103 @@
from flask import abort
from apiflask import APIBlueprint
from flask_sqlalchemy import get_debug_queries
from ...project_supervisor.models import ProjectSupervisor
from ..schemas import ProjectSupervisorSchema, ProjectSupervisorEditSchema, ProjectSupervisorsPaginationSchema, \
ProjectSupervisorCreateSchema, MessageSchema, FileSchema, ProjectSupervisorQuerySchema
from ...dependencies import db, ma
from ...base.utils import paginate_models
bp = APIBlueprint("project_supervisor", __name__, url_prefix="/project_supervisor")
@bp.route("/", methods=["GET"])
@bp.input(ProjectSupervisorQuerySchema, location='query')
@bp.output(ProjectSupervisorsPaginationSchema)
def list_project_supervisors(query: dict) -> dict:
fullname = query.get('fullname')
order_by_first_name = query.get('order_by_first_name')
order_by_last_name = query.get('order_by_last_name')
mode = query.get('mode')
page = query.get('page')
per_page = query.get('per_page')
project_supervisor_query = ProjectSupervisor.search_by_fullname_and_mode_and_order_by_first_name_or_last_name(
fullname, mode, order_by_first_name, order_by_last_name)
response = paginate_models(page, project_supervisor_query, per_page)
if (message := response.get('message')) is not None:
abort(response['status_code'], message)
# print(get_debug_queries()[0])
return {
"project_supervisors": response['items'],
"max_pages": response['max_pages']
}
@bp.route("/", methods=["POST"])
@bp.input(ProjectSupervisorCreateSchema)
@bp.output(MessageSchema)
def create_project_supervisor(data: dict) -> dict:
first_name = data['first_name']
last_name = data['last_name']
email = data['email']
limit_group = data['limit_group']
mode = data['mode']
project_supervisor = ProjectSupervisor.query.filter(first_name=first_name).filter(last_name=last_name).first()
if project_supervisor is not None:
abort(400, "Project Supervisor has already exists!")
project_supervisor = ProjectSupervisor(first_name=first_name,
last_name = last_name,
email = email,
limit_group = limit_group,
mode = mode)
db.session.add(project_supervisor)
db.session.commit()
return {"message": "Project Supervisor was created!"}
@bp.route("/<int:id>/", methods=["GET"])
@bp.output(ProjectSupervisorSchema)
def detail_project_supervisor(id: int) -> ProjectSupervisor:
project_supervisor = ProjectSupervisor.query.filter_by(id=id).first()
if project_supervisor is None:
abort(400, f"Project Supervisor with id {id} doesn't exist!")
return project_supervisor
@bp.route("/<int:id>/", methods=["DELETE"])
@bp.output(MessageSchema, status_code=202)
def delete_project_supervisor(id: int) -> dict:
project_supervisor = ProjectSupervisor.query.filter_by(id=id).first()
if project_supervisor is None:
abort(400, f"Project Supervisor with id {id} doesn't exist!")
if project_supervisor.count_groups > 0:
abort(400, f"Project Supervisor with id {id} has gropus!")
db.session.delete(project_supervisor)
db.session.commit()
return {"message": "Project Supervisor was deleted!"}
@bp.route("/<int:id>", methods=["PUT"])
@bp.input(ProjectSupervisorEditSchema)
@bp.output(MessageSchema)
def edit_project_supervisor(id: int, data: dict) -> dict:
if not data:
abort(400, 'You have passed empty data!')
project_supervisor_query = ProjectSupervisor.query.filter_by(id=id)
project_supervisor = project_supervisor_query.first()
if project_supervisor is None:
abort(400, f"Project Supervisor with id {id} doesn't exist!")
project_supervisor_query.update(project_supervisor)
db.session.commit()
return {"message": "Project Supervisor was updated!"}

View File

@ -1,8 +1,7 @@
from ..dependencies import ma
from ..students.models import Student, Group
from ..project_supervisor.models import ProjectSupervisor
from marshmallow import fields, validate, ValidationError
from ..project_supervisor.schemas import ProjectSupervisorSchema
def validate_index(index):
if len(str(index)) > 6:
@ -10,6 +9,13 @@ def validate_index(index):
elif len(str(index)) < 6:
raise ValidationError("Length of index is too short!")
class ProjectSupervisorSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = ProjectSupervisor
include_relationships = False
class GroupSchema(ma.SQLAlchemyAutoSchema):
project_supervisor = fields.Nested(ProjectSupervisorSchema)
@ -82,4 +88,35 @@ class GroupCreateSchema(ma.Schema):
class GroupEditSchema(ma.Schema):
name = fields.Str(validate=validate.Length(min=1, max=255))
project_supervisor_id = fields.Integer(validate=validate_index)
students = fields.List(fields.Nested(StudentSchema), validate=validate.Length(min=1, max=255))
students = fields.List(fields.Nested(StudentSchema), validate=validate.Length(min=1, max=255))
class ProjectSupervisorQuerySchema(ma.Schema):
fullname = fields.Str()
order_by_first_name = fields.Str()
order_by_last_name = fields.Str()
page = fields.Integer()
per_page = fields.Integer()
mode = fields.Integer()
class ProjectSupervisorsPaginationSchema(ma.Schema):
project_supervisors = fields.List(fields.Nested(ProjectSupervisorSchema))
max_pages = fields.Integer()
class ProjectSupervisorCreateSchema(ma.Schema):
first_name = fields.Str(validate=validate.Length(min=1, max=255), required=True)
last_name = fields.Str(validate=validate.Length(min=1, max=255), required=True)
email = fields.Str(validate=validate.Length(min=0, max=11), required=True)
limit_group = fields.Integer(validate=validate_index)
mode = fields.Integer(required=True)
class ProjectSupervisorEditSchema(ma.Schema):
first_name = fields.Str(validate=validate.Length(min=1, max=255), required=True)
last_name = fields.Str(validate=validate.Length(min=1, max=255), required=True)
email = fields.Str(validate=validate.Length(min=0, max=11), required=True)
limit_group = fields.Integer(validate=validate_index)
count_groups = fields.Integer(validate=validate_index)
mode = fields.Integer(required=True)

View File

@ -26,7 +26,7 @@ def check_columns(df: pd.DataFrame) -> bool:
def parse_csv(file, mode) -> Generator[Student, Any, None]:
df = pd.read_csv(file, mode)
df = pd.read_csv(file)
# if not check_columns(df):
# raise InvalidNameOrTypeHeaderException

View File

@ -1,10 +1,36 @@
from ..dependencies import db
from ..base.models import Person, Base
from flask_sqlalchemy import BaseQuery
from sqlalchemy import or_
from sqlalchemy.sql import text
from ..base.utils import order_by_column_name
class ProjectSupervisor(Base, Person):
__tablename__ = "project_supervisors"
limit_group = db.Column(db.Integer, default=1, nullable=False)
count_groups = db.Column(db.Integer, default=1, nullable=False)
limit_group = db.Column(db.Integer, default=3, nullable=False)
count_groups = db.Column(db.Integer, default=0, nullable=False)
mode = db.Column(db.Integer, default=0, nullable=False) # 0 - stationary, 1 - non-stationary, 2 - both
@classmethod
def search_by_fullname_and_mode_and_order_by_first_name_or_last_name(cls, fullname: str = None,
mode: int = None,
order_by_first_name: str = None,
order_by_last_name: str = None) -> BaseQuery:
project_supervisors_query = cls.query
if mode is not None:
project_supervisors_query = project_supervisors_query.filter(mode != 1-mode)
if fullname is not None:
"""This works only for sqlite3 database - concat function doesn't exist so i used builtin concat
operator specific only for sqlite db - || """
project_supervisors_query = project_supervisors_query.filter(
text("project_supervisorss_first_name || ' ' || project_supervisorss_last_name LIKE :fullname ")
).params(fullname=f'{fullname}%')
project_supervisors_query = order_by_column_name(project_supervisors_query, ProjectSupervisor.first_name, order_by_first_name)
project_supervisors_query = order_by_column_name(project_supervisors_query, ProjectSupervisor.last_name, order_by_last_name)
return project_supervisors_query

View File

@ -1,8 +0,0 @@
from ..dependencies import ma
from ..project_supervisor.models import ProjectSupervisor
class ProjectSupervisorSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = ProjectSupervisor
include_relationships = False

View File

@ -4,6 +4,7 @@ import { Route, Routes } from 'react-router-dom'
import './App.css'
import AddGroup from './views/coordinator/AddGroup'
import AddStudent from './views/coordinator/AddStudent'
import AddLeader from './views/coordinator/AddLeader'
import Coordinator from './views/coordinator/Coordinator'
import Groups from './views/coordinator/Groups'
import Leaders from './views/coordinator/Leaders'
@ -31,6 +32,7 @@ function App() {
<Route path="leaders" element={<Leaders />} />
<Route path="add-student" element={<AddStudent />} />
<Route path="add-group" element={<AddGroup />} />
<Route path="add-leader" element={<AddLeader />} />
</Route>
</Routes>
</QueryClientProvider>

View File

@ -0,0 +1,41 @@
import axiosInstance from './axiosInstance'
type OrderType = 'asc' | 'desc'
interface LeaderResponse {
max_pages: number
project_supervisors: Leader[]
}
export interface Leader {
id: number
first_name: string
last_name: string
email: string
limit_group: number
count_group: number
mode: number
}
export const getLeaders = (
params: Partial<{
fullname: string
order_by_first_name: OrderType
order_by_last_name: OrderType
page: number
per_page: number
}> = {},
) =>
axiosInstance.get<LeaderResponse>(
'http://127.0.0.1:5000/api/coordinator/project_supervisors',
{ params },
)
export const createLeader = (payload: Leader) =>
axiosInstance.post('http://127.0.0.1:5000/api/coordinator/project_supervisors/', payload)
export const deleteLeader = (payload: Number) =>
axiosInstance.delete(
'http://127.0.0.1:5000/api/coordinator/project_supervisors/'+payload.toString()+'/',
)

View File

@ -0,0 +1,151 @@
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { useMutation } from 'react-query'
import { createLeader, Leader } from '../../api/leaders'
import InputError from '../../components/InputError'
const AddLeader = () => {
const [isAlertVisible, setIsAlertVisible] = useState(false)
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm<Leader>()
const { mutate: mutateCreateLeader } = useMutation(
'createLeader',
(payload: Leader) => createLeader(payload),
{
onSuccess: () => {
reset()
setIsAlertVisible(true)
},
},
)
const onSubmit = (data: Leader) => {
console.log(data)
mutateCreateLeader(data)
}
return (
<form
className="w-full lg:w-1/4 flex flex-col mx-auto"
onSubmit={handleSubmit(onSubmit)}
>
{isAlertVisible && (
<div className="alert alert-success shadow-lg">
<span>Udało się dodać studenta!</span>
</div>
)}
<div className="form-control">
<label className="label" htmlFor="first_name">
Imię
</label>
<input
className="input input-bordered"
id="first_name"
type="text"
{...register('first_name', { required: true })}
/>
{errors.first_name?.type === 'required' && (
<InputError>Imię jest wymagane</InputError>
)}
</div>
<div className="form-control">
<label className="label" htmlFor="last_name">
Nazwisko
</label>
<input
className="input input-bordered"
id="last_name"
type="text"
{...register('last_name', { required: true })}
/>
{errors.last_name?.type === 'required' && (
<InputError>Nazwisko jest wymagane</InputError>
)}
</div>
<div className="form-control">
<label className="label" htmlFor="email">
Email
</label>
<input
className="input input-bordered"
id="email"
type="text"
{...register('email', {
required: true,
})}
/>
{errors.email?.type === 'required' && (
<InputError>Email jest wymagany</InputError>
)}
</div>
<div className="form-control">
<label className="label" htmlFor="limit_group">
Limit grup
</label>
<input
className="input input-bordered"
id="limit_group"
type="text"
{...register('limit_group', {
required: false,
pattern: /^[1-9][0-9]*$/,
})}
/>
{errors.limit_group?.type === 'pattern' && (
<InputError>Limit grup musi być liczbą dodatnią</InputError>
)}
</div>
<div className="form-control gap-2">
<label className="label">Tryb studiów</label>
<div className="flex gap-2">
<input
className="radio"
id="mode-1"
type="radio"
{...register('mode', {
required: true,
})}
value="1"
/>
<label htmlFor="mode-1">Stacjonarny</label>
</div>
<div className="flex gap-2">
<input
className="radio"
id="mode-0"
type="radio"
{...register('mode', {
required: true,
})}
value="0"
/>
<label htmlFor="mode-0">Niestacjonarny</label>
</div>
<div className="flex gap-2">
<input
className="radio"
id="mode-2"
type="radio"
{...register('mode', {
required: true,
})}
value="0"
/>
<label htmlFor="mode-2">Oba</label>
</div>
{errors.mode?.type === 'required' && (
<InputError>Wybierz tryb studiów</InputError>
)}
</div>
<button className="btn btn-success mt-4">Dodaj opiekuna</button>
</form>
)
}
export default AddLeader

View File

@ -1,3 +1,137 @@
const Leaders = () => <>Opiekunowie</>
import React, { useEffect, useState } from 'react'
import { useMutation, useQuery } from 'react-query'
import { useNavigate } from 'react-router-dom'
import { getLeaders, deleteLeader } from '../../api/leaders'
import classNames from 'classnames'
const Leaders = () => {
let navigate = useNavigate()
const [page, setPage] = useState(1)
const [perPage, setPerPage] = useState(10)
const perPageOptions = [
{
value: 10,
label: '10 rekordów',
},
{
value: 20,
label: '20 rekordów',
},
{
value: 50,
label: '50 rekordów',
},
{
value: 1000,
label: 'Pokaż wszystkie',
},
]
const {
isLoading: isLeadersLoading,
data: leaders,
refetch: refetchLeaders,
} = useQuery(['project_supervisors', page, perPage], () =>
getLeaders({ page, per_page: perPage }),
)
useEffect(() => {
setPage(1)
}, [perPage])
return (
<div>
<div className="flex items-center justify-between flex-col gap-3 md:flex-row md:gap-0">
<div>
<button
className="btn btn-success btn-xs md:btn-md"
onClick={() => navigate('/coordinator/add-leader')}
>
Dodaj nowego opiekuna
</button>
</div>
<div className="flex">
<select className="select select-xs md:select-md select-bordered mr-3">
{perPageOptions.map(({ value, label }) => (
<option key={value} onClick={() => setPerPage(value)}>
{label}
</option>
))}
</select>
</div>
</div>
{isLeadersLoading ? (
<div>Ładowanie</div>
) : (
<>
<div className="flex mx-auto mt-5 overflow-hidden overflow-x-auto border border-gray-100 rounded">
<table className="min-w-full table table-compact">
<thead>
<tr className="bg-gray-50">
<th>Imię</th>
<th>Nazwisko</th>
<th>Email</th>
<th>Limit grup</th>
<th>Liczba grup</th>
<th>Tryb</th>
<th></th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{leaders?.data?.project_supervisors
.map(({ id, first_name, last_name, email, limit_group, count_group, mode }) => (
<tr key={id}>
<td>{first_name}</td>
<td>{last_name}</td>
<td>{email}</td>
<td>{limit_group}</td>
<td>{mode==0 ? "Stacjonarny" : mode==1 ? "Niestacjonarny" : "Nie/stacjonarny"}</td>
<td><button onClick={() => deleteLeader(id).then(() => refetchLeaders())}>X</button></td>
</tr>
))}
</tbody>
</table>
</div>
<div className="w-full flex items-center justify-center mt-2">
<div className="btn-group">
<button
className="btn btn-outline"
onClick={() => setPage(page - 1)}
disabled={page === 1}
>
«
</button>
{[
...Array(
leaders?.data?.max_pages && leaders?.data?.max_pages + 1,
).keys(),
]
.slice(1)
.map((p) => (
<button
key={p}
className={classNames('btn btn-outline', {
'bg-success': p === page,
})}
onClick={() => setPage(p)}
>
{p}
</button>
))}
<button
className="btn btn-outline"
onClick={() => setPage(page + 1)}
disabled={page === leaders?.data?.max_pages}
>
»
</button>
</div>
</div>
</>
)}
</div>
)
}
export default Leaders;