From d06033747241d50a49cf40b13136f078ac1ad355 Mon Sep 17 00:00:00 2001 From: kyomi Date: Sat, 30 Sep 2023 02:12:14 -0300 Subject: [PATCH 1/5] feat(apps/contests): add the initial contests app --- apps/contests/__init__.py | 8 +++++ apps/contests/admin.py | 13 +++++++ apps/contests/migrations/0001_initial.py | 44 ++++++++++++++++++++++++ apps/contests/migrations/__init__.py | 0 apps/contests/models.py | 24 +++++++++++++ apps/users/models.py | 9 +++++ server/settings/base.py | 5 ++- 7 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 apps/contests/__init__.py create mode 100644 apps/contests/admin.py create mode 100644 apps/contests/migrations/0001_initial.py create mode 100644 apps/contests/migrations/__init__.py create mode 100644 apps/contests/models.py diff --git a/apps/contests/__init__.py b/apps/contests/__init__.py new file mode 100644 index 0000000..bbb321a --- /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 = "appscontests" diff --git a/apps/contests/admin.py b/apps/contests/admin.py new file mode 100644 index 0000000..18cdc6d --- /dev/null +++ b/apps/contests/admin.py @@ -0,0 +1,13 @@ +from django.contrib.admin import ModelAdmin, register +from django.utils.translation import gettext_lazy as _ + +from apps.contests.models import Contest + + +@register(Contest) +class ContestAdmin(ModelAdmin[Contest]): + list_display = ("title", "start_time", "end_time") + fieldsets = [ + (_("General"), {"fields": ("title", "description")}), + (_("Other"), {"fields": ("start_time", "end_time", "users")}), + ] 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/__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..4e836bb --- /dev/null +++ b/apps/contests/models.py @@ -0,0 +1,24 @@ +from django.db.models import CharField, DateTimeField, ManyToManyField + +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() + + users = ManyToManyField(User, related_name="contests") + + class Meta: + db_table = "contests" + + def __str__(self) -> str: + return f"{self.title} #{self.id}" diff --git a/apps/users/models.py b/apps/users/models.py index e4e5be0..542ad76 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -1,13 +1,22 @@ +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) diff --git a/server/settings/base.py b/server/settings/base.py index 4686d5b..60eb9be 100644 --- a/server/settings/base.py +++ b/server/settings/base.py @@ -75,7 +75,10 @@ "bootstrap5", ] -LOCAL_APPS = ["apps.users"] +LOCAL_APPS = [ + "apps.users", + "apps.contests", +] INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS From 443a5e3b899895d9f06d175a09564d3c8732522e Mon Sep 17 00:00:00 2001 From: kyomi Date: Sat, 30 Sep 2023 02:28:55 -0300 Subject: [PATCH 2/5] feat(apps/contests): add a status field to the contest model --- apps/contests/admin.py | 12 ++++++++-- apps/contests/enums.py | 8 +++++++ .../migrations/0002_contest_cancelled.py | 17 +++++++++++++ apps/contests/models.py | 24 +++++++++++++++++-- 4 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 apps/contests/enums.py create mode 100644 apps/contests/migrations/0002_contest_cancelled.py diff --git a/apps/contests/admin.py b/apps/contests/admin.py index 18cdc6d..4a63dca 100644 --- a/apps/contests/admin.py +++ b/apps/contests/admin.py @@ -1,12 +1,20 @@ +from typing import TYPE_CHECKING + from django.contrib.admin import ModelAdmin, register from django.utils.translation import gettext_lazy as _ from apps.contests.models import Contest +if TYPE_CHECKING: + ContestAdminBase = ModelAdmin[Contest] +else: + ContestAdminBase = ModelAdmin + @register(Contest) -class ContestAdmin(ModelAdmin[Contest]): - list_display = ("title", "start_time", "end_time") +class ContestAdmin(ContestAdminBase): + list_display = ("title", "start_time", "status") + list_filter = ("start_time", "end_time") fieldsets = [ (_("General"), {"fields": ("title", "description")}), (_("Other"), {"fields": ("start_time", "end_time", "users")}), 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/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/models.py b/apps/contests/models.py index 4e836bb..04570cb 100644 --- a/apps/contests/models.py +++ b/apps/contests/models.py @@ -1,5 +1,12 @@ -from django.db.models import CharField, DateTimeField, ManyToManyField - +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 @@ -14,6 +21,7 @@ class Contest(TimestampedModel): start_time = DateTimeField() end_time = DateTimeField() + cancelled = BooleanField(default=False) users = ManyToManyField(User, related_name="contests") @@ -22,3 +30,15 @@ class Meta: 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 From dcea9ca673ed318ea51befb258a6f49eac9efc57 Mon Sep 17 00:00:00 2001 From: kyomi Date: Sat, 30 Sep 2023 11:07:57 -0300 Subject: [PATCH 3/5] feat(apps/contests): add the initial views for the contest app --- apps/contests/urls.py | 11 +++++++++++ apps/contests/views.py | 28 ++++++++++++++++++++++++++++ server/urls.py | 2 +- templates/base.html | 2 +- templates/contests/detail.html | 27 +++++++++++++++++++++++++++ templates/contests/index.html | 19 +++++++++++++++++++ 6 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 apps/contests/urls.py create mode 100644 apps/contests/views.py create mode 100644 templates/contests/detail.html create mode 100644 templates/contests/index.html diff --git a/apps/contests/urls.py b/apps/contests/urls.py new file mode 100644 index 0000000..d8f5b2a --- /dev/null +++ b/apps/contests/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from apps.contests.views import DetailView, IndexView, send + +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 new file mode 100644 index 0000000..92ac625 --- /dev/null +++ b/apps/contests/views.py @@ -0,0 +1,28 @@ +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 + + +class IndexView(generic.ListView[Contest]): + template_name = "contests/index.html" + context_object_name = "contests" + + def get_queryset(self) -> QuerySet[Contest]: + return Contest._default_manager.order_by("-start_time")[:5] + + +class DetailView(generic.DetailView[Contest]): + 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"] + + eval(code) + + return HttpResponse(f"Contest {contest.title} ran successfully.") diff --git a/server/urls.py b/server/urls.py index 37b1f98..ecca0d6 100644 --- a/server/urls.py +++ b/server/urls.py @@ -13,5 +13,5 @@ path("", include("django.contrib.auth.urls")), # Local views path("", home_view, name="home"), - path("register/", register_view, name="register"), + path("contests/", include("apps.contests.urls")), ] diff --git a/templates/base.html b/templates/base.html index 4f60038..3bf8707 100644 --- a/templates/base.html +++ b/templates/base.html @@ -47,7 +47,7 @@ diff --git a/templates/contests/detail.html b/templates/contests/detail.html new file mode 100644 index 0000000..558b47e --- /dev/null +++ b/templates/contests/detail.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block title %}{{ contest.title }}{% endblock title %} + +{% block content %} +
+

{{ contest }}

+

{{ contest.description }}

+
+ +
+ {% csrf_token %} +
+ +

{{ question.question_text }}

+
+ + {% if error_message %} +

{{ error_message }}

+ {% endif %} + + +
+ + +
+{% endblock content %} diff --git a/templates/contests/index.html b/templates/contests/index.html new file mode 100644 index 0000000..6e84058 --- /dev/null +++ b/templates/contests/index.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block title %}Contests{% endblock title %} + +{% block content %} + {% if contests %} + + {% else %} +

