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..cf6b047 100644 --- a/apps/contests/views.py +++ b/apps/contests/views.py @@ -1,12 +1,9 @@ -import sys -from io import StringIO -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any 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.enums import ContestStatus from apps.contests.models import Contest if TYPE_CHECKING: @@ -22,30 +19,15 @@ class IndexView(IndexViewBase): context_object_name = "contests" def get_queryset(self) -> QuerySet[Contest]: - return Contest._default_manager.order_by("-start_time")[:5] + return Contest._default_manager.order_by("start_time")[:5] + + def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + 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" - - -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..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/server/settings/base.py b/server/settings/base.py index 60eb9be..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 = [ @@ -78,6 +79,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..1ff2c39 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..4b74122 100644 --- a/templates/contests/detail.html +++ b/templates/contests/detail.html @@ -2,26 +2,62 @@ {% block title %}{{ contest.title }}{% endblock title %} +{% load humanize %} + {% block content %}
-

{{ contest }}

+

+ + {{ contest }} + +

+

{{ contest.description }}

-
- {% csrf_token %} -
- -

{{ question.question_text }}

-
- - {% if error_message %} -

{{ error_message }}

- {% endif %} - - -
- - -
+
+

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 index 6e84058..7e1ef71 100644 --- a/templates/contests/index.html +++ b/templates/contests/index.html @@ -2,18 +2,112 @@ {% block title %}Contests{% endblock title %} +{% load humanize %} + {% block content %} - {% if contests %} -
    +
    +

    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 %} -
  • - - {{ contest }} - -
  • + {% 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 %} -
- {% else %} -

No contests are available.

- {% endif %} -{% endblock content %} + + + {% endblock content %} diff --git a/templates/pages/home.html b/templates/pages/home.html deleted file mode 100644 index e22d547..0000000 --- a/templates/pages/home.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "base.html" %} - -{% 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 %}