From 9f8686e5928726c627b3a602d8510ab90ff65096 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 23 Aug 2023 17:21:14 +0200 Subject: [PATCH] Improve django admin --- .../src/components/dueDates/dueDatesUtils.ts | 3 +- client/src/locales/de/translation.json | 7 +- client/src/locales/fr/translation.json | 2 +- client/src/locales/it/translation.json | 2 +- server/vbv_lernwelt/course/admin.py | 16 +- .../migrations/0004_auto_20230823_1744.py | 26 +++ server/vbv_lernwelt/course/models.py | 7 +- server/vbv_lernwelt/course_session/admin.py | 163 +++++++++++++++++- server/vbv_lernwelt/course_session/models.py | 9 + server/vbv_lernwelt/duedate/admin.py | 21 ++- server/vbv_lernwelt/duedate/models.py | 12 +- ...3_alter_feedbackresponse_course_session.py | 21 +++ server/vbv_lernwelt/feedback/models.py | 2 +- server/vbv_lernwelt/importer/services.py | 25 ++- 14 files changed, 294 insertions(+), 22 deletions(-) create mode 100644 server/vbv_lernwelt/course/migrations/0004_auto_20230823_1744.py create mode 100644 server/vbv_lernwelt/feedback/migrations/0003_alter_feedbackresponse_course_session.py diff --git a/client/src/components/dueDates/dueDatesUtils.ts b/client/src/components/dueDates/dueDatesUtils.ts index 78734c49..333a6893 100644 --- a/client/src/components/dueDates/dueDatesUtils.ts +++ b/client/src/components/dueDates/dueDatesUtils.ts @@ -1,4 +1,5 @@ import type { Dayjs } from "dayjs"; +import i18next from "i18next"; export const formatDate = (start: Dayjs, end: Dayjs) => { const startDateString = getDateString(start); @@ -7,7 +8,7 @@ export const formatDate = (start: Dayjs, end: Dayjs) => { // if start isundefined, dont show the day twice if (!start.isValid() && !end.isValid()) { - return "Termin nicht festgelegt"; + return i18next.t("Termin nicht festgelegt"); } if (!start || (!start.isValid() && end.isValid())) { diff --git a/client/src/locales/de/translation.json b/client/src/locales/de/translation.json index 901435f6..e46a5474 100644 --- a/client/src/locales/de/translation.json +++ b/client/src/locales/de/translation.json @@ -24,7 +24,7 @@ "assignmentSubmitted": "Du hast deine Ergebnisse erfolgreich abgegeben.", "confirmSubmitPerson": "Hiermit bestätige ich, dass die folgende Person meine Ergebnisse bewerten soll.", "confirmSubmitResults": "Hiermit bestätige ich, dass ich die Zusammenfassung meiner Ergebnisse überprüft habe und so abgeben will.", - "dueDateEvaluation": "assignment.dueDateEvaluation", + "dueDateEvaluation": "Freigabetermin Bewertung", "dueDateIntroduction": "Reiche deine Ergebnisse pünktlich ein bis am: ", "dueDateNotSet": "Keine Abgabedaten wurden erfasst für diese Durchführung", "dueDateSubmission": "Abgabetermin", @@ -109,7 +109,7 @@ "showAllDueDates": "Alle Termine anzeigen" }, "edoniqTest": { - "qualifiesForExtendedTime": "edoniqTest.qualifiesForExtendedTime" + "qualifiesForExtendedTime": "Ich habe Anrecht auf einen Nachteilsausgleich" }, "feedback": { "answers": "Antworten", @@ -206,6 +206,7 @@ "attendanceCourse": "Präsenzkurs", "casework": "Geleitete Fallarbeit", "documents": "Dokumente", + "edoniqTest": "Wissens- und Verständnisfragen", "feedback": "Feedback", "learningModule": "Lernmodul", "placeholder": "In Umsetzung", @@ -274,7 +275,7 @@ "selfEvaluationNo": "@:selfEvaluation: Muss ich nochmals anschauen.", "selfEvaluationYes": "@:selfEvaluation: Ich kann das.", "steps": "Schritt {{current}} von {{max}}", - "title": "@:selfEvaluation.selfEvaluation {{title}}", + "title": "Selbsteinschätzung {{title}}", "yes": "Ja, ich kann das" }, "settings": { diff --git a/client/src/locales/fr/translation.json b/client/src/locales/fr/translation.json index 2e7f167d..e45552fc 100644 --- a/client/src/locales/fr/translation.json +++ b/client/src/locales/fr/translation.json @@ -275,7 +275,7 @@ "selfEvaluationNo": "@:selfEvaluation: Il faut que je regarde cela encore une fois de plus près.", "selfEvaluationYes": "@:selfEvaluation: Je maîtrise cette question.", "steps": "Étape {{current}} sur {{max}}", - "title": "@:selfEvaluation.selfEvaluation {{title}}", + "title": "Selbsteinschätzung {{title}}", "yes": "Oui, je maîtrise cette question" }, "settings": { diff --git a/client/src/locales/it/translation.json b/client/src/locales/it/translation.json index e3044c50..cae917bf 100644 --- a/client/src/locales/it/translation.json +++ b/client/src/locales/it/translation.json @@ -275,7 +275,7 @@ "selfEvaluationNo": "@:selfEvaluation: Devo riguardarlo ancora una volta.", "selfEvaluationYes": "@:selfEvaluation: Ho compreso tutto.", "steps": "Passo {{current}} di {{max}}", - "title": "@:selfEvaluation.selfEvaluation {{title}}", + "title": "Selbsteinschätzung {{title}}", "yes": "Sì, ho compreso tutto" }, "settings": { diff --git a/server/vbv_lernwelt/course/admin.py b/server/vbv_lernwelt/course/admin.py index d4351a2c..3a83f956 100644 --- a/server/vbv_lernwelt/course/admin.py +++ b/server/vbv_lernwelt/course/admin.py @@ -33,6 +33,8 @@ class CourseSessionUserAdmin(admin.ModelAdmin): date_hierarchy = "created_at" list_display = [ "user", + "user_last_name", + "user_first_name", "course_session", "role", "created_at", @@ -45,13 +47,25 @@ class CourseSessionUserAdmin(admin.ModelAdmin): "course_session__title", ] list_filter = [ - "course_session", "role", + "course_session", ] raw_id_fields = [ "user", ] + def user_first_name(self, obj): + return obj.user.first_name + + user_first_name.short_description = "First Name" + user_first_name.admin_order_field = "user__first_name" + + def user_last_name(self, obj): + return obj.user.last_name + + user_last_name.short_description = "Last Name" + user_last_name.admin_order_field = "user__last_name" + fieldsets = [ (None, {"fields": ("user", "course_session", "role")}), ( diff --git a/server/vbv_lernwelt/course/migrations/0004_auto_20230823_1744.py b/server/vbv_lernwelt/course/migrations/0004_auto_20230823_1744.py new file mode 100644 index 00000000..ab084612 --- /dev/null +++ b/server/vbv_lernwelt/course/migrations/0004_auto_20230823_1744.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.20 on 2023-08-23 15:44 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("course", "0003_alter_coursecompletion_additional_json_data"), + ] + + operations = [ + migrations.AlterModelOptions( + name="course", + options={}, + ), + migrations.AlterModelOptions( + name="coursesession", + options={"ordering": ["title"]}, + ), + migrations.AlterModelOptions( + name="coursesessionuser", + options={ + "ordering": ["user__last_name", "user__first_name", "user__email"] + }, + ), + ] diff --git a/server/vbv_lernwelt/course/models.py b/server/vbv_lernwelt/course/models.py index f5e77d67..b4097344 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -24,9 +24,6 @@ class Course(models.Model): _("Slug"), max_length=255, unique=True, blank=True, allow_unicode=True ) - class Meta: - verbose_name = _("Lehrgang") - def get_course_url(self): return f"/course/{self.slug}" @@ -245,6 +242,9 @@ class CourseSession(models.Model): def __str__(self): return f"{self.title}" + class Meta: + ordering = ["title"] + class CourseSessionUser(models.Model): """ @@ -280,6 +280,7 @@ class CourseSessionUser(models.Model): name="course_session_user_unique_course_session_user", ) ] + ordering = ["user__last_name", "user__first_name", "user__email"] def to_dict(self): return { diff --git a/server/vbv_lernwelt/course_session/admin.py b/server/vbv_lernwelt/course_session/admin.py index 7fbd14e1..89d51c78 100644 --- a/server/vbv_lernwelt/course_session/admin.py +++ b/server/vbv_lernwelt/course_session/admin.py @@ -1,9 +1,12 @@ +from django import forms from django.contrib import admin from vbv_lernwelt.course_session.models import ( CourseSessionAssignment, CourseSessionAttendanceCourse, + CourseSessionEdoniqTest, ) +from vbv_lernwelt.learnpath.models import Circle @admin.register(CourseSessionAttendanceCourse) @@ -16,11 +19,59 @@ class CourseSessionAttendanceCourseAdmin(admin.ModelAdmin): ] list_display = [ "course_session", + "circle", "learning_content", + "start_date", + "end_date", "trainer", ] list_filter = ["course_session__course", "course_session"] + def start_date(self, obj): + return obj.due_date.start + + start_date.admin_order_field = "due_date__start" + + def end_date(self, obj): + return obj.due_date.end + + end_date.admin_order_field = "due_date__end" + + def circle(self, obj): + try: + return obj.learning_content.get_ancestors().exact_type(Circle).first().title + except Exception: + # noop + pass + return None + + # Create a method that serves as a form field + def circle_display(self, obj=None): + if obj: + return self.circle(obj) + return None + + circle_display.short_description = "Circle" + + # Make circle display-only in the form + def get_readonly_fields(self, request, obj=None): + readonly_fields = super( + CourseSessionAttendanceCourseAdmin, self + ).get_readonly_fields(request, obj) + return readonly_fields + ["circle_display"] + + # Override get_form to include circle_display + def get_form(self, request, obj=None, **kwargs): + form = super(CourseSessionAttendanceCourseAdmin, self).get_form( + request, obj, **kwargs + ) + form.base_fields["circle_display"] = forms.CharField( + required=False, + label="Circle", + widget=forms.TextInput(attrs={"readonly": "readonly"}), + ) + return form + @admin.register(CourseSessionAssignment) class CourseSessionAssignmentAdmin(admin.ModelAdmin): @@ -32,6 +83,116 @@ class CourseSessionAssignmentAdmin(admin.ModelAdmin): ] list_display = [ "course_session", + "circle", "learning_content", + "submission_date", + "evaluation_date", ] - list_filter = ["course_session__course"] + list_filter = ["course_session__course", "course_session"] + + def submission_date(self, obj): + if obj.submission_deadline: + return obj.submission_deadline.start + return None + + submission_date.admin_order_field = "submission_deadline__start" + + def evaluation_date(self, obj): + if obj.evaluation_deadline: + return obj.evaluation_deadline.start + return None + + evaluation_date.admin_order_field = "evaluation_deadline__start" + + def circle(self, obj): + try: + return obj.learning_content.get_ancestors().exact_type(Circle).first().title + except Exception: + # noop + pass + return None + + # Create a method that serves as a form field + def circle_display(self, obj=None): + if obj: + return self.circle(obj) + return None + + circle_display.short_description = "Circle" + + # Make circle display-only in the form + def get_readonly_fields(self, request, obj=None): + readonly_fields = super(CourseSessionAssignmentAdmin, self).get_readonly_fields( + request, obj + ) + return readonly_fields + ["circle_display"] + + # Override get_form to include circle_display + def get_form(self, request, obj=None, **kwargs): + form = super(CourseSessionAssignmentAdmin, self).get_form( + request, obj, **kwargs + ) + form.base_fields["circle_display"] = forms.CharField( + required=False, + label="Circle", + widget=forms.TextInput(attrs={"readonly": "readonly"}), + ) + return form + + +@admin.register(CourseSessionEdoniqTest) +class CourseSessionEdoniqTestAdmin(admin.ModelAdmin): + readonly_fields = [ + "course_session", + "learning_content", + "deadline", + ] + list_display = [ + "course_session", + "circle", + "learning_content", + "deadline_date", + ] + list_filter = ["course_session__course", "course_session"] + + def deadline_date(self, obj): + if obj.deadline: + return obj.deadline.start + return None + + deadline_date.admin_order_field = "deadline__start" + + def circle(self, obj): + try: + return obj.learning_content.get_ancestors().exact_type(Circle).first().title + except Exception: + # noop + pass + return None + + # Create a method that serves as a form field + def circle_display(self, obj=None): + if obj: + return self.circle(obj) + return None + + circle_display.short_description = "Circle" + + # Make circle display-only in the form + def get_readonly_fields(self, request, obj=None): + readonly_fields = super(CourseSessionEdoniqTestAdmin, self).get_readonly_fields( + request, obj + ) + return readonly_fields + ["circle_display"] + + # Override get_form to include circle_display + def get_form(self, request, obj=None, **kwargs): + form = super(CourseSessionEdoniqTestAdmin, self).get_form( + request, obj, **kwargs + ) + form.base_fields["circle_display"] = forms.CharField( + required=False, + label="Circle", + widget=forms.TextInput(attrs={"readonly": "readonly"}), + ) + return form diff --git a/server/vbv_lernwelt/course_session/models.py b/server/vbv_lernwelt/course_session/models.py index 56ad1541..09bf51f5 100644 --- a/server/vbv_lernwelt/course_session/models.py +++ b/server/vbv_lernwelt/course_session/models.py @@ -29,6 +29,9 @@ class CourseSessionAttendanceCourse(models.Model): location = models.CharField(max_length=255, blank=True, default="") trainer = models.CharField(max_length=255, blank=True, default="") + class Meta: + ordering = ["course_session", "due_date__start"] + # because the attendance list is more of a snapshot of the current state # we will store the attendance list as a JSONField # the important field of the list type is "user_id" @@ -103,6 +106,9 @@ class CourseSessionAssignment(models.Model): null=True, ) + class Meta: + ordering = ["course_session", "submission_deadline__start"] + def save(self, *args, **kwargs): if self.learning_content_id: title = self.learning_content.title @@ -178,6 +184,9 @@ class CourseSessionEdoniqTest(models.Model): null=True, ) + class Meta: + ordering = ["course_session", "deadline__start"] + def __str__(self): return f"{self.course_session} - {self.learning_content}" diff --git a/server/vbv_lernwelt/duedate/admin.py b/server/vbv_lernwelt/duedate/admin.py index 12fd4052..dcaf2359 100644 --- a/server/vbv_lernwelt/duedate/admin.py +++ b/server/vbv_lernwelt/duedate/admin.py @@ -3,6 +3,7 @@ from wagtail.models import Page from vbv_lernwelt.duedate.models import DueDate from vbv_lernwelt.learnpath.models import ( + Circle, LearningContentAttendanceCourse, LearningContentEdoniqTest, ) @@ -14,12 +15,13 @@ class DueDateAdmin(admin.ModelAdmin): date_hierarchy = "start" list_display = [ "course_session", + "circle", "title", "display_subtitle", "start", "end", ] - list_filter = ["course_session"] + list_filter = ["course_session__course", "course_session"] readonly_fields = ["course_session", "page"] def get_readonly_fields(self, request, obj=None): @@ -30,6 +32,7 @@ class DueDateAdmin(admin.ModelAdmin): if not obj.manual_override_fields: return default_readonly + [ + "circle", "title", "subtitle", "assignment_type_translation_key", @@ -39,6 +42,22 @@ class DueDateAdmin(admin.ModelAdmin): return default_readonly + def circle(self, obj): + try: + return obj.page.get_ancestors().exact_type(Circle).first().title + except Exception: + # noop + pass + return None + + # Create a method that serves as a form field + def circle_display(self, obj=None): + if obj: + return self.circle(obj) + return None + + circle_display.short_description = "Circle" + def formfield_for_foreignkey(self, db_field, request, **kwargs): if db_field.name == "page": if request.resolver_match.kwargs.get("object_id"): diff --git a/server/vbv_lernwelt/duedate/models.py b/server/vbv_lernwelt/duedate/models.py index 854b6da5..bb1f2028 100644 --- a/server/vbv_lernwelt/duedate/models.py +++ b/server/vbv_lernwelt/duedate/models.py @@ -60,12 +60,16 @@ class DueDate(models.Model): def __str__(self): if self.is_undefined: - return f"DueDate: {self.title} undefined" - start_str = self.start.strftime("%Y-%m-%d %H:%M") if self.start else "-" - result = f"DueDate: {self.title} {start_str}" + return f"{self.title} undefined" + + # Convert the UTC datetime to local time + local_start = timezone.localtime(self.start) if self.start else None + start_str = local_start.strftime("%Y-%m-%d %H:%M") if local_start else "-" + result = f"{self.title} {start_str}" if self.end: - end_str = self.end.strftime("%Y-%m-%d %H:%M") if self.end else "-" + local_end = timezone.localtime(self.end) if self.end else None + end_str = local_end.strftime("%Y-%m-%d %H:%M") if local_end else "-" result += f" - {end_str}" return result diff --git a/server/vbv_lernwelt/feedback/migrations/0003_alter_feedbackresponse_course_session.py b/server/vbv_lernwelt/feedback/migrations/0003_alter_feedbackresponse_course_session.py new file mode 100644 index 00000000..c584c4dc --- /dev/null +++ b/server/vbv_lernwelt/feedback/migrations/0003_alter_feedbackresponse_course_session.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.20 on 2023-08-23 15:44 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("course", "0004_auto_20230823_1744"), + ("feedback", "0002_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="feedbackresponse", + name="course_session", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="course.coursesession" + ), + ), + ] diff --git a/server/vbv_lernwelt/feedback/models.py b/server/vbv_lernwelt/feedback/models.py index f7f56444..f9c0aaf1 100644 --- a/server/vbv_lernwelt/feedback/models.py +++ b/server/vbv_lernwelt/feedback/models.py @@ -62,4 +62,4 @@ class FeedbackResponse(models.Model): created_at = models.DateTimeField(auto_now_add=True) circle = models.ForeignKey("learnpath.Circle", models.PROTECT) - course_session = models.ForeignKey("course.CourseSession", models.PROTECT) + course_session = models.ForeignKey("course.CourseSession", models.CASCADE) diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py index 8a941da6..a4ff3fab 100644 --- a/server/vbv_lernwelt/importer/services.py +++ b/server/vbv_lernwelt/importer/services.py @@ -141,6 +141,7 @@ LP_DATA = { "slug": "ménage-partie-1", "presence_course": "ménage-partie-1-lc-cours-de-présence-ménage-partie-1", "assignments": [], + "edoniq_tests": [], }, "it": { "title": "Economica domestica parte 1", @@ -163,6 +164,7 @@ LP_DATA = { "slug": "ménage-partie-2", "presence_course": "ménage-partie-2-lc-cours-de-présence-ménage-partie-2", "assignments": [], + "edoniq_tests": [], }, "it": { "title": "Economica domestica parte 2", @@ -281,7 +283,16 @@ def import_course_sessions_from_excel( course = get_uk_course(language) create_or_update_course_session( - course, data, language, circle_keys=["Kickoff", "Basis", "Fahrzeug"] + course, + data, + language, + circle_keys=[ + "Kickoff", + "Basis", + "Fahrzeug", + "Haushalt Teil 1", + "Haushalt Teil 2", + ], ) @@ -315,7 +326,7 @@ def create_or_update_course_session( title = f"{region} {generation} {group}" cs, _created = CourseSession.objects.get_or_create( - title=title, course=course, import_id=import_id + course=course, import_id=import_id ) cs.additional_json_data["import_data"] = data @@ -360,7 +371,7 @@ def create_or_update_course_session( for assignment_slug in circle_data["assignments"]: create_or_update_course_session_assignment( - cs, course.slug, assignment_slug, presence_day_start, presence_day_end + cs, course.slug, assignment_slug, presence_day_start ) for test_slug in circle_data["edoniq_tests"]: @@ -415,7 +426,6 @@ def create_or_update_course_session_assignment( course_slug: str, assignment_slug: str, start: datetime, - end: datetime, ): logger.debug("import", slug=f"{course_slug}-lp-circle-{assignment_slug}") @@ -443,13 +453,18 @@ def create_or_update_course_session_assignment( elif ( csa.learning_content.assignment_type == AssignmentType.CASEWORK.value - and end + and start ): csa.submission_deadline.start = timezone.make_aware( start ) + timezone.timedelta(days=30) csa.submission_deadline.end = None csa.submission_deadline.save() + csa.evaluation_deadline.start = timezone.make_aware( + start + ) + timezone.timedelta(days=45) + csa.evaluation_deadline.end = None + csa.evaluation_deadline.save() def create_or_update_course_session_edoniq_test(