No contests are available.

+ {% endif %} +{% endblock content %} From dd58307316b14a036fbe923d6255dc3e2d3d0ea4 Mon Sep 17 00:00:00 2001 From: kyomi Date: Sat, 30 Sep 2023 11:23:50 -0300 Subject: [PATCH 4/5] feat(apps/contests): save the sent code as a string buffer --- apps/contests/views.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/apps/contests/views.py b/apps/contests/views.py index 92ac625..fae0bd9 100644 --- a/apps/contests/views.py +++ b/apps/contests/views.py @@ -1,3 +1,7 @@ +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 @@ -5,8 +9,15 @@ 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(generic.ListView[Contest]): +class IndexView(IndexViewBase): template_name = "contests/index.html" context_object_name = "contests" @@ -14,7 +25,7 @@ def get_queryset(self) -> QuerySet[Contest]: return Contest._default_manager.order_by("-start_time")[:5] -class DetailView(generic.DetailView[Contest]): +class DetailView(DetailViewBase): model = Contest template_name = "contests/detail.html" @@ -23,6 +34,18 @@ def send(request: HttpRequest, contest_id: int) -> HttpResponse: contest = get_object_or_404(Contest, pk=contest_id) code = request.POST["code"] - eval(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.") + return HttpResponse( + f"Contest {contest.title} ran successfully.\n{message}" + ) From 47e940217f836437dd64e61492f8f9fc24341f77 Mon Sep 17 00:00:00 2001 From: kyomi Date: Sat, 30 Sep 2023 22:38:48 -0300 Subject: [PATCH 5/5] feat(apps/contests): improve the admin page --- apps/contests/admin.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/contests/admin.py b/apps/contests/admin.py index 4a63dca..9b14984 100644 --- a/apps/contests/admin.py +++ b/apps/contests/admin.py @@ -1,21 +1,47 @@ from typing import TYPE_CHECKING from django.contrib.admin import ModelAdmin, register +from django.forms import ( + CharField, + ModelForm, + ModelMultipleChoiceField, + 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] + ContestModelFormBase = ModelForm[Contest] else: ContestAdminBase = ModelAdmin + ContestModelFormBase = ModelForm + + +class ContestModelForm(ContestModelFormBase): + description = CharField(widget=Textarea(attrs={"rows": 14, "cols": 80})) + users = ModelMultipleChoiceField( + queryset=User.objects.all(), required=False + ) + + class Meta: + model = Contest + fields = "__all__" @register(Contest) class ContestAdmin(ContestAdminBase): - list_display = ("title", "start_time", "status") + 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", "users")}), + ( + _("Other"), + {"fields": ("start_time", "end_time", "users", "cancelled")}, + ), ]