diff --git a/Makefile b/Makefile
index d84d0734..5285e03a 100644
--- a/Makefile
+++ b/Makefile
@@ -2,11 +2,11 @@
# Configuration
config: copy-env setup-env config-mock entrypoint-chmod
-copy-env:
+copy-env:
cp ./api/.env.example ./api/.env
cp ./web/.env.example ./web/.env.local
-setup-env:
+setup-env:
bash scripts/env.sh
config-mock:
@@ -24,7 +24,7 @@ install:
## Docker ##
start:
- sudo docker compose up -d
+ sudo docker compose up -d
start-b:
sudo docker compose up --build -d
@@ -33,7 +33,7 @@ stop:
sudo docker compose down
stop-v:
- sudo docker compose down -v
+ sudo docker compose down -v
## Django Shortcuts ##
@@ -42,7 +42,7 @@ stop-v:
test:
python3 scripts/test.py --clean
-testfull:
+testfull:
python3 scripts/test.py
sudo docker exec django-api coverage html
python3 scripts/report.py
@@ -57,3 +57,9 @@ makemigrations:
migrate:
sudo docker exec django-api python3 manage.py migrate
+
+# Database Operations
+updatedb-all:
+ sudo docker exec django-api python3 manage.py updatedb -a
+deletedb-all:
+ sudo docker exec django-api python3 manage.py updatedb -d -a
diff --git a/README.md b/README.md
index ed933df7..1922d474 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,52 @@
-# Sua Grade UnB
-
-O Sua Grade UnB é um projeto em desenvolvimento da matéria **Métodos de Desenvolvimento de Software**, a qual tem como objetivo auxiliar os alunos da Universidade de Brasília a montarem suas grades horárias de maneira fácil e intuitiva.
+# [Sua Grade UnB](https://suagradeunb.com.br/)
+
+[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
+[![codecov](https://codecov.io/gh/unb-mds/2023-2-Squad11/branch/main/graph/badge.svg?token=ZQZQZQZQZQ)](https://codecov.io/gh/unb-mds/2023-2-Squad11)
+[![GitHub issues](https://img.shields.io/github/issues/unb-mds/2023-2-Squad11)]()
+[![GitHub contributors](https://img.shields.io/github/contributors/unb-mds/2023-2-Squad11)]()
+[![GitHub stars](https://img.shields.io/github/stars/unb-mds/2023-2-Squad11)]()
+[![Hit Counter](https://views.whatilearened.today/views/github/unb-mds/2023-2-Squad11.svg)](https://views.whatilearened.today/views/github/unb-mds/2023-2-Squad11.svg)
+
+
+[![Python version](https://img.shields.io/badge/python-3.11.6-blue)](https://www.python.org/downloads/release/python-3116/)
+[![Django version](https://img.shields.io/badge/django-4.2.5-blue)](https://www.djangoproject.com/download/)
+[![Node version](https://img.shields.io/badge/node-20.9.0-blue)](https://nodejs.org/en/download/)
+[![npm version](https://img.shields.io/badge/npm-10.2.3-blue)](https://nodejs.org/en/download/)
+[![Docker version](https://img.shields.io/badge/docker-24.0.7-blue)](https://docs.docker.com/engine/install/)
+[![Docker Compose version](https://img.shields.io/badge/docker_compose-2.21.0-blue)](https://docs.docker.com/compose/install/)
+
+O Sua Grade UnB é um projeto da matéria **Métodos de Desenvolvimento de Software**, a qual tem como objetivo auxiliar os alunos da Universidade de Brasília a montarem suas grades horárias de maneira fácil e intuitiva.
+
+Com apenas alguns cliques, o aluno poderá montar sua grade horária de acordo com as matérias que deseja cursar. Além disso, o sistema auxiliará o aluno ao resolver os conflitos de horários entre as matérias escolhidas, retornando as melhores opções de horários de acordo com suas preferências.
+
+O projeto é software livre e está sob a licença [MIT](./LICENSE).
+
+## 📝 Sumário
+
+- [Sua Grade UnB](#sua-grade-unb)
+ - [📝 Sumário](#-sumário)
+ - [👥 Equipe](#-equipe)
+ - [✨ Início](#-início)
+ - [📋 Pré-requisitos](#-pré-requisitos)
+ - [💻 Ambiente](#-ambiente)
+ - [📁 Dependências do projeto](#-dependências-do-projeto)
+ - [💾 Execução](#-execução)
+ - [✅ Autenticação do Google OAuth](#-autenticação-do-google-oauth)
+ - [🖱️ Acesso aos serviços](#-acesso-aos-serviços)
+ - [📍 Migrations](#-migrations)
+ - [📚 Documentação](#-documentação)
+ - [📎 Extra](#-extra)
+
+## 👥 Equipe
+
+| Nome | GitHub |
+| :--- | :----: |
+| Arthur Ribeiro e Sousa | [@artrsousa1](https://github.com/artrsousa1) |
+| Caio Falcão Habibe Costa | [@CaioHabibe](https://github.com/CaioHabibe) |
+| Caio Felipe Rocha Rodrigues| [@caio-felipee](https://github.com/caio-felipee) |
+| Gabriel Henrique Castelo Costa | [@GabrielCastelo-31](https://github.com/GabrielCastelo-31) |
+| Henrique Camelo Quenino | [@henriquecq](https://github.com/henriquecq) |
+| Mateus Vieira Rocha da Silva | [@mateusvrs](https://github.com/mateusvrs) |
## ✨ Início
@@ -10,7 +56,7 @@ Você pode clonar o repositório do projeto com o seguinte comando:
git clone https://github.com/unb-mds/2023-2-Squad11.git
```
-### Dependências globais
+### 📋 Pré-requisitos
Para rodar o projeto, você precisa instalar as dependências globais, que são:
@@ -19,7 +65,7 @@ Para rodar o projeto, você precisa instalar as dependências globais, que são:
- Node v20.9.0 e NPM v10.1.0 (ou superior)
- Docker Engine v24.0.6 e Docker Compose v2.21.0 (ou superior)
-### Ambiente
+### 💻 Ambiente
Para configurar o ambiente, você pode rodar o seguinte script:
@@ -27,7 +73,7 @@ Para configurar o ambiente, você pode rodar o seguinte script:
make config
```
-### Dependências do projeto
+### 📁 Dependências do projeto
Para instalar as dependências do projeto, você pode rodar os seguintes comando:
@@ -42,7 +88,7 @@ source env/bin/activate
make install
```
-### Execução
+### 💾 Execução
Para executar o projeto, você pode rodar o seguinte comando:
@@ -50,7 +96,7 @@ Para executar o projeto, você pode rodar o seguinte comando:
docker compose up
```
-**Observações do Docker:**
+#### Observações do Docker
```bash
# Se você quiser rodar em segundo plano
@@ -63,14 +109,69 @@ docker compose up --build
docker compose down -v
```
-### Migrations
+### ✅ Autenticação do Google OAuth
+
+Para que o login com o Google funcione, é necessário trocar o `your_client_id` no arquivo `web/.env.local` pelo **Client ID** do projeto no Google Cloud.
+
+1. Crie um projeto no [Google Cloud](https://console.cloud.google.com/).
+2. Vá para a página de [Credenciais](https://console.cloud.google.com/apis/credentials) do projeto.
+3. Clique em **Criar credenciais** e selecione **ID do cliente OAuth**.
+4. Selecione **Aplicativo da Web**.
+5. Adicione `http://localhost:3000` como **Origens JavaScript autorizadas** e **URIs de redirecionamento autorizadas**.
+6. Copie o **Client ID** e cole no arquivo `web/.env.local` no lugar de `your_client_id`.
+
+Após isto:
+
+1. Vá para a página de [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent).
+2. Selecione **Usuários externos** e clique em **Criar**.
+3. Preencha os campos obrigatórios e clique em **Salvar e continuar**.
+4. Na seção **Usuários de Teste** adicione o seu e-mail e clique em **Adicionar**.
+5. Clique em **Salvar e continuar**.
+
+Adicionando serviços:
+
+1. Entre na aba **APIs e Serviços**.
+2. Clique em **Ativar APIs e Serviços**.
+3. Ative os seguintes serviços:
+ - IAM Service Account Credentials API
+ - Identity and Access Management (IAM) API
+
+
+### 🖱️ Acesso aos serviços
+
+| Serviço | URL |
+| :--- | :----: |
+| Frontend | [http://localhost:3000](http://localhost:3000) |
+| Backend | [http://localhost:8000](http://localhost:8000) |
+
+### 📍 Migrations
+
+Migration é um recurso do Django que permite que você altere o modelo de dados do seu projeto. Portanto, sempre que você alterar o modelo de dados, você deve criar uma nova migration.
Para criar possíveis novas migrations, você pode rodar o seguinte comando:
```bash
-python3 ./api/manage.py makemigrations --settings=core.settings.dev
+# Crie as migrations
+make makemigrations
+
+# Execute as migrations
+make migrate
```
## 📚 Documentação
A documentação do projeto pode ser encontrada clicando [aqui](https://unb-mds.github.io/2023-2-Squad11/).
+
+## 📎 Extra
+
+### Story Map e Activity Flow
+
+- Para acessar o Story Map e o Activity Flow, clique [aqui](https://miro.com/app/board/uXjVNYnku7s=/?share_link_id=596015837126).
+
+### Arquitetura
+
+- Para acessar a arquitetura do projeto, clique [aqui](https://www.figma.com/file/ZhAq8LRcclpWHYi4XnUySw/Sua-Grade-UnB---System-Design?type=whiteboard&node-id=0%3A1&t=k46HHNk4NotrkTpX-1).
+
+### Protótipo
+
+- Para acessar o protótipo do projeto, clique [aqui](https://www.figma.com/proto/o5Ffh1fWmmQz7KcDGuHrVP/Sua-grade-UNB?type=design&node-id=16-2775&scaling=scale-down&page-id=0%3A1&mode=design&t=L5JwoVdZsjyLBGdb-1).
\ No newline at end of file
diff --git a/api/api/management/commands/updatedb.py b/api/api/management/commands/updatedb.py
index 812afbeb..7e59bcd6 100644
--- a/api/api/management/commands/updatedb.py
+++ b/api/api/management/commands/updatedb.py
@@ -15,8 +15,8 @@ def add_arguments(self, parser: CommandParser) -> None:
"""Adiciona os argumentos do comando."""
parser.add_argument('-a', '-all', action='store_true', dest='all', default=False,
help="Atualiza o banco de dados com as disciplinas dos períodos atual e seguinte.")
-
- parser.add_argument('-p', '--period', action='store', default=None,
+
+ parser.add_argument('-p', '--period', action='store', default=None,
choices=[".".join(sessions.get_current_year_and_period()), ".".join(sessions.get_next_period())],
dest='period', help="Atualiza o banco de dados com as disciplinas do período especificado.")
@@ -35,19 +35,19 @@ def handle(self, *args: Any, **options: Any):
print("Nenhum período foi especificado.")
print("Utilize o comando 'updatedb -h' para mais informações.")
return
-
- # Obtem o ano e o período atual e o ano e o período seguinte
+
+ # Obtem o ano e o período anterior ao período atual
previous_period_year, previous_period = sessions.get_previous_period()
- # Apaga as disciplinas do período interior
+ # Apaga as disciplinas do período anterior
delete_all_departments_using_year_and_period(
year=previous_period_year, period=previous_period)
-
+
if options["delete"]:
for year, period in choices:
self.delete_period(year=year, period=period)
return
-
+
departments_ids = web_scraping.get_list_of_departments()
if departments_ids is None:
@@ -55,7 +55,7 @@ def handle(self, *args: Any, **options: Any):
return
print("Atualizando o banco de dados...")
-
+
for year, period in choices:
start_time = time()
self.update_departments(departments_ids=departments_ids, year=year, period=period)
@@ -68,14 +68,14 @@ def update_departments(self, departments_ids: list, year: str, period: str) -> N
department_id=department_id, current_year=year, current_period=period)
department = get_or_create_department(
code=department_id, year=year, period=period)
-
+
# Para cada disciplina do período atual, deleta as turmas previamente cadastradas e cadastra novas turmas no banco de dados
for discipline_code in disciplines_list:
classes_info = disciplines_list[discipline_code]
# Cria ou pega a disciplina
discipline = get_or_create_discipline(
name=classes_info[0]["name"], code=discipline_code, department=department)
-
+
# Deleta as turmas previamente cadastradas
delete_classes_from_discipline(discipline=discipline)
@@ -104,5 +104,5 @@ def display_success_update_message(self, operation: str, start_time: float) -> N
def display_success_delete_message(self, operation: str, start_time: float) -> None:
print("Operação de remoção do banco de dados realizada com sucesso.")
- print(f"Sucesso em {operation}")
- print(f"Tempo de execução: {(time() - start_time):.1f}s\n")
\ No newline at end of file
+ print(f"Sucesso em {operation}")
+ print(f"Tempo de execução: {(time() - start_time):.1f}s\n")
diff --git a/api/api/migrations/0004_alter_class_special_dates.py b/api/api/migrations/0004_alter_class_special_dates.py
new file mode 100644
index 00000000..485e9bef
--- /dev/null
+++ b/api/api/migrations/0004_alter_class_special_dates.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.2.5 on 2023-11-27 15:05
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0003_remove_class_workload_class_special_dates_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='class',
+ name='special_dates',
+ field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=256), size=3), default=list, size=None),
+ ),
+ ]
diff --git a/api/api/migrations/0004_auto_20231126_1740.py b/api/api/migrations/0004_auto_20231126_1740.py
new file mode 100644
index 00000000..30b689f7
--- /dev/null
+++ b/api/api/migrations/0004_auto_20231126_1740.py
@@ -0,0 +1,22 @@
+# Generated by Django 4.2.5 on 2023-11-26 20:40
+
+from django.db import migrations
+from django.contrib.postgres.operations import CreateExtension
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0003_remove_class_workload_class_special_dates_and_more'),
+ ]
+
+ operations = [
+ CreateExtension("pg_trgm"),
+ migrations.RunSQL(
+ "CREATE TEXT SEARCH CONFIGURATION portuguese_unaccent( COPY = portuguese );"
+ ),
+ migrations.RunSQL(
+ "ALTER TEXT SEARCH CONFIGURATION portuguese_unaccent "
+ + "ALTER MAPPING FOR hword, hword_part, word "
+ + "WITH unaccent, portuguese_stem;"
+ )
+ ]
diff --git a/api/api/migrations/0005_discipline_unicode_name.py b/api/api/migrations/0005_discipline_unicode_name.py
new file mode 100644
index 00000000..2954b79b
--- /dev/null
+++ b/api/api/migrations/0005_discipline_unicode_name.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.5 on 2023-11-27 18:02
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0004_auto_20231126_1740'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='discipline',
+ name='unicode_name',
+ field=models.CharField(default='', max_length=128),
+ ),
+ ]
diff --git a/api/api/migrations/0006_merge_20231127_2255.py b/api/api/migrations/0006_merge_20231127_2255.py
new file mode 100644
index 00000000..0b240194
--- /dev/null
+++ b/api/api/migrations/0006_merge_20231127_2255.py
@@ -0,0 +1,14 @@
+# Generated by Django 4.2.5 on 2023-11-28 01:55
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0004_alter_class_special_dates'),
+ ('api', '0005_discipline_unicode_name'),
+ ]
+
+ operations = [
+ ]
diff --git a/api/api/models.py b/api/api/models.py
index ce73a062..b3fd1148 100644
--- a/api/api/models.py
+++ b/api/api/models.py
@@ -1,4 +1,5 @@
from django.db import models
+from unidecode import unidecode
from django.contrib.postgres.fields import ArrayField
class Department(models.Model):
@@ -17,15 +18,21 @@ def __str__(self):
class Discipline(models.Model):
"""Classe que representa uma disciplina.
name:str -> Nome da disciplina
+ unicode_name:str -> Nome da disciplina normalizado
code:str -> Código da disciplina
department:Department -> Departamento da disciplina
"""
name = models.CharField(max_length=128)
+ unicode_name = models.CharField(max_length=128, default='')
code = models.CharField(max_length=64)
department = models.ForeignKey(Department, on_delete=models.CASCADE, related_name='disciplines')
def __str__(self):
return self.name
+
+ def save(self, *args, **kwargs):
+ self.unicode_name = unidecode(self.name).casefold()
+ super(Discipline, self).save(*args, **kwargs)
class Class(models.Model):
"""Classe que representa uma turma.
@@ -42,7 +49,13 @@ class Class(models.Model):
days = ArrayField(models.CharField(max_length=64))
_class = models.CharField(max_length=64)
discipline = models.ForeignKey(Discipline, on_delete=models.CASCADE, related_name='classes')
- special_dates = ArrayField(models.CharField(max_length=256), default=list)
+ special_dates = ArrayField(
+ ArrayField(
+ models.CharField(max_length=256),
+ size=3,
+ ),
+ default=list
+ )
def __str__(self):
return self._class
diff --git a/api/api/swagger.py b/api/api/swagger.py
new file mode 100644
index 00000000..445dfb62
--- /dev/null
+++ b/api/api/swagger.py
@@ -0,0 +1,40 @@
+from drf_yasg import openapi
+from requests import status_codes
+
+
+class Errors():
+
+ def __init__(self, erros: list[int]) -> None:
+ self.erros = erros
+
+ def get_schema(self) -> openapi.Schema:
+ return openapi.Schema(
+ type=openapi.TYPE_OBJECT,
+ properties={
+ 'errors': openapi.Schema(
+ type=openapi.TYPE_STRING,
+ description='Mensagem de erro'
+ ),
+ }
+ )
+
+ def add_error(self, swagger_erros: dict[openapi.Response], error: int, key_error: KeyError):
+ check_error = str(error).startswith('4') or str(error).startswith('5')
+ if check_error:
+ message = status_codes._codes[error][0].upper()
+ swagger_erros[error] = openapi.Response(message, self.get_schema())
+ else: # pragma: no cover
+ raise key_error
+
+ def retrieve_erros(self) -> dict[openapi.Response]:
+ swagger_erros = dict()
+
+ for error in self.erros:
+ key_error = KeyError(f'Code {error} is not a valid HTTP error.')
+
+ try:
+ self.add_error(swagger_erros, error, key_error)
+ except KeyError: # pragma: no cover
+ raise key_error
+
+ return swagger_erros
diff --git a/api/api/templates/404.html b/api/api/templates/404.html
new file mode 100644
index 00000000..cc634da8
--- /dev/null
+++ b/api/api/templates/404.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/api/api/tests/test_discipline_models.py b/api/api/tests/test_discipline_models.py
index 0a4d5b7e..c59a7e23 100644
--- a/api/api/tests/test_discipline_models.py
+++ b/api/api/tests/test_discipline_models.py
@@ -1,6 +1,7 @@
from django.test import TestCase
from api.models import Department, Discipline, Class
+
class DisciplineModelsTest(TestCase):
def setUp(self):
self.department = Department.objects.create(
@@ -50,4 +51,4 @@ def test_str_method_of_class(self):
self.assertEqual(str(self._class), self._class._class)
def test_str_method_of_department(self):
- self.assertEqual(str(self.department), self.department.code)
\ No newline at end of file
+ self.assertEqual(str(self.department), self.department.code)
diff --git a/api/api/tests/test_search_api.py b/api/api/tests/test_search_api.py
index c28c1582..cc039a6b 100644
--- a/api/api/tests/test_search_api.py
+++ b/api/api/tests/test_search_api.py
@@ -1,6 +1,6 @@
from rest_framework.test import APITestCase
from utils.db_handler import get_or_create_department, get_or_create_discipline, create_class
-from api.views import ERROR_MESSAGE
+from api.views import ERROR_MESSAGE, ERROR_MESSAGE_SEARCH_LENGTH
import json
class TestSearchAPI(APITestCase):
@@ -16,42 +16,6 @@ def setUp(self) -> None:
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)
- def test_with_complete_correct_search(self):
- """
- Testa a busca por disciplinas com o nome completo e todos os parâmetros corretos
-
- Testes:
- - Status code (200 OK)
- - Quantidade de disciplinas retornadas
- - Código do departamento
- - Nome da disciplina
- - Professores da disciplina
- """
- response_for_discipline_1 = self.client.get(
- '/courses/?search=calculo+1&year=2023&period=2')
- response_for_discipline_2 = self.client.get(
- '/courses/?search=calculo+2&year=2023&period=2')
- content_1 = json.loads(response_for_discipline_1.content)
- content_2 = json.loads(response_for_discipline_2.content)
-
- # Testes da disciplina 1
- self.assertEqual(response_for_discipline_1.status_code, 200)
- self.assertEqual(len(content_1), 1)
- self.assertEqual(content_1[0]['department']
- ['code'], self.department.code)
- self.assertEqual(content_1[0]['name'], self.discipline_1.name)
- self.assertEqual(content_1[0]['classes'][0]
- ['teachers'], self._class_1.teachers)
-
- # Testes da disciplina 2
- self.assertEqual(response_for_discipline_2.status_code, 200)
- self.assertEqual(len(content_2), 1)
- self.assertEqual(content_2[0]['department']
- ['code'], self.department.code)
- self.assertEqual(content_2[0]['name'], self.discipline_2.name)
- self.assertEqual(content_2[0]['classes'][0]
- ['teachers'], self._class_2.teachers)
-
def test_with_incomplete_correct_search(self):
"""
Testa a busca por disciplinas com nome incompleto e todos os parâmetros corretos
@@ -104,29 +68,6 @@ def test_with_code_search(self):
# Testes da disciplina 2
self.assertEqual(response_for_discipline_2.status_code, 200)
self.assertEqual(len(content_2), 1)
-
- def test_with_code_search_spaced(self):
- """
- Testa a busca por disciplinas através do código da matéria com espaços
- Testes:
- - Status code (200 OK)
- - Quantidade de disciplinas retornadas
- """
-
- response_for_discipline_1 = self.client.get(
- '/courses/?search=MAT+518&year=2023&period=2')
- response_for_discipline_2 = self.client.get(
- '/courses/?search=MAT+519&year=2023&period=2')
- content_1 = json.loads(response_for_discipline_1.content)
- content_2 = json.loads(response_for_discipline_2.content)
-
- # Testes da disciplina 1
- self.assertEqual(response_for_discipline_1.status_code, 200)
- self.assertEqual(len(content_1), 1)
-
- # Testes da disciplina 2
- self.assertEqual(response_for_discipline_2.status_code, 200)
- self.assertEqual(len(content_2), 1)
def test_with_bad_url_search_missing_year(self):
"""
@@ -250,4 +191,18 @@ 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)
\ No newline at end of file
+ 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
+ Testes:
+ - Status code (400 BAD REQUEST)
+ """
+
+ response_1 = self.client.get('/courses/?search=cal&year=2023&period=2')
+ content_1 = json.loads(response_1.content)
+
+ 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
diff --git a/api/api/tests/test_year_period_api.py b/api/api/tests/test_year_period_api.py
new file mode 100644
index 00000000..87d8e071
--- /dev/null
+++ b/api/api/tests/test_year_period_api.py
@@ -0,0 +1,25 @@
+from rest_framework.test import APITestCase
+from utils import sessions as sns
+
+
+class TestYearPeriodAPI(APITestCase):
+
+ def test_year_period(self):
+ """
+ Testa se a API retorna o ano/periodo atual e o próximo ano/periodo
+
+ Testes:
+ - Status code (200 OK)
+ - Dados retornados (ano/periodo atual e próximo ano/periodo)
+ """
+
+ response = self.client.get('/courses/year-period/')
+ year, period = sns.get_current_year_and_period()
+ next_year, next_period = sns.get_next_period()
+
+ expected_data = {
+ 'year/period': [f'{year}/{period}', f'{next_year}/{next_period}'],
+ }
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.data, expected_data)
diff --git a/api/api/urls.py b/api/api/urls.py
index b613f9ac..42e281f2 100644
--- a/api/api/urls.py
+++ b/api/api/urls.py
@@ -4,5 +4,6 @@
app_name = 'api'
urlpatterns = [
- path('', views.Search.as_view(), name="search")
-]
\ No newline at end of file
+ path('', views.Search.as_view(), name="search"),
+ path('year-period/', views.YearPeriod.as_view(), name="year-period")
+]
diff --git a/api/api/views.py b/api/api/views.py
index 0aa07fa9..2ab4cb10 100644
--- a/api/api/views.py
+++ b/api/api/views.py
@@ -1,46 +1,125 @@
-from utils.db_handler import filter_disciplines_by_name, filter_disciplines_by_code, filter_disciplines_by_year_and_period
+from utils import db_handler as dbh
+from .models import Discipline
+from unidecode import unidecode
+from django.contrib import admin
+from django.db.models.query import QuerySet
from rest_framework.decorators import APIView
-from .serializers import DisciplineSerializer
-from rest_framework.response import Response
-from rest_framework.request import Request
-from rest_framework import status
+from utils.sessions import get_current_year_and_period, get_next_period
+from rest_framework import status, request, response
+from drf_yasg.utils import swagger_auto_schema
+from drf_yasg import openapi
+from .swagger import Errors
+from api import serializers
-MAXIMUM_RETURNED_DISCIPLINES = 5
+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"
class Search(APIView):
+
def treat_string(self, string: str | None) -> str | None:
if string is not None:
string = string.strip()
return string
-
- def get(self, request: Request, *args, **kwargs) -> Response:
+
+ 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']
+
+ disciplines = Discipline.objects.all()
+ disciplines, _ = model_handler.get_search_results(
+ request, disciplines, unicode_name)
+
+ return disciplines
+
+ @swagger_auto_schema(
+ operation_description="Busca disciplinas por nome ou código. O ano e período são obrigatórios.",
+ manual_parameters=[
+ openapi.Parameter('search', openapi.IN_QUERY,
+ description="Termo de pesquisa (Nome/Código)", 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),
+ ],
+ responses={
+ 200: openapi.Response('OK', serializers.DisciplineSerializer),
+ **Errors([400]).retrieve_erros()
+ }
+ )
+ 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))
- if name is None or len(name) == 0 or year is None or len(year) == 0 or period is None or len(period) == 0:
- return Response(
+ 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:
+ return response.Response(
{
"errors": ERROR_MESSAGE
}, status.HTTP_400_BAD_REQUEST)
- name = name.split()
- disciplines = filter_disciplines_by_name(name=name[0])
+ if len(name) < MINIMUM_SEARCH_LENGTH:
+ return response.Response(
+ {
+ "errors": ERROR_MESSAGE_SEARCH_LENGTH
+ }, status.HTTP_400_BAD_REQUEST)
- for term in name[1:]:
- disciplines &= filter_disciplines_by_name(name=term)
+ disciplines = self.filter_disciplines(request, name)
+ disciplines = dbh.get_best_similarities_by_name(name, disciplines)
if not disciplines.count():
- disciplines = filter_disciplines_by_code(code=name[0])
+ disciplines = dbh.filter_disciplines_by_code(code=name[0])
for term in name[1:]:
- disciplines &= filter_disciplines_by_code(code=term)
-
- filtered_disciplines = filter_disciplines_by_year_and_period(
+ disciplines &= dbh.filter_disciplines_by_code(code=term)
+
+ disciplines = dbh.filter_disciplines_by_code(name)
+
+ filtered_disciplines = dbh.filter_disciplines_by_year_and_period(
year=year, period=period, disciplines=disciplines)
- data = DisciplineSerializer(filtered_disciplines, many=True).data
+ data = serializers.DisciplineSerializer(
+ filtered_disciplines, many=True).data
+
+ return response.Response(data[:MAXIMUM_RETURNED_DISCIPLINES], status.HTTP_200_OK)
+
+
+class YearPeriod(APIView):
+
+ @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.",
+ responses={
+ 200: openapi.Response('OK', openapi.Schema(
+ type=openapi.TYPE_OBJECT,
+ properties={
+ 'year/period': openapi.Schema(
+ type=openapi.TYPE_ARRAY,
+ items=openapi.Schema(
+ type=openapi.TYPE_STRING
+ )
+ )
+ }
+ ), examples={
+ 'application/json': {
+ 'year/period': ['2020/1', '2020/2']
+ }
+ })
+ }
+ )
+ def get(self, request: request.Request, *args, **kwargs) -> response.Response:
+ year, period = get_current_year_and_period()
+ next_year, next_period = get_next_period()
+
+ data = {
+ 'year/period': [f'{year}/{period}', f'{next_year}/{next_period}'],
+ }
- return Response(data[:MAXIMUM_RETURNED_DISCIPLINES], status.HTTP_200_OK)
+ return response.Response(data, status.HTTP_200_OK)
diff --git a/api/core/settings/base.py b/api/core/settings/base.py
index 72e6afeb..968e54b4 100644
--- a/api/core/settings/base.py
+++ b/api/core/settings/base.py
@@ -43,6 +43,7 @@
'django.contrib.staticfiles',
'django.contrib.postgres',
'corsheaders',
+ 'drf_yasg',
'api',
'users',
'utils',
@@ -102,7 +103,7 @@
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [],
+ 'DIRS': [os.path.join(BASE_DIR, 'api/templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
diff --git a/api/core/urls.py b/api/core/urls.py
index 4a3bed89..21691b04 100644
--- a/api/core/urls.py
+++ b/api/core/urls.py
@@ -16,10 +16,47 @@
"""
from django.contrib import admin
from django.urls import path, include
+from rest_framework import permissions
+from drf_yasg.views import get_schema_view
+from drf_yasg import openapi
import sys
+schema_view = get_schema_view(
+ openapi.Info(
+ title="Sua Grade UnB - API",
+ default_version='v1',
+ description="""
+
+ O Sua Grade UnB é um projeto da matéria Métodos de Desenvolvimento de Software, a qual tem como objetivo auxiliar os alunos da Universidade de Brasília a montarem suas grades horárias de maneira fácil e intuitiva.
+ Contribuidores:
+