diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 87408384..e35cec7d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,17 @@ repos: - repo: https://github.com/rtts/djhtml - rev: 'v1.5.2' # replace with the latest tag on GitHub + rev: 3.0.7 # replace with the latest tag on GitHub hooks: - id: djhtml - entry: djhtml -i -t 2 + entry: djhtml -t 2 files: templates/. - id: djcss types: [scss] - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 24.10.0 hooks: - id: black - repo: https://github.com/hadialqattan/pycln - rev: 'v2.3.0' + rev: 'v2.4.0' hooks: - id: pycln diff --git a/dmoj/urls.py b/dmoj/urls.py index 1f077f40..7a0bdc1d 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -1,20 +1,18 @@ -import chat_box.views as chat - from django.conf import settings from django.conf.urls import include, url +from django.conf.urls.static import static as url_static from django.contrib import admin from django.contrib.auth import views as auth_views +from django.contrib.auth.decorators import login_required from django.contrib.sitemaps.views import sitemap -from django.http import Http404, HttpResponsePermanentRedirect, HttpResponseRedirect +from django.http import Http404, HttpResponsePermanentRedirect from django.templatetags.static import static from django.urls import reverse -from django.utils.functional import lazystr from django.utils.translation import ugettext_lazy as _ from django.views.generic import RedirectView -from django.contrib.auth.decorators import login_required -from django.conf.urls.static import static as url_static - +import chat_box.views as chat +from judge import authentication from judge.forms import CustomAuthenticationForm from judge.sitemap import ( BlogPostSitemap, @@ -37,7 +35,6 @@ license, mailgun, markdown_editor, - test_formatter, notification, organization, preview, @@ -62,10 +59,6 @@ email, custom_file_upload, ) -from judge import authentication - -from judge.views.test_formatter import test_formatter - from judge.views.problem_data import ( ProblemDataView, ProblemSubmissionDiff, @@ -86,6 +79,7 @@ UserSelect2View, ProblemAuthorSearchSelect2View, ) +from judge.views.test_formatter import test_formatter admin.autodiscover() @@ -554,11 +548,21 @@ def paged_list_view(view, name, **kwargs): course.CourseLessonDetail.as_view(), name="course_lesson_detail", ), + url( + r"^/lesson/create$", + course.CreateCourseLesson.as_view(), + name="course_lesson_create", + ), url( r"^/edit_lessons$", course.EditCourseLessonsView.as_view(), name="edit_course_lessons", ), + url( + r"^/edit_lessons_new/(?P\d+)$", + course.EditCourseLessonsViewNewWindow.as_view(), + name="edit_course_lessons_new", + ), url( r"^/grades$", course.CourseStudentResults.as_view(), diff --git a/judge/views/course.py b/judge/views/course.py index 7514d6f4..8d742b37 100644 --- a/judge/views/course.py +++ b/judge/views/course.py @@ -1,34 +1,43 @@ -from django.utils.html import mark_safe -from django.db import models -from django.views.generic import ListView, DetailView, View -from django.utils.translation import gettext, gettext_lazy as _ -from django.http import Http404 from django import forms +from django.contrib import messages +from django.core.exceptions import ObjectDoesNotExist +from django.db.models import Max, F, Sum from django.forms import ( inlineformset_factory, ModelForm, modelformset_factory, - BaseModelFormSet, ) -from django.views.generic.edit import FormView -from django.shortcuts import get_object_or_404 +from django.http import Http404, HttpResponseRedirect +from django.shortcuts import get_object_or_404, redirect from django.urls import reverse_lazy, reverse -from django.db.models import Max, F, Sum -from django.core.exceptions import ObjectDoesNotExist +from django.utils.html import mark_safe +from django.utils.translation import gettext_lazy as _ +from django.views.generic import ListView, DetailView +from django.views.generic.edit import FormView +from reversion import revisions +from judge.forms import ( + ContestProblemFormSet, +) from judge.models import ( Course, Contest, CourseLesson, Submission, Profile, - CourseRole, CourseLessonProblem, CourseContest, ContestProblem, ContestParticipation, ) from judge.models.course import RoleInCourse +from judge.utils.contest import ( + maybe_trigger_contest_rescore, +) +from judge.utils.problems import ( + user_attempted_ids, + user_completed_ids, +) from judge.widgets import ( HeavyPreviewPageDownWidget, HeavySelect2MultipleWidget, @@ -37,17 +46,6 @@ 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): @@ -92,9 +90,11 @@ def calculate_lessons_progress(profile, lessons): res["total"] = { "achieved_points": total_achieved_points, "total_points": total_lesson_points, - "percentage": total_achieved_points / total_lesson_points * 100 - if total_lesson_points - else 0, + "percentage": ( + total_achieved_points / total_lesson_points * 100 + if total_lesson_points + else 0 + ), } return res @@ -135,9 +135,11 @@ def calculate_contests_progress(profile, course_contests): 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, + "percentage": ( + total_achieved_points / total_contest_points * 100 + if total_contest_points + else 0 + ), } return res @@ -319,6 +321,177 @@ class Meta: ) +class CreateCourseLesson(CourseEditableMixin, FormView): + template_name = "course/create_lesson.html" + form_class = CourseLessonFormSet + other_form = CourseLessonForm + model = CourseLesson + + def get_context_data(self, **kwargs): + context = super(CreateCourseLesson, self).get_context_data(**kwargs) + + context["problem_formsets"] = CourseLessonProblemFormSet() + context["title"] = _("Edit lessons for %(course_name)s") % { + "course_name": self.course.name + } + context["content_title"] = mark_safe( + _("Edit lessons for %(course_name)s") + % { + "course_name": self.course.name, + "url": self.course.get_absolute_url(), + } + ) + context["page_type"] = "edit_lesson_new" + context["lesson_field"] = CourseLessonForm() + + return context + + def post(self, request, *args, **kwargs): + form = self.get_form(form_class=CourseLessonForm) # Get the CourseLessonForm + + if form.is_valid(): + form.instance.course_id = self.course.id + self.lesson = form.save() + formset = CourseLessonProblemFormSet( + data=self.request.POST, + prefix=f"problems_{self.lesson.id}" if self.lesson else "problems", + queryset=CourseLessonProblem.objects.filter( + lesson=self.lesson + ).order_by("order"), + ) + for problem_formset in formset: + problem_formset.save() + return self.form_valid(form) + else: + return self.form_invalid(form) + + def form_valid(self, form): + return super().form_valid(form) + + def form_invalid(self, form): + return self.render_to_response(self.get_context_data(form=form)) + + def get_success_url(self): + return reverse( + "edit_course_lessons", + args=[self.course.slug], + ) + + +class EditCourseLessonsViewNewWindow(CourseEditableMixin, FormView): + template_name = "course/edit_lesson_new_window.html" + form_class = CourseLessonFormSet + other_form = CourseLessonForm + model = CourseLesson + + def dispatch(self, request, *args, **kwargs): + self.lesson = CourseLesson.objects.get(id=kwargs["id"]) + res = super().dispatch(request, *args, **kwargs) + if not self.lesson.id: + return HttpResponseRedirect( + reverse( + "edit_course_lessons", + args=[self.course.slug], + ) + ) + return res + + def get(self, request, *args, **kwargs): + try: + self.lesson = CourseLesson.objects.get(id=kwargs["id"]) + + return super().get(request, *args, **kwargs) + except ObjectDoesNotExist: + raise Http404() + + def get_problem_formset(self, post=False, lesson=None): + formset = CourseLessonProblemFormSet( + data=self.request.POST if post else None, + prefix=f"problems_{lesson.id}" if lesson else "problems", + queryset=CourseLessonProblem.objects.filter(lesson=self.lesson).order_by( + "order" + ), + ) + if lesson: + for form in formset: + form.fields["lesson"].initial = self.lesson + return formset + + def get_context_data(self, **kwargs): + context = super(EditCourseLessonsViewNewWindow, self).get_context_data(**kwargs) + if self.request.method != "POST": + context["formset"] = self.form_class( + instance=self.course, queryset=self.course.lessons.order_by("order") + ) + context["problem_formsets"] = { + self.lesson.id: self.get_problem_formset(post=False, lesson=self.lesson) + for lesson in context["formset"].forms + if lesson.instance.id + } + + context["title"] = _("Edit lessons for %(course_name)s") % { + "course_name": self.course.name + } + context["content_title"] = mark_safe( + _("Edit lessons for %(course_name)s") + % { + "course_name": self.course.name, + "url": self.course.get_absolute_url(), + } + ) + context["page_type"] = "edit_lesson_new" + context["lesson_field"] = CourseLessonForm(instance=self.lesson) + context["lesson"] = self.lesson + + return context + + def post(self, request, *args, **kwargs): + form = self.get_form(form_class=CourseLessonForm) # Get the CourseLessonForm + if form.is_valid(): + if "delete_lesson" in request.POST: + form.instance.course_id = self.course.id + form.instance.lesson_id = self.lesson.id + self.lesson.delete() + messages.success(request, "Lesson deleted successfully.") + course_slug = self.course.slug + return HttpResponseRedirect( + reverse( + "edit_course_lessons", + args=[course_slug], + ) + ) + else: + form.instance.course_id = self.course.id + form.instance.id = self.lesson.id + self.lesson = form.save() + problem_formsets = self.get_problem_formset( + post=True, lesson=self.lesson + ) + if problem_formsets.is_valid(): + problem_formsets.save() + for obj in problem_formsets.deleted_objects: + if obj.pk is not None: + obj.delete() + return self.form_valid(form) + else: + return self.form_invalid(form) + + def form_valid(self, form): + if "delete_lesson" in self.request.POST: + return redirect("edit_course_lessons", slug=self.course.slug) + else: + return super().form_valid(form) + + def form_invalid(self, form): + return self.render_to_response(self.get_context_data(form=form)) + + def get_success_url(self): + return reverse( + "edit_course_lessons", + args=[self.course.slug], + ) + + class EditCourseLessonsView(CourseEditableMixin, FormView): template_name = "course/edit_lesson.html" form_class = CourseLessonFormSet @@ -496,9 +669,9 @@ def get_lesson_grades(self): grades[s]["total"] = { "achieved_points": achieved_points, "total_points": total_points, - "percentage": achieved_points / total_points * 100 - if total_points - else 0, + "percentage": ( + achieved_points / total_points * 100 if total_points else 0 + ), } return grades diff --git a/judge/views/register.py b/judge/views/register.py index 58d1d6f2..747b5ced 100644 --- a/judge/views/register.py +++ b/judge/views/register.py @@ -13,7 +13,6 @@ RegistrationView as OldRegistrationView, ) from registration.forms import RegistrationForm -from sortedm2m.forms import SortedMultipleChoiceField from judge.models import Language, Profile, TIMEZONE from judge.utils.recaptcha import ReCaptchaField, ReCaptchaWidget diff --git a/judge/widgets/mixins.py b/judge/widgets/mixins.py index 6317bac5..21c465d3 100644 --- a/judge/widgets/mixins.py +++ b/judge/widgets/mixins.py @@ -34,7 +34,7 @@ class CompressorWidgetMixin(object): compress_js = False try: - import compressor + pass except ImportError: pass else: @@ -47,10 +47,14 @@ def media(self): result = html.fromstring(template.render(Context({"media": media}))) return forms.Media( - css={"all": [result.find(".//link").get("href")]} - if self.compress_css - else media._css, - js=[result.find(".//script").get("src")] - if self.compress_js - else media._js, + css=( + {"all": [result.find(".//link").get("href")]} + if self.compress_css + else media._css + ), + js=( + [result.find(".//script").get("src")] + if self.compress_js + else media._js + ), ) diff --git a/templates/course/create_lesson.html b/templates/course/create_lesson.html new file mode 100644 index 00000000..bf2490af --- /dev/null +++ b/templates/course/create_lesson.html @@ -0,0 +1,56 @@ +{% extends "course/base.html" %} + +{% block two_col_media %} + {{ form.media.css }} + +{% endblock %} + +{% block js_media %} + {{ form.media.js }} + +{% endblock %} + +{% block middle_content %} + +

