diff --git a/README.md b/README.md index 0711d42..7b4003d 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ programação competitiva. | Gabriel Moura | 221008060 | [@thegm445](https://github.com/thegm445) | | Luiza Maluf | 221008294 | [@LuizaMaluf](https://github.com/LuizaMaluf) | | Letícia Hladczuk | 221039209 | [@HladczukLe](https://github.com/HladczukLe) | -| Esther Silva | 190106034 | [@EstherSousa](https://github.com/EstherSousa) | | Gabriel Fernando | 222022162 | [@MMcLovin](https://github.com/MMcLovin) | ## Instalação diff --git a/apps/contests/models.py b/apps/contests/models.py index 04570cb..3e6f590 100644 --- a/apps/contests/models.py +++ b/apps/contests/models.py @@ -1,3 +1,4 @@ +from django.core.exceptions import ValidationError from django.db.models import ( BooleanField, CharField, @@ -33,12 +34,47 @@ def __str__(self) -> str: @property def status(self) -> ContestStatus: + """ + Retorna o status atual do contest baseado no horário atual. + + .. list-table:: Status do contest + :header-rows: 1 + + * - Status + - Descrição + * - :attr:`ContestStatus.PENDING` + - Quando o contest ainda não começou. + * - :attr:`ContestStatus.RUNNING` + - Quando o contest está acontecendo. + * - :attr:`ContestStatus.FINISHED` + - Quando o contest já terminou. + * - :attr:`ContestStatus.CANCELLED` + - Quando o contest foi cancelado. + + Returns + ------- + :class:`ContestStatus` + O status atual do contest. + """ if self.cancelled: return ContestStatus.CANCELLED - - if self.start_time > now(): + elif self.start_time > now(): return ContestStatus.PENDING elif self.end_time < now(): return ContestStatus.FINISHED else: return ContestStatus.RUNNING + + def clean(self) -> None: + """ + Validação do contest. Verifica se o :attr:`start_time` é menor + que o :attr:`end_time`, ou seja, se o contest começa antes de + terminar. Se não for, lança um :class:`ValidationError`. + + Raises + ------ + :class:`ValidationError` + Se o :attr:`start_time` for maior que o :attr:`end_time`. + """ + if self.start_time > self.end_time: + raise ValidationError("Start time must be before end time.") diff --git a/apps/contests/tests.py b/apps/contests/tests.py index d0eba8a..3322a2d 100644 --- a/apps/contests/tests.py +++ b/apps/contests/tests.py @@ -1,6 +1,7 @@ from datetime import timedelta from django.contrib.admin import AdminSite +from django.core.exceptions import ValidationError from django.forms import CharField, Textarea from django.test import TestCase from django.urls import resolve, reverse @@ -11,13 +12,9 @@ from apps.contests.models import Contest -class ContestTestCase(TestCase): +class ContestModelTestCase(TestCase): def setUp(self) -> None: - self.contest = Contest( - title="Test Contest", - description="This is a test contest", - cancelled=False, - ) + self.contest = Contest(title="Test", description="Test contest") def test_status_pending(self) -> None: self.contest.start_time = timezone.now() + timedelta(hours=1) @@ -38,19 +35,41 @@ def test_status_cancelled(self) -> None: self.contest.cancelled = True self.assertEqual(self.contest.status, ContestStatus.CANCELLED) + def test_start_time_before_end_time(self) -> None: + self.contest.start_time = timezone.now() + self.contest.end_time = timezone.now() + timedelta(hours=1) -class ContestStatusTestCase(TestCase): - def test_pending(self) -> None: - self.assertEqual(ContestStatus.PENDING, "Pending") + try: + self.contest.clean() + except ValidationError: + self.fail("ValidationError raised unexpectedly.") - def test_running(self) -> None: - self.assertEqual(ContestStatus.RUNNING, "Running") + def test_start_time_after_end_time(self) -> None: + self.contest.start_time = timezone.now() + timedelta(hours=1) + self.contest.end_time = timezone.now() + + expected = ["Start time must be before end time."] + + with self.assertRaises(ValidationError) as context: + self.contest.clean() - def test_finished(self) -> None: - self.assertEqual(ContestStatus.FINISHED, "Finished") + self.assertEqual(context.exception.messages, expected) - def test_cancelled(self) -> None: - self.assertEqual(ContestStatus.CANCELLED, "Cancelled") + +class ContestStatusTestCase(TestCase): + def test_statuses_values(self) -> None: + self.assertEqual(ContestStatus.PENDING.value, "Pending") + self.assertEqual(ContestStatus.RUNNING.value, "Running") + self.assertEqual(ContestStatus.FINISHED.value, "Finished") + self.assertEqual(ContestStatus.CANCELLED.value, "Cancelled") + + def test_statuses_choices_length(self) -> None: + """ + Devemos ter 4 opções de status de contests: Pending, Running, + Finished, Cancelled. Qualquer mudança nesse número deve ser + refletida aqui. + """ + self.assertEqual(len(ContestStatus), 4) class ContestModelFormTestCase(TestCase): diff --git a/apps/contests/views.py b/apps/contests/views.py index cf6b047..865e76a 100644 --- a/apps/contests/views.py +++ b/apps/contests/views.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Dict from django.db.models.query import QuerySet from django.views import generic @@ -19,12 +19,15 @@ class IndexView(IndexViewBase): context_object_name = "contests" def get_queryset(self) -> QuerySet[Contest]: - return Contest._default_manager.order_by("start_time")[:5] + return Contest._default_manager.order_by("-start_time")[:5] - def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: ctx = super().get_context_data(**kwargs) + # Serve para separar os concursos que estão com status pendente + # ou em andamento dos concursos que já aconteceram ou que foram + # cancelados. Precisamos separar para que o template possa + # exibir os contests de forma diferente. ctx["valid_statuses"] = (ContestStatus.PENDING, ContestStatus.RUNNING) - return ctx diff --git a/docs/source/index.rst b/docs/source/index.rst index 1e7c7b1..bc42a88 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -36,7 +36,6 @@ Integrantes Gabriel Moura 221008060 `@thegm445 `_ Luiza Maluf 221008294 `@LuizaMaluf `_ Letícia Hladczuk 221039209 `@HladczukLe `_ - Esther Silva 190106034 `@EstherSousa `_ Gabriel Fernando 222022162 `@MMcLovin `_ ================ ========= ====================================================== diff --git a/docs/source/scrum/intro.rst b/docs/source/scrum/intro.rst index 557f01e..d1065cc 100644 --- a/docs/source/scrum/intro.rst +++ b/docs/source/scrum/intro.rst @@ -22,3 +22,4 @@ Sprints sprints/sprint-0 sprints/sprint-1 sprints/sprint-2 + sprints/sprint-3 diff --git a/docs/source/scrum/sprints/sprint-3.rst b/docs/source/scrum/sprints/sprint-3.rst new file mode 100644 index 0000000..070e97e --- /dev/null +++ b/docs/source/scrum/sprints/sprint-3.rst @@ -0,0 +1,20 @@ +Sprint 3 +======== + +:bdg-info:`04/11/2023` + +Resumo +------ + +Esta sprint sucedeu a primeira release e começou a segunda fase da disciplina. +Fase que é focada mais na implementação, portanto o objetivo desta sprint foi +melhorar a aparência da página de registro de usuário e iniciar o +desenvolvimento dos testes unitários (requisito presente no plano de ensino). + +Changelog +---------- + +- `Começar o desenvolvimento de testes unitários (#30) `_ +- `Desenvolvimento dos testes (#57) `_ +- `Criar view de registro de usuário (#29) `_ +- `Adicionar view de registro (#54) `_ diff --git a/poetry.lock b/poetry.lock index 55945d1..dd368fa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -331,6 +331,24 @@ files = [ [package.extras] toml = ["tomli"] +[[package]] +name = "crispy-bootstrap5" +version = "2023.10" +description = "Bootstrap5 template pack for django-crispy-forms" +optional = false +python-versions = ">=3.8" +files = [ + {file = "crispy-bootstrap5-2023.10.tar.gz", hash = "sha256:f16c44f1997310e5a89c0cf230402e7111cc1f942f64fb7e44603958b89b06a1"}, + {file = "crispy_bootstrap5-2023.10-py3-none-any.whl", hash = "sha256:9b5a6c9880f37cd32aa678c0d7576ae60b3e502c444c8712e582f8bd91659afb"}, +] + +[package.dependencies] +django = ">=4.2" +django-crispy-forms = ">=2" + +[package.extras] +test = ["pytest", "pytest-django"] + [[package]] name = "decli" version = "0.6.1" @@ -391,6 +409,20 @@ django = ">=2.2,<5.0" [package.extras] docs = ["m2r2 (>=0.2.5,<0.3.0)", "sphinx (>=4.4,<5.0)", "sphinx_rtd_theme (>=1.0,<2.0)"] +[[package]] +name = "django-crispy-forms" +version = "2.1" +description = "Best way to have Django DRY forms" +optional = false +python-versions = ">=3.8" +files = [ + {file = "django-crispy-forms-2.1.tar.gz", hash = "sha256:4d7ec431933ad4d4b5c5a6de4a584d24613c347db9ac168723c9aaf63af4bb96"}, + {file = "django_crispy_forms-2.1-py3-none-any.whl", hash = "sha256:d592044771412ae1bd539cc377203aa61d4eebe77fcbc07fbc8f12d3746d4f6b"}, +] + +[package.dependencies] +django = ">=4.2" + [[package]] name = "django-environ" version = "0.11.2" @@ -1310,4 +1342,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "d901e40c2a11996fcb56bee04219ee99c58261a9d9944173dcbccc0c7541df76" +content-hash = "0245fa06fc392f70e6e790d909f0c7fcbe72b91c39cbb5576d89707f1d66a8c7" diff --git a/pyproject.toml b/pyproject.toml index 31228dd..4f96f4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,8 @@ psycopg2 = "^2.9.7" django-environ = "^0.11.2" django-guardian = "^2.4.0" django-bootstrap-v5 = "^1.0.11" +django-crispy-forms = "^2.1" +crispy-bootstrap5 = "^2023.10" [tool.poetry.group.dev.dependencies] pre-commit = "^3.4.0" diff --git a/server/settings/base.py b/server/settings/base.py index b9f04be..7400bfd 100644 --- a/server/settings/base.py +++ b/server/settings/base.py @@ -74,6 +74,8 @@ THIRD_PARTY_APPS = [ "guardian", "bootstrap5", + "crispy_forms", + "crispy_bootstrap5", ] LOCAL_APPS = [ @@ -153,3 +155,11 @@ ##################### ANONYMOUS_USER_NAME = None + +################## +# Crispy Forms # +################## + +CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" + +CRISPY_TEMPLATE_PACK = "bootstrap5" diff --git a/setup.cfg b/setup.cfg index ff1b72f..53db72d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,6 +25,7 @@ max-complexity = 5 # These files can't be tested, so we exclude them from coverage. # We also exclude the manage.py file, since it's not a part of the app. omit = + apps/*/tests.py server/asgi.py server/wsgi.py server/settings/__init__.py diff --git a/templates/registration/login.html b/templates/registration/login.html index 069a545..1f3b94c 100644 --- a/templates/registration/login.html +++ b/templates/registration/login.html @@ -3,36 +3,40 @@ {% block title %}Login{% endblock title %} {% block content %} -{% if form.errors %} -

Your username and password didn't match. Please try again.

-{% endif %} {% if next %} - {% if user.is_authenticated %} -

Your account doesn't have access to this page. To proceed, - please login with an account that has access.

- {% else %} -

Please login to see this page.

- {% endif %} + {% if user.is_authenticated %} +

+ Your account doesn't have access to this page. To proceed, please login + with an account that has access. +

+ {% else %} +

+ Please login to see this page. +

+ {% endif %} {% endif %} +{% load crispy_forms_tags %} +
{% csrf_token %} - - - - - - - - - -
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
- - - + +
+
+
+
+ Sign in to your account +
+
+ {{ form|crispy }} + + + +
+
+
+
-{# Assumes you set up the password_reset view in your URLconf #} -

Lost password?

{% endblock %} diff --git a/templates/registration/register.html b/templates/registration/register.html index 13066fb..bde20b2 100644 --- a/templates/registration/register.html +++ b/templates/registration/register.html @@ -3,24 +3,38 @@ {% block title %}Register{% endblock title %} {% block content %} -
-
-
-
-
Register
-
-
- {% csrf_token %} - {% for field in form%} - {{field.label}} - {{field}} - {% endfor %} - - {{form.errors}} -
-
+ +{% if next %} + {% if user.is_authenticated %} +

+ Your account doesn't have access to this page. To proceed, please login + with an account that has access. +

+ {% else %} +

+ Please login to see this page. +

+ {% endif %} +{% endif %} + +{% load crispy_forms_tags %} + +
+ {% csrf_token %} + +
+
+
+
Register a new account
+
+ {{ form|crispy }} + + +
+
+ {% endblock content %}