From 3d67fb274e9e353eb5a16289fbfd3dab9b1fdd44 Mon Sep 17 00:00:00 2001 From: Phuoc Anh Kha Le <76896393+anhkha2003@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:06:33 -0500 Subject: [PATCH] Add contest to course (#126) --- dmoj/urls.py | 15 + judge/migrations/0194_course_contest.py | 67 +++ judge/models/__init__.py | 8 +- judge/models/contest.py | 19 +- judge/models/course.py | 23 +- judge/utils/contest.py | 32 ++ judge/views/contests.py | 8 + judge/views/course.py | 399 +++++++++++++++- judge/views/organization.py | 25 +- locale/vi/LC_MESSAGES/django.po | 579 +++++++++++++----------- locale/vi/LC_MESSAGES/dmoj-user.po | 10 +- resources/course.scss | 26 +- resources/darkmode.css | 63 ++- resources/table.scss | 33 +- resources/users.scss | 4 +- templates/contest/contest.html | 7 +- templates/course/add_contest.html | 13 + templates/course/contest_list.html | 35 ++ templates/course/course.html | 128 +++++- templates/course/edit_contest.html | 92 ++++ templates/course/grades.html | 104 +++-- templates/course/left_sidebar.html | 1 + 22 files changed, 1258 insertions(+), 433 deletions(-) create mode 100644 judge/migrations/0194_course_contest.py create mode 100644 judge/utils/contest.py create mode 100644 templates/course/add_contest.html create mode 100644 templates/course/contest_list.html create mode 100644 templates/course/edit_contest.html diff --git a/dmoj/urls.py b/dmoj/urls.py index 278b815..1f077f4 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -569,6 +569,21 @@ urlpatterns = [ course.CourseStudentResultsLesson.as_view(), name="course_grades_lesson", ), + url( + r"^/add_contest$", + course.AddCourseContest.as_view(), + name="add_course_contest", + ), + url( + r"^/edit_contest/(?P\w+)$", + course.EditCourseContest.as_view(), + name="edit_course_contest", + ), + url( + r"^/contests$", + course.CourseContestList.as_view(), + name="course_contest_list", + ), ] ), ), diff --git a/judge/migrations/0194_course_contest.py b/judge/migrations/0194_course_contest.py new file mode 100644 index 0000000..6c3e534 --- /dev/null +++ b/judge/migrations/0194_course_contest.py @@ -0,0 +1,67 @@ +# Generated by Django 3.2.21 on 2024-09-30 22:31 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("judge", "0193_remove_old_course_problems"), + ] + + operations = [ + migrations.AddField( + model_name="contest", + name="is_in_course", + field=models.BooleanField(default=False, verbose_name="contest in course"), + ), + migrations.AddField( + model_name="courselesson", + name="is_visible", + field=models.BooleanField(default=True, verbose_name="publicly visible"), + ), + migrations.AlterField( + model_name="courselesson", + name="content", + field=models.TextField(verbose_name="lesson content"), + ), + migrations.AlterField( + model_name="courselesson", + name="title", + field=models.TextField(verbose_name="lesson title"), + ), + migrations.CreateModel( + name="CourseContest", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("order", models.IntegerField(default=0, verbose_name="order")), + ("points", models.IntegerField(verbose_name="points")), + ( + "contest", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="course", + to="judge.contest", + unique=True, + ), + ), + ( + "course", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="contests", + to="judge.course", + ), + ), + ], + ), + ] diff --git a/judge/models/__init__.py b/judge/models/__init__.py index b4467a3..ef62797 100644 --- a/judge/models/__init__.py +++ b/judge/models/__init__.py @@ -61,7 +61,13 @@ from judge.models.ticket import Ticket, TicketMessage from judge.models.volunteer import VolunteerProblemVote from judge.models.pagevote import PageVote, PageVoteVoter from judge.models.bookmark import BookMark, MakeBookMark -from judge.models.course import Course, CourseRole, CourseLesson, CourseLessonProblem +from judge.models.course import ( + Course, + CourseRole, + CourseLesson, + CourseLessonProblem, + CourseContest, +) from judge.models.notification import Notification, NotificationProfile from judge.models.test_formatter import TestFormatterModel diff --git a/judge/models/contest.py b/judge/models/contest.py index 914e79f..625841d 100644 --- a/judge/models/contest.py +++ b/judge/models/contest.py @@ -246,6 +246,10 @@ class Contest(models.Model, PageVotable, Bookmarkable): verbose_name=_("organizations"), help_text=_("If private, only these organizations may see the contest"), ) + is_in_course = models.BooleanField( + verbose_name=_("contest in course"), + default=False, + ) og_image = models.CharField( verbose_name=_("OpenGraph image"), default="", max_length=150, blank=True ) @@ -561,6 +565,14 @@ class Contest(models.Model, PageVotable, Bookmarkable): if not self.is_visible: raise self.Inaccessible() + if self.is_in_course: + from judge.models import Course, CourseContest + + course_contest = CourseContest.objects.filter(contest=self).first() + if Course.is_accessible_by(course_contest.course, user.profile): + return + raise self.Inaccessible() + # Contest is not private if not self.is_private and not self.is_organization_private: return @@ -612,7 +624,10 @@ class Contest(models.Model, PageVotable, Bookmarkable): if not user.is_authenticated: return ( cls.objects.filter( - is_visible=True, is_organization_private=False, is_private=False + is_visible=True, + is_organization_private=False, + is_private=False, + is_in_course=False, ) .defer("description") .distinct() @@ -626,7 +641,7 @@ class Contest(models.Model, PageVotable, Bookmarkable): ) or show_own_contests_only ): - q = Q(is_visible=True) + q = Q(is_visible=True, is_in_course=False) q &= ( Q(view_contest_scoreboard=user.profile) | Q(is_organization_private=False, is_private=False) diff --git a/judge/models/course.py b/judge/models/course.py index 003c695..df4df21 100644 --- a/judge/models/course.py +++ b/judge/models/course.py @@ -4,7 +4,7 @@ from django.utils.translation import gettext, gettext_lazy as _ from django.urls import reverse from django.db.models import Q -from judge.models import BlogPost, Problem +from judge.models import BlogPost, Problem, Contest from judge.models.profile import Organization, Profile @@ -160,10 +160,11 @@ class CourseLesson(models.Model): related_name="lessons", on_delete=models.CASCADE, ) - title = models.TextField(verbose_name=_("course title")) - content = models.TextField(verbose_name=_("course content")) + title = models.TextField(verbose_name=_("lesson title")) + content = models.TextField(verbose_name=_("lesson content")) order = models.IntegerField(verbose_name=_("order"), default=0) points = models.IntegerField(verbose_name=_("points")) + is_visible = models.BooleanField(verbose_name=_("publicly visible"), default=True) def get_absolute_url(self): return reverse( @@ -182,3 +183,19 @@ class CourseLessonProblem(models.Model): problem = models.ForeignKey(Problem, on_delete=models.CASCADE) order = models.IntegerField(verbose_name=_("order"), default=0) score = models.IntegerField(verbose_name=_("score"), default=0) + + +class CourseContest(models.Model): + course = models.ForeignKey( + Course, on_delete=models.CASCADE, related_name="contests" + ) + contest = models.ForeignKey( + Contest, unique=True, on_delete=models.CASCADE, related_name="course" + ) + order = models.IntegerField(verbose_name=_("order"), default=0) + points = models.IntegerField(verbose_name=_("points")) + + def get_course_of_contest(contest): + course_contest = contest.course.get() + course = course_contest.course + return course diff --git a/judge/utils/contest.py b/judge/utils/contest.py new file mode 100644 index 0000000..eeb4134 --- /dev/null +++ b/judge/utils/contest.py @@ -0,0 +1,32 @@ +from django.db import transaction +from judge.tasks import rescore_contest +from judge.models import ( + Contest, +) + + +def maybe_trigger_contest_rescore(form, contest): + if any( + f in form.changed_data + for f in ( + "start_time", + "end_time", + "time_limit", + "format_config", + "format_name", + "freeze_after", + ) + ): + transaction.on_commit(rescore_contest.s(contest.key).delay) + + if any( + f in form.changed_data + for f in ( + "authors", + "curators", + "testers", + ) + ): + Contest._author_ids.dirty(contest) + Contest._curator_ids.dirty(contest) + Contest._tester_ids.dirty(contest) diff --git a/judge/views/contests.py b/judge/views/contests.py index 830a7fa..3dc27f2 100644 --- a/judge/views/contests.py +++ b/judge/views/contests.py @@ -534,6 +534,14 @@ class ContestDetail( ) context["editable_organizations"] = self.get_editable_organizations() context["is_clonable"] = is_contest_clonable(self.request, self.object) + + if self.object.is_in_course: + from judge.models import Course, CourseContest + + course = CourseContest.get_course_of_contest(self.object) + if Course.is_editable_by(course, self.request.profile): + context["editable_course"] = course + if self.request.in_contest: context["current_contest"] = self.request.participation.contest else: diff --git a/judge/views/course.py b/judge/views/course.py index cb0083c..17f794e 100644 --- a/judge/views/course.py +++ b/judge/views/course.py @@ -12,28 +12,42 @@ from django.forms import ( ) from django.views.generic.edit import FormView from django.shortcuts import get_object_or_404 -from django.urls import reverse_lazy -from django.db.models import Max, F +from django.urls import reverse_lazy, reverse +from django.db.models import Max, F, Sum from django.core.exceptions import ObjectDoesNotExist from judge.models import ( Course, + Contest, CourseLesson, Submission, Profile, CourseRole, CourseLessonProblem, + CourseContest, + ContestProblem, + ContestParticipation, ) from judge.models.course import RoleInCourse from judge.widgets import ( HeavyPreviewPageDownWidget, HeavySelect2MultipleWidget, HeavySelect2Widget, + DateTimePickerWidget, + Select2MultipleWidget, + Select2Widget, +) +from judge.forms import ( + ContestProblemFormSet, ) from judge.utils.problems import ( user_attempted_ids, user_completed_ids, ) +from judge.utils.contest import ( + maybe_trigger_contest_rescore, +) +from reversion import revisions def max_case_points_per_problem(profile, problems): @@ -85,6 +99,65 @@ def calculate_lessons_progress(profile, lessons): return res +def calculate_contests_progress(profile, course_contests): + res = {} + total_achieved_points = total_contest_points = 0 + for course_contest in course_contests: + contest = course_contest.contest + + achieved_points = 0 + participation = ContestParticipation.objects.filter( + contest=contest, user=profile, virtual=0 + ).first() + + if participation: + achieved_points = participation.score + + total_points = ( + ContestProblem.objects.filter(contest=contest).aggregate(Sum("points"))[ + "points__sum" + ] + or 0 + ) + + res[course_contest.id] = { + "achieved_points": achieved_points, + "total_points": total_points, + "percentage": achieved_points / total_points * 100 if total_points else 0, + } + + if total_points: + total_achieved_points += ( + achieved_points / total_points * course_contest.points + ) + total_contest_points += course_contest.points + + res["total"] = { + "achieved_points": total_achieved_points, + "total_points": total_contest_points, + "percentage": total_achieved_points / total_contest_points * 100 + if total_contest_points + else 0, + } + return res + + +def calculate_total_progress(profile, lesson_progress, contest_progress): + lesson_total = lesson_progress["total"] + contest_total = contest_progress["total"] + total_achieved_points = ( + lesson_total["achieved_points"] + contest_total["achieved_points"] + ) + total_points = lesson_total["total_points"] + contest_total["total_points"] + + res = { + "achieved_points": total_achieved_points, + "total_points": total_points, + "percentage": total_achieved_points / total_points * 100 if total_points else 0, + } + return res + + class CourseList(ListView): model = Course template_name = "course/list.html" @@ -130,13 +203,34 @@ class CourseDetail(CourseDetailMixin, DetailView): def get_context_data(self, **kwargs): context = super(CourseDetail, self).get_context_data(**kwargs) - lessons = self.course.lessons.prefetch_related("lesson_problems").all() + lessons = ( + self.course.lessons.filter(is_visible=True) + .order_by("order") + .prefetch_related("lesson_problems") + .all() + ) + course_contests = ( + self.course.contests.select_related("contest") + .filter(contest__is_visible=True) + .order_by("order") + ) context["title"] = self.course.name context["page_type"] = "home" context["lessons"] = lessons context["lesson_progress"] = calculate_lessons_progress( self.request.profile, lessons ) + context["course_contests"] = course_contests + context["contest_progress"] = calculate_contests_progress( + self.request.profile, course_contests + ) + + context["total_progress"] = calculate_total_progress( + self.request.profile, + context["lesson_progress"], + context["contest_progress"], + ) + return context @@ -149,6 +243,11 @@ class CourseLessonDetail(CourseDetailMixin, DetailView): self.lesson = CourseLesson.objects.get( course=self.course, id=self.kwargs["id"] ) + + is_editable = Course.is_editable_by(self.course, self.request.profile) + if not self.lesson.is_visible and not is_editable: + raise Http404() + return self.lesson except ObjectDoesNotExist: raise Http404() @@ -190,7 +289,7 @@ class CourseLessonDetail(CourseDetailMixin, DetailView): class CourseLessonForm(forms.ModelForm): class Meta: model = CourseLesson - fields = ["order", "title", "points", "content"] + fields = ["order", "title", "is_visible", "points", "content"] widgets = { "title": forms.TextInput(), "content": HeavyPreviewPageDownWidget(preview=reverse_lazy("blog_preview")), @@ -312,9 +411,29 @@ class CourseStudentResults(CourseEditableMixin, DetailView): def get_grades(self): students = self.course.get_students() students.sort(key=lambda u: u.username.lower()) - lessons = self.course.lessons.prefetch_related("lesson_problems").all() - grades = {s: calculate_lessons_progress(s, lessons) for s in students} - return grades + lessons = ( + self.course.lessons.filter(is_visible=True) + .prefetch_related("lesson_problems") + .all() + ) + course_contests = ( + self.course.contests.select_related("contest") + .filter(contest__is_visible=True) + .order_by("order") + ) + + grade_lessons = {} + grade_contests = {} + grade_total = {} + for s in students: + grade_lessons[s] = lesson_progress = calculate_lessons_progress(s, lessons) + grade_contests[s] = contest_progress = calculate_contests_progress( + s, course_contests + ) + grade_total[s] = calculate_total_progress( + s, lesson_progress, contest_progress + ) + return grade_lessons, grade_contests, grade_total def get_context_data(self, **kwargs): context = super(CourseStudentResults, self).get_context_data(**kwargs) @@ -329,7 +448,19 @@ class CourseStudentResults(CourseEditableMixin, DetailView): } ) context["page_type"] = "grades" - context["grades"] = self.get_grades() + ( + context["grade_lessons"], + context["grade_contests"], + context["grade_total"], + ) = self.get_grades() + context["lessons"] = self.course.lessons.filter(is_visible=True).order_by( + "order" + ) + context["course_contests"] = ( + self.course.contests.select_related("contest") + .filter(contest__is_visible=True) + .order_by("order") + ) return context @@ -392,3 +523,255 @@ class CourseStudentResultsLesson(CourseEditableMixin, DetailView): context["page_type"] = "grades" context["grades"] = self.get_lesson_grades() return context + + +class AddCourseContestForm(forms.ModelForm): + order = forms.IntegerField(label=_("Order")) + points = forms.IntegerField(label=_("Points")) + + class Meta: + model = Contest + fields = [ + "order", + "points", + "key", + "name", + "start_time", + "end_time", + "problems", + ] + widgets = { + "start_time": DateTimePickerWidget(), + "end_time": DateTimePickerWidget(), + "problems": HeavySelect2MultipleWidget(data_view="problem_select2"), + } + + def save(self, course, profile, commit=True): + contest = super().save(commit=False) + contest.is_in_course = True + + old_save_m2m = self.save_m2m + + def save_m2m(): + for i, problem in enumerate(self.cleaned_data["problems"]): + contest_problem = ContestProblem( + contest=contest, problem=problem, points=100, order=i + 1 + ) + contest_problem.save() + contest.contest_problems.add(contest_problem) + contest.authors.add(profile) + old_save_m2m() + + self.save_m2m = save_m2m + contest.save() + self.save_m2m() + + CourseContest.objects.create( + course=course, + contest=contest, + order=self.cleaned_data["order"], + points=self.cleaned_data["points"], + ) + + return contest + + +class AddCourseContest(CourseEditableMixin, FormView): + template_name = "course/add_contest.html" + form_class = AddCourseContestForm + + def get_title(self): + return _("Add contest") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["title"] = self.get_title() + return context + + def form_valid(self, form): + with revisions.create_revision(): + revisions.set_comment(_("Added from course") + " " + self.course.name) + revisions.set_user(self.request.user) + + self.contest = form.save(course=self.course, profile=self.request.profile) + + return super().form_valid(form) + + def get_success_url(self): + return reverse( + "edit_course_contest", + args=[self.course.slug, self.contest.key], + ) + + +class CourseContestList(CourseEditableMixin, ListView): + template_name = "course/contest_list.html" + context_object_name = "course_contests" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["title"] = _("Contest list") + context["page_type"] = "contests" + return context + + def get_queryset(self): + return self.course.contests.select_related("contest").all().order_by("order") + + +class EditCourseContestForm(ModelForm): + order = forms.IntegerField(label=_("Order")) + points = forms.IntegerField(label=_("Points")) + + class Meta: + model = Contest + fields = ( + "order", + "points", + "is_visible", + "key", + "name", + "start_time", + "end_time", + "format_name", + "authors", + "curators", + "testers", + "time_limit", + "freeze_after", + "use_clarifications", + "hide_problem_tags", + "public_scoreboard", + "scoreboard_visibility", + "points_precision", + "rate_limit", + "description", + "access_code", + "private_contestants", + "view_contest_scoreboard", + "banned_users", + ) + widgets = { + "authors": HeavySelect2MultipleWidget(data_view="profile_select2"), + "curators": HeavySelect2MultipleWidget(data_view="profile_select2"), + "testers": HeavySelect2MultipleWidget(data_view="profile_select2"), + "private_contestants": HeavySelect2MultipleWidget( + data_view="profile_select2" + ), + "banned_users": HeavySelect2MultipleWidget(data_view="profile_select2"), + "view_contest_scoreboard": HeavySelect2MultipleWidget( + data_view="profile_select2" + ), + "tags": Select2MultipleWidget, + "description": HeavyPreviewPageDownWidget( + preview=reverse_lazy("contest_preview") + ), + "start_time": DateTimePickerWidget(), + "end_time": DateTimePickerWidget(), + "format_name": Select2Widget(), + "scoreboard_visibility": Select2Widget(), + } + + def __init__(self, *args, **kwargs): + self.course_contest_instance = kwargs.pop("course_contest_instance", None) + super().__init__(*args, **kwargs) + + if self.course_contest_instance: + self.fields["order"].initial = self.course_contest_instance.order + self.fields["points"].initial = self.course_contest_instance.points + + def save(self, commit=True): + contest = super().save(commit=commit) + + if self.course_contest_instance: + self.course_contest_instance.order = self.cleaned_data["order"] + self.course_contest_instance.points = self.cleaned_data["points"] + if commit: + self.course_contest_instance.save() + + return contest + + +class EditCourseContest(CourseEditableMixin, FormView): + template_name = "course/edit_contest.html" + form_class = EditCourseContestForm + + def dispatch(self, request, *args, **kwargs): + self.contest = get_object_or_404(Contest, key=self.kwargs["contest"]) + res = super().dispatch(request, *args, **kwargs) + if not self.contest.is_in_course: + raise Http404() + return res + + def get_form_kwargs(self): + self.course_contest = get_object_or_404( + CourseContest, course=self.course, contest=self.contest + ) + + kwargs = super().get_form_kwargs() + kwargs["instance"] = self.contest + kwargs["course_contest_instance"] = self.course_contest + return kwargs + + def post(self, request, *args, **kwargs): + problem_formset = self.get_problem_formset(True) + if problem_formset.is_valid(): + for problem_form in problem_formset: + if problem_form.cleaned_data.get("DELETE") and problem_form.instance.pk: + problem_form.instance.delete() + + for problem_form in problem_formset.save(commit=False): + if problem_form: + problem_form.contest = self.contest + problem_form.save() + + return super().post(request, *args, **kwargs) + + self.object = self.contest + return self.render_to_response( + self.get_context_data( + problems_form=problem_formset, + ) + ) + + def get_title(self): + return _("Edit contest") + + def form_valid(self, form): + with revisions.create_revision(): + revisions.set_comment(_("Edited from course") + " " + self.course.name) + revisions.set_user(self.request.user) + + maybe_trigger_contest_rescore(form, self.contest) + + form.save() + + return super().form_valid(form) + + def get_problem_formset(self, post=False): + return ContestProblemFormSet( + data=self.request.POST if post else None, + prefix="problems", + queryset=ContestProblem.objects.filter(contest=self.contest).order_by( + "order" + ), + ) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["title"] = self.get_title() + context["content_title"] = mark_safe( + _("Edit %(contest_name)s") + % { + "contest_name": self.contest.name, + "url": self.contest.get_absolute_url(), + } + ) + if "problems_form" not in context: + context["problems_form"] = self.get_problem_formset() + return context + + def get_success_url(self): + return reverse( + "edit_course_contest", + args=[self.course.slug, self.contest.key], + ) diff --git a/judge/views/organization.py b/judge/views/organization.py index 672bf12..364b12a 100644 --- a/judge/views/organization.py +++ b/judge/views/organization.py @@ -69,6 +69,7 @@ from judge.utils.views import ( DiggPaginatorMixin, ) from judge.utils.problems import user_attempted_ids, user_completed_ids +from judge.utils.contest import maybe_trigger_contest_rescore from judge.views.problem import ProblemList from judge.views.contests import ContestList from judge.views.submission import SubmissionsListBase @@ -1038,30 +1039,8 @@ class EditOrganizationContest( self.object.is_organization_private = True self.object.save() - if any( - f in form.changed_data - for f in ( - "start_time", - "end_time", - "time_limit", - "format_config", - "format_name", - "freeze_after", - ) - ): - transaction.on_commit(rescore_contest.s(self.object.key).delay) + maybe_trigger_contest_rescore(form, self.object) - if any( - f in form.changed_data - for f in ( - "authors", - "curators", - "testers", - ) - ): - Contest._author_ids.dirty(self.object) - Contest._curator_ids.dirty(self.object) - Contest._tester_ids.dirty(self.object) return res def get_problem_formset(self, post=False): diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index e4ba449..60a2668 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: lqdoj2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-09-18 09:10+0700\n" +"POT-Creation-Date: 2024-10-03 02:46+0700\n" "PO-Revision-Date: 2021-07-20 03:44\n" "Last-Translator: Icyene\n" "Language-Team: Vietnamese\n" @@ -23,8 +23,8 @@ msgid "last seen" msgstr "xem lần cuối" #: chat_box/models.py:55 chat_box/models.py:80 chat_box/models.py:96 -#: judge/admin/interface.py:151 judge/models/contest.py:710 -#: judge/models/contest.py:916 judge/models/course.py:129 +#: judge/admin/interface.py:151 judge/models/contest.py:722 +#: judge/models/contest.py:928 judge/models/course.py:129 #: judge/models/profile.py:465 judge/models/profile.py:539 msgid "user" msgstr "người dùng" @@ -53,7 +53,7 @@ msgstr "Gần đây" #: chat_box/views.py:454 templates/base.html:198 #: templates/comments/content-list.html:72 #: templates/contest/contest-list-tabs.html:6 -#: templates/contest/ranking-table.html:52 templates/course/left_sidebar.html:8 +#: templates/contest/ranking-table.html:52 templates/course/left_sidebar.html:9 #: templates/internal/problem/problem.html:63 #: templates/organization/org-left-sidebar.html:12 #: templates/organization/users-table.html:19 @@ -123,7 +123,7 @@ msgid "Included contests" msgstr "" #: judge/admin/contest.py:87 judge/admin/volunteer.py:54 -#: templates/contest/clarification.html:42 templates/contest/contest.html:116 +#: templates/contest/clarification.html:42 templates/contest/contest.html:121 #: templates/contest/moss.html:41 templates/internal/left-sidebar.html:2 #: templates/internal/problem/problem.html:41 templates/problem/list.html:17 #: templates/problem/list.html:34 templates/problem/list.html:153 @@ -232,7 +232,7 @@ msgid "diff" msgstr "" #: judge/admin/organization.py:60 judge/admin/problem.py:290 -#: judge/admin/profile.py:127 +#: judge/admin/profile.py:128 msgid "View on site" msgstr "Xem trên trang" @@ -259,8 +259,10 @@ msgid "Taxonomy" msgstr "" #: judge/admin/problem.py:230 judge/admin/problem.py:463 -#: templates/contest/contest.html:117 -#: templates/contest/contests_summary.html:41 templates/problem/data.html:535 +#: judge/views/course.py:494 judge/views/course.py:579 +#: templates/contest/contest.html:122 +#: templates/contest/contests_summary.html:41 +#: templates/course/contest_list.html:21 templates/problem/data.html:535 #: templates/problem/list.html:22 templates/problem/list.html:48 #: templates/profile-table.html:31 templates/profile-table.html:41 #: templates/user/base-users-table.html:10 templates/user/user-about.html:36 @@ -331,11 +333,11 @@ msgstr "Tổng điểm" msgid "Vote" msgstr "" -#: judge/admin/profile.py:46 +#: judge/admin/profile.py:47 msgid "timezone" msgstr "múi giờ" -#: judge/admin/profile.py:136 judge/admin/submission.py:327 +#: judge/admin/profile.py:137 judge/admin/submission.py:327 #: templates/notification/list.html:9 #: templates/organization/requests/log.html:9 #: templates/organization/requests/pending.html:19 @@ -343,32 +345,32 @@ msgstr "múi giờ" msgid "User" msgstr "Thành viên" -#: judge/admin/profile.py:142 templates/registration/registration_form.html:40 +#: judge/admin/profile.py:143 templates/registration/registration_form.html:40 #: templates/user/edit-profile.html:123 templates/user/import/table_csv.html:8 msgid "Email" msgstr "Email" -#: judge/admin/profile.py:148 judge/views/register.py:36 +#: judge/admin/profile.py:149 judge/views/register.py:36 #: templates/registration/registration_form.html:68 #: templates/user/edit-profile.html:147 msgid "Timezone" msgstr "Múi giờ" -#: judge/admin/profile.py:154 +#: judge/admin/profile.py:155 msgid "date joined" msgstr "ngày tham gia" -#: judge/admin/profile.py:164 +#: judge/admin/profile.py:165 #, python-format msgid "%d user have scores recalculated." msgid_plural "%d users have scores recalculated." msgstr[0] "%d người dùng đã được tính điểm lại." -#: judge/admin/profile.py:171 +#: judge/admin/profile.py:172 msgid "Recalculate scores" msgstr "Tính điểm lại" -#: judge/admin/profile.py:177 judge/admin/profile.py:184 +#: judge/admin/profile.py:179 judge/admin/profile.py:186 msgid "Username can only contain letters, digits, and underscores." msgstr "Tên đăng nhập phải chứa ký tự, chữ số, hoặc dấu gạch dưới" @@ -540,7 +542,7 @@ msgstr "Mật khẩu không được toàn chữ số." msgid "Bug Report" msgstr "Báo cáo lỗi" -#: judge/custom_translations.py:21 judge/views/course.py:96 +#: judge/custom_translations.py:21 judge/views/course.py:164 #: templates/course/list.html:8 msgid "Courses" msgstr "Khóa học" @@ -727,7 +729,7 @@ msgstr "" msgid "contest tag" msgstr "" -#: judge/models/contest.py:87 judge/models/contest.py:263 +#: judge/models/contest.py:87 judge/models/contest.py:267 msgid "contest tags" msgstr "nhãn kỳ thi" @@ -788,7 +790,7 @@ msgstr "mô tả" msgid "problems" msgstr "bài tập" -#: judge/models/contest.py:137 judge/models/contest.py:715 +#: judge/models/contest.py:137 judge/models/contest.py:727 msgid "start time" msgstr "thời gian bắt đầu" @@ -821,7 +823,7 @@ msgstr "" "hãy nhập 02:00:00" #: judge/models/contest.py:156 judge/models/course.py:27 -#: judge/models/problem.py:225 +#: judge/models/course.py:167 judge/models/problem.py:225 msgid "publicly visible" msgstr "công khai" @@ -942,36 +944,42 @@ msgstr "tổ chức" msgid "If private, only these organizations may see the contest" msgstr "Nếu riêng tư, chỉ những tổ chức này thấy được kỳ thi" -#: judge/models/contest.py:250 judge/models/problem.py:256 +#: judge/models/contest.py:250 +#, fuzzy +#| msgid "contest name" +msgid "contest in course" +msgstr "tên kỳ thi" + +#: judge/models/contest.py:254 judge/models/problem.py:256 msgid "OpenGraph image" msgstr "Hình ảnh OpenGraph" -#: judge/models/contest.py:253 +#: judge/models/contest.py:257 msgid "Logo override image" msgstr "Hình ảnh ghi đè logo" -#: judge/models/contest.py:258 +#: judge/models/contest.py:262 msgid "" "This image will replace the default site logo for users inside the contest." msgstr "Ảnh này sẽ thay thế cho logo mặc định trong kỳ thi." -#: judge/models/contest.py:266 +#: judge/models/contest.py:270 msgid "the amount of live participants" msgstr "số lượng thí sinh thi trực tiếp" -#: judge/models/contest.py:270 +#: judge/models/contest.py:274 msgid "contest summary" msgstr "tổng kết kỳ thi" -#: judge/models/contest.py:272 judge/models/problem.py:262 +#: judge/models/contest.py:276 judge/models/problem.py:262 msgid "Plain-text, shown in meta description tag, e.g. for social media." msgstr "" -#: judge/models/contest.py:276 judge/models/profile.py:109 +#: judge/models/contest.py:280 judge/models/profile.py:109 msgid "access code" msgstr "mật khẩu truy cập" -#: judge/models/contest.py:281 +#: judge/models/contest.py:285 msgid "" "An optional code to prompt contestants before they are allowed to join the " "contest. Leave it blank to disable." @@ -979,345 +987,347 @@ msgstr "" "Mật khẩu truy cập cho các thí sinh muốn tham gia kỳ thi. Để trống nếu không " "dùng." -#: judge/models/contest.py:287 judge/models/problem.py:244 +#: judge/models/contest.py:291 judge/models/problem.py:244 msgid "personae non gratae" msgstr "Chặn tham gia" -#: judge/models/contest.py:289 +#: judge/models/contest.py:293 msgid "Bans the selected users from joining this contest." msgstr "Cấm những người dùng được chọn tham gia kỳ thi." -#: judge/models/contest.py:292 +#: judge/models/contest.py:296 msgid "contest format" msgstr "format kỳ thi" -#: judge/models/contest.py:296 +#: judge/models/contest.py:300 msgid "The contest format module to use." msgstr "Format kỳ thi sử dụng." -#: judge/models/contest.py:299 +#: judge/models/contest.py:303 msgid "contest format configuration" msgstr "Tùy chỉnh format kỳ thi" -#: judge/models/contest.py:303 +#: judge/models/contest.py:307 msgid "" "A JSON object to serve as the configuration for the chosen contest format " "module. Leave empty to use None. Exact format depends on the contest format " "selected." msgstr "" -#: judge/models/contest.py:316 +#: judge/models/contest.py:320 msgid "precision points" msgstr "Hiển thị điểm" -#: judge/models/contest.py:319 +#: judge/models/contest.py:323 msgid "Number of digits to round points to." msgstr "Số chữ số thập phân trên bảng điểm." -#: judge/models/contest.py:322 +#: judge/models/contest.py:326 msgid "rate limit" msgstr "giới hạn bài nộp" -#: judge/models/contest.py:327 +#: judge/models/contest.py:331 msgid "" "Maximum number of submissions per minute. Leave empty if you don't want rate " "limit." msgstr "Số bài nộp tối đa mỗi phút. Để trống nếu không muốn giới hạn." -#: judge/models/contest.py:358 +#: judge/models/contest.py:362 msgid "End time must be after start time" msgstr "Thời gian kết thúc phải sau thời gian bắt đầu" -#: judge/models/contest.py:669 +#: judge/models/contest.py:681 msgid "See private contests" msgstr "" -#: judge/models/contest.py:670 +#: judge/models/contest.py:682 msgid "Edit own contests" msgstr "" -#: judge/models/contest.py:671 +#: judge/models/contest.py:683 msgid "Edit all contests" msgstr "" -#: judge/models/contest.py:672 +#: judge/models/contest.py:684 msgid "Clone contest" msgstr "" -#: judge/models/contest.py:673 templates/contest/moss.html:72 +#: judge/models/contest.py:685 templates/contest/moss.html:72 msgid "MOSS contest" msgstr "" -#: judge/models/contest.py:674 +#: judge/models/contest.py:686 msgid "Rate contests" msgstr "" -#: judge/models/contest.py:675 +#: judge/models/contest.py:687 msgid "Contest access codes" msgstr "" -#: judge/models/contest.py:676 +#: judge/models/contest.py:688 msgid "Create private contests" msgstr "" -#: judge/models/contest.py:677 +#: judge/models/contest.py:689 msgid "Change contest visibility" msgstr "" -#: judge/models/contest.py:678 +#: judge/models/contest.py:690 msgid "Edit contest problem label script" msgstr "Cách hiển thị thứ tự bài tập" -#: judge/models/contest.py:680 judge/models/contest.py:841 -#: judge/models/contest.py:919 judge/models/contest.py:949 -#: judge/models/contest.py:1028 judge/models/submission.py:116 +#: judge/models/contest.py:692 judge/models/contest.py:853 +#: judge/models/contest.py:931 judge/models/contest.py:961 +#: judge/models/contest.py:1040 judge/models/submission.py:116 msgid "contest" msgstr "kỳ thi" -#: judge/models/contest.py:681 +#: judge/models/contest.py:693 msgid "contests" msgstr "kỳ thi" -#: judge/models/contest.py:704 +#: judge/models/contest.py:716 msgid "associated contest" msgstr "" -#: judge/models/contest.py:717 judge/models/course.py:184 +#: judge/models/contest.py:729 judge/models/course.py:185 msgid "score" msgstr "điểm" -#: judge/models/contest.py:718 +#: judge/models/contest.py:730 msgid "cumulative time" msgstr "tổng thời gian" -#: judge/models/contest.py:720 +#: judge/models/contest.py:732 msgid "is disqualified" msgstr "đã bị loại" -#: judge/models/contest.py:722 +#: judge/models/contest.py:734 msgid "Whether this participation is disqualified." msgstr "Quyết định thí sinh có bị loại không." -#: judge/models/contest.py:724 +#: judge/models/contest.py:736 msgid "tie-breaking field" msgstr "" -#: judge/models/contest.py:726 +#: judge/models/contest.py:738 msgid "virtual participation id" msgstr "id lần tham gia ảo" -#: judge/models/contest.py:728 +#: judge/models/contest.py:740 msgid "0 means non-virtual, otherwise the n-th virtual participation." msgstr "0 nghĩa là tham gia chính thức, ngược lại là lần tham gia ảo thứ n." -#: judge/models/contest.py:731 +#: judge/models/contest.py:743 msgid "contest format specific data" msgstr "" -#: judge/models/contest.py:734 +#: judge/models/contest.py:746 msgid "same as format_data, but including frozen results" msgstr "" -#: judge/models/contest.py:738 +#: judge/models/contest.py:750 msgid "final score" msgstr "điểm" -#: judge/models/contest.py:740 +#: judge/models/contest.py:752 msgid "final cumulative time" msgstr "tổng thời gian" -#: judge/models/contest.py:816 +#: judge/models/contest.py:828 #, python-format msgid "%s spectating in %s" msgstr "%s đang theo dõi trong %s" -#: judge/models/contest.py:821 +#: judge/models/contest.py:833 #, python-format msgid "%s in %s, v%d" msgstr "%s trong %s, v%d" -#: judge/models/contest.py:826 +#: judge/models/contest.py:838 #, python-format msgid "%s in %s" msgstr "%s trong %s" -#: judge/models/contest.py:829 +#: judge/models/contest.py:841 msgid "contest participation" msgstr "lần tham gia kỳ thi" -#: judge/models/contest.py:830 +#: judge/models/contest.py:842 msgid "contest participations" msgstr "lần tham gia kỳ thi" -#: judge/models/contest.py:837 judge/models/contest.py:890 -#: judge/models/contest.py:952 judge/models/problem.py:609 +#: judge/models/contest.py:849 judge/models/contest.py:902 +#: judge/models/contest.py:964 judge/models/problem.py:609 #: judge/models/problem.py:616 judge/models/problem.py:637 #: judge/models/problem.py:668 judge/models/problem_data.py:50 msgid "problem" msgstr "bài tập" -#: judge/models/contest.py:845 judge/models/contest.py:902 -#: judge/models/course.py:166 judge/models/problem.py:209 +#: judge/models/contest.py:857 judge/models/contest.py:914 +#: judge/models/course.py:166 judge/models/course.py:199 +#: judge/models/problem.py:209 msgid "points" msgstr "điểm" -#: judge/models/contest.py:846 +#: judge/models/contest.py:858 msgid "partial" msgstr "thành phần" -#: judge/models/contest.py:847 judge/models/contest.py:904 +#: judge/models/contest.py:859 judge/models/contest.py:916 msgid "is pretested" msgstr "dùng pretest" -#: judge/models/contest.py:848 judge/models/course.py:165 -#: judge/models/course.py:183 judge/models/interface.py:48 +#: judge/models/contest.py:860 judge/models/course.py:165 +#: judge/models/course.py:184 judge/models/course.py:198 +#: judge/models/interface.py:48 msgid "order" msgstr "thứ tự" -#: judge/models/contest.py:850 +#: judge/models/contest.py:862 msgid "visible testcases" msgstr "hiển thị test" -#: judge/models/contest.py:855 +#: judge/models/contest.py:867 msgid "Maximum number of submissions for this problem, or 0 for no limit." msgstr "Số lần nộp tối đa, đặt là 0 nếu không có giới hạn." -#: judge/models/contest.py:857 +#: judge/models/contest.py:869 msgid "max submissions" msgstr "số lần nộp tối đa" -#: judge/models/contest.py:860 +#: judge/models/contest.py:872 msgid "Why include a problem you can't submit to?" msgstr "" -#: judge/models/contest.py:864 +#: judge/models/contest.py:876 #, fuzzy #| msgid "Only for format new IOI. Separated by commas, e.g: 2, 3" msgid "Separated by commas, e.g: 2, 3" msgstr "" "Chỉ dùng với format IOI mới. Các sub cách nhau bởi dấu phẩy. Ví dụ: 2, 3" -#: judge/models/contest.py:865 +#: judge/models/contest.py:877 msgid "hidden subtasks" msgstr "Đóng băng subtasks" -#: judge/models/contest.py:877 +#: judge/models/contest.py:889 msgid "contest problem" msgstr "bài trong kỳ thi" -#: judge/models/contest.py:878 +#: judge/models/contest.py:890 msgid "contest problems" msgstr "bài trong kỳ thi" -#: judge/models/contest.py:884 judge/models/submission.py:274 +#: judge/models/contest.py:896 judge/models/submission.py:274 msgid "submission" msgstr "bài nộp" -#: judge/models/contest.py:897 judge/models/contest.py:923 +#: judge/models/contest.py:909 judge/models/contest.py:935 msgid "participation" msgstr "lần tham gia" -#: judge/models/contest.py:905 +#: judge/models/contest.py:917 msgid "Whether this submission was ran only on pretests." msgstr "Quyết định bài nộp chỉ được chạy trên pretest không." -#: judge/models/contest.py:910 +#: judge/models/contest.py:922 msgid "contest submission" msgstr "bài nộp kỳ thi" -#: judge/models/contest.py:911 +#: judge/models/contest.py:923 msgid "contest submissions" msgstr "bài nộp kỳ thi" -#: judge/models/contest.py:927 +#: judge/models/contest.py:939 msgid "rank" msgstr "rank" -#: judge/models/contest.py:928 +#: judge/models/contest.py:940 msgid "rating" msgstr "rating" -#: judge/models/contest.py:929 +#: judge/models/contest.py:941 msgid "raw rating" msgstr "rating thật" -#: judge/models/contest.py:930 +#: judge/models/contest.py:942 msgid "contest performance" msgstr "" -#: judge/models/contest.py:931 +#: judge/models/contest.py:943 msgid "last rated" msgstr "lần cuối được xếp hạng" -#: judge/models/contest.py:935 +#: judge/models/contest.py:947 msgid "contest rating" msgstr "rating kỳ thi" -#: judge/models/contest.py:936 +#: judge/models/contest.py:948 msgid "contest ratings" msgstr "rating kỳ thi" -#: judge/models/contest.py:960 +#: judge/models/contest.py:972 msgid "contest moss result" msgstr "kết quả MOSS kỳ thi" -#: judge/models/contest.py:961 +#: judge/models/contest.py:973 msgid "contest moss results" msgstr "kết quả MOSS kỳ thi" -#: judge/models/contest.py:966 +#: judge/models/contest.py:978 msgid "clarified problem" msgstr "" -#: judge/models/contest.py:968 +#: judge/models/contest.py:980 msgid "clarification body" msgstr "" -#: judge/models/contest.py:970 +#: judge/models/contest.py:982 msgid "clarification timestamp" msgstr "" -#: judge/models/contest.py:989 +#: judge/models/contest.py:1001 msgid "contests summary" msgstr "tổng kết kỳ thi" -#: judge/models/contest.py:990 +#: judge/models/contest.py:1002 msgid "contests summaries" msgstr "tổng kết kỳ thi" -#: judge/models/contest.py:1001 judge/models/contest.py:1008 +#: judge/models/contest.py:1013 judge/models/contest.py:1020 msgid "official contest category" msgstr "loại kỳ thi chính thức" -#: judge/models/contest.py:1009 +#: judge/models/contest.py:1021 msgid "official contest categories" msgstr "các loại kỳ thi chính thức" -#: judge/models/contest.py:1014 judge/models/contest.py:1021 +#: judge/models/contest.py:1026 judge/models/contest.py:1033 msgid "official contest location" msgstr "địa điểm kỳ thi chính thức" -#: judge/models/contest.py:1022 +#: judge/models/contest.py:1034 msgid "official contest locations" msgstr "các địa điểm kỳ thi chính thức" -#: judge/models/contest.py:1034 +#: judge/models/contest.py:1046 msgid "contest category" msgstr "loại kỳ thi" -#: judge/models/contest.py:1037 +#: judge/models/contest.py:1049 msgid "year" msgstr "năm" -#: judge/models/contest.py:1040 +#: judge/models/contest.py:1052 msgid "contest location" msgstr "địa điểm kỳ thi" -#: judge/models/contest.py:1045 +#: judge/models/contest.py:1057 msgid "official contest" msgstr "kỳ thi chính thức" -#: judge/models/contest.py:1046 +#: judge/models/contest.py:1058 msgid "official contests" msgstr "các kỳ thi chính thức" @@ -1371,12 +1381,16 @@ msgid "course" msgstr "khóa học" #: judge/models/course.py:163 -msgid "course title" -msgstr "tiêu đề khóa học" +#, fuzzy +#| msgid "message title" +msgid "lesson title" +msgstr "tiêu đề tin nhắn" #: judge/models/course.py:164 -msgid "course content" -msgstr "nội dung khóa học" +#, fuzzy +#| msgid "post content" +msgid "lesson content" +msgstr "đăng nội dung" #: judge/models/interface.py:29 msgid "configuration item" @@ -2819,7 +2833,7 @@ msgstr "%h:%m" msgid "M j, Y" msgstr "j M, Y" -#: judge/views/about.py:10 templates/course/course.html:5 +#: judge/views/about.py:10 templates/course/course.html:27 #: templates/organization/home.html:44 #: templates/organization/org-right-sidebar.html:74 #: templates/user/user-about.html:70 templates/user/user-tabs.html:4 @@ -2856,34 +2870,34 @@ msgstr "Bạn phải giải ít nhất 1 bài trước khi được vote." msgid "You already voted." msgstr "Bạn đã vote." -#: judge/views/comment.py:267 judge/views/organization.py:881 -#: judge/views/organization.py:1034 judge/views/organization.py:1229 +#: judge/views/comment.py:272 judge/views/organization.py:882 +#: judge/views/organization.py:1035 judge/views/organization.py:1208 msgid "Edited from site" msgstr "Chỉnh sửa từ web" -#: judge/views/comment.py:288 +#: judge/views/comment.py:293 msgid "Editing comment" msgstr "Chỉnh sửa bình luận" -#: judge/views/comment.py:340 +#: judge/views/comment.py:345 msgid "Comment body" msgstr "Nội dung bình luận" -#: judge/views/comment.py:346 judge/views/ticket.py:73 +#: judge/views/comment.py:351 judge/views/ticket.py:73 msgid "Your part is silent, little toad." msgstr "Bạn không được phép bình luận." -#: judge/views/comment.py:355 templates/comments/list.html:17 +#: judge/views/comment.py:360 templates/comments/list.html:17 msgid "" "You need to have solved at least one problem before your voice can be heard." msgstr "Bạn phải giải ít nhất một bài trước khi được phép bình luận." -#: judge/views/comment.py:398 +#: judge/views/comment.py:403 msgid "Posted comment" msgstr "Bình luận đã đăng" #: judge/views/contests.py:124 judge/views/contests.py:471 -#: judge/views/contests.py:476 judge/views/contests.py:776 +#: judge/views/contests.py:476 judge/views/contests.py:784 msgid "No such contest" msgstr "Không có contest nào như vậy" @@ -2892,10 +2906,11 @@ msgstr "Không có contest nào như vậy" msgid "Could not find a contest with the key \"%s\"." msgstr "Không tìm thấy kỳ thi với mã \"%s\"." -#: judge/views/contests.py:153 judge/views/contests.py:1553 +#: judge/views/contests.py:153 judge/views/contests.py:1561 #: judge/views/stats.py:178 templates/contest/list.html:171 #: templates/contest/list.html:213 templates/contest/list.html:250 -#: templates/contest/list.html:284 +#: templates/contest/list.html:284 templates/course/course.html:59 +#: templates/course/left_sidebar.html:5 #: templates/organization/org-left-sidebar.html:5 templates/stats/site.html:21 #: templates/user/user-bookmarks.html:19 templates/user/user-bookmarks.html:80 msgid "Contests" @@ -2909,11 +2924,11 @@ msgstr "Thời gian bắt đầu (tăng)" msgid "Start time (desc.)" msgstr "Thời gian bắt đầu (giảm)" -#: judge/views/contests.py:333 judge/views/organization.py:313 +#: judge/views/contests.py:333 judge/views/organization.py:314 msgid "Name (asc.)" msgstr "Tên (tăng)" -#: judge/views/contests.py:334 judge/views/organization.py:314 +#: judge/views/contests.py:334 judge/views/organization.py:315 msgid "Name (desc.)" msgstr "Tên (giảm)" @@ -2934,141 +2949,183 @@ msgstr "Không tìm thấy kỳ thi nào như vậy." msgid "Access to contest \"%s\" denied" msgstr "Truy cập tới kỳ thi \"%s\" bị từ chối" -#: judge/views/contests.py:562 +#: judge/views/contests.py:570 msgid "Clone Contest" msgstr "Nhân bản kỳ thi" -#: judge/views/contests.py:654 +#: judge/views/contests.py:662 msgid "Contest not ongoing" msgstr "Kỳ thi đang không diễn ra" -#: judge/views/contests.py:655 +#: judge/views/contests.py:663 #, python-format msgid "\"%s\" is not currently ongoing." msgstr "\"%s\" kỳ thi đang không diễn ra." -#: judge/views/contests.py:668 +#: judge/views/contests.py:676 msgid "Banned from joining" msgstr "Bị cấm tham gia" -#: judge/views/contests.py:670 +#: judge/views/contests.py:678 msgid "" "You have been declared persona non grata for this contest. You are " "permanently barred from joining this contest." msgstr "Bạn không được phép tham gia kỳ thi này." -#: judge/views/contests.py:760 +#: judge/views/contests.py:768 #, python-format msgid "Enter access code for \"%s\"" msgstr "Nhập mật khẩu truy cập cho \"%s\"" -#: judge/views/contests.py:777 +#: judge/views/contests.py:785 #, python-format msgid "You are not in contest \"%s\"." msgstr "Bạn không ở trong kỳ thi \"%s\"." -#: judge/views/contests.py:800 +#: judge/views/contests.py:808 msgid "ContestCalendar requires integer year and month" msgstr "Lịch thi yêu cầu giá trị cho năm và tháng là số nguyên" -#: judge/views/contests.py:858 +#: judge/views/contests.py:866 #, python-format msgid "Contests in %(month)s" msgstr "Các kỳ thi trong %(month)s" -#: judge/views/contests.py:859 +#: judge/views/contests.py:867 msgid "F Y" msgstr "F Y" -#: judge/views/contests.py:919 +#: judge/views/contests.py:927 #, python-format msgid "%s Statistics" msgstr "%s Thống kê" -#: judge/views/contests.py:1198 +#: judge/views/contests.py:1206 #, python-format msgid "%s Rankings" msgstr "%s Bảng điểm" -#: judge/views/contests.py:1209 +#: judge/views/contests.py:1217 msgid "???" msgstr "???" -#: judge/views/contests.py:1273 +#: judge/views/contests.py:1281 #, python-format msgid "Your participation in %s" msgstr "Lần tham gia trong %s" -#: judge/views/contests.py:1274 +#: judge/views/contests.py:1282 #, python-format msgid "%s's participation in %s" msgstr "Lần tham gia của %s trong %s" -#: judge/views/contests.py:1288 +#: judge/views/contests.py:1296 msgid "Live" msgstr "Trực tiếp" -#: judge/views/contests.py:1306 templates/contest/contest-tabs.html:21 +#: judge/views/contests.py:1314 templates/contest/contest-tabs.html:21 msgid "Participation" msgstr "Lần tham gia" -#: judge/views/contests.py:1355 +#: judge/views/contests.py:1363 #, python-format msgid "%s MOSS Results" msgstr "%s Kết quả MOSS" -#: judge/views/contests.py:1391 +#: judge/views/contests.py:1399 #, python-format msgid "Running MOSS for %s..." msgstr "Đang chạy MOSS cho %s..." -#: judge/views/contests.py:1414 +#: judge/views/contests.py:1422 #, python-format msgid "Contest tag: %s" msgstr "Nhãn kỳ thi: %s" -#: judge/views/contests.py:1429 judge/views/ticket.py:67 +#: judge/views/contests.py:1437 judge/views/ticket.py:67 msgid "Issue description" msgstr "Mô tả vấn đề" -#: judge/views/contests.py:1472 +#: judge/views/contests.py:1480 #, python-format msgid "New clarification for %s" msgstr "Thông báo mới cho %s" -#: judge/views/course.py:265 +#: judge/views/course.py:350 #, python-format msgid "Edit lessons for %(course_name)s" msgstr "Chỉnh sửa bài học cho %(course_name)s" -#: judge/views/course.py:269 +#: judge/views/course.py:354 #, python-format msgid "Edit lessons for %(course_name)s" msgstr "Chỉnh sửa bài học cho %(course_name)s" -#: judge/views/course.py:321 +#: judge/views/course.py:414 #, fuzzy, python-format #| msgid "Edit lessons for %(course_name)s" msgid "Grades in %(course_name)s" msgstr "Chỉnh sửa bài học cho %(course_name)s" -#: judge/views/course.py:325 +#: judge/views/course.py:418 #, python-format msgid "Grades in %(course_name)s" msgstr "Điểm trong %(course_name)s" -#: judge/views/course.py:377 +#: judge/views/course.py:472 #, fuzzy, python-format msgid "Grades of %(lesson_name)s in %(course_name)s" msgstr "Chỉnh sửa bài học cho %(course_name)s" -#: judge/views/course.py:383 +#: judge/views/course.py:478 #, fuzzy, python-format msgid "" "Grades of %(lesson_name)s in %(course_name)s" msgstr "Điểm trong %(course_name)s" +#: judge/views/course.py:493 judge/views/course.py:578 +#: templates/course/contest_list.html:20 +#, fuzzy +#| msgid "order" +msgid "Order" +msgstr "thứ tự" + +#: judge/views/course.py:540 judge/views/organization.py:930 +#: templates/organization/org-right-sidebar.html:47 +msgid "Add contest" +msgstr "Thêm kỳ thi" + +#: judge/views/course.py:549 +#, fuzzy +#| msgid "Added from site" +msgid "Added from course" +msgstr "Thêm từ web" + +#: judge/views/course.py:569 +#, fuzzy +#| msgid "Contests" +msgid "Contest list" +msgstr "Kỳ thi" + +#: judge/views/course.py:691 +#, fuzzy +#| msgid "Out contest" +msgid "Edit contest" +msgstr "Ngoài kỳ thi" + +#: judge/views/course.py:695 +#, fuzzy +#| msgid "Edited from site" +msgid "Edited from course" +msgstr "Chỉnh sửa từ web" + +#: judge/views/course.py:717 +#, fuzzy, python-format +#| msgid "Grades in %(course_name)s" +msgid "Edit %(contest_name)s" +msgstr "Điểm trong %(course_name)s" + #: judge/views/custom_file_upload.py:42 msgid "File Upload" msgstr "Tải file lên" @@ -3162,109 +3219,109 @@ msgstr "" msgid "Notifications (%d unseen)" msgstr "Thông báo (%d chưa xem)" -#: judge/views/organization.py:156 judge/views/organization.py:163 +#: judge/views/organization.py:157 judge/views/organization.py:164 msgid "No such organization" msgstr "Không có tổ chức như vậy" -#: judge/views/organization.py:157 +#: judge/views/organization.py:158 #, python-format msgid "Could not find an organization with the key \"%s\"." msgstr "Không tìm thấy tổ chức với mã \"%s\"." -#: judge/views/organization.py:164 +#: judge/views/organization.py:165 msgid "Could not find such organization." msgstr "" -#: judge/views/organization.py:188 +#: judge/views/organization.py:189 msgid "Can't edit organization" msgstr "Không thể chỉnh sửa tổ chức" -#: judge/views/organization.py:189 +#: judge/views/organization.py:190 msgid "You are not allowed to edit this organization." msgstr "Bạn không được phép chỉnh sửa tổ chức này." -#: judge/views/organization.py:201 judge/views/organization.py:399 +#: judge/views/organization.py:202 judge/views/organization.py:400 msgid "Can't access organization" msgstr "Không thể truy cập nhóm" -#: judge/views/organization.py:202 judge/views/organization.py:400 +#: judge/views/organization.py:203 judge/views/organization.py:401 msgid "You are not allowed to access this organization." msgstr "Bạn không được phép chỉnh sửa tổ chức này." -#: judge/views/organization.py:247 judge/views/stats.py:184 +#: judge/views/organization.py:248 judge/views/stats.py:184 #: templates/contest/list.html:78 templates/problem/list-base.html:90 #: templates/stats/site.html:33 templates/user/user-list-tabs.html:6 msgid "Groups" msgstr "Nhóm" -#: judge/views/organization.py:315 +#: judge/views/organization.py:316 msgid "Member count (asc.)" msgstr "Số lượng thành viên (tăng)" -#: judge/views/organization.py:316 +#: judge/views/organization.py:317 msgid "Member count (desc.)" msgstr "Số lượng thành viên (giảm)" -#: judge/views/organization.py:406 +#: judge/views/organization.py:407 #, python-format msgid "%s Members" msgstr "%s Thành viên" -#: judge/views/organization.py:528 +#: judge/views/organization.py:529 #, python-brace-format msgid "All submissions in {0}" msgstr "Bài nộp trong {0}" -#: judge/views/organization.py:536 judge/views/submission.py:858 +#: judge/views/organization.py:537 judge/views/submission.py:858 msgid "Submissions in" msgstr "Bài nộp trong" -#: judge/views/organization.py:561 judge/views/organization.py:567 -#: judge/views/organization.py:574 +#: judge/views/organization.py:562 judge/views/organization.py:568 +#: judge/views/organization.py:575 msgid "Joining group" msgstr "Tham gia nhóm" -#: judge/views/organization.py:562 +#: judge/views/organization.py:563 msgid "You are already in the group." msgstr "Bạn đã ở trong nhóm." -#: judge/views/organization.py:567 +#: judge/views/organization.py:568 msgid "This group is not open." msgstr "Nhóm này là nhóm kín." -#: judge/views/organization.py:575 +#: judge/views/organization.py:576 #, python-brace-format msgid "You may not be part of more than {count} public groups." msgstr "Bạn không thể tham gia nhiều hơn {count} nhóm công khai." -#: judge/views/organization.py:590 +#: judge/views/organization.py:591 msgid "Leaving group" msgstr "Rời nhóm" -#: judge/views/organization.py:591 +#: judge/views/organization.py:592 #, python-format msgid "You are not in \"%s\"." msgstr "Bạn không ở trong \"%s\"." -#: judge/views/organization.py:616 +#: judge/views/organization.py:617 #, python-format msgid "Request to join %s" msgstr "Đăng ký tham gia %s" -#: judge/views/organization.py:646 +#: judge/views/organization.py:647 msgid "Join request detail" msgstr "Chi tiết đơn đăng ký" -#: judge/views/organization.py:680 +#: judge/views/organization.py:681 msgid "Manage join requests" msgstr "Quản lý đơn đăng ký" -#: judge/views/organization.py:684 +#: judge/views/organization.py:685 #, python-format msgid "Managing join requests for %s" msgstr "Quản lý đơn đăng ký cho %s" -#: judge/views/organization.py:724 +#: judge/views/organization.py:725 #, python-format msgid "" "Your organization can only receive %d more members. You cannot approve %d " @@ -3273,125 +3330,121 @@ msgstr "" "Tổ chức chỉ có thể chứa %d thành viên. Bạn không thể chấp thuận nhiều hơn %d " "người." -#: judge/views/organization.py:742 +#: judge/views/organization.py:743 #, python-format msgid "Approved %d user." msgid_plural "Approved %d users." msgstr[0] "Đã chấp thuận %d người." -#: judge/views/organization.py:745 +#: judge/views/organization.py:746 #, python-format msgid "Rejected %d user." msgid_plural "Rejected %d users." msgstr[0] "Đã từ chối %d người." -#: judge/views/organization.py:785 +#: judge/views/organization.py:786 #, python-format msgid "Add member for %s" msgstr "Thêm thành viên cho %s" -#: judge/views/organization.py:801 +#: judge/views/organization.py:802 #, fuzzy #| msgid "Edited from site" msgid "Added members from site" msgstr "Chỉnh sửa từ web" -#: judge/views/organization.py:821 judge/views/organization.py:829 -#: judge/views/organization.py:838 +#: judge/views/organization.py:822 judge/views/organization.py:830 +#: judge/views/organization.py:839 msgid "Can't kick user" msgstr "Không thể đuổi" -#: judge/views/organization.py:822 +#: judge/views/organization.py:823 msgid "The user you are trying to kick does not exist!" msgstr "" -#: judge/views/organization.py:830 +#: judge/views/organization.py:831 #, python-format msgid "The user you are trying to kick is not in organization: %s." msgstr "" -#: judge/views/organization.py:839 +#: judge/views/organization.py:840 #, fuzzy #| msgid "Are you sure you want to leave this organization?" msgid "The user you are trying to kick is an organization admin." msgstr "Bạn có chắc muốn rời tổ chức?" -#: judge/views/organization.py:844 +#: judge/views/organization.py:845 #, fuzzy #| msgid "Add members" msgid "Kicked member" msgstr "Thêm thành viên" -#: judge/views/organization.py:864 judge/views/organization.py:1023 +#: judge/views/organization.py:865 judge/views/organization.py:1024 #, python-format msgid "Edit %s" msgstr "Chỉnh sửa %s" -#: judge/views/organization.py:892 templates/organization/search-form.html:19 +#: judge/views/organization.py:893 templates/organization/search-form.html:19 msgid "Create group" msgstr "Tạo nhóm" -#: judge/views/organization.py:907 +#: judge/views/organization.py:908 msgid "Exceeded limit" msgstr "" -#: judge/views/organization.py:908 +#: judge/views/organization.py:909 #, python-format msgid "You created too many groups. You can only create at most %d groups" msgstr "" -#: judge/views/organization.py:913 judge/views/organization.py:938 -#: judge/views/organization.py:1114 +#: judge/views/organization.py:914 judge/views/organization.py:939 +#: judge/views/organization.py:1093 msgid "Added from site" msgstr "Thêm từ web" -#: judge/views/organization.py:929 -#: templates/organization/org-right-sidebar.html:47 -msgid "Add contest" -msgstr "Thêm kỳ thi" - -#: judge/views/organization.py:972 judge/views/organization.py:1161 +#: judge/views/organization.py:973 judge/views/organization.py:1140 msgid "Permission denied" msgstr "Truy cập bị từ chối" -#: judge/views/organization.py:973 +#: judge/views/organization.py:974 #, fuzzy #| msgid "You are not allowed to edit this organization." msgid "You are not allowed to edit this contest" msgstr "Bạn không được phép chỉnh sửa tổ chức này." -#: judge/views/organization.py:1027 templates/blog/blog.html:31 +#: judge/views/organization.py:1028 templates/blog/blog.html:31 #: templates/comments/content-list.html:53 #: templates/comments/content-list.html:66 #: templates/contest/contest-tabs.html:36 templates/contest/macros.html:14 -#: templates/contest/tag-title.html:9 templates/flatpages/admin_link.html:3 -#: templates/license.html:10 templates/organization/blog/pending.html:56 +#: templates/contest/tag-title.html:9 templates/course/contest_list.html:25 +#: templates/flatpages/admin_link.html:3 templates/license.html:10 +#: templates/organization/blog/pending.html:56 #: templates/problem/editorial.html:15 templates/problem/feed/items.html:50 #: templates/test_formatter/download_test_formatter.html:83 msgid "Edit" msgstr "Chỉnh sửa" -#: judge/views/organization.py:1103 +#: judge/views/organization.py:1082 #, python-format msgid "Add blog for %s" msgstr "Thêm bài đăng cho %s" -#: judge/views/organization.py:1155 +#: judge/views/organization.py:1134 #, fuzzy #| msgid "Those who can edit this organization" msgid "This blog does not belong to this organization" msgstr "Những người có thể chỉnh sửa tổ chức" -#: judge/views/organization.py:1157 +#: judge/views/organization.py:1136 msgid "Not allowed to edit this blog" msgstr "Bạn không được phép chỉnh sửa bài đăng này." -#: judge/views/organization.py:1214 +#: judge/views/organization.py:1193 #, python-format msgid "Edit blog %s" msgstr "Chỉnh sửa %s" -#: judge/views/organization.py:1260 +#: judge/views/organization.py:1239 #, python-format msgid "Pending blogs in %s" msgstr "Bài đang đợi duyệt trong %s" @@ -3415,7 +3468,7 @@ msgstr "Hướng dẫn cho {0}" msgid "Editorial for {0}" msgstr "Hướng dẫn cho {0}" -#: judge/views/problem.py:460 templates/contest/contest.html:112 +#: judge/views/problem.py:460 templates/contest/contest.html:117 #: templates/course/lesson.html:14 #: templates/organization/org-left-sidebar.html:4 #: templates/profile-table.html:25 templates/user/user-about.html:28 @@ -3527,7 +3580,7 @@ msgstr "Các bài nộp tốt nhất cho %s" msgid "Best solutions for {0}" msgstr "Các bài nộp tốt nhất cho {0}" -#: judge/views/register.py:30 templates/course/grades.html:81 +#: judge/views/register.py:30 templates/course/grades.html:80 #: templates/course/grades_lesson.html:81 #: templates/registration/registration_form.html:34 #: templates/user/base-users-table.html:5 @@ -3665,8 +3718,8 @@ msgstr "Bài nộp trong {1}" #: judge/views/submission.py:900 #, python-brace-format msgid "" -"{0}'s submissions for {2} in {4}" +"{0}'s submissions for {2} in {4}" msgstr "" "Các bài nộp của {0} cho {2} trong {4}" @@ -3935,6 +3988,7 @@ msgid " posted on %(time)s" msgstr "đã đăng vào %(time)s" #: templates/blog/blog.html:35 templates/contest/contest.html:93 +#: templates/contest/contest.html:97 msgid "Edit in" msgstr "Chỉnh sửa trong" @@ -3978,7 +4032,7 @@ msgstr "Bạn không có báo cáo" msgid "Clarifications" msgstr "Thông báo" -#: templates/blog/list.html:78 +#: templates/blog/list.html:78 templates/course/contest_list.html:32 msgid "Add" msgstr "Thêm mới" @@ -4247,8 +4301,8 @@ msgstr "G:i T, j F, Y" #: templates/contest/contest-datetime.html:32 #, python-format msgid "" -"%(time_limit)s window between %(start_time)s and " -"%(end_time)s" +"%(time_limit)s window between %(start_time)s and " +"%(end_time)s" msgstr "" "Dài %(time_limit)s từ %(start_time)s đến %(end_time)s" @@ -4324,19 +4378,19 @@ msgstr "Tham gia kỳ thi" msgid "Login to participate" msgstr "Đăng nhập để tham gia" -#: templates/contest/contest.html:97 +#: templates/contest/contest.html:102 msgid "Clone" msgstr "Nhân bản" -#: templates/contest/contest.html:118 +#: templates/contest/contest.html:123 msgid "AC Rate" msgstr "Tỷ lệ AC" -#: templates/contest/contest.html:119 templates/problem/list.html:24 +#: templates/contest/contest.html:124 templates/problem/list.html:24 msgid "Users" msgstr "Người nộp" -#: templates/contest/contest.html:144 templates/problem/list.html:58 +#: templates/contest/contest.html:149 templates/problem/list.html:58 #: templates/problem/list.html:133 msgid "Editorial" msgstr "Hướng dẫn" @@ -4345,7 +4399,7 @@ msgstr "Hướng dẫn" msgid "Rank" msgstr "Rank" -#: templates/contest/contests_summary.html:37 +#: templates/contest/contests_summary.html:37 templates/course/course.html:64 #: templates/status/language-list.html:34 #: templates/user/import/table_csv.html:6 msgid "Name" @@ -4398,6 +4452,7 @@ msgid "Window ends in %(countdown)s" msgstr "Cửa số thi còn %(countdown)s" #: templates/contest/list.html:184 templates/contest/list.html:222 +#: templates/course/course.html:91 #, python-format msgid "Ends in %(countdown)s" msgstr "Kết thúc trong %(countdown)s" @@ -4431,23 +4486,25 @@ msgid "rated" msgstr "rated" #: templates/contest/macros.html:51 templates/contest/macros.html:58 +#: templates/course/contest_list.html:22 templates/course/course.html:65 msgid "Start" msgstr "Bắt đầu" -#: templates/contest/macros.html:54 +#: templates/contest/macros.html:54 templates/course/contest_list.html:23 +#: templates/course/course.html:66 msgid "End" msgstr "Kết thúc" -#: templates/contest/macros.html:62 +#: templates/contest/macros.html:62 templates/course/course.html:67 msgid "Length" msgstr "Dài" -#: templates/contest/macros.html:64 +#: templates/contest/macros.html:64 templates/course/course.html:97 #, python-format msgid "%(time_limit)s" msgstr "%(time_limit)s" -#: templates/contest/macros.html:66 +#: templates/contest/macros.html:66 templates/course/course.html:99 #, python-format msgid "%(duration)s" msgstr "%(duration)s" @@ -4639,25 +4696,35 @@ msgstr "Còn" msgid "Upcoming contests" msgstr "Kỳ thi sắp diễn ra" -#: templates/course/course.html:9 +#: templates/course/contest_list.html:29 +msgid "No contests available" +msgstr "Không có kì thi nào!" + +#: templates/course/course.html:33 msgid "Lessons" msgstr "Bài học" -#: templates/course/course.html:37 +#: templates/course/course.html:68 templates/course/grades.html:81 +#: templates/course/grades_lesson.html:82 templates/user/user-problems.html:99 +msgid "Score" +msgstr "Điểm" + +#: templates/course/course.html:119 msgid "Total achieved points" msgstr "Tổng điểm" -#: templates/course/edit_lesson.html:57 -msgid "Add new" -msgstr "Thêm mới" - -#: templates/course/edit_lesson.html:65 +#: templates/course/edit_contest.html:44 templates/course/edit_lesson.html:65 #: templates/organization/contest/edit.html:41 #: templates/organization/form.html:6 msgid "Please fix below errors" msgstr "Vui lòng sửa các lỗi bên dưới" -#: templates/course/edit_lesson.html:114 +#: templates/course/edit_contest.html:64 +#: templates/organization/contest/edit.html:61 +msgid "If you run out of rows, click Save" +msgstr "Ấn nút lưu lại nếu cần thêm hàng" + +#: templates/course/edit_contest.html:90 templates/course/edit_lesson.html:114 #: templates/markdown_editor/markdown_editor.html:122 #: templates/organization/blog/edit.html:36 #: templates/organization/contest/edit.html:87 @@ -4665,21 +4732,20 @@ msgstr "Vui lòng sửa các lỗi bên dưới" msgid "Save" msgstr "Lưu" -#: templates/course/grades.html:79 templates/course/grades_lesson.html:79 +#: templates/course/edit_lesson.html:57 +msgid "Add new" +msgstr "Thêm mới" + +#: templates/course/grades.html:78 templates/course/grades_lesson.html:79 msgid "Sort by" msgstr "Sắp xếp theo" -#: templates/course/grades.html:82 templates/course/grades_lesson.html:82 -#: templates/user/user-problems.html:99 -msgid "Score" -msgstr "Điểm" - -#: templates/course/grades.html:84 templates/course/grades_lesson.html:84 +#: templates/course/grades.html:83 templates/course/grades_lesson.html:84 #: templates/internal/problem/problem.html:34 msgid "Search" msgstr "Tìm kiếm" -#: templates/course/grades.html:99 templates/course/grades_lesson.html:99 +#: templates/course/grades.html:89 templates/course/grades_lesson.html:99 #: templates/submission/user-ajax.html:33 msgid "Total" msgstr "Tổng điểm" @@ -4688,7 +4754,7 @@ msgstr "Tổng điểm" msgid "Edit lessons" msgstr "Chỉnh sửa bài học" -#: templates/course/left_sidebar.html:5 +#: templates/course/left_sidebar.html:6 msgid "Grades" msgstr "Điểm" @@ -4838,10 +4904,6 @@ msgstr "Chấp thuận" msgid "Reject" msgstr "Từ chối" -#: templates/organization/contest/edit.html:61 -msgid "If you run out of rows, click Save" -msgstr "Ấn nút lưu lại nếu cần thêm hàng" - #: templates/organization/home-js.html:4 msgid "Are you sure you want to leave this organization?" msgstr "Bạn có chắc muốn rời tổ chức?" @@ -6355,6 +6417,12 @@ msgstr "Thông tin" msgid "Check all" msgstr "Chọn tất cả" +#~ msgid "course title" +#~ msgstr "tiêu đề khóa học" + +#~ msgid "course content" +#~ msgstr "nội dung khóa học" + #~ msgid "" #~ "This image will replace the default site logo for users viewing the " #~ "organization." @@ -6512,8 +6580,8 @@ msgstr "Chọn tất cả" #~ msgstr "bình luận nữa" #~ msgid "" -#~ "This comment is hidden due to too much negative feedback. Click here to view it." +#~ "This comment is hidden due to too much negative feedback. Click here to view it." #~ msgstr "" #~ "Bình luận bị ẩn vì nhiều phản hồi tiêu cực. Nhấp vào đây để mở." @@ -6712,9 +6780,6 @@ msgstr "Chọn tất cả" #~ msgid "Voting Statistics" #~ msgstr "Thống kê" -#~ msgid "No Votes Available!" -#~ msgstr "Không có bình chọn nào!" - #~ msgid "Median:" #~ msgstr "Trung vị:" diff --git a/locale/vi/LC_MESSAGES/dmoj-user.po b/locale/vi/LC_MESSAGES/dmoj-user.po index 4bacb85..29ad707 100644 --- a/locale/vi/LC_MESSAGES/dmoj-user.po +++ b/locale/vi/LC_MESSAGES/dmoj-user.po @@ -21,6 +21,9 @@ msgstr "Nhóm" msgid "About" msgstr "Giới thiệu" +msgid "Status" +msgstr "Máy chấm" + msgid "Courses" msgstr "Khóa học" @@ -39,9 +42,6 @@ msgstr "Đăng ký tên" msgid "Report" msgstr "Báo cáo tiêu cực" -msgid "Bug Report" -msgstr "Báo cáo lỗi" - msgid "2sat" msgstr "" @@ -597,8 +597,8 @@ msgstr "" msgid "z-function" msgstr "" -#~ msgid "Status" -#~ msgstr "Máy chấm" +#~ msgid "Bug Report" +#~ msgstr "Báo cáo lỗi" #~ msgid "Insert Image" #~ msgstr "Chèn hình ảnh" diff --git a/resources/course.scss b/resources/course.scss index 5f061ab..f7c5b2d 100644 --- a/resources/course.scss +++ b/resources/course.scss @@ -66,7 +66,7 @@ box-shadow: 0 2px 4px #ccc; } .lesson-title { - font-size: 1.5em; + font-size: 1.25em; margin-left: 1em; margin-right: 1em; color: initial; @@ -88,7 +88,7 @@ margin-top: 10px; } .progress-bar { - background: $theme_color; + background: forestgreen; height: 10px; border-radius: 3px; line-height: 10px; @@ -130,4 +130,26 @@ text-decoration: none; color: inherit; } +} + +.course-contest-card { + border: 1px solid #ddd; + border-radius: 8px; + margin-bottom: 20px; + padding: 15px; + box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1); + display: flex; + justify-content: space-between; + align-items: center; + + h5 { + margin: 0 0 10px; + font-size: 1.2em; + color: #333; + } + + p { + margin: 5px 0; + color: #555; + } } \ No newline at end of file diff --git a/resources/darkmode.css b/resources/darkmode.css index aa48690..81f4975 100644 --- a/resources/darkmode.css +++ b/resources/darkmode.css @@ -9,7 +9,7 @@ html { color-scheme: dark !important; } iframe { - color-scheme: initial; + color-scheme: dark !important; } html, body, input, textarea, select, button, dialog { background-color: #181a1b; @@ -36,21 +36,8 @@ select:-webkit-autofill { background-color: #404400 !important; color: #e8e6e3 !important; } -::-webkit-scrollbar { - background-color: #202324; - color: #aba499; -} -::-webkit-scrollbar-thumb { - background-color: #454a4d; -} -::-webkit-scrollbar-thumb:hover { - background-color: #575e62; -} -::-webkit-scrollbar-thumb:active { - background-color: #484e51; -} -::-webkit-scrollbar-corner { - background-color: #181a1b; +* { + scrollbar-color: #454a4d #202324; } ::selection { background-color: #004daa !important; @@ -63,14 +50,14 @@ select:-webkit-autofill { } /* Invert Style */ -.jfk-bubble.gtx-bubble, .captcheck_answer_label > input + img, span#closed_text > img[src^="https://www.gstatic.com/images/branding/googlelogo"], span[data-href^="https://www.hcaptcha.com/"] > #icon, ::-webkit-calendar-picker-indicator, img.Wirisformula { +.jfk-bubble.gtx-bubble, .captcheck_answer_label > input + img, span#closed_text > img[src^="https://www.gstatic.com/images/branding/googlelogo"], span[data-href^="https://www.hcaptcha.com/"] > #icon, img.Wirisformula { filter: invert(100%) hue-rotate(180deg) contrast(90%) !important; } /* Variables Style */ :root { - --darkreader-neutral-background: #131516; - --darkreader-neutral-text: #d8d4cf; + --darkreader-neutral-background: #181a1b; + --darkreader-neutral-text: #e8e6e3; --darkreader-selection-background: #004daa; --darkreader-selection-text: #e8e6e3; } @@ -1843,7 +1830,7 @@ input::placeholder { border-color: rgb(62, 68, 70); } .table th { - background-color: rgb(0, 0, 100); + background-color: rgb(174, 132, 26); border-color: rgb(62, 68, 70); color: rgb(232, 230, 227); } @@ -2185,7 +2172,7 @@ svg.rate-box.rate-target circle:last-child { color: rgb(232, 230, 227); } #users-table th a:hover { - color: rgb(26, 255, 26); + color: rgb(255, 211, 147); } #users-table tr:hover { background-color: rgb(36, 39, 40); @@ -3396,7 +3383,7 @@ div.dmmd-preview-stale { background-image: initial; } .lesson-list .progress-bar { - background-color: rgb(125, 44, 5); + background-color: rgb(27, 111, 27); background-image: initial; color: rgb(232, 230, 227); } @@ -3411,6 +3398,16 @@ div.dmmd-preview-stale { color: inherit; text-decoration-color: initial; } +.course-contest-card { + border-color: rgb(58, 62, 65); + box-shadow: rgba(0, 0, 0, 0.1) 2px 2px 10px; +} +.course-contest-card h5 { + color: rgb(200, 195, 188); +} +.course-contest-card p { + color: rgb(178, 172, 162); +} .fa-border { border: var(--darkreader-border--fa-border-width, .08em) var(--darkreader-border--fa-border-style, solid) var(--darkreader-border--fa-border-color, #35393b); } @@ -3721,12 +3718,6 @@ div.dmmd-preview-stale { border-style: initial; border-width: 0px; } -.recently-attempted ul { - list-style-image: initial; -} -.organization-row:last-child { - border-bottom: none; -} .katex * { border-color: currentcolor; } @@ -3758,9 +3749,9 @@ div.dmmd-preview-stale { /* Override Style */ .vimvixen-hint { - background-color: #7b5300 !important; - border-color: #d8b013 !important; - color: #f3e8c8 !important; + background-color: #684b00 !important; + border-color: #9e7e00 !important; + color: #d7d4cf !important; } #vimvixen-console-frame { color-scheme: light !important; @@ -3774,7 +3765,7 @@ div.dmmd-preview-stale { color: var(--darkreader-neutral-text) !important; } gr-main-header { - background-color: #0f3a48 !important; + background-color: #1b4958 !important; } .tou-z65h9k, .tou-mignzq, @@ -3783,7 +3774,7 @@ gr-main-header { background-color: var(--darkreader-neutral-background) !important; } .tou-75mvi { - background-color: #032029 !important; + background-color: #0f3a47 !important; } .tou-ta9e87, .tou-1w3fhi0, @@ -3792,13 +3783,13 @@ gr-main-header { .tou-1lpmd9d, .tou-1frrtv8, .tou-17ezmgn { - background-color: #0a0a0a !important; + background-color: #1e2021 !important; } .tou-uknfeu { - background-color: #231603 !important; + background-color: #432c09 !important; } .tou-6i3zyv { - background-color: #19576c !important; + background-color: #245d70 !important; } div.mermaid-viewer-control-panel .btn { background-color: var(--darkreader-neutral-background); diff --git a/resources/table.scss b/resources/table.scss index ff7d871..a077ed4 100644 --- a/resources/table.scss +++ b/resources/table.scss @@ -34,20 +34,12 @@ thead th { vertical-align: middle; - - &:first-child { - border-top-left-radius: $table_header_rounding; - } - - &:last-child { - border-top-right-radius: $table_header_rounding; - } } th { height: 2em; - color: white; - background-color: $widget_black; + color: black; + background-color: #DAA520; border-color: #cccccc; border-width: 1px 1px 0 0; border-style: solid; @@ -56,14 +48,6 @@ text-align: center; font-weight: 600; font-size: 1.1em; - - &:first-child { - border-top-left-radius: $table_header_rounding; - } - - &:last-child { - border-top-right-radius: $table_header_rounding; - } } td { @@ -74,19 +58,6 @@ vertical-align: middle; text-align: center; } - - // Monkey-patches for awkward table rounding - tr:not(:first-child) th { - border-radius: 0; - } - - tr:last-child th { - border-bottom-left-radius: $table_header_rounding; - } - - thead tr th { - border-bottom-left-radius: 0 !important; - } } #users-table th a { diff --git a/resources/users.scss b/resources/users.scss index 0b2a6e9..a619026 100644 --- a/resources/users.scss +++ b/resources/users.scss @@ -55,11 +55,11 @@ th.header.rank { #users-table { th a, th a:link, th a:visited { - color: white; + color: black; } th a:hover { - color: #0F0; + color: navajowhite; } .about-column { diff --git a/templates/contest/contest.html b/templates/contest/contest.html index 19e23a3..37f8cee 100644 --- a/templates/contest/contest.html +++ b/templates/contest/contest.html @@ -87,11 +87,16 @@ {% endif %} - {% if editable_organizations or is_clonable %} + {% if editable_organizations or is_clonable or editable_course %}
{% for org in editable_organizations %} [{{ _('Edit in') }} {{org.slug}}] {% endfor %} + {% if editable_course %} + + [{{ _('Edit in') }} {{editable_course.slug}}] + + {% endif %} {% if is_clonable %} [{{_('Clone')}}] diff --git a/templates/course/add_contest.html b/templates/course/add_contest.html new file mode 100644 index 0000000..c29e85e --- /dev/null +++ b/templates/course/add_contest.html @@ -0,0 +1,13 @@ +{% extends "course/base.html" %} + +{% block js_media %} + {{ form.media.js }} +{% endblock %} + +{% block two_col_media %} + {{ form.media.css }} +{% endblock %} + +{% block middle_content %} + {% include "organization/form.html" %} +{% endblock %} \ No newline at end of file diff --git a/templates/course/contest_list.html b/templates/course/contest_list.html new file mode 100644 index 0000000..2f921ad --- /dev/null +++ b/templates/course/contest_list.html @@ -0,0 +1,35 @@ +{% extends "course/base.html" %} + +{% block two_col_media %} + +{% endblock %} + +{% block middle_content %} +
+ {% if course_contests %} + {% for course_contest in course_contests %} +
+
+
{{ loop.index }}. {{ course_contest.contest.name }}
+

{{_("Order")}}: {{ course_contest.order }}

+

{{_("Points")}}: {{ course_contest.points }}

+

{{_("Start")}}: {{ course_contest.contest.start_time|date(_("H:i d/m/Y")) }}

+

{{_("End")}}: {{ course_contest.contest.end_time|date(_("H:i d/m/Y")) }}

+
+ {{ _('Edit') }} +
+ {% endfor %} + {% else %} +

{{_("No contests available")}}.

+ {% endif %} + + + +
+{% endblock %} \ No newline at end of file diff --git a/templates/course/course.html b/templates/course/course.html index f5ba8c8..6af2cc5 100644 --- a/templates/course/course.html +++ b/templates/course/course.html @@ -1,36 +1,118 @@ {% extends "course/base.html" %} +{% block two_col_media %} + +{% endblock %} + +{% block js_media %} + +{% endblock %} + {% block middle_content %}

{{title}}

{{_("About")}}

{{ course.about|markdown|reference|str|safe }}
-

{{_("Lessons")}}

- +
+
{{progress['percentage']|floatformat(0)}}%
+
+ + + {% endfor %} + + {% endif %} + {% if course_contests %} +
+

{{_("Contests")}}

+
+ + + + + + + + + + + + {% for course_contest in course_contests %} + {% set contest = course_contest.contest %} + {% set progress = contest_progress[course_contest.id] %} + + + + + + + + {% endfor %} + +
{{_("Name")}}{{_("Start")}}{{_("End")}}{{_("Length")}}{{_("Score")}}
+ {{ contest.name }} + + {{ contest.start_time|date(_("H:i d/m/Y")) }} +
+ {% if contest.time_before_start %} + {{ _('Starting in %(countdown)s.', countdown=contest.start_time|as_countdown) }} + {% endif %} +
+
+ {{ contest.end_time|date(_("H:i d/m/Y"))}} +
+ {% if contest.time_before_end %} + {% trans countdown=contest.end_time|as_countdown %}Ends in {{ countdown }}{% endtrans %} + {% endif %} +
+
+ {% if contest.time_limit %} + {% trans time_limit=contest.time_limit|timedelta('localized-no-seconds') %}{{ time_limit }}{% endtrans %} + {% else %} + {% trans duration=contest.contest_window_length|timedelta('localized-no-seconds') %}{{ duration }}{% endtrans %} + {% endif %} + + {% if progress['total_points'] %} + {{ (progress['achieved_points'] / progress['total_points'] * course_contest.points) | floatformat(1) }} / {{ course_contest.points }} + {% else %} + 0 / {{ course_contest.points }} + {% endif %} +
+ {% endif %} +

- {% set total_progress = lesson_progress['total'] %} {% set achieved_points = total_progress['achieved_points'] %} {% set total_points = total_progress['total_points'] %} {% set percentage = total_progress['percentage'] %} diff --git a/templates/course/edit_contest.html b/templates/course/edit_contest.html new file mode 100644 index 0000000..9e93f33 --- /dev/null +++ b/templates/course/edit_contest.html @@ -0,0 +1,92 @@ +{% extends "course/base.html" %} + +{% block js_media %} + {{ form.media.js }} +{% endblock %} + +{% block two_col_media %} + {{ form.media.css }} + +{% endblock %} + +{% block middle_content %} +

{{content_title}}

+
+ {% csrf_token %} + {% if form.errors or problems_form.errors %} +
+ x + {{_("Please fix below errors")}} +
+ {% endif %} + {% for field in form %} + {% if not field.is_hidden %} +
+ {{ field.errors }} + +
+ {{ field }} +
+ {% if field.help_text %} + {{ field.help_text|safe }} + {% endif %} +
+ {% endif %} + {% endfor %} + +

+ {{ problems_form.management_form }} + {{_('If you run out of rows, click Save')}} + + + + {% for field in problems_form[0] %} + {% if not field.is_hidden %} + + {% endif %} + {% endfor %} + + + + {% for form in problems_form %} + + {% for field in form %} + + {% endfor %} + + {% endfor %} + +
+ {{field.label}} +
{{field}}
{{field.errors}}
+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/course/grades.html b/templates/course/grades.html index ee39ad5..5dba12a 100644 --- a/templates/course/grades.html +++ b/templates/course/grades.html @@ -75,58 +75,84 @@ {% block middle_content %}

{{content_title}}

- {% set lessons = course.lessons.order_by('order') %} {{_("Sort by")}}: - - - - - {% if grades|length > 0 %} +
+
{{_('Student')}}
+ + + + {% for lesson in lessons %} - {% endfor %} - {% endif %} - - - - - {% for student, grade in grades.items() %} - - - {% for lesson in lessons %} - {% set val = grade.get(lesson.id) %} - {% endfor %} - - {% endfor %} - -
{{_('Student')}}{{_('Total')}} + - {{ lesson.title }} + L{{ loop.index0 + 1 }}
{{lesson.points}}
{{_('Total')}}
-
- {{link_user(student)}} -
-
- {{student.first_name}} -
-
- - {% if val and val['total_points'] %} - {{ (val['achieved_points'] / val['total_points'] * lesson.points) | floatformat(0) }} - {% else %} - 0 - {% endif %} + {% for course_contest in course_contests %} + + + C{{ loop.index0 + 1 }} +
{{course_contest.points}}
- +
- {{ grade['total']['percentage'] | floatformat(0) }}% -
+ + + {% for student in grade_total.keys() %} + {% set grade_lessons_student = grade_lessons.get(student) %} + {% set grade_contests_student = grade_contests.get(student) %} + {% set grade_total_student = grade_total.get(student) %} + + +
+ {{link_user(student)}} +
+
+ {{student.first_name}} +
+ + + {% if grade_total_student %} + {{ grade_total_student['percentage'] | floatformat(0) }}% + {% else %} + 0 + {% endif %} + + {% for lesson in lessons %} + {% set val = grade_lessons_student.get(lesson.id) %} + + + {% if val and val['total_points'] %} + {{ (val['achieved_points'] / val['total_points'] * lesson.points) | floatformat(0) }} + {% else %} + 0 + {% endif %} + + + {% endfor %} + {% for course_contest in course_contests %} + {% set val = grade_contests_student.get(course_contest.id) %} + + + {% if val and val['total_points'] %} + {{ (val['achieved_points'] / val['total_points'] * course_contest.points) | floatformat(0) }} + {% else %} + 0 + {% endif %} + + + {% endfor %} + + {% endfor %} + + +

{% endblock %} \ No newline at end of file diff --git a/templates/course/left_sidebar.html b/templates/course/left_sidebar.html index 77955f7..2454d97 100644 --- a/templates/course/left_sidebar.html +++ b/templates/course/left_sidebar.html @@ -2,6 +2,7 @@ {{ make_tab_item('home', 'fa fa-home', course.get_absolute_url(), _('Home')) }} {% if is_editable %} {{ make_tab_item('edit_lesson', 'fa fa-edit', url('edit_course_lessons', course.slug), _('Edit lessons')) }} + {{ make_tab_item('contests', 'fa fa-ranking-star', url('course_contest_list', course.slug), _('Contests')) }} {{ make_tab_item('grades', 'fa fa-check-square', url('course_grades', course.slug), _('Grades')) }} {% endif %} {% if perms.judge.change_course %}