Create new lesson

+ +
+ {% csrf_token %} +
+ {% for field in lesson_field %} + {% if field %} +
+ {{ field.errors }} + +
+ {{ field }} +
+ {% if field.help_text %} + {{ field.help_text|safe }} + {% endif %} +
+ {% endif %} + {% endfor %} + +
+
+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/course/edit_lesson.html b/templates/course/edit_lesson.html index f84adff7..9dae18b0 100644 --- a/templates/course/edit_lesson.html +++ b/templates/course/edit_lesson.html @@ -9,6 +9,14 @@ .form-header { margin-bottom: 0.5em; } + .title { + font-size:20px; + padding:3px; + } + .title:hover { + color: blue !important; + text-decoration: underline; + } {% endblock %} @@ -36,6 +44,9 @@ {% set ns = namespace(problem_formset_has_error=false) %} {% if lesson_form.instance.id %} +
+ {{lesson_form.order.value()}}.{{lesson_form.title.value()}} +
{% set problem_formset = problem_formsets[lesson_form.instance.id] %} {% for form in problem_formset %} {% if form.errors %} @@ -44,68 +55,9 @@ {% endif %} {% endfor %} {% endif %} -

- - {% if lesson_form.title.value() %} - {{lesson_form.order.value()}}. {{lesson_form.title.value()}} - {% else %} - + {{_("Add new")}} - {% endif %} -

