diff --git a/backend/app/coordinator/routes/enrollments.py b/backend/app/coordinator/routes/enrollments.py index a776879..eeda48f 100644 --- a/backend/app/coordinator/routes/enrollments.py +++ b/backend/app/coordinator/routes/enrollments.py @@ -132,6 +132,60 @@ def create_term_of_defence(examination_schedule_id: int, data: dict) -> dict: return {"message": "Term of defence was created!"} +@bp.post('//add-term-of-defences/') +@bp.input(TermOfDefenceSchema) +@bp.output(MessageSchema) +def create_many_term_of_defences(examination_schedule_id: int, data: dict) -> dict: + if not data: + abort(400, "You have passed empty data!") + + ex = get_examination_schedule_by_id(examination_schedule_id) + + yg_id = ex.year_group_id + project_supervisors_ids = data.pop('project_supervisors') + project_supervisors = ProjectSupervisor.query.filter( + or_(*[ProjectSupervisor.id == i for i in project_supervisors_ids])).filter(YearGroup.id == yg_id).all() + + if len(project_supervisors) != len(project_supervisors_ids): + abort(404, "Project Supervisors didn't exist!") + + start_date = data['start_date'] + end_date = data['end_date'] + if not (ex.start_date.timestamp() <= start_date.timestamp() and ex.end_date.timestamp() >= end_date.timestamp()): + abort(400, "Invalid date range!") + + if end_date <= start_date: + abort(400, "End date must be greater than start date!") + + delta_time = end_date - start_date + delta_time_in_minutes = delta_time.total_seconds() / 60 + if delta_time_in_minutes % ex.duration_time != 0: + abort(400, "Invalid duration time!") + + td = TermOfDefence.query.filter(TermOfDefence.examination_schedule_id == examination_schedule_id). \ + filter( + or_(and_(TermOfDefence.start_date >= start_date, + TermOfDefence.start_date < end_date, + TermOfDefence.end_date >= end_date), + and_(TermOfDefence.start_date <= start_date, + TermOfDefence.end_date > start_date, + TermOfDefence.end_date <= end_date))).first() + + if td is not None: + abort(400, "This term of defence is taken! You choose other date!") + + # create many here + dates = generate_range_dates(start_date, end_date, ex.duration_time) + for start_date in dates: + end_date = start_date + datetime.timedelta(minutes=ex.duration_time) + + td = TermOfDefence(start_date=start_date, end_date=end_date, examination_schedule_id=examination_schedule_id) + td.members_of_committee = project_supervisors + db.session.add(td) + db.session.commit() + return {"message": "Term of defences was created!"} + + @bp.put('//update//') @bp.input(TermOfDefenceSchema) @bp.output(MessageSchema) diff --git a/backend/app/coordinator/routes/project_supervisor.py b/backend/app/coordinator/routes/project_supervisor.py index f591fff..e8e594f 100644 --- a/backend/app/coordinator/routes/project_supervisor.py +++ b/backend/app/coordinator/routes/project_supervisor.py @@ -56,7 +56,7 @@ def list_project_supervisors_by_year_group(year_group_id: int, query: dict) -> d @bp.post("/") @bp.input(ProjectSupervisorCreateSchema) -@bp.output(MessageSchema) +@bp.output(MessageSchema, status_code=201) def create_project_supervisor(data: dict) -> dict: first_name = data['first_name'] last_name = data['last_name'] @@ -72,12 +72,12 @@ def create_project_supervisor(data: dict) -> dict: return {"message": "Project Supervisor was created!", "id": project_supervisor.id} -@bp.get("//detail") +@bp.get("//detail/") @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!") + abort(404, 'Not found project supervisor!') return project_supervisor diff --git a/backend/app/coordinator/routes/year_group.py b/backend/app/coordinator/routes/year_group.py index b6d8045..3496a80 100644 --- a/backend/app/coordinator/routes/year_group.py +++ b/backend/app/coordinator/routes/year_group.py @@ -11,7 +11,7 @@ bp = APIBlueprint("year_group", __name__, url_prefix="/year-group") @bp.post('/') @bp.input(YearGroupSchema) -@bp.output(MessageSchema, status_code=200) +@bp.output(MessageSchema, status_code=201) def create_year_group(data: dict) -> dict: name = data['name'] mode = data['mode'] diff --git a/backend/app/coordinator/schemas/project_supervisor.py b/backend/app/coordinator/schemas/project_supervisor.py index 7ce5244..51982ec 100644 --- a/backend/app/coordinator/schemas/project_supervisor.py +++ b/backend/app/coordinator/schemas/project_supervisor.py @@ -19,13 +19,13 @@ class ProjectSupervisorsPaginationSchema(Schema): class ProjectSupervisorCreateSchema(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=1, max=255), required=True) + email = fields.Str(validate=[validate.Length(min=1, max=255), validate.Email()], required=True) class ProjectSupervisorEditSchema(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=255), required=True) + email = fields.Str(validate=[validate.Length(min=0, max=255), validate.Email()], required=True) class ProjectSupervisorYearGroupSchema(Schema): diff --git a/backend/app/coordinator/schemas/year_group.py b/backend/app/coordinator/schemas/year_group.py index 3612925..4f1e2d0 100644 --- a/backend/app/coordinator/schemas/year_group.py +++ b/backend/app/coordinator/schemas/year_group.py @@ -10,7 +10,7 @@ def validate_mode(value: str) -> str: class YearGroupSchema(Schema): - name = fields.Str(validate=validate.Regexp(r'^\d{4}/\d{4}$'), required=True) + name = fields.Str(validate=validate.Regexp(r'^\d{4}\/\d{4}$'), required=True) mode = fields.Str(validate=validate_mode, required=True) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index ccec8f6..e916fe7 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,6 +1,7 @@ from typing import Generator import pytest +from apiflask import APIFlask from flask import Flask from flask.testing import FlaskClient from flask.ctx import AppContext @@ -22,5 +23,16 @@ def test_app_ctx_with_db(test_app) -> Generator[AppContext, None, None]: @pytest.fixture() -def test_client(test_app: Flask) -> FlaskClient: - return test_app.test_client() +def test_client() -> FlaskClient: + app = create_app("testing") + with app.app_context(): + db.create_all() + return app.test_client() + + +@pytest.fixture() +def test_app_with_context() -> Generator[APIFlask, None, None]: + app = create_app("testing") + with app.app_context(): + db.create_all() + yield app diff --git a/backend/tests/factory.py b/backend/tests/factory.py new file mode 100644 index 0000000..b88c6ad --- /dev/null +++ b/backend/tests/factory.py @@ -0,0 +1,47 @@ +from factory import alchemy, Sequence +from factory.faker import Faker +from factory.fuzzy import FuzzyInteger, FuzzyChoice + +from ..app.dependencies import db +from ..app.students.models import Student, Group +from ..app.project_supervisor.models import ProjectSupervisor, YearGroupProjectSupervisors + + +class ProjectSupervisorFactory(alchemy.SQLAlchemyModelFactory): + class Meta: + model = ProjectSupervisor + sqlalchemy_session = db.session + + first_name = Faker('first_name') + last_name = Faker('last_name') + email = Faker('email') + + +class YearGroupProjectSupervisorsFactory(alchemy.SQLAlchemyModelFactory): + class Meta: + model = YearGroupProjectSupervisors + sqlalchemy_session = db.session + + limit_group = 4 + +class GroupFactory(alchemy.SQLAlchemyModelFactory): + class Meta: + model = Group + sqlalchemy_session = db.session + + name = Sequence(lambda n: f'Group-{n}') + points_for_first_term = FuzzyInteger(1, 5) + points_for_second_term = FuzzyInteger(1, 5) + +# +# class StudentFactory(alchemy.SQLAlchemyModelFactory): +# class Meta: +# model = Student +# sqlalchemy_session = db.session +# +# first_name = Faker('first_name') +# last_name = Faker('last_name') +# email = Faker('email') +# index = Sequence(lambda n: 400_000 + n) +# # group = RelatedFactory(GroupFactory) +# mode = FuzzyChoice([True, False]) diff --git a/backend/tests/fake_data.py b/backend/tests/fake_data.py new file mode 100644 index 0000000..1d56796 --- /dev/null +++ b/backend/tests/fake_data.py @@ -0,0 +1,25 @@ +from typing import List + +from .factory import ProjectSupervisorFactory, YearGroupProjectSupervisorsFactory +from ..app.dependencies import db +from ..app.project_supervisor.models import YearGroup +from ..app.base.mode import ModeGroups + + +def create_year_group(data: dict = None) -> YearGroup: + if data is None: + data = {'mode': ModeGroups.STATIONARY.value, 'name': '2022/2023'} + yg = YearGroup(**data) + db.session.add(yg) + db.session.commit() + return yg + + +def create_project_supervisors(yg: YearGroup, amount: int) -> List[ProjectSupervisorFactory]: + ps = [ProjectSupervisorFactory() for _ in range(amount)] + db.session.add_all(ps) + db.session.commit() + db.session.add_all( + [YearGroupProjectSupervisorsFactory(limit_group=3, year_group_id=yg.id, project_supervisor_id=p.id) for p in ps]) + db.session.commit() + return ps diff --git a/backend/tests/functional_tests/__init__.py b/backend/tests/functional_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/functional_tests/coordinator/__init__.py b/backend/tests/functional_tests/coordinator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/functional_tests/coordinator/test_project_supervisors.py b/backend/tests/functional_tests/coordinator/test_project_supervisors.py new file mode 100644 index 0000000..25e196d --- /dev/null +++ b/backend/tests/functional_tests/coordinator/test_project_supervisors.py @@ -0,0 +1,78 @@ +from ...utils import _test_case_client, _test_case_client_without_response, assert_model_changes +from ...fake_data import create_project_supervisors, create_year_group +from ....app.dependencies import db +from ....app.project_supervisor.models import ProjectSupervisor + +valid_data = { + 'first_name': 'John', + 'last_name': 'Smith', + 'email': 'johnsmith@gmail.com' +} + + +def create_dummy_ps() -> ProjectSupervisor: + ps = ProjectSupervisor(**valid_data) + db.session.add(ps) + db.session.commit() + return ps + + +def test_list_project_supervisors(test_app_with_context) -> None: + with test_app_with_context.test_client() as client: + year_group = create_year_group() + create_project_supervisors(year_group, 25) + data = _test_case_client_without_response(client, '/api/coordinator/project_supervisor/?per_page=10', None, 200, + method='get') + assert data.get('max_pages') == 3 + assert len(data.get('project_supervisors')) == 10 + + +def test_list_project_supervisors_by_year_group(test_app_with_context) -> None: + with test_app_with_context.test_client() as client: + year_group = create_year_group() + year_group_2 = create_year_group() + create_project_supervisors(year_group, 12) + create_project_supervisors(year_group_2, 24) + data = _test_case_client_without_response(client, + f'/api/coordinator/project_supervisor/{year_group.id}/?per_page=10', + None, 200, method='get') + assert data.get('max_pages') == 2 + assert len(data.get('project_supervisors')) == 10 + + +def test_create_project_supervisors(test_app_with_context) -> None: + with test_app_with_context.test_client() as client: + _test_case_client(client, '/api/coordinator/project_supervisor/', valid_data, + 'Project Supervisor was created!', 201, method='post') + + +def test_create_project_supervisors_with_invalid_data(test_app_with_context) -> None: + data = { + 'first_name': 'John', + 'last_name': 'Smith', + 'email': 'johnsmitl.com' + } + with test_app_with_context.test_client() as client: + _test_case_client(client, '/api/coordinator/project_supervisor/', data, + 'Validation error', 400, method='post') + + +def test_create_project_supervisors_if_project_supervisor_has_already_exist(test_app_with_context) -> None: + with test_app_with_context.test_client() as client: + create_dummy_ps() + _test_case_client(client, '/api/coordinator/project_supervisor/', valid_data, + 'Project Supervisor has already exists!', 400, method='post', key='error') + + +def test_detail_project_supervisor(test_app_with_context) -> None: + with test_app_with_context.test_client() as client: + ps = create_dummy_ps() + data = _test_case_client_without_response(client, f'/api/coordinator/project_supervisor/{ps.id}/detail/', + None, 200, method='get') + assert_model_changes(ps, data) + + +def test_detail_project_supervisor_if_project_supervisor_doesnt_exist(test_app_with_context) -> None: + with test_app_with_context.test_client() as client: + _test_case_client(client, f'/api/coordinator/project_supervisor/23/detail/', None, + 'Not found project supervisor!', 404, method='get', key='error') diff --git a/backend/tests/functional_tests/coordinator/test_students.py b/backend/tests/functional_tests/coordinator/test_students.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/functional_tests/coordinator/test_year_group.py b/backend/tests/functional_tests/coordinator/test_year_group.py new file mode 100644 index 0000000..b35df5c --- /dev/null +++ b/backend/tests/functional_tests/coordinator/test_year_group.py @@ -0,0 +1,99 @@ +from ...fake_data import create_year_group +from ...utils import _test_case_client_without_response, _test_case_client, assert_model_changes +from ....app.base.mode import ModeGroups + +valid_data = { + 'mode': ModeGroups.STATIONARY.value, + 'name': '2022/2023' +} + +new_data = { + 'mode': ModeGroups.NON_STATIONARY.value, + 'name': '2021/2022' +} + +example_data = { + 'mode': ModeGroups.STATIONARY.value, + 'name': '2021/2022' +} + + +def test_create_year_group(test_client) -> None: + _test_case_client(test_client, '/api/coordinator/year-group/', valid_data, 'Year group was created!', 201) + + +def test_create_year_group_with_invalid_name(test_client) -> None: + data = { + 'mode': ModeGroups.STATIONARY.value, + 'name': '2022/203232a' + } + _test_case_client(test_client, '/api/coordinator/year-group/', data, 'Validation error', 400) + + +def test_create_year_group_with_invalid_mode(test_client) -> None: + data = { + 'mode': 'xxxx', + 'name': '2022/2033' + } + _test_case_client(test_client, '/api/coordinator/year-group/', data, 'Validation error', 400) + + +def test_create_year_group_if_year_group_already_exists(test_app_with_context) -> None: + with test_app_with_context.test_client() as client: + create_year_group(valid_data) + _test_case_client(client, '/api/coordinator/year-group/', valid_data, 'Year group has already exists!', 400, + 'error') + + +def test_delete_year_group(test_app_with_context) -> None: + with test_app_with_context.test_client() as client: + yg = create_year_group(valid_data) + _test_case_client(client, f'/api/coordinator/year-group/{yg.id}/', None, 'Year group was deleted!', 202, + method='delete') + + +def test_delete_year_group_if_year_group_not_exists(test_app_with_context) -> None: + with test_app_with_context.test_client() as client: + _test_case_client(client, '/api/coordinator/year-group/1/', None, 'Year group doesn\'t exist!', 404, + method='delete', key='error') + + +def test_update_year_group(test_app_with_context) -> None: + with test_app_with_context.test_client() as client: + yg = create_year_group(valid_data) + _test_case_client(client, f'/api/coordinator/year-group/{yg.id}/', new_data, "Year group was updated!", 200, + method='put', key='message') + assert_model_changes(yg, new_data) + + +def test_update_year_group_with_invalid_data(test_app_with_context) -> None: + with test_app_with_context.test_client() as client: + yg = create_year_group(valid_data) + _test_case_client(client, f'/api/coordinator/year-group/{yg.id}/', {'name': '', 'mode': ''}, 'Validation error', + 400, method='put', key='message') + assert_model_changes(yg, valid_data) + + +def test_update_year_group_with_invalid_route_param_year_group_id(test_app_with_context) -> None: + with test_app_with_context.test_client() as client: + _test_case_client(client, f'/api/coordinator/year-group/23/', new_data, 'Not found year group!', + 404, method='put', key='error') + + +def test_update_year_group_with_valid_data_and_year_group_which_has_already_exist(test_app_with_context) -> None: + with test_app_with_context.test_client() as client: + create_year_group(new_data) + yg = create_year_group(valid_data) + _test_case_client(client, f'/api/coordinator/year-group/{yg.id}/', new_data, 'Year group has already exists!', + 400, method='put', key='error') + assert_model_changes(yg, valid_data) + + +def test_list_year_group(test_app_with_context) -> None: + with test_app_with_context.test_client() as client: + ygs = [create_year_group(data) for data in (valid_data, new_data, example_data)] + data = _test_case_client_without_response(client, '/api/coordinator/year-group/', None, 200, method='get') + assert data.get('max_pages') == 1 + for year_group_data in data.get('year_groups'): + yg_id = year_group_data.get('id') + assert_model_changes(list(filter(lambda yg: yg.id == yg_id, ygs))[0], year_group_data) diff --git a/backend/tests/functional_tests/project_supervisors/__init__.py b/backend/tests/functional_tests/project_supervisors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/functional_tests/students/__init__.py b/backend/tests/functional_tests/students/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/utils.py b/backend/tests/utils.py new file mode 100644 index 0000000..2b4ae38 --- /dev/null +++ b/backend/tests/utils.py @@ -0,0 +1,29 @@ +from typing import Union + +from flask.testing import FlaskClient + +from ..app.dependencies import db + + +def assert_model_changes(model: db.Model, expected_data: dict) -> None: + for key, val in expected_data.items(): + assert getattr(model, key) == val + + +def _test_case_client_without_response(test_client: FlaskClient, url: str, data: Union[dict, None], status_code: int, + method: str = 'post') -> dict: + method_func = getattr(test_client, method) + if data is not None: + response = method_func(url, json=data) + else: + response = method_func(url) + + assert response.status_code == status_code + return response.json + + +def _test_case_client(test_client: FlaskClient, url: str, data: Union[dict, None], message: str, status_code: int, + key: str = 'message', method: str = 'post') -> None: + response_data = _test_case_client_without_response(test_client, url, data, status_code, method) + assert key in response_data.keys() + assert response_data.get(key) == message