diff --git a/api/api/serializers.py b/api/api/serializers.py index e7957ff..1a5525d 100644 --- a/api/api/serializers.py +++ b/api/api/serializers.py @@ -29,12 +29,15 @@ class DisciplineSerializer(DisciplineSerializerSchedule): def get_classes(self, discipline: Discipline): teacher_name = self.context.get('teacher_name') + schedule = self.context.get('schedule') classes = discipline.classes.all() if hasattr( discipline, 'classes') else Class.objects.none() if teacher_name: classes = dbh.filter_classes_by_teacher( name=teacher_name, classes=classes) - + if schedule: + classes = dbh.filter_classes_by_schedule( + schedule=schedule, classes=classes) return ClassSerializer(classes, many=True).data @@ -47,6 +50,7 @@ class Meta: model = Schedule exclude = ['user'] + class GenerateSchedulesSerializer(serializers.Serializer): message = serializers.CharField(max_length=200) - schedules = ClassSerializerSchedule(many=True) \ No newline at end of file + schedules = ClassSerializerSchedule(many=True) diff --git a/api/api/tests/test_search_api.py b/api/api/tests/test_search_api.py index 3357735..721d0ba 100644 --- a/api/api/tests/test_search_api.py +++ b/api/api/tests/test_search_api.py @@ -12,16 +12,26 @@ def setUp(self) -> None: name='CÁLCULO 1', code='MAT518', department=self.department) self.discipline_2 = get_or_create_discipline( name='CÁLCULO 2', code='MAT519', department=self.department) - self._class_1 = create_class(teachers=['RICARDO FRAGELLI'], classroom='MOCAP', schedule='46M34', - days=['Quarta-Feira 10:00 às 11:50', 'Sexta-Feira 10:00 às 11:50'], _class="1", special_dates=[], discipline=self.discipline_1) - self._class_2 = create_class(teachers=['VINICIUS RISPOLI'], classroom='S1', schedule='24M34', days=[ - 'Segunda-Feira 10:00 às 11:50', 'Quarta-Feira 10:00 às 11:50'], _class="1", special_dates=[], discipline=self.discipline_2) - self._class_3 = create_class(teachers=['RICARDO RAMOS FRAGELLI'], classroom='MOCAP', schedule='235M34', - days=['Segunda-Feira 10:00 às 11:50', 'Terça-Feira 10:00 às 11:50', 'Quinta-Feira 10:00 às 11:50'], _class="1", special_dates=[], discipline=self.discipline_2) - self._class_4 = create_class(teachers=['VINICIUS CARVALHO RISPOLI'], classroom='S1', schedule='35M34', days=[ - 'Terça-Feira 10:00 às 11:50', 'Quinta-Feira 10:00 às 11:50'], _class="1", special_dates=[], discipline=self.discipline_2) - self._class_5 = create_class(teachers=['RICARDO JUNIOR'], classroom='S1', schedule='35M34', days=[ - 'Terça-Feira 10:00 às 11:50', 'Quinta-Feira 10:00 às 11:50'], _class="1", special_dates=[], discipline=self.discipline_1) + self.discipline_3 = get_or_create_discipline( + name='ÁLGEBRA LINEAR', code='MAT520', department=self.department) + self._class_1 = create_class( + teachers=['RICARDO FRAGELLI'], classroom='MOCAP', schedule='46M34', + days=['Quarta-Feira 10:00 às 11:50', 'Sexta-Feira 10:00 às 11:50'], _class="1", special_dates=[], discipline=self.discipline_1) + self._class_2 = create_class( + teachers=['VINICIUS RISPOLI'], classroom='S1', schedule='24M34', days=[ + 'Segunda-Feira 10:00 às 11:50', 'Quarta-Feira 10:00 às 11:50'], _class="1", special_dates=[], discipline=self.discipline_2) + self._class_3 = create_class( + teachers=['RICARDO RAMOS FRAGELLI'], classroom='MOCAP', schedule='235M34', + days=['Segunda-Feira 10:00 às 11:50', 'Terça-Feira 10:00 às 11:50', 'Quinta-Feira 10:00 às 11:50'], _class="1", special_dates=[], discipline=self.discipline_2) + self._class_4 = create_class( + teachers=['VINICIUS CARVALHO RISPOLI'], classroom='S1', schedule='35M34', days=[ + 'Terça-Feira 10:00 às 11:50', 'Quinta-Feira 10:00 às 11:50'], _class="1", special_dates=[], discipline=self.discipline_2) + self._class_5 = create_class( + teachers=['RICARDO JUNIOR'], classroom='S1', schedule='35M34', days=[ + 'Terça-Feira 10:00 às 11:50', 'Quinta-Feira 10:00 às 11:50'], _class="1", special_dates=[], discipline=self.discipline_1) + self._class_6 = create_class( + teachers=['LUIZA YOKO'], classroom='S1', schedule='35M34', days=[ + 'Terça-Feira 10:00 às 11:50', 'Quinta-Feira 10:00 às 11:50'], _class="1", special_dates=[], discipline=self.discipline_3) def test_with_incomplete_correct_search(self): """ @@ -37,7 +47,6 @@ def test_with_incomplete_correct_search(self): response_for_disciplines = self.client.get( '/courses/?search=calculo&year=2023&period=2') content = json.loads(response_for_disciplines.content) - # Testes da disciplina 1 self.assertEqual(response_for_disciplines.status_code, 200) self.assertEqual(len(content), 2) @@ -427,3 +436,109 @@ def test_correct_search_with_teacher_last_name_with_more_than_one_discipline(sel self.assertEqual(content[1]['name'], self.discipline_2.name) self.assertEqual(content[1]['classes'][0] ['teachers'], self._class_3.teachers) + + def test_correct_search_with_class_schedule_and_department_code(self): + """ + Testa a busca por disciplinas com horário de aula e código do departamento. + É esperado que mais de uma disciplina seja retornada, pois o horário de aula é o mesmo para mais de uma disciplina. + Testes: + - Status code (200 OK) + - Quantidade de disciplinas retornadas + - Quantidade de turmas retornadas em cada disciplina + - Código do departamento + - Nome da disciplina + - Professores da disciplina + """ + schedule = '35M34' # Horário das aulas + department_code = '518' # Código do departamento de matemática + response_for_disciplines = self.client.get( + f'/courses/?search=&year=2023&period=2&department_code={department_code}&schedule={schedule}') + content = json.loads(response_for_disciplines.content) + # Teste da resposta da API + self.assertEqual(response_for_disciplines.status_code, 200) + # As 3 disciplinas existentes no setUp devem ser retornadas + self.assertEqual(len(content), 3) + # Testes das disciplinas retornadas + self.assertEqual(content[0]['department'] + ['code'], self.department.code) + # Testes da discipiina 1 + self.assertEqual(content[0]['name'], self.discipline_1.name) + # Verifica a quantidade de turmas retornadas + self.assertEqual(len(content[0]['classes']), 1) + # Teste do professor da 1ª turma do horário 35M34 + self.assertEqual(content[0]['classes'][0] + ['teachers'], self._class_5.teachers) + # Testes da 2ª disciplina retornada + self.assertEqual(content[1]['department'] + ['code'], self.department.code) + # Verifica o nome da disciplina 2 + self.assertEqual(content[1]['name'], self.discipline_2.name) + # Verifica a quantidade de turmas retornadas + self.assertEqual(len(content[1]['classes']), 2) + self.assertEqual(content[1]['classes'][0] + ['teachers'], self._class_3.teachers) + self.assertEqual(content[1]['classes'][1] + ['teachers'], self._class_4.teachers) + # Testes da 3ª disciplina retornada + self.assertEqual(content[2]['department'] + ['code'], self.department.code) + # Verifica o nome da disciplina 3 + self.assertEqual(content[2]['name'], self.discipline_3.name) + # Verifica a quantidade de turmas retornadas + self.assertEqual(len(content[2]['classes']), 1) + self.assertEqual(content[2]['classes'][0] + ['teachers'], self._class_6.teachers) + + def test_correct_search_with_class_name_and_schedule(self): + """ Testa a busca por disciplinas com nome da matéria e horário de aula. + Testes: + - Status code (200 OK) + - Quantidade de disciplinas retornadas + - Quantidade de turmas retornadas em cada disciplina + - Código do departamento + - Nome da disciplina + - Professores da disciplina + """ + schedule = '35M34' + name = 'calculo' + response_for_disciplines = self.client.get( + f'/courses/?search={name}&year=2023&period=2&schedule={schedule}') + content = json.loads(response_for_disciplines.content) + self.assertEqual(response_for_disciplines.status_code, 200) + + # Testa se retornou apenas as duas disciplinas de Cálculo presentes no setUp + self.assertEqual(len(content), 2) + self.assertEqual(content[0]['department'] + ['code'], self.department.code) + self.assertEqual(content[0]['name'], self.discipline_1.name) + # Testa se para a disciplina 1 foi retornado apenas a turma com o schedule especificado. + self.assertEqual(len(content[0]['classes']), 1) + self.assertEqual(content[0]['classes'][0] + ['teachers'], self._class_5.teachers) + + self.assertEqual(content[1]['department'] + ['code'], self.department.code) + self.assertEqual(content[1]['name'], self.discipline_2.name) + + # Testa se para a disciplina 2 foi retornado as 2 turmas com o schedule especificado. + self.assertEqual(len(content[1]['classes']), 2) + self.assertEqual(content[1]['classes'][0] + ['teachers'], self._class_3.teachers) + self.assertEqual(content[1]['classes'][1] + ['teachers'], self._class_4.teachers) + + def test_correct_search_with_class_name_and_schedule_but_no_classes_to_return(self): + """ + Testa a busca por disciplinas com nome da matéria e horário de aula, + mas não há turmas com o horário especificado. + Testes: + - Status code (200 OK) + - Quantidade de disciplinas retornadas + """ + schedule = '2N34' + name = 'cálculo' + response_for_disciplines = self.client.get( + f'/courses/?search={name}&year=2023&period=2&schedule={schedule}') + content = json.loads(response_for_disciplines.content) + self.assertEqual(response_for_disciplines.status_code, 200) + self.assertEqual(len(content), 0) diff --git a/api/api/views/views.py b/api/api/views/views.py index 353f087..9b239bb 100644 --- a/api/api/views/views.py +++ b/api/api/views/views.py @@ -1,4 +1,4 @@ -from ..models import Discipline +from ..models import Discipline, Class from django.db.models.query import QuerySet @@ -10,7 +10,7 @@ from utils.sessions import get_current_year_and_period, get_next_period from utils.schedule_generator import ScheduleGenerator -from utils.db_handler import get_best_similarities_by_name, filter_disciplines_by_teacher, filter_disciplines_by_year_and_period, filter_disciplines_by_code +from utils.db_handler import get_best_similarities_by_name, filter_disciplines_by_teacher, filter_disciplines_by_year_and_period, filter_disciplines_by_code, filter_disciplines_by_schedule_and_department_code from utils.search import SearchTool from .. import serializers @@ -21,7 +21,7 @@ from traceback import print_exception MAXIMUM_RETURNED_DISCIPLINES = 15 -ERROR_MESSAGE = "no valid argument found for 'search', 'year' or 'period'" +ERROR_MESSAGE = "Bad search parameters or missing parameters" MINIMUM_SEARCH_LENGTH = 4 ERROR_MESSAGE_SEARCH_LENGTH = f"search must have at least {MINIMUM_SEARCH_LENGTH} characters" MAXIMUM_RETURNED_SCHEDULES = 5 @@ -38,11 +38,11 @@ def treat_string(self, string: str | None) -> str | None: def filter_disciplines(self, request: request.Request, name: str) -> QuerySet[Discipline]: search_handler = SearchTool(Discipline) search_fields = ['unicode_name', 'code'] - + result = search_handler.filter_by_search_result( - request = request, - search_str = name, - search_fields = search_fields + request=request, + search_str=name, + search_fields=search_fields ) return result @@ -51,10 +51,8 @@ def retrieve_disciplines_by_similarity(self, request: request.Request, name: str disciplines = self.filter_disciplines(request, name) disciplines = get_best_similarities_by_name(name, disciplines) - if not disciplines.count(): disciplines = filter_disciplines_by_code(code=name[0]) - for term in name[1:]: disciplines &= filter_disciplines_by_code(code=term) @@ -70,12 +68,15 @@ def get_disciplines_and_search_flag(self, request, name): search_by_teacher = True return disciplines, search_by_teacher - def get_serialized_data(self, filter_params: dict, search_by_teacher: bool, name: str) -> list: + def get_serialized_data(self, filter_params: dict, search_by_teacher: bool, name: str, schedule=None, search_by_schedule=False) -> list: filtered_disciplines = filter_disciplines_by_year_and_period( **filter_params) if search_by_teacher: data = serializers.DisciplineSerializer( filtered_disciplines, many=True, context={'teacher_name': name}).data + elif search_by_schedule: + data = serializers.DisciplineSerializer( + filtered_disciplines, many=True, context={'schedule': schedule}).data else: data = serializers.DisciplineSerializer( filtered_disciplines, many=True).data @@ -85,18 +86,25 @@ def get_request_parameters(self, request): name = self.treat_string(request.GET.get('search', None)) year = self.treat_string(request.GET.get('year', None)) period = self.treat_string(request.GET.get('period', None)) - return name, year, period + department_code = self.treat_string( + request.GET.get('department_code', None)) + schedule = self.treat_string(request.GET.get('schedule', None)) + return name, year, period, schedule, department_code - @swagger_auto_schema( + @ swagger_auto_schema( operation_description="Busca disciplinas por nome ou código. O ano e período são obrigatórios.", security=[], manual_parameters=[ openapi.Parameter('search', openapi.IN_QUERY, - description="Termo de pesquisa (Nome/Código)", type=openapi.TYPE_STRING), + description="Termo de pesquisa (Nome/Código/Professor)", type=openapi.TYPE_STRING), openapi.Parameter('year', openapi.IN_QUERY, description="Ano", type=openapi.TYPE_INTEGER), openapi.Parameter('period', openapi.IN_QUERY, description="Período ", type=openapi.TYPE_INTEGER), + openapi.Parameter('department_code', openapi.IN_QUERY, + description="Código do departamento", type=openapi.TYPE_STRING), + openapi.Parameter('schedule', openapi.IN_QUERY, + description="Horário no formato 46M34", type=openapi.TYPE_STRING) ], responses={ 200: openapi.Response('OK', serializers.DisciplineSerializer), @@ -104,29 +112,46 @@ def get_request_parameters(self, request): } ) def get(self, request: request.Request, *args, **kwargs) -> response.Response: - name, year, period = self.get_request_parameters(request) - - if not all((name, year, period)): + name, year, period, schedule, department_code = self.get_request_parameters( + request) + if not all((year, period)): + return handle_400_error(ERROR_MESSAGE) + disciplines, search_by_teacher, search_by_schedule = None, False, False + if name: + if len(name) < MINIMUM_SEARCH_LENGTH: + return handle_400_error(ERROR_MESSAGE_SEARCH_LENGTH) + disciplines, search_by_teacher = self.get_disciplines_and_search_flag( + request, name) + if schedule: + search_by_schedule = True + elif schedule and department_code: + disciplines = filter_disciplines_by_schedule_and_department_code( + schedule=schedule, department_code=department_code) + search_by_schedule = True + else: return handle_400_error(ERROR_MESSAGE) - - if len(name) < MINIMUM_SEARCH_LENGTH: - return handle_400_error(ERROR_MESSAGE_SEARCH_LENGTH) - - disciplines, search_by_teacher = self.get_disciplines_and_search_flag( - request, name) data = self.get_serialized_data( filter_params={'year': year, 'period': period, 'disciplines': disciplines}, search_by_teacher=search_by_teacher, - name=name + search_by_schedule=search_by_schedule, + name=name, + schedule=schedule ) + + data_aux = [] + for i in range(len(data)): + if data[i]['classes'] == []: + data_aux.append(data[i]) + for i in data_aux: + data.remove(i) return response.Response(data[:MAXIMUM_RETURNED_DISCIPLINES], status.HTTP_200_OK) class YearPeriod(APIView): - @swagger_auto_schema( + @ swagger_auto_schema( operation_description="Retorna o ano e período atual, e o próximo ano e período letivos válidos para pesquisa.", security=[], responses={ @@ -159,7 +184,7 @@ def get(self, request: request.Request, *args, **kwargs) -> response.Response: class GenerateSchedule(APIView): - @swagger_auto_schema( + @ swagger_auto_schema( operation_description="Gera possíveis horários de acordo com as aulas escolhidas com preferência de turno", security=[], request_body=openapi.Schema( @@ -245,7 +270,7 @@ def post(self, request: request.Request, *args, **kwargs) -> response.Response: for schedule in schedules[:MAXIMUM_RETURNED_SCHEDULES]: data.append( list(map(lambda x: serializers.ClassSerializerSchedule(x).data, schedule))) - + return response.Response({ 'message': message, 'schedules': data diff --git a/api/utils/db_handler.py b/api/utils/db_handler.py index 08e4cb7..ccf1762 100644 --- a/api/utils/db_handler.py +++ b/api/utils/db_handler.py @@ -31,6 +31,7 @@ def create_class(teachers: list, classroom: str, schedule: str, return Class.objects.create(teachers=teachers, classroom=classroom, schedule=schedule, days=days, _class=_class, special_dates=special_dates, discipline=discipline) + @handle_cache_before_delete def delete_classes_from_discipline(discipline: Discipline) -> QuerySet: """Deleta todas as turmas de uma disciplina.""" @@ -70,11 +71,21 @@ def filter_disciplines_by_teacher(name: str) -> QuerySet: return search_disciplines +def filter_disciplines_by_schedule_and_department_code(schedule: str, department_code: str) -> QuerySet: + """Filtra as disciplinas pelo horário.""" + return Discipline.objects.filter(Q(classes__schedule__icontains=schedule) & Q(department__code=department_code)).distinct("id") + + def filter_disciplines_by_code(code: str, disciplines: Discipline = Discipline.objects) -> QuerySet: """Filtra as disciplinas pelo código.""" return disciplines.filter(code__icontains=code) +def filter_classes_by_schedule(schedule: str, classes: BaseManager[Class] = Class.objects) -> QuerySet: + """Filtra as turmas pelo horário.""" + return classes.filter(schedule__icontains=schedule) + + def filter_disciplines_by_year_and_period(year: str, period: str, disciplines: Discipline = Discipline.objects) -> QuerySet: """Filtra as disciplinas pelo ano e período.""" return disciplines.filter(department__year=year, department__period=period)