diff --git a/README.md b/README.md index a11421d..e0d2249 100644 --- a/README.md +++ b/README.md @@ -1 +1,27 @@ # 2023-2-Squad06 + +# Guia de instalação + +## Resumo + +Para a instalação e a operação corretas, deve se ter instalado na máquina: +- Python versão 3.11.5 +- Poetry versão 1.6.1 + +Após verificar quanto aos requisitos acima, rode estes comandos: + +- `poetry install` +- Se necessárias dependências de documentação, `poetry install --with docs` +- Para instalar Git Hooks: + ```bash + poetry run pre-commit install \ + --hook-type commit-msg \ + --hook-type pre-commit \ + --hook-type pre-push + ``` +- Gerar o arquivo config `poetry run ./bin/create-env` +- Para finalizar a instalação e conseguir visualizar a página: + + - `docker compose build && docker compose up -d` + - `docker compose run django python manage.py migrate` + - `docker compose run django python manage.py createsuperuser` diff --git a/apps/templates/.keep b/apps/__init__.py similarity index 100% rename from apps/templates/.keep rename to apps/__init__.py diff --git a/apps/contests/__init__.py b/apps/contests/__init__.py new file mode 100644 index 0000000..b58563e --- /dev/null +++ b/apps/contests/__init__.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + +default_app_config = "apps.contests.ContestsConfig" + + +class ContestsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.contests" diff --git a/apps/contests/admin.py b/apps/contests/admin.py new file mode 100644 index 0000000..04b81c2 --- /dev/null +++ b/apps/contests/admin.py @@ -0,0 +1,35 @@ +from typing import TYPE_CHECKING + +from django.contrib.admin import ModelAdmin, register +from django.forms import CharField, ModelForm, Textarea +from django.utils.translation import gettext_lazy as _ + +from apps.contests.models import Contest + +if TYPE_CHECKING: + ContestAdminBase = ModelAdmin[Contest] + ContestModelFormBase = ModelForm[Contest] +else: + ContestAdminBase = ModelAdmin + ContestModelFormBase = ModelForm + + +class ContestModelForm(ContestModelFormBase): + description = CharField(widget=Textarea(attrs={"rows": 14, "cols": 80})) + + class Meta: + model = Contest + fields = "__all__" + + +@register(Contest) +class ContestAdmin(ContestAdminBase): + form = ContestModelForm + + list_display = ("title", "start_time", "end_time", "status") + list_filter = ("start_time", "end_time") + + fieldsets = [ + (_("General"), {"fields": ("title", "description")}), + (_("Other"), {"fields": ("start_time", "end_time", "cancelled")}), + ] diff --git a/apps/contests/enums.py b/apps/contests/enums.py new file mode 100644 index 0000000..e7f34b5 --- /dev/null +++ b/apps/contests/enums.py @@ -0,0 +1,8 @@ +from enum import StrEnum + + +class ContestStatus(StrEnum): + PENDING = "Pending" + RUNNING = "Running" + FINISHED = "Finished" + CANCELLED = "Cancelled" diff --git a/apps/contests/migrations/0001_initial.py b/apps/contests/migrations/0001_initial.py new file mode 100644 index 0000000..ddc6601 --- /dev/null +++ b/apps/contests/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 4.2.5 on 2023-09-30 05:03 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Contest", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("title", models.CharField(max_length=256)), + ("description", models.CharField(max_length=1024)), + ("start_time", models.DateTimeField()), + ("end_time", models.DateTimeField()), + ( + "users", + models.ManyToManyField( + related_name="contests", to=settings.AUTH_USER_MODEL + ), + ), + ], + options={ + "db_table": "contests", + }, + ), + ] diff --git a/apps/contests/migrations/0002_contest_cancelled.py b/apps/contests/migrations/0002_contest_cancelled.py new file mode 100644 index 0000000..b22a598 --- /dev/null +++ b/apps/contests/migrations/0002_contest_cancelled.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.5 on 2023-09-30 05:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("contests", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="contest", + name="cancelled", + field=models.BooleanField(default=False), + ), + ] diff --git a/apps/contests/migrations/__init__.py b/apps/contests/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/contests/models.py b/apps/contests/models.py new file mode 100644 index 0000000..04570cb --- /dev/null +++ b/apps/contests/models.py @@ -0,0 +1,44 @@ +from django.db.models import ( + BooleanField, + CharField, + DateTimeField, + ManyToManyField, +) +from django.utils.timezone import now + +from apps.contests.enums import ContestStatus +from apps.users.models import User +from core.models import TimestampedModel + + +class Contest(TimestampedModel): + """Represents a contest.""" + + id: int + + title = CharField(max_length=256) + description = CharField(max_length=1024) + + start_time = DateTimeField() + end_time = DateTimeField() + cancelled = BooleanField(default=False) + + users = ManyToManyField(User, related_name="contests") + + class Meta: + db_table = "contests" + + def __str__(self) -> str: + return f"{self.title} #{self.id}" + + @property + def status(self) -> ContestStatus: + if self.cancelled: + return ContestStatus.CANCELLED + + if self.start_time > now(): + return ContestStatus.PENDING + elif self.end_time < now(): + return ContestStatus.FINISHED + else: + return ContestStatus.RUNNING diff --git a/apps/contests/urls.py b/apps/contests/urls.py new file mode 100644 index 0000000..9d31b9b --- /dev/null +++ b/apps/contests/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from apps.contests.views import DetailView + +app_name = "contests" + +urlpatterns = [ + path("/", DetailView.as_view(), name="detail"), +] diff --git a/apps/contests/views.py b/apps/contests/views.py new file mode 100644 index 0000000..cf6b047 --- /dev/null +++ b/apps/contests/views.py @@ -0,0 +1,33 @@ +from typing import TYPE_CHECKING, Any + +from django.db.models.query import QuerySet +from django.views import generic + +from apps.contests.enums import ContestStatus +from apps.contests.models import Contest + +if TYPE_CHECKING: + IndexViewBase = generic.ListView[Contest] + DetailViewBase = generic.DetailView[Contest] +else: + IndexViewBase = generic.ListView + DetailViewBase = generic.DetailView + + +class IndexView(IndexViewBase): + template_name = "contests/index.html" + context_object_name = "contests" + + def get_queryset(self) -> QuerySet[Contest]: + 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) + ctx["valid_statuses"] = (ContestStatus.PENDING, ContestStatus.RUNNING) + + return ctx + + +class DetailView(DetailViewBase): + model = Contest + template_name = "contests/detail.html" diff --git a/apps/problems/__init__.py b/apps/problems/__init__.py new file mode 100644 index 0000000..4dc04c0 --- /dev/null +++ b/apps/problems/__init__.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + +default_app_config = "apps.problems.ProblemsConfig" + + +class ProblemsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.problems" diff --git a/apps/problems/admin.py b/apps/problems/admin.py new file mode 100644 index 0000000..77e46f9 --- /dev/null +++ b/apps/problems/admin.py @@ -0,0 +1,44 @@ +from typing import TYPE_CHECKING + +from django.contrib.admin import ModelAdmin, register +from django.forms import CharField, IntegerField, ModelForm, Textarea +from django.utils.translation import gettext_lazy as _ + +from apps.problems.models import Problem + +if TYPE_CHECKING: + ProblemAdminBase = ModelAdmin[Problem] + ProblemModelFormBase = ModelForm[Problem] +else: + ProblemAdminBase = ModelAdmin + ProblemModelFormBase = ModelForm + + +class ProblemModelForm(ProblemModelFormBase): + description = CharField(widget=Textarea(attrs={"rows": 14, "cols": 80})) + score = IntegerField(min_value=0, required=False) + + memory_limit = IntegerField( + min_value=0, required=False, help_text=_("In bytes.") + ) + time_limit = IntegerField( + min_value=0, required=False, help_text=_("In seconds.") + ) + + class Meta: + model = Problem + fields = "__all__" + + +@register(Problem) +class ProblemAdmin(ProblemAdminBase): + form = ProblemModelForm + + list_display = ("title", "contest", "memory_limit", "time_limit") + list_filter = ("contest", "score") + + fieldsets = [ + (_("General"), {"fields": ("title", "description")}), + (_("Meta"), {"fields": ("contest", "score")}), + (_("Limits"), {"fields": ("memory_limit", "time_limit")}), + ] diff --git a/apps/problems/migrations/0001_initial.py b/apps/problems/migrations/0001_initial.py new file mode 100644 index 0000000..46dde8f --- /dev/null +++ b/apps/problems/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# Generated by Django 4.2.5 on 2023-10-03 12:48 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("contests", "0002_contest_cancelled"), + ] + + operations = [ + migrations.CreateModel( + name="Problem", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("title", models.CharField(max_length=256)), + ("description", models.CharField(max_length=4096)), + ( + "contest", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="problems", + to="contests.contest", + ), + ), + ], + options={ + "ordering": ["-created_at", "-updated_at"], + "abstract": False, + }, + ), + ] diff --git a/apps/problems/migrations/0002_alter_problem_options_alter_problem_table.py b/apps/problems/migrations/0002_alter_problem_options_alter_problem_table.py new file mode 100644 index 0000000..036ca10 --- /dev/null +++ b/apps/problems/migrations/0002_alter_problem_options_alter_problem_table.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.5 on 2023-10-03 17:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("problems", "0001_initial"), + ] + + operations = [ + migrations.AlterModelOptions( + name="problem", + options={}, + ), + migrations.AlterModelTable( + name="problem", + table="problems", + ), + ] diff --git a/apps/problems/migrations/0003_problem_memory_limit_problem_score_and_more.py b/apps/problems/migrations/0003_problem_memory_limit_problem_score_and_more.py new file mode 100644 index 0000000..c0e1bb9 --- /dev/null +++ b/apps/problems/migrations/0003_problem_memory_limit_problem_score_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.5 on 2023-10-04 22:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("problems", "0002_alter_problem_options_alter_problem_table"), + ] + + operations = [ + migrations.AddField( + model_name="problem", + name="memory_limit", + field=models.IntegerField(null=True), + ), + migrations.AddField( + model_name="problem", + name="score", + field=models.IntegerField(null=True), + ), + migrations.AddField( + model_name="problem", + name="time_limit", + field=models.IntegerField(null=True), + ), + ] diff --git a/apps/problems/migrations/__init__.py b/apps/problems/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/problems/models.py b/apps/problems/models.py new file mode 100644 index 0000000..54b53c2 --- /dev/null +++ b/apps/problems/models.py @@ -0,0 +1,31 @@ +from django.db.models import CASCADE, CharField, ForeignKey, IntegerField + +from apps.contests.enums import ContestStatus +from apps.contests.models import Contest +from core.models import TimestampedModel + + +class Problem(TimestampedModel): + """Represents a problem in a contest.""" + + title = CharField(max_length=256) + description = CharField(max_length=4096) + + contest = ForeignKey(Contest, related_name="problems", on_delete=CASCADE) + score = IntegerField(null=True) + + memory_limit = IntegerField(null=True) + time_limit = IntegerField(null=True) + + class Meta: + db_table = "problems" + + def __str__(self) -> str: + return self.title + + @property + def is_accessible(self) -> bool: + return self.contest.status in ( + ContestStatus.RUNNING, + ContestStatus.FINISHED, + ) diff --git a/apps/problems/urls.py b/apps/problems/urls.py new file mode 100644 index 0000000..0ada153 --- /dev/null +++ b/apps/problems/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from apps.problems.views import DetailView + +app_name = "problems" + +urlpatterns = [ + path("/", DetailView.as_view(), name="detail"), +] diff --git a/apps/problems/views.py b/apps/problems/views.py new file mode 100644 index 0000000..ebaff9b --- /dev/null +++ b/apps/problems/views.py @@ -0,0 +1,15 @@ +from typing import TYPE_CHECKING + +from django.views import generic + +from apps.problems.models import Problem + +if TYPE_CHECKING: + DetailViewBase = generic.DetailView[Problem] +else: + DetailViewBase = generic.DetailView + + +class DetailView(DetailViewBase): + model = Problem + template_name = "problems/detail.html" diff --git a/apps/users/__init__.py b/apps/users/__init__.py new file mode 100644 index 0000000..c38d98c --- /dev/null +++ b/apps/users/__init__.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + +default_app_config = "apps.users.UsersConfig" + + +class UsersConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.users" diff --git a/apps/users/admin.py b/apps/users/admin.py new file mode 100644 index 0000000..3a345a6 --- /dev/null +++ b/apps/users/admin.py @@ -0,0 +1,34 @@ +from django.contrib.admin import register +from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin +from django.utils.translation import gettext_lazy as _ + +from apps.users.models import User + + +@register(User) +class UserAdmin(DefaultUserAdmin): + list_display = ("username", "email", "is_staff", "is_active") + fieldsets = [ + (_("Personal info"), {"fields": ("username", "email", "password")}), + ( + _("Permissions"), + { + "fields": ( + "user_permissions", + "groups", + "is_active", + "is_staff", + "is_superuser", + ) + }, + ), + ] + add_fieldsets = ( + ( + None, + { + "classes": ("wide",), + "fields": ("username", "email", "password1", "password2"), + }, + ), + ) diff --git a/apps/users/managers.py b/apps/users/managers.py new file mode 100644 index 0000000..b40b715 --- /dev/null +++ b/apps/users/managers.py @@ -0,0 +1,41 @@ +from typing import TYPE_CHECKING, Any + +from django.contrib.auth.models import BaseUserManager + +if TYPE_CHECKING: + from apps.users.models import User +else: + User = Any + + +class UserManager(BaseUserManager[User]): + def _create_user( + self, + username: str, + email: str, + password: str, + **fields: bool, + ) -> User: + email = self.normalize_email(email) + user = self.model(username=username, email=email, **fields) + + user.set_password(password) + user.save() + + return user + + def create_user( + self, username: str, email: str, password: str, **fields: bool + ) -> User: + fields.setdefault("is_staff", False) + fields.setdefault("is_superuser", False) + + return self._create_user(username, email, password, **fields) + + def create_superuser( + self, username: str, email: str, password: str, **fields: bool + ) -> User: + fields["is_staff"] = True + fields["is_superuser"] = True + + return self._create_user(username, email, password, **fields) diff --git a/apps/users/migrations/0001_initial.py b/apps/users/migrations/0001_initial.py new file mode 100644 index 0000000..4e757db --- /dev/null +++ b/apps/users/migrations/0001_initial.py @@ -0,0 +1,87 @@ +# Generated by Django 4.2.5 on 2023-09-30 02:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "password", + models.CharField(max_length=128, verbose_name="password"), + ), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "email", + models.EmailField( + db_index=True, max_length=256, unique=True + ), + ), + ( + "username", + models.CharField( + db_index=True, max_length=128, unique=True + ), + ), + ("is_active", models.BooleanField(default=True)), + ("is_staff", models.BooleanField(default=False)), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "db_table": "users", + }, + ), + ] diff --git a/apps/users/migrations/__init__.py b/apps/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/users/models.py b/apps/users/models.py new file mode 100644 index 0000000..542ad76 --- /dev/null +++ b/apps/users/models.py @@ -0,0 +1,42 @@ +from typing import TYPE_CHECKING, Any, List + +from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin +from django.db.models import BooleanField, CharField, EmailField + +from apps.users.managers import UserManager +from core.models import TimestampedModel + +if TYPE_CHECKING: + from apps.contests.models import Contest +else: + Contest = Any + + +class User(AbstractBaseUser, PermissionsMixin, TimestampedModel): + """Represents an user.""" + + contests: List[Contest] + + email = EmailField(db_index=True, max_length=256, unique=True) + username = CharField(db_index=True, max_length=128, unique=True) + + # When a user no longer wishes to use our platform, they may try to + # delete there account. That's a problem for us because the data we + # collect is valuable to us and we don't want to delete it. To solve + # this problem, we will simply offer users a way to deactivate their + # account instead of letting them delete it. That way they won't + # show up on the site anymore, but we can still analyze the data. + is_active = BooleanField(default=True) + + # Designates whether the user can log into the admin site. + is_staff = BooleanField(default=False) + + # Telling Django that the email field should be used for + # authentication instead of the username field. + USERNAME_FIELD = "email" + REQUIRED_FIELDS = ["username"] + + objects = UserManager() + + class Meta: + db_table = "users" diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/models.py b/core/models.py new file mode 100644 index 0000000..8ae1e78 --- /dev/null +++ b/core/models.py @@ -0,0 +1,15 @@ +from django.db.models import DateTimeField, Model + + +class TimestampedModel(Model): + created_at = DateTimeField(auto_now_add=True) + updated_at = DateTimeField(auto_now=True) + + class Meta: + abstract = True + + # By default, any model that inherits from this class should be + # ordered in reverse-chronological order. We can override this + # on a per-model basis as needed, but reverse-chronological is a + # good default ordering for most models. + ordering = ["-created_at", "-updated_at"] diff --git a/docs/source/index.rst b/docs/source/index.rst index 282f6ac..2df465e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -47,3 +47,5 @@ Explorar :titlesonly: reunioes/intro + scrum/intro + instalation/intro diff --git a/docs/source/instalation/guia/guia01.rst b/docs/source/instalation/guia/guia01.rst new file mode 100644 index 0000000..ec4aad9 --- /dev/null +++ b/docs/source/instalation/guia/guia01.rst @@ -0,0 +1,29 @@ +Guia01 +====== + +:bdg-info:`HowTo` + +Resumo +------ + +Para a instalação e a operação corretas, deve se ter instalado na máquina: +- Python versão 3.11.5 +- Poetry versão 1.6.1 + +Após verificar quanto aos requisitos acima, rode estes comandos: + +- ``poetry install`` +- Se necessárias dependências de documentação, ``poetry install --with docs`` +- Para instalar Git Hooks: + .. code-block:: bash + + poetry run pre-commit install \ + --hook-type commit-msg \ + --hook-type pre-commit \ + --hook-type pre-push +- Gerar o arquivo config ``poetry run ./bin/create-env`` +- Para finalizar a instalação e conseguir visualizar a página: + + - ``docker compose build && docker compose up -d`` + - ``docker compose run django python manage.py migrate`` + - ``docker compose run django python manage.py createsuperuser`` diff --git a/docs/source/instalation/intro.rst b/docs/source/instalation/intro.rst new file mode 100644 index 0000000..fcec217 --- /dev/null +++ b/docs/source/instalation/intro.rst @@ -0,0 +1,20 @@ +Instalação do projeto +===================== + +.. rst-class:: lead + + Aqui você encontrará instruções e informações necessárias para a instalação + e execução do projeto. + +Sumário +------- + +- Guia de instalação no Linux (ambiente recomendado): Guia 01 + +Guias +----- + +.. toctree:: + :titlesonly: + + guia/guia01 diff --git a/docs/source/reunioes/atas/reuniao-02.rst b/docs/source/reunioes/atas/reuniao-02.rst new file mode 100644 index 0000000..e8c588d --- /dev/null +++ b/docs/source/reunioes/atas/reuniao-02.rst @@ -0,0 +1,12 @@ +Reunião 02 +========== + +:bdg-info:`02/10/2023` :bdg-warning:`Sprint 0` + +Resumo +------ + +Esta reunião teve como objetivo solucionar problemas que surgiram nos ambientes +durante a sprint 0. Teve como objetivo também, discutir o que foi feito na +primeira sprint, fechar as issues que já deveriam ter sido fechadas e planejar +o que deve ser feito na próxima sprint. diff --git a/docs/source/reunioes/atas/reuniao-03.rst b/docs/source/reunioes/atas/reuniao-03.rst new file mode 100644 index 0000000..70301d2 --- /dev/null +++ b/docs/source/reunioes/atas/reuniao-03.rst @@ -0,0 +1,11 @@ +Reunião 03 +========== + +:bdg-info:`09/10/2023` :bdg-warning:`Sprint 1` + +Resumo +------ + +Esta reunião teve como objetivos verficar o progresso nas tasks passadas no +início da sprint e solucionar problemas encontrados com a compatibilidade dos +ambientes. Reunião rápida, condizente com a metodologia usada. diff --git a/docs/source/reunioes/intro.rst b/docs/source/reunioes/intro.rst index 5f777f2..9dbd348 100644 --- a/docs/source/reunioes/intro.rst +++ b/docs/source/reunioes/intro.rst @@ -22,3 +22,5 @@ Atas :titlesonly: atas/reuniao-01 + atas/reuniao-02 + atas/reuniao-03 diff --git a/docs/source/scrum/intro.rst b/docs/source/scrum/intro.rst new file mode 100644 index 0000000..987830f --- /dev/null +++ b/docs/source/scrum/intro.rst @@ -0,0 +1,22 @@ +Metodologia Scrum +================= + +.. rst-class:: lead + + Aqui você encontrará informações sobre a metodologia de desenvolvimento do + projeto. + +Sumário +------- + +Utilizamos o framework scrum como metodologia de desenvolvimento do projeto, +onde cada sprint tem uma duração de aproximadamente uma semana. Em cada ciclo, são +desenvolvidas as demandas definidas para a sprint. + +Sprints +------- + +.. toctree:: + :titlesonly: + + sprints/sprint-0 diff --git a/docs/source/scrum/sprints/sprint-0.rst b/docs/source/scrum/sprints/sprint-0.rst new file mode 100644 index 0000000..39b3e81 --- /dev/null +++ b/docs/source/scrum/sprints/sprint-0.rst @@ -0,0 +1,27 @@ +Sprint 0 +======== + +:bdg-info:`02/10/2023` + +Resumo +------ + +Esta sprint teve como objetivo primário criar o ambiente inicial de +desenvolvimento, a definição do tema, separação dos papéis exercidos pelos +membros do grupo e a iniciação da documentação do projeto. + +Changelog +---------- + +- `Decidir o espaço e os horários das reuniões (#7) `_ +- `Relembrando Python/Django (#6) `_ +- `Como configuro meu ambiente? (#2) `_ +- `O que devo aprender? (#1) `_ +- `Definição do tema do projeto (#4) `_ +- `Tema 1: Juiz online (online judge) (#10) `_ +- `Tema 2: Site para avaliação de professores (#11) `_ +- `Tema 3: Um clone do Twitter [agora chamam de X, né?] (#12) `_ +- `Adicionar documentação inicial do projeto (#9) `_ +- `Desenvolvimento da documentação do projeto (#8) `_ +- `Estudo sobre metodologia ágil (#5) `_ +- `Estudo sobre Docker e Docker Compose (#3) `_ diff --git a/poetry.lock b/poetry.lock index 5858235..d51465a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -309,6 +309,24 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] +[[package]] +name = "django-bootstrap-v5" +version = "1.0.11" +description = "Bootstrap 5 support for Django projects" +optional = false +python-versions = ">=3.6,<4.0" +files = [ + {file = "django-bootstrap-v5-1.0.11.tar.gz", hash = "sha256:2d431308859ce3cab7729bb09c76039059cd5fbdd34484da82c4c7f8d49da3a2"}, + {file = "django_bootstrap_v5-1.0.11-py3-none-any.whl", hash = "sha256:a207aa804938164c8450bbbef4faaba6d2093b3236000557a50f3bd44b53d268"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.8.0,<5.0.0" +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-environ" version = "0.11.2" @@ -325,6 +343,20 @@ develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.dev0)", "py docs = ["furo (>=2021.8.17b43,<2021.9.dev0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] +[[package]] +name = "django-guardian" +version = "2.4.0" +description = "Implementation of per object permissions for Django." +optional = false +python-versions = ">=3.5" +files = [ + {file = "django-guardian-2.4.0.tar.gz", hash = "sha256:c58a68ae76922d33e6bdc0e69af1892097838de56e93e78a8361090bcd9f89a0"}, + {file = "django_guardian-2.4.0-py3-none-any.whl", hash = "sha256:440ca61358427e575323648b25f8384739e54c38b3d655c81d75e0cd0d61b697"}, +] + +[package.dependencies] +Django = ">=2.2" + [[package]] name = "django-stubs" version = "4.2.4" @@ -340,7 +372,7 @@ files = [ django = "*" django-stubs-ext = ">=4.2.2" mypy = [ - {version = ">=1.0.0", optional = true, markers = "extra != \"compatible-mypy\""}, + {version = ">=1.0.0"}, {version = "==1.5.*", optional = true, markers = "extra == \"compatible-mypy\""}, ] types-pytz = "*" @@ -524,6 +556,16 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -774,6 +816,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -781,8 +824,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -799,6 +849,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -806,6 +857,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1150,13 +1202,13 @@ files = [ [[package]] name = "urllib3" -version = "2.0.5" +version = "2.0.6" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.7" files = [ - {file = "urllib3-2.0.5-py3-none-any.whl", hash = "sha256:ef16afa8ba34a1f989db38e1dbbe0c302e4289a47856990d0682e374563ce35e"}, - {file = "urllib3-2.0.5.tar.gz", hash = "sha256:13abf37382ea2ce6fb744d4dad67838eec857c9f4f57009891805e0b5e123594"}, + {file = "urllib3-2.0.6-py3-none-any.whl", hash = "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2"}, + {file = "urllib3-2.0.6.tar.gz", hash = "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564"}, ] [package.extras] @@ -1214,4 +1266,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "967a300f6e56903859bf57de51046e3b4a61148fa292662d8da1b7e420727b3c" +content-hash = "3a8f0148cc9385bc37f6705a756bbc9fbeed1bb3b7c44c7b65e0b34f63dfcdc6" diff --git a/pyproject.toml b/pyproject.toml index d47c19c..f319d12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "2023-2-squad06" +name = "mds" version = "0.1.0" description = "" authors = ["Your Name "] @@ -11,6 +11,8 @@ python = "^3.11" django = "^4.2.5" psycopg2 = "^2.9.7" django-environ = "^0.11.2" +django-guardian = "^2.4.0" +django-bootstrap-v5 = "^1.0.11" [tool.poetry.group.dev.dependencies] pre-commit = "^3.4.0" diff --git a/server/settings/__init__.py b/server/settings/__init__.py index 0ddd0b5..5e9c0fe 100644 --- a/server/settings/__init__.py +++ b/server/settings/__init__.py @@ -1,13 +1,14 @@ -from os.path import join from pathlib import Path from environ import Env BASE_DIR = Path(__file__).parent.parent.parent -APPS_DIR = BASE_DIR / "apps" # Load environment variables from config/.env file. See # https://django-environ.readthedocs.io/en/latest/ env = Env() -env.read_env(join(BASE_DIR, "config", ".env")) +READ_DOTENV = env.bool("DJANGO_READ_DOTENV", default=False) + +if READ_DOTENV: + env.read_env(BASE_DIR / "config" / ".env") diff --git a/server/settings/base.py b/server/settings/base.py index ac2e850..d6dacd4 100644 --- a/server/settings/base.py +++ b/server/settings/base.py @@ -1,7 +1,7 @@ from os.path import join from typing import List -from server.settings import APPS_DIR, BASE_DIR, env +from server.settings import BASE_DIR, env ###################### # General Settings # @@ -24,11 +24,6 @@ LOCALE_PATHS = [join(BASE_DIR, "locale")] -# Default primary key field type -# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" - ################# # Middlewares # ################# @@ -49,6 +44,11 @@ DATABASES = {"default": env.db()} +# Default primary key field type +# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + ########## # URLs # ########## @@ -68,11 +68,19 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "django.contrib.humanize", ] -THIRD_PARTY_APPS: List[str] = [] +THIRD_PARTY_APPS = [ + "guardian", + "bootstrap5", +] -LOCAL_APPS: List[str] = [] +LOCAL_APPS = [ + "apps.users", + "apps.contests", + "apps.problems", +] INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS @@ -100,7 +108,7 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [APPS_DIR / "templates"], + "DIRS": [BASE_DIR / "templates"], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -118,3 +126,29 @@ ################## STATIC_URL = "/static/" + +#################### +# Authentication # +#################### + +AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + "guardian.backends.ObjectPermissionBackend", +] + +LOGIN_REDIRECT_URL = "home" + +LOGOUT_REDIRECT_URL = "home" + +LOGIN_URL = "login" + +# The model to use to represent an user. +# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-user-model + +AUTH_USER_MODEL = "users.User" + +##################### +# Django Guardian # +##################### + +ANONYMOUS_USER_NAME = None diff --git a/server/urls.py b/server/urls.py index 083932c..58d3041 100644 --- a/server/urls.py +++ b/server/urls.py @@ -1,6 +1,14 @@ from django.contrib import admin -from django.urls import path +from django.urls import include, path + +from apps.contests.views import IndexView urlpatterns = [ + # Django views path("admin/", admin.site.urls), + path("", include("django.contrib.auth.urls")), + # Local views + path("", IndexView.as_view(), name="home"), + path("contests/", include("apps.contests.urls")), + path("problems/", include("apps.problems.urls")), ] diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..1ff2c39 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,89 @@ +{% load static i18n %} + +{% load bootstrap5 %} + +{# Load CSS and JavaScript #} +{% bootstrap_css %} +{% bootstrap_javascript %} + +{# Display django.contrib.messages as Bootstrap alerts #} +{% bootstrap_messages %} + +{% get_current_language as LANGUAGE_CODE %} + + + + + + + {% block title %}{% endblock title %} | Virtual Judge + + +
+ +
+ +
+ {% block content %}{% endblock content %} +
+ +
+ + diff --git a/templates/contests/detail.html b/templates/contests/detail.html new file mode 100644 index 0000000..4b74122 --- /dev/null +++ b/templates/contests/detail.html @@ -0,0 +1,63 @@ +{% extends "base.html" %} + +{% block title %}{{ contest.title }}{% endblock title %} + +{% load humanize %} + +{% block content %} +
+

