Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

task(schedule): Cria o algoritmo de geração de matérias #138

Merged
merged 20 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e5415e7
api(serializers): add serializers for schedule view
caio-felipee Dec 4, 2023
5e9b453
api(urls): add schedule endpoint
caio-felipee Dec 4, 2023
b39a342
api(views): create schedule api
caio-felipee Dec 4, 2023
2d15331
utils(db_handler): add get class by id function
caio-felipee Dec 4, 2023
0ef6348
utils(schedule_generator): add schedule generator
caio-felipee Dec 4, 2023
e489052
api(schedule-generator): Adiciona prioridade de turno
GabrielCastelo-31 Dec 4, 2023
b069bd3
api(): turn functions more readable
caio-felipee Dec 4, 2023
32de665
utils(schedule-generator): adiciona msg erros
caio-felipee Dec 5, 2023
a3e6dbc
api(views): arruma erro de verificação de aulas
caio-felipee Dec 5, 2023
0392640
test(schedule-view): add test for schedule view
caio-felipee Dec 5, 2023
3780312
test(schedule-api): ignora o teste não-testável
caio-felipee Dec 5, 2023
7e1644c
test(db_handler): add teste para o get_class_by_id
caio-felipee Dec 5, 2023
499ef4f
utils(generator): coloca o erro de forma global
caio-felipee Dec 5, 2023
2b78632
test(schedule_generator): adiciona testes
caio-felipee Dec 5, 2023
792021d
api(schedule): fix repeated code in serializer
GabrielCastelo-31 Dec 6, 2023
f7dd132
fix misstyped word
GabrielCastelo-31 Dec 6, 2023
7e62be2
fix mistyped word
GabrielCastelo-31 Dec 6, 2023
0fc9e7b
test(schedule_generator): add test for range error
caio-felipee Dec 6, 2023
52efcb5
utils(schedule_generator): add pref range verification
caio-felipee Dec 6, 2023
391368f
views(schedule): update openapi swagger parameters
caio-felipee Dec 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions api/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,16 @@ class DisciplineSerializer(ModelSerializer):

class Meta:
model = Discipline
fields = '__all__'

class DisciplineSerializerSchedule(ModelSerializer):
GabrielCastelo-31 marked this conversation as resolved.
Show resolved Hide resolved
class Meta:
model = Discipline
fields = '__all__'

class ClassSerializerSchedule(ModelSerializer):
discipline = DisciplineSerializerSchedule()

class Meta:
model = Class
fields = '__all__'
124 changes: 124 additions & 0 deletions api/api/tests/test_schedule_api.py
Original file line number Diff line number Diff line change
@@ -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): # pragma: no cover
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)
3 changes: 2 additions & 1 deletion api/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
]
65 changes: 64 additions & 1 deletion api/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
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'"
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):

Expand Down Expand Up @@ -123,3 +124,65 @@ def get(self, request: request.Request, *args, **kwargs) -> response.Response:
}

return response.Response(data, status.HTTP_200_OK)


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)),
caio-felipee marked this conversation as resolved.
Show resolved Hide resolved
],
responses={
200: openapi.Response('OK', serializers.ClassSerializerSchedule),
caio-felipee marked this conversation as resolved.
Show resolved Hide resolved
**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)
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"""
return response.Response(
{
"errors": "preference must be a list of 3 integers"
}, status.HTTP_400_BAD_REQUEST)

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 and must be a list of integers with at least one element"
}, status.HTTP_400_BAD_REQUEST)

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": str(error)
}, status.HTTP_400_BAD_REQUEST)

data = []

for schedule in schedules[:MAXIMUM_RETURNED_SCHEDULES]:
data.append(
list(map(lambda x: serializers.ClassSerializerSchedule(x).data, schedule)))

return response.Response(data, status.HTTP_200_OK)
3 changes: 3 additions & 0 deletions api/utils/db_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading