From 993a34634f5324bff647ee55864164788e804f88 Mon Sep 17 00:00:00 2001 From: LuizaMaluf Date: Mon, 13 Nov 2023 21:47:55 -0300 Subject: [PATCH 01/20] feat(validators): contest admin form validator feat(validators): contest admin form validator - fix errors feat(validators): contest admin form validator - fix errors.2 feat(validators): contest admin form validator - fix errors.3 feat(validators): contest admin form validator test --- .gitignore | 2 +- apps/contests/models.py | 9 +++++++++ apps/contests/tests.py | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 68bc17f..979488d 100644 --- a/.gitignore +++ b/.gitignore @@ -84,7 +84,7 @@ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: +# intended to in multiple environments; otherwise, check them in: # .python-version # pipenv diff --git a/apps/contests/models.py b/apps/contests/models.py index 04570cb..52fceeb 100644 --- a/apps/contests/models.py +++ b/apps/contests/models.py @@ -1,3 +1,6 @@ +from typing import Any + +from django.core.exceptions import ValidationError from django.db.models import ( BooleanField, CharField, @@ -42,3 +45,9 @@ def status(self) -> ContestStatus: return ContestStatus.FINISHED else: return ContestStatus.RUNNING + + def clean(self) -> Any: + super().clean() + + 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..a5644bc 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 @@ -9,6 +10,7 @@ from apps.contests.admin import ContestAdmin, ContestModelForm from apps.contests.enums import ContestStatus from apps.contests.models import Contest +from apps.users.models import User class ContestTestCase(TestCase): @@ -18,6 +20,7 @@ def setUp(self) -> None: description="This is a test contest", cancelled=False, ) + self.user = User(username="test_user") def test_status_pending(self) -> None: self.contest.start_time = timezone.now() + timedelta(hours=1) @@ -38,6 +41,20 @@ def test_status_cancelled(self) -> None: self.contest.cancelled = True self.assertEqual(self.contest.status, ContestStatus.CANCELLED) + def test_clean_method(self) -> None: + invalid_contest = Contest( + title="Invalid Contest", + description="This contest has invalid times", + start_time=timezone.now() + timedelta(days=2), + end_time=timezone.now() + timedelta(days=1), + ) + with self.assertRaises(ValidationError) as context: + invalid_contest.clean() + + self.assertEqual( + context.exception.messages, ["Start time must be before end time."] + ) + class ContestStatusTestCase(TestCase): def test_pending(self) -> None: From 189b27e29bbe2715aa64d51f7774a2b38b0da7e5 Mon Sep 17 00:00:00 2001 From: LuizaMaluf Date: Tue, 14 Nov 2023 19:44:17 -0300 Subject: [PATCH 02/20] feat(validators): contest status form validator feat(validators): contest status form validator tests feat(validators): contest status form validator tests feat(validators): contest status form validator tests --- apps/contests/enums.py | 15 +++++++++++++++ apps/contests/tests.py | 14 ++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/apps/contests/enums.py b/apps/contests/enums.py index e7f34b5..29df77b 100644 --- a/apps/contests/enums.py +++ b/apps/contests/enums.py @@ -1,4 +1,5 @@ from enum import StrEnum +from typing import Any class ContestStatus(StrEnum): @@ -6,3 +7,17 @@ class ContestStatus(StrEnum): RUNNING = "Running" FINISHED = "Finished" CANCELLED = "Cancelled" + + def validate_contest_status(status) -> Any: + if not isinstance(status, str): + raise ValueError("O status do concurso deve ser uma string.") + + try: + _ = ContestStatus(status) + except ValueError: + valid_values = ", ".join(e.value for e in ContestStatus) + error_message = ( + f"Status do concurso inválido: {status}. " + f"Deve ser um dos valores: {valid_values}" + ) + raise ValueError(error_message) diff --git a/apps/contests/tests.py b/apps/contests/tests.py index a5644bc..0fc8f9d 100644 --- a/apps/contests/tests.py +++ b/apps/contests/tests.py @@ -69,6 +69,20 @@ def test_finished(self) -> None: def test_cancelled(self) -> None: self.assertEqual(ContestStatus.CANCELLED, "Cancelled") + def test_valid_status(self) -> None: + valid_statuses = [ + ContestStatus.PENDING, + ContestStatus.RUNNING, + ContestStatus.FINISHED, + ContestStatus.CANCELLED, + ] + + for status in valid_statuses: + with self.subTest(status=status): + self.assertIsNone( + ContestStatus.validate_contest_status(status) + ) + class ContestModelFormTestCase(TestCase): def test_description_field_widget(self) -> None: From affb0a3b3ea51dd6a19e785f6aa2c133bce0443a Mon Sep 17 00:00:00 2001 From: thegm445 Date: Fri, 10 Nov 2023 07:54:30 -0300 Subject: [PATCH 03/20] =?UTF-8?q?docs(remove-name):=20name=20removed=20fro?= =?UTF-8?q?m=20index.rst=20[introdu=C3=A7=C3=A3o]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/index.rst | 1 - 1 file changed, 1 deletion(-) 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 `_ ================ ========= ====================================================== From dd6ab9dcf96a10fffce2424e9619ffb05fde6453 Mon Sep 17 00:00:00 2001 From: thegm445 Date: Fri, 10 Nov 2023 07:55:11 -0300 Subject: [PATCH 04/20] docs(remove-name): name removed from README.md --- README.md | 1 - 1 file changed, 1 deletion(-) 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 From e068398d7a1febcf17b142f6057c6f0592a04464 Mon Sep 17 00:00:00 2001 From: thegm445 Date: Wed, 8 Nov 2023 12:27:18 -0300 Subject: [PATCH 05/20] docs(sprint-03): add initial file for docs refering to the fourthsprint --- docs/source/scrum/sprints/sprint-3.rst | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/source/scrum/sprints/sprint-3.rst diff --git a/docs/source/scrum/sprints/sprint-3.rst b/docs/source/scrum/sprints/sprint-3.rst new file mode 100644 index 0000000..e69de29 From f586355e180783b98ed31583334d628603e77791 Mon Sep 17 00:00:00 2001 From: thegm445 Date: Fri, 10 Nov 2023 07:39:48 -0300 Subject: [PATCH 06/20] docs(sprint-03): add some issues that were in other milestones --- docs/source/scrum/sprints/sprint-3.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/source/scrum/sprints/sprint-3.rst b/docs/source/scrum/sprints/sprint-3.rst index e69de29..7732c1b 100644 --- a/docs/source/scrum/sprints/sprint-3.rst +++ b/docs/source/scrum/sprints/sprint-3.rst @@ -0,0 +1,18 @@ +Sprint 3 +======== + +:bdg-info:`04/11/2023` + +Resumo +------ + +Esta sprint teve como objetivo finalizar e polir o projeto, visto que esta +sprint antecedeu a primeira release do projeto. + +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) `_ From cb9fef531a44ff6f55c50fe66a2cdfff7564cd38 Mon Sep 17 00:00:00 2001 From: thegm445 Date: Fri, 10 Nov 2023 07:44:58 -0300 Subject: [PATCH 07/20] docs(sprint-03): add reference on intro.rst --- docs/source/scrum/intro.rst | 1 + 1 file changed, 1 insertion(+) 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 From 9413abfc7e2852b52abfe407dc45db1a1b3d57f1 Mon Sep 17 00:00:00 2001 From: thegm445 Date: Mon, 13 Nov 2023 21:50:17 -0300 Subject: [PATCH 08/20] docs(sprint-03): brief sprint description has been rewritten --- docs/source/scrum/sprints/sprint-3.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/source/scrum/sprints/sprint-3.rst b/docs/source/scrum/sprints/sprint-3.rst index 7732c1b..070e97e 100644 --- a/docs/source/scrum/sprints/sprint-3.rst +++ b/docs/source/scrum/sprints/sprint-3.rst @@ -6,8 +6,10 @@ Sprint 3 Resumo ------ -Esta sprint teve como objetivo finalizar e polir o projeto, visto que esta -sprint antecedeu a primeira release do projeto. +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 ---------- From a7c9d22f483af4cae288d8e3b1f3426e3f8f30f5 Mon Sep 17 00:00:00 2001 From: HladczukLe Date: Mon, 13 Nov 2023 19:48:22 -0300 Subject: [PATCH 09/20] feat(LoginTemplate): add style to login page --- templates/registration/login.html | 39 +++++++++++++++++-------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/templates/registration/login.html b/templates/registration/login.html index 069a545..ba22941 100644 --- a/templates/registration/login.html +++ b/templates/registration/login.html @@ -17,22 +17,27 @@ {% endif %}
- {% csrf_token %} - - - - - - - - - -
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
- - - +
+
+
+
Login
+
+ {% csrf_token %} +
+ + {{ form.username }} +
+
+ + {{ form.password }} +
+ + + {# Assumes you set up the password_reset view in your URLconf #} +

Lost password?

+
+
+
+
- -{# Assumes you set up the password_reset view in your URLconf #} -

Lost password?

{% endblock %} From f9c0ed63fa729dd44e8323a75cbb3eefb9a87c12 Mon Sep 17 00:00:00 2001 From: kyomi Date: Tue, 14 Nov 2023 21:51:07 -0300 Subject: [PATCH 10/20] feat(templates/registration): make the forms look better fix(templates/registration): remove unecessary code --- poetry.lock | 34 +++++++++++++++++++- pyproject.toml | 2 ++ server/settings/base.py | 10 ++++++ templates/registration/login.html | 48 +++++++++++++--------------- templates/registration/register.html | 45 ++++++++++++++++---------- 5 files changed, 97 insertions(+), 42 deletions(-) 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/templates/registration/login.html b/templates/registration/login.html index ba22941..7c0382a 100644 --- a/templates/registration/login.html +++ b/templates/registration/login.html @@ -3,38 +3,36 @@ {% 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 %} + +
+
-
Login
+
+ Sign in to your account +
- {% csrf_token %} -
- - {{ form.username }} -
-
- - {{ form.password }} -
- - - {# Assumes you set up the password_reset view in your URLconf #} -

Lost password?

+ {{ form|crispy }} + + +
diff --git a/templates/registration/register.html b/templates/registration/register.html index 13066fb..be522c4 100644 --- a/templates/registration/register.html +++ b/templates/registration/register.html @@ -3,24 +3,37 @@ {% 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 %} From 8188fbc7bbacefe9ae806fdb2eb7f37e72cda026 Mon Sep 17 00:00:00 2001 From: kyomi Date: Tue, 14 Nov 2023 21:53:28 -0300 Subject: [PATCH 11/20] style(templates/registration): add an extra space between the form and the template tag --- templates/registration/login.html | 1 + templates/registration/register.html | 1 + 2 files changed, 2 insertions(+) diff --git a/templates/registration/login.html b/templates/registration/login.html index 7c0382a..1f3b94c 100644 --- a/templates/registration/login.html +++ b/templates/registration/login.html @@ -38,4 +38,5 @@
+ {% endblock %} diff --git a/templates/registration/register.html b/templates/registration/register.html index be522c4..bde20b2 100644 --- a/templates/registration/register.html +++ b/templates/registration/register.html @@ -36,4 +36,5 @@
+ {% endblock content %} From 249f980022095b1479a5e31acc8df3e049212a67 Mon Sep 17 00:00:00 2001 From: kyomi Date: Tue, 14 Nov 2023 22:18:26 -0300 Subject: [PATCH 12/20] revert: revert gitignore change --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 979488d..68bc17f 100644 --- a/.gitignore +++ b/.gitignore @@ -84,7 +84,7 @@ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is -# intended to in multiple environments; otherwise, check them in: +# intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv From 89b15a02660fb8fcfb559ccc117eeb59681bf077 Mon Sep 17 00:00:00 2001 From: LuizaMaluf Date: Tue, 14 Nov 2023 22:18:45 -0300 Subject: [PATCH 13/20] feat(validators): contest status form validator tests --- apps/contests/views.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/contests/views.py b/apps/contests/views.py index cf6b047..fd6ade2 100644 --- a/apps/contests/views.py +++ b/apps/contests/views.py @@ -1,6 +1,8 @@ from typing import TYPE_CHECKING, Any +from django.core.exceptions import ValidationError from django.db.models.query import QuerySet +from django.utils import timezone from django.views import generic from apps.contests.enums import ContestStatus @@ -19,7 +21,12 @@ class IndexView(IndexViewBase): context_object_name = "contests" def get_queryset(self) -> QuerySet[Contest]: - return Contest._default_manager.order_by("start_time")[:5] + queryset = Contest._default_manager.filter( + start_time__gte=timezone.now() + ).order_by("start_time")[:5] + if not queryset.exists(): + raise ValidationError("Não há concursos futuros disponíveis.") + return queryset def get_context_data(self, **kwargs: Any) -> dict[str, Any]: ctx = super().get_context_data(**kwargs) From 12c69c9627bd95cb744f9d792e8211096ead8c79 Mon Sep 17 00:00:00 2001 From: LuizaMaluf Date: Tue, 14 Nov 2023 22:19:59 -0300 Subject: [PATCH 14/20] feat(validators): contest views validator --- apps/contests/views.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/contests/views.py b/apps/contests/views.py index fd6ade2..c9ad155 100644 --- a/apps/contests/views.py +++ b/apps/contests/views.py @@ -30,7 +30,14 @@ def get_queryset(self) -> QuerySet[Contest]: def get_context_data(self, **kwargs: Any) -> dict[str, Any]: ctx = super().get_context_data(**kwargs) - ctx["valid_statuses"] = (ContestStatus.PENDING, ContestStatus.RUNNING) + + valid_statuses = (ContestStatus.PENDING, ContestStatus.RUNNING) + if not all(status in ContestStatus for status in valid_statuses): + raise ValidationError( + "Defina pelo menos dois estados de concurso válidos." + ) + + ctx["valid_statuses"] = valid_statuses return ctx From 9996f17458d6dc490b1debb81f0380f0efff8c64 Mon Sep 17 00:00:00 2001 From: LuizaMaluf Date: Tue, 14 Nov 2023 22:34:57 -0300 Subject: [PATCH 15/20] feat(validators): contest views validator --- apps/contests/views.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/contests/views.py b/apps/contests/views.py index c9ad155..9da5444 100644 --- a/apps/contests/views.py +++ b/apps/contests/views.py @@ -25,7 +25,7 @@ def get_queryset(self) -> QuerySet[Contest]: start_time__gte=timezone.now() ).order_by("start_time")[:5] if not queryset.exists(): - raise ValidationError("Não há concursos futuros disponíveis.") + raise ValidationError("There are no future contests available.") return queryset def get_context_data(self, **kwargs: Any) -> dict[str, Any]: @@ -33,9 +33,7 @@ def get_context_data(self, **kwargs: Any) -> dict[str, Any]: valid_statuses = (ContestStatus.PENDING, ContestStatus.RUNNING) if not all(status in ContestStatus for status in valid_statuses): - raise ValidationError( - "Defina pelo menos dois estados de concurso válidos." - ) + raise ValidationError("Define at least two valid contest states.") ctx["valid_statuses"] = valid_statuses From e5ada46c7c38d3f33bc820f57bc9a75881cceb11 Mon Sep 17 00:00:00 2001 From: kyomi Date: Wed, 15 Nov 2023 08:12:21 -0300 Subject: [PATCH 16/20] fix(apps/contests): fix model validation return type --- apps/contests/models.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/contests/models.py b/apps/contests/models.py index 52fceeb..8cc589b 100644 --- a/apps/contests/models.py +++ b/apps/contests/models.py @@ -1,5 +1,3 @@ -from typing import Any - from django.core.exceptions import ValidationError from django.db.models import ( BooleanField, @@ -46,8 +44,6 @@ def status(self) -> ContestStatus: else: return ContestStatus.RUNNING - def clean(self) -> Any: - super().clean() - + def clean(self) -> None: if self.start_time > self.end_time: raise ValidationError("Start time must be before end time.") From b8285b2a3f1b295bedf4ce1c909b751ac3700f55 Mon Sep 17 00:00:00 2001 From: kyomi Date: Wed, 15 Nov 2023 08:51:14 -0300 Subject: [PATCH 17/20] fix(apps/contests): fix redundant methods and tests --- apps/contests/enums.py | 15 -------- apps/contests/tests.py | 79 +++++++++++++++++++----------------------- apps/contests/views.py | 27 +++++---------- 3 files changed, 45 insertions(+), 76 deletions(-) diff --git a/apps/contests/enums.py b/apps/contests/enums.py index 29df77b..e7f34b5 100644 --- a/apps/contests/enums.py +++ b/apps/contests/enums.py @@ -1,5 +1,4 @@ from enum import StrEnum -from typing import Any class ContestStatus(StrEnum): @@ -7,17 +6,3 @@ class ContestStatus(StrEnum): RUNNING = "Running" FINISHED = "Finished" CANCELLED = "Cancelled" - - def validate_contest_status(status) -> Any: - if not isinstance(status, str): - raise ValueError("O status do concurso deve ser uma string.") - - try: - _ = ContestStatus(status) - except ValueError: - valid_values = ", ".join(e.value for e in ContestStatus) - error_message = ( - f"Status do concurso inválido: {status}. " - f"Deve ser um dos valores: {valid_values}" - ) - raise ValueError(error_message) diff --git a/apps/contests/tests.py b/apps/contests/tests.py index 0fc8f9d..ab4e393 100644 --- a/apps/contests/tests.py +++ b/apps/contests/tests.py @@ -10,17 +10,11 @@ from apps.contests.admin import ContestAdmin, ContestModelForm from apps.contests.enums import ContestStatus from apps.contests.models import Contest -from apps.users.models import User -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.user = User(username="test_user") + self.contest = Contest(title="Test", description="Test contest") def test_status_pending(self) -> None: self.contest.start_time = timezone.now() + timedelta(hours=1) @@ -41,47 +35,46 @@ def test_status_cancelled(self) -> None: self.contest.cancelled = True self.assertEqual(self.contest.status, ContestStatus.CANCELLED) - def test_clean_method(self) -> None: - invalid_contest = Contest( - title="Invalid Contest", - description="This contest has invalid times", - start_time=timezone.now() + timedelta(days=2), - end_time=timezone.now() + timedelta(days=1), - ) - with self.assertRaises(ValidationError) as context: - invalid_contest.clean() - - self.assertEqual( - context.exception.messages, ["Start time must be before end time."] - ) - + 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_start_time_after_end_time(self) -> None: + """ + Se o :attr:`start_time` for maior que o :attr:`end_time` (ou + seja, o contest começa depois de terminar), devemos lançar um + :class:`ValidationError`. + """ + self.contest.start_time = timezone.now() + timedelta(hours=1) + self.contest.end_time = timezone.now() - def test_running(self) -> None: - self.assertEqual(ContestStatus.RUNNING, "Running") + expected = ["Start time must be before end time."] - def test_finished(self) -> None: - self.assertEqual(ContestStatus.FINISHED, "Finished") + with self.assertRaises(ValidationError) as context: + self.contest.clean() - def test_cancelled(self) -> None: - self.assertEqual(ContestStatus.CANCELLED, "Cancelled") + self.assertEqual(context.exception.messages, expected) - def test_valid_status(self) -> None: - valid_statuses = [ - ContestStatus.PENDING, - ContestStatus.RUNNING, - ContestStatus.FINISHED, - ContestStatus.CANCELLED, - ] - for status in valid_statuses: - with self.subTest(status=status): - self.assertIsNone( - ContestStatus.validate_contest_status(status) - ) +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 9da5444..865e76a 100644 --- a/apps/contests/views.py +++ b/apps/contests/views.py @@ -1,8 +1,6 @@ -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Dict -from django.core.exceptions import ValidationError from django.db.models.query import QuerySet -from django.utils import timezone from django.views import generic from apps.contests.enums import ContestStatus @@ -21,22 +19,15 @@ class IndexView(IndexViewBase): context_object_name = "contests" def get_queryset(self) -> QuerySet[Contest]: - queryset = Contest._default_manager.filter( - start_time__gte=timezone.now() - ).order_by("start_time")[:5] - if not queryset.exists(): - raise ValidationError("There are no future contests available.") - return queryset - - def get_context_data(self, **kwargs: Any) -> dict[str, Any]: - ctx = super().get_context_data(**kwargs) - - valid_statuses = (ContestStatus.PENDING, ContestStatus.RUNNING) - if not all(status in ContestStatus for status in valid_statuses): - raise ValidationError("Define at least two valid contest states.") - - ctx["valid_statuses"] = valid_statuses + return Contest._default_manager.order_by("-start_time")[:5] + 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 From 5298df5e667fe15aa905260c76a11b1a8c0dba25 Mon Sep 17 00:00:00 2001 From: kyomi Date: Wed, 15 Nov 2023 09:01:13 -0300 Subject: [PATCH 18/20] docs(apps/contests): add some documentation for confusing methods/properties --- apps/contests/models.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/apps/contests/models.py b/apps/contests/models.py index 8cc589b..3e6f590 100644 --- a/apps/contests/models.py +++ b/apps/contests/models.py @@ -34,10 +34,31 @@ 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 @@ -45,5 +66,15 @@ def status(self) -> ContestStatus: 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.") From 94b99cf5e957c535cb2fa34a9ee8b2f6a4b8da38 Mon Sep 17 00:00:00 2001 From: kyomi Date: Wed, 15 Nov 2023 09:07:37 -0300 Subject: [PATCH 19/20] chore(coverage.py): do not report tests files --- setup.cfg | 1 + 1 file changed, 1 insertion(+) 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 From 1d1de6d4c35f8f2711e8d9b1e0aca7b5c60439aa Mon Sep 17 00:00:00 2001 From: kyomi Date: Wed, 15 Nov 2023 09:12:42 -0300 Subject: [PATCH 20/20] docs(apps/contests): remove redundant documentation --- apps/contests/tests.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/contests/tests.py b/apps/contests/tests.py index ab4e393..3322a2d 100644 --- a/apps/contests/tests.py +++ b/apps/contests/tests.py @@ -45,11 +45,6 @@ def test_start_time_before_end_time(self) -> None: self.fail("ValidationError raised unexpectedly.") def test_start_time_after_end_time(self) -> None: - """ - Se o :attr:`start_time` for maior que o :attr:`end_time` (ou - seja, o contest começa depois de terminar), devemos lançar um - :class:`ValidationError`. - """ self.contest.start_time = timezone.now() + timedelta(hours=1) self.contest.end_time = timezone.now()