From 4d8c2190763e1e0ee2171b118ce82b35ed0caae6 Mon Sep 17 00:00:00 2001 From: Lorenz Padberg Date: Thu, 8 Jun 2023 15:08:57 +0200 Subject: [PATCH 01/39] Add events app --- env_secrets/local_lorenz.env | Bin 295 -> 327 bytes server/config/settings/base.py | 7 +- server/vbv_lernwelt/events/__init__.py | 0 server/vbv_lernwelt/events/admin.py | 3 + server/vbv_lernwelt/events/apps.py | 13 ++++ server/vbv_lernwelt/events/factories.py | 18 +++++ .../events/migrations/__init__.py | 0 server/vbv_lernwelt/events/models.py | 67 ++++++++++++++++++ server/vbv_lernwelt/events/tests/__init__.py | 0 .../events/tests/test_event_model.py | 38 ++++++++++ server/vbv_lernwelt/events/views.py | 3 + 11 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 server/vbv_lernwelt/events/__init__.py create mode 100644 server/vbv_lernwelt/events/admin.py create mode 100644 server/vbv_lernwelt/events/apps.py create mode 100644 server/vbv_lernwelt/events/factories.py create mode 100644 server/vbv_lernwelt/events/migrations/__init__.py create mode 100644 server/vbv_lernwelt/events/models.py create mode 100644 server/vbv_lernwelt/events/tests/__init__.py create mode 100644 server/vbv_lernwelt/events/tests/test_event_model.py create mode 100644 server/vbv_lernwelt/events/views.py diff --git a/env_secrets/local_lorenz.env b/env_secrets/local_lorenz.env index 22665c987059eaf1f531854f1de29e10ae4fdeaa..2c971824867b5e8a5e8b71aacf3042dd5ce27ebc 100644 GIT binary patch literal 327 zcmV-N0l5AEM@dveQdv+`09|^phkV}hY3G3cMDSJw^)l3ZK#_4SD|UL{30J1$MKGE) z%|M)5cMt^Ic$%)Q+=`zk2r{XplyeD%X$5)iKS#^8oV-ooyu&9o3(JC&r21FI(rxye z)R#Rysd~qIQxhF9D6y}m!iA(^m@G_r(&+8Gt=(Kq-a;-6&#YO0VlD5>Hs70+2s|t1 z+(-mY+?1Bjb5>J-Cg0_mm%NmM+i(KKlkBcHN@sN)=;>`p6nqz818z2M0rW;!`m)?E zVQZ8EV&1faLFV*Mr4gE?rLT-t=W18qM;;+O93vH`7&lNZ>&*;Mf)HOf)74nv=|i5? zrYAdCc(q)@XUbovKf!^QmLA5v(^xh6&{;QdpA&)F4m~R%{>H;S*UVNxD2FvEj;<=> Zz{f$Z$Mop;a0?3=S|7^Oh?}^{E(pYYpT+%EF;PSG0&NR!P&OuZ$$5nz&c}%V@unLW zW+s>rgKKoTcH(W6zfKgF#NaP58LT&Dbvc>0rX|CiSbOM=$7^A5|2~5OX|-RWnXU5h z^3gHig&|5i0R>kRxon*@U4kWv{yh6F)cL+nUUQ%06+2n{5~bASe Date: Tue, 13 Jun 2023 17:20:47 +0200 Subject: [PATCH 02/39] Add default event genration function for Course assignment --- .../core/management/commands/reset_schema.py | 1 + server/vbv_lernwelt/course/models.py | 2 + server/vbv_lernwelt/events/admin.py | 14 +++++ server/vbv_lernwelt/events/factories.py | 55 ++++++++++++++++++- .../events/management/__init__.py | 0 .../events/management/commands/__init__.py | 0 .../commands/create_default_events.py | 9 +++ server/vbv_lernwelt/events/models.py | 21 ++++--- server/vbv_lernwelt/learnpath/models.py | 3 + 9 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 server/vbv_lernwelt/events/management/__init__.py create mode 100644 server/vbv_lernwelt/events/management/commands/__init__.py create mode 100644 server/vbv_lernwelt/events/management/commands/create_default_events.py diff --git a/server/vbv_lernwelt/core/management/commands/reset_schema.py b/server/vbv_lernwelt/core/management/commands/reset_schema.py index 4d9c153a..a5d5603c 100644 --- a/server/vbv_lernwelt/core/management/commands/reset_schema.py +++ b/server/vbv_lernwelt/core/management/commands/reset_schema.py @@ -29,3 +29,4 @@ def command(): call_command("migrate") call_command("create_default_users") call_command("create_default_courses") + call_command("create_default_events") diff --git a/server/vbv_lernwelt/course/models.py b/server/vbv_lernwelt/course/models.py index 2a49e8d9..2394009d 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -193,6 +193,8 @@ class CourseSession(models.Model): Das anhängen kann via CourseSessionUser oder "Schulklasse (TODO)" geschehen """ + # TODO: Das wird durch event modell ersetzt + ATTENDANCE_COURSES_SCHEMA = { "type": "array", "items": { diff --git a/server/vbv_lernwelt/events/admin.py b/server/vbv_lernwelt/events/admin.py index 8c38f3f3..056c7148 100644 --- a/server/vbv_lernwelt/events/admin.py +++ b/server/vbv_lernwelt/events/admin.py @@ -1,3 +1,17 @@ from django.contrib import admin +from vbv_lernwelt.events.models import Event + + # Register your models here. +@admin.register(Event) +class EventAdmin(admin.ModelAdmin): + date_hierarchy = "end" + list_display = [ + "title", + "course_session", + "start", + "end", + "unset" + ] + list_filter = ["course_session"] diff --git a/server/vbv_lernwelt/events/factories.py b/server/vbv_lernwelt/events/factories.py index c6bfa7c6..e7248354 100644 --- a/server/vbv_lernwelt/events/factories.py +++ b/server/vbv_lernwelt/events/factories.py @@ -1,13 +1,19 @@ -from datetime import datetime +import datetime +import structlog from django.utils import timezone from factory.django import DjangoModelFactory from .models import Event +from ..assignment.models import Assignment +from ..course.models import CourseSession +from ..learnpath.models import LearningContentAttendanceCourse + +logger = structlog.get_logger(__name__) def get_date(date_string): - return datetime.strptime(date_string, '%b %d %Y', ).astimezone(timezone.get_current_timezone()) + return datetime.datetime.strptime(date_string, '%b %d %Y', ).astimezone(timezone.get_current_timezone()) class EventFactory(DjangoModelFactory): @@ -16,3 +22,48 @@ class EventFactory(DjangoModelFactory): title = "Prüfung Versicherungsvermittler/-in" end = get_date("Jan 01 2021") + + +def generate_events(start=timezone.now()): + for i in range(20): + EventFactory(title=f"{i}", start=start + datetime.timedelta(days=i), + end=start + datetime.timedelta(days=i, hours=1)) + + +def hour_rounder(t): + # Rounds to nearest hour by adding a timedelta hour if minute >= 30 + return (t.replace(second=0, microsecond=0, minute=0, hour=t.hour) + + datetime.timedelta(hours=t.minute // 30)) + + +def create_events_for_all_course_sessions(): + all_course_sessions: list[CourseSession] = CourseSession.objects.all() + + for course_session in all_course_sessions: + create_events_for_course_session(course_session) + + +def create_events_for_course_session(course_session: CourseSession): + course = course_session.course + attendance_courses = list(LearningContentAttendanceCourse.objects.descendant_of(course.get_learning_path())) + assignments = list(Assignment.objects.descendant_of(course.get_learning_path())) + contents = attendance_courses + assignments + + for content in contents: + if callable(getattr(content, 'get_frontend_url', None)): + url = content.get_frontend_url() + + event, created = Event.objects.get_or_create(course_session=course_session, title=content.title, url=url) + if created: + logger.info(f"Created event {event} for course session {course_session} and content {content}") + + +# + +def set_default_times_for_events(): + now = hour_rounder(timezone.now()) + + for i, event in enumerate(Event.objects.filter(end__isnull=True)): + event.start = now + datetime.timedelta(days=i) + event.end = event.start + datetime.timedelta(hours=3) + event.save() diff --git a/server/vbv_lernwelt/events/management/__init__.py b/server/vbv_lernwelt/events/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/events/management/commands/__init__.py b/server/vbv_lernwelt/events/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/events/management/commands/create_default_events.py b/server/vbv_lernwelt/events/management/commands/create_default_events.py new file mode 100644 index 00000000..57461dfa --- /dev/null +++ b/server/vbv_lernwelt/events/management/commands/create_default_events.py @@ -0,0 +1,9 @@ +import djclick as click + +from vbv_lernwelt.events.factories import create_events_for_all_course_sessions, set_default_times_for_events + + +@click.command() +def command(): + create_events_for_all_course_sessions() + set_default_times_for_events() diff --git a/server/vbv_lernwelt/events/models.py b/server/vbv_lernwelt/events/models.py index e93284c3..a2382f86 100644 --- a/server/vbv_lernwelt/events/models.py +++ b/server/vbv_lernwelt/events/models.py @@ -10,8 +10,9 @@ from vbv_lernwelt.course.models import CourseSession class Event(models.Model): start = models.DateTimeField(null=True, db_index=True) - end = models.DateTimeField(db_index=True) - title = models.CharField(default=_('Termin'), max_length=255) + end = models.DateTimeField(db_index=True, null=True) + title = models.CharField(default=_('Termin'), max_length=1024) + url = models.URLField(null=True, blank=True, max_length=1024) course_session = models.ForeignKey( 'course.CourseSession', on_delete=models.CASCADE, @@ -20,20 +21,18 @@ class Event(models.Model): blank=True, ) - learning_content = models.ForeignKey( - 'learnpath.LearningContentAttendanceCourse', - on_delete=models.CASCADE, - related_name='events', - null=True, - blank=True, - ) + learning_content_id = models.CharField(blank=True, null=True, max_length=255) def Meta(self): ordering = ['start', 'end'] verbose_name = _("Termin") def __str__(self): - return f"{self.title} {self.start}" + return f"Event: {self.title} {self.start} to {self.end}" + + @property + def unset(self): + return self.end is None @property def duration(self): @@ -55,7 +54,7 @@ class Event(models.Model): qs = qs.filter(course_session=course_session, course_session__course_assignment__user=user) else: qs = qs.filter(course_session__course_assignment__user=user) - + qs = qs.order_by('start')[:limit] return qs diff --git a/server/vbv_lernwelt/learnpath/models.py b/server/vbv_lernwelt/learnpath/models.py index 5f412e0c..ccc8d53f 100644 --- a/server/vbv_lernwelt/learnpath/models.py +++ b/server/vbv_lernwelt/learnpath/models.py @@ -279,6 +279,9 @@ class LearningContent(CourseBasePage): class LearningContentAttendanceCourse(LearningContent): + """ + Präsenzkurs + """ parent_page_types = ["learnpath.Circle"] subpage_types = [] From f05d7b22790301f9d812e59ee23604771702962e Mon Sep 17 00:00:00 2001 From: Lorenz Padberg Date: Wed, 14 Jun 2023 11:33:39 +0200 Subject: [PATCH 03/39] Rename Event -> DueDate --- server/config/settings/base.py | 2 +- .../core/management/commands/reset_schema.py | 2 +- .../{events => duedate}/__init__.py | 0 server/vbv_lernwelt/duedate/admin.py | 34 +++++++++++++++++ .../vbv_lernwelt/{events => duedate}/apps.py | 2 +- .../{events => duedate}/factories.py | 31 ++++++++------- .../management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/create_default_duedates.py | 9 +++++ .../duedate/migrations/0001_initial.py | 28 ++++++++++++++ .../migrations/0002_auto_20230614_1500.py | 24 ++++++++++++ .../migrations/__init__.py | 0 .../{events => duedate}/models.py | 19 ++++++---- .../{events => duedate}/tests/__init__.py | 0 .../duedate/tests/test_duedate_model.py | 38 +++++++++++++++++++ .../vbv_lernwelt/{events => duedate}/views.py | 0 server/vbv_lernwelt/events/admin.py | 17 --------- .../commands/create_default_events.py | 9 ----- .../events/tests/test_event_model.py | 38 ------------------- 19 files changed, 164 insertions(+), 89 deletions(-) rename server/vbv_lernwelt/{events => duedate}/__init__.py (100%) create mode 100644 server/vbv_lernwelt/duedate/admin.py rename server/vbv_lernwelt/{events => duedate}/apps.py (90%) rename server/vbv_lernwelt/{events => duedate}/factories.py (62%) rename server/vbv_lernwelt/{events => duedate}/management/__init__.py (100%) rename server/vbv_lernwelt/{events => duedate}/management/commands/__init__.py (100%) create mode 100644 server/vbv_lernwelt/duedate/management/commands/create_default_duedates.py create mode 100644 server/vbv_lernwelt/duedate/migrations/0001_initial.py create mode 100644 server/vbv_lernwelt/duedate/migrations/0002_auto_20230614_1500.py rename server/vbv_lernwelt/{events => duedate}/migrations/__init__.py (100%) rename server/vbv_lernwelt/{events => duedate}/models.py (70%) rename server/vbv_lernwelt/{events => duedate}/tests/__init__.py (100%) create mode 100644 server/vbv_lernwelt/duedate/tests/test_duedate_model.py rename server/vbv_lernwelt/{events => duedate}/views.py (100%) delete mode 100644 server/vbv_lernwelt/events/admin.py delete mode 100644 server/vbv_lernwelt/events/management/commands/create_default_events.py delete mode 100644 server/vbv_lernwelt/events/tests/test_event_model.py diff --git a/server/config/settings/base.py b/server/config/settings/base.py index cb1c3daf..5b3fe2dd 100644 --- a/server/config/settings/base.py +++ b/server/config/settings/base.py @@ -120,7 +120,7 @@ LOCAL_APPS = [ "vbv_lernwelt.files", "vbv_lernwelt.notify", "vbv_lernwelt.assignment", - "vbv_lernwelt.events", + "vbv_lernwelt.duedate", ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS diff --git a/server/vbv_lernwelt/core/management/commands/reset_schema.py b/server/vbv_lernwelt/core/management/commands/reset_schema.py index a5d5603c..885a9e51 100644 --- a/server/vbv_lernwelt/core/management/commands/reset_schema.py +++ b/server/vbv_lernwelt/core/management/commands/reset_schema.py @@ -29,4 +29,4 @@ def command(): call_command("migrate") call_command("create_default_users") call_command("create_default_courses") - call_command("create_default_events") + call_command("create_default_duedates") diff --git a/server/vbv_lernwelt/events/__init__.py b/server/vbv_lernwelt/duedate/__init__.py similarity index 100% rename from server/vbv_lernwelt/events/__init__.py rename to server/vbv_lernwelt/duedate/__init__.py diff --git a/server/vbv_lernwelt/duedate/admin.py b/server/vbv_lernwelt/duedate/admin.py new file mode 100644 index 00000000..9f3a60f9 --- /dev/null +++ b/server/vbv_lernwelt/duedate/admin.py @@ -0,0 +1,34 @@ +from django.contrib import admin +from wagtail.models import Page + +from vbv_lernwelt.duedate.models import DueDate +from vbv_lernwelt.learnpath.models import ( + LearningContentAttendanceCourse, + LearningContentTest, +) + + +# Register your models here. +@admin.register(DueDate) +class DueDateAdmin(admin.ModelAdmin): + date_hierarchy = "end" + list_display = [ + "title", + "course_session", + "start", + "end", + "unset" + ] + list_filter = ["course_session"] + + def formfield_for_foreignkey(self, db_field, request, **kwargs): + if db_field.name == "page": + if request.resolver_match.kwargs.get("object_id"): + object_id = int(request.resolver_match.kwargs.get("object_id")) + csd = DueDate.objects.get(id=object_id) + kwargs["queryset"] = Page.objects.descendant_of( + csd.course_session.course.coursepage + ).exact_type(LearningContentAttendanceCourse, LearningContentTest) + else: + kwargs["queryset"] = Page.objects.none() + return super().formfield_for_foreignkey(db_field, request, **kwargs) diff --git a/server/vbv_lernwelt/events/apps.py b/server/vbv_lernwelt/duedate/apps.py similarity index 90% rename from server/vbv_lernwelt/events/apps.py rename to server/vbv_lernwelt/duedate/apps.py index f0629a22..9956b15f 100644 --- a/server/vbv_lernwelt/events/apps.py +++ b/server/vbv_lernwelt/duedate/apps.py @@ -3,7 +3,7 @@ from django.apps import AppConfig class EventsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "vbv_lernwelt.events" + name = "vbv_lernwelt.duedate" def ready(self): try: diff --git a/server/vbv_lernwelt/events/factories.py b/server/vbv_lernwelt/duedate/factories.py similarity index 62% rename from server/vbv_lernwelt/events/factories.py rename to server/vbv_lernwelt/duedate/factories.py index e7248354..f097374e 100644 --- a/server/vbv_lernwelt/events/factories.py +++ b/server/vbv_lernwelt/duedate/factories.py @@ -4,7 +4,7 @@ import structlog from django.utils import timezone from factory.django import DjangoModelFactory -from .models import Event +from .models import DueDate from ..assignment.models import Assignment from ..course.models import CourseSession from ..learnpath.models import LearningContentAttendanceCourse @@ -16,9 +16,9 @@ def get_date(date_string): return datetime.datetime.strptime(date_string, '%b %d %Y', ).astimezone(timezone.get_current_timezone()) -class EventFactory(DjangoModelFactory): +class DueDateFactory(DjangoModelFactory): class Meta: - model = Event + model = DueDate title = "Prüfung Versicherungsvermittler/-in" end = get_date("Jan 01 2021") @@ -26,8 +26,8 @@ class EventFactory(DjangoModelFactory): def generate_events(start=timezone.now()): for i in range(20): - EventFactory(title=f"{i}", start=start + datetime.timedelta(days=i), - end=start + datetime.timedelta(days=i, hours=1)) + DueDateFactory(title=f"{i}", start=start + datetime.timedelta(days=i), + end=start + datetime.timedelta(days=i, hours=1)) def hour_rounder(t): @@ -36,14 +36,14 @@ def hour_rounder(t): + datetime.timedelta(hours=t.minute // 30)) -def create_events_for_all_course_sessions(): +def create_duedates_for_all_course_sessions(): all_course_sessions: list[CourseSession] = CourseSession.objects.all() for course_session in all_course_sessions: - create_events_for_course_session(course_session) + create_duedates_for_course_session(course_session) -def create_events_for_course_session(course_session: CourseSession): +def create_duedates_for_course_session(course_session: CourseSession): course = course_session.course attendance_courses = list(LearningContentAttendanceCourse.objects.descendant_of(course.get_learning_path())) assignments = list(Assignment.objects.descendant_of(course.get_learning_path())) @@ -53,17 +53,20 @@ def create_events_for_course_session(course_session: CourseSession): if callable(getattr(content, 'get_frontend_url', None)): url = content.get_frontend_url() - event, created = Event.objects.get_or_create(course_session=course_session, title=content.title, url=url) + duedate, created = DueDate.objects.get_or_create(course_session=course_session, + title=content.title, + url=url, + page=content.specific) + if created: - logger.info(f"Created event {event} for course session {course_session} and content {content}") + pass + logger.info("Created duedate ", duedate=duedate, course_session=course_session, content=content) -# - -def set_default_times_for_events(): +def set_default_times_for_duedates(): now = hour_rounder(timezone.now()) - for i, event in enumerate(Event.objects.filter(end__isnull=True)): + for i, event in enumerate(DueDate.objects.filter(end__isnull=True)): event.start = now + datetime.timedelta(days=i) event.end = event.start + datetime.timedelta(hours=3) event.save() diff --git a/server/vbv_lernwelt/events/management/__init__.py b/server/vbv_lernwelt/duedate/management/__init__.py similarity index 100% rename from server/vbv_lernwelt/events/management/__init__.py rename to server/vbv_lernwelt/duedate/management/__init__.py diff --git a/server/vbv_lernwelt/events/management/commands/__init__.py b/server/vbv_lernwelt/duedate/management/commands/__init__.py similarity index 100% rename from server/vbv_lernwelt/events/management/commands/__init__.py rename to server/vbv_lernwelt/duedate/management/commands/__init__.py diff --git a/server/vbv_lernwelt/duedate/management/commands/create_default_duedates.py b/server/vbv_lernwelt/duedate/management/commands/create_default_duedates.py new file mode 100644 index 00000000..f85e18bd --- /dev/null +++ b/server/vbv_lernwelt/duedate/management/commands/create_default_duedates.py @@ -0,0 +1,9 @@ +import djclick as click + +from vbv_lernwelt.duedate.factories import create_duedates_for_all_course_sessions, set_default_times_for_duedates + + +@click.command() +def command(): + create_duedates_for_all_course_sessions() + set_default_times_for_duedates() diff --git a/server/vbv_lernwelt/duedate/migrations/0001_initial.py b/server/vbv_lernwelt/duedate/migrations/0001_initial.py new file mode 100644 index 00000000..d0c2e98d --- /dev/null +++ b/server/vbv_lernwelt/duedate/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.13 on 2023-06-14 09:29 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('course', '0004_import_fields'), + ] + + operations = [ + migrations.CreateModel( + name='DueDate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('start', models.DateTimeField(db_index=True, null=True)), + ('end', models.DateTimeField(db_index=True, null=True)), + ('title', models.CharField(default='Termin', max_length=1024)), + ('url', models.URLField(blank=True, max_length=1024, null=True)), + ('learning_content_id', models.CharField(blank=True, max_length=255, null=True)), + ('course_session', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='events', to='course.coursesession')), + ], + ), + ] diff --git a/server/vbv_lernwelt/duedate/migrations/0002_auto_20230614_1500.py b/server/vbv_lernwelt/duedate/migrations/0002_auto_20230614_1500.py new file mode 100644 index 00000000..d56fd237 --- /dev/null +++ b/server/vbv_lernwelt/duedate/migrations/0002_auto_20230614_1500.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.13 on 2023-06-14 13:00 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailcore', '0083_workflowcontenttype'), + ('duedate', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='duedate', + name='learning_content_id', + ), + migrations.AddField( + model_name='duedate', + name='page', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='wagtailcore.page'), + ), + ] diff --git a/server/vbv_lernwelt/events/migrations/__init__.py b/server/vbv_lernwelt/duedate/migrations/__init__.py similarity index 100% rename from server/vbv_lernwelt/events/migrations/__init__.py rename to server/vbv_lernwelt/duedate/migrations/__init__.py diff --git a/server/vbv_lernwelt/events/models.py b/server/vbv_lernwelt/duedate/models.py similarity index 70% rename from server/vbv_lernwelt/events/models.py rename to server/vbv_lernwelt/duedate/models.py index a2382f86..884fa9be 100644 --- a/server/vbv_lernwelt/events/models.py +++ b/server/vbv_lernwelt/duedate/models.py @@ -3,12 +3,13 @@ import datetime from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ +from wagtail.models import Page from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import CourseSession -class Event(models.Model): +class DueDate(models.Model): start = models.DateTimeField(null=True, db_index=True) end = models.DateTimeField(db_index=True, null=True) title = models.CharField(default=_('Termin'), max_length=1024) @@ -21,14 +22,16 @@ class Event(models.Model): blank=True, ) - learning_content_id = models.CharField(blank=True, null=True, max_length=255) + page = models.ForeignKey(Page, on_delete=models.SET_NULL, null=True, blank=True) def Meta(self): ordering = ['start', 'end'] verbose_name = _("Termin") def __str__(self): - return f"Event: {self.title} {self.start} to {self.end}" + start_str = self.start.strftime('%Y-%m-%d %H:%M') if self.start else '' + end_str = self.end.strftime('%Y-%m-%d %H:%M') if self.end else '' + return f"DueDate: {self.title} {start_str} to {end_str}" @property def unset(self): @@ -43,12 +46,12 @@ class Event(models.Model): @classmethod def get_users_next_events_qs(cls, user: User, course_session: CourseSession = None, limit=10): """ - Returns a queryset of all events that are relevant for the given user. Ordered nearest start date first. - If course_session is given, only events for that course_session are returned. - The user is determined by via a course session user of an course_assignment. + Returns a queryset of all duedates that are relevant for the given user. Ordered nearest start date first. + If course_session is given, only duedates for that course_session are returned. + The user is determined by via a course session user of a course_assignment. """ - qs = cls.get_next_events_qs() + qs = cls.get_next_duedates_qs() if course_session: qs = qs.filter(course_session=course_session, course_session__course_assignment__user=user) @@ -60,7 +63,7 @@ class Event(models.Model): return qs @classmethod - def get_next_events_qs(cls): + def get_next_duedates_qs(cls): now = timezone.now() qs = cls.objects.filter(end__gte=now) return qs diff --git a/server/vbv_lernwelt/events/tests/__init__.py b/server/vbv_lernwelt/duedate/tests/__init__.py similarity index 100% rename from server/vbv_lernwelt/events/tests/__init__.py rename to server/vbv_lernwelt/duedate/tests/__init__.py diff --git a/server/vbv_lernwelt/duedate/tests/test_duedate_model.py b/server/vbv_lernwelt/duedate/tests/test_duedate_model.py new file mode 100644 index 00000000..d3646cee --- /dev/null +++ b/server/vbv_lernwelt/duedate/tests/test_duedate_model.py @@ -0,0 +1,38 @@ +import datetime + +from django.test import TestCase +from django.utils import timezone + +from vbv_lernwelt.duedate.factories import DueDateFactory +from vbv_lernwelt.duedate.models import DueDate + + +class TesDueDatetModel(TestCase): + def test_duedate_model_factory(self): + DueDateFactory() + assert DueDate.objects.count() == 1 + + def test_get_next_duedate_qs_is_really_next(self): + start = timezone.now() - datetime.timedelta(days=18) + generate_duedates(start=start) + self.assertEqual(DueDate.objects.count(), 20) + self.assertEqual(DueDate.get_next_duedates_qs().count(), 2) + + # def test_event_model_factory_validation(self): + # e = DueDateFactory() + # e.start = get_date("Jan 01 2021") + # e.end = get_date("Jan 02 2021") + # e.validate() + # self.assertTrue(True) + # + # def test_event_model_factory_validation_invalid(self): + # e = DueDateFactory() + # e.start = get_date("Jan 04 2021") + # e.end = get_date("Jan 02 2021") + # self.assertRaises(ValueError, e.validate) + + +def generate_duedates(start=timezone.now()): + for i in range(20): + DueDateFactory(title=f"{i}", start=start + datetime.timedelta(days=i), + end=start + datetime.timedelta(days=i, hours=1)) diff --git a/server/vbv_lernwelt/events/views.py b/server/vbv_lernwelt/duedate/views.py similarity index 100% rename from server/vbv_lernwelt/events/views.py rename to server/vbv_lernwelt/duedate/views.py diff --git a/server/vbv_lernwelt/events/admin.py b/server/vbv_lernwelt/events/admin.py deleted file mode 100644 index 056c7148..00000000 --- a/server/vbv_lernwelt/events/admin.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.contrib import admin - -from vbv_lernwelt.events.models import Event - - -# Register your models here. -@admin.register(Event) -class EventAdmin(admin.ModelAdmin): - date_hierarchy = "end" - list_display = [ - "title", - "course_session", - "start", - "end", - "unset" - ] - list_filter = ["course_session"] diff --git a/server/vbv_lernwelt/events/management/commands/create_default_events.py b/server/vbv_lernwelt/events/management/commands/create_default_events.py deleted file mode 100644 index 57461dfa..00000000 --- a/server/vbv_lernwelt/events/management/commands/create_default_events.py +++ /dev/null @@ -1,9 +0,0 @@ -import djclick as click - -from vbv_lernwelt.events.factories import create_events_for_all_course_sessions, set_default_times_for_events - - -@click.command() -def command(): - create_events_for_all_course_sessions() - set_default_times_for_events() diff --git a/server/vbv_lernwelt/events/tests/test_event_model.py b/server/vbv_lernwelt/events/tests/test_event_model.py deleted file mode 100644 index 412e2190..00000000 --- a/server/vbv_lernwelt/events/tests/test_event_model.py +++ /dev/null @@ -1,38 +0,0 @@ -import datetime - -from django.test import TestCase -from django.utils import timezone - -from vbv_lernwelt.events.factories import EventFactory -from vbv_lernwelt.events.models import Event - - -class TestEventModel(TestCase): - def test_event_model_factory(self): - EventFactory() - assert Event.objects.count() == 1 - - def test_get_next_events_qs_is_really_next(self): - start = timezone.now() - datetime.timedelta(days=18) - generate_events(start=start) - self.assertEqual(Event.objects.count(), 20) - self.assertEqual(Event.get_next_events_qs().count(), 2) - - # def test_event_model_factory_validation(self): - # e = EventFactory() - # e.start = get_date("Jan 01 2021") - # e.end = get_date("Jan 02 2021") - # e.validate() - # self.assertTrue(True) - # - # def test_event_model_factory_validation_invalid(self): - # e = EventFactory() - # e.start = get_date("Jan 04 2021") - # e.end = get_date("Jan 02 2021") - # self.assertRaises(ValueError, e.validate) - - -def generate_events(start=timezone.now()): - for i in range(20): - EventFactory(title=f"{i}", start=start + datetime.timedelta(days=i), - end=start + datetime.timedelta(days=i, hours=1)) From cda7ae07a82ba46e69f54eb6f1069d63b81e26ce Mon Sep 17 00:00:00 2001 From: Lorenz Padberg Date: Wed, 14 Jun 2023 15:44:16 +0200 Subject: [PATCH 04/39] Add duedate to API --- server/vbv_lernwelt/course/serializers.py | 9 +++++++++ .../course/tests/test_course_session_api.py | 3 +++ server/vbv_lernwelt/duedate/serializers.py | 9 +++++++++ .../duedate/tests/test_duedate_serializer.py | 15 +++++++++++++++ 4 files changed, 36 insertions(+) create mode 100644 server/vbv_lernwelt/duedate/serializers.py create mode 100644 server/vbv_lernwelt/duedate/tests/test_duedate_serializer.py diff --git a/server/vbv_lernwelt/course/serializers.py b/server/vbv_lernwelt/course/serializers.py index d92365fd..e5322181 100644 --- a/server/vbv_lernwelt/course/serializers.py +++ b/server/vbv_lernwelt/course/serializers.py @@ -7,6 +7,8 @@ from vbv_lernwelt.course.models import ( CourseCompletion, CourseSession, ) +from vbv_lernwelt.duedate.models import DueDate +from vbv_lernwelt.duedate.serializers import DueDateSerializer class CourseSerializer(serializers.ModelSerializer): @@ -50,6 +52,7 @@ class CourseSessionSerializer(serializers.ModelSerializer): competence_url = serializers.SerializerMethodField() media_library_url = serializers.SerializerMethodField() documents = serializers.SerializerMethodField() + duedates = serializers.SerializerMethodField() def get_course(self, obj): return CourseSerializer(obj.course).data @@ -75,6 +78,11 @@ class CourseSessionSerializer(serializers.ModelSerializer): ) return CircleDocumentSerializer(documents, many=True).data + def get_duedates(self, obj): + # TODO: Filter by user / userrole + duedates = DueDate.objects.filter(course_session=obj) + return DueDateSerializer(duedates, many=True).data + class Meta: model = CourseSession fields = [ @@ -94,6 +102,7 @@ class CourseSessionSerializer(serializers.ModelSerializer): "media_library_url", "course_url", "documents", + "duedates" ] diff --git a/server/vbv_lernwelt/course/tests/test_course_session_api.py b/server/vbv_lernwelt/course/tests/test_course_session_api.py index 17d3616c..56ebb988 100644 --- a/server/vbv_lernwelt/course/tests/test_course_session_api.py +++ b/server/vbv_lernwelt/course/tests/test_course_session_api.py @@ -7,6 +7,7 @@ from vbv_lernwelt.core.models import User from vbv_lernwelt.course.consts import COURSE_TEST_ID from vbv_lernwelt.course.creators.test_course import create_test_course from vbv_lernwelt.course.models import CourseSession, CourseSessionUser +from vbv_lernwelt.duedate.factories import DueDateFactory class CourseCompletionApiTestCase(APITestCase): @@ -20,6 +21,7 @@ class CourseCompletionApiTestCase(APITestCase): course_id=COURSE_TEST_ID, title="Test Lehrgang Session", ) + DueDateFactory(course_session=self.course_session) self.client.login(username="student", password="test") @@ -51,3 +53,4 @@ class CourseCompletionApiTestCase(APITestCase): print(json.dumps(response.json(), indent=4)) self.assertEqual(response.json()[0]["id"], self.course_session.id) + self.assertNotEquals(response.json()[0]["duedates"], []) diff --git a/server/vbv_lernwelt/duedate/serializers.py b/server/vbv_lernwelt/duedate/serializers.py new file mode 100644 index 00000000..d2219461 --- /dev/null +++ b/server/vbv_lernwelt/duedate/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from vbv_lernwelt.duedate.models import DueDate + + +class DueDateSerializer(serializers.ModelSerializer): + class Meta: + model = DueDate + fields = '__all__' diff --git a/server/vbv_lernwelt/duedate/tests/test_duedate_serializer.py b/server/vbv_lernwelt/duedate/tests/test_duedate_serializer.py new file mode 100644 index 00000000..e9e4e027 --- /dev/null +++ b/server/vbv_lernwelt/duedate/tests/test_duedate_serializer.py @@ -0,0 +1,15 @@ +from django.test import TestCase + +from vbv_lernwelt.duedate.factories import DueDateFactory +from vbv_lernwelt.duedate.models import DueDate +from vbv_lernwelt.duedate.serializers import DueDateSerializer + + +class TestDueDatetSerializer(TestCase): + def test_duedate_serializer(self): + DueDateFactory() + assert DueDate.objects.count() == 1 + + duedates = DueDate.objects.all() + result = DueDateSerializer(duedates, many=True).data + assert result[0]['title'] == 'Prüfung Versicherungsvermittler/-in' From b92ac8b08aa86be558befb8967598c4d968d3889 Mon Sep 17 00:00:00 2001 From: Lorenz Padberg Date: Thu, 15 Jun 2023 12:05:20 +0200 Subject: [PATCH 05/39] Add duedates to frontend --- .../LearningPathAppointmentsMock.vue | 57 +++++++++++++++---- client/src/types.ts | 11 ++++ server/vbv_lernwelt/duedate/models.py | 2 +- 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/client/src/pages/learningPath/learningPathPage/LearningPathAppointmentsMock.vue b/client/src/pages/learningPath/learningPathPage/LearningPathAppointmentsMock.vue index b0816122..22fb8516 100644 --- a/client/src/pages/learningPath/learningPathPage/LearningPathAppointmentsMock.vue +++ b/client/src/pages/learningPath/learningPathPage/LearningPathAppointmentsMock.vue @@ -1,13 +1,48 @@ + + diff --git a/client/src/types.ts b/client/src/types.ts index 547abd8b..c9cd0b80 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -442,6 +442,7 @@ export interface CourseSession { assignment_details_list: CourseSessionAssignmentDetails[]; documents: CircleDocument[]; users: CourseSessionUser[]; + duedates: DueDate[]; } export type Role = "MEMBER" | "EXPERT" | "TUTOR"; @@ -556,3 +557,13 @@ export interface UserAssignmentCompletionStatus { completion_status: AssignmentCompletionStatus; evaluation_grade: number | null; } + +export type DueDate = { + id: number; + start: Date; + end: Date; + title: string; + url: string; + course_session: number | null; + page: number | null; +}; diff --git a/server/vbv_lernwelt/duedate/models.py b/server/vbv_lernwelt/duedate/models.py index 884fa9be..0b375c5c 100644 --- a/server/vbv_lernwelt/duedate/models.py +++ b/server/vbv_lernwelt/duedate/models.py @@ -17,7 +17,7 @@ class DueDate(models.Model): course_session = models.ForeignKey( 'course.CourseSession', on_delete=models.CASCADE, - related_name='events', + related_name='duedates', null=True, blank=True, ) From 639683c06d14b8b02f6205efd4f2f3d90c69523c Mon Sep 17 00:00:00 2001 From: Lorenz Padberg Date: Thu, 15 Jun 2023 14:43:47 +0200 Subject: [PATCH 06/39] Add DueDatesFrontendComponents --- .../dueDates/DueDateSingle.vue} | 32 +++++++++---------- .../components/dueDates/DueDatesShortList.vue | 28 ++++++++++++++++ .../learningPathPage/LearningPathPage.vue | 4 +-- 3 files changed, 45 insertions(+), 19 deletions(-) rename client/src/{pages/learningPath/learningPathPage/LearningPathAppointmentsMock.vue => components/dueDates/DueDateSingle.vue} (56%) create mode 100644 client/src/components/dueDates/DueDatesShortList.vue diff --git a/client/src/pages/learningPath/learningPathPage/LearningPathAppointmentsMock.vue b/client/src/components/dueDates/DueDateSingle.vue similarity index 56% rename from client/src/pages/learningPath/learningPathPage/LearningPathAppointmentsMock.vue rename to client/src/components/dueDates/DueDateSingle.vue index 22fb8516..68e6cc1c 100644 --- a/client/src/pages/learningPath/learningPathPage/LearningPathAppointmentsMock.vue +++ b/client/src/components/dueDates/DueDateSingle.vue @@ -1,26 +1,20 @@ diff --git a/client/src/components/dueDates/DueDatesShortList.vue b/client/src/components/dueDates/DueDatesShortList.vue new file mode 100644 index 00000000..1b73f8f2 --- /dev/null +++ b/client/src/components/dueDates/DueDatesShortList.vue @@ -0,0 +1,28 @@ + + + diff --git a/client/src/pages/learningPath/learningPathPage/LearningPathPage.vue b/client/src/pages/learningPath/learningPathPage/LearningPathPage.vue index 4a854672..4fcb1fdc 100644 --- a/client/src/pages/learningPath/learningPathPage/LearningPathPage.vue +++ b/client/src/pages/learningPath/learningPathPage/LearningPathPage.vue @@ -1,5 +1,5 @@ diff --git a/client/src/components/dueDates/DueDatesLongList.vue b/client/src/components/dueDates/DueDatesLongList.vue new file mode 100644 index 00000000..b4399ced --- /dev/null +++ b/client/src/components/dueDates/DueDatesLongList.vue @@ -0,0 +1,43 @@ + + + diff --git a/client/src/components/dueDates/DueDatesShortList.vue b/client/src/components/dueDates/DueDatesShortList.vue index 1b73f8f2..abf95b82 100644 --- a/client/src/components/dueDates/DueDatesShortList.vue +++ b/client/src/components/dueDates/DueDatesShortList.vue @@ -1,10 +1,11 @@ @@ -13,16 +14,17 @@ import SingleDueDate from "@/components/dueDates/DueDateSingle.vue"; import { useCurrentCourseSession } from "@/composables"; import { defineProps } from "vue"; +// TODO: MaxCount is not working const props = defineProps<{ maxCount: { - type: number; default: 3; + type: number; }; }>(); const courseSession = useCurrentCourseSession(); console.log(props.maxCount); -const dueDates = courseSession.value.duedates.slice(0, 3); +const dueDates = courseSession.value.duedates.slice(0, props.maxCount); console.log(courseSession.value.duedates); diff --git a/client/src/components/dueDates/DueDatesTestData.ts b/client/src/components/dueDates/DueDatesTestData.ts new file mode 100644 index 00000000..1fdd6e16 --- /dev/null +++ b/client/src/components/dueDates/DueDatesTestData.ts @@ -0,0 +1,58 @@ +export const dueDatesTestData = () => { + return [ + { + id: 1, + start: "2023-06-14T15:00:00+02:00", + end: "2023-06-14T18:00:00+02:00", + title: "Präsenzkurs Kickoff", + url: "/course/überbetriebliche-kurse/learn/kickoff/präsenzkurs-kickoff", + course_session: 2, + page: 383, + }, + { + id: 2, + start: "2023-06-15T15:00:00+02:00", + end: "2023-06-15T18:00:00+02:00", + title: "Präsenzkurs Basis", + url: "/course/überbetriebliche-kurse/learn/basis/präsenzkurs-basis", + course_session: 2, + page: 397, + }, + { + id: 3, + start: "2023-06-16T15:00:00+02:00", + end: "2023-06-16T18:00:00+02:00", + title: "Präsenzkurs Fahrzeug", + url: "/course/überbetriebliche-kurse/learn/fahrzeug/präsenzkurs-fahrzeug", + course_session: 2, + page: 413, + }, + { + id: 4, + start: "2023-06-16T15:00:00+02:00", + end: "2023-06-16T18:00:00+02:00", + title: "Präsenzkurs Flugzeuge", + url: "/course/überbetriebliche-kurse/learn/fahrzeug/präsenzkurs-fahrzeug", + course_session: 2, + page: 413, + }, + { + id: 5, + start: "2023-07-16T11:00:00+02:00", + end: "2023-07-16T18:00:00+02:00", + title: "Präsenzkurs Motorräder", + url: "/course/überbetriebliche-kurse/learn/fahrzeug/präsenzkurs-fahrzeug", + course_session: 2, + page: 413, + }, + { + id: 6, + start: "2023-08-09T15:00:00+02:00", + end: "2023-08-09T19:00:00+02:00", + title: "Präsenzkurs Fahrräder", + url: "/course/überbetriebliche-kurse/learn/fahrzeug/präsenzkurs-fahrzeug", + course_session: 2, + page: 413, + }, + ]; +}; diff --git a/client/src/components/dueDates/DueDatesUtils.ts b/client/src/components/dueDates/DueDatesUtils.ts new file mode 100644 index 00000000..749b656b --- /dev/null +++ b/client/src/components/dueDates/DueDatesUtils.ts @@ -0,0 +1,27 @@ +export const formatDate = (start_str: string, end_str: string) => { + const start = new Date(start_str); + const end = new Date(end_str); + + const startDateString = getDateString(start); + const endDateString = getDateString(end); + + // if start and end are on the same day, dont show the day twice + if (startDateString === endDateString) { + return `${startDateString} ${getTimeString(start)} - ${getTimeString(end)}`; + } + return `${startDateString} ${getTimeString(start)} - ${endDateString} ${getTimeString( + end + )}`; +}; + +const getTimeString = (date: Date) => { + return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); +}; + +const getDateString = (date: Date) => { + return date.toLocaleDateString([], { + day: "numeric", + month: "short", + year: "numeric", + }); +}; diff --git a/client/src/pages/DashboardPage.vue b/client/src/pages/DashboardPage.vue index 44c3c8b5..89b8a608 100644 --- a/client/src/pages/DashboardPage.vue +++ b/client/src/pages/DashboardPage.vue @@ -1,4 +1,5 @@ diff --git a/client/src/components/dueDates/DueDatesShortList.vue b/client/src/components/dueDates/DueDatesShortList.vue index abf95b82..e4352088 100644 --- a/client/src/components/dueDates/DueDatesShortList.vue +++ b/client/src/components/dueDates/DueDatesShortList.vue @@ -23,8 +23,6 @@ const props = defineProps<{ }>(); const courseSession = useCurrentCourseSession(); -console.log(props.maxCount); const dueDates = courseSession.value.duedates.slice(0, props.maxCount); -console.log(courseSession.value.duedates); diff --git a/client/src/components/dueDates/DueDatesTestData.ts b/client/src/components/dueDates/dueDatesTestData.ts similarity index 66% rename from client/src/components/dueDates/DueDatesTestData.ts rename to client/src/components/dueDates/dueDatesTestData.ts index 1fdd6e16..fe14ff32 100644 --- a/client/src/components/dueDates/DueDatesTestData.ts +++ b/client/src/components/dueDates/dueDatesTestData.ts @@ -1,9 +1,11 @@ +import dayjs from "dayjs"; + export const dueDatesTestData = () => { return [ { id: 1, - start: "2023-06-14T15:00:00+02:00", - end: "2023-06-14T18:00:00+02:00", + start: dayjs("2023-06-14T15:00:00+02:00"), + end: dayjs("2023-06-14T18:00:00+02:00"), title: "Präsenzkurs Kickoff", url: "/course/überbetriebliche-kurse/learn/kickoff/präsenzkurs-kickoff", course_session: 2, @@ -11,8 +13,8 @@ export const dueDatesTestData = () => { }, { id: 2, - start: "2023-06-15T15:00:00+02:00", - end: "2023-06-15T18:00:00+02:00", + start: dayjs("2023-06-15T15:00:00+02:00"), + end: dayjs("2023-06-15T18:00:00+02:00"), title: "Präsenzkurs Basis", url: "/course/überbetriebliche-kurse/learn/basis/präsenzkurs-basis", course_session: 2, @@ -20,8 +22,8 @@ export const dueDatesTestData = () => { }, { id: 3, - start: "2023-06-16T15:00:00+02:00", - end: "2023-06-16T18:00:00+02:00", + start: dayjs("2023-06-16T15:00:00+02:00"), + end: dayjs("2023-06-16T18:00:00+02:00"), title: "Präsenzkurs Fahrzeug", url: "/course/überbetriebliche-kurse/learn/fahrzeug/präsenzkurs-fahrzeug", course_session: 2, @@ -29,8 +31,8 @@ export const dueDatesTestData = () => { }, { id: 4, - start: "2023-06-16T15:00:00+02:00", - end: "2023-06-16T18:00:00+02:00", + start: dayjs("2023-06-16T15:00:00+02:00"), + end: dayjs("2023-06-16T18:00:00+02:00"), title: "Präsenzkurs Flugzeuge", url: "/course/überbetriebliche-kurse/learn/fahrzeug/präsenzkurs-fahrzeug", course_session: 2, @@ -38,8 +40,8 @@ export const dueDatesTestData = () => { }, { id: 5, - start: "2023-07-16T11:00:00+02:00", - end: "2023-07-16T18:00:00+02:00", + start: dayjs("2023-07-16T11:00:00+02:00"), + end: dayjs("2023-07-16T18:00:00+02:00"), title: "Präsenzkurs Motorräder", url: "/course/überbetriebliche-kurse/learn/fahrzeug/präsenzkurs-fahrzeug", course_session: 2, @@ -47,8 +49,8 @@ export const dueDatesTestData = () => { }, { id: 6, - start: "2023-08-09T15:00:00+02:00", - end: "2023-08-09T19:00:00+02:00", + start: dayjs("2023-08-09T15:00:00+02:00"), + end: dayjs("2023-08-09T19:00:00+02:00"), title: "Präsenzkurs Fahrräder", url: "/course/überbetriebliche-kurse/learn/fahrzeug/präsenzkurs-fahrzeug", course_session: 2, diff --git a/client/src/components/dueDates/DueDatesUtils.ts b/client/src/components/dueDates/dueDatesUtils.ts similarity index 50% rename from client/src/components/dueDates/DueDatesUtils.ts rename to client/src/components/dueDates/dueDatesUtils.ts index 749b656b..be340135 100644 --- a/client/src/components/dueDates/DueDatesUtils.ts +++ b/client/src/components/dueDates/dueDatesUtils.ts @@ -1,7 +1,6 @@ -export const formatDate = (start_str: string, end_str: string) => { - const start = new Date(start_str); - const end = new Date(end_str); +import type { Dayjs } from "dayjs"; +export const formatDate = (start: Dayjs, end: Dayjs) => { const startDateString = getDateString(start); const endDateString = getDateString(end); @@ -14,14 +13,10 @@ export const formatDate = (start_str: string, end_str: string) => { )}`; }; -const getTimeString = (date: Date) => { - return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); +const getTimeString = (date: Dayjs) => { + return `${date.format("HH:mm")}`; }; -const getDateString = (date: Date) => { - return date.toLocaleDateString([], { - day: "numeric", - month: "short", - year: "numeric", - }); +const getDateString = (date: Dayjs) => { + return `${date.format("DD MMM YYYY")}`; }; diff --git a/client/src/pages/learningPath/learningPathPage/LearningPathCircle.vue b/client/src/pages/learningPath/learningPathPage/LearningPathCircle.vue index e5e39c4d..12f31916 100644 --- a/client/src/pages/learningPath/learningPathPage/LearningPathCircle.vue +++ b/client/src/pages/learningPath/learningPathPage/LearningPathCircle.vue @@ -1,6 +1,5 @@ diff --git a/client/src/pages/DashboardPage.vue b/client/src/pages/DashboardPage.vue index 89b8a608..e97c97bc 100644 --- a/client/src/pages/DashboardPage.vue +++ b/client/src/pages/DashboardPage.vue @@ -16,7 +16,13 @@ onMounted(async () => { log.debug("DashboardPage mounted"); }); +const allDueDates = courseSessionsStore.allDueDates(); const getNextStepLink = (courseSession: CourseSession) => { + console.log("courseSession: ", courseSession); + + courseSessionsStore.allCourseSessions.forEach((courseSession) => { + console.log("duedates: ", courseSession.duedates); + }); return computed(() => { if (courseSessionsStore.hasCockpit(courseSession)) { return courseSession.cockpit_url; @@ -71,7 +77,7 @@ const getNextStepLink = (courseSession: CourseSession) => {

