From 9dd72aeb2007446e9587755c4cd97002e167cc5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Drzewi=C5=84ski?= Date: Sun, 12 Jun 2022 23:09:34 +0200 Subject: [PATCH] add project_supervisors endpoints for coordinator and add Leaders card in frontend --- .../coordinator/routes/project_supervisor.py | 103 ++++++++++++ backend/app/coordinator/schemas.py | 43 ++++- backend/app/coordinator/utils.py | 2 +- backend/app/project_supervisor/models.py | 30 +++- backend/app/project_supervisor/schemas.py | 8 - frontend/src/App.tsx | 2 + frontend/src/api/leaders.ts | 41 +++++ frontend/src/views/coordinator/AddLeader.tsx | 151 ++++++++++++++++++ frontend/src/views/coordinator/Leaders.tsx | 136 +++++++++++++++- 9 files changed, 501 insertions(+), 15 deletions(-) create mode 100644 backend/app/coordinator/routes/project_supervisor.py create mode 100644 frontend/src/api/leaders.ts create mode 100644 frontend/src/views/coordinator/AddLeader.tsx diff --git a/backend/app/coordinator/routes/project_supervisor.py b/backend/app/coordinator/routes/project_supervisor.py new file mode 100644 index 0000000..3caf9f5 --- /dev/null +++ b/backend/app/coordinator/routes/project_supervisor.py @@ -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("//", 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("//", 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("/", 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!"} diff --git a/backend/app/coordinator/schemas.py b/backend/app/coordinator/schemas.py index 048128f..d6ab649 100644 --- a/backend/app/coordinator/schemas.py +++ b/backend/app/coordinator/schemas.py @@ -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)) \ No newline at end of file + 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) diff --git a/backend/app/coordinator/utils.py b/backend/app/coordinator/utils.py index ab53f18..011263f 100644 --- a/backend/app/coordinator/utils.py +++ b/backend/app/coordinator/utils.py @@ -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 diff --git a/backend/app/project_supervisor/models.py b/backend/app/project_supervisor/models.py index 60ec9bb..6d296a6 100644 --- a/backend/app/project_supervisor/models.py +++ b/backend/app/project_supervisor/models.py @@ -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 diff --git a/backend/app/project_supervisor/schemas.py b/backend/app/project_supervisor/schemas.py index 66ef104..e69de29 100644 --- a/backend/app/project_supervisor/schemas.py +++ b/backend/app/project_supervisor/schemas.py @@ -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 - diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1e1e41f..99dfbf8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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() { } /> } /> } /> + } /> diff --git a/frontend/src/api/leaders.ts b/frontend/src/api/leaders.ts new file mode 100644 index 0000000..6210960 --- /dev/null +++ b/frontend/src/api/leaders.ts @@ -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( + '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()+'/', + ) diff --git a/frontend/src/views/coordinator/AddLeader.tsx b/frontend/src/views/coordinator/AddLeader.tsx new file mode 100644 index 0000000..aa1bb62 --- /dev/null +++ b/frontend/src/views/coordinator/AddLeader.tsx @@ -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() + + const { mutate: mutateCreateLeader } = useMutation( + 'createLeader', + (payload: Leader) => createLeader(payload), + { + onSuccess: () => { + reset() + setIsAlertVisible(true) + }, + }, + ) + + const onSubmit = (data: Leader) => { + console.log(data) + mutateCreateLeader(data) + } + + return ( +
+ {isAlertVisible && ( +
+ Udało się dodać studenta! +
+ )} + +
+ + + {errors.first_name?.type === 'required' && ( + Imię jest wymagane + )} +
+
+ + + {errors.last_name?.type === 'required' && ( + Nazwisko jest wymagane + )} +
+
+ + + {errors.email?.type === 'required' && ( + Email jest wymagany + )} +
+
+ + + {errors.limit_group?.type === 'pattern' && ( + Limit grup musi być liczbą dodatnią + )} +
+
+ +
+ + +
+
+ + +
+
+ + +
+ {errors.mode?.type === 'required' && ( + Wybierz tryb studiów + )} +
+ +
+ ) +} + +export default AddLeader diff --git a/frontend/src/views/coordinator/Leaders.tsx b/frontend/src/views/coordinator/Leaders.tsx index 616617f..1b7df01 100644 --- a/frontend/src/views/coordinator/Leaders.tsx +++ b/frontend/src/views/coordinator/Leaders.tsx @@ -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 ( +
+
+
+ +
+
+ +
+
+ {isLeadersLoading ? ( +
Ładowanie
+ ) : ( + <> +
+ + + + + + + + + + + + + + {leaders?.data?.project_supervisors + .map(({ id, first_name, last_name, email, limit_group, count_group, mode }) => ( + + + + + + + + + ))} + +
ImięNazwiskoEmailLimit grupLiczba grupTryb
{first_name}{last_name}{email}{limit_group}{mode==0 ? "Stacjonarny" : mode==1 ? "Niestacjonarny" : "Nie/stacjonarny"}
+
+
+
+ + {[ + ...Array( + leaders?.data?.max_pages && leaders?.data?.max_pages + 1, + ).keys(), + ] + .slice(1) + .map((p) => ( + + ))} + +
+
+ + )} +
+ ) +} export default Leaders; \ No newline at end of file