From ecb4c0e5b44b329883abb4a0cd7d7352b69e9074 Mon Sep 17 00:00:00 2001 From: kyomi Date: Wed, 4 Oct 2023 13:40:52 -0300 Subject: [PATCH 1/6] feat(apps/problems): add the initial problem app --- apps/contests/__init__.py | 2 +- apps/contests/admin.py | 16 +------ apps/contests/urls.py | 4 +- apps/contests/views.py | 27 +---------- apps/problems/__init__.py | 8 ++++ apps/problems/admin.py | 31 +++++++++++++ apps/problems/migrations/0001_initial.py | 45 +++++++++++++++++++ ...ter_problem_options_alter_problem_table.py | 20 +++++++++ apps/problems/migrations/__init__.py | 0 apps/problems/models.py | 19 ++++++++ apps/problems/urls.py | 9 ++++ apps/problems/views.py | 15 +++++++ server/settings/base.py | 1 + server/urls.py | 9 ++-- templates/base.html | 18 +------- templates/contests/detail.html | 21 +++------ templates/contests/index.html | 30 +++++++------ templates/pages/home.html | 7 --- templates/problems/detail.html | 8 ++++ 19 files changed, 189 insertions(+), 101 deletions(-) create mode 100644 apps/problems/__init__.py create mode 100644 apps/problems/admin.py create mode 100644 apps/problems/migrations/0001_initial.py create mode 100644 apps/problems/migrations/0002_alter_problem_options_alter_problem_table.py create mode 100644 apps/problems/migrations/__init__.py create mode 100644 apps/problems/models.py create mode 100644 apps/problems/urls.py create mode 100644 apps/problems/views.py create mode 100644 templates/problems/detail.html diff --git a/apps/contests/__init__.py b/apps/contests/__init__.py index bbb321a..b58563e 100644 --- a/apps/contests/__init__.py +++ b/apps/contests/__init__.py @@ -5,4 +5,4 @@ class ContestsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "appscontests" + name = "apps.contests" diff --git a/apps/contests/admin.py b/apps/contests/admin.py index 9b14984..04b81c2 100644 --- a/apps/contests/admin.py +++ b/apps/contests/admin.py @@ -1,16 +1,10 @@ from typing import TYPE_CHECKING from django.contrib.admin import ModelAdmin, register -from django.forms import ( - CharField, - ModelForm, - ModelMultipleChoiceField, - Textarea, -) +from django.forms import CharField, ModelForm, Textarea from django.utils.translation import gettext_lazy as _ from apps.contests.models import Contest -from apps.users.models import User if TYPE_CHECKING: ContestAdminBase = ModelAdmin[Contest] @@ -22,9 +16,6 @@ class ContestModelForm(ContestModelFormBase): description = CharField(widget=Textarea(attrs={"rows": 14, "cols": 80})) - users = ModelMultipleChoiceField( - queryset=User.objects.all(), required=False - ) class Meta: model = Contest @@ -40,8 +31,5 @@ class ContestAdmin(ContestAdminBase): fieldsets = [ (_("General"), {"fields": ("title", "description")}), - ( - _("Other"), - {"fields": ("start_time", "end_time", "users", "cancelled")}, - ), + (_("Other"), {"fields": ("start_time", "end_time", "cancelled")}), ] diff --git a/apps/contests/urls.py b/apps/contests/urls.py index d8f5b2a..9d31b9b 100644 --- a/apps/contests/urls.py +++ b/apps/contests/urls.py @@ -1,11 +1,9 @@ from django.urls import path -from apps.contests.views import DetailView, IndexView, send +from apps.contests.views import DetailView app_name = "contests" urlpatterns = [ - path("", IndexView.as_view(), name="index"), path("/", DetailView.as_view(), name="detail"), - path("/send/", send, name="send"), ] diff --git a/apps/contests/views.py b/apps/contests/views.py index fae0bd9..f2701b9 100644 --- a/apps/contests/views.py +++ b/apps/contests/views.py @@ -1,10 +1,6 @@ -import sys -from io import StringIO from typing import TYPE_CHECKING from django.db.models.query import QuerySet -from django.http import HttpRequest, HttpResponse -from django.shortcuts import get_object_or_404 from django.views import generic from apps.contests.models import Contest @@ -22,30 +18,9 @@ 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] class DetailView(DetailViewBase): model = Contest template_name = "contests/detail.html" - - -def send(request: HttpRequest, contest_id: int) -> HttpResponse: - contest = get_object_or_404(Contest, pk=contest_id) - code = request.POST["code"] - - old_stdout = sys.stdout - sys.stdout = buffer = StringIO() - - try: - eval(code) - except Exception as exc: - sys.stdout = old_stdout - return HttpResponse(f"Contest {contest.title} failed.\n{exc}") - - sys.stdout = old_stdout - message = buffer.getvalue() - - return HttpResponse( - f"Contest {contest.title} ran successfully.\n{message}" - ) 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..3cd1818 --- /dev/null +++ b/apps/problems/admin.py @@ -0,0 +1,31 @@ +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.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})) + + class Meta: + model = Problem + fields = "__all__" + + +@register(Problem) +class ProblemAdmin(ProblemAdminBase): + form = ProblemModelForm + + fieldsets = [ + (_("Problem"), {"fields": ("title", "description", "contest")}), + ] 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/__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..52d4051 --- /dev/null +++ b/apps/problems/models.py @@ -0,0 +1,19 @@ +from django.db.models import CASCADE, CharField, ForeignKey + +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) + + class Meta: + db_table = "problems" + + def __str__(self) -> str: + return self.title 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/server/settings/base.py b/server/settings/base.py index 60eb9be..dd412b9 100644 --- a/server/settings/base.py +++ b/server/settings/base.py @@ -78,6 +78,7 @@ LOCAL_APPS = [ "apps.users", "apps.contests", + "apps.problems", ] INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS diff --git a/server/urls.py b/server/urls.py index ecca0d6..58d3041 100644 --- a/server/urls.py +++ b/server/urls.py @@ -1,17 +1,14 @@ from django.contrib import admin from django.urls import include, path -from django.views.generic import TemplateView -home_view = TemplateView.as_view(template_name="pages/home.html") -register_view = TemplateView.as_view( - template_name="registration/register.html" -) +from apps.contests.views import IndexView urlpatterns = [ # Django views path("admin/", admin.site.urls), path("", include("django.contrib.auth.urls")), # Local views - path("", home_view, name="home"), + 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 index 3bf8707..aea7850 100644 --- a/templates/base.html +++ b/templates/base.html @@ -22,7 +22,7 @@
-
+
{% block content %}{% endblock content %}
diff --git a/templates/contests/detail.html b/templates/contests/detail.html index 558b47e..3607292 100644 --- a/templates/contests/detail.html +++ b/templates/contests/detail.html @@ -8,20 +8,11 @@

{{ contest }}

{{ contest.description }}

-
- {% csrf_token %} -
- -

{{ question.question_text }}

-
+

Tasks

- {% if error_message %} -

{{ error_message }}

- {% endif %} - - -
- - -
+
    + {% for problem in contest.problems.all %} +
  • {{ problem }}
  • + {% endfor %} +
{% endblock content %} diff --git a/templates/contests/index.html b/templates/contests/index.html index 6e84058..0124df5 100644 --- a/templates/contests/index.html +++ b/templates/contests/index.html @@ -3,17 +3,21 @@ {% block title %}Contests{% endblock title %} {% block content %} - {% if contests %} - - {% else %} -

No contests are available.

- {% endif %} +
+ {% for contest in contests %} +
+ +
+

{{ contest.description }}

+ +
    +
  • Starting at {{ contest.start_time }}
  • +
  • Ending at: {{ contest.end_time }}
  • +
+
+
+ {% endfor %} +
{% endblock content %} diff --git a/templates/pages/home.html b/templates/pages/home.html index e22d547..dafd18f 100644 --- a/templates/pages/home.html +++ b/templates/pages/home.html @@ -3,11 +3,4 @@ {% block title %}Home{% endblock title %} {% block content %} -

Home

- - {% if user.is_authenticated %} -

Welcome, {{ user.username }}!

- {% else %} -

Welcome, new user!

- {% endif %} {% 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 %} From 67ac74d8c03d37dfef7109dd173de25bb8d0727f Mon Sep 17 00:00:00 2001 From: kyomi Date: Wed, 4 Oct 2023 14:28:40 -0300 Subject: [PATCH 2/6] feat(templates/contests): add the contest duration inside the card --- server/settings/base.py | 1 + templates/contests/index.html | 17 ++++++++++++++--- templates/pages/home.html | 6 ------ 3 files changed, 15 insertions(+), 9 deletions(-) delete mode 100644 templates/pages/home.html diff --git a/server/settings/base.py b/server/settings/base.py index dd412b9..d6dacd4 100644 --- a/server/settings/base.py +++ b/server/settings/base.py @@ -68,6 +68,7 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "django.contrib.humanize", ] THIRD_PARTY_APPS = [ diff --git a/templates/contests/index.html b/templates/contests/index.html index 0124df5..af51bf5 100644 --- a/templates/contests/index.html +++ b/templates/contests/index.html @@ -2,19 +2,30 @@ {% block title %}Contests{% endblock title %} +{% load humanize %} + {% block content %}
{% for contest in contests %}
{{ contest }} + + Posted at: {{ contest.created_at }} +

{{ contest.description }}

-
    -
  • Starting at {{ contest.start_time }}
  • -
  • Ending at: {{ contest.end_time }}
  • +
  • + Starting at {{ contest.start_time }} +
  • +
  • + Ending at: {{ contest.end_time }} +
  • +
  • + Duration: {{ contest.start_time|timesince:contest.end_time }} +
diff --git a/templates/pages/home.html b/templates/pages/home.html deleted file mode 100644 index dafd18f..0000000 --- a/templates/pages/home.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Home{% endblock title %} - -{% block content %} -{% endblock content %} From e2973462ba2919b60b7bad6550b7706a19701c8f Mon Sep 17 00:00:00 2001 From: kyomi Date: Wed, 4 Oct 2023 16:13:54 -0300 Subject: [PATCH 3/6] feat(templates/contests): improve contests list page --- apps/contests/views.py | 9 ++- templates/base.html | 8 ++- templates/contests/index.html | 119 ++++++++++++++++++++++++++-------- 3 files changed, 108 insertions(+), 28 deletions(-) diff --git a/apps/contests/views.py b/apps/contests/views.py index f2701b9..cf6b047 100644 --- a/apps/contests/views.py +++ b/apps/contests/views.py @@ -1,8 +1,9 @@ -from typing import TYPE_CHECKING +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: @@ -20,6 +21,12 @@ class IndexView(IndexViewBase): 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 diff --git a/templates/base.html b/templates/base.html index aea7850..3b9a514 100644 --- a/templates/base.html +++ b/templates/base.html @@ -57,7 +57,11 @@
@@ -91,6 +94,9 @@

Past Contests

  • Duration: {{ contest.start_time|timesince:contest.end_time }}
  • +
  • + {{ contest.problems.all|length }} problems +
  • From 6b27a6cc6a3c1f30ec9a76a3c7fac6f857b2cb0c Mon Sep 17 00:00:00 2001 From: kyomi Date: Thu, 5 Oct 2023 00:12:40 -0300 Subject: [PATCH 5/6] feat(templates/contests): improve the contest details page --- apps/problems/admin.py | 17 +++++- ...lem_memory_limit_problem_score_and_more.py | 27 ++++++++ apps/problems/models.py | 14 ++++- templates/base.html | 2 +- templates/contests/detail.html | 61 ++++++++++++++++--- 5 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 apps/problems/migrations/0003_problem_memory_limit_problem_score_and_more.py diff --git a/apps/problems/admin.py b/apps/problems/admin.py index 3cd1818..77e46f9 100644 --- a/apps/problems/admin.py +++ b/apps/problems/admin.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING from django.contrib.admin import ModelAdmin, register -from django.forms import CharField, ModelForm, Textarea +from django.forms import CharField, IntegerField, ModelForm, Textarea from django.utils.translation import gettext_lazy as _ from apps.problems.models import Problem @@ -16,6 +16,14 @@ 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 @@ -26,6 +34,11 @@ class Meta: class ProblemAdmin(ProblemAdminBase): form = ProblemModelForm + list_display = ("title", "contest", "memory_limit", "time_limit") + list_filter = ("contest", "score") + fieldsets = [ - (_("Problem"), {"fields": ("title", "description", "contest")}), + (_("General"), {"fields": ("title", "description")}), + (_("Meta"), {"fields": ("contest", "score")}), + (_("Limits"), {"fields": ("memory_limit", "time_limit")}), ] 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/models.py b/apps/problems/models.py index 52d4051..54b53c2 100644 --- a/apps/problems/models.py +++ b/apps/problems/models.py @@ -1,5 +1,6 @@ -from django.db.models import CASCADE, CharField, ForeignKey +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 @@ -11,9 +12,20 @@ class Problem(TimestampedModel): 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/templates/base.html b/templates/base.html index 3b9a514..1ff2c39 100644 --- a/templates/base.html +++ b/templates/base.html @@ -80,7 +80,7 @@ -
    +
    {% block content %}{% endblock content %}
    diff --git a/templates/contests/detail.html b/templates/contests/detail.html index 3607292..4b74122 100644 --- a/templates/contests/detail.html +++ b/templates/contests/detail.html @@ -2,17 +2,62 @@ {% block title %}{{ contest.title }}{% endblock title %} +{% load humanize %} + {% block content %}
    -

    {{ contest }}

    +

    + + {{ contest }} + +

    +

    {{ contest.description }}

    -

    Tasks

    - -
      - {% for problem in contest.problems.all %} -
    • {{ problem }}
    • - {% endfor %} -
    +
    +

    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 %} From cdd8cb6ad4920e6b26729f9a040f41712fac492a Mon Sep 17 00:00:00 2001 From: kyomi Date: Thu, 5 Oct 2023 00:21:58 -0300 Subject: [PATCH 6/6] fix(templates/contests): pluralize the word `problem` correctly --- templates/contests/index.html | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/templates/contests/index.html b/templates/contests/index.html index 3c00eda..7e1ef71 100644 --- a/templates/contests/index.html +++ b/templates/contests/index.html @@ -44,9 +44,12 @@

    Upcoming Contests

  • Duration: {{ contest.start_time|timesince:contest.end_time }}
  • -
  • - {{ contest.problems.all|length }} problems -
  • + {% if contest.problems.all.count > 0 %} +
  • + {{ contest.problems.all.count }} + problem{{ contest.problems.all.count|pluralize:"s" }} +
  • + {% endif %} @@ -94,9 +97,12 @@

    Past Contests

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