-
- {{lesson_form.id}} - {% if lesson_form.errors %} -
- x - {{_("Please fix below errors")}} -
- {% endif %} - {% for field in lesson_form %} - {% if not field.is_hidden %} -
- {{ field.errors }} - -
- {{ field }} -
- {% if field.help_text %} - {{ field.help_text|safe }} - {% endif %} -
- {% endif %} - {% endfor %} - - - {% if problem_formset %} - {{ problem_formset.management_form }} - - - - {% for field in problem_formset.forms.0 %} - {% if not field.is_hidden %} - - {% endif %} - {% endfor %} - - - - {% for form in problem_formset %} - - {% for field in form %} - - {% endfor %} - - {% endfor %} - -
- {{field.label}} -
- {{field}}
{{field.errors}}
-
- {% endif %} -
-
{% endfor %} - + {% endblock %} \ No newline at end of file diff --git a/templates/course/edit_lesson_new_window.html b/templates/course/edit_lesson_new_window.html new file mode 100644 index 00000000..a374a39c --- /dev/null +++ b/templates/course/edit_lesson_new_window.html @@ -0,0 +1,108 @@ +{% extends "course/base.html" %} + +{% block two_col_media %} + {{ form.media.css }} + +{% endblock %} + +{% block js_media %} + {{ form.media.js }} + +{% endblock %} + +{% block middle_content %} + {% if lesson.id %} +

