Skip to content

Commit

Permalink
Merge pull request #138 from unb-mds/task/schedule
Browse files Browse the repository at this point in the history
task(schedule): Cria o algoritmo de geração de matérias
  • Loading branch information
caio-felipee authored Dec 6, 2023
2 parents ec2e851 + 391368f commit 52504b6
Show file tree
Hide file tree
Showing 8 changed files with 596 additions and 7 deletions.
20 changes: 15 additions & 5 deletions api/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,19 +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 DisciplineSerializerSchedule(ModelSerializer):
class Meta:
model = Discipline
fields = '__all__'
fields = '__all__'


class DisciplineSerializer(DisciplineSerializerSchedule):
department = DepartmentSerializer()
classes = ClassSerializer(many=True)


class ClassSerializerSchedule(ClassSerializer):
discipline = DisciplineSerializerSchedule()
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")
]
82 changes: 82 additions & 0 deletions api/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +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 +125,83 @@ 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 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:
"""
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

0 comments on commit 52504b6

Please sign in to comment.