Termine

- +
diff --git a/client/src/stores/courseSessions.ts b/client/src/stores/courseSessions.ts index d9be1282..3d5f9e75 100644 --- a/client/src/stores/courseSessions.ts +++ b/client/src/stores/courseSessions.ts @@ -6,6 +6,7 @@ import type { CourseSessionAssignmentDetails, CourseSessionAttendanceCourse, CourseSessionUser, + DueDate, ExpertSessionUser, } from "@/types"; import eventBus from "@/utils/eventBus"; @@ -42,7 +43,6 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => { cs.duedates.forEach((dueDate) => { dueDate.start = dayjs(dueDate.start); dueDate.end = dayjs(dueDate.end); - console.log(dueDate); }); }) ); @@ -195,6 +195,16 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => { currentCourseSession.value?.documents.push(document); } + function allDueDates() { + const allDueDatesReturn: DueDate[] = []; + + allCourseSessions.value?.forEach((cs) => { + allDueDatesReturn.push(...cs.duedates); + }); + allDueDatesReturn.sort((a, b) => dayjs(a.end).diff(dayjs(b.end))); + return allDueDatesReturn; + } + async function startUpload() { log.debug("loadCourseSessionsData called"); allCourseSessions.value = await itPost(`/api/core/file/start`, { @@ -250,6 +260,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => { removeDocument, findAttendanceCourse, findAssignmentDetails, + allDueDates, // use `useCurrentCourseSession` whenever possible currentCourseSession, From e7917a6cdbc773b70ecea349517ece86b71f5801 Mon Sep 17 00:00:00 2001 From: Lorenz Padberg Date: Mon, 19 Jun 2023 15:42:50 +0200 Subject: [PATCH 10/39] Fix linting errors --- client/src/components/dueDates/DueDateSingle.vue | 2 +- client/src/components/dueDates/DueDatesLongList.vue | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/client/src/components/dueDates/DueDateSingle.vue b/client/src/components/dueDates/DueDateSingle.vue index ba1ec7f2..d40dfb59 100644 --- a/client/src/components/dueDates/DueDateSingle.vue +++ b/client/src/components/dueDates/DueDateSingle.vue @@ -11,7 +11,7 @@ import { formatDate } from "@/components/dueDates/dueDatesUtils"; import type { DueDate } from "@/types"; import { defineProps } from "vue"; -const props = defineProps<{ +defineProps<{ dueDate: DueDate; }>(); diff --git a/client/src/components/dueDates/DueDatesLongList.vue b/client/src/components/dueDates/DueDatesLongList.vue index 8ef652d4..85e8e0cf 100644 --- a/client/src/components/dueDates/DueDatesLongList.vue +++ b/client/src/components/dueDates/DueDatesLongList.vue @@ -18,8 +18,7 @@ From ffefa520e197e6d92c851ba84552fd1383d41e9d Mon Sep 17 00:00:00 2001 From: Lorenz Padberg Date: Mon, 19 Jun 2023 16:31:47 +0200 Subject: [PATCH 11/39] Fix typechecker --- client/src/components/dueDates/DueDateSingle.vue | 7 +++---- client/src/components/dueDates/DueDatesLongList.vue | 8 ++------ client/src/components/dueDates/DueDatesShortList.vue | 6 +----- client/src/pages/DashboardPage.vue | 2 +- .../learningPath/learningPathPage/LearningPathPage.vue | 2 +- client/src/types.ts | 2 +- 6 files changed, 9 insertions(+), 18 deletions(-) diff --git a/client/src/components/dueDates/DueDateSingle.vue b/client/src/components/dueDates/DueDateSingle.vue index d40dfb59..3f2c6d59 100644 --- a/client/src/components/dueDates/DueDateSingle.vue +++ b/client/src/components/dueDates/DueDateSingle.vue @@ -1,7 +1,7 @@ @@ -9,9 +9,8 @@ diff --git a/client/src/components/dueDates/DueDatesLongList.vue b/client/src/components/dueDates/DueDatesLongList.vue index 85e8e0cf..786b45a3 100644 --- a/client/src/components/dueDates/DueDatesLongList.vue +++ b/client/src/components/dueDates/DueDatesLongList.vue @@ -18,15 +18,11 @@ diff --git a/client/src/components/dueDates/DueDatesShortList.vue b/client/src/components/dueDates/DueDatesShortList.vue index e4352088..58dae48f 100644 --- a/client/src/components/dueDates/DueDatesShortList.vue +++ b/client/src/components/dueDates/DueDatesShortList.vue @@ -12,14 +12,10 @@ diff --git a/client/src/components/dueDates/dueDatesUtils.ts b/client/src/components/dueDates/dueDatesUtils.ts index be340135..7535e2fb 100644 --- a/client/src/components/dueDates/dueDatesUtils.ts +++ b/client/src/components/dueDates/dueDatesUtils.ts @@ -20,3 +20,7 @@ const getTimeString = (date: Dayjs) => { const getDateString = (date: Dayjs) => { return `${date.format("DD MMM YYYY")}`; }; + +export const getWeekday = (date: Dayjs) => { + return `${date.format("dd")}`; +}; From a5acc6698123901378f6dc783fa8f07194f78025 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 14 Jun 2023 17:12:37 +0200 Subject: [PATCH 13/39] VBV-450: Custom CourseSessionAttendanceCourse model --- client/src/stores/courseSessions.ts | 2 +- client/src/types.ts | 2 +- server/config/settings/base.py | 1 + .../core/management/commands/reset_schema.py | 2 +- .../commands/create_default_courses.py | 43 ++++++++++++++----- ...remove_coursesession_attendance_courses.py | 17 ++++++++ server/vbv_lernwelt/course/models.py | 37 ++++++++-------- server/vbv_lernwelt/course/serializers.py | 10 +++++ .../vbv_lernwelt/course_session/__init__.py | 0 server/vbv_lernwelt/course_session/admin.py | 0 server/vbv_lernwelt/course_session/apps.py | 13 ++++++ .../vbv_lernwelt/course_session/factories.py | 0 .../course_session/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../course_session/migrations/0001_initial.py | 29 +++++++++++++ .../course_session/migrations/__init__.py | 0 server/vbv_lernwelt/course_session/models.py | 23 ++++++++++ .../course_session/serializers.py | 27 ++++++++++++ .../course_session/tests/__init__.py | 0 server/vbv_lernwelt/course_session/views.py | 3 ++ server/vbv_lernwelt/importer/services.py | 37 ++++++++-------- .../tests/test_import_course_sessions.py | 41 ++++++------------ 22 files changed, 207 insertions(+), 80 deletions(-) create mode 100644 server/vbv_lernwelt/course/migrations/0005_remove_coursesession_attendance_courses.py create mode 100644 server/vbv_lernwelt/course_session/__init__.py create mode 100644 server/vbv_lernwelt/course_session/admin.py create mode 100644 server/vbv_lernwelt/course_session/apps.py create mode 100644 server/vbv_lernwelt/course_session/factories.py create mode 100644 server/vbv_lernwelt/course_session/management/__init__.py create mode 100644 server/vbv_lernwelt/course_session/management/commands/__init__.py create mode 100644 server/vbv_lernwelt/course_session/migrations/0001_initial.py create mode 100644 server/vbv_lernwelt/course_session/migrations/__init__.py create mode 100644 server/vbv_lernwelt/course_session/models.py create mode 100644 server/vbv_lernwelt/course_session/serializers.py create mode 100644 server/vbv_lernwelt/course_session/tests/__init__.py create mode 100644 server/vbv_lernwelt/course_session/views.py diff --git a/client/src/stores/courseSessions.ts b/client/src/stores/courseSessions.ts index 3d5f9e75..d6382ef9 100644 --- a/client/src/stores/courseSessions.ts +++ b/client/src/stores/courseSessions.ts @@ -230,7 +230,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => { ): CourseSessionAttendanceCourse | undefined { if (currentCourseSession.value) { return currentCourseSession.value.attendance_courses.find( - (attendanceCourse) => attendanceCourse.learningContentId === contentId + (attendanceCourse) => attendanceCourse.learning_content === contentId ); } } diff --git a/client/src/types.ts b/client/src/types.ts index 6f25ce92..ee8187a8 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -413,7 +413,7 @@ export interface CircleDocument { } export interface CourseSessionAttendanceCourse { - learningContentId: number; + learning_content: number; start: string; end: string; location: string; diff --git a/server/config/settings/base.py b/server/config/settings/base.py index 4aaa3c86..7e92f087 100644 --- a/server/config/settings/base.py +++ b/server/config/settings/base.py @@ -116,6 +116,7 @@ LOCAL_APPS = [ "vbv_lernwelt.learnpath", "vbv_lernwelt.competence", "vbv_lernwelt.media_library", + "vbv_lernwelt.course_session", "vbv_lernwelt.feedback", "vbv_lernwelt.files", "vbv_lernwelt.notify", diff --git a/server/vbv_lernwelt/core/management/commands/reset_schema.py b/server/vbv_lernwelt/core/management/commands/reset_schema.py index 885a9e51..d0f8a469 100644 --- a/server/vbv_lernwelt/core/management/commands/reset_schema.py +++ b/server/vbv_lernwelt/core/management/commands/reset_schema.py @@ -29,4 +29,4 @@ def command(): call_command("migrate") call_command("create_default_users") call_command("create_default_courses") - call_command("create_default_duedates") + # call_command("create_default_duedates") diff --git a/server/vbv_lernwelt/course/management/commands/create_default_courses.py b/server/vbv_lernwelt/course/management/commands/create_default_courses.py index 466efe4e..b43d1414 100644 --- a/server/vbv_lernwelt/course/management/commands/create_default_courses.py +++ b/server/vbv_lernwelt/course/management/commands/create_default_courses.py @@ -1,7 +1,9 @@ import os import random +from datetime import datetime import djclick as click +from django.utils import timezone from vbv_lernwelt.assignment.creators.create_assignments import ( create_uk_basis_prep_assignment, @@ -68,6 +70,8 @@ from vbv_lernwelt.course.models import ( CourseSessionUser, ) from vbv_lernwelt.course.services import mark_course_completion +from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse +from vbv_lernwelt.duedate.models import DueDate from vbv_lernwelt.feedback.creators.create_demo_feedback import create_feedback from vbv_lernwelt.importer.services import ( import_course_sessions_from_excel, @@ -237,17 +241,17 @@ def create_course_uk_de(): cs = CourseSession.objects.create( course_id=COURSE_UK, title="Bern 2023 a", - attendance_courses=[ - { - "learningContentId": LearningContentAttendanceCourse.objects.get( - slug="überbetriebliche-kurse-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug" - ).id, - "start": "2023-05-23T08:30:00+0200", - "end": "2023-05-23T17:00:00+0200", - "location": "Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern", - "trainer": "Roland Grossenbacher, roland.grossenbacher@helvetia.ch", - } - ], + # attendance_courses=[ + # { + # "learningContentId": LearningContentAttendanceCourse.objects.get( + # slug="überbetriebliche-kurse-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug" + # ).id, + # "start": "2023-05-23T08:30:00+0200", + # "end": "2023-05-23T17:00:00+0200", + # "location": "Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern", + # "trainer": "Roland Grossenbacher, roland.grossenbacher@helvetia.ch", + # } + # ], assignment_details_list=[ { "learningContentId": LearningContentAssignment.objects.get( @@ -266,6 +270,23 @@ def create_course_uk_de(): ], ) + csac = CourseSessionAttendanceCourse.objects.create( + course_session=cs, + learning_content=LearningContentAttendanceCourse.objects.get( + slug="überbetriebliche-kurse-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug" + ), + due_date=DueDate.objects.create( + course_session=cs, + start=timezone.make_aware(datetime(2023, 6, 14, 8, 30)), + end=timezone.make_aware(datetime(2023, 6, 14, 17, 0)), + page=LearningContentAttendanceCourse.objects.get( + slug="überbetriebliche-kurse-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug" + ), + ), + location="Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern", + trainer="Roland Grossenbacher, roland.grossenbacher@helvetia.ch", + ) + # figma demo users and data csu = CourseSessionUser.objects.create( course_session=cs, diff --git a/server/vbv_lernwelt/course/migrations/0005_remove_coursesession_attendance_courses.py b/server/vbv_lernwelt/course/migrations/0005_remove_coursesession_attendance_courses.py new file mode 100644 index 00000000..a02995df --- /dev/null +++ b/server/vbv_lernwelt/course/migrations/0005_remove_coursesession_attendance_courses.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.13 on 2023-06-14 14:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0004_import_fields'), + ] + + operations = [ + migrations.RemoveField( + model_name='coursesession', + name='attendance_courses', + ), + ] diff --git a/server/vbv_lernwelt/course/models.py b/server/vbv_lernwelt/course/models.py index 2394009d..b0910612 100644 --- a/server/vbv_lernwelt/course/models.py +++ b/server/vbv_lernwelt/course/models.py @@ -195,23 +195,23 @@ class CourseSession(models.Model): # TODO: Das wird durch event modell ersetzt - ATTENDANCE_COURSES_SCHEMA = { - "type": "array", - "items": { - "type": "object", - "properties": { - "learningContentId": { - "type": "number", - "title": "ID des Lerninhalts", - "required": True, - }, - "start": {"type": "string", "format": "datetime"}, - "end": {"type": "string", "format": "datetime"}, - "location": {"type": "string"}, - "trainer": {"type": "string"}, - }, - }, - } + # ATTENDANCE_COURSES_SCHEMA = { + # "type": "array", + # "items": { + # "type": "object", + # "properties": { + # "learningContentId": { + # "type": "number", + # "title": "ID des Lerninhalts", + # "required": True, + # }, + # "start": {"type": "string", "format": "datetime"}, + # "end": {"type": "string", "format": "datetime"}, + # "location": {"type": "string"}, + # "trainer": {"type": "string"}, + # }, + # }, + # } created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -228,9 +228,6 @@ class CourseSession(models.Model): start_date = models.DateField(null=True, blank=True) end_date = models.DateField(null=True, blank=True) - attendance_courses = JSONField( - schema=ATTENDANCE_COURSES_SCHEMA, blank=True, default=list - ) assignment_details_list = models.JSONField(default=list, blank=True) additional_json_data = models.JSONField(default=dict, blank=True) diff --git a/server/vbv_lernwelt/course/serializers.py b/server/vbv_lernwelt/course/serializers.py index 6c162cce..86ba9af0 100644 --- a/server/vbv_lernwelt/course/serializers.py +++ b/server/vbv_lernwelt/course/serializers.py @@ -7,6 +7,10 @@ from vbv_lernwelt.course.models import ( CourseCompletion, CourseSession, ) +from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse +from vbv_lernwelt.course_session.serializers import ( + CourseSessionAttendanceCourseSerializer, +) from vbv_lernwelt.duedate.models import DueDate from vbv_lernwelt.duedate.serializers import DueDateSerializer @@ -52,6 +56,7 @@ class CourseSessionSerializer(serializers.ModelSerializer): competence_url = serializers.SerializerMethodField() media_library_url = serializers.SerializerMethodField() documents = serializers.SerializerMethodField() + attendance_courses = serializers.SerializerMethodField() duedates = serializers.SerializerMethodField() def get_course(self, obj): @@ -78,6 +83,11 @@ class CourseSessionSerializer(serializers.ModelSerializer): ) return CircleDocumentSerializer(documents, many=True).data + def get_attendance_courses(self, obj): + return CourseSessionAttendanceCourseSerializer( + CourseSessionAttendanceCourse.objects.filter(course_session=obj), many=True + ).data + def get_duedates(self, obj): # TODO: Filter by user / userrole duedates = DueDate.objects.filter(course_session=obj) diff --git a/server/vbv_lernwelt/course_session/__init__.py b/server/vbv_lernwelt/course_session/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/course_session/admin.py b/server/vbv_lernwelt/course_session/admin.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/course_session/apps.py b/server/vbv_lernwelt/course_session/apps.py new file mode 100644 index 00000000..851de1b9 --- /dev/null +++ b/server/vbv_lernwelt/course_session/apps.py @@ -0,0 +1,13 @@ +from django.apps import AppConfig + + +class CourseSessionConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "vbv_lernwelt.course_session" + + def ready(self): + try: + # pylint: disable=unused-import,import-outside-toplevel + import vbv_lernwelt.course_session.signals # noqa F401 + except ImportError: + pass diff --git a/server/vbv_lernwelt/course_session/factories.py b/server/vbv_lernwelt/course_session/factories.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/course_session/management/__init__.py b/server/vbv_lernwelt/course_session/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/course_session/management/commands/__init__.py b/server/vbv_lernwelt/course_session/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/course_session/migrations/0001_initial.py b/server/vbv_lernwelt/course_session/migrations/0001_initial.py new file mode 100644 index 00000000..64efbf20 --- /dev/null +++ b/server/vbv_lernwelt/course_session/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.13 on 2023-06-14 15:01 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('learnpath', '0007_learningunit_title_hidden'), + ('course', '0005_remove_coursesession_attendance_courses'), + ('duedate', '0002_auto_20230614_1500'), + ] + + operations = [ + migrations.CreateModel( + name='CourseSessionAttendanceCourse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('location', models.CharField(blank=True, default='', max_length=255)), + ('trainer', models.CharField(blank=True, default='', max_length=255)), + ('course_session', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.coursesession')), + ('due_date', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='attendance_course_due_date', to='duedate.duedate')), + ('learning_content', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='learnpath.learningcontentattendancecourse')), + ], + ), + ] diff --git a/server/vbv_lernwelt/course_session/migrations/__init__.py b/server/vbv_lernwelt/course_session/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/course_session/models.py b/server/vbv_lernwelt/course_session/models.py new file mode 100644 index 00000000..5c102872 --- /dev/null +++ b/server/vbv_lernwelt/course_session/models.py @@ -0,0 +1,23 @@ +from django.db import models + + +class CourseSessionAttendanceCourse(models.Model): + course_session = models.ForeignKey( + "course.CourseSession", + on_delete=models.CASCADE, + ) + learning_content = models.ForeignKey( + "learnpath.LearningContentAttendanceCourse", + on_delete=models.CASCADE, + ) + due_date = models.OneToOneField( + "duedate.DueDate", + on_delete=models.CASCADE, + related_name="attendance_course_due_date", + ) + + location = models.CharField(max_length=255, blank=True, default="") + trainer = models.CharField(max_length=255, blank=True, default="") + + def __str__(self): + return f"{self.course_session} - {self.learning_content}" diff --git a/server/vbv_lernwelt/course_session/serializers.py b/server/vbv_lernwelt/course_session/serializers.py new file mode 100644 index 00000000..41dbc6fb --- /dev/null +++ b/server/vbv_lernwelt/course_session/serializers.py @@ -0,0 +1,27 @@ +from rest_framework import serializers + +from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse + + +class CourseSessionAttendanceCourseSerializer(serializers.ModelSerializer): + start = serializers.SerializerMethodField() + end = serializers.SerializerMethodField() + + class Meta: + model = CourseSessionAttendanceCourse + fields = [ + "id", + "course_session", + "learning_content", + "due_date", + "location", + "trainer", + "start", + "end", + ] + + def get_start(self, obj): + return obj.due_date.start + + def get_end(self, obj): + return obj.due_date.end diff --git a/server/vbv_lernwelt/course_session/tests/__init__.py b/server/vbv_lernwelt/course_session/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/course_session/views.py b/server/vbv_lernwelt/course_session/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/server/vbv_lernwelt/course_session/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/server/vbv_lernwelt/importer/services.py b/server/vbv_lernwelt/importer/services.py index c3a29d96..c9840c17 100644 --- a/server/vbv_lernwelt/importer/services.py +++ b/server/vbv_lernwelt/importer/services.py @@ -5,6 +5,8 @@ from openpyxl.reader.excel import load_workbook from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser +from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse +from vbv_lernwelt.duedate.models import DueDate from vbv_lernwelt.importer.utils import ( calc_header_tuple_list_from_pyxl_sheet, parse_circle_group_string, @@ -109,42 +111,40 @@ def create_or_update_course_session( cs.save() for circle in circles: + attendance_course_lp_qs = None if language == "de": attendance_course_lp_qs = LearningContentAttendanceCourse.objects.filter( slug=f"{course.slug}-lp-circle-{circle.lower()}-lc-präsenzkurs-{circle.lower()}" ) - add_attendance_course_date(cs, attendance_course_lp_qs, circle, data) + elif language == "fr": # todo: this is a hack remove me attendance_course_lp_qs = LearningContentAttendanceCourse.objects.filter( slug=f"{course.slug}-lp-circle-véhicule-lc-cours-de-présence-véhicule-à-moteur" ) - add_attendance_course_date(cs, attendance_course_lp_qs, circle, data) elif language == "it": # todo: this is a hack remove me attendance_course_lp_qs = LearningContentAttendanceCourse.objects.filter( slug=f"{course.slug}-lp-circle-veicolo-lc-corso-di-presenza-veicolo" ) - print(attendance_course_lp_qs) - add_attendance_course_date(cs, attendance_course_lp_qs, circle, data) + + if attendance_course_lp_qs and attendance_course_lp_qs.exists(): + CourseSessionAttendanceCourse.objects.create( + course_session=cs, + learning_content=attendance_course_lp_qs.first(), + due_date=DueDate.objects.create( + course_session=cs, + start=try_parse_datetime(data[f"{circle} Start"])[1], + end=try_parse_datetime(data[f"{circle} Ende"])[1], + page=attendance_course_lp_qs.first(), + ), + location=data[f"{circle} Raum"], + trainer="", + ) return cs -def add_attendance_course_date(course_session, attendance_course_lp_qs, circle, data): - if attendance_course_lp_qs.exists(): - course_session.attendance_courses.append( - { - "learningContentId": attendance_course_lp_qs.first().id, - "start": try_parse_datetime(data[f"{circle} Start"])[1].isoformat(), - "end": try_parse_datetime(data[f"{circle} Ende"])[1].isoformat(), - "location": data[f"{circle} Raum"], - "trainer": "", - } - ) - course_session.save() - - def import_trainers_from_excel(course: Course, filename: str, language="de"): workbook = load_workbook(filename=filename) sheet = workbook["Schulungen Trainer"] @@ -157,6 +157,7 @@ def import_trainers_from_excel(course: Course, filename: str, language="de"): def create_or_update_trainer(course: Course, data: Dict[str, Any], language="de"): logger.debug( "create_or_update_trainer", + course=course.title, data=data, label="import", ) diff --git a/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py b/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py index cb63de9f..c5c72503 100644 --- a/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py +++ b/server/vbv_lernwelt/importer/tests/test_import_course_sessions.py @@ -5,6 +5,7 @@ from openpyxl.reader.excel import load_workbook from vbv_lernwelt.course.creators.test_course import create_test_course from vbv_lernwelt.course.models import CourseSession +from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse from vbv_lernwelt.importer.services import create_or_update_course_session from vbv_lernwelt.importer.utils import calc_header_tuple_list_from_pyxl_sheet @@ -61,20 +62,12 @@ class CreateOrUpdateCourseSessionTestCase(TestCase): self.assertEqual(cs.region, "Deutschschweiz") self.assertEqual(cs.group, "A") - attendance_course = cs.attendance_courses[0] - attendance_course = { - k: v - for k, v in attendance_course.items() - if k not in ["learningContentId", "location"] - } - - self.assertDictEqual( - attendance_course, - { - "start": "2023-06-06T13:30:00", - "end": "2023-06-06T15:00:00", - "trainer": "", - }, + attendance_course = CourseSessionAttendanceCourse.objects.first() + self.assertEqual( + attendance_course.due_date.start.isoformat(), "2023-06-06T11:30:00+00:00" + ) + self.assertEqual( + attendance_course.due_date.end.isoformat(), "2023-06-06T13:00:00+00:00" ) def test_update_course_session(self): @@ -112,18 +105,10 @@ class CreateOrUpdateCourseSessionTestCase(TestCase): self.assertEqual(cs.region, "Deutschschweiz") self.assertEqual(cs.group, "A") - attendance_course = cs.attendance_courses[0] - attendance_course = { - k: v - for k, v in attendance_course.items() - if k not in ["learningContentId", "location"] - } - - self.assertDictEqual( - attendance_course, - { - "start": "2023-06-06T13:30:00", - "end": "2023-06-06T15:00:00", - "trainer": "", - }, + attendance_course = CourseSessionAttendanceCourse.objects.first() + self.assertEqual( + attendance_course.due_date.start.isoformat(), "2023-06-06T11:30:00+00:00" + ) + self.assertEqual( + attendance_course.due_date.end.isoformat(), "2023-06-06T13:00:00+00:00" ) From 2706d6785d06c198bd390ba35af5954ce89c2c16 Mon Sep 17 00:00:00 2001 From: Lorenz Padberg Date: Mon, 26 Jun 2023 17:44:31 +0200 Subject: [PATCH 14/39] Add CourseSessionAttendance course and CourseSessionAssignment --- .../course/creators/test_course.py | 37 +++++- .../commands/create_default_courses.py | 119 ++++++++++-------- server/vbv_lernwelt/course_session/admin.py | 26 ++++ server/vbv_lernwelt/course_session/models.py | 71 +++++++++++ .../course_session/tests/test_factories.py | 47 +++++++ .../course_session/tests/test_models.py | 39 ++++++ server/vbv_lernwelt/duedate/admin.py | 1 + 7 files changed, 288 insertions(+), 52 deletions(-) create mode 100644 server/vbv_lernwelt/course_session/tests/test_factories.py create mode 100644 server/vbv_lernwelt/course_session/tests/test_models.py diff --git a/server/vbv_lernwelt/course/creators/test_course.py b/server/vbv_lernwelt/course/creators/test_course.py index aaa0d743..09ce6bff 100644 --- a/server/vbv_lernwelt/course/creators/test_course.py +++ b/server/vbv_lernwelt/course/creators/test_course.py @@ -1,7 +1,9 @@ import json +from datetime import datetime import wagtail_factories from django.conf import settings +from django.utils import timezone from slugify import slugify from wagtail.models import Site from wagtail.rich_text import RichText @@ -34,7 +36,8 @@ from vbv_lernwelt.course.models import ( CourseSession, CourseSessionUser, ) -from vbv_lernwelt.learnpath.models import Circle +from vbv_lernwelt.course_session.models import CourseSessionAssignment, CourseSessionAttendanceCourse +from vbv_lernwelt.learnpath.models import Circle, LearningContentAssignment, LearningContentAttendanceCourse from vbv_lernwelt.learnpath.tests.learning_path_factories import ( CircleFactory, LearningContentAssignmentFactory, @@ -79,16 +82,20 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False): create_test_media_library() if with_sessions: + now = timezone.now() # course sessions cs_bern = CourseSession.objects.create( course_id=COURSE_TEST_ID, title="Test Bern 2022 a", id=TEST_COURSE_SESSION_BERN_ID, + start_date=now, ) cs_zurich = CourseSession.objects.create( course_id=COURSE_TEST_ID, title="Test Zürich 2022 a", id=TEST_COURSE_SESSION_ZURICH_ID, + start_date=now, + ) trainer1 = User.objects.get(email="test-trainer1@example.com") @@ -115,6 +122,12 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False): course_session=cs_zurich, user=student2, ) + for cs in CourseSession.objects.all(): + for assignment in LearningContentAssignment.objects.all(): + create_course_session_assignment(cs, assignment) + + for attendance_course in LearningContentAttendanceCourse.objects.all(): + create_course_session_attendance_course(cs, attendance_course) return course @@ -143,6 +156,28 @@ def create_test_assignment_submitted_data(assignment, course_session, user): ) +def create_course_session_assignment(course_session, assignment): + csa, created = CourseSessionAssignment.objects.get_or_create( + course_session=course_session, + learning_content=assignment, + ) + return csa + + +def create_course_session_attendance_course(course_session, course): + casc = CourseSessionAttendanceCourse.objects.create( + course_session=course_session, + learning_content=course, + location="Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern", + trainer="Roland Grossenbacher, roland.grossenbacher@helvetia.ch", + ) + + casc.due_date.start = timezone.make_aware(datetime(2023, 6, 14, 8, 30)) + casc.due_date.end = timezone.make_aware(datetime(2023, 6, 14, 17, 0)) + casc.due_date.save() + return casc + + def create_test_course_with_categories(apps=None, schema_editor=None): if apps is not None: Course = apps.get_model("course", "Course") diff --git a/server/vbv_lernwelt/course/management/commands/create_default_courses.py b/server/vbv_lernwelt/course/management/commands/create_default_courses.py index b43d1414..c9fb5ae2 100644 --- a/server/vbv_lernwelt/course/management/commands/create_default_courses.py +++ b/server/vbv_lernwelt/course/management/commands/create_default_courses.py @@ -1,6 +1,6 @@ import os import random -from datetime import datetime +from datetime import datetime, timedelta import djclick as click from django.utils import timezone @@ -70,7 +70,7 @@ from vbv_lernwelt.course.models import ( CourseSessionUser, ) from vbv_lernwelt.course.services import mark_course_completion -from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse +from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse, CourseSessionAssignment from vbv_lernwelt.duedate.models import DueDate from vbv_lernwelt.feedback.creators.create_demo_feedback import create_feedback from vbv_lernwelt.importer.services import ( @@ -84,9 +84,9 @@ from vbv_lernwelt.learnpath.create_vv_new_learning_path import ( from vbv_lernwelt.learnpath.models import ( Circle, LearningContent, - LearningContentAssignment, LearningContentAttendanceCourse, ) +from vbv_lernwelt.learnpath.models import LearningContentAssignment from vbv_lernwelt.media_library.create_default_media_library import ( create_default_media_library, ) @@ -252,22 +252,22 @@ def create_course_uk_de(): # "trainer": "Roland Grossenbacher, roland.grossenbacher@helvetia.ch", # } # ], - assignment_details_list=[ - { - "learningContentId": LearningContentAssignment.objects.get( - slug="überbetriebliche-kurse-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice" - ).id, - "submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z", - "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z", - }, - { - "learningContentId": LearningContentAssignment.objects.get( - slug="überbetriebliche-kurse-lp-circle-fahrzeug-lc-fahrzeug-mein-erstes-auto" - ).id, - "submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z", - "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z", - }, - ], + # assignment_details_list=[ + # { + # "learningContentId": LearningContentAssignment.objects.get( + # slug="überbetriebliche-kurse-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice" + # ).id, + # "submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z", + # "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z", + # }, + # { + # "learningContentId": LearningContentAssignment.objects.get( + # slug="überbetriebliche-kurse-lp-circle-fahrzeug-lc-fahrzeug-mein-erstes-auto" + # ).id, + # "submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z", + # "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z", + # }, + # ], ) csac = CourseSessionAttendanceCourse.objects.create( @@ -559,8 +559,25 @@ def create_course_training_de(): ) for cs in CourseSession.objects.filter(course_id=COURSE_UK_TRAINING): + csa = CourseSessionAssignment.objects.create( + course_session=cs, + learning_content=LearningContentAssignment.objects.get( + slug=f"{course.slug}-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice" + ), + ) + print(csa) + + submission_deadline = csa.submission_deadline + submission_deadline.end = cs.start_date + timedelta(days=14) + submission_deadline.save() + + evaluation_deadline = csa.evaluation_deadline + evaluation_deadline.end = cs.start_date + timedelta(days=28) + evaluation_deadline.save() + cs.assignment_details_list = [ { + "learningContentId": LearningContentAssignment.objects.get( slug=f"{course.slug}-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice" ).id, @@ -643,22 +660,22 @@ def create_course_training_fr(): ) for cs in CourseSession.objects.filter(course_id=COURSE_UK_TRAINING_FR): - cs.assignment_details_list = [ - { - "learningContentId": LearningContentAssignment.objects.get( - slug=f"{course.slug}-lp-circle-véhicule-lc-vérification-dune-police-dassurance-de-véhicule-à-moteur" - ).id, - "submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z", - "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z", - }, - { - "learningContentId": LearningContentAssignment.objects.get( - slug=f"{course.slug}-lp-circle-véhicule-lc-véhicule-à-moteur-ma-première-voiture" - ).id, - "submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z", - "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z", - }, - ] + # cs.assignment_details_list = [ + # { + # "learningContentId": LearningContentAssignment.objects.get( + # slug=f"{course.slug}-lp-circle-véhicule-lc-vérification-dune-police-dassurance-de-véhicule-à-moteur" + # ).id, + # "submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z", + # "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z", + # }, + # { + # "learningContentId": LearningContentAssignment.objects.get( + # slug=f"{course.slug}-lp-circle-véhicule-lc-véhicule-à-moteur-ma-première-voiture" + # ).id, + # "submissionDeadlineDateTimeUtc": "2023-06-13T19:00:00Z", + # "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z", + # }, + # ] cs.save() # attach users as trainers to ÜK course @@ -730,22 +747,22 @@ def create_course_training_it(): ) for cs in CourseSession.objects.filter(course_id=COURSE_UK_TRAINING_IT): - cs.assignment_details_list = [ - { - "learningContentId": LearningContentAssignment.objects.get( - slug=f"{course.slug}-lp-circle-veicolo-lc-verifica-di-una-polizza-di-assicurazione-veicoli-a-motore" - ).id, - "submissionDeadlineDateTimeUtc": "2023-06-20T19:00:00Z", - "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z", - }, - { - "learningContentId": LearningContentAssignment.objects.get( - slug=f"{course.slug}-lp-circle-veicolo-lc-veicolo-la-mia-prima-auto" - ).id, - "submissionDeadlineDateTimeUtc": "2023-06-20T19:00:00Z", - "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z", - }, - ] + # cs.assignment_details_list = [ + # { + # "learningContentId": LearningContentAssignment.objects.get( + # slug=f"{course.slug}-lp-circle-veicolo-lc-verifica-di-una-polizza-di-assicurazione-veicoli-a-motore" + # ).id, + # "submissionDeadlineDateTimeUtc": "2023-06-20T19:00:00Z", + # "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z", + # }, + # { + # "learningContentId": LearningContentAssignment.objects.get( + # slug=f"{course.slug}-lp-circle-veicolo-lc-veicolo-la-mia-prima-auto" + # ).id, + # "submissionDeadlineDateTimeUtc": "2023-06-20T19:00:00Z", + # "evaluationDeadlineDateTimeUtc": "2023-06-27T19:00:00Z", + # }, + # ] cs.save() # attach users as trainers to ÜK course diff --git a/server/vbv_lernwelt/course_session/admin.py b/server/vbv_lernwelt/course_session/admin.py index e69de29b..6e43ba5f 100644 --- a/server/vbv_lernwelt/course_session/admin.py +++ b/server/vbv_lernwelt/course_session/admin.py @@ -0,0 +1,26 @@ +from django.contrib import admin + +from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse, CourseSessionAssignment + + +@admin.register(CourseSessionAttendanceCourse) +class CourseSessionAttendanceCourseAdmin(admin.ModelAdmin): + # Inline fields are not possible for the DueDate model, because it is not a ForeignKey relatoion. + readonly_fields = ['course_session', 'learning_content', 'due_date'] + list_display = [ + "course_session", + "learning_content", + "trainer", + ] + list_filter = ["course_session__course"] + + +@admin.register(CourseSessionAssignment) +class CourseSessionAssignmentAdmin(admin.ModelAdmin): + # Inline fields are not possible for the DueDate model, because it is not a ForeignKey relatoion. + readonly_fields = ['course_session', 'learning_content'] + list_display = [ + "course_session", + "learning_content", + ] + list_filter = ["course_session__course"] diff --git a/server/vbv_lernwelt/course_session/models.py b/server/vbv_lernwelt/course_session/models.py index 5c102872..f4d06266 100644 --- a/server/vbv_lernwelt/course_session/models.py +++ b/server/vbv_lernwelt/course_session/models.py @@ -1,7 +1,15 @@ from django.db import models +from django.utils.translation import gettext_lazy as _ + +from vbv_lernwelt.duedate.models import DueDate class CourseSessionAttendanceCourse(models.Model): + """ + Präsenzkurs Durchührung + + Kann über einen Zeitraum von meheren Tagen gehen. + """ course_session = models.ForeignKey( "course.CourseSession", on_delete=models.CASCADE, @@ -19,5 +27,68 @@ class CourseSessionAttendanceCourse(models.Model): location = models.CharField(max_length=255, blank=True, default="") trainer = models.CharField(max_length=255, blank=True, default="") + def save(self, *args, **kwargs): + if not self.pk: + title = "" + page = None + if self.learning_content_id: + title = self.learning_content.title + page = self.learning_content.page_ptr + + self.due_date = DueDate.objects.create( + title=f"{title} {_('Präsenzkurs')}", + course_session=self.course_session, + page=page) + super().save(*args, **kwargs) + + def __str__(self): + return f"{self.course_session} - {self.learning_content}" + + +class CourseSessionAssignment(models.Model): + """ + Auftrag + - Geletitete Fallarbeit ist eine speziefische ausprägung eines Auftrags (assignment_type) + + """ + course_session = models.ForeignKey( + "course.CourseSession", + on_delete=models.CASCADE, + ) + learning_content = models.ForeignKey( + "learnpath.LearningContentAssignment", + on_delete=models.CASCADE, + ) + submission_deadline = models.OneToOneField( + "duedate.DueDate", + on_delete=models.CASCADE, + related_name="assignment_submission_deadline", + ) + + evaluation_deadline = models.OneToOneField( + "duedate.DueDate", + on_delete=models.CASCADE, + related_name="assignment_evaluation_deadline", + ) + + def save(self, *args, **kwargs): + if not self.pk: + title = "" + page = None + if self.learning_content_id: + title = self.learning_content.title + page = self.learning_content.page_ptr + + self.submission_deadline = DueDate.objects.create( + title=f"{title} {_('Submission Deadline')}", + course_session=self.course_session, + page=page) + + self.evaluation_deadline = DueDate.objects.create( + title=f"{title} {_('Evaluation Deadline')}", + course_session=self.course_session, + page=page) + super().save(*args, **kwargs) + def __str__(self): return f"{self.course_session} - {self.learning_content}" diff --git a/server/vbv_lernwelt/course_session/tests/test_factories.py b/server/vbv_lernwelt/course_session/tests/test_factories.py new file mode 100644 index 00000000..6bcbaa07 --- /dev/null +++ b/server/vbv_lernwelt/course_session/tests/test_factories.py @@ -0,0 +1,47 @@ +from datetime import timedelta + +from django.test import TestCase + +from vbv_lernwelt.core.create_default_users import create_default_users +from vbv_lernwelt.core.models import User +from vbv_lernwelt.course.consts import COURSE_TEST_ID +from vbv_lernwelt.course.creators.test_course import create_test_course +from vbv_lernwelt.course.models import CourseSession, CourseSessionUser +from vbv_lernwelt.course_session.models import CourseSessionAssignment +from vbv_lernwelt.learnpath.models import Circle + + +class CourseSessionModelsTestCase(TestCase): + def setUp(self) -> None: + create_default_users() + create_test_course() + + self.user = User.objects.get(username="student") + self.expert = User.objects.get( + username="patrizia.huggel@eiger-versicherungen.ch" + ) + + self.course_session = CourseSession.objects.create( + course_id=COURSE_TEST_ID, + title="Test Lehrgang Session", + ) + + csu = CourseSessionUser.objects.create( + course_session=self.course_session, + user=User.objects.get(username="patrizia.huggel@eiger-versicherungen.ch"), + role=CourseSessionUser.Role.EXPERT, + ) + csu.expert.add(Circle.objects.get(slug="test-lehrgang-lp-circle-fahrzeug")) + + def test_course_session_assignment(self): + csa = CourseSessionAssignment.objects.create( + course_session=self.course_session, + # cs learning_content=LearningContentAssignment.objects.get( + # slug=f"{course.slug}-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice" + # ), + ) + print(csa) + + submission_deadline = csa.submission_deadline + submission_deadline.end = self.course_session.start_date + timedelta(days=14) + submission_deadline.save() diff --git a/server/vbv_lernwelt/course_session/tests/test_models.py b/server/vbv_lernwelt/course_session/tests/test_models.py new file mode 100644 index 00000000..ce7e54b5 --- /dev/null +++ b/server/vbv_lernwelt/course_session/tests/test_models.py @@ -0,0 +1,39 @@ +from datetime import datetime + +from django.test import TestCase +from django.utils import timezone + +from vbv_lernwelt.core.create_default_users import create_default_users +from vbv_lernwelt.course.creators.test_course import create_test_course +from vbv_lernwelt.course_session.models import CourseSessionAssignment, CourseSessionAttendanceCourse +from vbv_lernwelt.duedate.models import DueDate + + +class CourseSessionModelsTestCase(TestCase): + def setUp(self) -> None: + create_default_users() + create_test_course(with_sessions=True) + + def test_course_session_assignment(self): + csa = CourseSessionAssignment.objects.all().first() + + submission_deadline = csa.submission_deadline + + deadline_date = datetime(2023, 7, 6, 8, 30, tzinfo=timezone.get_current_timezone()) + submission_deadline.end = deadline_date + submission_deadline.save() + + this_date = DueDate.objects.get(pk=submission_deadline.pk) + self.assertEqual(this_date.end, deadline_date) + + def test_course_session_attendance_course(self): + csac = CourseSessionAttendanceCourse.objects.all().first() + + due_date = csac.due_date + + deadline_date = datetime(2023, 7, 6, 8, 30, tzinfo=timezone.get_current_timezone()) + due_date.end = deadline_date + due_date.save() + + this_date = DueDate.objects.get(pk=due_date.pk) + self.assertEqual(this_date.end, deadline_date) diff --git a/server/vbv_lernwelt/duedate/admin.py b/server/vbv_lernwelt/duedate/admin.py index d01132ef..1f0ad4db 100644 --- a/server/vbv_lernwelt/duedate/admin.py +++ b/server/vbv_lernwelt/duedate/admin.py @@ -14,6 +14,7 @@ class DueDateAdmin(admin.ModelAdmin): date_hierarchy = "end" list_display = ["title", "course_session", "start", "end", "unset"] list_filter = ["course_session"] + readonly_fields = ["course_session", "page"] def formfield_for_foreignkey(self, db_field, request, **kwargs): if db_field.name == "page": From 1671abe512fe1fb6d289286d27e0f657af61c499 Mon Sep 17 00:00:00 2001 From: Lorenz Padberg Date: Wed, 28 Jun 2023 16:25:36 +0200 Subject: [PATCH 15/39] Refactor Duedates List to new design --- .../src/components/dueDates/DueDateSingle.vue | 26 +++++++++++----- .../src/components/dueDates/DueDatesList.vue | 30 +++++++++++++++++++ .../components/dueDates/DueDatesLongList.vue | 30 ------------------- .../components/dueDates/DueDatesShortList.vue | 22 +++----------- .../src/components/dueDates/dueDatesUtils.ts | 21 +++++++++++-- client/src/pages/DashboardPage.vue | 8 +++-- client/src/types.ts | 2 ++ 7 files changed, 78 insertions(+), 61 deletions(-) create mode 100644 client/src/components/dueDates/DueDatesList.vue delete mode 100644 client/src/components/dueDates/DueDatesLongList.vue diff --git a/client/src/components/dueDates/DueDateSingle.vue b/client/src/components/dueDates/DueDateSingle.vue index 3f2c6d59..b152a6e5 100644 --- a/client/src/components/dueDates/DueDateSingle.vue +++ b/client/src/components/dueDates/DueDateSingle.vue @@ -1,11 +1,3 @@ - - + + diff --git a/client/src/components/dueDates/DueDatesList.vue b/client/src/components/dueDates/DueDatesList.vue new file mode 100644 index 00000000..ff988e22 --- /dev/null +++ b/client/src/components/dueDates/DueDatesList.vue @@ -0,0 +1,30 @@ + + + diff --git a/client/src/components/dueDates/DueDatesLongList.vue b/client/src/components/dueDates/DueDatesLongList.vue deleted file mode 100644 index 8c375479..00000000 --- a/client/src/components/dueDates/DueDatesLongList.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - diff --git a/client/src/components/dueDates/DueDatesShortList.vue b/client/src/components/dueDates/DueDatesShortList.vue index b8c639ba..170814dc 100644 --- a/client/src/components/dueDates/DueDatesShortList.vue +++ b/client/src/components/dueDates/DueDatesShortList.vue @@ -1,28 +1,14 @@ diff --git a/client/src/components/dueDates/dueDatesUtils.ts b/client/src/components/dueDates/dueDatesUtils.ts index 7535e2fb..7f3f9adf 100644 --- a/client/src/components/dueDates/dueDatesUtils.ts +++ b/client/src/components/dueDates/dueDatesUtils.ts @@ -4,9 +4,24 @@ export const formatDate = (start: Dayjs, end: Dayjs) => { const startDateString = getDateString(start); const endDateString = getDateString(end); + // if start isundefined, dont show the day twice + + if (!start.isValid() && !end.isValid()) { + return "Termin nicht festgelegt"; + } + + if (!start || (!start.isValid() && end.isValid())) { + return `${endDateString} ${getTimeString(end)}`; + } + if (!end || (!end.isValid() && start.isValid())) { + return `${startDateString} ${getTimeString(start)}`; + } + // if start and end are on the same day, dont show the day twice if (startDateString === endDateString) { - return `${startDateString} ${getTimeString(start)} - ${getTimeString(end)}`; + return `${startDateString} ${getTimeString(start)} - ${getTimeString( + end + )} ${end.format("[Uhr]")}`; } return `${startDateString} ${getTimeString(start)} - ${endDateString} ${getTimeString( end @@ -14,11 +29,11 @@ export const formatDate = (start: Dayjs, end: Dayjs) => { }; const getTimeString = (date: Dayjs) => { - return `${date.format("HH:mm")}`; + return `${date.format("H:mm")}`; }; const getDateString = (date: Dayjs) => { - return `${date.format("DD MMM YYYY")}`; + return `${date.format("D. MMMM YYYY")}`; }; export const getWeekday = (date: Dayjs) => { diff --git a/client/src/pages/DashboardPage.vue b/client/src/pages/DashboardPage.vue index 688b343e..d0eed038 100644 --- a/client/src/pages/DashboardPage.vue +++ b/client/src/pages/DashboardPage.vue @@ -1,5 +1,5 @@ @@ -26,13 +26,14 @@ import { computed } from "vue"; const props = defineProps<{ maxCount: number; dueDates: DueDate[]; + showTopBoarder: boolean; }>(); const allDueDates = computed(() => { return props.dueDates; }); -const dueDatesDiplayed = computed(() => { +const dueDatesDisplayed = computed(() => { return props.dueDates.slice(0, props.maxCount); }); diff --git a/client/src/components/dueDates/DueDatesShortList.vue b/client/src/components/dueDates/DueDatesShortList.vue index b9aa4fbe..e47a5e68 100644 --- a/client/src/components/dueDates/DueDatesShortList.vue +++ b/client/src/components/dueDates/DueDatesShortList.vue @@ -1,6 +1,10 @@ @@ -10,6 +14,7 @@ import { useCurrentCourseSession } from "@/composables"; const props = defineProps<{ maxCount: number; + showTopBoarder: boolean; }>(); const courseSession = useCurrentCourseSession(); diff --git a/client/src/locales/de/translation.json b/client/src/locales/de/translation.json index ad9eb840..9c609eea 100644 --- a/client/src/locales/de/translation.json +++ b/client/src/locales/de/translation.json @@ -11,9 +11,9 @@ "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.", - "dueDateIntroduction": "Reiche deine Ergebnisse pünktlich ein bis am {{date}} um {{time}} Uhr ein.", + "dueDateIntroduction": "Reiche deine Ergebnisse pünktlich ein bis am: ", "dueDateNotSet": "Keine Abgabedaten wurden erfasst für diese Durchführung", - "dueDateSubmission": "Einreichungstermin: {{date}}", + "dueDateSubmission": "Einreichungstermin:", "dueDateTitle": "Abgabetermin", "edit": "Bearbeiten", "effortTitle": "Zeitaufwand", @@ -85,12 +85,17 @@ }, "dashboard": { "courses": "Lehrgang", + "dueDatesTitle": "Termine", "nocourses": "Du wurdest noch keinem Lehrgang zugewiesen.", "welcome": "Willkommen, {{name}}" }, "dueDates": { "nextDueDates": "Nächste Termine" }, + "duedates": { + "noDueDatesAvailable": "Keine Termine vorhanden", + "showAllDueDates": "Alle Termine anzeigen" + }, "feedback": { "answers": "Antworten", "areYouSatisfied": "Wie zufrieden bist du?", @@ -185,6 +190,7 @@ "learningPathPage": { "currentCircle": "Aktueller Circle", "listView": "Listenansicht", + "nextDueDates": "Nächste Termine", "nextStep": "Nächster Schritt", "pathView": "Pfadansicht", "progressText": "Du hast {{ inProgressCount }} von {{ allCount }} Circles bearbeitet", @@ -240,4 +246,4 @@ "settings": { "emailNotifications": "Email Benachrichtigungen" } -} +} \ No newline at end of file diff --git a/client/src/locales/fr/translation.json b/client/src/locales/fr/translation.json index df0983bf..f83ec0e2 100644 --- a/client/src/locales/fr/translation.json +++ b/client/src/locales/fr/translation.json @@ -11,9 +11,9 @@ "assignmentSubmitted": "Tes résultats ont bien été transmis.", "confirmSubmitPerson": "Par la présente, je confirme que la personne suivante doit évaluer mes résultats.", "confirmSubmitResults": "Par la présente, je confirme que j’ai vérifié la synthèse de mes résultats et que je souhaite la remettre telle quelle.", - "dueDateIntroduction": "Envoie tes résultats dans les délais avant le {{date}} à {{time}} heures.", + "dueDateIntroduction": "Envoie tes résultats dans les délais avant le:", "dueDateNotSet": "Aucune date de remise n’a été spécifiée pour cette opération.", - "dueDateSubmission": "Date de clôture : {{date}}", + "dueDateSubmission": "Date de clôture: ", "dueDateTitle": "Date de remise", "edit": "Traiter", "effortTitle": "Temps nécessaire", @@ -247,4 +247,4 @@ "settings": { "emailNotifications": "Notifications par e-mail" } -} +} \ No newline at end of file diff --git a/client/src/locales/it/translation.json b/client/src/locales/it/translation.json index b1dd568b..57e6193f 100644 --- a/client/src/locales/it/translation.json +++ b/client/src/locales/it/translation.json @@ -11,9 +11,9 @@ "assignmentSubmitted": "I tuoi risultati sono stati consegnati con successo.", "confirmSubmitPerson": "Confermo che i miei risultati dovranno essere valutati dalla seguente persona.", "confirmSubmitResults": "Confermo di aver controllato il riepilogo dei miei risultati e di volerli consegnare.", - "dueDateIntroduction": "Presenta i tuoi risultati entro il {{date}} alle {{time}}.", + "dueDateIntroduction": "Presenta i tuoi risultati entro il:", "dueDateNotSet": "Non sono stati registrati dati di consegna per questo svolgimento", - "dueDateSubmission": "Termine di presentazione: {{date}}", + "dueDateSubmission": "Termine di presentazione: ", "dueDateTitle": "Termine di consegna", "edit": "Modificare", "effortTitle": "Tempo richiesto", @@ -247,4 +247,4 @@ "settings": { "emailNotifications": "Notifiche e-mail" } -} +} \ No newline at end of file diff --git a/client/src/pages/DashboardPage.vue b/client/src/pages/DashboardPage.vue index 6762d084..605e7f34 100644 --- a/client/src/pages/DashboardPage.vue +++ b/client/src/pages/DashboardPage.vue @@ -76,11 +76,12 @@ const getNextStepLink = (courseSession: CourseSession) => {

{{ $t("dashboard.nocourses") }}

-

Termine

+

{{ $t("dashboard.dueDatesTitle") }}

diff --git a/client/src/pages/learningPath/learningPathPage/LearningPathPage.vue b/client/src/pages/learningPath/learningPathPage/LearningPathPage.vue index e86ee1a4..eb44ad2b 100644 --- a/client/src/pages/learningPath/learningPathPage/LearningPathPage.vue +++ b/client/src/pages/learningPath/learningPathPage/LearningPathPage.vue @@ -92,9 +92,10 @@ const changeViewType = (viewType: ViewType) => {
-
Nächste Termine
- - +
+ {{ $t("learningPathPage.nextDueDates") }} +
+
diff --git a/env_secrets/local_lorenz.env b/env_secrets/local_lorenz.env index 2c971824867b5e8a5e8b71aacf3042dd5ce27ebc..231e4a04a16d17982c63548a5ac488a211b60f75 100644 GIT binary patch literal 514 zcmV+d0{#5}M@dveQdv+`05@HtNW1Y1N*-~LZ2q0=`M{j8dA>Zzu(IP>JQ-R8SU$=R zx7ohcp56zeK{_j$eVS)T*y=!URdS}J3LV(4xA(gh(lY@R`R*d~=$-y63P`q+%x!Yy{SqEDH1JZ!^3JdmY~orUh6t!>)t`PHK2eY1Wog2IY=O%fFnm9f7( z1PXVk1>VJu)FFJLnxW1Bw=TLQn^M^8q-l~#KM2Q>R_H?I`lb*l|KQ6OE2-(ZuBOC> zUwQNr<24>|EQAJS4{BE(L&uNxl(}dKHs70+2s|t1 z+(-mY+?1Bjb5>J-Cg0_mm%NmM+i(KKlkBcHN@sN)=;>`p6nqz818z2M0rW;!`m)?E zVQZ8EV&1faLFV*Mr4gE?rLT-t=W18qM;;+O93vH`7&lNZ>&*;Mf)HOf)74nv=|i5? zrYAdCc(q)@XUbovKf!^QmLA5v(^xh6&{;QdpA&)F4m~R%{>H;S*UVNxD2FvEj;<=> Zz{f$Z$Mop;a0?3=S|7^Oh?}^{E(pYYpT+ Date: Tue, 11 Jul 2023 11:18:51 +0200 Subject: [PATCH 33/39] Resolve comments from Pullrequest --- client/src/pages/DashboardPage.vue | 6 +---- server/vbv_lernwelt/duedate/apps.py | 2 +- .../migrations/0008_auto_20230711_1116.py | 23 +++++++++++++++++++ server/vbv_lernwelt/duedate/models.py | 10 ++++---- 4 files changed, 30 insertions(+), 11 deletions(-) create mode 100644 server/vbv_lernwelt/duedate/migrations/0008_auto_20230711_1116.py diff --git a/client/src/pages/DashboardPage.vue b/client/src/pages/DashboardPage.vue index 605e7f34..9b664619 100644 --- a/client/src/pages/DashboardPage.vue +++ b/client/src/pages/DashboardPage.vue @@ -18,11 +18,7 @@ onMounted(async () => { const allDueDates = courseSessionsStore.allDueDates(); const getNextStepLink = (courseSession: CourseSession) => { - console.log("courseSession: ", courseSession); - - courseSessionsStore.allCourseSessions.forEach((courseSession) => { - console.log("duedates: ", courseSession.duedates); - }); + courseSessionsStore.allCourseSessions.forEach((courseSession) => {}); return computed(() => { if (courseSessionsStore.hasCockpit(courseSession)) { return courseSession.cockpit_url; diff --git a/server/vbv_lernwelt/duedate/apps.py b/server/vbv_lernwelt/duedate/apps.py index 9956b15f..65bde14f 100644 --- a/server/vbv_lernwelt/duedate/apps.py +++ b/server/vbv_lernwelt/duedate/apps.py @@ -1,7 +1,7 @@ from django.apps import AppConfig -class EventsConfig(AppConfig): +class DueDatesConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "vbv_lernwelt.duedate" diff --git a/server/vbv_lernwelt/duedate/migrations/0008_auto_20230711_1116.py b/server/vbv_lernwelt/duedate/migrations/0008_auto_20230711_1116.py new file mode 100644 index 00000000..b7e6d494 --- /dev/null +++ b/server/vbv_lernwelt/duedate/migrations/0008_auto_20230711_1116.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.13 on 2023-07-11 09:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('duedate', '0007_auto_20230703_1741'), + ] + + operations = [ + migrations.AlterField( + model_name='duedate', + name='start', + field=models.DateTimeField(blank=True, db_index=True, null=True), + ), + migrations.AlterField( + model_name='duedate', + name='url', + field=models.CharField(blank=True, default='', max_length=1024), + ), + ] diff --git a/server/vbv_lernwelt/duedate/models.py b/server/vbv_lernwelt/duedate/models.py index 153402d0..aeb404c6 100644 --- a/server/vbv_lernwelt/duedate/models.py +++ b/server/vbv_lernwelt/duedate/models.py @@ -10,13 +10,13 @@ from vbv_lernwelt.course.models import CourseSession class DueDate(models.Model): - start = models.DateTimeField(null=True, db_index=True) + start = models.DateTimeField(null=True, blank=True, db_index=True) end = models.DateTimeField(db_index=True, null=True) # TODO: Welcher Default Wert ist hier sinnvoll? title = models.CharField(default=_("Termin"), max_length=1024) learning_content_description = models.CharField(default="", max_length=1024) description = models.CharField(default="", max_length=1024) - url = models.URLField(default="", blank=True, max_length=1024) + url = models.CharField(default="", blank=True, max_length=1024) course_session = models.ForeignKey( "course.CourseSession", on_delete=models.CASCADE, @@ -30,12 +30,12 @@ class DueDate(models.Model): ordering = ["start", "end"] verbose_name = _("Termin") help = ( - "Set only the end date if you want to create a deadline without a duration." + "The end date is mandatory. You can set the start date if you want to have a deadline with a duration." ) def __str__(self): - start_str = self.start.strftime("%Y-%m-%d %H:%M") if self.start else "" - end_str = self.end.strftime("%Y-%m-%d %H:%M") if self.end else "" + start_str = self.start.strftime("%Y-%m-%d %H:%M") if self.start else "-" + end_str = self.end.strftime("%Y-%m-%d %H:%M") if self.end else "-" return f"DueDate: {self.title} {start_str} to {end_str}" @property From bf3582d33801ee8fdddace89cce13dd57e7da1b7 Mon Sep 17 00:00:00 2001 From: Lorenz Padberg Date: Tue, 11 Jul 2023 11:23:36 +0200 Subject: [PATCH 34/39] Fix linting --- client/src/locales/de/translation.json | 2 +- client/src/locales/fr/translation.json | 2 +- client/src/locales/it/translation.json | 2 +- client/src/pages/DashboardPage.vue | 1 - .../vbv_lernwelt/course/creators/test_course.py | 15 ++++++++++++--- .../duedate/migrations/0008_auto_20230711_1116.py | 12 ++++++------ server/vbv_lernwelt/duedate/models.py | 4 +--- 7 files changed, 22 insertions(+), 16 deletions(-) diff --git a/client/src/locales/de/translation.json b/client/src/locales/de/translation.json index 9c609eea..e1dbac5b 100644 --- a/client/src/locales/de/translation.json +++ b/client/src/locales/de/translation.json @@ -246,4 +246,4 @@ "settings": { "emailNotifications": "Email Benachrichtigungen" } -} \ No newline at end of file +} diff --git a/client/src/locales/fr/translation.json b/client/src/locales/fr/translation.json index f83ec0e2..ce39c427 100644 --- a/client/src/locales/fr/translation.json +++ b/client/src/locales/fr/translation.json @@ -247,4 +247,4 @@ "settings": { "emailNotifications": "Notifications par e-mail" } -} \ No newline at end of file +} diff --git a/client/src/locales/it/translation.json b/client/src/locales/it/translation.json index 57e6193f..8948cffc 100644 --- a/client/src/locales/it/translation.json +++ b/client/src/locales/it/translation.json @@ -247,4 +247,4 @@ "settings": { "emailNotifications": "Notifiche e-mail" } -} \ No newline at end of file +} diff --git a/client/src/pages/DashboardPage.vue b/client/src/pages/DashboardPage.vue index 9b664619..c3e806be 100644 --- a/client/src/pages/DashboardPage.vue +++ b/client/src/pages/DashboardPage.vue @@ -18,7 +18,6 @@ onMounted(async () => { const allDueDates = courseSessionsStore.allDueDates(); const getNextStepLink = (courseSession: CourseSession) => { - courseSessionsStore.allCourseSessions.forEach((courseSession) => {}); return computed(() => { if (courseSessionsStore.hasCockpit(courseSession)) { return courseSession.cockpit_url; diff --git a/server/vbv_lernwelt/course/creators/test_course.py b/server/vbv_lernwelt/course/creators/test_course.py index 931fc1e0..2366d0bd 100644 --- a/server/vbv_lernwelt/course/creators/test_course.py +++ b/server/vbv_lernwelt/course/creators/test_course.py @@ -42,7 +42,10 @@ from vbv_lernwelt.course_session.models import ( CourseSessionAttendanceCourse, ) from vbv_lernwelt.learnpath.models import ( - Circle, LearningContentAssignment, LearningContentAttendanceCourse, ) + Circle, + LearningContentAssignment, + LearningContentAttendanceCourse, +) from vbv_lernwelt.learnpath.tests.learning_path_factories import ( CircleFactory, LearningContentAssignmentFactory, @@ -128,9 +131,15 @@ def create_test_course(include_uk=True, include_vv=True, with_sessions=False): ) course = Course.objects.get(id=COURSE_TEST_ID) for cs in CourseSession.objects.filter(course_id=COURSE_TEST_ID): - for assignment in LearningContentAssignment.objects.descendant_of(course.coursepage): + for assignment in LearningContentAssignment.objects.descendant_of( + course.coursepage + ): create_course_session_assignment(cs, assignment) - for (attendance_course) in LearningContentAttendanceCourse.objects.descendant_of(course.coursepage): + for ( + attendance_course + ) in LearningContentAttendanceCourse.objects.descendant_of( + course.coursepage + ): create_course_session_attendance_course(cs, attendance_course) return course diff --git a/server/vbv_lernwelt/duedate/migrations/0008_auto_20230711_1116.py b/server/vbv_lernwelt/duedate/migrations/0008_auto_20230711_1116.py index b7e6d494..006f14b7 100644 --- a/server/vbv_lernwelt/duedate/migrations/0008_auto_20230711_1116.py +++ b/server/vbv_lernwelt/duedate/migrations/0008_auto_20230711_1116.py @@ -6,18 +6,18 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('duedate', '0007_auto_20230703_1741'), + ("duedate", "0007_auto_20230703_1741"), ] operations = [ migrations.AlterField( - model_name='duedate', - name='start', + model_name="duedate", + name="start", field=models.DateTimeField(blank=True, db_index=True, null=True), ), migrations.AlterField( - model_name='duedate', - name='url', - field=models.CharField(blank=True, default='', max_length=1024), + model_name="duedate", + name="url", + field=models.CharField(blank=True, default="", max_length=1024), ), ] diff --git a/server/vbv_lernwelt/duedate/models.py b/server/vbv_lernwelt/duedate/models.py index aeb404c6..32484c39 100644 --- a/server/vbv_lernwelt/duedate/models.py +++ b/server/vbv_lernwelt/duedate/models.py @@ -29,9 +29,7 @@ class DueDate(models.Model): def Meta(self): ordering = ["start", "end"] verbose_name = _("Termin") - help = ( - "The end date is mandatory. You can set the start date if you want to have a deadline with a duration." - ) + help = "The end date is mandatory. You can set the start date if you want to have a deadline with a duration." def __str__(self): start_str = self.start.strftime("%Y-%m-%d %H:%M") if self.start else "-" From 324725964baadb524e44b0ae3c6d6b10997b5896 Mon Sep 17 00:00:00 2001 From: Lorenz Padberg Date: Tue, 11 Jul 2023 15:57:07 +0200 Subject: [PATCH 35/39] Add deadline to course session assigments in test_course --- server/vbv_lernwelt/course/creators/test_course.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/vbv_lernwelt/course/creators/test_course.py b/server/vbv_lernwelt/course/creators/test_course.py index 2366d0bd..44a9a8d0 100644 --- a/server/vbv_lernwelt/course/creators/test_course.py +++ b/server/vbv_lernwelt/course/creators/test_course.py @@ -174,6 +174,18 @@ def create_course_session_assignment(course_session, assignment): course_session=course_session, learning_content=assignment, ) + + if course_session.start_date is None: + course_session.start_date = datetime.now() + timedelta(days=12) + course_session.save() + submission_deadline = csa.submission_deadline + if submission_deadline: + submission_deadline.end = course_session.start_date + timedelta(days=14) + submission_deadline.save() + evaluation_deadline = csa.evaluation_deadline + if evaluation_deadline: + evaluation_deadline.end = course_session.start_date + timedelta(days=28) + evaluation_deadline.save() return csa From 5890e908f2b4e28f7b4e026014e2b963fcd83329 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 11 Jul 2023 17:39:50 +0200 Subject: [PATCH 36/39] Add CourseSessionAssignment to serializer --- client/src/gql/fragment-masking.ts | 48 ++-- client/src/gql/gql.ts | 4 +- client/src/gql/graphql.ts | 214 +++++++++--------- .../AssignmentEvaluationPage.vue | 6 +- .../EvaluationContainer.vue | 4 +- .../assignmentsPage/AssignmentDetails.vue | 4 +- .../assignment/AssignmentIntroductionView.vue | 3 + .../assignment/AssignmentView.vue | 10 +- client/src/services/assignmentService.ts | 2 +- client/src/stores/courseSessions.ts | 18 +- client/src/types.ts | 20 +- server/vbv_lernwelt/course/serializers.py | 14 +- .../course_session/serializers.py | 36 ++- 13 files changed, 223 insertions(+), 160 deletions(-) diff --git a/client/src/gql/fragment-masking.ts b/client/src/gql/fragment-masking.ts index a94b5c77..c000279a 100644 --- a/client/src/gql/fragment-masking.ts +++ b/client/src/gql/fragment-masking.ts @@ -1,11 +1,13 @@ -import type { ResultOf, TypedDocumentNode as DocumentNode, } from '@graphql-typed-document-node/core'; +import type { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import type { FragmentDefinitionNode } from 'graphql'; +import type { Incremental } from './graphql'; -export type FragmentType> = TDocumentType extends DocumentNode< +export type FragmentType> = TDocumentType extends DocumentTypeDecoration< infer TType, any > - ? TType extends { ' $fragmentName'?: infer TKey } + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] ? TKey extends string ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never @@ -14,35 +16,51 @@ export type FragmentType> = TDocume // return non-nullable if `fragmentType` is non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> ): TType; // return nullable if `fragmentType` is nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if `fragmentType` is array of non-nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if `fragmentType` is array of nullable export function useFragment( - _documentNode: DocumentNode, - fragmentType: ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function useFragment( - _documentNode: DocumentNode, - fragmentType: FragmentType> | ReadonlyArray>> | null | undefined + _documentNode: DocumentTypeDecoration, + fragmentType: FragmentType> | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } export function makeFragmentData< - F extends DocumentNode, + F extends DocumentTypeDecoration, FT extends ResultOf >(data: FT, _fragment: F): FragmentType { return data as FragmentType; -} \ No newline at end of file +} +export function isFragmentReady( + queryNode: DocumentTypeDecoration, + fragmentNode: TypedDocumentNode, + data: FragmentType, any>> | null | undefined +): data is FragmentType { + const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ + ?.deferredFields; + + if (!deferredFields) return true; + + const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; + const fragName = fragDef?.name?.value; + + const fields = (fragName && deferredFields[fragName]) || []; + return fields.length > 0 && fields.every(field => data && field in data); +} diff --git a/client/src/gql/gql.ts b/client/src/gql/gql.ts index f47da5f3..c0a9d025 100644 --- a/client/src/gql/gql.ts +++ b/client/src/gql/gql.ts @@ -10,7 +10,7 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document- * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. * 3. It does not support dead code elimination, so it will add unused operations. * - * Therefore it is highly recommended to use the babel-plugin for production. + * Therefore it is highly recommended to use the babel or swc plugin for production. */ const documents = { "\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n send_feedback(input: $input) {\n feedback_response {\n id\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument, @@ -25,7 +25,7 @@ const documents = { * * @example * ```ts - * const query = gql(`query GetUser($id: ID!) { user(id: $id) { name } }`); + * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`); * ``` * * The query argument is unknown! diff --git a/client/src/gql/graphql.ts b/client/src/gql/graphql.ts index 6f9da652..158e17c7 100644 --- a/client/src/gql/graphql.ts +++ b/client/src/gql/graphql.ts @@ -5,33 +5,35 @@ export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { - ID: string; - String: string; - Boolean: boolean; - Int: number; - Float: number; + ID: { input: string; output: string; } + String: { input: string; output: string; } + Boolean: { input: boolean; output: boolean; } + Int: { input: number; output: number; } + Float: { input: number; output: number; } /** * The `DateTime` scalar type represents a DateTime * value as specified by * [iso8601](https://en.wikipedia.org/wiki/ISO_8601). */ - DateTime: any; + DateTime: { input: any; output: any; } /** * The `GenericScalar` scalar type represents a generic * GraphQL scalar value that could be: * String, Boolean, Int, Float, List or Object. */ - GenericScalar: any; - JSONStreamField: any; + GenericScalar: { input: any; output: any; } + JSONStreamField: { input: any; output: any; } /** * Allows use of a JSON String for input / output from the GraphQL schema. * * Use of this type is *not recommended* as you lose the benefits of having a defined, static * schema (one of the key benefits of GraphQL). */ - JSONString: any; + JSONString: { input: any; output: any; } }; /** An enumeration. */ @@ -61,19 +63,19 @@ export type AssignmentCompletionMutation = { export type AssignmentCompletionObjectType = { __typename?: 'AssignmentCompletionObjectType'; - additional_json_data: Scalars['JSONString']; + additional_json_data: Scalars['JSONString']['output']; assignment: AssignmentObjectType; assignment_user: UserType; - completion_data?: Maybe; + completion_data?: Maybe; completion_status: AssignmentAssignmentCompletionCompletionStatusChoices; - created_at: Scalars['DateTime']; - evaluation_grade?: Maybe; - evaluation_points?: Maybe; - evaluation_submitted_at?: Maybe; + created_at: Scalars['DateTime']['output']; + evaluation_grade?: Maybe; + evaluation_points?: Maybe; + evaluation_submitted_at?: Maybe; evaluation_user?: Maybe; - id: Scalars['ID']; - submitted_at?: Maybe; - updated_at: Scalars['DateTime']; + id: Scalars['ID']['output']; + submitted_at?: Maybe; + updated_at: Scalars['DateTime']['output']; }; /** An enumeration. */ @@ -86,24 +88,24 @@ export type AssignmentCompletionStatus = export type AssignmentObjectType = CoursePageInterface & { __typename?: 'AssignmentObjectType'; assignment_type: AssignmentAssignmentAssignmentTypeChoices; - content_type?: Maybe; + content_type?: Maybe; /** Zeitaufwand als Text */ - effort_required: Scalars['String']; + effort_required: Scalars['String']['output']; /** Beschreibung der Bewertung */ - evaluation_description: Scalars['String']; + evaluation_description: Scalars['String']['output']; /** URL zum Beurteilungsinstrument */ - evaluation_document_url: Scalars['String']; - evaluation_tasks?: Maybe; - frontend_url?: Maybe; - id?: Maybe; + evaluation_document_url: Scalars['String']['output']; + evaluation_tasks?: Maybe; + frontend_url?: Maybe; + id?: Maybe; /** Erläuterung der Ausgangslage */ - intro_text: Scalars['String']; - live?: Maybe; - performance_objectives?: Maybe; - slug?: Maybe; - tasks?: Maybe; - title?: Maybe; - translation_key?: Maybe; + intro_text: Scalars['String']['output']; + live?: Maybe; + performance_objectives?: Maybe; + slug?: Maybe; + tasks?: Maybe; + title?: Maybe; + translation_key?: Maybe; }; /** An enumeration. */ @@ -116,69 +118,69 @@ export type CoreUserLanguageChoices = | 'IT'; export type CoursePageInterface = { - content_type?: Maybe; - frontend_url?: Maybe; - id?: Maybe; - live?: Maybe; - slug?: Maybe; - title?: Maybe; - translation_key?: Maybe; + content_type?: Maybe; + frontend_url?: Maybe; + id?: Maybe; + live?: Maybe; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; }; export type CourseType = { __typename?: 'CourseType'; - category_name: Scalars['String']; - id: Scalars['ID']; + category_name: Scalars['String']['output']; + id: Scalars['ID']['output']; learning_path?: Maybe; - slug: Scalars['String']; - title: Scalars['String']; + slug: Scalars['String']['output']; + title: Scalars['String']['output']; }; export type ErrorType = { __typename?: 'ErrorType'; - field: Scalars['String']; - messages: Array; + field: Scalars['String']['output']; + messages: Array; }; export type FeedbackResponse = Node & { __typename?: 'FeedbackResponse'; - created_at: Scalars['DateTime']; - data?: Maybe; + created_at: Scalars['DateTime']['output']; + data?: Maybe; /** The ID of the object */ - id: Scalars['ID']; + id: Scalars['ID']['output']; }; export type LearningPathType = CoursePageInterface & { __typename?: 'LearningPathType'; - content_type?: Maybe; - depth: Scalars['Int']; - draft_title: Scalars['String']; - expire_at?: Maybe; - expired: Scalars['Boolean']; - first_published_at?: Maybe; - frontend_url?: Maybe; - go_live_at?: Maybe; - has_unpublished_changes: Scalars['Boolean']; - id?: Maybe; - last_published_at?: Maybe; - latest_revision_created_at?: Maybe; - live?: Maybe; - locked: Scalars['Boolean']; - locked_at?: Maybe; + content_type?: Maybe; + depth: Scalars['Int']['output']; + draft_title: Scalars['String']['output']; + expire_at?: Maybe; + expired: Scalars['Boolean']['output']; + first_published_at?: Maybe; + frontend_url?: Maybe; + go_live_at?: Maybe; + has_unpublished_changes: Scalars['Boolean']['output']; + id?: Maybe; + last_published_at?: Maybe; + latest_revision_created_at?: Maybe; + live?: Maybe; + locked: Scalars['Boolean']['output']; + locked_at?: Maybe; locked_by?: Maybe; - numchild: Scalars['Int']; + numchild: Scalars['Int']['output']; owner?: Maybe; - path: Scalars['String']; + path: Scalars['String']['output']; /** Die informative Beschreibung, dargestellt in Suchmaschinen-Ergebnissen unter der Überschrift. */ - search_description: Scalars['String']; + search_description: Scalars['String']['output']; /** Der Titel der Seite, dargestellt in Suchmaschinen-Ergebnissen als die verlinkte Überschrift. */ - seo_title: Scalars['String']; + seo_title: Scalars['String']['output']; /** Ob ein Link zu dieser Seite in automatisch generierten Menüs auftaucht. */ - show_in_menus: Scalars['Boolean']; - slug?: Maybe; - title?: Maybe; - translation_key?: Maybe; - url_path: Scalars['String']; + show_in_menus: Scalars['Boolean']['output']; + slug?: Maybe; + title?: Maybe; + translation_key?: Maybe; + url_path: Scalars['String']['output']; }; export type Mutation = { @@ -194,19 +196,19 @@ export type MutationSendFeedbackArgs = { export type MutationUpsertAssignmentCompletionArgs = { - assignment_id: Scalars['ID']; - assignment_user_id?: InputMaybe; - completion_data_string?: InputMaybe; + assignment_id: Scalars['ID']['input']; + assignment_user_id?: InputMaybe; + completion_data_string?: InputMaybe; completion_status?: InputMaybe; - course_session_id: Scalars['ID']; - evaluation_grade?: InputMaybe; - evaluation_points?: InputMaybe; + course_session_id: Scalars['ID']['input']; + evaluation_grade?: InputMaybe; + evaluation_points?: InputMaybe; }; /** An object with an ID */ export type Node = { /** The ID of the object */ - id: Scalars['ID']; + id: Scalars['ID']['output']; }; export type Query = { @@ -218,32 +220,32 @@ export type Query = { export type QueryAssignmentArgs = { - id?: InputMaybe; - slug?: InputMaybe; + id?: InputMaybe; + slug?: InputMaybe; }; export type QueryAssignmentCompletionArgs = { - assignment_id: Scalars['ID']; - assignment_user_id?: InputMaybe; - course_session_id: Scalars['ID']; + assignment_id: Scalars['ID']['input']; + assignment_user_id?: InputMaybe; + course_session_id: Scalars['ID']['input']; }; export type QueryCourseArgs = { - id?: InputMaybe; + id?: InputMaybe; }; export type SendFeedbackInput = { - clientMutationId?: InputMaybe; - course_session: Scalars['Int']; - data?: InputMaybe; - page: Scalars['String']; + clientMutationId?: InputMaybe; + course_session: Scalars['Int']['input']; + data?: InputMaybe; + page: Scalars['String']['input']; }; export type SendFeedbackPayload = { __typename?: 'SendFeedbackPayload'; - clientMutationId?: Maybe; + clientMutationId?: Maybe; /** May contain more than one error for same field. */ errors?: Maybe>>; feedback_response?: Maybe; @@ -251,14 +253,14 @@ export type SendFeedbackPayload = { export type UserType = { __typename?: 'UserType'; - avatar_url: Scalars['String']; - email: Scalars['String']; - first_name: Scalars['String']; - id: Scalars['ID']; + avatar_url: Scalars['String']['output']; + email: Scalars['String']['output']; + first_name: Scalars['String']['output']; + id: Scalars['ID']['output']; language: CoreUserLanguageChoices; - last_name: Scalars['String']; + last_name: Scalars['String']['output']; /** Erforderlich. 150 Zeichen oder weniger. Nur Buchstaben, Ziffern und @/./+/-/_. */ - username: Scalars['String']; + username: Scalars['String']['output']; }; export type SendFeedbackMutationMutationVariables = Exact<{ @@ -269,29 +271,29 @@ export type SendFeedbackMutationMutationVariables = Exact<{ export type SendFeedbackMutationMutation = { __typename?: 'Mutation', send_feedback?: { __typename?: 'SendFeedbackPayload', feedback_response?: { __typename?: 'FeedbackResponse', id: string } | null, errors?: Array<{ __typename?: 'ErrorType', field: string, messages: Array } | null> | null } | null }; export type UpsertAssignmentCompletionMutationVariables = Exact<{ - assignmentId: Scalars['ID']; - courseSessionId: Scalars['ID']; - assignmentUserId?: InputMaybe; + assignmentId: Scalars['ID']['input']; + courseSessionId: Scalars['ID']['input']; + assignmentUserId?: InputMaybe; completionStatus: AssignmentCompletionStatus; - completionDataString: Scalars['String']; - evaluationGrade?: InputMaybe; - evaluationPoints?: InputMaybe; + completionDataString: Scalars['String']['input']; + evaluationGrade?: InputMaybe; + evaluationPoints?: InputMaybe; }>; export type UpsertAssignmentCompletionMutation = { __typename?: 'Mutation', upsert_assignment_completion?: { __typename?: 'AssignmentCompletionMutation', assignment_completion?: { __typename?: 'AssignmentCompletionObjectType', id: string, completion_status: AssignmentAssignmentCompletionCompletionStatusChoices, submitted_at?: any | null, evaluation_submitted_at?: any | null, evaluation_grade?: number | null, evaluation_points?: number | null, completion_data?: any | null } | null } | null }; export type AssignmentCompletionQueryQueryVariables = Exact<{ - assignmentId: Scalars['ID']; - courseSessionId: Scalars['ID']; - assignmentUserId?: InputMaybe; + assignmentId: Scalars['ID']['input']; + courseSessionId: Scalars['ID']['input']; + assignmentUserId?: InputMaybe; }>; export type AssignmentCompletionQueryQuery = { __typename?: 'Query', assignment?: { __typename?: 'AssignmentObjectType', assignment_type: AssignmentAssignmentAssignmentTypeChoices, content_type?: string | null, effort_required: string, evaluation_description: string, evaluation_document_url: string, evaluation_tasks?: any | null, id?: string | null, intro_text: string, performance_objectives?: any | null, slug?: string | null, tasks?: any | null, title?: string | null, translation_key?: string | null } | null, assignment_completion?: { __typename?: 'AssignmentCompletionObjectType', id: string, completion_status: AssignmentAssignmentCompletionCompletionStatusChoices, submitted_at?: any | null, evaluation_submitted_at?: any | null, evaluation_grade?: number | null, evaluation_points?: number | null, completion_data?: any | null, evaluation_user?: { __typename?: 'UserType', id: string } | null, assignment_user: { __typename?: 'UserType', id: string } } | null }; export type CourseQueryQueryVariables = Exact<{ - courseId: Scalars['Int']; + courseId: Scalars['Int']['input']; }>; diff --git a/client/src/pages/cockpit/assignmentEvaluationPage/AssignmentEvaluationPage.vue b/client/src/pages/cockpit/assignmentEvaluationPage/AssignmentEvaluationPage.vue index 9b45fd5e..6872cb56 100644 --- a/client/src/pages/cockpit/assignmentEvaluationPage/AssignmentEvaluationPage.vue +++ b/client/src/pages/cockpit/assignmentEvaluationPage/AssignmentEvaluationPage.vue @@ -6,7 +6,7 @@ import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentP import type { Assignment, AssignmentCompletion, - CourseSessionAssignmentDetails, + CourseSessionAssignment, CourseSessionUser, } from "@/types"; import { useQuery } from "@urql/vue"; @@ -23,12 +23,12 @@ const props = defineProps<{ log.debug("AssignmentEvaluationPage created", props.assignmentId, props.userId); interface StateInterface { - courseSessionAssignmentDetails: CourseSessionAssignmentDetails | undefined; + courseSessionAssignment: CourseSessionAssignment | undefined; assignmentUser: CourseSessionUser | undefined; } const state: StateInterface = reactive({ - courseSessionAssignmentDetails: undefined, + courseSessionAssignment: undefined, assignmentUser: undefined, }); diff --git a/client/src/pages/cockpit/assignmentEvaluationPage/EvaluationContainer.vue b/client/src/pages/cockpit/assignmentEvaluationPage/EvaluationContainer.vue index 0adbe94f..1b4a872f 100644 --- a/client/src/pages/cockpit/assignmentEvaluationPage/EvaluationContainer.vue +++ b/client/src/pages/cockpit/assignmentEvaluationPage/EvaluationContainer.vue @@ -60,9 +60,7 @@ function editTask(task: AssignmentEvaluationTask) { const assignmentDetail = computed(() => findAssignmentDetail(props.assignment.id)); -const dueDate = computed(() => - dayjs(assignmentDetail.value?.evaluationDeadlineDateTimeUtc) -); +const dueDate = computed(() => dayjs(assignmentDetail.value?.evaluation_deadline_end)); const inEvaluationTask = computed( () => stepIndex.value >= 1 && stepIndex.value <= numTasks.value diff --git a/client/src/pages/cockpit/assignmentsPage/AssignmentDetails.vue b/client/src/pages/cockpit/assignmentsPage/AssignmentDetails.vue index 4410ae42..eaae98e0 100644 --- a/client/src/pages/cockpit/assignmentsPage/AssignmentDetails.vue +++ b/client/src/pages/cockpit/assignmentsPage/AssignmentDetails.vue @@ -56,12 +56,12 @@ const assignmentDetail = computed(() =>
Abgabetermin: - {{ dayjs(assignmentDetail.submissionDeadlineDateTimeUtc).format("DD.MM.YYYY") }} + {{ dayjs(assignmentDetail.submission_deadline_end).format("DD.MM.YYYY") }} - Freigabetermin: - {{ dayjs(assignmentDetail.evaluationDeadlineDateTimeUtc).format("DD.MM.YYYY") }} + {{ dayjs(assignmentDetail.evaluation_deadline_end).format("DD.MM.YYYY") }}
diff --git a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentIntroductionView.vue b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentIntroductionView.vue index c48ed595..0d3381a8 100644 --- a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentIntroductionView.vue +++ b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentIntroductionView.vue @@ -3,6 +3,7 @@ import DateEmbedding from "@/components/dueDates/DateEmbedding.vue"; import type { Assignment } from "@/types"; import { useRouteQuery } from "@vueuse/router"; import type { Dayjs } from "dayjs"; +import log from "loglevel"; interface Props { assignment: Assignment; @@ -13,6 +14,8 @@ const props = withDefaults(defineProps(), { dueDate: undefined, }); +log.debug("AssignmentIntroductionView created", props.assignment, props.dueDate); + // TODO: Test if submission deadline is set correctly, and evaluation_deadline is set. const step = useRouteQuery("step"); diff --git a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue index 0fec60c3..a310c78f 100644 --- a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue +++ b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue @@ -13,7 +13,7 @@ import type { Assignment, AssignmentCompletion, AssignmentTask, - CourseSessionAssignmentDetails, + CourseSessionAssignment, CourseSessionUser, LearningContentAssignment, } from "@/types"; @@ -29,11 +29,11 @@ const courseSession = useCurrentCourseSession(); const userStore = useUserStore(); interface State { - courseSessionAssignmentDetails: CourseSessionAssignmentDetails | undefined; + courseSessionAssignment: CourseSessionAssignment | undefined; } const state: State = reactive({ - courseSessionAssignmentDetails: undefined, + courseSessionAssignment: undefined, }); const props = defineProps<{ @@ -80,7 +80,7 @@ onMounted(async () => { props.learningContent ); - state.courseSessionAssignmentDetails = useCourseSessionsStore().findAssignmentDetails( + state.courseSessionAssignment = useCourseSessionsStore().findCourseSessionAssignment( props.learningContent.id ); @@ -123,7 +123,7 @@ const showPreviousButton = computed(() => stepIndex.value != 0); const showNextButton = computed(() => stepIndex.value + 1 < numPages.value); const showExitButton = computed(() => numPages.value === stepIndex.value + 1); const dueDate = computed(() => - dayjs(state.courseSessionAssignmentDetails?.submissionDeadlineDateTimeUtc) + dayjs(state.courseSessionAssignment?.submission_deadline_end) ); const currentTask = computed(() => { if (stepIndex.value > 0 && stepIndex.value <= numTasks.value) { diff --git a/client/src/services/assignmentService.ts b/client/src/services/assignmentService.ts index fa44b688..3db79596 100644 --- a/client/src/services/assignmentService.ts +++ b/client/src/services/assignmentService.ts @@ -109,7 +109,7 @@ export function findAssignmentDetail(assignmentId: number) { (lc) => lc.assignmentId === assignmentId ); - return courseSessionsStore.findAssignmentDetails(learningContent?.id); + return courseSessionsStore.findCourseSessionAssignment(learningContent?.id); } export function maxAssignmentPoints(assignment: Assignment) { diff --git a/client/src/stores/courseSessions.ts b/client/src/stores/courseSessions.ts index 95734fea..d1cb8d8f 100644 --- a/client/src/stores/courseSessions.ts +++ b/client/src/stores/courseSessions.ts @@ -3,7 +3,7 @@ import { deleteCircleDocument } from "@/services/files"; import type { CircleDocument, CourseSession, - CourseSessionAssignmentDetails, + CourseSessionAssignment, CourseSessionAttendanceCourse, CourseSessionUser, DueDate, @@ -230,20 +230,18 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => { ): CourseSessionAttendanceCourse | undefined { if (currentCourseSession.value) { return currentCourseSession.value.attendance_courses.find( - (attendanceCourse) => attendanceCourse.learning_content === contentId + (attendanceCourse) => attendanceCourse.learning_content_id === contentId ); } } - function findAssignmentDetails( + function findCourseSessionAssignment( contentId?: number - ): CourseSessionAssignmentDetails | undefined { + ): CourseSessionAssignment | undefined { if (contentId && currentCourseSession.value) { - return; - // TODO: Commented out because DueDate replaced assignment_details_list, not shure if other iformation is needed - // currentCourseSession.value.assignment_details_list.find( - // (assignmentDetails) => assignmentDetails.learningContentId === contentId - // ); + return currentCourseSession.value.assignments.find( + (a) => a.learning_content_id === contentId + ); } } @@ -261,7 +259,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => { startUpload, removeDocument, findAttendanceCourse, - findAssignmentDetails, + findCourseSessionAssignment, allDueDates, // use `useCurrentCourseSession` whenever possible diff --git a/client/src/types.ts b/client/src/types.ts index fbfaf876..bcb7763f 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -413,18 +413,24 @@ export interface CircleDocument { } export interface CourseSessionAttendanceCourse { - learning_content: number; + id: number; + course_session_id: number; + learning_content_id: number; start: string; end: string; location: string; trainer: string; - due_date: DueDate; + due_date_id: number; } -export interface CourseSessionAssignmentDetails { - learningContentId: number; - submissionDeadlineDateTimeUtc: string; - evaluationDeadlineDateTimeUtc: string; +export interface CourseSessionAssignment { + id: number; + course_session_id: number; + learning_content_id: number; + submission_deadline_id: number; + submission_deadline_end: string; + evaluation_deadline_id: number; + evaluation_deadline_end: string; } export interface CourseSession { @@ -441,7 +447,7 @@ export interface CourseSession { course_url: string; media_library_url: string; attendance_courses: CourseSessionAttendanceCourse[]; - assignment_details_list: CourseSessionAssignmentDetails[]; + assignments: CourseSessionAssignment[]; documents: CircleDocument[]; users: CourseSessionUser[]; duedates: DueDate[]; diff --git a/server/vbv_lernwelt/course/serializers.py b/server/vbv_lernwelt/course/serializers.py index 718366bd..a30eea17 100644 --- a/server/vbv_lernwelt/course/serializers.py +++ b/server/vbv_lernwelt/course/serializers.py @@ -7,8 +7,12 @@ from vbv_lernwelt.course.models import ( CourseCompletion, CourseSession, ) -from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse +from vbv_lernwelt.course_session.models import ( + CourseSessionAssignment, + CourseSessionAttendanceCourse, +) from vbv_lernwelt.course_session.serializers import ( + CourseSessionAssignmentSerializer, CourseSessionAttendanceCourseSerializer, ) from vbv_lernwelt.duedate.models import DueDate @@ -57,6 +61,7 @@ class CourseSessionSerializer(serializers.ModelSerializer): media_library_url = serializers.SerializerMethodField() documents = serializers.SerializerMethodField() attendance_courses = serializers.SerializerMethodField() + assignments = serializers.SerializerMethodField() duedates = serializers.SerializerMethodField() def get_course(self, obj): @@ -88,6 +93,11 @@ class CourseSessionSerializer(serializers.ModelSerializer): CourseSessionAttendanceCourse.objects.filter(course_session=obj), many=True ).data + def get_assignments(self, obj): + return CourseSessionAssignmentSerializer( + CourseSessionAssignment.objects.filter(course_session=obj), many=True + ).data + def get_duedates(self, obj): # TODO: Filter by user / userrole duedates = DueDate.objects.filter(course_session=obj) @@ -105,7 +115,7 @@ class CourseSessionSerializer(serializers.ModelSerializer): "end_date", "additional_json_data", "attendance_courses", - # "assignment_details_list", + "assignments", "learning_path_url", "cockpit_url", "competence_url", diff --git a/server/vbv_lernwelt/course_session/serializers.py b/server/vbv_lernwelt/course_session/serializers.py index 41dbc6fb..9d524ccd 100644 --- a/server/vbv_lernwelt/course_session/serializers.py +++ b/server/vbv_lernwelt/course_session/serializers.py @@ -1,6 +1,9 @@ from rest_framework import serializers -from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse +from vbv_lernwelt.course_session.models import ( + CourseSessionAssignment, + CourseSessionAttendanceCourse, +) class CourseSessionAttendanceCourseSerializer(serializers.ModelSerializer): @@ -11,9 +14,9 @@ class CourseSessionAttendanceCourseSerializer(serializers.ModelSerializer): model = CourseSessionAttendanceCourse fields = [ "id", - "course_session", - "learning_content", - "due_date", + "course_session_id", + "learning_content_id", + "due_date_id", "location", "trainer", "start", @@ -25,3 +28,28 @@ class CourseSessionAttendanceCourseSerializer(serializers.ModelSerializer): def get_end(self, obj): return obj.due_date.end + + +class CourseSessionAssignmentSerializer(serializers.ModelSerializer): + submission_deadline_end = serializers.SerializerMethodField() + evaluation_deadline_end = serializers.SerializerMethodField() + + class Meta: + model = CourseSessionAssignment + fields = [ + "id", + "course_session_id", + "learning_content_id", + "submission_deadline_id", + "submission_deadline_end", + "evaluation_deadline_id", + "evaluation_deadline_end", + ] + + def get_evaluation_deadline_end(self, obj): + if obj.evaluation_deadline: + return obj.evaluation_deadline.end + + def get_submission_deadline_end(self, obj): + if obj.submission_deadline: + return obj.submission_deadline.end From ba7533614de9bd52fd48086fd565f265145da6ad Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 11 Jul 2023 18:13:11 +0200 Subject: [PATCH 37/39] Format [Uhr] for single dates --- client/src/components/dueDates/DueDatesList.vue | 6 +++--- client/src/components/dueDates/DueDatesShortList.vue | 4 ++-- client/src/components/dueDates/dueDatesUtils.ts | 5 +++-- client/src/pages/DashboardPage.vue | 2 +- .../learningPath/learningPathPage/LearningPathPage.vue | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/client/src/components/dueDates/DueDatesList.vue b/client/src/components/dueDates/DueDatesList.vue index f5bf95cb..48aae0cc 100644 --- a/client/src/components/dueDates/DueDatesList.vue +++ b/client/src/components/dueDates/DueDatesList.vue @@ -1,10 +1,10 @@ @@ -14,7 +14,7 @@ import { useCurrentCourseSession } from "@/composables"; const props = defineProps<{ maxCount: number; - showTopBoarder: boolean; + showTopBorder: boolean; }>(); const courseSession = useCurrentCourseSession(); diff --git a/client/src/components/dueDates/dueDatesUtils.ts b/client/src/components/dueDates/dueDatesUtils.ts index dfabaaea..78734c49 100644 --- a/client/src/components/dueDates/dueDatesUtils.ts +++ b/client/src/components/dueDates/dueDatesUtils.ts @@ -11,10 +11,10 @@ export const formatDate = (start: Dayjs, end: Dayjs) => { } if (!start || (!start.isValid() && end.isValid())) { - return `${endDateString} ${getTimeString(end)}`; + return `${endDateString} ${getTimeString(end)} ${end.format("[Uhr]")}`; } if (!end || (!end.isValid() && start.isValid())) { - return `${startDateString} ${getTimeString(start)}`; + return `${startDateString} ${getTimeString(start)} ${start.format("[Uhr]")}`; } // if start and end are on the same day, dont show the day twice @@ -23,6 +23,7 @@ export const formatDate = (start: Dayjs, end: Dayjs) => { end )} ${end.format("[Uhr]")}`; } + return `${startDateString} ${getTimeString(start)} - ${endDateString} ${getTimeString( end )}`; diff --git a/client/src/pages/DashboardPage.vue b/client/src/pages/DashboardPage.vue index c3e806be..78893e11 100644 --- a/client/src/pages/DashboardPage.vue +++ b/client/src/pages/DashboardPage.vue @@ -76,7 +76,7 @@ const getNextStepLink = (courseSession: CourseSession) => { class="bg-white p-6" :due-dates="allDueDates" :max-count="10" - :show-top-boarder="false" + :show-top-border="false" > diff --git a/client/src/pages/learningPath/learningPathPage/LearningPathPage.vue b/client/src/pages/learningPath/learningPathPage/LearningPathPage.vue index eb44ad2b..af505914 100644 --- a/client/src/pages/learningPath/learningPathPage/LearningPathPage.vue +++ b/client/src/pages/learningPath/learningPathPage/LearningPathPage.vue @@ -95,7 +95,7 @@ const changeViewType = (viewType: ViewType) => {
{{ $t("learningPathPage.nextDueDates") }}
- + From c4d10badf6e43ca1d717e8fe1e206bfa7ced3f9a Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 12 Jul 2023 09:15:21 +0200 Subject: [PATCH 38/39] Standardize name `duedates` to `due_dates` --- .../src/components/dueDates/DueDatesList.vue | 4 +- .../components/dueDates/DueDatesShortList.vue | 2 +- client/src/locales/de/translation.json | 4 +- client/src/stores/courseSessions.ts | 4 +- client/src/types.ts | 2 +- .../core/management/commands/reset_schema.py | 1 - server/vbv_lernwelt/course/serializers.py | 11 +- server/vbv_lernwelt/duedate/factories.py | 28 --- .../0009_alter_duedate_course_session.py | 24 +++ server/vbv_lernwelt/duedate/models.py | 11 +- .../migrations/0002_auto_20230712_0905.py | 162 ++++++++++++++++++ 11 files changed, 203 insertions(+), 50 deletions(-) create mode 100644 server/vbv_lernwelt/duedate/migrations/0009_alter_duedate_course_session.py create mode 100644 server/vbv_lernwelt/notify/migrations/0002_auto_20230712_0905.py diff --git a/client/src/components/dueDates/DueDatesList.vue b/client/src/components/dueDates/DueDatesList.vue index 48aae0cc..35094f4f 100644 --- a/client/src/components/dueDates/DueDatesList.vue +++ b/client/src/components/dueDates/DueDatesList.vue @@ -10,11 +10,11 @@ -
{{ $t("duedates.noDueDatesAvailable") }}
+
{{ $t("dueDates.noDueDatesAvailable") }}
diff --git a/client/src/components/dueDates/DueDatesShortList.vue b/client/src/components/dueDates/DueDatesShortList.vue index 98b908bd..752206e6 100644 --- a/client/src/components/dueDates/DueDatesShortList.vue +++ b/client/src/components/dueDates/DueDatesShortList.vue @@ -18,5 +18,5 @@ const props = defineProps<{ }>(); const courseSession = useCurrentCourseSession(); -const allDueDates = courseSession.value.duedates; +const allDueDates = courseSession.value.due_dates; diff --git a/client/src/locales/de/translation.json b/client/src/locales/de/translation.json index e1dbac5b..e391e89d 100644 --- a/client/src/locales/de/translation.json +++ b/client/src/locales/de/translation.json @@ -90,9 +90,7 @@ "welcome": "Willkommen, {{name}}" }, "dueDates": { - "nextDueDates": "Nächste Termine" - }, - "duedates": { + "nextDueDates": "Nächste Termine", "noDueDatesAvailable": "Keine Termine vorhanden", "showAllDueDates": "Alle Termine anzeigen" }, diff --git a/client/src/stores/courseSessions.ts b/client/src/stores/courseSessions.ts index d1cb8d8f..ada625f0 100644 --- a/client/src/stores/courseSessions.ts +++ b/client/src/stores/courseSessions.ts @@ -40,7 +40,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => { reload: reload, })) as CourseSessionUser[]; cs.users = users; - cs.duedates.forEach((dueDate) => { + cs.due_dates.forEach((dueDate) => { dueDate.start = dayjs(dueDate.start); dueDate.end = dayjs(dueDate.end); }); @@ -199,7 +199,7 @@ export const useCourseSessionsStore = defineStore("courseSessions", () => { const allDueDatesReturn: DueDate[] = []; allCourseSessions.value?.forEach((cs) => { - allDueDatesReturn.push(...cs.duedates); + allDueDatesReturn.push(...cs.due_dates); }); allDueDatesReturn.sort((a, b) => dayjs(a.end).diff(dayjs(b.end))); return allDueDatesReturn; diff --git a/client/src/types.ts b/client/src/types.ts index bcb7763f..ab0f521d 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -450,7 +450,7 @@ export interface CourseSession { assignments: CourseSessionAssignment[]; documents: CircleDocument[]; users: CourseSessionUser[]; - duedates: DueDate[]; + due_dates: DueDate[]; } export type Role = "MEMBER" | "EXPERT" | "TUTOR"; diff --git a/server/vbv_lernwelt/core/management/commands/reset_schema.py b/server/vbv_lernwelt/core/management/commands/reset_schema.py index d0f8a469..4d9c153a 100644 --- a/server/vbv_lernwelt/core/management/commands/reset_schema.py +++ b/server/vbv_lernwelt/core/management/commands/reset_schema.py @@ -29,4 +29,3 @@ def command(): call_command("migrate") call_command("create_default_users") call_command("create_default_courses") - # call_command("create_default_duedates") diff --git a/server/vbv_lernwelt/course/serializers.py b/server/vbv_lernwelt/course/serializers.py index a30eea17..7ce71f0a 100644 --- a/server/vbv_lernwelt/course/serializers.py +++ b/server/vbv_lernwelt/course/serializers.py @@ -62,7 +62,7 @@ class CourseSessionSerializer(serializers.ModelSerializer): documents = serializers.SerializerMethodField() attendance_courses = serializers.SerializerMethodField() assignments = serializers.SerializerMethodField() - duedates = serializers.SerializerMethodField() + due_dates = serializers.SerializerMethodField() def get_course(self, obj): return CourseSerializer(obj.course).data @@ -98,10 +98,9 @@ class CourseSessionSerializer(serializers.ModelSerializer): CourseSessionAssignment.objects.filter(course_session=obj), many=True ).data - def get_duedates(self, obj): - # TODO: Filter by user / userrole - duedates = DueDate.objects.filter(course_session=obj) - return DueDateSerializer(duedates, many=True).data + def get_due_dates(self, obj): + due_dates = DueDate.objects.filter(course_session=obj) + return DueDateSerializer(due_dates, many=True).data class Meta: model = CourseSession @@ -122,7 +121,7 @@ class CourseSessionSerializer(serializers.ModelSerializer): "media_library_url", "course_url", "documents", - "duedates", + "due_dates", ] diff --git a/server/vbv_lernwelt/duedate/factories.py b/server/vbv_lernwelt/duedate/factories.py index 724f9fc8..af848ef0 100644 --- a/server/vbv_lernwelt/duedate/factories.py +++ b/server/vbv_lernwelt/duedate/factories.py @@ -2,11 +2,8 @@ import datetime import structlog from django.utils import timezone -from factory import SubFactory from factory.django import DjangoModelFactory -from ..course_session.factories import CourseSessionFactory - from .models import DueDate logger = structlog.get_logger(__name__) @@ -25,28 +22,3 @@ class DueDateFactory(DjangoModelFactory): title = "Prüfung Versicherungsvermittler/-in" end = get_date("Jan 01 2021") - - -def generate_events(start=timezone.now()): - for i in range(20): - DueDateFactory( - title=f"{i}", - start=start + datetime.timedelta(days=i), - end=start + datetime.timedelta(days=i, hours=1), - ) - - -def hour_rounder(t): - # Rounds to nearest hour by adding a timedelta hour if minute >= 30 - return t.replace( - second=0, microsecond=0, minute=0, hour=t.hour - ) + datetime.timedelta(hours=t.minute // 30) - - -def set_default_times_for_duedates(): - now = hour_rounder(timezone.now()) - - for i, event in enumerate(DueDate.objects.filter(end__isnull=True)): - event.start = now + datetime.timedelta(days=i) - event.end = event.start + datetime.timedelta(hours=3) - event.save() diff --git a/server/vbv_lernwelt/duedate/migrations/0009_alter_duedate_course_session.py b/server/vbv_lernwelt/duedate/migrations/0009_alter_duedate_course_session.py new file mode 100644 index 00000000..4b149840 --- /dev/null +++ b/server/vbv_lernwelt/duedate/migrations/0009_alter_duedate_course_session.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.13 on 2023-07-12 07:05 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("course", "0006_remove_coursesession_assignment_details_list"), + ("duedate", "0008_auto_20230711_1116"), + ] + + operations = [ + migrations.AlterField( + model_name="duedate", + name="course_session", + field=models.ForeignKey( + blank=True, + on_delete=django.db.models.deletion.CASCADE, + to="course.coursesession", + ), + ), + ] diff --git a/server/vbv_lernwelt/duedate/models.py b/server/vbv_lernwelt/duedate/models.py index 32484c39..c584d49d 100644 --- a/server/vbv_lernwelt/duedate/models.py +++ b/server/vbv_lernwelt/duedate/models.py @@ -20,7 +20,6 @@ class DueDate(models.Model): course_session = models.ForeignKey( "course.CourseSession", on_delete=models.CASCADE, - related_name="duedates", blank=True, ) @@ -51,12 +50,12 @@ class DueDate(models.Model): cls, user: User, course_session: CourseSession = None, limit=10 ): """ - Returns a queryset of all duedates that are relevant for the given user. Ordered nearest start date first. - If course_session is given, only duedates for that course_session are returned. + Returns a queryset of all due dates that are relevant for the given user. + If course_session is given, only due dates for that course_session are returned. The user is determined by via a course session user of a course_assignment. """ - qs = cls.get_next_duedates_qs() + qs = cls.get_next_due_dates_qs() if course_session: qs = qs.filter( @@ -66,12 +65,12 @@ class DueDate(models.Model): else: qs = qs.filter(course_session__course_assignment__user=user) - qs = qs.order_by("start")[:limit] + qs = qs.order_by("end")[:limit] return qs @classmethod - def get_next_duedates_qs(cls): + def get_next_due_dates_qs(cls): now = timezone.now() qs = cls.objects.filter(end__gte=now) return qs diff --git a/server/vbv_lernwelt/notify/migrations/0002_auto_20230712_0905.py b/server/vbv_lernwelt/notify/migrations/0002_auto_20230712_0905.py new file mode 100644 index 00000000..cbc4d336 --- /dev/null +++ b/server/vbv_lernwelt/notify/migrations/0002_auto_20230712_0905.py @@ -0,0 +1,162 @@ +# Generated by Django 3.2.13 on 2023-07-12 07:05 + +import django.db.models.deletion +import django.utils.timezone +import jsonfield.fields +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("contenttypes", "0002_remove_content_type_name"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("notify", "0001_initial"), + ] + + operations = [ + migrations.AlterModelOptions( + name="notification", + options={ + "ordering": ["-timestamp"], + "verbose_name": "Notification", + "verbose_name_plural": "Notifications", + }, + ), + migrations.AlterField( + model_name="notification", + name="action_object_content_type", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="notify_action_object", + to="contenttypes.contenttype", + verbose_name="action object content type", + ), + ), + migrations.AlterField( + model_name="notification", + name="action_object_object_id", + field=models.CharField( + blank=True, + max_length=255, + null=True, + verbose_name="action object object id", + ), + ), + migrations.AlterField( + model_name="notification", + name="actor_content_type", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="notify_actor", + to="contenttypes.contenttype", + verbose_name="actor content type", + ), + ), + migrations.AlterField( + model_name="notification", + name="actor_object_id", + field=models.CharField(max_length=255, verbose_name="actor object id"), + ), + migrations.AlterField( + model_name="notification", + name="data", + field=jsonfield.fields.JSONField( + blank=True, null=True, verbose_name="data" + ), + ), + migrations.AlterField( + model_name="notification", + name="deleted", + field=models.BooleanField( + db_index=True, default=False, verbose_name="deleted" + ), + ), + migrations.AlterField( + model_name="notification", + name="description", + field=models.TextField(blank=True, null=True, verbose_name="description"), + ), + migrations.AlterField( + model_name="notification", + name="emailed", + field=models.BooleanField( + db_index=True, default=False, verbose_name="emailed" + ), + ), + migrations.AlterField( + model_name="notification", + name="level", + field=models.CharField( + choices=[ + ("success", "success"), + ("info", "info"), + ("warning", "warning"), + ("error", "error"), + ], + default="info", + max_length=20, + verbose_name="level", + ), + ), + migrations.AlterField( + model_name="notification", + name="public", + field=models.BooleanField( + db_index=True, default=True, verbose_name="public" + ), + ), + migrations.AlterField( + model_name="notification", + name="recipient", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="notifications", + to=settings.AUTH_USER_MODEL, + verbose_name="recipient", + ), + ), + migrations.AlterField( + model_name="notification", + name="target_content_type", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="notify_target", + to="contenttypes.contenttype", + verbose_name="target content type", + ), + ), + migrations.AlterField( + model_name="notification", + name="target_object_id", + field=models.CharField( + blank=True, max_length=255, null=True, verbose_name="target object id" + ), + ), + migrations.AlterField( + model_name="notification", + name="timestamp", + field=models.DateTimeField( + db_index=True, + default=django.utils.timezone.now, + verbose_name="timestamp", + ), + ), + migrations.AlterField( + model_name="notification", + name="unread", + field=models.BooleanField( + db_index=True, default=True, verbose_name="unread" + ), + ), + migrations.AlterField( + model_name="notification", + name="verb", + field=models.CharField(max_length=255, verbose_name="verb"), + ), + ] From 282e62ef6fd46d7748310192d369b9441f5c84cd Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 12 Jul 2023 09:39:55 +0200 Subject: [PATCH 39/39] "start" not "end" should be mandatory --- .../EvaluationContainer.vue | 4 +- .../assignmentsPage/AssignmentDetails.vue | 4 +- .../assignment/AssignmentView.vue | 2 +- client/src/types.ts | 4 +- .../course/creators/test_course.py | 4 +- .../commands/create_default_courses.py | 4 +- .../course_session/serializers.py | 16 ++++---- .../course_session/tests/test_models.py | 4 +- .../migrations/0010_auto_20230712_0920.py | 38 +++++++++++++++++++ server/vbv_lernwelt/duedate/models.py | 34 ++++++++++------- 10 files changed, 81 insertions(+), 33 deletions(-) create mode 100644 server/vbv_lernwelt/duedate/migrations/0010_auto_20230712_0920.py diff --git a/client/src/pages/cockpit/assignmentEvaluationPage/EvaluationContainer.vue b/client/src/pages/cockpit/assignmentEvaluationPage/EvaluationContainer.vue index 1b4a872f..0ff0f641 100644 --- a/client/src/pages/cockpit/assignmentEvaluationPage/EvaluationContainer.vue +++ b/client/src/pages/cockpit/assignmentEvaluationPage/EvaluationContainer.vue @@ -60,7 +60,9 @@ function editTask(task: AssignmentEvaluationTask) { const assignmentDetail = computed(() => findAssignmentDetail(props.assignment.id)); -const dueDate = computed(() => dayjs(assignmentDetail.value?.evaluation_deadline_end)); +const dueDate = computed(() => + dayjs(assignmentDetail.value?.evaluation_deadline_start) +); const inEvaluationTask = computed( () => stepIndex.value >= 1 && stepIndex.value <= numTasks.value diff --git a/client/src/pages/cockpit/assignmentsPage/AssignmentDetails.vue b/client/src/pages/cockpit/assignmentsPage/AssignmentDetails.vue index eaae98e0..515ad009 100644 --- a/client/src/pages/cockpit/assignmentsPage/AssignmentDetails.vue +++ b/client/src/pages/cockpit/assignmentsPage/AssignmentDetails.vue @@ -56,12 +56,12 @@ const assignmentDetail = computed(() =>
Abgabetermin: - {{ dayjs(assignmentDetail.submission_deadline_end).format("DD.MM.YYYY") }} + {{ dayjs(assignmentDetail.submission_deadline_start).format("DD.MM.YYYY") }} - Freigabetermin: - {{ dayjs(assignmentDetail.evaluation_deadline_end).format("DD.MM.YYYY") }} + {{ dayjs(assignmentDetail.evaluation_deadline_start).format("DD.MM.YYYY") }}
diff --git a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue index a310c78f..98d3af32 100644 --- a/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue +++ b/client/src/pages/learningPath/learningContentPage/assignment/AssignmentView.vue @@ -123,7 +123,7 @@ const showPreviousButton = computed(() => stepIndex.value != 0); const showNextButton = computed(() => stepIndex.value + 1 < numPages.value); const showExitButton = computed(() => numPages.value === stepIndex.value + 1); const dueDate = computed(() => - dayjs(state.courseSessionAssignment?.submission_deadline_end) + dayjs(state.courseSessionAssignment?.submission_deadline_start) ); const currentTask = computed(() => { if (stepIndex.value > 0 && stepIndex.value <= numTasks.value) { diff --git a/client/src/types.ts b/client/src/types.ts index ab0f521d..7b523401 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -428,9 +428,9 @@ export interface CourseSessionAssignment { course_session_id: number; learning_content_id: number; submission_deadline_id: number; - submission_deadline_end: string; + submission_deadline_start: string; evaluation_deadline_id: number; - evaluation_deadline_end: string; + evaluation_deadline_start: string; } export interface CourseSession { diff --git a/server/vbv_lernwelt/course/creators/test_course.py b/server/vbv_lernwelt/course/creators/test_course.py index 44a9a8d0..1e3df840 100644 --- a/server/vbv_lernwelt/course/creators/test_course.py +++ b/server/vbv_lernwelt/course/creators/test_course.py @@ -180,11 +180,11 @@ def create_course_session_assignment(course_session, assignment): course_session.save() submission_deadline = csa.submission_deadline if submission_deadline: - submission_deadline.end = course_session.start_date + timedelta(days=14) + submission_deadline.start = course_session.start_date + timedelta(days=14) submission_deadline.save() evaluation_deadline = csa.evaluation_deadline if evaluation_deadline: - evaluation_deadline.end = course_session.start_date + timedelta(days=28) + evaluation_deadline.start = course_session.start_date + timedelta(days=28) evaluation_deadline.save() return csa diff --git a/server/vbv_lernwelt/course/management/commands/create_default_courses.py b/server/vbv_lernwelt/course/management/commands/create_default_courses.py index ec204b8d..3023653e 100644 --- a/server/vbv_lernwelt/course/management/commands/create_default_courses.py +++ b/server/vbv_lernwelt/course/management/commands/create_default_courses.py @@ -589,11 +589,11 @@ def create_course_session_assignments(course_session, assignment_slug, i=1): course_session.save() submission_deadline = csa.submission_deadline if submission_deadline: - submission_deadline.end = course_session.start_date + timedelta(days=14) + submission_deadline.start = course_session.start_date + timedelta(days=14) submission_deadline.save() evaluation_deadline = csa.evaluation_deadline if evaluation_deadline: - evaluation_deadline.end = course_session.start_date + timedelta(days=28) + evaluation_deadline.start = course_session.start_date + timedelta(days=28) evaluation_deadline.save() diff --git a/server/vbv_lernwelt/course_session/serializers.py b/server/vbv_lernwelt/course_session/serializers.py index 9d524ccd..defd7c6a 100644 --- a/server/vbv_lernwelt/course_session/serializers.py +++ b/server/vbv_lernwelt/course_session/serializers.py @@ -31,8 +31,8 @@ class CourseSessionAttendanceCourseSerializer(serializers.ModelSerializer): class CourseSessionAssignmentSerializer(serializers.ModelSerializer): - submission_deadline_end = serializers.SerializerMethodField() - evaluation_deadline_end = serializers.SerializerMethodField() + submission_deadline_start = serializers.SerializerMethodField() + evaluation_deadline_start = serializers.SerializerMethodField() class Meta: model = CourseSessionAssignment @@ -41,15 +41,15 @@ class CourseSessionAssignmentSerializer(serializers.ModelSerializer): "course_session_id", "learning_content_id", "submission_deadline_id", - "submission_deadline_end", + "submission_deadline_start", "evaluation_deadline_id", - "evaluation_deadline_end", + "evaluation_deadline_start", ] - def get_evaluation_deadline_end(self, obj): + def get_evaluation_deadline_start(self, obj): if obj.evaluation_deadline: - return obj.evaluation_deadline.end + return obj.evaluation_deadline.start - def get_submission_deadline_end(self, obj): + def get_submission_deadline_start(self, obj): if obj.submission_deadline: - return obj.submission_deadline.end + return obj.submission_deadline.start diff --git a/server/vbv_lernwelt/course_session/tests/test_models.py b/server/vbv_lernwelt/course_session/tests/test_models.py index e3590f37..8e1e40b5 100644 --- a/server/vbv_lernwelt/course_session/tests/test_models.py +++ b/server/vbv_lernwelt/course_session/tests/test_models.py @@ -22,8 +22,8 @@ class CourseSessionModelsTestCase(TestCase): deadline_date = datetime( 2023, 7, 6, 8, 30, tzinfo=timezone.get_current_timezone() ) - due_date.end = deadline_date + due_date.start = deadline_date due_date.save() this_date = DueDate.objects.get(pk=due_date.pk) - self.assertEqual(this_date.end, deadline_date) + self.assertEqual(this_date.start, deadline_date) diff --git a/server/vbv_lernwelt/duedate/migrations/0010_auto_20230712_0920.py b/server/vbv_lernwelt/duedate/migrations/0010_auto_20230712_0920.py new file mode 100644 index 00000000..a54f8713 --- /dev/null +++ b/server/vbv_lernwelt/duedate/migrations/0010_auto_20230712_0920.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.13 on 2023-07-12 07:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("duedate", "0009_alter_duedate_course_session"), + ] + + operations = [ + migrations.AlterField( + model_name="duedate", + name="description", + field=models.CharField(blank=True, default="", max_length=1024), + ), + migrations.AlterField( + model_name="duedate", + name="end", + field=models.DateTimeField(blank=True, db_index=True, null=True), + ), + migrations.AlterField( + model_name="duedate", + name="learning_content_description", + field=models.CharField(blank=True, default="", max_length=1024), + ), + migrations.AlterField( + model_name="duedate", + name="start", + field=models.DateTimeField(db_index=True, null=True), + ), + migrations.AlterField( + model_name="duedate", + name="title", + field=models.CharField(default="", max_length=1024), + ), + ] diff --git a/server/vbv_lernwelt/duedate/models.py b/server/vbv_lernwelt/duedate/models.py index c584d49d..36d8f773 100644 --- a/server/vbv_lernwelt/duedate/models.py +++ b/server/vbv_lernwelt/duedate/models.py @@ -10,12 +10,13 @@ from vbv_lernwelt.course.models import CourseSession class DueDate(models.Model): - start = models.DateTimeField(null=True, blank=True, db_index=True) - end = models.DateTimeField(db_index=True, null=True) - # TODO: Welcher Default Wert ist hier sinnvoll? - title = models.CharField(default=_("Termin"), max_length=1024) - learning_content_description = models.CharField(default="", max_length=1024) - description = models.CharField(default="", max_length=1024) + start = models.DateTimeField(null=True, db_index=True) + end = models.DateTimeField(null=True, blank=True, db_index=True) + title = models.CharField(default="", max_length=1024) + learning_content_description = models.CharField( + default="", blank=True, max_length=1024 + ) + description = models.CharField(default="", blank=True, max_length=1024) url = models.CharField(default="", blank=True, max_length=1024) course_session = models.ForeignKey( "course.CourseSession", @@ -28,20 +29,27 @@ class DueDate(models.Model): def Meta(self): ordering = ["start", "end"] verbose_name = _("Termin") - help = "The end date is mandatory. You can set the start date if you want to have a deadline with a duration." + help = "The start date is mandatory. You can set the end date if you want to have a deadline with a duration." 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 "-" - end_str = self.end.strftime("%Y-%m-%d %H:%M") if self.end else "-" - return f"DueDate: {self.title} {start_str} to {end_str}" + result = f"DueDate: {self.title} {start_str}" + + if self.end: + end_str = self.end.strftime("%Y-%m-%d %H:%M") if self.end else "-" + result += f" - {end_str}" + + return result @property def is_undefined(self): - return self.end is None + return self.start is None @property def duration(self): - if self.start is None: + if self.end is None: return datetime.timedelta(0) return self.end - self.start @@ -65,12 +73,12 @@ class DueDate(models.Model): else: qs = qs.filter(course_session__course_assignment__user=user) - qs = qs.order_by("end")[:limit] + qs = qs.order_by("start")[:limit] return qs @classmethod def get_next_due_dates_qs(cls): now = timezone.now() - qs = cls.objects.filter(end__gte=now) + qs = cls.objects.filter(start__gte=now) return qs