+ + {{ contest }} + +

+
+

{{ contest.description }}

+
+ +
+

Tasks

+
+ + + + + + + + + + + {% for problem in contest.problems.all %} + + + + + + + {% endfor %} + +
NameScoreMemory limitTime limit
+ {% if problem.is_accessible %} + {{ problem }} + {% else %} + {{ problem }} + {% endif %} + + {{ problem.score|default:"???" }} + + {% if problem.memory_limit %} + {{ problem.memory_limit|filesizeformat }} + {% else %} + Unlimited + {% endif %} + + {% if problem.time_limit %} + {{ problem.time_limit}} + second{{ problem.time_limit|pluralize:"s" }} + {% else %} + Unlimited + {% endif %} +
+
+{% endblock content %} diff --git a/templates/contests/index.html b/templates/contests/index.html new file mode 100644 index 0000000..7e1ef71 --- /dev/null +++ b/templates/contests/index.html @@ -0,0 +1,113 @@ +{% extends "base.html" %} + +{% block title %}Contests{% endblock title %} + +{% load humanize %} + +{% block content %} +
+

Upcoming Contests

+ +
+ {% for contest in contests %} + {% if contest.status in valid_statuses %} +
+
+ + {{ contest }} + + + Posted at: {{ contest.created_at }} + + {% if contest.status == "Pending" %} + + {{ contest.status }} + + {% elif contest.status == "Running" %} + + {{ contest.status }} + + {% endif %} +
+
+

