From 3455261f4ff7987de61a9994b2ab86077790b653 Mon Sep 17 00:00:00 2001 From: Gabriel Henrique Castelo <81991244+GabrielCastelo-31@users.noreply.github.com> Date: Wed, 20 Dec 2023 22:54:02 -0300 Subject: [PATCH] api(search): Filtra busca por nome do professor (#167) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * api(search): Adiciona testes para a busca por nome de professor - Testes para casos de busca com nome completo, nome parcial e sobrenome. * api(search):Adiciona filtro de busca por professor - Apenas as turmas que possuem o professor buscado serão retornadas * api(search): Reduz complexidade cognitiva da view de busca - Foram adicionadas funções para reduzir a complexidade da view de busca * api(searh): Reduz a quantidade de argumetos da função get_serialized_data --- api/api/serializers.py | 15 +- api/api/tests/test_search_api.py | 231 ++++++++++++++++++++++++++++++- api/api/views/views.py | 66 ++++++--- api/utils/db_handler.py | 25 +++- 4 files changed, 308 insertions(+), 29 deletions(-) diff --git a/api/api/serializers.py b/api/api/serializers.py index d08db8b..6c12837 100644 --- a/api/api/serializers.py +++ b/api/api/serializers.py @@ -1,5 +1,7 @@ from rest_framework.serializers import ModelSerializer +from rest_framework import serializers from api.models import Department, Discipline, Class, Schedule +import utils.db_handler as dbh class DepartmentSerializer(ModelSerializer): @@ -23,12 +25,23 @@ class Meta: class DisciplineSerializer(DisciplineSerializerSchedule): - classes = ClassSerializer(many=True) + classes = serializers.SerializerMethodField() + + def get_classes(self, discipline: Discipline): + teacher_name = self.context.get('teacher_name') + 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) + + return ClassSerializer(classes, many=True).data class ClassSerializerSchedule(ClassSerializer): discipline = DisciplineSerializerSchedule() + class ScheduleSerializer(ModelSerializer): class Meta: model = Schedule diff --git a/api/api/tests/test_search_api.py b/api/api/tests/test_search_api.py index ca011d2..3357735 100644 --- a/api/api/tests/test_search_api.py +++ b/api/api/tests/test_search_api.py @@ -3,6 +3,7 @@ from api.views.views import ERROR_MESSAGE, ERROR_MESSAGE_SEARCH_LENGTH import json + class TestSearchAPI(APITestCase): def setUp(self) -> None: self.department = get_or_create_department( @@ -15,6 +16,12 @@ def setUp(self) -> None: 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) def test_with_incomplete_correct_search(self): """ @@ -174,9 +181,12 @@ def test_with_only_spaces(self): - Status code (400 BAD REQUEST) """ - response_1 = self.client.get('/courses/?search= &year=2023&period=2') - response_2 = self.client.get('/courses/?search=calculo&year= &period=2') - response_3 = self.client.get('/courses/?search=calculo&year=2023&period= ') + response_1 = self.client.get( + '/courses/?search= &year=2023&period=2') + response_2 = self.client.get( + '/courses/?search=calculo&year= &period=2') + response_3 = self.client.get( + '/courses/?search=calculo&year=2023&period= ') content_1 = json.loads(response_1.content) content_2 = json.loads(response_2.content) content_3 = json.loads(response_3.content) @@ -192,7 +202,7 @@ def test_with_only_spaces(self): self.assertEqual(response_3.status_code, 400) self.assertEqual(len(content_3), 1) self.assertEqual(content_3['errors'], ERROR_MESSAGE) - + def test_with_insufficient_search_length(self): """ Testa a busca por disciplinas com menos de 4 caracteres no parâmetro de busca @@ -205,4 +215,215 @@ def test_with_insufficient_search_length(self): self.assertEqual(response_1.status_code, 400) self.assertEqual(len(content_1), 1) - self.assertEqual(content_1['errors'], ERROR_MESSAGE_SEARCH_LENGTH) \ No newline at end of file + self.assertEqual(content_1['errors'], ERROR_MESSAGE_SEARCH_LENGTH) + + def test_correct_search_with_teacher_full_name(self): + """ + Testa a busca por disciplinas com nome completo de professor + 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 + """ + response_for_disciplines = self.client.get( + '/courses/?search=Ricardo Ramos Fragelli&year=2023&period=2') + content = json.loads(response_for_disciplines.content) + + self.assertEqual(response_for_disciplines.status_code, 200) + self.assertEqual(len(content), 1) + + # Testes da disciplina retornada + self.assertEqual(content[0]['department'] + ['code'], self.department.code) + self.assertEqual(content[0]['name'], self.discipline_2.name) + + # Verifica a quantidade de turmas retornadas + self.assertEqual(len(content[0]['classes']), 1) + + # Teste da 1ª turma do professor Ricardo + self.assertEqual(content[0]['classes'][0] + ['teachers'], self._class_3.teachers) + + def test_correct_search_with_teacher_first_name_and_last_name(self): + """ + Testa a busca por disciplinas com primeiro nome e sobrenome de professor + 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 + """ + response_for_disciplines = self.client.get( + '/courses/?search=Ricardo Fragelli&year=2023&period=2') + content = json.loads(response_for_disciplines.content) + + # Teste da resposta da API + self.assertEqual(response_for_disciplines.status_code, 200) + self.assertEqual(len(content), 2) + + # Verifica a quantidade de turmas retornadas em cada disciplina + self.assertEqual(len(content[0]['classes']), 1) + self.assertEqual(len(content[1]['classes']), 1) + + # Testes da 1ª disciplina do professor Ricardo + self.assertEqual(content[0]['department'] + ['code'], self.department.code) + self.assertEqual(content[0]['name'], self.discipline_1.name) + self.assertEqual(content[0]['classes'][0] + ['teachers'], self._class_1.teachers) + + # Testes da 2ª disciplina do professor Ricardo + self.assertEqual(content[1]['department'] + ['code'], self.department.code) + 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_teacher_first_name_with_one_discipline(self): + """ + Testa a busca por disciplinas com primeiro nome de professor que leciona apenas 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 + """ + response_for_disciplines = self.client.get( + '/courses/?search=vinicius&year=2023&period=2') + content = json.loads(response_for_disciplines.content) + + # Teste da resposta da API + self.assertEqual(response_for_disciplines.status_code, 200) + self.assertEqual(len(content), 1) + + # Testes da disciplina retornada + self.assertEqual(content[0]['department'] + ['code'], self.department.code) + self.assertEqual(content[0]['name'], self.discipline_2.name) + + # Verifica a quantidade de turmas retornadas + self.assertEqual(len(content[0]['classes']), 2) + + # Teste da 1ª turma do professor Vinicius + self.assertEqual(content[0]['classes'][0] + ['teachers'], self._class_2.teachers) + + # Teste da 2ª turma do professor Vinicius + self.assertEqual(content[0]['classes'][1] + ['teachers'], self._class_4.teachers) + + def test_correct_search_with_teacher_first_name_with_more_than_one_discipline(self): + """ + Testa a busca por disciplinas com primeiro nome de professor que leciona 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 + """ + response_for_disciplines = self.client.get( + '/courses/?search=ricardo&year=2023&period=2') + content = json.loads(response_for_disciplines.content) + + # Testes da resposta da API + self.assertEqual(response_for_disciplines.status_code, 200) + self.assertEqual(len(content), 2) + + # Verifica a quantidade de turmas retornadas em cada disciplina + self.assertEqual(len(content[0]['classes']), 2) + self.assertEqual(len(content[1]['classes']), 1) + + # Testes da 1ª disciplina do professor Ricardo + self.assertEqual(content[0]['department'] + ['code'], self.department.code) + self.assertEqual(content[0]['name'], self.discipline_1.name) + self.assertEqual(content[0]['classes'][0] + ['teachers'], self._class_1.teachers) + + # Testes da 2ª disciplina do professor Ricardo + self.assertEqual(content[1]['department'] + ['code'], self.department.code) + 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_teacher_last_name_with_one_discipline(self): + """ + Testa a busca por disciplinas com sobrenome de professor que leciona apenas 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 + """ + response_for_disciplines = self.client.get( + '/courses/?search=rispoli&year=2023&period=2') + content = json.loads(response_for_disciplines.content) + + # Teste da resposta da API + self.assertEqual(response_for_disciplines.status_code, 200) + self.assertEqual(len(content), 1) + + # Testes da disciplina retornada + self.assertEqual(content[0]['department'] + ['code'], self.department.code) + self.assertEqual(content[0]['name'], self.discipline_2.name) + + # Verifica a quantidade de turmas retornadas + self.assertEqual(len(content[0]['classes']), 2) + + # Teste da 1ª turma do professor Vinicius + self.assertEqual(content[0]['classes'][0] + ['teachers'], self._class_2.teachers) + + # Teste da 2ª turma do professor Vinicius + self.assertEqual(content[0]['classes'][1] + ['teachers'], self._class_4.teachers) + + def test_correct_search_with_teacher_last_name_with_more_than_one_discipline(self): + """ + Testa a busca por disciplinas com sobrenome de professor que leciona 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 + """ + response_for_disciplines = self.client.get( + '/courses/?search=fragelli&year=2023&period=2') + content = json.loads(response_for_disciplines.content) + + # Testes da resposta da API + self.assertEqual(response_for_disciplines.status_code, 200) + self.assertEqual(len(content), 2) + + # Verifica a quantidade de turmas retornadas em cada disciplina + self.assertEqual(len(content[0]['classes']), 1) + self.assertEqual(len(content[1]['classes']), 1) + + # Testes da 1ª disciplina do professor Fragelli + self.assertEqual(content[0]['department'] + ['code'], self.department.code) + self.assertEqual(content[0]['name'], self.discipline_1.name) + self.assertEqual(content[0]['classes'][0] + ['teachers'], self._class_1.teachers) + + # Testes da 2ª disciplina do professor Fragelli + self.assertEqual(content[1]['department'] + ['code'], self.department.code) + self.assertEqual(content[1]['name'], self.discipline_2.name) + self.assertEqual(content[1]['classes'][0] + ['teachers'], self._class_3.teachers) diff --git a/api/api/views/views.py b/api/api/views/views.py index 735a285..b0070f7 100644 --- a/api/api/views/views.py +++ b/api/api/views/views.py @@ -1,4 +1,5 @@ from unidecode import unidecode +from ..models import Class, Discipline from django.contrib import admin from django.db.models.query import QuerySet @@ -11,9 +12,9 @@ from utils.sessions import get_current_year_and_period, get_next_period from utils.schedule_generator import ScheduleGenerator -from utils import db_handler as dbh +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 api import serializers +from .. import serializers from api.swagger import Errors from api.models import Discipline from api.views.utils import handle_400_error @@ -36,11 +37,12 @@ def treat_string(self, string: str | None) -> str | None: def filter_disciplines(self, request: request.Request, name: str) -> QuerySet[Discipline]: unicode_name = unidecode(name).casefold() - model_handler = admin.ModelAdmin(Discipline, admin.site) - model_handler.search_fields = ['unicode_name', 'code'] + model_handler.search_fields = [ + 'unicode_name', 'code'] disciplines = Discipline.objects.all() + disciplines, _ = model_handler.get_search_results( request, disciplines, unicode_name) @@ -48,18 +50,43 @@ def filter_disciplines(self, request: request.Request, name: str) -> QuerySet[Di def retrieve_disciplines_by_similarity(self, request: request.Request, name: str) -> QuerySet[Discipline]: disciplines = self.filter_disciplines(request, name) - disciplines = dbh.get_best_similarities_by_name(name, disciplines) + + disciplines = get_best_similarities_by_name(name, disciplines) if not disciplines.count(): - disciplines = dbh.filter_disciplines_by_code(code=name[0]) + disciplines = filter_disciplines_by_code(code=name[0]) for term in name[1:]: - disciplines &= dbh.filter_disciplines_by_code(code=term) + disciplines &= filter_disciplines_by_code(code=term) - disciplines = dbh.filter_disciplines_by_code(name) + disciplines = filter_disciplines_by_code(name) return disciplines + def get_disciplines_and_search_flag(self, request, name): + disciplines = self.retrieve_disciplines_by_similarity(request, name) + search_by_teacher = False + if not disciplines.count(): + disciplines = filter_disciplines_by_teacher(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: + 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 + else: + data = serializers.DisciplineSerializer( + filtered_disciplines, many=True).data + return data + + 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 + @swagger_auto_schema( operation_description="Busca disciplinas por nome ou código. O ano e período são obrigatórios.", security=[], @@ -77,27 +104,22 @@ def retrieve_disciplines_by_similarity(self, request: request.Request, name: str } ) def get(self, request: request.Request, *args, **kwargs) -> response.Response: - 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)) + name, year, period = self.get_request_parameters(request) - name_verified = name is not None and len(name) > 0 - year_verified = year is not None and len(year) > 0 - period_verified = period is not None and len(period) > 0 - - if not name_verified or not year_verified or not period_verified: + if not all((name, year, period)): return handle_400_error(ERROR_MESSAGE) if len(name) < MINIMUM_SEARCH_LENGTH: return handle_400_error(ERROR_MESSAGE_SEARCH_LENGTH) - disciplines = self.retrieve_disciplines_by_similarity(request, name) - - filtered_disciplines = dbh.filter_disciplines_by_year_and_period( - year=year, period=period, disciplines=disciplines) - data = serializers.DisciplineSerializer( - filtered_disciplines, many=True).data + 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 + ) return response.Response(data[:MAXIMUM_RETURNED_DISCIPLINES], status.HTTP_200_OK) diff --git a/api/utils/db_handler.py b/api/utils/db_handler.py index 1779b8c..36ffde0 100644 --- a/api/utils/db_handler.py +++ b/api/utils/db_handler.py @@ -55,6 +55,19 @@ def get_best_similarities_by_name(name: str, disciplines: Discipline = Disciplin return values +def filter_disciplines_by_teacher(name: str) -> QuerySet: + """Filtra as disciplinas pelo nome do professor na classe.""" + disciplines = Discipline.objects.all() + search_words = name.split() + + query = Q() + for word in search_words: + query &= Q(classes__teachers__icontains=word) + search_disciplines = disciplines.filter(query).distinct("id") + + return search_disciplines + + def filter_disciplines_by_code(code: str, disciplines: Discipline = Discipline.objects) -> QuerySet: """Filtra as disciplinas pelo código.""" return disciplines.filter(code__icontains=code) @@ -111,5 +124,15 @@ def delete_schedule(user: User, id: int) -> bool: Schedule.objects.get(user=user, id=id).delete() except Schedule.DoesNotExist: return False - + return True + + +def filter_classes_by_teacher(name: str, classes: QuerySet) -> QuerySet: + """Filtra as turmas pelo nome do professor.""" + search_words = name.split() + query = Q() + + for word in search_words: + query &= Q(teachers__icontains=word) + return classes.filter(query)