{{lesson.title}}

+ {% endif %} +
+ {% csrf_token %} + {{ formset.management_form }} + {% set ns = namespace(problem_formset_has_error=false) %} + + {% if lesson.id %} + {% set problem_formset = problem_formsets[lesson.id] %} + {% for form in problem_formset %} + {% if form.errors %} + {% set ns.problem_formset_has_error = true %} + {% break %} + {% endif %} + {% endfor %} + {% endif %} + +
+ {% if lesson.errors %} +
+ x + {{_("Please fix below errors")}} +
+ {% endif %} + {% for field in lesson_field %} + {% if field %} +
+ {{ field.errors }} + +
+ {{ field }} +
+ {% if field.help_text %} + {{ field.help_text|safe }} + {% endif %} +
+ {% endif %} + {% endfor %} + + + {% if problem_formset %} + {{ problem_formset.management_form }} + + + + {% for field in problem_formset.forms.0 %} + {% if not field.is_hidden %} + + {% endif %} + {% endfor %} + + + + {% for form in problem_formset %} + + {% for field in form %} + + {% endfor %} + + {% endfor %} + +
+ {{field.label}} +
+ {{field}}
{{field.errors}}
+
+ {% endif %} +
+
+
+ + +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/course/grades_lesson.html b/templates/course/grades_lesson.html index 0701b589..ce6a61b0 100644 --- a/templates/course/grades_lesson.html +++ b/templates/course/grades_lesson.html @@ -1,132 +1,135 @@ -{% extends "course/base.html" %} - -{% block two_col_media %} - -{% endblock %} - -{% block js_media %} - -{% endblock %} - -{% block middle_content %} -

{{content_title}}

- {% set lesson_problems = lesson.lesson_problems.order_by('order') %} - {{_("Sort by")}}: - - - - - - - {% if grades|length > 0 %} - {% for lesson_problem in lesson_problems %} - - {% endfor %} - {% endif %} - - - - - {% for student, grade in grades.items() %} - - - {% for lesson_problem in lesson_problems %} - {% set val = grade.get(lesson_problem.problem.id) %} - - {% endfor %} - - - {% endfor %} - -
{{_('Student')}} - - {{ lesson_problem.problem.name }} -
{{lesson_problem.score}}
-
-
{{_('Total')}}
-
- {{link_user(student)}} -
-
- {{student.first_name}} -
-
- - {% if val and val['case_total'] %} - {{ (val['case_points'] / val['case_total'] * lesson_problem.score) | floatformat(0) }} - {% else %} - 0 - {% endif %} - - - {{ grade['total']['percentage'] | floatformat(0) }}% -
+{% extends "course/base.html" %} + +{% block two_col_media %} + +{% endblock %} + +{% block js_media %} + +{% endblock %} + +{% block middle_content %} +

{{content_title}}

+ {% set lesson_problems = lesson.lesson_problems.order_by('order') %} + {{_("Sort by")}}: + + +
+ + + + + {% if grades|length > 0 %} + {% for lesson_problem in lesson_problems %} + + {% endfor %} + {% endif %} + + + + + {% for student, grade in grades.items() %} + + + {% for lesson_problem in lesson_problems %} + {% set val = grade.get(lesson_problem.problem.id) %} + + {% endfor %} + + + {% endfor %} + +
{{_('Student')}} + + P({{ loop.index0 }}) +
{{lesson_problem.score}}
+
+
{{_('Total')}}
+
+ {{link_user(student)}} +
+
+ {{student.first_name}} +
+
+ + {% if val and val['case_total'] %} + {{ (val['case_points'] / val['case_total'] * lesson_problem.score) | floatformat(0) }} + {% else %} + 0 + {% endif %} + + + {{ grade['total']['percentage'] | floatformat(0) }}% +
+
{% endblock %} \ No newline at end of file