{{ contest.description }}

+
    +
  • + Starting at {{ contest.start_time }} +
  • +
  • + Ending at: {{ contest.end_time }} +
  • +
  • + Duration: {{ contest.start_time|timesince:contest.end_time }} +
  • + {% if contest.problems.all.count > 0 %} +
  • + {{ contest.problems.all.count }} + problem{{ contest.problems.all.count|pluralize:"s" }} +
  • + {% endif %} +
+
+
+ {% endif %} + {% endfor %} +
+
+ +
+

Past Contests

+ +
+ {% for contest in contests %} + {% if contest.status not in valid_statuses %} +
+
+ + {{ contest }} + + + Posted at: {{ contest.created_at }} + + {% if contest.status == "Cancelled" %} + + {{ contest.status }} + + {% elif contest.status == "Finished" %} + + {{ contest.status }} + + {% endif %} +
+
+

{{ contest.description }}

+
    +
  • + Starting at {{ contest.start_time }} +
  • +
  • + Ending at: {{ contest.end_time }} +
  • +
  • + Duration: {{ contest.start_time|timesince:contest.end_time }} +
  • + {% if contest.problems.all.count > 0 %} +
  • + {{ contest.problems.all.count }} + problem{{ contest.problems.all.count|pluralize:"s" }} +
  • + {% endif %} +
+
+
+ {% endif %} + {% endfor %} +
+
+ {% endblock content %} diff --git a/templates/problems/detail.html b/templates/problems/detail.html new file mode 100644 index 0000000..ae91dbb --- /dev/null +++ b/templates/problems/detail.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block title %}{{ problem.title }}{% endblock title %} + +{% block content %} +

{{ problem.title }}

+

{{ problem.description }}

+{% endblock content %} diff --git a/templates/registration/login.html b/templates/registration/login.html new file mode 100644 index 0000000..069a545 --- /dev/null +++ b/templates/registration/login.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} + +{% 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 %} +{% endif %} + +
+ {% csrf_token %} + + + + + + + + + +
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
+ + + +
+ +{# 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 new file mode 100644 index 0000000..fdedbcc --- /dev/null +++ b/templates/registration/register.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} + +{% block title %}Register{% endblock title %} + +{% block content %} +
+
+
+
+
Register
+
+
+ {% csrf_token %} +
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+
+
+{% endblock content %}