From e5415e7ab5cbef53997292eebee60f94de1e8e57 Mon Sep 17 00:00:00 2001 From: Caio Date: Mon, 4 Dec 2023 16:46:45 -0300 Subject: [PATCH 01/20] api(serializers): add serializers for schedule view --- api/api/serializers.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/api/api/serializers.py b/api/api/serializers.py index ef30f00b..b6be530b 100644 --- a/api/api/serializers.py +++ b/api/api/serializers.py @@ -16,4 +16,16 @@ class DisciplineSerializer(ModelSerializer): class Meta: model = Discipline + fields = '__all__' + +class DisciplineSerializerSchedule(ModelSerializer): + class Meta: + model = Discipline + fields = '__all__' + +class ClassSerializerSchedule(ModelSerializer): + discipline = DisciplineSerializerSchedule() + + class Meta: + model = Class fields = '__all__' \ No newline at end of file From 5e9b453c014832f3c680a4f850338d27f933e49f Mon Sep 17 00:00:00 2001 From: Caio Date: Mon, 4 Dec 2023 16:48:22 -0300 Subject: [PATCH 02/20] api(urls): add schedule endpoint --- api/api/urls.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/api/urls.py b/api/api/urls.py index 42e281f2..23e9530a 100644 --- a/api/api/urls.py +++ b/api/api/urls.py @@ -5,5 +5,6 @@ urlpatterns = [ path('', views.Search.as_view(), name="search"), - path('year-period/', views.YearPeriod.as_view(), name="year-period") + path('year-period/', views.YearPeriod.as_view(), name="year-period"), + path('schedule/', views.Schedule.as_view(), name="schedule") ] From b39a3424c3d6c447b18ae1d0e649c0d82f459304 Mon Sep 17 00:00:00 2001 From: Caio Date: Mon, 4 Dec 2023 16:48:37 -0300 Subject: [PATCH 03/20] api(views): create schedule api --- api/api/views.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/api/api/views.py b/api/api/views.py index 2ab4cb10..bab70b9b 100644 --- a/api/api/views.py +++ b/api/api/views.py @@ -10,6 +10,7 @@ from drf_yasg import openapi from .swagger import Errors from api import serializers +from utils.schedule_generator import ScheduleGenerator MAXIMUM_RETURNED_DISCIPLINES = 8 ERROR_MESSAGE = "no valid argument found for 'search', 'year' or 'period'" @@ -123,3 +124,37 @@ def get(self, request: request.Request, *args, **kwargs) -> response.Response: } return response.Response(data, status.HTTP_200_OK) + +class Schedule(APIView): + def post(self, request: request.Request, *args, **kwargs) -> response.Response: + classes_id = request.data.get('classes', None) + preference = request.data.get('preference', None) + preference_valid = preference is not None and isinstance(preference, list) and all(isinstance(x, int) for x in preference) and len(preference) == 3 + + if preference is not None and not preference_valid: + return response.Response( + { + "errors": "preference must be a list of 3 integers" + }, status.HTTP_400_BAD_REQUEST) + + if classes_id is None: + return response.Response( + { + "errors": "classes is required" + }, status.HTTP_400_BAD_REQUEST) + + schedule_generator = ScheduleGenerator(classes_id, preference) + schedules = schedule_generator.generate() + + if schedules is None: + return response.Response( + { + "errors": "classes must be a list of valid classes id." + }, status.HTTP_400_BAD_REQUEST) + + data = [] + + for schedule in schedules: + data.append(list(map(lambda x: serializers.ClassSerializerSchedule(x).data, schedule))) + + return response.Response(data, status.HTTP_200_OK) \ No newline at end of file From 2d153319c2b5c4eb9384b1e6b38ea9b6a9929c8c Mon Sep 17 00:00:00 2001 From: Caio Date: Mon, 4 Dec 2023 16:48:57 -0300 Subject: [PATCH 04/20] utils(db_handler): add get class by id function --- api/utils/db_handler.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/utils/db_handler.py b/api/utils/db_handler.py index a73922f2..c7638bb9 100644 --- a/api/utils/db_handler.py +++ b/api/utils/db_handler.py @@ -50,3 +50,6 @@ def filter_disciplines_by_year_and_period(year: str, period: str, disciplines: D """Filtra as disciplinas pelo ano e período.""" return disciplines.filter(department__year=year, department__period=period) +def get_class_by_id(id: int, classes: Class = Class.objects) -> Class: + """Filtra as turmas pelo id.""" + return classes.get(id=id) From 0ef6348042a0f9d91dc643efb89ddf4d7ec2e35c Mon Sep 17 00:00:00 2001 From: Caio Date: Mon, 4 Dec 2023 16:49:14 -0300 Subject: [PATCH 05/20] utils(schedule_generator): add schedule generator --- api/utils/schedule_generator.py | 117 ++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 api/utils/schedule_generator.py diff --git a/api/utils/schedule_generator.py b/api/utils/schedule_generator.py new file mode 100644 index 00000000..9131f34a --- /dev/null +++ b/api/utils/schedule_generator.py @@ -0,0 +1,117 @@ +from itertools import product +from collections import defaultdict +from .db_handler import get_class_by_id +from re import search +from api.models import Class + +def check(function): + def wrapper(self, *args, **kwargs): + if not self.is_valid(): + return None + return function(self, *args, **kwargs) + + return wrapper + +class ScheduleGenerator: + def __init__(self, classes_id: list[int], preference: [] = [1, 1, 1]): + self.schedule_info = defaultdict(lambda: None) + self.preference = preference + self.generated = False + self._get_and_validate_classes(classes_id=classes_id) + self._make_disciplines_list() + + def _get_and_validate_classes(self, classes_id: list[int]) -> None: + self.disciplines = defaultdict(list) + self.classes = dict() + self.schedules = [] + self.valid = True + + if not len(classes_id): + self.valid = False + return + + for class_id in classes_id: + try: + _class = get_class_by_id(id=class_id) + + self.classes[class_id] = _class + self.disciplines[_class.discipline].append(class_id) + self._add_schedule_code(_class.schedule) + except Class.DoesNotExist: + self.valid = False + return + + def is_valid(self) -> bool: + return self.valid + + def _add_schedule_code(self, schedules: str) -> None: + if self.schedule_info[schedules] is not None: + return + + regex = "[MTN]" + schedules_list = schedules.split() + schedules_dict = { + "priority": 0, + "times": set() + } + + for schedule in schedules_list: + match = search(regex, schedule) + schedule = list(schedule) + days = schedule[:match.start()] + turn = schedule[match.start() + 1:] + letter = match.group() + + values = [days, [letter], turn] + code_product = product(*values) + + # schedules_dict["priority"] += _get_priority(letter) + schedules_dict["times"] = schedules_dict["times"].union(set(code_product)) + + self.schedule_info[schedules] = schedules_dict + + @check + def _make_disciplines_list(self) -> None: + self.disciplines_list = [] + + for classes in self.disciplines.values(): + self.disciplines_list.append(classes) + + def _is_valid_schedule(self, schedule: tuple) -> bool: + codes_counter = 0 + schedule_codes = set() + + for class_id in schedule: + _class = self.classes[class_id] + schedule_code = self.schedule_info[_class.schedule]["times"] + codes_counter += len(schedule_code) + schedule_codes = schedule_codes.union(schedule_code) + + if codes_counter > len(schedule_codes): + return False + + return True + + def _add_schedule(self, schedule: tuple) -> None: + parsed_schedule = [] + + for class_id in schedule: + _class = self.classes[class_id] + parsed_schedule.append(_class) + + self.schedules.append(parsed_schedule) + + @check + def generate(self) -> list | None: + if self.generated: + return self.schedules + + self.generated = True + possible_schedules = product(*self.disciplines_list) + + for schedule in possible_schedules: + if self._is_valid_schedule(schedule): + print("valid", schedule) + self._add_schedule(schedule) + + return self.schedules \ No newline at end of file From e489052776ec17a54b24a66427dc2c43f4d726cf Mon Sep 17 00:00:00 2001 From: GabrielCastelo-31 Date: Mon, 4 Dec 2023 18:58:57 -0300 Subject: [PATCH 06/20] =?UTF-8?q?api(schedule-generator):=20Adiciona=20pri?= =?UTF-8?q?oridade=20de=20turno=20-=20O=20usu=C3=A1rio=20pode=20definir=20?= =?UTF-8?q?a=20prioridade=20para=20turno=20de=20aula=20-=20Lida=20com=20co?= =?UTF-8?q?nflitos=20de=20hor=C3=A1rios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Caio Felipe --- api/api/views.py | 33 +++++++---- api/utils/schedule_generator.py | 101 ++++++++++++++++++++++---------- 2 files changed, 94 insertions(+), 40 deletions(-) diff --git a/api/api/views.py b/api/api/views.py index bab70b9b..915ba7f0 100644 --- a/api/api/views.py +++ b/api/api/views.py @@ -125,36 +125,49 @@ def get(self, request: request.Request, *args, **kwargs) -> response.Response: return response.Response(data, status.HTTP_200_OK) + class Schedule(APIView): def post(self, request: request.Request, *args, **kwargs) -> response.Response: - classes_id = request.data.get('classes', None) - preference = request.data.get('preference', None) - preference_valid = preference is not None and isinstance(preference, list) and all(isinstance(x, int) for x in preference) and len(preference) == 3 - + """ + View para gerar horários. + Funcionamento: Recebe uma lista de ids de classes e uma lista de preferências + e verifica se as classes e preferências são válidas. + Caso sejam válidas, gera os horários e retorna uma lista de horários. + + """ + classes_id = request.data.get('classes', None) # Recebe a lista de ids de classes + preference = request.data.get('preference', None) # Recebe a lista de preferências + preference_valid = preference is not None and isinstance(preference, list) and all( + isinstance(x, int) for x in preference) and len(preference) == 3 + # Verifica se a preferência é uma lista de 3 inteiros + if preference is not None and not preference_valid: + """Retorna um erro caso a preferência não seja uma lista de 3 inteiros""" return response.Response( { "errors": "preference must be a list of 3 integers" }, status.HTTP_400_BAD_REQUEST) if classes_id is None: + """Retorna um erro caso a lista de ids de classes não seja enviada""" return response.Response( { "errors": "classes is required" }, status.HTTP_400_BAD_REQUEST) - schedule_generator = ScheduleGenerator(classes_id, preference) - schedules = schedule_generator.generate() + schedule_generator = ScheduleGenerator(classes_id, preference) # Cria um objeto ScheduleGenerator + schedules = schedule_generator.generate() # Gera os horários if schedules is None: return response.Response( { "errors": "classes must be a list of valid classes id." }, status.HTTP_400_BAD_REQUEST) - + data = [] - + for schedule in schedules: - data.append(list(map(lambda x: serializers.ClassSerializerSchedule(x).data, schedule))) + data.append( + list(map(lambda x: serializers.ClassSerializerSchedule(x).data, schedule))) - return response.Response(data, status.HTTP_200_OK) \ No newline at end of file + return response.Response(data, status.HTTP_200_OK) diff --git a/api/utils/schedule_generator.py b/api/utils/schedule_generator.py index 9131f34a..b8eb7ca2 100644 --- a/api/utils/schedule_generator.py +++ b/api/utils/schedule_generator.py @@ -4,114 +4,155 @@ from re import search from api.models import Class + def check(function): + """ + Decorator para verificar se a classe é válida antes de executar a função + """ + def wrapper(self, *args, **kwargs): if not self.is_valid(): return None return function(self, *args, **kwargs) - + return wrapper -class ScheduleGenerator: - def __init__(self, classes_id: list[int], preference: [] = [1, 1, 1]): + +class ScheduleGenerator: + """Classe que representa um gerador de horários. + Atributos: + + """ + + def __init__(self, classes_id: list[int], preference: list = None): + """Construtor da classe ScheduleGenerator + """ self.schedule_info = defaultdict(lambda: None) self.preference = preference self.generated = False self._get_and_validate_classes(classes_id=classes_id) self._make_disciplines_list() - + def _get_and_validate_classes(self, classes_id: list[int]) -> None: self.disciplines = defaultdict(list) self.classes = dict() self.schedules = [] self.valid = True - + if not len(classes_id): self.valid = False return - + for class_id in classes_id: try: _class = get_class_by_id(id=class_id) - + self.classes[class_id] = _class self.disciplines[_class.discipline].append(class_id) self._add_schedule_code(_class.schedule) except Class.DoesNotExist: self.valid = False return - + def is_valid(self) -> bool: return self.valid - + + # [3,2,1] + def get_priority(self, days: list, turn: list[str], letter: str): + """ + Calcula a prioridade de uma disciplina ser escolhida para a grade de horários. + Quanto mais cedo for o horário, maior será a prioridade, independemente do turno. + Quanto mais aulas na semana a disciplina tiver, maior será a prioridade. + """ + letters = "MTN" + turn_priority = 5 * (len(turn)) - sum(map(int, turn)) + days_quantity = len(days) + + priority = (self.preference[letters.find( + letter)]) * (turn_priority + days_quantity) + return priority + def _add_schedule_code(self, schedules: str) -> None: if self.schedule_info[schedules] is not None: return - + regex = "[MTN]" schedules_list = schedules.split() schedules_dict = { "priority": 0, "times": set() } - + for schedule in schedules_list: match = search(regex, schedule) schedule = list(schedule) days = schedule[:match.start()] turn = schedule[match.start() + 1:] letter = match.group() - + values = [days, [letter], turn] code_product = product(*values) - - # schedules_dict["priority"] += _get_priority(letter) - schedules_dict["times"] = schedules_dict["times"].union(set(code_product)) - + if self.preference is not None: + schedules_dict["priority"] += self.get_priority( + days, turn, letter) + + schedules_dict["times"] = schedules_dict["times"].union( + set(code_product)) + self.schedule_info[schedules] = schedules_dict - + @check def _make_disciplines_list(self) -> None: self.disciplines_list = [] - + for classes in self.disciplines.values(): self.disciplines_list.append(classes) - + def _is_valid_schedule(self, schedule: tuple) -> bool: codes_counter = 0 schedule_codes = set() - + for class_id in schedule: _class = self.classes[class_id] schedule_code = self.schedule_info[_class.schedule]["times"] + """Variável que contém o produto cartesiano dos horários""" codes_counter += len(schedule_code) + """Contador do tamanho do conjunto de produtos cartesianos""" schedule_codes = schedule_codes.union(schedule_code) - + """Faz a união dos produtos cartesianos com o horário pretendido""" + if codes_counter > len(schedule_codes): + """Caso o contador seja maior do que a união dos produtos cartesianos com o horário pretendido, + o horário não é válido, pois há horários que se sobrepõem""" return False - + return True - + def _add_schedule(self, schedule: tuple) -> None: parsed_schedule = [] - + for class_id in schedule: _class = self.classes[class_id] parsed_schedule.append(_class) - + self.schedules.append(parsed_schedule) - + @check def generate(self) -> list | None: if self.generated: return self.schedules - + self.generated = True possible_schedules = product(*self.disciplines_list) - + for schedule in possible_schedules: if self._is_valid_schedule(schedule): print("valid", schedule) self._add_schedule(schedule) - - return self.schedules \ No newline at end of file + + return self.sort_by_priority() + + def sort_by_priority(self): + self.schedules.sort(key=lambda priority: sum(map( + lambda _class: self.schedule_info[_class.schedule]["priority"], priority)), reverse=True) + return self.schedules From b069bd3dbecd926bfc12e50400925b4acfd14f48 Mon Sep 17 00:00:00 2001 From: Caio Date: Mon, 4 Dec 2023 19:38:18 -0300 Subject: [PATCH 07/20] api(): turn functions more readable --- api/api/views.py | 24 ++++++++++++++++++------ api/utils/schedule_generator.py | 31 +++++++++++++++---------------- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/api/api/views.py b/api/api/views.py index 915ba7f0..8389702b 100644 --- a/api/api/views.py +++ b/api/api/views.py @@ -127,19 +127,31 @@ def get(self, request: request.Request, *args, **kwargs) -> response.Response: class Schedule(APIView): + @swagger_auto_schema( + operation_description="Gera uma lista de horários válidos para as turmas enviadas.", + manual_parameters=[ + openapi.Parameter('classes', openapi.IN_QUERY, + description="Lista de ids de turmas", type=openapi.TYPE_ARRAY, items=openapi.Items(type=openapi.TYPE_INTEGER)), + openapi.Parameter('preference', openapi.IN_QUERY, + description="Lista de preferências (manhã/tarde/noite)", type=openapi.TYPE_ARRAY, items=openapi.Items(type=openapi.TYPE_INTEGER)), + ], + responses={ + 200: openapi.Response('OK', serializers.ClassSerializerSchedule), + **Errors([400]).retrieve_erros(), + } + ) def post(self, request: request.Request, *args, **kwargs) -> response.Response: """ View para gerar horários. Funcionamento: Recebe uma lista de ids de classes e uma lista de preferências e verifica se as classes e preferências são válidas. Caso sejam válidas, gera os horários e retorna uma lista de horários. - """ - classes_id = request.data.get('classes', None) # Recebe a lista de ids de classes - preference = request.data.get('preference', None) # Recebe a lista de preferências + + classes_id = request.data.get('classes', None) + preference = request.data.get('preference', None) preference_valid = preference is not None and isinstance(preference, list) and all( isinstance(x, int) for x in preference) and len(preference) == 3 - # Verifica se a preferência é uma lista de 3 inteiros if preference is not None and not preference_valid: """Retorna um erro caso a preferência não seja uma lista de 3 inteiros""" @@ -155,8 +167,8 @@ def post(self, request: request.Request, *args, **kwargs) -> response.Response: "errors": "classes is required" }, status.HTTP_400_BAD_REQUEST) - schedule_generator = ScheduleGenerator(classes_id, preference) # Cria um objeto ScheduleGenerator - schedules = schedule_generator.generate() # Gera os horários + schedule_generator = ScheduleGenerator(classes_id, preference) + schedules = schedule_generator.generate() if schedules is None: return response.Response( diff --git a/api/utils/schedule_generator.py b/api/utils/schedule_generator.py index b8eb7ca2..29eac1ef 100644 --- a/api/utils/schedule_generator.py +++ b/api/utils/schedule_generator.py @@ -19,14 +19,10 @@ def wrapper(self, *args, **kwargs): class ScheduleGenerator: - """Classe que representa um gerador de horários. - Atributos: - - """ + """Classe que representa um gerador de horários.""" + available_letters = "MTN" def __init__(self, classes_id: list[int], preference: list = None): - """Construtor da classe ScheduleGenerator - """ self.schedule_info = defaultdict(lambda: None) self.preference = preference self.generated = False @@ -57,27 +53,29 @@ def _get_and_validate_classes(self, classes_id: list[int]) -> None: def is_valid(self) -> bool: return self.valid - # [3,2,1] - def get_priority(self, days: list, turn: list[str], letter: str): + def _get_priority(self, days: list, turn: list[str], letter: str): """ Calcula a prioridade de uma disciplina ser escolhida para a grade de horários. Quanto mais cedo for o horário, maior será a prioridade, independemente do turno. Quanto mais aulas na semana a disciplina tiver, maior será a prioridade. """ - letters = "MTN" turn_priority = 5 * (len(turn)) - sum(map(int, turn)) days_quantity = len(days) - priority = (self.preference[letters.find( - letter)]) * (turn_priority + days_quantity) + priority = self.preference[self.available_letters.find( + letter)] * (turn_priority + days_quantity) + return priority def _add_schedule_code(self, schedules: str) -> None: if self.schedule_info[schedules] is not None: return - regex = "[MTN]" + regex = f"[{self.available_letters}]" schedules_list = schedules.split() + + """Cria um dicionário com a prioridade e os horários + em produto cartesianode uma disciplina""" schedules_dict = { "priority": 0, "times": set() @@ -86,14 +84,16 @@ def _add_schedule_code(self, schedules: str) -> None: for schedule in schedules_list: match = search(regex, schedule) schedule = list(schedule) + days = schedule[:match.start()] turn = schedule[match.start() + 1:] letter = match.group() values = [days, [letter], turn] code_product = product(*values) + if self.preference is not None: - schedules_dict["priority"] += self.get_priority( + schedules_dict["priority"] += self._get_priority( days, turn, letter) schedules_dict["times"] = schedules_dict["times"].union( @@ -114,12 +114,10 @@ def _is_valid_schedule(self, schedule: tuple) -> bool: for class_id in schedule: _class = self.classes[class_id] + schedule_code = self.schedule_info[_class.schedule]["times"] - """Variável que contém o produto cartesiano dos horários""" codes_counter += len(schedule_code) - """Contador do tamanho do conjunto de produtos cartesianos""" schedule_codes = schedule_codes.union(schedule_code) - """Faz a união dos produtos cartesianos com o horário pretendido""" if codes_counter > len(schedule_codes): """Caso o contador seja maior do que a união dos produtos cartesianos com o horário pretendido, @@ -155,4 +153,5 @@ def generate(self) -> list | None: def sort_by_priority(self): self.schedules.sort(key=lambda priority: sum(map( lambda _class: self.schedule_info[_class.schedule]["priority"], priority)), reverse=True) + return self.schedules From 32de665363d0fd646e29abe3a4f2bc5fbf7b8b83 Mon Sep 17 00:00:00 2001 From: Caio Date: Mon, 4 Dec 2023 22:38:15 -0300 Subject: [PATCH 08/20] utils(schedule-generator): adiciona msg erros --- api/api/views.py | 15 ++++++++------- api/utils/schedule_generator.py | 23 +++++++++++++++++++---- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/api/api/views.py b/api/api/views.py index 8389702b..48432f34 100644 --- a/api/api/views.py +++ b/api/api/views.py @@ -16,7 +16,7 @@ ERROR_MESSAGE = "no valid argument found for 'search', 'year' or 'period'" MINIMUM_SEARCH_LENGTH = 4 ERROR_MESSAGE_SEARCH_LENGTH = f"search must have at least {MINIMUM_SEARCH_LENGTH} characters" - +MAXIMUM_RETURNED_SCHEDULES = 5 class Search(APIView): @@ -167,18 +167,19 @@ def post(self, request: request.Request, *args, **kwargs) -> response.Response: "errors": "classes is required" }, status.HTTP_400_BAD_REQUEST) - schedule_generator = ScheduleGenerator(classes_id, preference) - schedules = schedule_generator.generate() - - if schedules is None: + try: + schedule_generator = ScheduleGenerator(classes_id, preference) + schedules = schedule_generator.generate() + except Exception as error: + """Retorna um erro caso ocorra algum erro ao criar o gerador de horários""" return response.Response( { - "errors": "classes must be a list of valid classes id." + "errors": str(error) }, status.HTTP_400_BAD_REQUEST) data = [] - for schedule in schedules: + for schedule in schedules[:MAXIMUM_RETURNED_SCHEDULES]: data.append( list(map(lambda x: serializers.ClassSerializerSchedule(x).data, schedule))) diff --git a/api/utils/schedule_generator.py b/api/utils/schedule_generator.py index 29eac1ef..7e5800cd 100644 --- a/api/utils/schedule_generator.py +++ b/api/utils/schedule_generator.py @@ -4,6 +4,8 @@ from re import search from api.models import Class +MAXIMUM_DISCIPLINES = 11 +MAXIMUM_CLASSES_FOR_DISCIPLINE = 4 def check(function): """ @@ -26,10 +28,11 @@ def __init__(self, classes_id: list[int], preference: list = None): self.schedule_info = defaultdict(lambda: None) self.preference = preference self.generated = False - self._get_and_validate_classes(classes_id=classes_id) + self._get_and_validate_classes(classes_id=set(classes_id)) self._make_disciplines_list() + self._validate_parameters_length() - def _get_and_validate_classes(self, classes_id: list[int]) -> None: + def _get_and_validate_classes(self, classes_id: set[int]) -> None: self.disciplines = defaultdict(list) self.classes = dict() self.schedules = [] @@ -48,7 +51,20 @@ def _get_and_validate_classes(self, classes_id: list[int]) -> None: self._add_schedule_code(_class.schedule) except Class.DoesNotExist: self.valid = False - return + raise ValueError(f"class with id {class_id} does not exist.") + + @check + def _validate_parameters_length(self) -> None: + if len(self.disciplines) > MAXIMUM_DISCIPLINES: + self.valid = False + + for classes in self.disciplines.values(): + if len(classes) > MAXIMUM_CLASSES_FOR_DISCIPLINE: + self.valid = False + break + + if not self.valid: + raise ValueError(f"you can only send {MAXIMUM_DISCIPLINES} disciplines and {MAXIMUM_CLASSES_FOR_DISCIPLINE} classes for each discipline.") def is_valid(self) -> bool: return self.valid @@ -145,7 +161,6 @@ def generate(self) -> list | None: for schedule in possible_schedules: if self._is_valid_schedule(schedule): - print("valid", schedule) self._add_schedule(schedule) return self.sort_by_priority() From a3e6dbcbc4883725b3cc725e1c288facac139c5d Mon Sep 17 00:00:00 2001 From: Caio Date: Tue, 5 Dec 2023 10:11:01 -0300 Subject: [PATCH 09/20] =?UTF-8?q?api(views):=20arruma=20erro=20de=20verifi?= =?UTF-8?q?ca=C3=A7=C3=A3o=20de=20aulas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A verificação de validade de aulas era apenas para caso a lista não estivesse vazia, mas não verificava se a lista estava vazia e se era uma lista de inteiros. --- api/api/views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/api/views.py b/api/api/views.py index 48432f34..d5ee81b7 100644 --- a/api/api/views.py +++ b/api/api/views.py @@ -152,6 +152,8 @@ def post(self, request: request.Request, *args, **kwargs) -> response.Response: preference = request.data.get('preference', None) preference_valid = preference is not None and isinstance(preference, list) and all( isinstance(x, int) for x in preference) and len(preference) == 3 + classes_valid = classes_id is not None and isinstance( + classes_id, list) and all(isinstance(x, int) for x in classes_id) and len(classes_id) > 0 if preference is not None and not preference_valid: """Retorna um erro caso a preferência não seja uma lista de 3 inteiros""" @@ -160,11 +162,11 @@ def post(self, request: request.Request, *args, **kwargs) -> response.Response: "errors": "preference must be a list of 3 integers" }, status.HTTP_400_BAD_REQUEST) - if classes_id is None: + if not classes_valid: """Retorna um erro caso a lista de ids de classes não seja enviada""" return response.Response( { - "errors": "classes is required" + "errors": "classes is required and must be a list of integers with at least one element" }, status.HTTP_400_BAD_REQUEST) try: From 039264005defd15ca3a9ce015bd39e518e0f39ec Mon Sep 17 00:00:00 2001 From: Caio Date: Tue, 5 Dec 2023 10:12:37 -0300 Subject: [PATCH 10/20] test(schedule-view): add test for schedule view --- api/api/tests/test_schedule_api.py | 124 +++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 api/api/tests/test_schedule_api.py diff --git a/api/api/tests/test_schedule_api.py b/api/api/tests/test_schedule_api.py new file mode 100644 index 00000000..32b0f456 --- /dev/null +++ b/api/api/tests/test_schedule_api.py @@ -0,0 +1,124 @@ +from rest_framework.test import APITestCase, APIRequestFactory +from utils.db_handler import get_or_create_department, get_or_create_discipline, create_class +from random import randint +import json + +class TestScheduleAPI(APITestCase): + def setUp(self): + self.factory = APIRequestFactory() + self.content_type = 'application/json' + self.api_url = '/courses/schedule/' + self.department = get_or_create_department( + code='518', year='2023', period='2') + self.discipline = get_or_create_discipline( + name='CÁLCULO 1', code='MAT518', department=self.department) + self.class_1 = create_class(teachers=['RICARDO FRAGELLI'], classroom='S9', schedule='46M34', days=[ + 'Quarta-Feira 10:00 às 11:50', 'Sexta-Feira 10:00 às 11:50'], _class="1", special_dates=[], discipline=self.discipline) + 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="2", special_dates=[], discipline=self.discipline) + self.discipline_2 = get_or_create_discipline( + name='CÁLCULO 2', code='MAT519', department=self.department) + self.class_3 = create_class(teachers=['LUIZA YOKO'], classroom='S1', schedule='56M23', 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_4 = create_class(teachers=['Tatiana'], classroom='S1', schedule='7M1234', days=[ + 'Sábado 08:00 às 11:50'], _class="2", special_dates=[], discipline=self.discipline_2) + + def test_with_correct_parameters(self): + """ + Testa a geração de horários com todos os parâmetros corretos + Os parâmetros enviados permitem que pelo menos uma solução seja encontrada + """ + body = json.dumps({ + 'preference': [3, 2, 1], + 'classes': [self.class_1.id, self.class_2.id, self.class_3.id, self.class_4.id] + }) + + response = self.client.post(self.api_url, body, content_type=self.content_type) + + self.assertEqual(response.status_code, 200) + self.assertTrue(len(response.data) > 0) + + def test_with_conflicting_classes(self): + """ + Testa a geração de horários com classes conflitantes + """ + body = json.dumps({ + 'preference': [3, 2, 1], + 'classes': [self.class_1.id, self.class_3.id] + }) + + response = self.client.post(self.api_url, body, content_type=self.content_type) + + self.assertEqual(response.status_code, 200) + self.assertFalse(len(response.data)) + + def test_with_invalid_class(self): + """ + Testa a geração de horários com uma classe inválida + """ + + classes_ids = [self.class_1.id, self.class_2.id, self.class_3.id, self.class_4.id] + random_id = randint(1, 10000) + + while(random_id in classes_ids): + random_id = randint(1, 10000) + + body = json.dumps({ + 'preference': [3, 2, 1], + 'classes': classes_ids + [random_id] + }) + + response = self.client.post(self.api_url, body, content_type=self.content_type) + + self.assertEqual(response.status_code, 400) + + def test_with_invalid_preference(self): + """ + Testa a geração de horários com uma preferência inválida + """ + body = json.dumps({ + 'preference': [3, 2, 1, 4], + 'classes': [self.class_1.id, self.class_2.id, self.class_3.id, self.class_4.id] + }) + + response = self.client.post(self.api_url, body, content_type=self.content_type) + + self.assertEqual(response.status_code, 400) + + def test_with_invalid_preference_type(self): + """ + Testa a geração de horários com uma preferência inválida (tipo) + """ + body = json.dumps({ + 'preference': [3, 2, '1'], + 'classes': [self.class_1.id, self.class_2.id, self.class_3.id, self.class_4.id] + }) + + response = self.client.post(self.api_url, body, content_type=self.content_type) + + self.assertEqual(response.status_code, 400) + + def test_with_no_classes(self): + """ + Testa a geração de horários sem classes + """ + body = json.dumps({ + 'classes': [] + }) + + response = self.client.post(self.api_url, body, content_type=self.content_type) + + self.assertEqual(response.status_code, 400) + + def test_with_no_preference(self): + """ + Testa a geração de horários sem preferência + """ + body = json.dumps({ + 'classes': [self.class_1.id] + }) + + response = self.client.post(self.api_url, body, content_type=self.content_type) + + self.assertEqual(response.status_code, 200) + self.assertTrue(len(response.data) > 0) \ No newline at end of file From 378031209815743b117f8a6afc07d405ea930291 Mon Sep 17 00:00:00 2001 From: Caio Date: Tue, 5 Dec 2023 10:15:45 -0300 Subject: [PATCH 11/20] =?UTF-8?q?test(schedule-api):=20ignora=20o=20teste?= =?UTF-8?q?=20n=C3=A3o-test=C3=A1vel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/api/tests/test_schedule_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/api/tests/test_schedule_api.py b/api/api/tests/test_schedule_api.py index 32b0f456..a2517c38 100644 --- a/api/api/tests/test_schedule_api.py +++ b/api/api/tests/test_schedule_api.py @@ -60,7 +60,7 @@ def test_with_invalid_class(self): classes_ids = [self.class_1.id, self.class_2.id, self.class_3.id, self.class_4.id] random_id = randint(1, 10000) - while(random_id in classes_ids): + while(random_id in classes_ids): # pragma: no cover random_id = randint(1, 10000) body = json.dumps({ From 7e1644c485a269e90228f8bb18015c358103bf97 Mon Sep 17 00:00:00 2001 From: Caio Date: Tue, 5 Dec 2023 11:00:39 -0300 Subject: [PATCH 12/20] test(db_handler): add teste para o get_class_by_id --- api/utils/tests/test_database_handler.py | 31 +++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/api/utils/tests/test_database_handler.py b/api/utils/tests/test_database_handler.py index ab59f062..189a02ca 100644 --- a/api/utils/tests/test_database_handler.py +++ b/api/utils/tests/test_database_handler.py @@ -178,4 +178,33 @@ def test_get_best_similarities_by_name(self): self.assertTrue(disciplines.count() == 3) self.assertTrue(discipline in disciplines) self.assertTrue(discipline_2 in disciplines) - self.assertTrue(discipline_3 in disciplines) \ No newline at end of file + self.assertTrue(discipline_3 in disciplines) + + def test_get_class_by_id(self): + department = dbh.get_or_create_department( + code = 'MAT', + year = '2027', + period = '1' + ) + + discipline = dbh.get_or_create_discipline( + name = 'Cálculo 1', + code = 'MAT0026', + department = department + ) + + _class = dbh.create_class( + teachers = ['Luiza Yoko'], + classroom = 'S9', + schedule = '46M34', + days = ['Quarta-Feira 10:00 às 11:50', 'Sexta-Feira 10:00 às 11:50'], + _class = "1", + special_dates=[], + discipline = discipline + ) + + class_from_db = dbh.get_class_by_id( + id = _class.id + ) + + self.assertTrue(class_from_db == _class) \ No newline at end of file From 499ef4f0ca2c6f2aaea0d4a4845279a2551225f2 Mon Sep 17 00:00:00 2001 From: Caio Date: Tue, 5 Dec 2023 11:01:38 -0300 Subject: [PATCH 13/20] utils(generator): coloca o erro de forma global --- api/utils/schedule_generator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/utils/schedule_generator.py b/api/utils/schedule_generator.py index 7e5800cd..2c944f5f 100644 --- a/api/utils/schedule_generator.py +++ b/api/utils/schedule_generator.py @@ -6,6 +6,7 @@ MAXIMUM_DISCIPLINES = 11 MAXIMUM_CLASSES_FOR_DISCIPLINE = 4 +LIMIT_ERROR_MESSAGE = f"you can only send {MAXIMUM_DISCIPLINES} disciplines and {MAXIMUM_CLASSES_FOR_DISCIPLINE} classes for each discipline." def check(function): """ @@ -55,7 +56,7 @@ def _get_and_validate_classes(self, classes_id: set[int]) -> None: @check def _validate_parameters_length(self) -> None: - if len(self.disciplines) > MAXIMUM_DISCIPLINES: + if len(self.disciplines) > MAXIMUM_DISCIPLINES: # pragma: no cover self.valid = False for classes in self.disciplines.values(): @@ -64,7 +65,7 @@ def _validate_parameters_length(self) -> None: break if not self.valid: - raise ValueError(f"you can only send {MAXIMUM_DISCIPLINES} disciplines and {MAXIMUM_CLASSES_FOR_DISCIPLINE} classes for each discipline.") + raise ValueError(LIMIT_ERROR_MESSAGE) def is_valid(self) -> bool: return self.valid From 2b78632d528cc1ea209f60bc01ae22b5d2fe8274 Mon Sep 17 00:00:00 2001 From: Caio Date: Tue, 5 Dec 2023 11:02:20 -0300 Subject: [PATCH 14/20] test(schedule_generator): adiciona testes --- api/utils/tests/test_schedule_generator.py | 147 +++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 api/utils/tests/test_schedule_generator.py diff --git a/api/utils/tests/test_schedule_generator.py b/api/utils/tests/test_schedule_generator.py new file mode 100644 index 00000000..f8df63f7 --- /dev/null +++ b/api/utils/tests/test_schedule_generator.py @@ -0,0 +1,147 @@ +from rest_framework.test import APITestCase +from utils import db_handler as dbh +from utils.schedule_generator import ScheduleGenerator, LIMIT_ERROR_MESSAGE +from random import randint + +class TestSchedule(APITestCase): + def setUp(self): + self.department = dbh.get_or_create_department( + code = 'CIC', + year = '2030', + period = '2' + ) + self.discipline_1 = dbh.get_or_create_discipline( + name = 'Programação Competitiva', + code = 'CIC1234', + department = self.department + ) + self.class_1 = dbh.create_class( + teachers = ['Edson Alves'], + classroom = 'I6', + schedule = '46T34', + days=['Quarta-Feira 16:00 às 17:50', 'Sexta-Feira 16:00 às 17:50'], + _class = "1", + special_dates= [], + discipline = self.discipline_1 + ) + self.class_2 = dbh.create_class( + teachers = ['Edson Alves'], + classroom = 'MOCAP', + schedule = '35T12', + days = ['Terça-Feira 14:00 às 15:50', 'Quinta-Feira 14:00 às 15:50'], + _class = "2", + special_dates = [], + discipline = self.discipline_1 + ) + self.class_3 = dbh.create_class( + teachers = ['VINICIUS RUELA'], + classroom = 'PJC', + schedule = '24T45', + days = ['Segunda-Feira 16:00 às 17:50', 'Quarta-Feira 16:00 às 17:50'], + _class = "3", + special_dates=[], + discipline = self.discipline_1 + ) + self.class_4 = dbh.create_class( + teachers = ['VINICIUS RUELA'], + classroom = 'PJC', + schedule = '35T12', + days=['Terça-Feira 14:00 às 15:50', 'Quinta-Feira 14:00 às 15:50'], + _class = "4", + special_dates=[], + discipline = self.discipline_1 + ) + self.class_5 = dbh.create_class( + teachers = ["A definir"], + classroom = 'A definir', + schedule = '7M1234', + days=['Sábado 08:00 às 11:50'], + _class = "5", + special_dates=[], + discipline = self.discipline_1 + ) + self.discipline_2 = dbh.get_or_create_discipline( + name = 'Estrutura de Dados', + code = 'CIC1000', + department = self.department + ) + self.class_6 = dbh.create_class( + teachers = ['Fabiana'], + classroom = 'MOCAP', + schedule = '35T12', + days=['Terça-Feira 14:00 às 15:50', 'Quinta-Feira 14:00 às 15:50'], + _class = "1", + special_dates=[], + discipline = self.discipline_2 + ) + + def test_with_correct_parameters(self): + """ + Testa a geração de horários com todos os parâmetros corretos + Os parâmetros enviados permitem que pelo menos uma solução seja encontrada + """ + + schedule_generator = ScheduleGenerator(classes_id=[self.class_1.id, self.class_2.id, self.class_3.id, self.class_4.id], preference=[3, 2, 1]) + schedules = schedule_generator.generate() + + self.assertEqual(len(schedules), 4) + + def test_with_higher_classes_limit(self): + """ + Testa a geração de horários com um limite de classes maior que o número de classes permitidas para uma matéria + """ + + try: + schedule_generator = ScheduleGenerator(classes_id=[self.class_1.id, self.class_2.id, self.class_3.id, self.class_4.id, self.class_5.id], preference=[3, 2, 1]) + except Exception as error: + self.assertEqual(str(error), LIMIT_ERROR_MESSAGE) + + def test_with_conflicting_classes(self): + """ + Testa a geração de horários com classes conflitantes + """ + + schedule_generator = ScheduleGenerator(classes_id=[self.class_4.id, self.class_6.id]) + schedules = schedule_generator.generate() + + self.assertFalse(len(schedules)) + + def test_with_empty_classes(self): + """ + Testa a geração de horários com uma lista de classes vazia + """ + + schedule_generator = ScheduleGenerator(classes_id=[]) + schedules = schedule_generator.generate() + + self.assertIsNone(schedules) + + def test_with_invalid_class(self): + """ + Testa a geração de horários com uma classe inválida + """ + + classes_ids = [self.class_1.id, self.class_2.id, self.class_3.id, self.class_4.id] + random_id = randint(1, 10000) + + while(random_id in classes_ids): # pragma: no cover + random_id = randint(1, 10000) + + try: + schedule_generator = ScheduleGenerator(classes_id=classes_ids + [random_id]) + except Exception as error: + self.assertEqual(str(error), f"class with id {random_id} does not exist.") + + def test_make_twice(self): + """ + Testa a geração de horários duas vezes + """ + + schedule_generator = ScheduleGenerator(classes_id=[self.class_1.id, self.class_2.id, self.class_3.id, self.class_4.id], preference=[3, 2, 1]) + + schedules = schedule_generator.generate() + self.assertEqual(len(schedules), 4) + + schedules = schedule_generator.generate() + self.assertEqual(len(schedules), 4) + \ No newline at end of file From 792021d891899d915c0bc5ae86e162c2a8a704be Mon Sep 17 00:00:00 2001 From: GabrielCastelo-31 Date: Tue, 5 Dec 2023 22:47:08 -0300 Subject: [PATCH 15/20] api(schedule): fix repeated code in serializer - Added inheritance to avoid repeated code --- api/api/serializers.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/api/api/serializers.py b/api/api/serializers.py index b6be530b..9a4ca97c 100644 --- a/api/api/serializers.py +++ b/api/api/serializers.py @@ -1,31 +1,29 @@ from rest_framework.serializers import ModelSerializer from api.models import Department, Discipline, Class + class DepartmentSerializer(ModelSerializer): class Meta: model = Department fields = '__all__' + + class ClassSerializer(ModelSerializer): class Meta: model = Class fields = '__all__' -class DisciplineSerializer(ModelSerializer): - department = DepartmentSerializer() - classes = ClassSerializer(many=True) - - class Meta: - model = Discipline - fields = '__all__' class DisciplineSerializerSchedule(ModelSerializer): class Meta: model = Discipline fields = '__all__' -class ClassSerializerSchedule(ModelSerializer): + +class DisciplineSerializer(DisciplineSerializerSchedule): + department = DepartmentSerializer() + classes = ClassSerializer(many=True) + + +class ClassSerializerSchedule(ClassSerializer): discipline = DisciplineSerializerSchedule() - - class Meta: - model = Class - fields = '__all__' \ No newline at end of file From f7dd13249cc08f335dfc4a3285ec4785ab02a6e8 Mon Sep 17 00:00:00 2001 From: GabrielCastelo-31 Date: Tue, 5 Dec 2023 23:16:54 -0300 Subject: [PATCH 16/20] fix misstyped word --- api/utils/schedule_generator.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/api/utils/schedule_generator.py b/api/utils/schedule_generator.py index 2c944f5f..fe9644e1 100644 --- a/api/utils/schedule_generator.py +++ b/api/utils/schedule_generator.py @@ -8,6 +8,7 @@ MAXIMUM_CLASSES_FOR_DISCIPLINE = 4 LIMIT_ERROR_MESSAGE = f"you can only send {MAXIMUM_DISCIPLINES} disciplines and {MAXIMUM_CLASSES_FOR_DISCIPLINE} classes for each discipline." + def check(function): """ Decorator para verificar se a classe é válida antes de executar a função @@ -53,17 +54,17 @@ def _get_and_validate_classes(self, classes_id: set[int]) -> None: except Class.DoesNotExist: self.valid = False raise ValueError(f"class with id {class_id} does not exist.") - + @check def _validate_parameters_length(self) -> None: - if len(self.disciplines) > MAXIMUM_DISCIPLINES: # pragma: no cover + if len(self.disciplines) > MAXIMUM_DISCIPLINES: # pragma: no cover self.valid = False for classes in self.disciplines.values(): if len(classes) > MAXIMUM_CLASSES_FOR_DISCIPLINE: self.valid = False break - + if not self.valid: raise ValueError(LIMIT_ERROR_MESSAGE) @@ -73,7 +74,7 @@ def is_valid(self) -> bool: def _get_priority(self, days: list, turn: list[str], letter: str): """ Calcula a prioridade de uma disciplina ser escolhida para a grade de horários. - Quanto mais cedo for o horário, maior será a prioridade, independemente do turno. + Quanto mais cedo for o horário, maior será a prioridade, independentemente do turno. Quanto mais aulas na semana a disciplina tiver, maior será a prioridade. """ turn_priority = 5 * (len(turn)) - sum(map(int, turn)) From 7e62be242e50cf577db2d0e22ecded01ef42cf62 Mon Sep 17 00:00:00 2001 From: GabrielCastelo-31 Date: Tue, 5 Dec 2023 23:19:44 -0300 Subject: [PATCH 17/20] fix mistyped word --- api/utils/schedule_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/utils/schedule_generator.py b/api/utils/schedule_generator.py index fe9644e1..77f7d8e6 100644 --- a/api/utils/schedule_generator.py +++ b/api/utils/schedule_generator.py @@ -93,7 +93,7 @@ def _add_schedule_code(self, schedules: str) -> None: schedules_list = schedules.split() """Cria um dicionário com a prioridade e os horários - em produto cartesianode uma disciplina""" + em produto cartesiano de uma disciplina""" schedules_dict = { "priority": 0, "times": set() From 0fc9e7bf773ee4b42e0e09b83d0cdd172c93cdab Mon Sep 17 00:00:00 2001 From: Caio Date: Wed, 6 Dec 2023 10:49:50 -0300 Subject: [PATCH 18/20] test(schedule_generator): add test for range error --- api/utils/tests/test_schedule_generator.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/api/utils/tests/test_schedule_generator.py b/api/utils/tests/test_schedule_generator.py index f8df63f7..cba459ca 100644 --- a/api/utils/tests/test_schedule_generator.py +++ b/api/utils/tests/test_schedule_generator.py @@ -1,6 +1,6 @@ from rest_framework.test import APITestCase from utils import db_handler as dbh -from utils.schedule_generator import ScheduleGenerator, LIMIT_ERROR_MESSAGE +from utils.schedule_generator import ScheduleGenerator, LIMIT_ERROR_MESSAGE, PREFERENCE_RANGE_ERROR from random import randint class TestSchedule(APITestCase): @@ -144,4 +144,13 @@ def test_make_twice(self): schedules = schedule_generator.generate() self.assertEqual(len(schedules), 4) - \ No newline at end of file + + def test_with_invalid_preference(self): + """ + Testa a geração de horários com preferência inválida + """ + + try: + schedule_generator = ScheduleGenerator(classes_id=[self.class_1.id], preference=[1, 2, '3']) + except Exception as error: + self.assertEqual(str(error), PREFERENCE_RANGE_ERROR) \ No newline at end of file From 52efcb51640661e6b65b8949c9d38e985282dd4e Mon Sep 17 00:00:00 2001 From: Caio Date: Wed, 6 Dec 2023 10:51:02 -0300 Subject: [PATCH 19/20] utils(schedule_generator): add pref range verification --- api/utils/schedule_generator.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/api/utils/schedule_generator.py b/api/utils/schedule_generator.py index 77f7d8e6..eafc14d4 100644 --- a/api/utils/schedule_generator.py +++ b/api/utils/schedule_generator.py @@ -4,10 +4,13 @@ from re import search from api.models import Class -MAXIMUM_DISCIPLINES = 11 MAXIMUM_CLASSES_FOR_DISCIPLINE = 4 -LIMIT_ERROR_MESSAGE = f"you can only send {MAXIMUM_DISCIPLINES} disciplines and {MAXIMUM_CLASSES_FOR_DISCIPLINE} classes for each discipline." +MINIMUM_PREFERENCE_RANGE = 1 +MAXIMUM_PREFERENCE_RANGE = 3 +MAXIMUM_DISCIPLINES = 11 +LIMIT_ERROR_MESSAGE = f"you can only send {MAXIMUM_DISCIPLINES} disciplines and {MAXIMUM_CLASSES_FOR_DISCIPLINE} classes for each discipline." +PREFERENCE_RANGE_ERROR = f"preference must be a list of integers with range [{MINIMUM_PREFERENCE_RANGE}, {MAXIMUM_PREFERENCE_RANGE}]" def check(function): """ @@ -30,15 +33,22 @@ def __init__(self, classes_id: list[int], preference: list = None): self.schedule_info = defaultdict(lambda: None) self.preference = preference self.generated = False + self._validate_preference() self._get_and_validate_classes(classes_id=set(classes_id)) self._make_disciplines_list() self._validate_parameters_length() + + def _validate_preference(self) -> None: + self.valid = self.preference is None or all(isinstance(x, int) and MINIMUM_PREFERENCE_RANGE <= x <= MAXIMUM_PREFERENCE_RANGE for x in self.preference) + + if not self.valid: + raise ValueError(PREFERENCE_RANGE_ERROR) + @check def _get_and_validate_classes(self, classes_id: set[int]) -> None: self.disciplines = defaultdict(list) self.classes = dict() self.schedules = [] - self.valid = True if not len(classes_id): self.valid = False From 391368f2b862fab7f0749a9eb2326c190eadb706 Mon Sep 17 00:00:00 2001 From: Caio Date: Wed, 6 Dec 2023 16:24:51 -0300 Subject: [PATCH 20/20] views(schedule): update openapi swagger parameters --- api/api/views.py | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/api/api/views.py b/api/api/views.py index d5ee81b7..bdc7ba66 100644 --- a/api/api/views.py +++ b/api/api/views.py @@ -18,6 +18,7 @@ ERROR_MESSAGE_SEARCH_LENGTH = f"search must have at least {MINIMUM_SEARCH_LENGTH} characters" MAXIMUM_RETURNED_SCHEDULES = 5 + class Search(APIView): def treat_string(self, string: str | None) -> str | None: @@ -128,16 +129,34 @@ def get(self, request: request.Request, *args, **kwargs) -> response.Response: class Schedule(APIView): @swagger_auto_schema( - operation_description="Gera uma lista de horários válidos para as turmas enviadas.", - manual_parameters=[ - openapi.Parameter('classes', openapi.IN_QUERY, - description="Lista de ids de turmas", type=openapi.TYPE_ARRAY, items=openapi.Items(type=openapi.TYPE_INTEGER)), - openapi.Parameter('preference', openapi.IN_QUERY, - description="Lista de preferências (manhã/tarde/noite)", type=openapi.TYPE_ARRAY, items=openapi.Items(type=openapi.TYPE_INTEGER)), - ], - responses={ - 200: openapi.Response('OK', serializers.ClassSerializerSchedule), - **Errors([400]).retrieve_erros(), + operation_description="Gera possíveis horários de acordo com as aulas escolhidas com preferência de turno", + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + title="body", + required=['classes'], + properties={ + 'classes': openapi.Schema( + description="Lista de ids de aulas escolhidas", + type=openapi.TYPE_ARRAY, + items=openapi.Schema( + description="Id da aula", + type=openapi.TYPE_INTEGER + ) + ), + 'preference': openapi.Schema( + description="Lista de preferências (manhã, tarde, noite)", + type=openapi.TYPE_ARRAY, + items=openapi.Schema( + description="Define o peso de cada turno", + type=openapi.TYPE_INTEGER, + enum=[1, 2, 3] + ) + ) + } + ), + responses = { + 200: openapi.Response('OK', serializers.ClassSerializerSchedule(many=True)), + **Errors([400]).retrieve_erros() } ) def post(self, request: request.Request, *args, **kwargs) -> response.Response: