add project_supervisors endpoints for coordinator and add Leaders card in frontend
This commit is contained in:
parent
a4a2807fd2
commit
9dd72aeb20
103
backend/app/coordinator/routes/project_supervisor.py
Normal file
103
backend/app/coordinator/routes/project_supervisor.py
Normal 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!"}
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
41
frontend/src/api/leaders.ts
Normal file
41
frontend/src/api/leaders.ts
Normal 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()+'/',
|
||||
)
|
151
frontend/src/views/coordinator/AddLeader.tsx
Normal file
151
frontend/src/views/coordinator/AddLeader.tsx
Normal 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
|
@ -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;
|
Loading…
Reference in New Issue
Block a user