From 93232b9b4797cff0812c479ef26be48d12a07cb7 Mon Sep 17 00:00:00 2001 From: Cobel Date: Mon, 9 Mar 2026 16:39:46 +0900 Subject: [PATCH 1/2] tutor WIP On branch tutor Changes to be committed: modified: core/apps/account/migrations/0001_initial.py modified: core/apps/assignment/api/v1.py modified: core/apps/assignment/migrations/0001_initial.py modified: core/apps/assignment/models.py modified: core/apps/assignment/tests/factories.py modified: core/apps/assistant/migrations/0001_initial.py modified: core/apps/common/models.py modified: core/apps/competency/migrations/0001_initial.py modified: core/apps/content/migrations/0001_initial.py modified: core/apps/course/api/schema.py modified: core/apps/course/api/v1.py modified: core/apps/course/migrations/0001_initial.py modified: core/apps/course/models.py modified: core/apps/course/tests/factories.py modified: core/apps/discussion/api/schema.py modified: core/apps/discussion/api/v1.py modified: core/apps/discussion/migrations/0001_initial.py modified: core/apps/discussion/models.py modified: core/apps/discussion/tests/factories.py modified: core/apps/exam/api/v1.py modified: core/apps/exam/migrations/0001_initial.py modified: core/apps/exam/models.py modified: core/apps/exam/tests/factories.py modified: core/apps/learning/api/access_control.py modified: core/apps/learning/api/v1.py modified: core/apps/learning/management/commands/setup_demo_data.py modified: core/apps/learning/migrations/0001_initial.py modified: core/apps/operation/admin.py modified: core/apps/operation/api/schema.py modified: core/apps/operation/migrations/0001_initial.py modified: core/apps/operation/models.py modified: core/apps/partner/migrations/0001_initial.py modified: core/apps/quiz/api/v1.py modified: core/apps/quiz/migrations/0001_initial.py modified: core/apps/quiz/models.py modified: core/apps/quiz/tests/factories.py modified: core/apps/sso/migrations/0001_initial.py modified: core/apps/store/migrations/0001_initial.py modified: core/apps/studio/api/v1/assignment.py modified: core/apps/studio/api/v1/course.py modified: core/apps/studio/api/v1/discussion.py modified: core/apps/studio/api/v1/exam.py modified: core/apps/studio/api/v1/media.py modified: core/apps/studio/api/v1/quiz.py deleted: core/apps/studio/api/v1/schema.py modified: core/apps/studio/api/v1/survey.py modified: core/apps/studio/migrations/0001_initial.py modified: core/apps/studio/models.py modified: core/apps/survey/api/v1.py modified: core/apps/survey/migrations/0001_initial.py modified: core/apps/survey/models.py modified: core/apps/survey/tests/factories.py modified: core/apps/tracking/migrations/0001_initial.py new file: core/apps/tutor/__init__.py new file: core/apps/tutor/admin.py new file: core/apps/tutor/api/v1/__init__.py new file: core/apps/tutor/api/v1/exam.py new file: core/apps/tutor/apps.py new file: core/apps/tutor/decorator.py new file: core/apps/tutor/migrations/0001_initial.py new file: core/apps/tutor/migrations/__init__.py new file: core/apps/tutor/models.py new file: core/apps/tutor/tasks.py new file: core/apps/tutor/tests/test_tutor_exam_api.py modified: core/apps/warehouse/migrations/0001_initial.py modified: core/apps/warehouse/views.py modified: core/minima/settings.py modified: core/pyproject.toml modified: core/uv.lock modified: dev.sh modified: web/package-lock.json deleted: web/public/image/logo/logo-dark.png deleted: web/public/image/logo/logo-icon.png deleted: web/public/image/logo/logo-large.png deleted: web/public/image/logo/logo-square.png deleted: web/public/image/logo/logo-studio-dark.png deleted: web/public/image/logo/logo-studio-large.png deleted: web/public/image/logo/logo-studio.png modified: web/public/image/logo/logo.png modified: web/src/api/index.ts modified: web/src/api/sdk.gen.ts modified: web/src/api/types.gen.ts modified: web/src/api/valibot.gen.ts modified: web/src/routeTree.gen.ts modified: web/src/routes/(app)/-shared/AccountButton.tsx modified: web/src/routes/(app)/-shared/Inquiry.tsx modified: web/src/routes/(app)/-shared/Notification.tsx modified: web/src/routes/(app)/-shared/SearchBox.tsx modified: web/src/routes/(app)/-shared/aichat/Chat.tsx modified: web/src/routes/(app)/-shared/goal/CategorySelect.tsx modified: web/src/routes/(app)/-shared/goal/GoalForm.tsx modified: web/src/routes/(app)/-shared/grading/Appeal.tsx modified: web/src/routes/(app)/-shared/grading/SessionStart.tsx modified: web/src/routes/(app)/-shared/quiz/QuizDialog.tsx modified: web/src/routes/(app)/-shared/thread/Thread.tsx modified: web/src/routes/(app)/account/device.tsx modified: web/src/routes/(app)/account/group.tsx modified: web/src/routes/(app)/account/link.tsx modified: web/src/routes/(app)/assignment/$id.session.tsx modified: web/src/routes/(app)/assignment/-session/GradingReview.tsx modified: web/src/routes/(app)/course/$id.session.tsx modified: web/src/routes/(app)/course/-session/Achievement.tsx modified: web/src/routes/(app)/course/-session/CourseDetail.tsx modified: web/src/routes/(app)/course/-session/GettingStarted.tsx modified: web/src/routes/(app)/course/-session/Schedule.tsx modified: web/src/routes/(app)/dashboard/achievement.tsx modified: web/src/routes/(app)/dashboard/announcement.tsx modified: web/src/routes/(app)/dashboard/catalog.tsx modified: web/src/routes/(app)/dashboard/goal.tsx modified: web/src/routes/(app)/dashboard/learning.tsx modified: web/src/routes/(app)/dashboard/report.tsx modified: web/src/routes/(app)/dashboard/search.tsx modified: web/src/routes/(app)/discussion/$id.session.tsx modified: web/src/routes/(app)/discussion/-session/GradingReview.tsx modified: web/src/routes/(app)/discussion/-session/Thread.tsx modified: web/src/routes/(app)/exam/$id.session.tsx modified: web/src/routes/(app)/exam/-session/GradingReview.tsx modified: web/src/routes/(app)/exam/-session/QuestionReview.tsx modified: web/src/routes/(app)/exam/-session/TakeExam.tsx modified: web/src/routes/(app)/media/$id.tsx modified: web/src/routes/(app)/media/-media/Note.tsx modified: web/src/routes/(app)/route.tsx modified: web/src/routes/(auth)/join.tsx modified: web/src/routes/(public)/survey/$id.tsx modified: web/src/routes/(public)/survey/-SurveyForm.tsx modified: web/src/routes/-SitePolicy.tsx new file: web/src/routes/protected.tsx modified: web/src/routes/studio/-course/Course.tsx modified: web/src/routes/studio/-course/data.ts deleted: web/src/routes/studio/-studio/NavbarLogo.tsx modified: web/src/routes/studio/route.tsx new file: web/src/routes/tutor/-context.tsx new file: web/src/routes/tutor/exam/$id.grading.tsx new file: web/src/routes/tutor/exam/-exam/GradingPaper.tsx new file: web/src/routes/tutor/exam/-exam/Question.tsx new file: web/src/routes/tutor/exam/-exam/context.ts new file: web/src/routes/tutor/index.tsx new file: web/src/routes/tutor/route.tsx modified: web/src/shared/CollapseButton.tsx modified: web/src/shared/NavbarLogo.tsx --- core/apps/account/migrations/0001_initial.py | 2 +- core/apps/assignment/api/v1.py | 6 +- .../assignment/migrations/0001_initial.py | 10 +- core/apps/assignment/models.py | 5 +- core/apps/assignment/tests/factories.py | 1 + .../apps/assistant/migrations/0001_initial.py | 2 +- core/apps/common/models.py | 1 + .../competency/migrations/0001_initial.py | 2 +- core/apps/content/migrations/0001_initial.py | 2 +- core/apps/course/api/schema.py | 2 + core/apps/course/api/v1.py | 4 +- core/apps/course/migrations/0001_initial.py | 22 +- core/apps/course/models.py | 13 +- core/apps/course/tests/factories.py | 4 +- core/apps/discussion/api/schema.py | 9 +- core/apps/discussion/api/v1.py | 6 +- .../discussion/migrations/0001_initial.py | 10 +- core/apps/discussion/models.py | 11 +- core/apps/discussion/tests/factories.py | 1 + core/apps/exam/api/v1.py | 6 +- core/apps/exam/migrations/0001_initial.py | 10 +- core/apps/exam/models.py | 22 +- core/apps/exam/tests/factories.py | 1 + core/apps/learning/api/access_control.py | 36 +- core/apps/learning/api/v1.py | 1 - .../management/commands/setup_demo_data.py | 14 +- core/apps/learning/migrations/0001_initial.py | 2 +- core/apps/operation/admin.py | 5 - core/apps/operation/api/schema.py | 1 - .../apps/operation/migrations/0001_initial.py | 14 +- core/apps/operation/models.py | 8 +- core/apps/partner/migrations/0001_initial.py | 2 +- core/apps/quiz/api/v1.py | 6 +- core/apps/quiz/migrations/0001_initial.py | 10 +- core/apps/quiz/models.py | 5 +- core/apps/quiz/tests/factories.py | 1 + core/apps/sso/migrations/0001_initial.py | 2 +- core/apps/store/migrations/0001_initial.py | 2 +- core/apps/studio/api/v1/assignment.py | 4 +- core/apps/studio/api/v1/course.py | 8 +- core/apps/studio/api/v1/discussion.py | 4 +- core/apps/studio/api/v1/exam.py | 6 +- core/apps/studio/api/v1/media.py | 2 +- core/apps/studio/api/v1/quiz.py | 6 +- core/apps/studio/api/v1/schema.py | 6 - core/apps/studio/api/v1/survey.py | 2 +- core/apps/studio/migrations/0001_initial.py | 2 +- core/apps/studio/models.py | 3 +- core/apps/survey/api/v1.py | 6 +- core/apps/survey/migrations/0001_initial.py | 10 +- core/apps/survey/models.py | 6 +- core/apps/survey/tests/factories.py | 2 + core/apps/tracking/migrations/0001_initial.py | 2 +- core/apps/tutor/__init__.py | 0 core/apps/tutor/admin.py | 23 + core/apps/tutor/api/v1/__init__.py | 66 ++ core/apps/tutor/api/v1/exam.py | 195 ++++++ core/apps/tutor/apps.py | 8 + core/apps/tutor/decorator.py | 41 ++ core/apps/tutor/migrations/0001_initial.py | 78 +++ core/apps/tutor/migrations/__init__.py | 0 core/apps/tutor/models.py | 351 ++++++++++ core/apps/tutor/tasks.py | 11 + core/apps/tutor/tests/test_tutor_exam_api.py | 124 ++++ .../apps/warehouse/migrations/0001_initial.py | 2 +- core/apps/warehouse/views.py | 2 +- core/minima/settings.py | 1 + core/pyproject.toml | 1 + core/uv.lock | 57 +- dev.sh | 6 +- web/package-lock.json | 51 +- web/public/image/logo/logo-dark.png | Bin 13123 -> 0 bytes web/public/image/logo/logo-icon.png | Bin 64699 -> 0 bytes web/public/image/logo/logo-large.png | Bin 100086 -> 0 bytes web/public/image/logo/logo-square.png | Bin 6444 -> 0 bytes web/public/image/logo/logo-studio-dark.png | Bin 9543 -> 0 bytes web/public/image/logo/logo-studio-large.png | Bin 36680 -> 0 bytes web/public/image/logo/logo-studio.png | Bin 9475 -> 0 bytes web/public/image/logo/logo.png | Bin 12822 -> 5951 bytes web/src/api/index.ts | 4 +- web/src/api/sdk.gen.ts | 84 ++- web/src/api/types.gen.ts | 616 +++++++++++++++++- web/src/api/valibot.gen.ts | 308 ++++++++- web/src/routeTree.gen.ts | 71 ++ .../routes/(app)/-shared/AccountButton.tsx | 8 +- web/src/routes/(app)/-shared/Inquiry.tsx | 5 +- web/src/routes/(app)/-shared/Notification.tsx | 7 +- web/src/routes/(app)/-shared/SearchBox.tsx | 5 +- web/src/routes/(app)/-shared/aichat/Chat.tsx | 10 +- .../(app)/-shared/goal/CategorySelect.tsx | 5 +- .../routes/(app)/-shared/goal/GoalForm.tsx | 5 +- .../routes/(app)/-shared/grading/Appeal.tsx | 6 +- .../(app)/-shared/grading/SessionStart.tsx | 4 +- .../routes/(app)/-shared/quiz/QuizDialog.tsx | 5 +- .../routes/(app)/-shared/thread/Thread.tsx | 10 +- web/src/routes/(app)/account/device.tsx | 5 +- web/src/routes/(app)/account/group.tsx | 5 +- web/src/routes/(app)/account/link.tsx | 5 +- .../routes/(app)/assignment/$id.session.tsx | 5 +- .../assignment/-session/GradingReview.tsx | 2 +- web/src/routes/(app)/course/$id.session.tsx | 5 +- .../(app)/course/-session/Achievement.tsx | 5 +- .../(app)/course/-session/CourseDetail.tsx | 5 +- .../(app)/course/-session/GettingStarted.tsx | 8 + .../routes/(app)/course/-session/Schedule.tsx | 24 + .../routes/(app)/dashboard/achievement.tsx | 5 +- .../routes/(app)/dashboard/announcement.tsx | 5 +- web/src/routes/(app)/dashboard/catalog.tsx | 10 +- web/src/routes/(app)/dashboard/goal.tsx | 9 +- web/src/routes/(app)/dashboard/learning.tsx | 5 +- web/src/routes/(app)/dashboard/report.tsx | 10 +- web/src/routes/(app)/dashboard/search.tsx | 5 +- .../routes/(app)/discussion/$id.session.tsx | 5 +- .../discussion/-session/GradingReview.tsx | 13 +- .../(app)/discussion/-session/Thread.tsx | 5 +- web/src/routes/(app)/exam/$id.session.tsx | 5 +- .../(app)/exam/-session/GradingReview.tsx | 6 +- .../(app)/exam/-session/QuestionReview.tsx | 72 +- .../routes/(app)/exam/-session/TakeExam.tsx | 2 + web/src/routes/(app)/media/$id.tsx | 5 +- web/src/routes/(app)/media/-media/Note.tsx | 5 +- web/src/routes/(app)/route.tsx | 31 +- web/src/routes/(auth)/join.tsx | 5 +- web/src/routes/(public)/survey/$id.tsx | 3 +- .../routes/(public)/survey/-SurveyForm.tsx | 3 +- web/src/routes/-SitePolicy.tsx | 8 +- web/src/routes/protected.tsx | 14 + web/src/routes/studio/-course/Course.tsx | 13 +- web/src/routes/studio/-course/data.ts | 6 + web/src/routes/studio/-studio/NavbarLogo.tsx | 12 - web/src/routes/studio/route.tsx | 22 +- web/src/routes/tutor/-context.tsx | 15 + web/src/routes/tutor/exam/$id.grading.tsx | 139 ++++ .../routes/tutor/exam/-exam/GradingPaper.tsx | 158 +++++ web/src/routes/tutor/exam/-exam/Question.tsx | 108 +++ web/src/routes/tutor/exam/-exam/context.ts | 15 + web/src/routes/tutor/index.tsx | 170 +++++ web/src/routes/tutor/route.tsx | 45 ++ web/src/shared/CollapseButton.tsx | 4 +- web/src/shared/NavbarLogo.tsx | 6 +- 140 files changed, 3075 insertions(+), 438 deletions(-) delete mode 100644 core/apps/studio/api/v1/schema.py create mode 100644 core/apps/tutor/__init__.py create mode 100644 core/apps/tutor/admin.py create mode 100644 core/apps/tutor/api/v1/__init__.py create mode 100644 core/apps/tutor/api/v1/exam.py create mode 100644 core/apps/tutor/apps.py create mode 100644 core/apps/tutor/decorator.py create mode 100644 core/apps/tutor/migrations/0001_initial.py create mode 100644 core/apps/tutor/migrations/__init__.py create mode 100644 core/apps/tutor/models.py create mode 100644 core/apps/tutor/tasks.py create mode 100644 core/apps/tutor/tests/test_tutor_exam_api.py delete mode 100644 web/public/image/logo/logo-dark.png delete mode 100644 web/public/image/logo/logo-icon.png delete mode 100644 web/public/image/logo/logo-large.png delete mode 100644 web/public/image/logo/logo-square.png delete mode 100644 web/public/image/logo/logo-studio-dark.png delete mode 100644 web/public/image/logo/logo-studio-large.png delete mode 100644 web/public/image/logo/logo-studio.png create mode 100644 web/src/routes/protected.tsx delete mode 100644 web/src/routes/studio/-studio/NavbarLogo.tsx create mode 100644 web/src/routes/tutor/-context.tsx create mode 100644 web/src/routes/tutor/exam/$id.grading.tsx create mode 100644 web/src/routes/tutor/exam/-exam/GradingPaper.tsx create mode 100644 web/src/routes/tutor/exam/-exam/Question.tsx create mode 100644 web/src/routes/tutor/exam/-exam/context.ts create mode 100644 web/src/routes/tutor/index.tsx create mode 100644 web/src/routes/tutor/route.tsx diff --git a/core/apps/account/migrations/0001_initial.py b/core/apps/account/migrations/0001_initial.py index 868430e..d45301e 100644 --- a/core/apps/account/migrations/0001_initial.py +++ b/core/apps/account/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-03-07 04:59 +# Generated by Django 6.0.3 on 2026-03-08 06:48 import apps.common.storage import apps.common.util diff --git a/core/apps/assignment/api/v1.py b/core/apps/assignment/api/v1.py index 04c9b87..ba6e32e 100644 --- a/core/apps/assignment/api/v1.py +++ b/core/apps/assignment/api/v1.py @@ -34,7 +34,11 @@ async def get_session(request: HttpRequest, id: str): @access_date("assignment", "assignment") async def start_attempt(request: HttpRequest, id: str): return await Attempt.start( - assignment_id=id, learner_id=request.auth, context=request.active_context, mode=request.access_mode + assignment_id=id, + learner_id=request.auth, + lock=request.access_date["end"], + context=request.active_context, + mode=request.access_mode, ) diff --git a/core/apps/assignment/migrations/0001_initial.py b/core/apps/assignment/migrations/0001_initial.py index ef04c2c..4f54554 100644 --- a/core/apps/assignment/migrations/0001_initial.py +++ b/core/apps/assignment/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-03-07 04:59 +# Generated by Django 6.0.3 on 2026-03-08 06:49 import apps.common.util import django.contrib.postgres.fields @@ -152,6 +152,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('started', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Start Time')), + ('lock', models.DateTimeField(verbose_name='Lock')), ('active', models.BooleanField(default=True, verbose_name='Active')), ('context', models.CharField(blank=True, default='', max_length=255, verbose_name='Context Key')), ('mode', models.CharField(choices=[('', ''), ('preview', 'Preview'), ('audit', 'Audit')], db_index=True, default='', max_length=30, verbose_name='Mode')), @@ -297,6 +298,7 @@ class Migration(migrations.Migration): ('pgh_label', models.TextField(help_text='The event label.')), ('id', models.BigIntegerField()), ('started', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Start Time')), + ('lock', models.DateTimeField(verbose_name='Lock')), ('active', models.BooleanField(default=True, verbose_name='Active')), ('context', models.CharField(blank=True, default='', max_length=255, verbose_name='Context Key')), ('mode', models.CharField(choices=[('', ''), ('preview', 'Preview'), ('audit', 'Audit')], default='', max_length=30, verbose_name='Mode')), @@ -539,15 +541,15 @@ class Migration(migrations.Migration): ), pgtrigger.migrations.AddTrigger( model_name='attempt', - trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "assignment_attemptevent" ("active", "assignment_id", "context", "id", "learner_id", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "question_id", "retry", "started") VALUES (NEW."active", NEW."assignment_id", NEW."context", NEW."id", NEW."learner_id", NEW."mode", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."question_id", NEW."retry", NEW."started"); RETURN NULL;', hash='f2adaad4af56a38d9b01bb6e2196d41bb08a6568', operation='INSERT', pgid='pgtrigger_insert_insert_0e484', table='assignment_attempt', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "assignment_attemptevent" ("active", "assignment_id", "context", "id", "learner_id", "lock", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "question_id", "retry", "started") VALUES (NEW."active", NEW."assignment_id", NEW."context", NEW."id", NEW."learner_id", NEW."lock", NEW."mode", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."question_id", NEW."retry", NEW."started"); RETURN NULL;', hash='f34057d0b9dd986803a99bfc1e8efe71b535560c', operation='INSERT', pgid='pgtrigger_insert_insert_0e484', table='assignment_attempt', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='attempt', - trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "assignment_attemptevent" ("active", "assignment_id", "context", "id", "learner_id", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "question_id", "retry", "started") VALUES (NEW."active", NEW."assignment_id", NEW."context", NEW."id", NEW."learner_id", NEW."mode", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."question_id", NEW."retry", NEW."started"); RETURN NULL;', hash='ef8bee4e4f31cdcf4bd1af6f7eab0f270a234241', operation='UPDATE', pgid='pgtrigger_update_update_a72bb', table='assignment_attempt', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "assignment_attemptevent" ("active", "assignment_id", "context", "id", "learner_id", "lock", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "question_id", "retry", "started") VALUES (NEW."active", NEW."assignment_id", NEW."context", NEW."id", NEW."learner_id", NEW."lock", NEW."mode", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."question_id", NEW."retry", NEW."started"); RETURN NULL;', hash='2988f1e3ae4f1ab0055c62b5b28a25636202ea71', operation='UPDATE', pgid='pgtrigger_update_update_a72bb', table='assignment_attempt', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='attempt', - trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "assignment_attemptevent" ("active", "assignment_id", "context", "id", "learner_id", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "question_id", "retry", "started") VALUES (OLD."active", OLD."assignment_id", OLD."context", OLD."id", OLD."learner_id", OLD."mode", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."question_id", OLD."retry", OLD."started"); RETURN NULL;', hash='f18e7d26c3f3e955ec528dee5aa66ecafeed9146', operation='DELETE', pgid='pgtrigger_delete_delete_49776', table='assignment_attempt', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "assignment_attemptevent" ("active", "assignment_id", "context", "id", "learner_id", "lock", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "question_id", "retry", "started") VALUES (OLD."active", OLD."assignment_id", OLD."context", OLD."id", OLD."learner_id", OLD."lock", OLD."mode", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."question_id", OLD."retry", OLD."started"); RETURN NULL;', hash='2344f65f79763ea1f77ecf15b43170d326ff0c53', operation='DELETE', pgid='pgtrigger_delete_delete_49776', table='assignment_attempt', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='attempt', diff --git a/core/apps/assignment/models.py b/core/apps/assignment/models.py index 779a862..5ec5a0a 100644 --- a/core/apps/assignment/models.py +++ b/core/apps/assignment/models.py @@ -291,7 +291,7 @@ class Meta: submission: "Submission" @classmethod - async def start(cls, *, assignment_id: str, learner_id: str, context: str, mode: ModeChoices): + async def start(cls, *, assignment_id: str, learner_id: str, lock: datetime, context: str, mode: ModeChoices): assignment = await Assignment.objects.aget(id=assignment_id) if assignment.verification_required: @@ -313,6 +313,7 @@ async def start(cls, *, assignment_id: str, learner_id: str, context: str, mode: attempt = await Attempt.objects.acreate( assignment=assignment, learner_id=learner_id, + lock=lock, context=context, active=True, started=timezone.now() + timedelta(seconds=1), @@ -448,6 +449,8 @@ async def grade(self, grader_id: str | None = None): self.earned_point = sum(filter(None, self.earned_details.values())) self.score = (self.earned_point * 100.0 / self.possible_point) if self.possible_point else 0 self.passed = self.score >= (self.attempt.assignment.passing_point or 0) + if not self.completed: + self.completed = timezone.now() self.grader_id = grader_id await self.asave() diff --git a/core/apps/assignment/tests/factories.py b/core/apps/assignment/tests/factories.py index 17e8f6f..0efd852 100644 --- a/core/apps/assignment/tests/factories.py +++ b/core/apps/assignment/tests/factories.py @@ -155,6 +155,7 @@ class AttemptFactory(DjangoModelFactory[Attempt]): learner = SubFactory(UserFactory) started = LazyFunction(lambda: timezone.now()) question = LazyAttribute(lambda o: async_to_sync(o.assignment.question_pool.select_question)()) + lock = LazyFunction(timezone.now) active = True class Meta: diff --git a/core/apps/assistant/migrations/0001_initial.py b/core/apps/assistant/migrations/0001_initial.py index f7c6aad..8ebd22f 100644 --- a/core/apps/assistant/migrations/0001_initial.py +++ b/core/apps/assistant/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-03-07 04:59 +# Generated by Django 6.0.3 on 2026-03-08 06:49 import django.db.models.deletion import pgtrigger.compiler diff --git a/core/apps/common/models.py b/core/apps/common/models.py index 3d93109..89df5fa 100644 --- a/core/apps/common/models.py +++ b/core/apps/common/models.py @@ -150,6 +150,7 @@ def duration_seconds(self, value: int | None): class AttemptMixin(Model): started = DateTimeField(_("Start Time"), default=timezone.now) + lock = DateTimeField(_("Lock")) active = BooleanField(_("Active"), default=True) context = CharField(_("Context Key"), max_length=255, blank=True, default="") mode = CharField(_("Mode"), max_length=30, choices=ModeChoices.choices, default=ModeChoices.NORMAL, db_index=True) diff --git a/core/apps/competency/migrations/0001_initial.py b/core/apps/competency/migrations/0001_initial.py index ab34855..0f5f0a7 100644 --- a/core/apps/competency/migrations/0001_initial.py +++ b/core/apps/competency/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-03-07 04:59 +# Generated by Django 6.0.3 on 2026-03-08 06:49 import django.contrib.postgres.fields import django.db.models.deletion diff --git a/core/apps/content/migrations/0001_initial.py b/core/apps/content/migrations/0001_initial.py index 940bebc..437f42c 100644 --- a/core/apps/content/migrations/0001_initial.py +++ b/core/apps/content/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-03-07 04:59 +# Generated by Django 6.0.3 on 2026-03-08 06:49 import apps.common.util import apps.content.models diff --git a/core/apps/course/api/schema.py b/core/apps/course/api/schema.py index 9d96efb..8cad215 100644 --- a/core/apps/course/api/schema.py +++ b/core/apps/course/api/schema.py @@ -7,6 +7,7 @@ from apps.common.schema import ( AccessDateSchema, AttemptMixinSchema, + GradingDateSchema, LearningObjectMixinSchema, Schema, TimeStampedMixinSchema, @@ -133,6 +134,7 @@ class GradingCriterionSchema(Schema): class CourseSessionSchema(Schema): access_date: AccessDateSchema + grading_date: GradingDateSchema course: CourseSchema engagement: Annotated[CourseEngagementSchema, Field(None)] otp_token: Annotated[str, Field(None)] diff --git a/core/apps/course/api/v1.py b/core/apps/course/api/v1.py index fb42fb9..b7dde72 100644 --- a/core/apps/course/api/v1.py +++ b/core/apps/course/api/v1.py @@ -25,7 +25,9 @@ async def get_session(request: HttpRequest, id: str): @access_mode() @access_date("course", "course") async def start_engagement(request: HttpRequest, id: str): - return await Engagement.start(course_id=id, learner_id=request.auth, mode=request.access_mode) + return await Engagement.start( + course_id=id, learner_id=request.auth, lock=request.access_date["end"], mode=request.access_mode + ) @router.get("/{id}/detail", response=CourseDetailSchema) diff --git a/core/apps/course/migrations/0001_initial.py b/core/apps/course/migrations/0001_initial.py index 69dfc2d..bcca1ed 100644 --- a/core/apps/course/migrations/0001_initial.py +++ b/core/apps/course/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-03-07 04:59 +# Generated by Django 6.0.3 on 2026-03-08 06:49 import apps.common.util import django.contrib.postgres.fields @@ -87,6 +87,9 @@ class Migration(migrations.Migration): ('max_attempts', models.PositiveSmallIntegerField(default=0, verbose_name='Max Attempts')), ('verification_required', models.BooleanField(default=False, verbose_name='Verification Required')), ('published', models.DateTimeField(blank=True, null=True, verbose_name='Published')), + ('grade_due_days', models.PositiveSmallIntegerField(verbose_name='Grading Due Days')), + ('appeal_deadline_days', models.PositiveSmallIntegerField(verbose_name='Appeal Deadline Days')), + ('confirm_due_days', models.PositiveSmallIntegerField(verbose_name='Confirm Due Days')), ('objective', models.TextField(blank=True, default='', verbose_name='Objective')), ('preview_url', models.URLField(blank=True, null=True, verbose_name='Preview URL')), ('effort_hours', models.PositiveSmallIntegerField(verbose_name='Effort Hours')), @@ -304,6 +307,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('started', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Start Time')), + ('lock', models.DateTimeField(verbose_name='Lock')), ('active', models.BooleanField(default=True, verbose_name='Active')), ('context', models.CharField(blank=True, default='', max_length=255, verbose_name='Context Key')), ('mode', models.CharField(choices=[('', ''), ('preview', 'Preview'), ('audit', 'Audit')], db_index=True, default='', max_length=30, verbose_name='Mode')), @@ -324,6 +328,7 @@ class Migration(migrations.Migration): ('pgh_label', models.TextField(help_text='The event label.')), ('id', models.BigIntegerField()), ('started', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Start Time')), + ('lock', models.DateTimeField(verbose_name='Lock')), ('active', models.BooleanField(default=True, verbose_name='Active')), ('context', models.CharField(blank=True, default='', max_length=255, verbose_name='Context Key')), ('mode', models.CharField(choices=[('', ''), ('preview', 'Preview'), ('audit', 'Audit')], default='', max_length=30, verbose_name='Mode')), @@ -498,6 +503,9 @@ class Migration(migrations.Migration): ('max_attempts', models.PositiveSmallIntegerField(default=0, verbose_name='Max Attempts')), ('verification_required', models.BooleanField(default=False, verbose_name='Verification Required')), ('published', models.DateTimeField(blank=True, null=True, verbose_name='Published')), + ('grade_due_days', models.PositiveSmallIntegerField(verbose_name='Grading Due Days')), + ('appeal_deadline_days', models.PositiveSmallIntegerField(verbose_name='Appeal Deadline Days')), + ('confirm_due_days', models.PositiveSmallIntegerField(verbose_name='Confirm Due Days')), ('objective', models.TextField(blank=True, default='', verbose_name='Objective')), ('preview_url', models.URLField(blank=True, null=True, verbose_name='Preview URL')), ('effort_hours', models.PositiveSmallIntegerField(verbose_name='Effort Hours')), @@ -669,15 +677,15 @@ class Migration(migrations.Migration): ), pgtrigger.migrations.AddTrigger( model_name='engagement', - trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "course_engagementevent" ("active", "context", "course_id", "id", "learner_id", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "started") VALUES (NEW."active", NEW."context", NEW."course_id", NEW."id", NEW."learner_id", NEW."mode", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."started"); RETURN NULL;', hash='355ab145a728354afebc187bffcf5ea2b51ccc24', operation='INSERT', pgid='pgtrigger_insert_insert_dc2ef', table='course_engagement', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "course_engagementevent" ("active", "context", "course_id", "id", "learner_id", "lock", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "started") VALUES (NEW."active", NEW."context", NEW."course_id", NEW."id", NEW."learner_id", NEW."lock", NEW."mode", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."started"); RETURN NULL;', hash='329a9d1b0a38124e82f5ca9fc98906983e35b404', operation='INSERT', pgid='pgtrigger_insert_insert_dc2ef', table='course_engagement', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='engagement', - trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "course_engagementevent" ("active", "context", "course_id", "id", "learner_id", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "started") VALUES (NEW."active", NEW."context", NEW."course_id", NEW."id", NEW."learner_id", NEW."mode", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."started"); RETURN NULL;', hash='646532a419d9328d25f82a9d3581963d46628ccb', operation='UPDATE', pgid='pgtrigger_update_update_bc152', table='course_engagement', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "course_engagementevent" ("active", "context", "course_id", "id", "learner_id", "lock", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "started") VALUES (NEW."active", NEW."context", NEW."course_id", NEW."id", NEW."learner_id", NEW."lock", NEW."mode", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."started"); RETURN NULL;', hash='3e014a2797a672a6ee7d1cb817940323458eb01a', operation='UPDATE', pgid='pgtrigger_update_update_bc152', table='course_engagement', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='engagement', - trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "course_engagementevent" ("active", "context", "course_id", "id", "learner_id", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "started") VALUES (OLD."active", OLD."context", OLD."course_id", OLD."id", OLD."learner_id", OLD."mode", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."started"); RETURN NULL;', hash='52257ed31f728c7792d36f7f9534d16be2aa05de', operation='DELETE', pgid='pgtrigger_delete_delete_39407', table='course_engagement', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "course_engagementevent" ("active", "context", "course_id", "id", "learner_id", "lock", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "started") VALUES (OLD."active", OLD."context", OLD."course_id", OLD."id", OLD."learner_id", OLD."lock", OLD."mode", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."started"); RETURN NULL;', hash='1e50f89a531cfa8133f19012c85db2463a10d691', operation='DELETE', pgid='pgtrigger_delete_delete_39407', table='course_engagement', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='engagementevent', @@ -765,15 +773,15 @@ class Migration(migrations.Migration): ), pgtrigger.migrations.AddTrigger( model_name='course', - trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "course_courseevent" ("audience", "created", "description", "duration", "effort_hours", "faq_id", "featured", "format", "honor_code_id", "id", "level", "max_attempts", "message_preset_id", "modified", "objective", "owner_id", "passing_point", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "preview_url", "published", "thumbnail", "title", "verification_required") VALUES (NEW."audience", NEW."created", NEW."description", NEW."duration", NEW."effort_hours", NEW."faq_id", NEW."featured", NEW."format", NEW."honor_code_id", NEW."id", NEW."level", NEW."max_attempts", NEW."message_preset_id", NEW."modified", NEW."objective", NEW."owner_id", NEW."passing_point", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."preview_url", NEW."published", NEW."thumbnail", NEW."title", NEW."verification_required"); RETURN NULL;', hash='aabd0bb049bc09bd40f2f86de5d072499bf68196', operation='INSERT', pgid='pgtrigger_insert_insert_b0bd1', table='course_course', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "course_courseevent" ("appeal_deadline_days", "audience", "confirm_due_days", "created", "description", "duration", "effort_hours", "faq_id", "featured", "format", "grade_due_days", "honor_code_id", "id", "level", "max_attempts", "message_preset_id", "modified", "objective", "owner_id", "passing_point", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "preview_url", "published", "thumbnail", "title", "verification_required") VALUES (NEW."appeal_deadline_days", NEW."audience", NEW."confirm_due_days", NEW."created", NEW."description", NEW."duration", NEW."effort_hours", NEW."faq_id", NEW."featured", NEW."format", NEW."grade_due_days", NEW."honor_code_id", NEW."id", NEW."level", NEW."max_attempts", NEW."message_preset_id", NEW."modified", NEW."objective", NEW."owner_id", NEW."passing_point", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."preview_url", NEW."published", NEW."thumbnail", NEW."title", NEW."verification_required"); RETURN NULL;', hash='b3939aedb9fa4063d8597071a191c0a448ce77ff', operation='INSERT', pgid='pgtrigger_insert_insert_b0bd1', table='course_course', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='course', - trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD."audience" IS DISTINCT FROM (NEW."audience") OR OLD."description" IS DISTINCT FROM (NEW."description") OR OLD."duration" IS DISTINCT FROM (NEW."duration") OR OLD."effort_hours" IS DISTINCT FROM (NEW."effort_hours") OR OLD."faq_id" IS DISTINCT FROM (NEW."faq_id") OR OLD."featured" IS DISTINCT FROM (NEW."featured") OR OLD."format" IS DISTINCT FROM (NEW."format") OR OLD."honor_code_id" IS DISTINCT FROM (NEW."honor_code_id") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."level" IS DISTINCT FROM (NEW."level") OR OLD."max_attempts" IS DISTINCT FROM (NEW."max_attempts") OR OLD."message_preset_id" IS DISTINCT FROM (NEW."message_preset_id") OR OLD."objective" IS DISTINCT FROM (NEW."objective") OR OLD."owner_id" IS DISTINCT FROM (NEW."owner_id") OR OLD."passing_point" IS DISTINCT FROM (NEW."passing_point") OR OLD."preview_url" IS DISTINCT FROM (NEW."preview_url") OR OLD."published" IS DISTINCT FROM (NEW."published") OR OLD."thumbnail" IS DISTINCT FROM (NEW."thumbnail") OR OLD."title" IS DISTINCT FROM (NEW."title") OR OLD."verification_required" IS DISTINCT FROM (NEW."verification_required"))', func='INSERT INTO "course_courseevent" ("audience", "created", "description", "duration", "effort_hours", "faq_id", "featured", "format", "honor_code_id", "id", "level", "max_attempts", "message_preset_id", "modified", "objective", "owner_id", "passing_point", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "preview_url", "published", "thumbnail", "title", "verification_required") VALUES (NEW."audience", NEW."created", NEW."description", NEW."duration", NEW."effort_hours", NEW."faq_id", NEW."featured", NEW."format", NEW."honor_code_id", NEW."id", NEW."level", NEW."max_attempts", NEW."message_preset_id", NEW."modified", NEW."objective", NEW."owner_id", NEW."passing_point", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."preview_url", NEW."published", NEW."thumbnail", NEW."title", NEW."verification_required"); RETURN NULL;', hash='f47ae042c6e7e7a10354192d997e81b5934e029b', operation='UPDATE', pgid='pgtrigger_update_update_8ff8f', table='course_course', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD."appeal_deadline_days" IS DISTINCT FROM (NEW."appeal_deadline_days") OR OLD."audience" IS DISTINCT FROM (NEW."audience") OR OLD."confirm_due_days" IS DISTINCT FROM (NEW."confirm_due_days") OR OLD."description" IS DISTINCT FROM (NEW."description") OR OLD."duration" IS DISTINCT FROM (NEW."duration") OR OLD."effort_hours" IS DISTINCT FROM (NEW."effort_hours") OR OLD."faq_id" IS DISTINCT FROM (NEW."faq_id") OR OLD."featured" IS DISTINCT FROM (NEW."featured") OR OLD."format" IS DISTINCT FROM (NEW."format") OR OLD."grade_due_days" IS DISTINCT FROM (NEW."grade_due_days") OR OLD."honor_code_id" IS DISTINCT FROM (NEW."honor_code_id") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."level" IS DISTINCT FROM (NEW."level") OR OLD."max_attempts" IS DISTINCT FROM (NEW."max_attempts") OR OLD."message_preset_id" IS DISTINCT FROM (NEW."message_preset_id") OR OLD."objective" IS DISTINCT FROM (NEW."objective") OR OLD."owner_id" IS DISTINCT FROM (NEW."owner_id") OR OLD."passing_point" IS DISTINCT FROM (NEW."passing_point") OR OLD."preview_url" IS DISTINCT FROM (NEW."preview_url") OR OLD."published" IS DISTINCT FROM (NEW."published") OR OLD."thumbnail" IS DISTINCT FROM (NEW."thumbnail") OR OLD."title" IS DISTINCT FROM (NEW."title") OR OLD."verification_required" IS DISTINCT FROM (NEW."verification_required"))', func='INSERT INTO "course_courseevent" ("appeal_deadline_days", "audience", "confirm_due_days", "created", "description", "duration", "effort_hours", "faq_id", "featured", "format", "grade_due_days", "honor_code_id", "id", "level", "max_attempts", "message_preset_id", "modified", "objective", "owner_id", "passing_point", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "preview_url", "published", "thumbnail", "title", "verification_required") VALUES (NEW."appeal_deadline_days", NEW."audience", NEW."confirm_due_days", NEW."created", NEW."description", NEW."duration", NEW."effort_hours", NEW."faq_id", NEW."featured", NEW."format", NEW."grade_due_days", NEW."honor_code_id", NEW."id", NEW."level", NEW."max_attempts", NEW."message_preset_id", NEW."modified", NEW."objective", NEW."owner_id", NEW."passing_point", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."preview_url", NEW."published", NEW."thumbnail", NEW."title", NEW."verification_required"); RETURN NULL;', hash='390a525812f48a2d95f11b97e1ce9d1f567c27e1', operation='UPDATE', pgid='pgtrigger_update_update_8ff8f', table='course_course', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='course', - trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "course_courseevent" ("audience", "created", "description", "duration", "effort_hours", "faq_id", "featured", "format", "honor_code_id", "id", "level", "max_attempts", "message_preset_id", "modified", "objective", "owner_id", "passing_point", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "preview_url", "published", "thumbnail", "title", "verification_required") VALUES (OLD."audience", OLD."created", OLD."description", OLD."duration", OLD."effort_hours", OLD."faq_id", OLD."featured", OLD."format", OLD."honor_code_id", OLD."id", OLD."level", OLD."max_attempts", OLD."message_preset_id", OLD."modified", OLD."objective", OLD."owner_id", OLD."passing_point", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."preview_url", OLD."published", OLD."thumbnail", OLD."title", OLD."verification_required"); RETURN NULL;', hash='2d9743409281694ed38d213212c6c0d5697ce7bf', operation='DELETE', pgid='pgtrigger_delete_delete_36642', table='course_course', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "course_courseevent" ("appeal_deadline_days", "audience", "confirm_due_days", "created", "description", "duration", "effort_hours", "faq_id", "featured", "format", "grade_due_days", "honor_code_id", "id", "level", "max_attempts", "message_preset_id", "modified", "objective", "owner_id", "passing_point", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "preview_url", "published", "thumbnail", "title", "verification_required") VALUES (OLD."appeal_deadline_days", OLD."audience", OLD."confirm_due_days", OLD."created", OLD."description", OLD."duration", OLD."effort_hours", OLD."faq_id", OLD."featured", OLD."format", OLD."grade_due_days", OLD."honor_code_id", OLD."id", OLD."level", OLD."max_attempts", OLD."message_preset_id", OLD."modified", OLD."objective", OLD."owner_id", OLD."passing_point", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."preview_url", OLD."published", OLD."thumbnail", OLD."title", OLD."verification_required"); RETURN NULL;', hash='5d8e0b26f6fa0ecd69fd5a5f2e3ac07b122e02a9', operation='DELETE', pgid='pgtrigger_delete_delete_36642', table='course_course', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='course', diff --git a/core/apps/course/models.py b/core/apps/course/models.py index ed50fde..4366285 100644 --- a/core/apps/course/models.py +++ b/core/apps/course/models.py @@ -45,8 +45,8 @@ from apps.assignment.models import Assignment from apps.assignment.models import Grade as AssignmentGrade from apps.common.error import ErrorCode -from apps.common.models import AttemptMixin, LearningObjectMixin, OrderableMixin, TimeStampedMixin -from apps.common.util import AccessDate, ModeChoices, OtpTokenDict, issue_active_context, track_fields +from apps.common.models import AttemptMixin, GradeWorkflowMixin, LearningObjectMixin, OrderableMixin, TimeStampedMixin +from apps.common.util import AccessDate, GradingDate, ModeChoices, OtpTokenDict, issue_active_context, track_fields from apps.competency.models import Certificate, CertificateAward, CertificateAwardDataDict from apps.content.models import Media from apps.course.trigger import course_create_grading_policy, lesson_media_unifier @@ -79,6 +79,7 @@ class SessionDict(TypedDict): access_date: AccessDate + grading_date: GradingDate course: Course engagement: NotRequired[Engagement] otp_token: NotRequired[str] @@ -105,7 +106,7 @@ def save(self, *args, **kwargs): @pghistory.track() -class Course(LearningObjectMixin): +class Course(LearningObjectMixin, GradeWorkflowMixin): class LevelChoices(TextChoices): BEGINNER = "beginner", _("Beginner") INTERMEDIATE = "intermediate", _("Intermediate") @@ -168,7 +169,7 @@ async def get_session(cls, *, course_id: str, learner_id: str, access_date: Acce ) course.grading_criteria = await course.grading_policy.grading_criteria(access_date) - session = SessionDict(access_date=access_date, course=course) + session = SessionDict(access_date=access_date, grading_date=course.get_grading_date(access_date), course=course) for unit in [*course.lessons.all(), *course.course_surveys.all()]: unit.start_date = access_date["start"] + timedelta(days=unit.start_offset) @@ -590,7 +591,7 @@ def issue_context(self): return issue_active_context("course", self.course_id, self.pk) @classmethod - async def start(cls, *, course_id: str, learner_id: str, mode: ModeChoices): + async def start(cls, *, course_id: str, learner_id: str, lock: datetime, mode: ModeChoices): course = await Course.objects.aget(id=course_id) if course.verification_required: @@ -599,7 +600,7 @@ async def start(cls, *, course_id: str, learner_id: str, mode: ModeChoices): try: engagement = await Engagement.objects.acreate( - course_id=course_id, learner_id=learner_id, active=True, mode=mode + course_id=course_id, learner_id=learner_id, active=True, lock=lock, mode=mode ) except IntegrityError: raise ValueError(ErrorCode.ALREADY_EXISTS) diff --git a/core/apps/course/tests/factories.py b/core/apps/course/tests/factories.py index 4bd0c56..afeed95 100644 --- a/core/apps/course/tests/factories.py +++ b/core/apps/course/tests/factories.py @@ -7,7 +7,7 @@ from apps.account.tests.factories import UserFactory from apps.assignment.tests.factories import AssignmentFactory -from apps.common.tests.factories import LearningObjectFactory +from apps.common.tests.factories import GradeWorkflowFactory, LearningObjectFactory from apps.competency.tests.factories import CertificateFactory from apps.content.tests.factories import MediaFactory from apps.course.models import ( @@ -44,7 +44,7 @@ class Meta: skip_postgeneration_save = True -class CourseFactory(LearningObjectFactory[Course]): +class CourseFactory(LearningObjectFactory[Course], GradeWorkflowFactory[Course]): passing_point = FactoryField("choice", items=[60, 80]) max_attempts = FactoryField("choice", items=[1, 2]) verification_required = True diff --git a/core/apps/discussion/api/schema.py b/core/apps/discussion/api/schema.py index e5a9e94..6c9424e 100644 --- a/core/apps/discussion/api/schema.py +++ b/core/apps/discussion/api/schema.py @@ -25,13 +25,6 @@ class DiscussionSchema(LearningObjectMixinSchema): class DiscussionQuestionSchema(Schema): - class DiscussionPointRequirementsSchema(Schema): - post: Annotated[int, Field(None)] - reply: Annotated[int, Field(None)] - tutor_assessment: Annotated[int, Field(None)] - post_min_characters: Annotated[int, Field(None)] - reply_min_characters: Annotated[int, Field(None)] - id: int directive: str supplement: str @@ -55,7 +48,7 @@ class DiscussionAttemptSchema(AttemptMixinSchema): class DiscussionEarnedDetailsSchema(Schema): post: int reply: int - tutor_assessment: int + tutor_assessment: int | None class DiscussionGradeSchema(GradeFieldMixinSchema, TimeStampedMixinSchema): diff --git a/core/apps/discussion/api/v1.py b/core/apps/discussion/api/v1.py index 8451956..5f29920 100644 --- a/core/apps/discussion/api/v1.py +++ b/core/apps/discussion/api/v1.py @@ -38,7 +38,11 @@ async def get_session(request: HttpRequest, id: str): @access_date("discussion", "discussion") async def start_attempt(request: HttpRequest, id: str): return await Attempt.start( - discussion_id=id, learner_id=request.auth, context=request.active_context, mode=request.access_mode + discussion_id=id, + learner_id=request.auth, + lock=request.access_date["end"], + context=request.active_context, + mode=request.access_mode, ) diff --git a/core/apps/discussion/migrations/0001_initial.py b/core/apps/discussion/migrations/0001_initial.py index e27d662..624b204 100644 --- a/core/apps/discussion/migrations/0001_initial.py +++ b/core/apps/discussion/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-03-07 04:59 +# Generated by Django 6.0.3 on 2026-03-08 06:49 import apps.common.util import django.db.models.deletion @@ -54,6 +54,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('started', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Start Time')), + ('lock', models.DateTimeField(verbose_name='Lock')), ('active', models.BooleanField(default=True, verbose_name='Active')), ('context', models.CharField(blank=True, default='', max_length=255, verbose_name='Context Key')), ('mode', models.CharField(choices=[('', ''), ('preview', 'Preview'), ('audit', 'Audit')], db_index=True, default='', max_length=30, verbose_name='Mode')), @@ -179,6 +180,7 @@ class Migration(migrations.Migration): ('pgh_label', models.TextField(help_text='The event label.')), ('id', models.BigIntegerField()), ('started', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Start Time')), + ('lock', models.DateTimeField(verbose_name='Lock')), ('active', models.BooleanField(default=True, verbose_name='Active')), ('context', models.CharField(blank=True, default='', max_length=255, verbose_name='Context Key')), ('mode', models.CharField(choices=[('', ''), ('preview', 'Preview'), ('audit', 'Audit')], default='', max_length=30, verbose_name='Mode')), @@ -339,15 +341,15 @@ class Migration(migrations.Migration): ), pgtrigger.migrations.AddTrigger( model_name='attempt', - trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "discussion_attemptevent" ("active", "context", "discussion_id", "id", "learner_id", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "question_id", "retry", "started") VALUES (NEW."active", NEW."context", NEW."discussion_id", NEW."id", NEW."learner_id", NEW."mode", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."question_id", NEW."retry", NEW."started"); RETURN NULL;', hash='0d46845e7851c82fcd70d20e8735e894f1550364', operation='INSERT', pgid='pgtrigger_insert_insert_c592d', table='discussion_attempt', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "discussion_attemptevent" ("active", "context", "discussion_id", "id", "learner_id", "lock", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "question_id", "retry", "started") VALUES (NEW."active", NEW."context", NEW."discussion_id", NEW."id", NEW."learner_id", NEW."lock", NEW."mode", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."question_id", NEW."retry", NEW."started"); RETURN NULL;', hash='1d4074578689732f994af0dd4192acc4648ff9e2', operation='INSERT', pgid='pgtrigger_insert_insert_c592d', table='discussion_attempt', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='attempt', - trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "discussion_attemptevent" ("active", "context", "discussion_id", "id", "learner_id", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "question_id", "retry", "started") VALUES (NEW."active", NEW."context", NEW."discussion_id", NEW."id", NEW."learner_id", NEW."mode", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."question_id", NEW."retry", NEW."started"); RETURN NULL;', hash='95ef4e3333085a7128756233a7e58c4a5f7bde79', operation='UPDATE', pgid='pgtrigger_update_update_7f758', table='discussion_attempt', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "discussion_attemptevent" ("active", "context", "discussion_id", "id", "learner_id", "lock", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "question_id", "retry", "started") VALUES (NEW."active", NEW."context", NEW."discussion_id", NEW."id", NEW."learner_id", NEW."lock", NEW."mode", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."question_id", NEW."retry", NEW."started"); RETURN NULL;', hash='a9d6e0b0789b6a98f2f2287913a50100f89d8852', operation='UPDATE', pgid='pgtrigger_update_update_7f758', table='discussion_attempt', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='attempt', - trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "discussion_attemptevent" ("active", "context", "discussion_id", "id", "learner_id", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "question_id", "retry", "started") VALUES (OLD."active", OLD."context", OLD."discussion_id", OLD."id", OLD."learner_id", OLD."mode", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."question_id", OLD."retry", OLD."started"); RETURN NULL;', hash='785dd072e5f98a9ced87e44aa6bdbde75a2c73a8', operation='DELETE', pgid='pgtrigger_delete_delete_88e52', table='discussion_attempt', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "discussion_attemptevent" ("active", "context", "discussion_id", "id", "learner_id", "lock", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "question_id", "retry", "started") VALUES (OLD."active", OLD."context", OLD."discussion_id", OLD."id", OLD."learner_id", OLD."lock", OLD."mode", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."question_id", OLD."retry", OLD."started"); RETURN NULL;', hash='67f22e65f2b0563c8cd8f3a2e124629feeeb7f72', operation='DELETE', pgid='pgtrigger_delete_delete_88e52', table='discussion_attempt', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='attempt', diff --git a/core/apps/discussion/models.py b/core/apps/discussion/models.py index 64d3b5d..2b19fcf 100644 --- a/core/apps/discussion/models.py +++ b/core/apps/discussion/models.py @@ -223,7 +223,7 @@ class Meta: max_attempts: int # annotated @classmethod - async def start(cls, *, discussion_id: str, learner_id: str, context: str, mode: ModeChoices): + async def start(cls, *, discussion_id: str, learner_id: str, lock: datetime, context: str, mode: ModeChoices): discussion = await Discussion.objects.aget(id=discussion_id) if discussion.verification_required: @@ -237,6 +237,7 @@ async def start(cls, *, discussion_id: str, learner_id: str, context: str, mode: attempt = await Attempt.objects.acreate( discussion=discussion, learner_id=learner_id, + lock=lock, context=context, active=True, started=timezone.now() + timedelta(seconds=1), @@ -449,13 +450,17 @@ async def grade(self, grader_id: str | None = None): self.earned_details = { "post": min(post_count["valid_post"], question.post_point), "reply": min(post_count["valid_reply"], question.reply_point), - "tutor_assessment": min(tutor_assessment_point, question.tutor_assessment_point), + "tutor_assessment": min(tutor_assessment_point, question.tutor_assessment_point) + if tutor_assessment_point is not None + else None, } self.possible_point = question.point - self.earned_point = sum(self.earned_details.values()) + self.earned_point = sum(v for v in self.earned_details.values() if v is not None) self.score = self.earned_point * 100.0 / self.possible_point if self.possible_point else 0.0 self.passed = self.score >= (self.attempt.discussion.passing_point or 0) + if not self.completed: + self.completed = None if None in self.earned_details.values() else timezone.now() self.grader_id = grader_id await self.asave() diff --git a/core/apps/discussion/tests/factories.py b/core/apps/discussion/tests/factories.py index aa62726..c9281d4 100644 --- a/core/apps/discussion/tests/factories.py +++ b/core/apps/discussion/tests/factories.py @@ -84,6 +84,7 @@ class AttemptFactory(DjangoModelFactory[Attempt]): learner = SubFactory(UserFactory) started = LazyFunction(lambda: timezone.now()) question = LazyAttribute(lambda o: async_to_sync(o.discussion.question_pool.select_question)()) + lock = LazyFunction(timezone.now) active = True class Meta: diff --git a/core/apps/exam/api/v1.py b/core/apps/exam/api/v1.py index e057774..0473a29 100644 --- a/core/apps/exam/api/v1.py +++ b/core/apps/exam/api/v1.py @@ -24,7 +24,11 @@ async def get_session(request: HttpRequest, id: str): @access_date("exam", "exam") async def start_attempt(request: HttpRequest, id: str): return await Attempt.start( - exam_id=id, learner_id=request.auth, context=request.active_context, mode=request.access_mode + exam_id=id, + learner_id=request.auth, + lock=request.access_date["end"], + context=request.active_context, + mode=request.access_mode, ) diff --git a/core/apps/exam/migrations/0001_initial.py b/core/apps/exam/migrations/0001_initial.py index fde618b..b2b385f 100644 --- a/core/apps/exam/migrations/0001_initial.py +++ b/core/apps/exam/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-03-07 04:59 +# Generated by Django 6.0.3 on 2026-03-08 06:49 import apps.common.util import django.contrib.postgres.fields @@ -26,6 +26,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('started', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Start Time')), + ('lock', models.DateTimeField(verbose_name='Lock')), ('active', models.BooleanField(default=True, verbose_name='Active')), ('context', models.CharField(blank=True, default='', max_length=255, verbose_name='Context Key')), ('mode', models.CharField(choices=[('', ''), ('preview', 'Preview'), ('audit', 'Audit')], db_index=True, default='', max_length=30, verbose_name='Mode')), @@ -74,6 +75,7 @@ class Migration(migrations.Migration): ('pgh_label', models.TextField(help_text='The event label.')), ('id', models.BigIntegerField()), ('started', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Start Time')), + ('lock', models.DateTimeField(verbose_name='Lock')), ('active', models.BooleanField(default=True, verbose_name='Active')), ('context', models.CharField(blank=True, default='', max_length=255, verbose_name='Context Key')), ('mode', models.CharField(choices=[('', ''), ('preview', 'Preview'), ('audit', 'Audit')], default='', max_length=30, verbose_name='Mode')), @@ -385,15 +387,15 @@ class Migration(migrations.Migration): ), pgtrigger.migrations.AddTrigger( model_name='attempt', - trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "exam_attemptevent" ("active", "context", "exam_id", "id", "learner_id", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "retry", "started") VALUES (NEW."active", NEW."context", NEW."exam_id", NEW."id", NEW."learner_id", NEW."mode", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."retry", NEW."started"); RETURN NULL;', hash='de127c1af3300fbd986094463f2c96da0d9a34c8', operation='INSERT', pgid='pgtrigger_insert_insert_d95a6', table='exam_attempt', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "exam_attemptevent" ("active", "context", "exam_id", "id", "learner_id", "lock", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "retry", "started") VALUES (NEW."active", NEW."context", NEW."exam_id", NEW."id", NEW."learner_id", NEW."lock", NEW."mode", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."retry", NEW."started"); RETURN NULL;', hash='7dc048c866365e1c045c3273e5d9457c64af9fd7', operation='INSERT', pgid='pgtrigger_insert_insert_d95a6', table='exam_attempt', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='attempt', - trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "exam_attemptevent" ("active", "context", "exam_id", "id", "learner_id", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "retry", "started") VALUES (NEW."active", NEW."context", NEW."exam_id", NEW."id", NEW."learner_id", NEW."mode", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."retry", NEW."started"); RETURN NULL;', hash='dd3396dc65ce8b9559a9b48101e6c60cc336d165', operation='UPDATE', pgid='pgtrigger_update_update_3268a', table='exam_attempt', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "exam_attemptevent" ("active", "context", "exam_id", "id", "learner_id", "lock", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "retry", "started") VALUES (NEW."active", NEW."context", NEW."exam_id", NEW."id", NEW."learner_id", NEW."lock", NEW."mode", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."retry", NEW."started"); RETURN NULL;', hash='f06b143888b64a87108dd0192b6de6848ff4ba3f', operation='UPDATE', pgid='pgtrigger_update_update_3268a', table='exam_attempt', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='attempt', - trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "exam_attemptevent" ("active", "context", "exam_id", "id", "learner_id", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "retry", "started") VALUES (OLD."active", OLD."context", OLD."exam_id", OLD."id", OLD."learner_id", OLD."mode", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."retry", OLD."started"); RETURN NULL;', hash='9f28d3a773608a78c600c26bd459cce058ea9399', operation='DELETE', pgid='pgtrigger_delete_delete_9811e', table='exam_attempt', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "exam_attemptevent" ("active", "context", "exam_id", "id", "learner_id", "lock", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "retry", "started") VALUES (OLD."active", OLD."context", OLD."exam_id", OLD."id", OLD."learner_id", OLD."lock", OLD."mode", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."retry", OLD."started"); RETURN NULL;', hash='b5c366e9638a0bbce3b6337f3b03e1d0bc2f75e9', operation='DELETE', pgid='pgtrigger_delete_delete_9811e', table='exam_attempt', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='attempt', diff --git a/core/apps/exam/models.py b/core/apps/exam/models.py index 9141039..c654c25 100644 --- a/core/apps/exam/models.py +++ b/core/apps/exam/models.py @@ -234,6 +234,22 @@ async def analyze_answers(self, question_ids: Sequence[int]): return await sync_to_async(SubmissionDocument.analyze_answers)(question_ids=question_ids) + @classmethod + async def regrade_question(cls, *, exam_id: str, question_id: int, from_answers: list[str], to_answers: list[str]): + affected = set(from_answers).symmetric_difference(set(to_answers)) + attempts = [ + a + async for a in Attempt.objects.select_related("grade").filter( + exam_id=exam_id, + questions__id=question_id, + active=True, + **{f"submission__answers__{question_id}__in": list(affected)}, + ) + ] + for attempt in attempts: + if hasattr(attempt, "grade"): + await attempt.grade.regrade() + @pghistory.track() class Attempt(AttemptMixin): @@ -266,7 +282,7 @@ def saved_answers(self): return self.tempanswer.answers @classmethod - async def start(cls, *, exam_id: str, learner_id: str, context: str, mode: ModeChoices): + async def start(cls, *, exam_id: str, learner_id: str, lock: datetime, context: str, mode: ModeChoices): exam = await Exam.objects.prefetch_related("question_pool__questions").aget(id=exam_id) if exam.verification_required: @@ -280,6 +296,7 @@ async def start(cls, *, exam_id: str, learner_id: str, context: str, mode: ModeC attempt = await Attempt.objects.acreate( exam=exam, learner_id=learner_id, + lock=lock, context=context, active=True, started=timezone.now() + timedelta(seconds=1), @@ -399,6 +416,7 @@ class Meta(GradeFieldMixin.Meta, TimeStampedMixin.Meta): pk: int pgh_event_model: type[Model] attempt_id: int + analysis: dict[str, dict[str, int]] async def grade(self, earned_existing: dict[str, int | None] | None = None, grader_id: str | None = None): questions = [q async for q in self.attempt.questions.all()] @@ -438,6 +456,8 @@ async def grade(self, earned_existing: dict[str, int | None] | None = None, grad self.earned_point = sum(filter(None, self.earned_details.values())) self.score = self.earned_point * 100.0 / self.possible_point if self.possible_point else 0.0 self.passed = self.score >= (self.attempt.exam.passing_point or 0) + if not self.completed: + self.completed = None if None in self.earned_details.values() else timezone.now() self.grader_id = grader_id await self.asave() diff --git a/core/apps/exam/tests/factories.py b/core/apps/exam/tests/factories.py index fe274cf..83b68f7 100644 --- a/core/apps/exam/tests/factories.py +++ b/core/apps/exam/tests/factories.py @@ -129,6 +129,7 @@ class AttemptFactory(DjangoModelFactory[Attempt]): exam = SubFactory(ExamFactory) learner = SubFactory(UserFactory) started = LazyFunction(lambda: timezone.now()) + lock = LazyFunction(timezone.now) active = True class Meta: diff --git a/core/apps/learning/api/access_control.py b/core/apps/learning/api/access_control.py index 8357ad2..2c64b8c 100644 --- a/core/apps/learning/api/access_control.py +++ b/core/apps/learning/api/access_control.py @@ -12,13 +12,18 @@ from apps.course.models import Course from apps.learning.models import ENROLLABLE_MODEL_MAP, Enrollment from apps.quiz.models import Quiz +from apps.tutor.models import TUTORING_MODEL_MAP, Allocation log = logging.getLogger(__name__) +SPECIAL_ACCESS_TIME = timedelta(hours=1) + + def access_date(app_label, model, *, id_field: str = "id"): def decorator(func): # openapi query param + openapi_query_param(func=func, name="mode", schema_type="string", required=False, nullable=False) openapi_query_param(func=func, name="media", schema_type="string", required=False, nullable=False) # currently only quiz is allowed to be inlined @@ -60,17 +65,30 @@ async def wrapper(request: HttpRequest, *args, **kwargs): public_access = await PublicAccessMedia.get_access_date(media_id=media_id) if not (enrollment or public_access): - if "editor" in request.roles: - ContentModel = ENROLLABLE_MODEL_MAP[(app_label, model)] - # Check ownership - if await ContentModel.objects.filter(id=content_id, owner_id=user_id).aexists(): - # grant 1 hour temporary access to editor + # Check preview and special access + if "preview" == request.GET.get("mode"): + has_special_access = False + + # editor requires ownership + if "editor" in request.roles: + ContentModel = ENROLLABLE_MODEL_MAP[(app_label, model)] + if await ContentModel.objects.filter(id=content_id, owner_id=user_id).aexists(): + has_special_access = True + + # tutor rquires allocations + elif "tutor" in request.roles: + ContentModel = TUTORING_MODEL_MAP[(app_label, model)] + if await Allocation.objects.filter( + tutor_id=user_id, active=True, content_id=content_id + ).aexists(): + has_special_access = True + + if has_special_access: + # grant temporary access to tutor now = timezone.now() - accessible = AccessDate( - start=now, end=now + timedelta(hours=1), archive=now + timedelta(hours=1) - ) + expire = now + SPECIAL_ACCESS_TIME + accessible = AccessDate(start=now, end=expire, archive=expire) request.access_date = accessible - request.active_context = "" return await func(request, *args, **kwargs) # more favorable access date between enrollment and public access diff --git a/core/apps/learning/api/v1.py b/core/apps/learning/api/v1.py index 150354d..a64459c 100644 --- a/core/apps/learning/api/v1.py +++ b/core/apps/learning/api/v1.py @@ -26,7 +26,6 @@ async def get_enrolled( page: Annotated[int, functions.Query(1, ge=1)], size: Annotated[int, functions.Query(settings.DEFAULT_PAGINATION_SIZE, gte=1, le=100)], ): - # Custom pagination with generic relationship return await Enrollment.get_enrolled(user_id=request.auth, page=page, size=size) diff --git a/core/apps/learning/management/commands/setup_demo_data.py b/core/apps/learning/management/commands/setup_demo_data.py index eeea4da..a5cd92e 100644 --- a/core/apps/learning/management/commands/setup_demo_data.py +++ b/core/apps/learning/management/commands/setup_demo_data.py @@ -16,14 +16,18 @@ from mimesis.plugins.factory import FactoryField from apps.account.models import User +from apps.assignment.models import Assignment from apps.content.models import Media, MediaQuiz, PublicAccessMedia -from apps.content.tests.factories import MediaFactory +from apps.content.tests.factories import _REAL_DATA, MediaFactory +from apps.discussion.models import Discussion +from apps.exam.models import Exam from apps.learning.models import CatalogItem from apps.learning.tests.factories import CatalogFactory, CohortCatalogFactory, UserCatalogFactory from apps.operation.tests.factories import AnnouncementFactory, InquiryFactory, PolicyFactory from apps.partner.models import CohortMember, Group from apps.partner.tests.factories import CohortFactory, MemberFactory, PartnerFactory from apps.quiz.models import Quiz +from apps.tutor.models import Allocation class Command(BaseCommand): @@ -79,8 +83,6 @@ def handle(self, *args, **options): # all the rest video content - from apps.content.tests.factories import _REAL_DATA - remains = len(_REAL_DATA) - Media.objects.filter(format=Media.MediaFormatChoices.VIDEO).count() if remains > 0: new_medias = MediaFactory.create_batch( @@ -137,6 +139,12 @@ def handle(self, *args, **options): with FactoryField.override_locale(settings.DEFAULT_LANGUAGE): PolicyFactory.create_batch(5) + # tutor allocation + exams = [Allocation(tutor=test_user, content=exam) for exam in Exam.objects.all()] + assignments = [Allocation(tutor=test_user, content=assignment) for assignment in Assignment.objects.all()] + discussions = [Allocation(tutor=test_user, content=discussion) for discussion in Discussion.objects.all()] + Allocation.objects.bulk_create(exams + assignments + discussions, ignore_conflicts=True) + @staticmethod def create_public_catalog(name: str, media_size: int): public_catalog = CatalogFactory.create( diff --git a/core/apps/learning/migrations/0001_initial.py b/core/apps/learning/migrations/0001_initial.py index 23c4756..f7367d8 100644 --- a/core/apps/learning/migrations/0001_initial.py +++ b/core/apps/learning/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-03-07 04:59 +# Generated by Django 6.0.3 on 2026-03-08 06:49 import django.db.models.deletion import django.utils.timezone diff --git a/core/apps/operation/admin.py b/core/apps/operation/admin.py index de64932..b42dfe9 100644 --- a/core/apps/operation/admin.py +++ b/core/apps/operation/admin.py @@ -140,11 +140,6 @@ class InquiryResponseAdmin(HiddenModelAdmin[InquiryResponse]): @admin.register(Appeal) class AppealAdmin(ModelAdmin[Appeal]): - class AppealForm(BooleanDatetimeFormMixin): - boolean_datetime_fields = ["closed"] - - form = AppealForm - class AttachmentInline(TabularInline[Attachment]): model = Appeal.attachments.through verbose_name = _("Attachments") diff --git a/core/apps/operation/api/schema.py b/core/apps/operation/api/schema.py index bd7262b..babe169 100644 --- a/core/apps/operation/api/schema.py +++ b/core/apps/operation/api/schema.py @@ -107,7 +107,6 @@ class AppealSchema(TimeStampedMixinSchema): question_id: int explanation: str review: str - closed: datetime | None path: str @staticmethod diff --git a/core/apps/operation/migrations/0001_initial.py b/core/apps/operation/migrations/0001_initial.py index f10aed9..b04246a 100644 --- a/core/apps/operation/migrations/0001_initial.py +++ b/core/apps/operation/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-03-07 04:59 +# Generated by Django 6.0.3 on 2026-03-08 06:48 import django.contrib.postgres.fields import django.db.models.deletion @@ -87,7 +87,6 @@ class Migration(migrations.Migration): ('modified', models.DateTimeField(auto_now=True, db_index=True, verbose_name='Modified')), ('explanation', models.TextField(verbose_name='Explanation')), ('review', models.TextField(blank=True, default='', verbose_name='Review')), - ('closed', models.DateTimeField(blank=True, null=True, verbose_name='Closed')), ('path', models.CharField(blank=True, default='', max_length=500, verbose_name='Path')), ('question_id', models.IntegerField(verbose_name='Question ID')), ], @@ -108,7 +107,6 @@ class Migration(migrations.Migration): ('modified', models.DateTimeField(auto_now=True, verbose_name='Modified')), ('explanation', models.TextField(verbose_name='Explanation')), ('review', models.TextField(blank=True, default='', verbose_name='Review')), - ('closed', models.DateTimeField(blank=True, null=True, verbose_name='Closed')), ('path', models.CharField(blank=True, default='', max_length=500, verbose_name='Path')), ('question_id', models.IntegerField(verbose_name='Question ID')), ], @@ -1258,21 +1256,25 @@ class Migration(migrations.Migration): model_name='attachment', trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "operation_attachmentevent" ("created", "deleted", "file", "hash", "id", "mime_type", "modified", "owner_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "size") VALUES (OLD."created", OLD."deleted", OLD."file", OLD."hash", OLD."id", OLD."mime_type", OLD."modified", OLD."owner_id", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."size"); RETURN NULL;', hash='b0f21ebd93b944dc5a7f7a10be87ce6d4c3d3497', operation='DELETE', pgid='pgtrigger_delete_delete_b66a6', table='operation_attachment', when='AFTER')), ), + migrations.AddIndex( + model_name='appeal', + index=models.Index(fields=['question_type', 'question_id'], name='operation_a_questio_0c69e6_idx'), + ), migrations.AddConstraint( model_name='appeal', constraint=models.UniqueConstraint(fields=('question_type', 'question_id', 'learner'), name='operation_appeal_quid_quty_le_uniq'), ), pgtrigger.migrations.AddTrigger( model_name='appeal', - trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "operation_appealevent" ("closed", "created", "explanation", "id", "learner_id", "modified", "path", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "question_id", "question_type_id", "review") VALUES (NEW."closed", NEW."created", NEW."explanation", NEW."id", NEW."learner_id", NEW."modified", NEW."path", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."question_id", NEW."question_type_id", NEW."review"); RETURN NULL;', hash='07dc4960990fcedf7f9620a936cce816acbd698e', operation='INSERT', pgid='pgtrigger_insert_insert_c7b97', table='operation_appeal', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "operation_appealevent" ("created", "explanation", "id", "learner_id", "modified", "path", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "question_id", "question_type_id", "review") VALUES (NEW."created", NEW."explanation", NEW."id", NEW."learner_id", NEW."modified", NEW."path", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."question_id", NEW."question_type_id", NEW."review"); RETURN NULL;', hash='ac8a43c03a7924854832f25c2d1bf89420d4138a', operation='INSERT', pgid='pgtrigger_insert_insert_c7b97', table='operation_appeal', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='appeal', - trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD."closed" IS DISTINCT FROM (NEW."closed") OR OLD."explanation" IS DISTINCT FROM (NEW."explanation") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."learner_id" IS DISTINCT FROM (NEW."learner_id") OR OLD."path" IS DISTINCT FROM (NEW."path") OR OLD."question_id" IS DISTINCT FROM (NEW."question_id") OR OLD."question_type_id" IS DISTINCT FROM (NEW."question_type_id") OR OLD."review" IS DISTINCT FROM (NEW."review"))', func='INSERT INTO "operation_appealevent" ("closed", "created", "explanation", "id", "learner_id", "modified", "path", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "question_id", "question_type_id", "review") VALUES (NEW."closed", NEW."created", NEW."explanation", NEW."id", NEW."learner_id", NEW."modified", NEW."path", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."question_id", NEW."question_type_id", NEW."review"); RETURN NULL;', hash='2c23232d27ee0da91c7f196c32ca8e150e0fbf01', operation='UPDATE', pgid='pgtrigger_update_update_3fe73', table='operation_appeal', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD."explanation" IS DISTINCT FROM (NEW."explanation") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."learner_id" IS DISTINCT FROM (NEW."learner_id") OR OLD."path" IS DISTINCT FROM (NEW."path") OR OLD."question_id" IS DISTINCT FROM (NEW."question_id") OR OLD."question_type_id" IS DISTINCT FROM (NEW."question_type_id") OR OLD."review" IS DISTINCT FROM (NEW."review"))', func='INSERT INTO "operation_appealevent" ("created", "explanation", "id", "learner_id", "modified", "path", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "question_id", "question_type_id", "review") VALUES (NEW."created", NEW."explanation", NEW."id", NEW."learner_id", NEW."modified", NEW."path", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."question_id", NEW."question_type_id", NEW."review"); RETURN NULL;', hash='00ca54fa87f8eaf2dfeef72eb1487b7eac9adcfc', operation='UPDATE', pgid='pgtrigger_update_update_3fe73', table='operation_appeal', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='appeal', - trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "operation_appealevent" ("closed", "created", "explanation", "id", "learner_id", "modified", "path", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "question_id", "question_type_id", "review") VALUES (OLD."closed", OLD."created", OLD."explanation", OLD."id", OLD."learner_id", OLD."modified", OLD."path", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."question_id", OLD."question_type_id", OLD."review"); RETURN NULL;', hash='dcc8a44c3d2f096c700ab570834bc022b126d98b', operation='DELETE', pgid='pgtrigger_delete_delete_1853c', table='operation_appeal', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "operation_appealevent" ("created", "explanation", "id", "learner_id", "modified", "path", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "question_id", "question_type_id", "review") VALUES (OLD."created", OLD."explanation", OLD."id", OLD."learner_id", OLD."modified", OLD."path", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."question_id", OLD."question_type_id", OLD."review"); RETURN NULL;', hash='f00f7a018e77653a57a9c86faf64ba22175c90a1', operation='DELETE', pgid='pgtrigger_delete_delete_1853c', table='operation_appeal', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='attachmentevent', diff --git a/core/apps/operation/models.py b/core/apps/operation/models.py index 8c52e4b..c3bcc28 100644 --- a/core/apps/operation/models.py +++ b/core/apps/operation/models.py @@ -472,13 +472,12 @@ def save(self, *args, **kwargs): ) -@track_fields("closed") +@track_fields("review") @pghistory.track() class Appeal(TimeStampedMixin, AttachmentMixin): learner = ForeignKey(User, CASCADE, verbose_name=_("Learner"), related_name="+") explanation = TextField(_("Explanation")) review = TextField(_("Review"), blank=True, default="") - closed = DateTimeField(_("Closed"), null=True, blank=True) path = CharField(_("Path"), max_length=500, default="", blank=True) limit_choices_to = {"model__in": ["question"]} @@ -494,6 +493,7 @@ class Meta(TimeStampedMixin.Meta, AttachmentMixin.Meta): fields=["question_type", "question_id", "learner"], name="operation_appeal_quid_quty_le_uniq" ) ] + indexes = [Index(fields=["question_type", "question_id"])] if TYPE_CHECKING: learner_id: str @@ -528,8 +528,8 @@ async def create( await appeal.update_attachments(files=files, owner_id=learner_id, content=appeal.explanation) return appeal - def on_closed_changed(self, old_value: datetime | None): - if self.closed: + def on_review_changed(self, old_value: str): + if not old_value and self.review: user_message_created.send( source=self, path=self.path, diff --git a/core/apps/partner/migrations/0001_initial.py b/core/apps/partner/migrations/0001_initial.py index 13733a4..422b0cd 100644 --- a/core/apps/partner/migrations/0001_initial.py +++ b/core/apps/partner/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-03-07 04:59 +# Generated by Django 6.0.3 on 2026-03-08 06:48 import django.db.models.deletion import pgtrigger.compiler diff --git a/core/apps/quiz/api/v1.py b/core/apps/quiz/api/v1.py index 2483a42..6cba4b9 100644 --- a/core/apps/quiz/api/v1.py +++ b/core/apps/quiz/api/v1.py @@ -23,7 +23,11 @@ async def get_session(request: HttpRequest, id: str): @access_date("quiz", "quiz") async def start_attempt(request: HttpRequest, id: str): return await Attempt.start( - quiz_id=id, learner_id=request.auth, context=request.active_context, mode=request.access_mode + quiz_id=id, + learner_id=request.auth, + lock=request.access_date["end"], + context=request.active_context, + mode=request.access_mode, ) diff --git a/core/apps/quiz/migrations/0001_initial.py b/core/apps/quiz/migrations/0001_initial.py index b4840cd..a0d4dac 100644 --- a/core/apps/quiz/migrations/0001_initial.py +++ b/core/apps/quiz/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-03-07 04:59 +# Generated by Django 6.0.3 on 2026-03-08 06:48 import apps.common.util import django.contrib.postgres.fields @@ -26,6 +26,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('started', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Start Time')), + ('lock', models.DateTimeField(verbose_name='Lock')), ('active', models.BooleanField(default=True, verbose_name='Active')), ('context', models.CharField(blank=True, default='', max_length=255, verbose_name='Context Key')), ('mode', models.CharField(choices=[('', ''), ('preview', 'Preview'), ('audit', 'Audit')], db_index=True, default='', max_length=30, verbose_name='Mode')), @@ -194,6 +195,7 @@ class Migration(migrations.Migration): ('pgh_label', models.TextField(help_text='The event label.')), ('id', models.BigIntegerField()), ('started', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Start Time')), + ('lock', models.DateTimeField(verbose_name='Lock')), ('active', models.BooleanField(default=True, verbose_name='Active')), ('context', models.CharField(blank=True, default='', max_length=255, verbose_name='Context Key')), ('mode', models.CharField(choices=[('', ''), ('preview', 'Preview'), ('audit', 'Audit')], default='', max_length=30, verbose_name='Mode')), @@ -382,15 +384,15 @@ class Migration(migrations.Migration): ), pgtrigger.migrations.AddTrigger( model_name='attempt', - trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "quiz_attemptevent" ("active", "context", "id", "learner_id", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "quiz_id", "retry", "started") VALUES (NEW."active", NEW."context", NEW."id", NEW."learner_id", NEW."mode", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."quiz_id", NEW."retry", NEW."started"); RETURN NULL;', hash='975c7ce3d64d1ccb276250e020528303986cebc6', operation='INSERT', pgid='pgtrigger_insert_insert_db65b', table='quiz_attempt', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "quiz_attemptevent" ("active", "context", "id", "learner_id", "lock", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "quiz_id", "retry", "started") VALUES (NEW."active", NEW."context", NEW."id", NEW."learner_id", NEW."lock", NEW."mode", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."quiz_id", NEW."retry", NEW."started"); RETURN NULL;', hash='7457692680e34381a5adb7b5861122ab067ade6b', operation='INSERT', pgid='pgtrigger_insert_insert_db65b', table='quiz_attempt', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='attempt', - trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "quiz_attemptevent" ("active", "context", "id", "learner_id", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "quiz_id", "retry", "started") VALUES (NEW."active", NEW."context", NEW."id", NEW."learner_id", NEW."mode", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."quiz_id", NEW."retry", NEW."started"); RETURN NULL;', hash='47bc85779a76d44d2ac5cda8b1946b46c03841b4', operation='UPDATE', pgid='pgtrigger_update_update_7b6c0', table='quiz_attempt', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "quiz_attemptevent" ("active", "context", "id", "learner_id", "lock", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "quiz_id", "retry", "started") VALUES (NEW."active", NEW."context", NEW."id", NEW."learner_id", NEW."lock", NEW."mode", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."quiz_id", NEW."retry", NEW."started"); RETURN NULL;', hash='7e2a121e02c55ad7b584f545f9efd56653c1905c', operation='UPDATE', pgid='pgtrigger_update_update_7b6c0', table='quiz_attempt', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='attempt', - trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "quiz_attemptevent" ("active", "context", "id", "learner_id", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "quiz_id", "retry", "started") VALUES (OLD."active", OLD."context", OLD."id", OLD."learner_id", OLD."mode", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."quiz_id", OLD."retry", OLD."started"); RETURN NULL;', hash='68622659dc0eba5b20e531b8cac252759a3c68fa', operation='DELETE', pgid='pgtrigger_delete_delete_12426', table='quiz_attempt', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "quiz_attemptevent" ("active", "context", "id", "learner_id", "lock", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "quiz_id", "retry", "started") VALUES (OLD."active", OLD."context", OLD."id", OLD."learner_id", OLD."lock", OLD."mode", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."quiz_id", OLD."retry", OLD."started"); RETURN NULL;', hash='9c599e6c2b9b037ff9b78908f7a5e6af412cb523', operation='DELETE', pgid='pgtrigger_delete_delete_12426', table='quiz_attempt', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='attempt', diff --git a/core/apps/quiz/models.py b/core/apps/quiz/models.py index b00e1cc..3b74b08 100644 --- a/core/apps/quiz/models.py +++ b/core/apps/quiz/models.py @@ -1,5 +1,5 @@ import random -from datetime import timedelta +from datetime import datetime, timedelta from typing import TYPE_CHECKING, NotRequired, Sequence, TypedDict import pghistory @@ -260,7 +260,7 @@ class Meta(TimeStampedMixin.Meta): _prefetched_objects_cache: dict[str, QuerySet[Question]] @classmethod - async def start(cls, *, quiz_id: str, learner_id: str, context: str, mode: ModeChoices): + async def start(cls, *, quiz_id: str, learner_id: str, lock: datetime, context: str, mode: ModeChoices): quiz = await Quiz.objects.prefetch_related("question_pool__questions").aget(id=quiz_id) questions = await quiz.question_pool.select_questions() await aprefetch_related_objects(questions, "attachments") # type: ignore @@ -269,6 +269,7 @@ async def start(cls, *, quiz_id: str, learner_id: str, context: str, mode: ModeC attempt = await Attempt.objects.acreate( quiz=quiz, learner_id=learner_id, + lock=lock, context=context, active=True, started=timezone.now() + timedelta(seconds=1), diff --git a/core/apps/quiz/tests/factories.py b/core/apps/quiz/tests/factories.py index 5157467..490a80e 100644 --- a/core/apps/quiz/tests/factories.py +++ b/core/apps/quiz/tests/factories.py @@ -93,6 +93,7 @@ class AttemptFactory(DjangoModelFactory[Attempt]): quiz = SubFactory(QuizFactory) learner = SubFactory(UserFactory) started = LazyFunction(lambda: timezone.now()) + lock = LazyFunction(timezone.now) active = True class Meta: diff --git a/core/apps/sso/migrations/0001_initial.py b/core/apps/sso/migrations/0001_initial.py index 80132a4..d55e70b 100644 --- a/core/apps/sso/migrations/0001_initial.py +++ b/core/apps/sso/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-03-07 04:59 +# Generated by Django 6.0.3 on 2026-03-08 06:48 import apps.common.util import django.db.models.deletion diff --git a/core/apps/store/migrations/0001_initial.py b/core/apps/store/migrations/0001_initial.py index a9f3cfe..6fd251a 100644 --- a/core/apps/store/migrations/0001_initial.py +++ b/core/apps/store/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-03-07 04:59 +# Generated by Django 6.0.3 on 2026-03-08 06:48 import django.db.models.deletion import django.utils.timezone diff --git a/core/apps/studio/api/v1/assignment.py b/core/apps/studio/api/v1/assignment.py index 8d900fd..d83bd53 100644 --- a/core/apps/studio/api/v1/assignment.py +++ b/core/apps/studio/api/v1/assignment.py @@ -179,7 +179,7 @@ def create_new(): @editor_required() @track_editing(Assignment, id_field="id") async def delete_assignment(request: HttpRequest, id: str): - if await Attempt.objects.filter(assignment_id=id).exclude(mode=ModeChoices.PREVIEW).aexists(): + if await Attempt.objects.filter(assignment_id=id, mode=ModeChoices.NORMAL).aexists(): raise ValueError(ErrorCode.ATTEMPT_EXISTS) await Assignment.objects.filter(id=id, owner_id=request.auth, published__isnull=True).adelete() @@ -246,7 +246,7 @@ async def save_assignment_questions( @track_editing(Assignment, id_field="id") async def delete_assignment_quesion(request: HttpRequest, id: str, question_id: int): if await Attempt.objects.filter( - assignment_id=id, question=question_id, assignment__owner_id=request.auth + assignment_id=id, question=question_id, assignment__owner_id=request.auth, mode=ModeChoices.NORMAL ).aexists(): raise ValueError(ErrorCode.IN_USE) diff --git a/core/apps/studio/api/v1/course.py b/core/apps/studio/api/v1/course.py index 9b11343..b5536dd 100644 --- a/core/apps/studio/api/v1/course.py +++ b/core/apps/studio/api/v1/course.py @@ -18,6 +18,7 @@ ContentTypeSchema, FileSizeValidator, FileTypeValidator, + GradeWorkflowMixinSchema, LearningObjectMixinSchema, Schema, ) @@ -115,7 +116,7 @@ class CourseAssetsSpec(Schema): course_instructors: list[CourseInstructorSpec] -class CourseSpec(LearningObjectMixinSchema): +class CourseSpec(LearningObjectMixinSchema, GradeWorkflowMixinSchema): id: str objective: str preview_url: str | None @@ -152,6 +153,9 @@ class CourseSaveSpec(Schema): preview_url: HttpUrl effort_hours: int level: Course.LevelChoices + grade_due_days: int + appeal_deadline_days: int + confirm_due_days: int honor_code_id: int faq_id: int grading_policy: GradingPolicySpec @@ -243,7 +247,7 @@ def create_new(): @editor_required() @track_editing(Course, id_field="id") async def delete_course(request: HttpRequest, id: str): - if await Engagement.objects.filter(course_id=id).exclude(mode=ModeChoices.PREVIEW).aexists(): + if await Engagement.objects.filter(course_id=id, mode=ModeChoices.NORMAL).aexists(): raise ValueError(ErrorCode.ATTEMPT_EXISTS) await Course.objects.filter(id=id, owner_id=request.auth, published__isnull=True).adelete() diff --git a/core/apps/studio/api/v1/discussion.py b/core/apps/studio/api/v1/discussion.py index 6845a2b..acae8f1 100644 --- a/core/apps/studio/api/v1/discussion.py +++ b/core/apps/studio/api/v1/discussion.py @@ -132,7 +132,7 @@ def create_new(): @editor_required() @track_editing(Discussion, id_field="id") async def delete_discussion(request: HttpRequest, id: str): - if await Attempt.objects.filter(discussion_id=id).exclude(mode=ModeChoices.PREVIEW).aexists(): + if await Attempt.objects.filter(discussion_id=id, mode=ModeChoices.NORMAL).aexists(): raise ValueError(ErrorCode.ATTEMPT_EXISTS) await Discussion.objects.filter(id=id, owner_id=request.auth, published__isnull=True).adelete() @@ -201,7 +201,7 @@ async def save_discussion_questions( @track_editing(Discussion, id_field="id") async def delete_discussion_quesion(request: HttpRequest, id: str, question_id: int): if await Attempt.objects.filter( - discussion_id=id, question_id=question_id, discussion__owner_id=request.auth + discussion_id=id, question_id=question_id, discussion__owner_id=request.auth, mode=ModeChoices.NORMAL ).aexists(): raise ValueError(ErrorCode.IN_USE) diff --git a/core/apps/studio/api/v1/exam.py b/core/apps/studio/api/v1/exam.py index ac9451e..bb27989 100644 --- a/core/apps/studio/api/v1/exam.py +++ b/core/apps/studio/api/v1/exam.py @@ -152,7 +152,7 @@ def create_new(): @editor_required() @track_editing(Exam, id_field="id") async def delete_exam(request: HttpRequest, id: str): - if await Attempt.objects.filter(exam_id=id).exclude(mode=ModeChoices.PREVIEW).aexists(): + if await Attempt.objects.filter(exam_id=id, mode=ModeChoices.NORMAL).aexists(): raise ValueError(ErrorCode.ATTEMPT_EXISTS) await Exam.objects.filter(id=id, owner_id=request.auth, published__isnull=True).adelete() @@ -224,7 +224,9 @@ async def save_exam_questions( @editor_required() @track_editing(Exam, id_field="id") async def delete_exam_quesion(request: HttpRequest, id: str, question_id: int): - if await Attempt.objects.filter(exam_id=id, questions=question_id, exam__owner_id=request.auth).aexists(): + if await Attempt.objects.filter( + exam_id=id, questions=question_id, exam__owner_id=request.auth, mode=ModeChoices.NORMAL + ).aexists(): raise ValueError(ErrorCode.IN_USE) count, _ = await Question.objects.filter(id=question_id, pool__exam__id=id).adelete() diff --git a/core/apps/studio/api/v1/media.py b/core/apps/studio/api/v1/media.py index 8fe72b4..0a1e4e3 100644 --- a/core/apps/studio/api/v1/media.py +++ b/core/apps/studio/api/v1/media.py @@ -119,7 +119,7 @@ async def save_media( @editor_required() @track_editing(Media, id_field="id") async def delete_media(request: HttpRequest, id: str): - if await Watch.objects.filter(media_id=id).exclude(mode=ModeChoices.PREVIEW).aexists(): + if await Watch.objects.filter(media_id=id, mode=ModeChoices.NORMAL).aexists(): raise ValueError(ErrorCode.ATTEMPT_EXISTS) await Media.objects.filter(id=id, owner_id=request.auth, published__isnull=True).adelete() diff --git a/core/apps/studio/api/v1/quiz.py b/core/apps/studio/api/v1/quiz.py index 92af5e5..3c703bc 100644 --- a/core/apps/studio/api/v1/quiz.py +++ b/core/apps/studio/api/v1/quiz.py @@ -135,7 +135,7 @@ def create_new(): @editor_required() @track_editing(Quiz, id_field="id") async def delete_quiz(request: HttpRequest, id: str): - if await Attempt.objects.filter(quiz_id=id).exclude(mode=ModeChoices.PREVIEW).aexists(): + if await Attempt.objects.filter(quiz_id=id, mode=ModeChoices.NORMAL).aexists(): raise ValueError(ErrorCode.ATTEMPT_EXISTS) await Quiz.objects.filter(id=id, owner_id=request.auth, published__isnull=True).adelete() @@ -207,7 +207,9 @@ async def save_quiz_questions( @editor_required() @track_editing(Quiz, id_field="id") async def delete_quiz_quesion(request: HttpRequest, id: str, question_id: int): - if await Attempt.objects.filter(quiz_id=id, questions=question_id, quiz__owner_id=request.auth).aexists(): + if await Attempt.objects.filter( + quiz_id=id, questions=question_id, quiz__owner_id=request.auth, mode=ModeChoices.NORMAL + ).aexists(): raise ValueError(ErrorCode.IN_USE) count, _ = await Question.objects.filter(id=question_id, pool__quiz__id=id).adelete() diff --git a/core/apps/studio/api/v1/schema.py b/core/apps/studio/api/v1/schema.py deleted file mode 100644 index 896721a..0000000 --- a/core/apps/studio/api/v1/schema.py +++ /dev/null @@ -1,6 +0,0 @@ -from apps.common.schema import Schema - - -class HonorCodeSpec(Schema): - title: str - code: str diff --git a/core/apps/studio/api/v1/survey.py b/core/apps/studio/api/v1/survey.py index b3de3de..f859e61 100644 --- a/core/apps/studio/api/v1/survey.py +++ b/core/apps/studio/api/v1/survey.py @@ -122,7 +122,7 @@ def create_new(): @editor_required() @track_editing(Survey, id_field="id") async def delete_survey(request: HttpRequest, id: str): - if await Submission.objects.filter(survey_id=id).exclude(mode=ModeChoices.PREVIEW).aexists(): + if await Submission.objects.filter(survey_id=id, mode=ModeChoices.NORMAL).aexists(): raise ValueError(ErrorCode.ATTEMPT_EXISTS) await Survey.objects.filter(id=id, owner_id=request.auth, published__isnull=True).adelete() diff --git a/core/apps/studio/migrations/0001_initial.py b/core/apps/studio/migrations/0001_initial.py index b84fb5a..f56eebe 100644 --- a/core/apps/studio/migrations/0001_initial.py +++ b/core/apps/studio/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-03-07 04:59 +# Generated by Django 6.0.3 on 2026-03-08 06:49 import django.db.models.deletion import django.utils.timezone diff --git a/core/apps/studio/models.py b/core/apps/studio/models.py index d475c48..942ad92 100644 --- a/core/apps/studio/models.py +++ b/core/apps/studio/models.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING + import pghistory from django.contrib.auth import get_user_model from django.contrib.contenttypes.fields import GenericForeignKey @@ -5,7 +7,6 @@ from django.db.models import CASCADE, CharField, DateTimeField, ForeignKey, Model, TextField, UniqueConstraint from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from PIL.GifImagePlugin import TYPE_CHECKING User = get_user_model() diff --git a/core/apps/survey/api/v1.py b/core/apps/survey/api/v1.py index 30470c0..8d4436b 100644 --- a/core/apps/survey/api/v1.py +++ b/core/apps/survey/api/v1.py @@ -1,3 +1,4 @@ +from django.utils import timezone from ninja.router import Router from apps.common.util import HttpRequest @@ -24,6 +25,7 @@ async def submit(request: HttpRequest, id: str, data: SurveyAnswersSchema): respondent_id=request.auth, context=request.active_context, answers=data.model_dump(), + lock=request.access_date["end"], mode=request.access_mode, ) @@ -42,7 +44,9 @@ async def get_anonymous_survey(request: HttpRequest, id: str): @router.post("/{id}/anonymous/submit", auth=None) @access_mode() async def submit_anonymous(request: HttpRequest, id: str, data: SurveyAnswersSchema): - await Submission.submit(survey_id=id, answers=data.model_dump(), anonymous=True, mode=request.access_mode) + await Submission.submit( + survey_id=id, answers=data.model_dump(), lock=timezone.now(), anonymous=True, mode=request.access_mode + ) @router.get("/{id}/anonymous/results", auth=None, response=dict[str, dict[str, int]]) diff --git a/core/apps/survey/migrations/0001_initial.py b/core/apps/survey/migrations/0001_initial.py index c26dcc6..e362430 100644 --- a/core/apps/survey/migrations/0001_initial.py +++ b/core/apps/survey/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-03-07 04:59 +# Generated by Django 6.0.3 on 2026-03-08 06:48 import apps.common.util import django.contrib.postgres.fields @@ -100,6 +100,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('started', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Start Time')), + ('lock', models.DateTimeField(verbose_name='Lock')), ('active', models.BooleanField(default=True, verbose_name='Active')), ('context', models.CharField(blank=True, default='', max_length=255, verbose_name='Context Key')), ('mode', models.CharField(choices=[('', ''), ('preview', 'Preview'), ('audit', 'Audit')], db_index=True, default='', max_length=30, verbose_name='Mode')), @@ -148,6 +149,7 @@ class Migration(migrations.Migration): ('pgh_label', models.TextField(help_text='The event label.')), ('id', models.BigIntegerField()), ('started', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Start Time')), + ('lock', models.DateTimeField(verbose_name='Lock')), ('active', models.BooleanField(default=True, verbose_name='Active')), ('context', models.CharField(blank=True, default='', max_length=255, verbose_name='Context Key')), ('mode', models.CharField(choices=[('', ''), ('preview', 'Preview'), ('audit', 'Audit')], default='', max_length=30, verbose_name='Mode')), @@ -256,15 +258,15 @@ class Migration(migrations.Migration): ), pgtrigger.migrations.AddTrigger( model_name='submission', - trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "survey_submissionevent" ("active", "answers", "context", "id", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "respondent_id", "started", "survey_id") VALUES (NEW."active", NEW."answers", NEW."context", NEW."id", NEW."mode", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."respondent_id", NEW."started", NEW."survey_id"); RETURN NULL;', hash='994783c4693b6fcb8288760aadb9d3cb6a148c8a', operation='INSERT', pgid='pgtrigger_insert_insert_ebca0', table='survey_submission', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "survey_submissionevent" ("active", "answers", "context", "id", "lock", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "respondent_id", "started", "survey_id") VALUES (NEW."active", NEW."answers", NEW."context", NEW."id", NEW."lock", NEW."mode", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."respondent_id", NEW."started", NEW."survey_id"); RETURN NULL;', hash='a7dc3860509041e407a61d9b50f33251fb4b8d7c', operation='INSERT', pgid='pgtrigger_insert_insert_ebca0', table='survey_submission', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='submission', - trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "survey_submissionevent" ("active", "answers", "context", "id", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "respondent_id", "started", "survey_id") VALUES (NEW."active", NEW."answers", NEW."context", NEW."id", NEW."mode", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."respondent_id", NEW."started", NEW."survey_id"); RETURN NULL;', hash='d773d7c6d9033665cf3f111104f9fc97481c556c', operation='UPDATE', pgid='pgtrigger_update_update_d6efc', table='survey_submission', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "survey_submissionevent" ("active", "answers", "context", "id", "lock", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "respondent_id", "started", "survey_id") VALUES (NEW."active", NEW."answers", NEW."context", NEW."id", NEW."lock", NEW."mode", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."respondent_id", NEW."started", NEW."survey_id"); RETURN NULL;', hash='2d1002c5696b19c02e894aab167694ff8f31bdbb', operation='UPDATE', pgid='pgtrigger_update_update_d6efc', table='survey_submission', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='submission', - trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "survey_submissionevent" ("active", "answers", "context", "id", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "respondent_id", "started", "survey_id") VALUES (OLD."active", OLD."answers", OLD."context", OLD."id", OLD."mode", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."respondent_id", OLD."started", OLD."survey_id"); RETURN NULL;', hash='e38565e0b5e22fd144cba1f30438a49c20bb0f56', operation='DELETE', pgid='pgtrigger_delete_delete_fa339', table='survey_submission', when='AFTER')), + trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "survey_submissionevent" ("active", "answers", "context", "id", "lock", "mode", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "respondent_id", "started", "survey_id") VALUES (OLD."active", OLD."answers", OLD."context", OLD."id", OLD."lock", OLD."mode", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."respondent_id", OLD."started", OLD."survey_id"); RETURN NULL;', hash='2b7bf95477a09412dc2ee3a92cfa56a522a157f8', operation='DELETE', pgid='pgtrigger_delete_delete_fa339', table='survey_submission', when='AFTER')), ), pgtrigger.migrations.AddTrigger( model_name='surveyevent', diff --git a/core/apps/survey/models.py b/core/apps/survey/models.py index 1d4b1b6..776e814 100644 --- a/core/apps/survey/models.py +++ b/core/apps/survey/models.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import TYPE_CHECKING import pghistory @@ -142,6 +143,7 @@ async def submit( *, survey_id: str, answers: dict[str, str], + lock: datetime, mode: ModeChoices, respondent_id: str | None = None, context: str = "", @@ -150,7 +152,7 @@ async def submit( survey = await Survey.objects.aget(id=survey_id) if survey.anonymous: - await Submission.objects.acreate(survey=survey, answers=answers, mode=mode) + await Submission.objects.acreate(survey=survey, answers=answers, lock=lock, mode=mode) else: if anonymous: raise ValueError(ErrorCode.ANONYMOUS_NOT_ALLOWED) @@ -160,5 +162,5 @@ async def submit( survey=survey, respondent_id=respondent_id, context=context, - defaults={"answers": answers, "active": True, "mode": mode}, + defaults={"answers": answers, "lock": lock, "active": True, "mode": mode}, ) diff --git a/core/apps/survey/tests/factories.py b/core/apps/survey/tests/factories.py index d9e4d2b..6cfc123 100644 --- a/core/apps/survey/tests/factories.py +++ b/core/apps/survey/tests/factories.py @@ -1,5 +1,6 @@ import mimesis from django.conf import settings +from django.utils import timezone from factory.declarations import Iterator, LazyFunction, Sequence, SubFactory from factory.django import DjangoModelFactory from factory.helpers import lazy_attribute, post_generation @@ -84,6 +85,7 @@ def post_generation(self, create: bool, extracted: object, **kwargs: object): class SubmissionFactory(DjangoModelFactory[Submission]): survey = SubFactory(SurveyFactory) respondent = SubFactory(UserFactory) + lock = LazyFunction(timezone.now) active = True context = "" diff --git a/core/apps/tracking/migrations/0001_initial.py b/core/apps/tracking/migrations/0001_initial.py index ad55837..f41b552 100644 --- a/core/apps/tracking/migrations/0001_initial.py +++ b/core/apps/tracking/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-03-07 04:59 +# Generated by Django 6.0.3 on 2026-03-08 06:48 import pghistory.utils from django.db import migrations, models diff --git a/core/apps/tutor/__init__.py b/core/apps/tutor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/tutor/admin.py b/core/apps/tutor/admin.py new file mode 100644 index 0000000..42f77a5 --- /dev/null +++ b/core/apps/tutor/admin.py @@ -0,0 +1,23 @@ +from django.contrib import admin +from django.utils.translation import gettext_lazy as _ + +from apps.common.admin import ModelAdmin, ReadOnlyHiddenModelAdmin, ReadOnlyTabularInline +from apps.tutor.models import Allocation + + +@admin.register(Allocation) +class AllocationAdmin(ModelAdmin): + class AllocationEventInline(ReadOnlyTabularInline[Allocation.pgh_event_model]): + model = Allocation.pgh_event_model + verbose_name = _("Allocation History") + verbose_name_plural = _("Allocation Histories") + + def get_queryset(self, request): + return super().get_queryset(request).select_related("pgh_context", "tutor", "content_type") + + inlines = (AllocationEventInline,) + + +@admin.register(Allocation.pgh_event_model) +class AllocationEventAdmin(ReadOnlyHiddenModelAdmin[Allocation.pgh_event_model]): + pass diff --git a/core/apps/tutor/api/v1/__init__.py b/core/apps/tutor/api/v1/__init__.py new file mode 100644 index 0000000..9ba2b2e --- /dev/null +++ b/core/apps/tutor/api/v1/__init__.py @@ -0,0 +1,66 @@ +from datetime import datetime +from typing import Annotated, Literal + +from django.conf import settings +from ninja import Router +from ninja.params import functions + +from apps.common.schema import ContentTypeSchema, Schema +from apps.common.util import HttpRequest, PaginatedResponse +from apps.tutor.api.v1.exam import router as exam_router +from apps.tutor.decorator import tutor_required +from apps.tutor.models import Allocation + +router = Router(by_alias=True) + + +TutoringModel = Literal["exam", "assignment", "discussion"] + + +class AllocationSchema(Schema): + class TutorContentSchema(Schema): + id: str + created: datetime + title: str + last_grading: datetime | None + submission_count: int + grade_completed_count: int + grade_confirmed_count: int + appeal_count: int + appeal_open_count: int + + id: int + content: TutorContentSchema + content_type: ContentTypeSchema + + @staticmethod + def resolve_content(allocation: Allocation): + return allocation._content_cache + + +@router.get("/allocation", response=PaginatedResponse[AllocationSchema]) +@tutor_required() +async def get_allocation( + request: HttpRequest, + page: Annotated[int, functions.Query(1, ge=1)], + size: Annotated[int, functions.Query(settings.DEFAULT_PAGINATION_SIZE, gte=1, le=100)], +): + return await Allocation.get_allocated(tutor_id=request.auth, page=page, size=size) + + +class AllocationStatsSchema(Schema): + allocation_count: int + submission_count: int + grade_completed_count: int + grade_confirmed_count: int + appeal_count: int + appeal_open_count: int + + +@router.get("/allocation/stats", response=AllocationStatsSchema) +@tutor_required() +async def get_allocation_stats(request: HttpRequest): + return await Allocation.get_stats(tutor_id=request.auth) + + +router.add_router("", exam_router, tags=["tutor"]) diff --git a/core/apps/tutor/api/v1/exam.py b/core/apps/tutor/api/v1/exam.py new file mode 100644 index 0000000..08400a8 --- /dev/null +++ b/core/apps/tutor/api/v1/exam.py @@ -0,0 +1,195 @@ +from datetime import datetime +from typing import Annotated + +from django.db.models import F, Prefetch, Q +from django.shortcuts import aget_object_or_404 +from ninja import Field, Router +from ninja.pagination import paginate + +from apps.account.api.schema import OwnerSchema +from apps.common.schema import Schema +from apps.common.util import GradingDate, HttpRequest, Pagination +from apps.exam.api.schema import ExamQuestionSchema, ExamSolutionSchema +from apps.exam.models import Exam, Grade, Question, Solution +from apps.operation.api.schema import AppealSchema +from apps.operation.models import Appeal +from apps.tutor.decorator import allocation_required, tutor_required +from apps.tutor.tasks import regrade_question + +router = Router(by_alias=True) + + +class TutorExamGradeSchema(Schema): + id: int + created: datetime + score: float + passed: bool + completed: datetime | None + confirmed: datetime | None + attempt_retry: int + grading_date: GradingDate + + @staticmethod + def resolve_grading_date(grade: Grade): + return grade.attempt.exam.get_grading_date(access_date={"end": grade.attempt.lock}) + + +@router.get("/exam/{id}/grade", response=list[TutorExamGradeSchema]) +@tutor_required() +@allocation_required("exam", "exam") +@paginate(Pagination) +async def get_exam_grades(request: HttpRequest, id: str): + return ( + Grade.objects + .select_related("attempt__exam") + .annotate(attempt_retry=F("attempt__retry")) + .filter(attempt__exam_id=id, attempt__active=True) + .order_by("-created") + ) + + +class TutorExamGradePaperSchema(Schema): + class TutorExamQuestionSchema(ExamQuestionSchema): + solution: ExamSolutionSchema | None + + id: int + earned_details: dict[str, int | None] + answers: dict[str, str] + feedback: dict[str, str] + grader: OwnerSchema | None + questions: list[TutorExamQuestionSchema] + analysis: dict[str, dict[str, int]] + + @staticmethod + def resolve_answers(grade: Grade): + question_ids = [q.id for q in grade.attempt.questions.all()] + return {k: v for k, v in grade.attempt.submission.answers.items() if int(k) in question_ids} + + @staticmethod + def resolve_earned_details(grade: Grade): + question_ids = [q.id for q in grade.attempt.questions.all()] + return {k: v for k, v in grade.earned_details.items() if int(k) in question_ids} + + @staticmethod + def resolve_questions(grade: Grade): + return grade.attempt.questions.all() + + +@router.get("/exam/{id}/grade/{grade_id}", response=TutorExamGradePaperSchema) +@tutor_required() +@allocation_required("exam", "exam") +async def get_exam_grade_paper(request: HttpRequest, id: str, grade_id: int): + grade = await aget_object_or_404( + Grade.objects.select_related("attempt__submission", "grader").prefetch_related( + Prefetch( + "attempt__questions", + queryset=Question.objects + .select_related("solution") + .prefetch_related("attachments") + .filter( + Q(solution__correct_answers=[]) + | Q( + format__in=[ + Question.ExamQuestionFormatChoices.ESSAY, + Question.ExamQuestionFormatChoices.TEXT_INPUT, + ] + ) + ) + .order_by("id"), + ) + ), + id=grade_id, + attempt__exam_id=id, + attempt__active=True, + ) + grade.analysis = await Exam().analyze_answers([q.id for q in grade.attempt.questions.all()]) + + return grade + + +class TutorExamGradeSaveSchema(Schema): + earned_details: dict[str, int | None] + feedback: dict[str, str] + + +class TutorExamGradeSavedSchema(Schema): + score: float + passed: bool + completed: datetime | None + + +@router.post("/exam/{id}/grade/{grade_id}", response=TutorExamGradeSavedSchema) +@tutor_required() +@allocation_required("exam", "exam") +async def complete_exam_grade(request: HttpRequest, id: str, grade_id: int, data: TutorExamGradeSaveSchema): + grade = await aget_object_or_404( + Grade.objects + .annotate(attempt_retry=F("attempt__retry")) + .select_related("attempt__submission", "attempt__exam") + .prefetch_related( + Prefetch( + "attempt__questions", + queryset=Question.objects.select_related("solution").prefetch_related("attachments"), + ) + ), + id=grade_id, + attempt__exam_id=id, + attempt__active=True, + ) + grade.feedback.update(data.feedback) + await grade.grade(data.earned_details, grader_id=request.auth) + return grade + + +@router.get("/exam/{id}/appeal", response=list[AppealSchema]) +@tutor_required() +@allocation_required("exam", "exam") +@paginate(Pagination) +async def get_exam_appeals(request: HttpRequest, id: str): + question_ids = Question.objects.filter(pool__exam__id=id).values("id") + return Appeal.objects.prefetch_related("attachments").filter( + question_type__app_label="exam", question_type__model="question", question_id__in=question_ids + ) + + +class TutorExamAppealSaveSchema(Schema): + review: Annotated[str, Field(min_length=1)] + appeal_ids: list[int] + + +@router.post("/exam/{id}/appeal") +@tutor_required() +@allocation_required("exam", "exam") +async def review_exam_appeals(request: HttpRequest, id: str, data: TutorExamAppealSaveSchema): + question_ids = Question.objects.filter(pool__exam__id=id).values("id") + await Appeal.objects.filter( + id__in=data.appeal_ids, + question_type__app_label="exam", + question_type__model="question", + question_id__in=question_ids, + ).aupdate(review=data.review) + + +class TutorExamQuestionSolutionSchema(Schema): + correct_answers: list[str] + correct_criteria: str + explanation: str + + +@router.post("/exam/{id}/question/{question_id}/solution") +@tutor_required() +@allocation_required("exam", "exam") +async def update_exam_question_solution( + request: HttpRequest, id: str, question_id: int, data: TutorExamQuestionSolutionSchema +): + solution = await aget_object_or_404(Solution.objects.filter(question__pool__exam__id=id, question_id=question_id)) + + original_answers = set(solution.correct_answers) + for key, value in data.dict().items(): + setattr(solution, key, value) + await solution.asave() + + if original_answers != set(data.correct_answers): + regrade_question.delay( + exam_id=id, question_id=question_id, from_answers=list(original_answers), to_answers=data.correct_answers + ) diff --git a/core/apps/tutor/apps.py b/core/apps/tutor/apps.py new file mode 100644 index 0000000..4fa808a --- /dev/null +++ b/core/apps/tutor/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class TutorConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.tutor" + verbose_name = _("Tutor") diff --git a/core/apps/tutor/decorator.py b/core/apps/tutor/decorator.py new file mode 100644 index 0000000..d4ba2df --- /dev/null +++ b/core/apps/tutor/decorator.py @@ -0,0 +1,41 @@ +from functools import wraps + +from asgiref.sync import sync_to_async +from django.contrib.contenttypes.models import ContentType + +from apps.common.error import ErrorCode +from apps.common.util import HttpRequest +from apps.tutor.models import Allocation + + +def tutor_required(): + def decorator(func): + @wraps(func) + async def wrapper(request: HttpRequest, *args, **kwargs): + if "tutor" not in request.roles: + raise ValueError(ErrorCode.PERMISSION_DENIED) + return await func(request, *args, **kwargs) + + return wrapper + + return decorator + + +def allocation_required(app_label: str, model: str, id_field: str = "id"): + def decorator(func): + @wraps(func) + async def wrapper(request: HttpRequest, *args, **kwargs): + + content_type = await sync_to_async(ContentType.objects.get_by_natural_key)(app_label, model) + allocated = await Allocation.objects.filter( + tutor_id=request.auth, content_type=content_type, content_id=kwargs[id_field] + ).aexists() + + if not allocated: + raise ValueError(ErrorCode.PERMISSION_DENIED) + + return await func(request, *args, **kwargs) + + return wrapper + + return decorator diff --git a/core/apps/tutor/migrations/0001_initial.py b/core/apps/tutor/migrations/0001_initial.py new file mode 100644 index 0000000..bcdeaed --- /dev/null +++ b/core/apps/tutor/migrations/0001_initial.py @@ -0,0 +1,78 @@ +# Generated by Django 6.0.3 on 2026-03-08 06:49 + +import django.db.models.deletion +import pgtrigger.compiler +import pgtrigger.migrations +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('pghistory', '0007_auto_20250421_0444'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Allocation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Created')), + ('modified', models.DateTimeField(auto_now=True, db_index=True, verbose_name='Modified')), + ('active', models.BooleanField(default=True, verbose_name='Active')), + ('content_id', models.CharField(max_length=36, verbose_name='Content ID')), + ('content_type', models.ForeignKey(limit_choices_to={'model__in': ('exam', 'assignment', 'discussion')}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name='Content type')), + ('tutor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Tutor')), + ], + options={ + 'verbose_name': 'Tutor Allocation', + 'verbose_name_plural': 'Tutor Allocations', + 'abstract': False, + }, + ), + migrations.CreateModel( + name='AllocationEvent', + fields=[ + ('pgh_id', models.AutoField(primary_key=True, serialize=False)), + ('pgh_created_at', models.DateTimeField(auto_now_add=True)), + ('pgh_label', models.TextField(help_text='The event label.')), + ('id', models.BigIntegerField()), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Modified')), + ('active', models.BooleanField(default=True, verbose_name='Active')), + ('content_id', models.CharField(max_length=36, verbose_name='Content ID')), + ('content_type', models.ForeignKey(db_constraint=False, limit_choices_to={'model__in': ('exam', 'assignment', 'discussion')}, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', related_query_name='+', to='contenttypes.contenttype', verbose_name='Content type')), + ('pgh_context', models.ForeignKey(db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='pghistory.context')), + ('pgh_obj', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='events', to='tutor.allocation')), + ('tutor', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', related_query_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Tutor')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddConstraint( + model_name='allocation', + constraint=models.UniqueConstraint(fields=('tutor', 'content_type', 'content_id'), name='tutor_allocation_coty_coid_uniq'), + ), + pgtrigger.migrations.AddTrigger( + model_name='allocation', + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "tutor_allocationevent" ("active", "content_id", "content_type_id", "created", "id", "modified", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "tutor_id") VALUES (NEW."active", NEW."content_id", NEW."content_type_id", NEW."created", NEW."id", NEW."modified", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."tutor_id"); RETURN NULL;', hash='d32c9391e8701a85b0f0e9ed380044660422222c', operation='INSERT', pgid='pgtrigger_insert_insert_d54dd', table='tutor_allocation', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='allocation', + trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD."active" IS DISTINCT FROM (NEW."active") OR OLD."content_id" IS DISTINCT FROM (NEW."content_id") OR OLD."content_type_id" IS DISTINCT FROM (NEW."content_type_id") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."tutor_id" IS DISTINCT FROM (NEW."tutor_id"))', func='INSERT INTO "tutor_allocationevent" ("active", "content_id", "content_type_id", "created", "id", "modified", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "tutor_id") VALUES (NEW."active", NEW."content_id", NEW."content_type_id", NEW."created", NEW."id", NEW."modified", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."tutor_id"); RETURN NULL;', hash='ca721b80fb7aa65fb12a8d402beff29e90f3075a', operation='UPDATE', pgid='pgtrigger_update_update_86aff', table='tutor_allocation', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='allocation', + trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "tutor_allocationevent" ("active", "content_id", "content_type_id", "created", "id", "modified", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "tutor_id") VALUES (OLD."active", OLD."content_id", OLD."content_type_id", OLD."created", OLD."id", OLD."modified", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."tutor_id"); RETURN NULL;', hash='bb5f3cad86ea0cea7eaf7e4e43fd1f0459c0af29', operation='DELETE', pgid='pgtrigger_delete_delete_57622', table='tutor_allocation', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='allocationevent', + trigger=pgtrigger.compiler.Trigger(name='append_only', sql=pgtrigger.compiler.UpsertTriggerSql(func="RAISE EXCEPTION 'pgtrigger: Cannot update or delete rows from % table', TG_TABLE_NAME;", hash='4530583757a0806520333f7a3bab587961ab3263', operation='UPDATE OR DELETE', pgid='pgtrigger_append_only_7a1fd', table='tutor_allocationevent', when='BEFORE')), + ), + ] diff --git a/core/apps/tutor/migrations/__init__.py b/core/apps/tutor/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/tutor/models.py b/core/apps/tutor/models.py new file mode 100644 index 0000000..6243b77 --- /dev/null +++ b/core/apps/tutor/models.py @@ -0,0 +1,351 @@ +import asyncio +from collections import defaultdict +from typing import TYPE_CHECKING + +import pghistory +import psycopg +from asgiref.sync import sync_to_async +from django.contrib.auth import get_user_model +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.db import connection +from django.db.models import CASCADE, BooleanField, CharField, Count, ForeignKey, Max, Model, Q, UniqueConstraint +from django.utils.translation import gettext_lazy as _ + +from apps.assignment.models import Assignment +from apps.assignment.models import Attempt as AssignmentAttempt +from apps.assignment.models import Grade as AssignmentGrade +from apps.common.models import TimeStampedMixin +from apps.common.util import offset_paginate +from apps.discussion.models import Attempt as DiscussionAttempt +from apps.discussion.models import Discussion +from apps.discussion.models import Grade as DiscussionGrade +from apps.exam.models import Attempt as ExamAttempt +from apps.exam.models import Exam +from apps.exam.models import Grade as ExamGrade +from apps.operation.models import Appeal + +User = get_user_model() + + +TUTORING_MODELS = {"exam": Exam, "assignment": Assignment, "discussion": Discussion} +TUTORING_MODEL_MAP = {(m._meta.app_label.lower(), m._meta.model_name): m for m in TUTORING_MODELS.values()} + + +@pghistory.track() +class Allocation(TimeStampedMixin): + tutor = ForeignKey(User, CASCADE, verbose_name=_("Tutor")) + active = BooleanField(_("Active"), default=True) + + limit_choices_to = {"model__in": TUTORING_MODELS.keys()} + content_type = ForeignKey(ContentType, CASCADE, verbose_name=_("Content type"), limit_choices_to=limit_choices_to) + content_id = CharField(_("Content ID"), max_length=36) + content = GenericForeignKey("content_type", "content_id") + + class Meta(TimeStampedMixin.Meta): + verbose_name = _("Tutor Allocation") + verbose_name_plural = _("Tutor Allocations") + constraints = [ + UniqueConstraint(fields=["tutor", "content_type", "content_id"], name="tutor_allocation_coty_coid_uniq") + ] + + if TYPE_CHECKING: + pgh_event_model: type[Model] + _content_cache: GenericForeignKey + + @classmethod + async def get_allocated(cls, *, tutor_id: str, page: int, size: int): + base_qs = cls.objects.select_related("content_type").filter(tutor_id=tutor_id).order_by("-modified") + paginated = await offset_paginate(base_qs, page=page, size=size) + + if not paginated["items"]: + return paginated + + content_ids = defaultdict(set) + for allocation in paginated["items"]: + content_ids[(allocation.content_type.app_label, allocation.content_type.model)].add(allocation.content_id) + + contents = await _fetch_allocatable_contents(content_ids) + appeal_counts = await _fetch_appeal_counts(content_ids) + + valid_items = [] + for item in paginated["items"]: + content = contents.get(item.content_id) + if not content: + continue + content.update(appeal_counts.get(item.content_id, {"appeal_count": 0, "appeal_open_count": 0})) + item._content_cache = content + valid_items.append(item) + + paginated["items"] = valid_items + + return paginated + + @classmethod + async def get_stats(cls, *, tutor_id: str) -> dict: + content_ids_by_type: dict[tuple[str, str], set[str]] = defaultdict(set) + allocation_count = 0 + async for allocation in cls.objects.select_related("content_type").filter(tutor_id=tutor_id): + key = (allocation.content_type.app_label, allocation.content_type.model) + content_ids_by_type[key].add(allocation.content_id) + allocation_count += 1 + + if allocation_count == 0: + return { + "allocation_count": 0, + "submission_count": 0, + "grade_completed_count": 0, + "grade_confirmed_count": 0, + "appeal_count": 0, + "appeal_open_count": 0, + } + + exam_ids = list(content_ids_by_type.get(("exam", "exam"), set())) + assignment_ids = list(content_ids_by_type.get(("assignment", "assignment"), set())) + discussion_ids = list(content_ids_by_type.get(("discussion", "discussion"), set())) + + grade_stats, appeal_stats = await asyncio.gather( + _fetch_grade_stats(exam_ids, assignment_ids, discussion_ids), + _fetch_appeal_stats(exam_ids, assignment_ids, discussion_ids), + ) + + return {"allocation_count": allocation_count, **grade_stats, **appeal_stats} + + +async def _fetch_allocatable_contents(content_ids_by_type: dict): + union_qs = [] + + for M in TUTORING_MODELS.values(): + key = (M._meta.app_label.lower(), M._meta.model_name) + ids = content_ids_by_type.get(key) + + if not ids: + continue + + union_qs.append( + M.objects + .filter(id__in=ids) + .annotate( + last_grading=Max("attempt__grade__completed"), + submission_count=Count("attempt__grade", filter=Q(attempt__active=True), distinct=True), + grade_completed_count=Count( + "attempt__grade", + filter=Q(attempt__active=True, attempt__grade__completed__isnull=False), + distinct=True, + ), + grade_confirmed_count=Count( + "attempt__grade", + filter=Q(attempt__active=True, attempt__grade__confirmed__isnull=False), + distinct=True, + ), + ) + .values( + "id", + "created", + "modified", + "title", + "format", + "last_grading", + "submission_count", + "grade_completed_count", + "grade_confirmed_count", + ) + ) + + if not union_qs: + return {} + + qs = union_qs[0] if len(union_qs) == 1 else union_qs[0].union(*union_qs[1:], all=True) + return {content["id"]: content async for content in qs} + + +async def _fetch_appeal_counts(content_ids_by_type: dict[tuple[str, str], set[str]]) -> dict[str, dict]: + exam_attempt_table = ExamAttempt._meta.db_table + exam_m2m_table = ExamAttempt.questions.through._meta.db_table + assignment_attempt_table = AssignmentAttempt._meta.db_table + discussion_attempt_table = DiscussionAttempt._meta.db_table + appeal_table = Appeal._meta.db_table + + ct_cache = { + app_label: await sync_to_async(ContentType.objects.get_by_natural_key)(app_label, "question") + for app_label in ["exam", "assignment", "discussion"] + } + + exam_ids = list(content_ids_by_type.get(("exam", "exam"), set())) + assignment_ids = list(content_ids_by_type.get(("assignment", "assignment"), set())) + discussion_ids = list(content_ids_by_type.get(("discussion", "discussion"), set())) + + sql = f""" + SELECT ea.exam_id AS content_id, op.review + FROM {appeal_table} op + JOIN {exam_m2m_table} aq ON aq.question_id = op.question_id + JOIN {exam_attempt_table} ea ON ea.id = aq.attempt_id AND ea.active = TRUE + WHERE op.question_type_id = %s AND ea.exam_id = ANY(%s) + UNION ALL + SELECT ea.assignment_id AS content_id, op.review + FROM {appeal_table} op + JOIN {assignment_attempt_table} ea ON ea.question_id = op.question_id AND ea.active = TRUE + WHERE op.question_type_id = %s AND ea.assignment_id = ANY(%s) + UNION ALL + SELECT ea.discussion_id AS content_id, op.review + FROM {appeal_table} op + JOIN {discussion_attempt_table} ea ON ea.question_id = op.question_id AND ea.active = TRUE + WHERE op.question_type_id = %s AND ea.discussion_id = ANY(%s) + """ + + params = connection.get_connection_params() + params["cursor_factory"] = psycopg.AsyncCursor + aconnection = await psycopg.AsyncConnection.connect(**params) + + result: dict[str, dict] = {} + appeal_count: dict[str, int] = defaultdict(int) + open_count: dict[str, int] = defaultdict(int) + + async with aconnection: + async with aconnection.cursor() as cursor: + await cursor.execute( # type: ignore + sql, + [ + ct_cache["exam"].pk, + exam_ids, + ct_cache["assignment"].pk, + assignment_ids, + ct_cache["discussion"].pk, + discussion_ids, + ], + ) + rows = await cursor.fetchall() + + for content_id, review in rows: + appeal_count[content_id] += 1 + if not review: + open_count[content_id] += 1 + + for ids in content_ids_by_type.values(): + for cid in ids: + result[cid] = {"appeal_count": appeal_count.get(cid, 0), "appeal_open_count": open_count.get(cid, 0)} + + return result + + +async def _fetch_grade_stats(exam_ids: list, assignment_ids: list, discussion_ids: list) -> dict: + ea = ExamAttempt._meta.db_table + eg = ExamGrade._meta.db_table + aa = AssignmentAttempt._meta.db_table + ag = AssignmentGrade._meta.db_table + da = DiscussionAttempt._meta.db_table + dg = DiscussionGrade._meta.db_table + + sql = f""" + SELECT + SUM(submission_count) AS submission_count, + SUM(grade_completed_count) AS grade_completed_count, + SUM(grade_confirmed_count) AS grade_confirmed_count + FROM ( + SELECT + COUNT(DISTINCT g.id) FILTER (WHERE ea.active) AS submission_count, + COUNT(DISTINCT g.id) FILTER (WHERE ea.active AND g.completed IS NOT NULL) AS grade_completed_count, + COUNT(DISTINCT g.id) FILTER (WHERE ea.active AND g.confirmed IS NOT NULL) AS grade_confirmed_count + FROM {eg} g + JOIN {ea} ea ON ea.id = g.attempt_id + WHERE ea.exam_id = ANY(%s) + + UNION ALL + + SELECT + COUNT(DISTINCT g.id) FILTER (WHERE aa.active) AS submission_count, + COUNT(DISTINCT g.id) FILTER (WHERE aa.active AND g.completed IS NOT NULL) AS grade_completed_count, + COUNT(DISTINCT g.id) FILTER (WHERE aa.active AND g.confirmed IS NOT NULL) AS grade_confirmed_count + FROM {ag} g + JOIN {aa} aa ON aa.id = g.attempt_id + WHERE aa.assignment_id = ANY(%s) + + UNION ALL + + SELECT + COUNT(DISTINCT g.id) FILTER (WHERE da.active) AS submission_count, + COUNT(DISTINCT g.id) FILTER (WHERE da.active AND g.completed IS NOT NULL) AS grade_completed_count, + COUNT(DISTINCT g.id) FILTER (WHERE da.active AND g.confirmed IS NOT NULL) AS grade_confirmed_count + FROM {dg} g + JOIN {da} da ON da.id = g.attempt_id + WHERE da.discussion_id = ANY(%s) + ) combined + """ + + db_params = connection.get_connection_params() + db_params["cursor_factory"] = psycopg.AsyncCursor + aconnection = await psycopg.AsyncConnection.connect(**db_params) + + async with aconnection: + async with aconnection.cursor() as cursor: + await cursor.execute(sql, [exam_ids, assignment_ids, discussion_ids]) # type: ignore + row = await cursor.fetchone() + + if not row: + return {"submission_count": 0, "grade_completed_count": 0, "grade_confirmed_count": 0} + + return {"submission_count": row[0] or 0, "grade_completed_count": row[1] or 0, "grade_confirmed_count": row[2] or 0} + + +async def _fetch_appeal_stats(exam_ids: list, assignment_ids: list, discussion_ids: list) -> dict: + exam_attempt_table = ExamAttempt._meta.db_table + exam_m2m_table = ExamAttempt.questions.through._meta.db_table + assignment_attempt_table = AssignmentAttempt._meta.db_table + discussion_attempt_table = DiscussionAttempt._meta.db_table + appeal_table = Appeal._meta.db_table + + ct_cache = { + app_label: await sync_to_async(ContentType.objects.get_by_natural_key)(app_label, "question") + for app_label in ["exam", "assignment", "discussion"] + } + + sql = f""" + SELECT + COUNT(*) AS appeal_count, + COUNT(*) FILTER (WHERE review = '') AS appeal_open_count + FROM ( + SELECT op.review + FROM {appeal_table} op + JOIN {exam_m2m_table} aq ON aq.question_id = op.question_id + JOIN {exam_attempt_table} ea ON ea.id = aq.attempt_id AND ea.active = TRUE + WHERE op.question_type_id = %s AND ea.exam_id = ANY(%s) + + UNION ALL + + SELECT op.review + FROM {appeal_table} op + JOIN {assignment_attempt_table} aa ON aa.question_id = op.question_id AND aa.active = TRUE + WHERE op.question_type_id = %s AND aa.assignment_id = ANY(%s) + + UNION ALL + + SELECT op.review + FROM {appeal_table} op + JOIN {discussion_attempt_table} da ON da.question_id = op.question_id AND da.active = TRUE + WHERE op.question_type_id = %s AND da.discussion_id = ANY(%s) + ) combined + """ + + db_params = connection.get_connection_params() + db_params["cursor_factory"] = psycopg.AsyncCursor + aconnection = await psycopg.AsyncConnection.connect(**db_params) + + async with aconnection: + async with aconnection.cursor() as cursor: + await cursor.execute( # type: ignore + sql, + [ + ct_cache["exam"].pk, + exam_ids, + ct_cache["assignment"].pk, + assignment_ids, + ct_cache["discussion"].pk, + discussion_ids, + ], + ) + row = await cursor.fetchone() + + if not row: + return {"appeal_count": 0, "appeal_open_count": 0} + + return {"appeal_count": row[0] or 0, "appeal_open_count": row[1] or 0} diff --git a/core/apps/tutor/tasks.py b/core/apps/tutor/tasks.py new file mode 100644 index 0000000..11a50fd --- /dev/null +++ b/core/apps/tutor/tasks.py @@ -0,0 +1,11 @@ +from asgiref.sync import async_to_sync +from celery import shared_task + +from apps.exam.models import Exam + + +@shared_task +def regrade_question(exam_id: str, question_id: int, from_answers: list[str], to_answers: list[str]): + async_to_sync(Exam.regrade_question)( + exam_id=exam_id, question_id=question_id, from_answers=from_answers, to_answers=to_answers + ) diff --git a/core/apps/tutor/tests/test_tutor_exam_api.py b/core/apps/tutor/tests/test_tutor_exam_api.py new file mode 100644 index 0000000..8bf5992 --- /dev/null +++ b/core/apps/tutor/tests/test_tutor_exam_api.py @@ -0,0 +1,124 @@ +import json + +import pytest +from django.contrib.contenttypes.models import ContentType +from django.test.client import Client +from mimesis.providers.generic import Generic +from pytest_mock import MockerFixture + +from apps.exam.models import Question +from apps.exam.tests.factories import ExamFactory +from apps.operation.models import Appeal +from apps.tutor.api.v1.exam import regrade_question +from apps.tutor.models import Allocation +from conftest import AdminUser + + +@pytest.mark.e2e +@pytest.mark.django_db +def test_tutor_exam_flow(client: Client, mimesis: Generic, admin_user: AdminUser, mocker: MockerFixture): + admin_user.login() + + exam = ExamFactory() + exam_ct = ContentType.objects.get_for_model(exam) + + tutor = admin_user.get_user() + Allocation.objects.create(tutor=tutor, content_type=exam_ct, content_id=exam.id) + + res = client.get("/api/v1/tutor/allocation") + assert res.status_code == 200, "get allocation" + + # get exam grades list + res = client.get(f"/api/v1/tutor/exam/{exam.id}/grade") + assert res.status_code == 200, "get exam grades" + + grades = res.json()["items"] + assert len(grades) > 0, "exam has grades from factory" + grade_id = grades[0]["id"] + + # get grade paper + res = client.get(f"/api/v1/tutor/exam/{exam.id}/grade/{grade_id}") + assert res.status_code == 200, "get grade paper" + + paper = res.json() + assert "earnedDetails" in paper + assert "questions" in paper + + questions = paper["questions"] + assert len(questions) > 0, "grade paper has manual grading questions" + + # complete grade + earned_details = {str(q["id"]): q["point"] for q in questions} + feedback = {str(q["id"]): mimesis.text.sentence() for q in questions} + res = client.post( + f"/api/v1/tutor/exam/{exam.id}/grade/{grade_id}", + data=json.dumps({"earnedDetails": earned_details, "feedback": feedback}), + content_type="application/json", + ) + assert res.status_code == 200, "complete grade" + assert res.json()["completed"] is not None, "grade completed after grading" + + # get exam appeals + res = client.get(f"/api/v1/tutor/exam/{exam.id}/appeal") + assert res.status_code == 200, "get exam appeals" + assert res.json()["count"] == 0, "no appeals yet" + + # create an appeal for a question + question = Question.objects.filter(pool__exam=exam).first() + assert question is not None + question_ct = ContentType.objects.get_for_model(question) + appeal = Appeal.objects.create( + learner=tutor, explanation=mimesis.text.text(), question_type=question_ct, question_id=question.id + ) + + # get appeals again + res = client.get(f"/api/v1/tutor/exam/{exam.id}/appeal") + assert res.status_code == 200, "get exam appeals with appeal" + assert res.json()["count"] == 1, "one appeal" + + # review appeal + res = client.post( + f"/api/v1/tutor/exam/{exam.id}/appeal", + data=json.dumps({"review": mimesis.text.sentence(), "appealIds": [appeal.id]}), + content_type="application/json", + ) + assert res.status_code == 200, "review appeal" + appeal.refresh_from_db() + assert appeal.review != "", "appeal review saved" + + # update question solution + mocker.patch("apps.tutor.api.v1.exam.regrade_question.delay") + single_choice_q = Question.objects.filter( + pool__exam=exam, format=Question.ExamQuestionFormatChoices.SINGLE_CHOICE + ).first() + new_answer = "99" + assert single_choice_q is not None + original_correct_answers = list(single_choice_q.solution.correct_answers) + + res = client.post( + f"/api/v1/tutor/exam/{exam.id}/question/{single_choice_q.id}/solution", + data=json.dumps({ + "correctAnswers": [new_answer], + "correct_criteria": "update criteria", + "explanation": "update explanation", + }), + content_type="application/json", + ) + assert res.status_code == 200, "update question solution" + assert single_choice_q is not None + + regrade_question.delay.assert_called_once_with( # type: ignore + exam_id=exam.id, question_id=single_choice_q.id, from_answers=original_correct_answers, to_answers=[new_answer] + ) + + res = client.post( + f"/api/v1/tutor/exam/{exam.id}/question/{single_choice_q.id}/solution", + data=json.dumps({ + "correctAnswers": [new_answer], + "correct_criteria": "update criteria", + "explanation": "update explanation", + }), + content_type="application/json", + ) + assert res.status_code == 200, "update solution no-op" + assert regrade_question.delay.call_count == 1, "regrade not called again for same answers" # type: ignore diff --git a/core/apps/warehouse/migrations/0001_initial.py b/core/apps/warehouse/migrations/0001_initial.py index ecb7871..f780f2d 100644 --- a/core/apps/warehouse/migrations/0001_initial.py +++ b/core/apps/warehouse/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 6.0.3 on 2026-03-07 04:59 +# Generated by Django 6.0.3 on 2026-03-08 06:48 from django.db import migrations, models diff --git a/core/apps/warehouse/views.py b/core/apps/warehouse/views.py index f72e9ca..f889ed2 100644 --- a/core/apps/warehouse/views.py +++ b/core/apps/warehouse/views.py @@ -20,7 +20,7 @@ def dashboard_callback(request, context): SELECT 1 FROM {response_table} r WHERE r.inquiry_id = i.id AND r.solved IS NOT NULL )), - (SELECT COUNT(*) FROM {appeal_table} WHERE closed IS NULL) + (SELECT COUNT(*) FROM {appeal_table} WHERE review = '') """.format( inquiry_table=Inquiry._meta.db_table, response_table=InquiryResponse._meta.db_table, diff --git a/core/minima/settings.py b/core/minima/settings.py index 0757d0b..c30edb2 100644 --- a/core/minima/settings.py +++ b/core/minima/settings.py @@ -104,6 +104,7 @@ "apps.store", "apps.assistant", "apps.studio", + "apps.tutor", "apps.tracking", "apps.warehouse", ] diff --git a/core/pyproject.toml b/core/pyproject.toml index fa378d4..9f326cc 100644 --- a/core/pyproject.toml +++ b/core/pyproject.toml @@ -64,6 +64,7 @@ dev = [ "openpyxl", "django-stubs", "typing-extensions", + "celery-types>=0.24.0", ] [tool.django-stubs] diff --git a/core/uv.lock b/core/uv.lock index c2eb927..ef9791e 100644 --- a/core/uv.lock +++ b/core/uv.lock @@ -140,29 +140,29 @@ wheels = [ [[package]] name = "boto3" -version = "1.42.62" +version = "1.42.63" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/7e/c952803c8900f14e6f6158fddbd35da5afb2e3fa68bf498a761e6ba2c2ae/boto3-1.42.62.tar.gz", hash = "sha256:6b26ff56c458685caec3d42adde0549f6a55410e557e1f51bebde5c8abcf3037", size = 112848, upload-time = "2026-03-05T21:20:37.755Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/2a/33d5d4b16fd97dfd629421ebed2456392eae1553cc401d9f86010c18065e/boto3-1.42.63.tar.gz", hash = "sha256:cd008cfd0d7ea30f1c5e22daf0998c55b7c6c68cb68eea05110e33fe641173d5", size = 112778, upload-time = "2026-03-06T22:47:55.96Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/68/b5e82dedd9c8d53a9542df4e3475d2d3ec331eef4a4a801e9c5fa98b583a/boto3-1.42.62-py3-none-any.whl", hash = "sha256:eef0ee08f30e5ed16d8296719808801a827fa0f3126a3e2a9ef9be9eb5e6a313", size = 140556, upload-time = "2026-03-05T21:20:35.354Z" }, + { url = "https://files.pythonhosted.org/packages/f5/19/f1d8d2b24871d3d0ccb2cbd0b0cb64a3396d439384bd9643d2c25c641b84/boto3-1.42.63-py3-none-any.whl", hash = "sha256:d502a89a0acc701692ae020d15981f2a82e9eb3485acc651cfd0cf1a3afe79ee", size = 140554, upload-time = "2026-03-06T22:47:53.463Z" }, ] [[package]] name = "boto3-stubs" -version = "1.42.62" +version = "1.42.63" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore-stubs" }, { name = "types-s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/21/ac7e0e79d095ff267d2a4179ec6600fe06ed28d1167c8ef019b03d11b22e/boto3_stubs-1.42.62.tar.gz", hash = "sha256:59eea2a768af74a8fd962f0a2c39f9b3aa897532a49a25ba747af3e4bae5f853", size = 101191, upload-time = "2026-03-06T03:57:00.015Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/30/45d21d3df4598b6b37d8de9923603010e490dfc73a130c8ffa806750b1ff/boto3_stubs-1.42.63.tar.gz", hash = "sha256:b64726c86c0b3efbc6ccd4f0510fa5a8279caec46da36a91c037a4b395001e68", size = 101176, upload-time = "2026-03-06T22:50:00.514Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/d2/4ef7be58ecfb73bc2be98d06209ff7495930548b4adc97c609b9b776e5c2/boto3_stubs-1.42.62-py3-none-any.whl", hash = "sha256:578687643aae8be69a21bfa024f8b74f3e721136baed80c04aba4811a422b4ee", size = 69915, upload-time = "2026-03-06T03:56:51.905Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/933ecf99d0cc1ae1b463086d203b27a29ba05c1d1cdd8a3efb18bc1e8c98/boto3_stubs-1.42.63-py3-none-any.whl", hash = "sha256:132e9c57bfef5ea943745d6cdad16e62d4e82dca64f6ae9ddfba9789fcdc8378", size = 69913, upload-time = "2026-03-06T22:49:49.765Z" }, ] [package.optional-dependencies] @@ -172,16 +172,16 @@ s3 = [ [[package]] name = "botocore" -version = "1.42.62" +version = "1.42.63" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/e7/031f2f03f22817f8a8def7ad1caa138979c20ac35062b055274e0a505c3f/botocore-1.42.62.tar.gz", hash = "sha256:c210dc93b0b81bf72cfe745a7b1c8df765d04bd90b4ac6c8707fbb6714141dae", size = 14966114, upload-time = "2026-03-05T21:20:25.518Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/eb/a1c042f6638ada552399a9977335a6de2668a85bf80bece193c953531236/botocore-1.42.63.tar.gz", hash = "sha256:1fdfc33cff58d21e8622cf620ba2bba3cff324557932aaf935b5374e4610f059", size = 14965362, upload-time = "2026-03-06T22:47:44.158Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/57/9bc5c1aad3a354dd7da54ba52d43ee821badb3deedbea4c5117c4bd05eab/botocore-1.42.62-py3-none-any.whl", hash = "sha256:86d327fded96775268ffe8d8bd6ed96c4a1db86cf24eb64ff85233db12dbc287", size = 14638389, upload-time = "2026-03-05T21:20:22.359Z" }, + { url = "https://files.pythonhosted.org/packages/9a/60/17a2d3b94658bb999c6aee7bba6c76b271905debf0c8c8e6ac63ca8491bc/botocore-1.42.63-py3-none-any.whl", hash = "sha256:83f39d04f2b316bdfc59a3cac2d12238bde7126ac99d9a57d910dbd86d58c528", size = 14639889, upload-time = "2026-03-06T22:47:39.347Z" }, ] [[package]] @@ -229,6 +229,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dd/bd/9ecd619e456ae4ba73b6583cc313f26152afae13e9a82ac4fe7f8856bfd1/celery-5.6.2-py3-none-any.whl", hash = "sha256:3ffafacbe056951b629c7abcf9064c4a2366de0bdfc9fdba421b97ebb68619a5", size = 445502, upload-time = "2026-01-04T12:35:55.894Z" }, ] +[[package]] +name = "celery-types" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/25/2276a1f00f8ab9fc88128c939333933a24db7df1d75aa57ecc27b7dd3a22/celery_types-0.24.0.tar.gz", hash = "sha256:c93fbcd0b04a9e9c2f55d5540aca4aa1ea4cc06a870c0c8dee5062fdd59663fe", size = 33148, upload-time = "2025-12-23T17:16:30.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/7e/3252cba5f5c9a65a3f52a69734d8e51e023db8981022b503e8183cf0225e/celery_types-0.24.0-py3-none-any.whl", hash = "sha256:a21e04681e68719a208335e556a79909da4be9c5e0d6d2fd0dd4c5615954b3fd", size = 60473, upload-time = "2025-12-23T17:16:29.89Z" }, +] + [[package]] name = "certifi" version = "2026.2.25" @@ -870,16 +882,16 @@ grpc = [ [[package]] name = "google-auth" -version = "2.48.0" +version = "2.49.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/59/7371175bfd949abfb1170aa076352131d7281bd9449c0f978604fc4431c3/google_auth-2.49.0.tar.gz", hash = "sha256:9cc2d9259d3700d7a257681f81052db6737495a1a46b610597f4b8bafe5286ae", size = 333444, upload-time = "2026-03-06T21:53:06.07Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" }, + { url = "https://files.pythonhosted.org/packages/37/45/de64b823b639103de4b63dd193480dce99526bd36be6530c2dba85bf7817/google_auth-2.49.0-py3-none-any.whl", hash = "sha256:f893ef7307f19cf53700b7e2f61b5a6affe3aa0edf9943b13788920ab92d8d87", size = 240676, upload-time = "2026-03-06T21:52:38.304Z" }, ] [package.optional-dependencies] @@ -902,18 +914,19 @@ wheels = [ [[package]] name = "google-cloud-firestore" -version = "2.23.0" +version = "2.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, { name = "google-auth" }, { name = "google-cloud-core" }, + { name = "grpcio" }, { name = "proto-plus" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/9c/ec28ca4ec88fa89e41366316cf92c037feaa2c4200aa5d9da69fe011d2f6/google_cloud_firestore-2.23.0.tar.gz", hash = "sha256:a9cffba7cdc6101111d6d54cde22d521c98f9e7d415e67486b137fa16f06aa03", size = 615238, upload-time = "2026-01-14T23:50:54.574Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/5e/9a0f2378623a21d46800815e6ff4c3119af3914e3b8405b1f33f12b4f1b1/google_cloud_firestore-2.24.0.tar.gz", hash = "sha256:e7f831f150e787c46ac3a96407f58d9edccd5455dbc9dae472f4cadb6daf6dc9", size = 620822, upload-time = "2026-03-06T21:53:04.143Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/99/2a627c8ea7ae72a686dda8bf2b79747362b425c237d2729eb76bcee55a25/google_cloud_firestore-2.23.0-py3-none-any.whl", hash = "sha256:19f2326cb466b0d52aed9fabbd89758be431f6ce18c422966cfdb8326b424314", size = 411195, upload-time = "2026-01-14T23:50:52.825Z" }, + { url = "https://files.pythonhosted.org/packages/69/d0/2ac7ff231eb380c88ddac21551156b7d03e25b1699b55641cf694def7c60/google_cloud_firestore-2.24.0-py3-none-any.whl", hash = "sha256:8b778fe766dacc54ef19b4f1b64adc751fca51b59ce569f1ec38c2ebd985dda1", size = 416388, upload-time = "2026-03-06T21:52:50.144Z" }, ] [[package]] @@ -981,14 +994,14 @@ wheels = [ [[package]] name = "googleapis-common-protos" -version = "1.72.0" +version = "1.73.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/96/a0205167fa0154f4a542fd6925bdc63d039d88dab3588b875078107e6f06/googleapis_common_protos-1.73.0.tar.gz", hash = "sha256:778d07cd4fbeff84c6f7c72102f0daf98fa2bfd3fa8bea426edc545588da0b5a", size = 147323, upload-time = "2026-03-06T21:53:09.727Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, + { url = "https://files.pythonhosted.org/packages/69/28/23eea8acd65972bbfe295ce3666b28ac510dfcb115fac089d3edb0feb00a/googleapis_common_protos-1.73.0-py3-none-any.whl", hash = "sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8", size = 297578, upload-time = "2026-03-06T21:52:33.933Z" }, ] [[package]] @@ -1228,6 +1241,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "boto3-stubs", extra = ["s3"] }, + { name = "celery-types" }, { name = "django-stubs" }, { name = "factory-boy" }, { name = "mimesis" }, @@ -1290,6 +1304,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "boto3-stubs", extras = ["s3"] }, + { name = "celery-types", specifier = ">=0.24.0" }, { name = "django-stubs" }, { name = "factory-boy" }, { name = "mimesis" }, @@ -1833,11 +1848,11 @@ wheels = [ [[package]] name = "redis" -version = "7.2.1" +version = "7.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e9/31/1476f206482dd9bc53fdbbe9f6fbd5e05d153f18e54667ce839df331f2e6/redis-7.2.1.tar.gz", hash = "sha256:6163c1a47ee2d9d01221d8456bc1c75ab953cbda18cfbc15e7140e9ba16ca3a5", size = 4906735, upload-time = "2026-02-25T20:05:18.171Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/82/4d1a5279f6c1251d3d2a603a798a1137c657de9b12cfc1fba4858232c4d2/redis-7.3.0.tar.gz", hash = "sha256:4d1b768aafcf41b01022410b3cc4f15a07d9b3d6fe0c66fc967da2c88e551034", size = 4928081, upload-time = "2026-03-06T18:18:16.287Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/98/1dd1a5c060916cf21d15e67b7d6a7078e26e2605d5c37cbc9f4f5454c478/redis-7.2.1-py3-none-any.whl", hash = "sha256:49e231fbc8df2001436ae5252b3f0f3dc930430239bfeb6da4c7ee92b16e5d33", size = 396057, upload-time = "2026-02-25T20:05:16.533Z" }, + { url = "https://files.pythonhosted.org/packages/f0/28/84e57fce7819e81ec5aa1bd31c42b89607241f4fb1a3ea5b0d2dbeaea26c/redis-7.3.0-py3-none-any.whl", hash = "sha256:9d4fcb002a12a5e3c3fbe005d59c48a2cc231f87fbb2f6b70c2d89bb64fec364", size = 404379, upload-time = "2026-03-06T18:18:14.583Z" }, ] [[package]] diff --git a/dev.sh b/dev.sh index 2345e41..f85bdd2 100755 --- a/dev.sh +++ b/dev.sh @@ -37,8 +37,10 @@ up) echo "" echo "Elapsed: $((SECONDS - _start_time))s" echo "" - echo "Admin: http://localhost:8000/admin" - echo "Web: http://localhost:5173" + echo "Admin: http://localhost:8000/admin" + echo "Web: http://localhost:5173" + echo "Studio: http://localhost:5173/studio" + echo "Ttutor: http://localhost:5173/tutor" echo "" ;; diff --git a/web/package-lock.json b/web/package-lock.json index 2d4465d..a2bb4b8 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -74,7 +74,7 @@ "version": "4.4.4", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@asamuzakjp/css-color": { @@ -160,6 +160,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -428,6 +429,7 @@ "integrity": "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ==", "dev": true, "license": "MIT OR Apache-2.0", + "peer": true, "bin": { "biome": "bin/biome" }, @@ -823,6 +825,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -863,6 +866,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -1364,6 +1368,7 @@ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.9.tgz", "integrity": "sha512-3gtUX0e584MYkKBQMgSECMvE1Dwzg+eONefDQ0wxVSe5YMBsZwdN5pL7UapwWBlV8+i8QCztF9TP947tEjZAGA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@firebase/component": "0.7.1", "@firebase/logger": "0.5.0", @@ -1430,6 +1435,7 @@ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.9.tgz", "integrity": "sha512-e5LzqjO69/N2z7XcJeuMzIp4wWnW696dQeaHAUpQvGk89gIWHAIvG6W+mA3UotGW6jBoqdppEJ9DnuwbcBByug==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@firebase/app": "0.14.9", "@firebase/component": "0.7.1", @@ -1445,7 +1451,8 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@firebase/auth": { "version": "1.12.1", @@ -1896,6 +1903,7 @@ "integrity": "sha512-/gnejm7MKkVIXnSJGpc9L2CvvvzJvtDPeAEq5jAwgVlf/PeNxot+THx/bpD20wQ8uL5sz0xqgXy1nisOYMU+mw==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" }, @@ -4148,7 +4156,7 @@ "version": "6.9.1", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.4.0", @@ -4168,7 +4176,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@tiptap/core": { @@ -4176,6 +4184,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.1.tgz", "integrity": "sha512-SwkPEWIfaDEZjC8SEIi4kZjqIYUbRgLUHUuQezo5GbphUNC8kM1pi3C3EtoOPtxXrEbY6e4pWEzW54Pcrd+rVA==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -4406,6 +4415,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.20.1.tgz", "integrity": "sha512-euBRAn0mkV7R2VEE+AuOt3R0j9RHEMFXamPFmtvTo8IInxDClusrm6mJoDjS8gCGAXsQCRiAe1SCQBPgGbOOwg==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -4577,6 +4587,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.20.1.tgz", "integrity": "sha512-JRc/v+OBH0qLTdvQ7HvHWTxGJH73QOf1MC0R8NhOX2QnAbg2mPFv1h+FjGa2gfLGuCXBdWQomjekWkUKbC4e5A==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -4591,6 +4602,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.1.tgz", "integrity": "sha512-6kCiGLvpES4AxcEuOhb7HR7/xIeJWMjZlb6J7e8zpiIh5BoQc7NoRdctsnmFEjZvC19bIasccshHQ7H2zchWqw==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", @@ -4986,7 +4998,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "dequal": "^2.0.3" @@ -5177,6 +5189,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5590,7 +5603,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/cssesc": { @@ -5765,7 +5778,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6523,6 +6536,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4" }, @@ -6678,7 +6692,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7478,7 +7492,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -8091,6 +8105,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", "license": "MIT", + "peer": true, "dependencies": { "orderedmap": "^2.0.0" } @@ -8120,6 +8135,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", @@ -8168,6 +8184,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.4.tgz", "integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -8246,6 +8263,7 @@ "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -8326,7 +8344,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "indent-string": "^4.0.0", @@ -8522,6 +8540,7 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.0.tgz", "integrity": "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -8586,6 +8605,7 @@ "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.11.tgz", "integrity": "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", @@ -8702,7 +8722,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "min-indent": "^1.0.0" @@ -8722,7 +8742,8 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -8804,6 +8825,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -8921,6 +8943,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9033,6 +9056,7 @@ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } @@ -9063,6 +9087,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -9151,6 +9176,7 @@ "integrity": "sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", @@ -9192,6 +9218,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/web/public/image/logo/logo-dark.png b/web/public/image/logo/logo-dark.png deleted file mode 100644 index 33421db0a0525b2c96620ea23463fb4128ba1d78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13123 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 f-or^UV(?^lepml&jYTyc10ICQtjS!K| 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 ^xuky>@LFCs0 z6N-VPsBXoMEX4z`MahaH`sS?pOu}u`H}R?h%PBeO2i$ZB19;ZgJhQ+~QIkE-L#gV& z_Jh_9Rp2(<0A`HwQL9;F3(E|S4t@DI0ZpGwv>9Ws)M;xa1Y^K3xAy>U3a6kT^}Opz z^461ajWdPwkfmTk@FL`MFGp{?1WN?F=Ih3pCfH1)XawPZhDr$-stpS-jg>rU(~AF)>S=Pg8GiRyg7Bjrcj+Ew&ATrGOf|)8_bTpmG_z zvbT6~OiB>{nW{R!=S5y>(9SyP3CuV|NvQRhx!0`NCM7k8%5*z$vE*DR)~0lCwxe3f zo^UzL)>`o5Mwq1T0Z_&Lv3I1bh=bUZXif@D}HVbWqDjpIm;|Eo0P%#72hZ@ag;mdL6ZL3 z)TnW1UBDb0-vs|iLMvSzvU0wPZ#xzo>*@IfATd)`;`!(YQ2^p5P~cpAh@;Ij|G!lS zm`)WoLatf2!kJ2baSiZOm?YeqitQMyWHzZH5ss~P7M7?wC8{8qb9P0~kFT_3Jb&bV ze?f<@Y4dnVWrM`m9qG*eW0CB-Y1kD~NIxfJer+z$xiZY;i69lkLKhlY%fjV_(CVME>$YyF{Biy4eT2OE$S zumO^(SFJ_G%X)ahun+J4{Y*ndwY6QaTZ|(9`9BJQzQ+pO(59$zgJ?rYklo%ft4yY+ z6W;iEtNn}pm4~kd)S*F7!R(KA6lEj&*#^SW`fJqGi(^60Tlc4-ZfiH4gBvk=%xf{W zaUCLc{0d2wqO!7Od{xZ`Tsc1D-JJP8*eTR`zub}6aAjs{3P*UijGZhGkLaSESsdeb zDyLhTK%MTN?sc;N(j686pSwb7$m)209w^TFy^*?}et8=KOnbu>K|CS$BxESAhj$Pg zY2TKa8|~^EUgAL37wdcEomsi~Ce3i_Cfsl1l?D?;N>y3-a9xfDOKlEDnMaVK#WUxk zqPf~)6?#uy`uk~4RhEGO1w8hzbAwca3@^fqC)KegG(7}VDx;Q?$rGo?Gq7^KCwOe& z@b!U)s5s;1Miop)J`__N6BH_Nf=gmmW=7Bq@IUIQ|3i(Q7}8YZb+`U8n)IfjkjCzX zAL4w|M6#mJc*AvVN4zAX%f6cf@-9fA?4F^9$U_n~#q)Ix$KZx|ljCq+Ii)akUqire zr!oOyG~%TVnxZ|TI=inY^Zk}*88TbW;c#2+M*slrO)U!f8P`~u_x!q=v7=qSm;fnXGpS2RuhB``S+ztDFuV#} zfTwlrF+)=^=qNDhtoDPlT~!E;v6`Yzz1wHETJ9YNIPS3E^G6y@q2${$L2Tj2?-#hq zjG9sxi~=EPRF|usRr_%)qI>Vi9@0)NP>I#%M*EW6BJE8LYm0Vn@^EV>4zR`Tm>Wf@ z0e(=GK`3e2n+H!6WxHs**%e&$9%PLFv5O_X=z1kKT-3uxRj5kbt8Vusv%ZcVlAZ}d zA5CDTjju{eLd=fwkuyC@en0!pInfgPHfg~E`3;RT_$1#QL zhDQ#XQomk=(jSF2(m6T`^5Z z>`djT*oNwAO8|oiU!oo3NViuArImSY^f+m{ zpf(C;n^s2dYH+yEacbxlBa`i7*3^?#)v`&}NE0D>DnW}>=@?q$yy7yaR~mS=4AzWN zJ*mu()vS7tZb4$z1f4ItB9K*?Lw@BF@wrUNNMRbmCVnU=6-|d}OxJanbEVxuO4P1* z#*^^H$#|$epKLVGk)*JCv)p_G8}{C0f92Pj@dx+nf7Q8htKSJD-XfgEWhaGvMH?L- zP88n3zqp}y)hez^C2Ovwf~p78hhN(N(5&TVxEJ89vUE9e<}+JXdcg-Ub_;{%I~s)3 zQ?Py#`?hk;nqaISw47On8|6lGe^Iv@a*yyQ`$!girANBc*~{!Z=iYSjT#Y>)07>n1 zk<+h~YoA{xH##hs4G)iao4!k!ZuNzD&NXVOCGp18!gT9Sc}uus>=U=Xu*Gi?-fst;S^6UiRMj$h8It)g-58YE!3jH8=EYu6%bE!~W*66Ls3*PY*gx7W3#UHPc2QoZ!Kc zy=%V&cVUIbw72E%+=3zNPrv!2euok#kYeL=AyfW@Bil-YDw87vC5AtfsALbg5j?ba zK+0#J{&}5x5@bX0*yY})H&{TvK=J!*Oz4XN8rK((U7VfO9zIOg%Uq)f{7`RJmA=0A zB9@kcf7Dgr{YHbWHC?hbw~bzjl%29sf%^Nn`HO;}jE4TdN{N!Cm2Ep zB>{JDByBHHqx*|c=^kxnt(SsLY%Bxm<-`j2ClOYmg-|RlpWy6D`;j8jv8ARt(zt~^ zJ-A@Ta6)Q%B5(Y^sU~gdZO2GEhV%=yti7zv%rbnbX)teNkLU>hhH6CarPm(0QSB07 zm?#F%qq-3p{iY>Qw1cCgI#=1Q#4-|_JzzzU~f(Bi1pbro7b77zINlTTK)1IxAF zI($9pXAZ8&#b-OZ3~!6JL2c7L+Oc9llTA_Ye(BenxK$7*QDjZ2NOYt z{u+mp(vu-ac|<}$51Z(tyFMp{ER`GhwT}&;1v4$}QN}UU{ArH#6<{Kp!lEppErt-E z?l|$y9pTaOm9B^+_Tj!rwTTdCeVU9F38d~Drs55m4ap7q2wSc0+F-EqxBPBo3mYqbBxB%O;i4 z5M*5l9;YNKDO+YWKare!{#q6zIXa#n&DhmKRIUBr!A_nhC;vB&y2nh`QcT9Pp2bS# z(GTmrLKAa}{G0SuZfs*_d8UH35GfMG=U$^-`0-O?)9E{A`KT3?vVzdJ z>GJYbZ?#Va)hdXy#Mg?=Ac0NeDRD;qr~K^dmW`p&{9XeE6(sQmE6g{F)3_T=5;`!T z_T_P*?HcK{`sblv9wg(Kr=H}Cvn`43NuUKn=C$TF{9P>Jc^rWY!~J#x z0#0l7Vva3D+>?Pitb;7maYN9xY`;85g#=Mn18SjA9xrzZx0c@kB$nEOmxccx zO;sH~JztWL*b|IpyFl)aUpT5mFNV|N-doqL#})@boh_Sp2hPt*c+cz&=}js%E?({{ z#RYa=4n5Zrm3&c5A=s>%CvklXjAsB5!f>xToR2vP>0nS6C+DRpU$@MF#CWB#^I7?l zSiFL%<~gVyrh5hOm!vgvcTE5xKR;pb3=YLqT_k!mZ{2pEX)A8C2x`v-P->cfuSH+0 zgI4UUwQ$a5YrKh_29tTF=!#$^C_P(QDLi{%WMx|va_pH>OeaKYBi z_wtyEI^Vh7rp+S_A%H+%C{```MGv9x-v(mQX;P_JYh$?>_(LyGcX3DQ@bb=gYDpq9 zd1GwxGI|(lQ>blx;5YbruMm>L`Xr>%ssR_)yuP<|5SAB;l#~LV>~pLoz73!r6DupP zkPPqP)*XF=^05~fNI-iFOw27nf!^{&i9-#kYQNhm8icIN-PI1pO<=#3%4^9Vrm&!f zhQf2Mrqey($9Lh&tqlK@xN*gjT}sLvpfKm}hDsAKUn}Dq*{^smjJ*s6UTQOWFp2#K z^)l3fuN+qUhH=}*9VGxRE6fxB9;g8)*b;(@?neV4(V>YAZ;ij>j^zeMZm0I^yUhuZGH zmWum59yEGyJ+i}BTKQWK$*ZXA-YWoyJvQj4yvdTw=rJ}X?@z?PkmJ?^oJ^Tf8UmsF zX;3jV)eG96aG<5fKJV!cz5{r zAVfq~)Y8vJOmM|~lvZR(rY%UG+}lx+xjapY%BW(|*!)}3?XK)}d#Iy0k1vWm@4N_f z#Ul^;ZJ`f776-2`o%e|YcA{hwny$5_+SzNIVL9s)o-lNbhZb4Dxz2Ez!}!Wd(p~AD z=Q$yRJU}c)``pT5zr3P32Wqelk+<+EiE#`*MfZD z1n1nsBUyoIh}+nnfpg}%ZK$0+W%O6TDSF}x$3cR3MSVIy)8o$I!6kQP*X~gryK-r) zsM;q@)wzyFx+1#IBHn{QohoS@rs;I)Ct2`AhM>tpKSEVI!04YhX%pf{09AqS@@IM% z8<1lAR}=k*$(h;NLrZD3?;%wTb-suMQ3v%2k^LB6%mZ$TLDC!ZEa%RpIVCLF$HWUC_ZN~h9zJ}Snf+sEB)WY6KKjz_ z(6f4J=JGY&C&9igwFv_tX+ZQ5rKqTAJ{JHq2Q5=r{X$(GL3~gCzF`@ILfg@`2*ESq zQK>ib5|@k%K^TGAdgnU3GMtw-{wsN}ZbdV_@mA5(PQ5m+^e&Y5b^SR*625YvBf5C+ z$=d!$=Oo^3WwVB$hIl!g(|=UJ#Aw?|ZxC)G7o?L*O%zNIXgjD~=lqJUx$^uiL;Z`& zBofrt^Fl%`nkJQrS~;FCQC5@=M#kHZ6+c2wxpWfDW`?~Z=*EVBpQ-+=AoGs8VL635 zOzW6;RXld6wp`pS;n0++XUTJ`bdhdgS1{PM2J-6Q2!#$*yz<8(BIClnPGZlHq{bCQ zu{}AFM)ApY9N-I2;-rBMjK8nU*XuQ_v;Dbrz3hxj#f+frz$)FjGzgkNd5UcVrqhkp zXb&z5hcZ_tMI?rC(`82T!X_ zZpn4O96UQjKOkD>#h!cmW%MP|t<(De*9%lTV_kUl38K15iLa#XF_uD8M92TB%bce51 zP3n`_z}xnWIH6tF@VX1##<$lc00h5^Kii_d-b^@w5>BBo!+moP*swNm3x{H|2xU}f zOVF(h9xW~HfdbXs^{-!L{+LYJirkYNgiy2g`mO^}Z15gkJN2a3b4~k`!1hkgyL#Pz zg%(0VtdDt_F8&mrUb@x|i=r%63Fu;|4ow#BVLSi7itP!5UH*(gdK=01wmCsP-gMeL zX|2qq8YK)0I>BRo-Ue)@V6Nx*#k014!nG$sH=4=o$9%N9{%F(!&j>V~@JpiKo%Tg+ zN1?aECDW447LOD(en5BJSz8kHcWDrXco0*%7e320N&FZ4udJJKGcr3M@n6P~% zd868MjO1QebOY4##Uw}5S-qcGZTq zat+aOGZ&v%(S=us^+{Rr>&!o{*1TA5#Dx1(%l%kq5Jt0DKx$}In#?))w;B9IOT{o_ znKInWO<-4jWb1q_5_e3YPM4lyqFrl})?Aw_Y3Y&Zuuh6=;TxTDt!gn47Ap)_j*&^^ zNh_{$3#oFmjui@vvt*=~LZCD_gsFg|53msCWRQETkljogc3Pii)8DmizZu77WHgAd zwO8~mZ?!VKjm6;cP1)h^tpv`kLwKqNDIV&jdBP~|K4GHm8}IIz%Xeb;KFtntBe6%y zySnU_z8NRu=@ge4Lcz-E+tAXqcfPx!RhdS&_}26o&yM&?OoA^25{9U(RF*YFwH|0` zYh(Jds!-`2Cmeu$3u@iTyepd}=6UK5zm(0)$j}qm)2ASz?;11uSfANA6pgM7W}}M! zs%d(&$n0%gzreovDjRI~R5!BFN*;MXLBhfnS(V0BB~gnEqEpMCwJv||rK>d@Z^#b( z&~|nAU1KQxtV3?NGI#3!8|Ereh%pZnXW-NFzYDW>J{$Mi!AnIUsEU9ES15c$TW0xn zPT=~PFCp~Id1~8GYHdu3UCsHWK_;i+6{yIg^N{hZsV&=x@vwrTyuNm_Ykf3v%>c!V;?EP9B>cN}c>qu-9*mB)Mw)J97sfF#k4;V%adMT407SNilR98c zGXn(GrogqUQGY0Z~qsn+KWXuIs-`mO`8DHvu79PwixSI7R6{6NN zj3V`gd{(X%+R|NBf+m!C{#91gX?kfQqvy#o14XiZ&r7awZ{bPfTj=<{AjhC(cO%q4 zqf~uZq~XUV7qr~~9=dQ=FO`(W8RV@&!vl+JIzO6dyBz~3F?tvVj@j8z`rK{kU#rbCEg4s-R^G1(w{5MsVHSqr!f&)raA|$Jh z_&2^rC9*3tH}gQXeBUUD3QarAXnm*Qd^9Na$#_ilfnpMup1e^ILTh}=IN-`wds*Wz zNMv?g2MtsEYmIlBmI_fKi;z%6Ha4py+5MWCG38Jz$N^%Q_R(r#tj}BB+-A%-#BE>c zrP*z2Yi<)0u5Dyjowd&#WiXE~cUyxH=8Fa99v#X+OBaZee-bGF1bWWKF~!y&2;ND{ z$+3AkY0DB^;pefGf4K+OZeOhGpcA=!dMB9(L9m>wTj5Sa6NhEOlCF$?pGxOO!asHl z`6RQBCgiw6Np9BR9Q~81022keWr;}{etCg?ANU}jzMszw-@Xrmg7;GN>j6bA0Fz8) zin^k69n_j}4t>0lU5JpouAoJ-sNFpL@XO5>_*F`jM2VKg;m6k3>&Q9bB1n_Ebt@+Q(eb}h{(Q?9etTm1r zC?0^4LLk{$f-!}B_GM!2T;8)e1F%W##}qT9uRYBhGq^`39Ua}t6f{KHG30m-2fjYh zx(GZQU zdLB2vItBMP#kQASY1(FHRbO6Q4Q{SQ9 zpkK)UcFVvJ)KUMlqT0&PnAf+V^_SJ?tREsQ=spMY0;qcZ`znt4$hL%MyuGeWzC z+P%LX?Qd>bi#>koQ-lAr27u)so+ex#aU0nsvKA+)wR0nDn&wNEnvY~ES$fQ}vkjhb z#jrw{{L|NT7jf2f%pE(=N&1HY#v@(D^qL8k5>U=Wam?#+;%`d#=VV6=blhg1eNal~ zmOk2D_>dd6M^5Z)mFwwx`#9E{!IEey&6yF6yrDddsQNq6PDnc#$r}I^ zO{=-*?0B>_zL<_~!%zjEeko{WfkK4PQ^P+wUOwvu`kwGbeAU@EYcC;bET$CqxHPY7 zW@e_s@cltkG2XJ^n$tR178MaYWgW%t1(Z%(`-JP^p!JWr`2ek%p^RXvLb}Ig84)H- z+Kl%?buV(Ow)a0=OuO{go)%#fT8jQXd6}%IUH@_8s)~9dvJ!5(=pRBe>8^=?BlpqZ zGO?Y+*!Wdz#M2cZ3uXtt)Fbs0raf zckr(I-~C{kUcP`(xfG`Fnl^cdE|bAS+BM!CdCE-&7tE@Iu=_D-)Y91>x=Qg{o1(AVmnvF6c2tZQ}iQH-{|)wSRQq@eQ;_LstyrgbXmRv};i z)0>u6b!?Z*dfyq5JqsZe$_FLCmAQD0#U(m8ZXCUMetV`=BX)39-eVY3kdqLow_{!= zV{gPn>`#@H4@AZd4b@MIx9{$HqRa_fl^vmTNaA(TMwmzIg##g0o}fX%5Ux>DgaV5H zC_Y~didV0QT?^r56tdPZ0~+dfwKq1N`PGPOoaRV0I zpEp=8@D)97-MLiOZInC=9}9rz(Z#}0{~Z6&qOA_QuWQcNSYZ~nn)BCFr#1Xn2kXl{ z3N!v7MB{O@$Axc`1?P0GDK^f}IB&ch#5u>OpJqC8~dcKJ|_{N_lZ{SI#W6NNn{i&bsws^Iolo`zih)U`4_4ca$l@hz8v z_(jpB@rQI`JiA)FU$qihO*MR*zugd-#uWWWQ<|Q>cCMjN zRqx4;`RWK2OW@d#tviwHrt>8~tlaC2)ZIGMW|uj@&rXq^=C38gLtb6H27a(P;)xSPSX6FL-; z9TpqV7sAdU8%3eZi}bB6ZE$(x{&06x!pN^JfSQtWtff%bjbm-zo?^$O;;O<^z&7<$D`UPMy}pdZS~4SIrZ^ z8`4XMDEJ;#>t+{)?{q$f#x$kDPUuv2X9=8;jX4nQz&vmTACDVA931N<;6LFw1RM#w z-d!N=xddO#aI-w@&kVNj6rE~2jp1r|{oASbooC0O%<%0FL5#x`^EZU0f$)^s?uUPq zi8l&}!Ds%Sr%fH$O2Cz3G(2Kw3ACpbL0vpcc88|#*7UchWh;113_E3l8gFjcXplwa=aD%=~8r0yP?_jz@8d07Z6U%V6A)xI$=*?l6MCta%ax#>L| z+eUj01Y`1*dlF7P^VTNb22%mSmfjWC$;p4Si-{;g)wv7W(*kwUd+c@SLqF_s%Kyvg zbdI5NMgEfqa!lKfXiNXCXL&yPDu(0}^JvYHKmDtH5Nmb7dUaAN-?z4Q3pp|pA*=$M zdVu4PsR_G|{(yA}>va6g=>NmGh)B|O^=PaHV=ybeEud*!cJ84hy&+oLx&HX0$` zWjgs?jc@FUegW@((G24DSG{#)20NxZL+avo_Kb2ou2Ga6jIkk}d&3g1lW+Di9Le-^ z1xt9}Tx{;6B$72%r4;-uQ?1?ePna&6-CQZ->hOEk+bpB#W@;L{c7hxuaU?lHBYCL( z9y5<75q;!~hUr_PbT^cr-gZ&F;qt7K7_;}IpJGIon7R!wHR($J-6=|%fUv(Gsro%D zZgv$mQ3;tADzYS$crc@(-T#QVeA zB24d2Y4+8VNf+|j_nvobf6*W|FFET5PSu<<-~0L9F3kf;d9+`w8#28EAI~lQ#){8> z_r!Ku9wC)76wM=Oo(=2mG-5mgmXGsDiJZjeWWO_qK1DhZn}H-gz(X5Bk2_}SxH8Xh z9Kkj>PP6743K{RcUyA(qRZG=szgx1*$ZKFig=4NiWEWz+3akyLMz@)W23J)6LdqIs zu5l2zb6sJ;cy(eX?j0$XpVO$T@nA?(dv%Gh`cPuGq162&D?gWZH-8eK)uu@HkcE6& zs6EN21~b#u@>8e=>6f?SPwvI*6YdzQdbV%cdf9|)OKllmO+i7l3`po3t*PH-BY8*D z*|cnLi_hSkIeW?lcJF4(9?{p&@c^A#^B+%eB5>hS%;Z7}~T>JF=N)AN=pk+ zuU>pIWwsDIa3mOw`ZPrmxqjzi7);%&G|&`jdiH=!t?y?q$my{3G#&IVM?RwEn)eY79n)$%*VHTM1u}OHf1xfVN;(n8dt}kGyN80 zO?E8UVf5!7jM7@1%?RRG5>~m%b-&|TKG|p>m+)^R$-{5$yOnO|o#$#E?sjY8XPT6! zIYW0{IEKP3VGNLG(i{H&T6X<2aHsJV`@~lE2@{W7_nkx;eps9IB)*aVl6!hmY6J4P z8x;F}{uvC82)ip_dz#sxDvA&A}`3uijYhL>D+d5mdbrddLiMKQ(<#PS(Yt-#{1L zO!ECc|19-7s?qqw=*5#X$a1O zrGhH^p+CQ!N_qPF`+(4%-2eo`;yB7rfx&a>k5K3U;&7D(PX^nmPyU|V`KOgs=XRNJ zk}4g!p7c?F;z?EyJzA}3%uTe$W7iD86*rctO51OkzX_54nGmvO8*;?=wv!5F8Qb9M zps1rd5keshHuQ1ZX=;1xJVr+HJelF=WxJT)V)c!U?2m$0=WX;W#`1(-vGmqgpYP0( z@)9e2zC+u{m+h<8w{$^l;}Z4z^N8lXdFq~NQ-vg!v`M>YH{WMYx9%{*((isvct?c( zbe}@w@|o6AFmWQ;4Ob|SjPTPW=0KL@xr_mUua=3-Nb-~P;KD~e-$VBIDtQ-t*+{RR zixueeOAC{_z%@m;Uhu7DnnuqKP3`*SRLGrSab+3K zA!o3q&KB_1>KG{sa4TLx_X)=J<9?uf6+2h=eOm4}dlJV(rX$$gOC|n=;k{Psa?uy{ z(etNKKSb+@@2o&!9nmZoY>+f2O2IlT=2)rb3TMud&2G zGI9r+o~hM!{V1j071XB;$sb6XZS1;;S08U+G%@``wrfNYywl1cciE!7nq^HmCA(*{ zFl2)ue>p$vaku1J^UA-P0}qdKC9PxBJAsttStm zw$qECR!S$jX{ag3cU9sXQ&aO|-g{0U0O+M3H=R_!aywp^KBpA|a|lWhZtk>Vs+`2K zEZ^_3#NTVZmtDs(FR%ZWsX$C6%z*-fs_Jgp$c+?-9||bR65F>luf!lO0!KsTz$a`h zy_0LV`uaG()AoQhrJr(UJiOP?w7>Et%LIo~8-(Z9+me*j~quM$CBG&OntZ4^z8~D`3_|XGVRN}V?&aV3>w#zDaGh-c# z7GgeR-$>vvLeMD0vU z6P%epomH8r=Z&`UZjw11blq%DeMMVGcu1tbOGDkf@+v#vaQ{W5)RT!jIu{*RcO+Y& zPg6pXoS#`e-fZ}*(S1<8s{5WULx>@Z^P?DkPa9T8=+LZVC}Vi`^v_2D1>Mq=R3%sw znxEV!L&5dwbN*}*-ameJD>S!YFK%w0Y{2bA$W$))!9t=e8ecX0?CX997k|&bXoHR4P!BV;Sw=fyp^P6^Fy9zCMR= z5usCLN$7JD!L89*{EtJxFMOwMthAr1lHJ4ZhZ2s2Li!#%zH74dSAs0iNx6P?>jmRa zju>Ho;WVv4Dqmwl^bA>P9c={poGXy>;xViD zsWo2^-in^@hJsUlrU}e}33Q0CULoKSq?hqtKhAeOL5NZCBE=zW>H02gzmad8Un$Mr zDlwRtw%JzpxH+NVh5hoG@1nx5jiW8QZ#>uHt0P^t$PvA72?;jd-VZ#3=jILzS*|`X zoQD3lE}3;669MbAdf^{I@XtZID_Pw{#QZ_PlWJq(>VGkd?2=Sj|HMakjgqbj^^ParDdu zq!rb3=qRcF-WnanhnldhF|Iae>SEFHLvrtpzq{G-_Ov*yLW zC`h8^MU%@xoxqs;8e^z38pQ&%{U#lnfN*7Z%>*6Mf(-_s8enSQM8M|1(yvQuf!3AJY_hKDV?~ z5{1PQKri4Iah?&ZpMxrI2e@-qfH^LVc%zFTtmcX5ty=Jdv~90WO5vA7Lz=6W`=RvL z=h4g1*{|VR-F&gRy@cmQergKw&}*a33QB|kbDp($Ig#)<@PCL04S@Vm{<2Kl7Feyy znI(JMUEHqL%-TXnC)-Hvk0v+68k6eHs4C{11waRp`>Q}7g&ya=K5QWT0qzc#nu)j4 z`3r7X3oNW&CeQXXL^V)DkLFqVYQ%JE(Xi1&z=zQ--f^rIa*SDeL~e=UlE2`|3pwJM zZVMC^b2CZsgXBQ{-Y;;Gby=wHfE+3OLk)uN3ZBHJ=gY3^nNBJmeh~>j7AZ2lqx|XX zSMs)9chBww(7|dVch((R-fx`$ckffoSHD0*@wqw-#195XuWyYVzkcEfwFnr@f}rMH z2aIF8F=J>n7ml@s1Hq9khetC+G#!0uKByfA!K=$h#+Y*nmC-_{BB znD^PQ2&X1#x-EC`r)R(CU7xUwq>kJaeAY)6#(Q_!u!!nL%l)6JB~A^Q&nMRK?cR(L z78ZOGfh?kA5_M(s+cig0C*bqhL_UfXI-v(FtSIH%I`=)E_v)9B)VKlfkcJ>EmVe0RElbJyGJdaq&5A_o!nWRE70E?rTo{L zLh*KXSIof?^{+1P;-(^oPJe_sJ*1z3?fT!9t#4$^OR)Y)l%iz^T0MJ0C?xT}x)d~{k#)&BMeb`Z^S zH{Cqf-aE42mkDo36!uBI(=Er8HY@n5A&=;07i2xTXZ!I>X{<+8bS)cpzGuD&r~sPd zBllG}Kw3|WVPgOfTU3ISm(N1_z#qD>P6rIKtsL;0lu?d_<1Jhd_=&xTY#}y{0`(yV zv2w0zQ|uubZkVtP@i2)-o16C>sVObGvn`G&Mhu-SP`dZ>XmTGgZV1TgY}7Ux(7Z(6 zQn^Tq^+yjy?Hfn_>dR}Vh^OI@ z7|7@t-@vXQ?uV5BQy|R37h6bM}ls_4cq&fDFl=B7CEITJ|%*!v_&466F-STtb^m$J3k-vLA zzY`i^c|K)GDOzy#SQev2i8CLII7BLF|#Hr#^TH}C3ky$ABc62%Tovbt=; zCi~rooun8f`>e~2ki0XZE@K*lD(^aW{uo{-dcWdVRM=|NEPip4e#^WFs2Xu+tFgum zJi+3zk66a(Q5m#Lalq~J@E}Z-b$A8%x~E*-|E$=8n596h4$F2D%^dtsC36{;bG}3A z-!61A;=4{6F*g<{oD@IjGY-79q%ws-h)c~IkhuCDE+K90Q2g9;A|~cBkb88jE4p2v zC|Vv_-AvSz;!z(rctJf_ufujrHD(Dbbv*yr(X53-mQxv~?BOa(D)zB+@lA*h@K`_p z4_27iDy)lM3Dg)(htP;LAs%$7=n@JAY#Jt@lp;~2AOzsLLnZ=`)}wJ*_XR;yBZ zkPlN6g4{55I#hjhwRYcK8`vAhG?$D*^qoqLYiQ{aN8d2ygjs|n3ybZupdr8epk!{7 zn#P`eklR@$d8VYg3H`={!b82yt3k_afLC13|RwE#QC6OWPt|Bg}#Qo|kiv zVv|GUUxH{izy5GHl+4f_uRCVF-h3KG=LFvvh@JfrLu=ElV{g$GECjzuaQufnL}eyP zUeCNcZ1UA#A}>6v=XiA4d#jhXzXL@cHJR`YtC@blt)L*2meK+S{vQ6rHFj`3br4h$ zp8SLJYDWt61s@uD@*~DVkmI06KmIEp6Al6|i3;Kh!~(7wp?%9wj0(7hUC!v~4WTgC zgvgZI`U(Nd%d*_@n-wzkIHLr!B>F>x8P6$L<><1ceX!E06Y|Pl>sktLyIaboY0B(k zb8<)np1Ze30l1g*tY?AiQ)RWiEu%M9?cKTOt|sr#z6+T;HYdNCVc||v^kL44m}}mE z0~KgYLi1$=GkCcd#ld+_%)@mLI?6P{E-LoDufa-}M1FWAY!4dz6V#NX&X z5c64k!6rJ{hIDLf>^e&dN@^{qLCGzs_(+#SFZ1p0gKuo>@y#kP|bg zaQcgrZv|!p4}+Dpp~m~7--wBpGKJe?E8Y`7zbaiBrIPS%${zfYu#}!Y#vbO8AyErE ze;<6{>JXM5`*va_1)c}q_U+p*t8ejFcH#7R4c-jtp{-xePr5)xZOw}g;hGs>*~4}b zM&7`nO8>M;DJ*Y*rsThDZHdd_z3VA3<_1NNNC1$SbY}Tu-$#T`^IP?q$-y{}Gv=K? z`*#T(@Gi=E-y4Wcg)E}R?d~X`2Bk>a&a%BTIFO~Yy~6AMU%`_U)59Z7=b{ngF z3qLQkqKA?3CSTB>&}6DhXcqSN)2Xv`uVAK>%c_TWx+uP(6)#EmWs{hR|EgX24htbQ z76*6A!B7K|HbXzB0|CMhf@aR6Xoo+Z0qE5S(?*xbSlG60aIw7!&u)MHo7^J#$4FrJ z?)*%VBFuEWbGY!`ar~`w4V_zQXR|0)0>(Rg^fSMjH0=FJXy2}sdbCr&#y}_xxCl~> zHu8@)M9<%j>_iKpU@Y-QF!c~LsD0G2wTmSS`9jdl!HzOHZSM7i@6Zn#a6S*ZJLmAV zHHUO~#>dcwkU}4eOp#z9o)a#P8a6Lp25+gs=R4F1a?&_w_T)ZEOjOXs?zobn_vPsT6w z|1~XvDrJ{jviND)u@I&H>nNHPND=S+X`K0Z(5onTN3Bojb>O-YC|D|rTSRFK37cxZ z9QDbh@NV`cTzi&VU)~bGwjsG3w_bk;l4m3%hv$NlKjcn+lbK-)vtm3R)}3ik0=v12 zU&ka>?ooS-Uo1BxzGIYcbrG8e3=lg(b5xysMJ?4oY6X62G$iZ*g4}5pdni;&aYXx`&JO5hgJfpOUQ0N%^9o9a@&{j| zL+-*rx~f~Emk{&^dg><@rZHwWSuNs?V*Guy&w;eeSJ_hX8m$P*sWh5sG4BRoGRZJG z-jHP%nGOsM!2umIO&uN4<)e&ccDdaRi^&Xc$g9^)7gEaYJyAAN!Ez0BHfvs6;8*PJ zL{)P3zzzVLMaqCJ2ENe8kqhw$A1l7?s7(Z=lwyVjtQiXuL0X3T>G@khJS)VU0}34{ zeK?&%;Z|7Cv>}rb4rd$6O>b7-FD7BtIQ>Vy6h6L!_NPuxAvUB}1f-l^kol1Latj(F*=I=5bY8X8FtxiAn%KO=0o4izKdvk$yp-wx6C8Asb-WGC&(VhAhrzt4aVU6X| zOvRDN;wIJJA-1)F=I?=wkIl613J?Eh)T>_~#qIYjsULX`4~aDzk`N!e^DpD3*zqFr zijFdz$FthBIk-5?KozRX0pmpLO?LS6I^^BwUF7%cf!Jr=GeOJ2{Vz?Iea{v%68A&( z-4~Gtb&-H>jI@2z4PDT=4<19BQMu_|Q=0e(8foHa=N|Mu=URHS z*~CPg^$P8{Jxo8OGX&y%Q#3#|Vfy(4B}_0ATNER(F~88$Sn$5(QGQ50rl+iwDQVH$ z{PzcEYNLX!*ic@iTo7nt7HJXHf5R2I@OG(aw>TlMxI&Od9xY-dKHyePF)O+4t#YdV zLd@em3RAcE<4|Y>W@;VnSEWh{l2unOT2+1lqAVT}G2`fOpjlgo*d8A9MiC5b1ljr1 z8+x{h;EXJE*vG+f(F-+%pn(LpKGl?Ax~9{@6*4qAg*8#upSjBz9#j513P*&e;l`dp z6F&Ya#;Gtpuk-H2-lL7n!9%e&i(h9B zeewv8BdyH(97xT4oijJ*q|F?|dRpV;n-7sx3?;=YD|v(hUyFOO`bFODHDKR&5f04Q zrPw^qar5QTOGbuqtq^?Y?WamdVDq*~I^hOFHRY3^Qu{xCsM%2X^A2?Bb1m^90+yzF zamtrSKXY&c%QXMxxYaZ663ZinVmCNi5@;j~HJOw@5=-P7M9)~@s zFA&7J$7N?zjLQXLJW~YVB#PdAqVNERH@WVeFFS7qUJoE|^b7O`znq#r3iZEo{=IWG zx&@*4JA-I zD1LA{nWoni;)RNC` z3XJ}X_Tm?=N#nrZQq{=qM|stfz1ego(Q}dt~ z@~v&^m#cBTevltvFE_PQ&VEt`XevBlGVCn;+#D4nN_N1O#)f0$cy7H-b}uTAnQ7Su;6nm8Q`GOY*+&659`k{{g~Y2^3{w~EAz{=At9$8PH10` z2F-|JkBjfqZqOT|dg#<=S96<>>%#H|cFBv!~?T@+)6-uxOf-&a{pQ zRhJ=rhaEomxz+75oxD1=Eb{u=m8Cq{dKD*GC^frj0=I6)%V~(6phQ3wbRAY`gtxFK zlC?J1KpFx^z?!V}snzh4kReURjI?w6n$m}`vP=^A@IMC`T2)Jbl|BRF^sVv{OUsf? zwyG(tjq&2EBG<@kfURqDrJsg8&bSV>s2#IT9qi6eat%TtaDug_;a-{Kge%W*3gy_V z^rZbZ4#hVg-Os;AjCLaZsWXmKqyj5ZJV#r)4!|N?6a|7WYUHWCmd!nu&7*RjYfOCK zKyQfqhp>Zg1C4xkdR+F%~n4j{W`wkx4!d0JL@c6|Y#9g`)Qx*Db;mlmc_Ky%Bb{v<9pEX;|Kj;%ajK zTGxm@hqa^)H;44Ij*b{Tw^FIpE)#j8uLD-3OS!l8trfoAOaXr~!vuf>{tl;_6zDep z=rc}9KFLt|bLHKP@n%<-qbBd0H!_tyPgX(aH74{7f5U$hp|IlnXAttA_T42o@BL8s z<)e0+knsCv3A9cTLW8LvaQ@Wlhu0b>LxZ}b1#?M;iInl-(^-JqeWx&rD)Oj*AF($Y z5rvE~zB`R#BP`>j6gCA^@HOEsJPca!D>4Wx_NYto zLOTTx{*yqSk|8IcpV^fJ_i6I!a_`<4 ziXb-rB31NE4BVkE8<cJ26PFo&gz}~M(O^$5KDhyh%#KiTx%X|G&8?9~&*G?++ zc%36F1jBI|a?FP$F;5-cOopTs6em`~lF}UJny&B4>7j*NLrzO?DgswL=TQ7cD5=1E z6*Y>=jRdep@JjF}AXEA>0A^e&fDDJlH9v(=)b|~J4uJaGZjYTh4QN@S0)}MD++Rc$ zwzfB2Q%~FGrHo>CH5?gDQ5;=-ibs*%^7Q;7pA9`Iw5Qh9Kd3>3@>KtQ;^$qh%ST+^ zqklLJT}J{F2f`D@@x&aLQSJXsl`MX?tX>@1diBB$@;u~j0#xlB99;i$*+!Q~5I!r1 z)#?#$A2xkD8Y$`HYKXwS86G`Vri6lf`6HcRgrArCr!k{PX+Jx;Utw)<7M_H)r4^&l z^un>u&}T4wD(4h9%3HSG3Us**r;Z($diA0BQ+nM)eAc}s`Ta6zW*FV%Rvdt1pnEd` z_wF70yDB_l-AGe8{Aw-bgqd-rTnO_kO|g$|R-_e7mDdRk!RRwSWBk5giEkePdi?D8 zKbD+C1ac|j_&V8&TES#~*A7?yyO^|H>Cl9|Yp9pv%Q2_!He2c-k<=!NjFUNA0t}xC zbSX>wC!5{JQ;}E@TF}vV2-)g4}Z^ zAmJFin^yEnF!NsBJL6)H8cHJQyb%wm;OHD}rtUB`cO08YP2kF&Mkh@uf6O8H;rk*23 z8(-)Utt_w4i2^%5^4|*aVjETm{Lr^_#RZ7_O>ieQ)pN)AZsRcaxOtQ((}<1DjiAe| zrsB?VX(HbYK=KB>US2|krXklk%RQd_!2h1i8yb7t$4C4wEPMUH0+qNGUN1pb*Fqao zrV8fkZmCx+Z->pP)5s3YWKbQxEmIwaPUXq>-bET|Mx+%red<4N&MTAG_EFacikGzE z9uX47P3YcMkR*)yYE(3in{@A)V+SJ?LiU+=xBV&Bc~7E%ZW^DnQfhb@KHGHya->`RQzhq(a%YkNAHcVYp?JqQA?%5{Y&5?u78J%pEktl0&#%yHGJNj6o(Oc9F69B2hhHP*q;mzc&jFi8QfpSxcfRHD2sQ<%^hpLc- z)K&63r51I{QJZixQQFu67@d#Na=QVdATyC7QGKBGOv>< zi7S4Jrjts`oWW2R3!mxU!OU*V6qNCpnK5cI9*b+>2P>=KR^T_7E?DIf%&H9rixGFM z1!|g4kkGKMc8Xm%8oj1w^-=Uc95lF`FD_Yr{ygsx*BJ1+QcgsOqAxZ&@_-O@2WU=T zE`6mopI^Zq7(10x&!oZ7cxnq98zeiiU)U;dzK&_FE9(_Kz}ylH{mK*E(`PP>MwYrA zl&BQkV*7FLexQfSjrSOh3F{TtW<65g{ez`c|8@_!c7(lmW5a9<)zjN4uDN#Q($vZb zGILdw3|X<(q0-{doioGyYCfy6=;v3%m&#GpLF(c_Db=2HFv$L46Z#614tw{c*H;&J z8LCpXsKwM}WN&WsJB_Tq2Z9DU!9Lo>O$vC3ou4P2G=!h!d@ubSL@P+^7(mz0hWNNy ziZjLdM(b98Rj(2yK?ncbEB1K<9OQGzUlc=s;fZj+-U4+GsHm(sj!_SQD|{GX-d^f| zdJ}xJP&Cwe1VQCrXlWFw_D7X6&B_`CTEkA&7-Tly8S3R54)k6Ga*N;nP#>qKcBlA* z4);{=-K(Mt_>=Tyqp18?uq19Z{{3nP&Visjz}MdwVPlC31nNvot*+APxT=LvVRxL@ zK_;yVbLgxX%3MI4?b}gXP+k+)`N^)BZU@MoU4!hZ6QG2A4%TZA;v^s-p-7aCg_h@& z*?cIz`P*&{BBF4=4Zp4s>P1AR_zu02(m)hE`wrM-3cYuOz`fZ0&l{I;mGKO$ZJg;k zxqRt;Hky7sh&t+c|D@tI|EJ=*vTFrq#1G4D5ACKr^+oh7h*oERrE&dup4NNt_pFOH zIp}9p^$1+)hXnj5h-ctTO4kBc_*sjX&bo1+K?c?jln(gQ+f&qQKLVmhDffLjXu|Yd zceMESJNfz72tpSKK24h!J+57U=`V^v-gJ4xRE4F6B8MhTDT0`#GHBtl7d>gkpLAwY z)Hm_C9R>Hg{*&B^dB3l{OnW=h;{hh+EwCA{I)vd_qdLm|g`egKG;WAD3xffJr-cl6 zj*rgzz%kyHed)L-d@^CnLMvo;zI^y)`9VfOHjg)SH`7_XZ0@x9{GWQTx*ovWYxR^| zmbzc_v&dQelFW|^yh|2duLY{e2oYp7oplZHmm|L_(W)9Q`yOFF{9SrQmwMPFE&uAg z?j!ckA2A4M>rw8dRb&Ql*%f9X(I~%k(4#24P=MI>ve%)>m&U+0G>DYL z+sw2U#%;RpoRqpNqZp^?JnpLTW||}B5Yx6%Atq+C(1=Hov+m0vACTcz;!NS|V`wrI z4|w!RdI9arSMR>(f^6e%qyzDw@jPp~>?d(uz{HM{cb7lfFbB8?jBHBq{_gL-W>0Em5Sr66^X!2?DaRxN0a#z=#L^tryLV4V{OPiPG@QGe zq=ofs9YF|2Y-{3_noqq0Rm?8F_2CjxJzX>VeRtLPzWlcYWLVU{Qv*??0RlwCpbXAY z9o5LIDBSTnVmFvQ{Ea6{_a@z7hOaRB=N|iK0nA2^@4q-gMu!r5y!rBE!MspO3#4e4 zESj5T5khfz0$6mzkbVdsM*mOD2Uk!Ju1PpYTP>_}pIMmCw}7FBw+18s(L! za_nRG_vv)+0S&p1ILFY{v4RfG9J>J@My9CnbQz+jidz zMbsI#ZC|VVph-d>xwltlBtyMc?R;!2#a~gAD%Au3YA<_&H+ZC^lH?ej@}j-qZkXny z4=Q2$3~7`GuTg6ayNMDM=$Eug&t;F8*TZtuCS^O;=R z>QRjjjv1$dlK364s}EJEw$p3ItWDGJF(Xr$G%u(V98s`DtO;!>lqaF0gB#a>v`zvboEZ(8i^BL(%{|1Qh9tayoq`zWsaOz zV`pu~&erLu+)grlmKn3mv&wmszlA40=Z6B#mJYrtje#{4OASP&*U={h#2 zIvSV8LpHl8Z#d2b;?@H--IN{Pgc|HQd3Tcs0#m@44;)20Vx9JttkjoP#-Z?KISA+7d@Ob z5DZYW*8`7wL<8u%)T3_25g&3ezBE; zuB_!lmV8FpO=Xh&rYsvz5>>0J4eT^sPZH3bzoa#2>7X6G9<0ME83H`QIqCn-*3gC* z$x+`WxV<~bCe9;nyY&p5yb0b@B6-_oFX_jPU69=eg~zjCuHD&wtw=rL&aHs9$W4n; zN*w!shjn;|7(L!6EAns2vSphSKW!9B^WWeVxRUH7tI$504jZ<}2HCAN?>5wtDAy!B zZ+VDN1wKYBTYtqggTt6+ZkJ7BRCad_OA%^{(2Y>UW4^TukMKOtY);YSUPz84QJscw zJR@w4g` z5^CeY@Q6;57na;fnsD#r7_Lqm?QL-=c&H^*VZx8vk2*_$tKn`!q*{_Ul}wh9pla{tqyx{Zzwf($~zux0wO z44U#%-&3OlEl^=_$r6|ZYqvtu)I@!KzjA?#_a*0t1iK?+39}rPtJ-R#(A|s0W_g3V zt$SC}>qlrqrDFhal%nu?op}4s`C~VR3Lo;}AZ2m)w*qIFT=N z6{*y;hG7s>X#ES&HSa~!>^&@fifXT#7WJV&9Hm?QkP9`QWy?fHtE929BP1M46oncx zi`--$&924(%!*(Ou!NUv>0)$)k933eQccHO-FF0SP8+eMNs>s=*J|iM5b}l4tpK&} zZOUz|E(D$^iwFeY=bx%$8c|xA?QbK^8sU|@J4WuT*EIFA{X^X!?`3JJFH9Y>*1vba z9gKJZ9YD7D{F&^R4Lop04+5sKshJttNv+?hqJmp}s10K0s=}`^NtC2FrhDhkQ0B9y zql-`F=N8@Cay9yuuAd(geq}h>j6Ez1*LOXk0SN2DXAe4HXeH77PfFn6p6{>v(^N8# zM?^*h|9xgn%GF1W-u?i|HZynQZxp^eBI`|H_8-r}>)CoEj_rlu#HP6G-yd!&b4rfI zM3jhX9h!nQ)o7I3bmYiHYNfH8yB&=u2Q#P;GBJtbVA)p!nl?$R;j|E~VpFt`bOub? zH$p34?y#4S?m4T>$woqcqOj3VKK`ZU(QaZJC}Y4GoZ13oJ0yD|hkbIv%3X{)ZEkrh z4O)JT55fP)uGfwFR!#9IImM99MD8l7ib}QU&%=udt?7XxCAcj4#)^qO|r~( z`CIBtz=L@3ly+xw^z|rdM9f%GU(%f-_V(d?__y;iA(k4hdp6gRv8yBkuFFM|rg=1r zWvi~hh?fMv`xX+=CUdPnQ$n98O!3P`_Pf7w*&TU>F`=CN)?1jdzi${&cO)+Txdoki z3ShSvie5Rmx~}M^rl)9B_}!O3XHQG=zw_#qU-Q`@H%;nZx1O;+5T0c1cM_nSHT)1M zXlnC#hhC6L_^~(99Gm!MWUlV;*zfQC%%c?3(7YBrw(s$<3ovyVhmNoh(?(If0}e-i z9w8y*42Io=si;jA{=&d`IpCZs3X~8K7R9m)W$9}2KxK*J4L7uzBf9rV@fb)IVE~Ssb@RmiT0p!1~V?+g=);o4xo&hfm1EuFw z%573ZFeXslRP?n0vZQ+j#0Mz%Twpp60F6Q<>wB}q00tU<+R#rmP6;Lf66a?0TYD~% zyDz%XdyhxCWlEc}PYJb5?*|x{TzP~iHK=R$y6hWOtW5>;O~{Oj?BNE$xdD*$+7tUcVOQFKdJwiSokb= zQezL*YD7sZ>>ca!QjusXL*bAEKV5bscfskb@s^bXZ&2|*Vg)uI!?Nvam2{b{=V49; z&ezTk1gl5)K$OZK?21ZB+xPgz4jm#*+&a++ujj4RqmtKLMV@QJ)FJwMxK_13hY!}= zoV-GOlKO*s5L7Ue5m8X`(b2N7%Oc~pJr{|je|vbzEzVg+q z8+nA1!m-40)P08bvI<^;`V=JX0w+%t8Ux_f`nPMJ!L{@(ex09>t;@e~XDwhVY!JVh zIfnb-5XT>`ZTj*V(9Sw)zdZwL&1vtK1HA7N{|ZjdC@ z?{?&%BHg`<>3Lde`XWzAiz93edG!t+8QPr}Tx`AQ0vn%9lv2rY`^}-B3{$`21cJy- zOQDdz!8`7i!Bm$)TitEtpNy}o6eZB?ZI_A*+PeRuyu@3!Sm0@w&J8T;8cUFvvjYM^ z^#+e3Vu;E-lR1%AJgl>MYTAq=k82QWpbt<6hv}ABsrz z22Mm^R$M+7njhW@j!P)t0FWoOfA8d|AmZc?0iMAGvKoUbtDBjz z{ZOMWF9=7h0K*U3;-{O)xqqu5gt4<}D_+UvzK$E2KZY$@7_RQ6G@FaJ>>#zl$sooR!MI zMH9$VWtsbyf2)8yQ?p*n9Pb}d^2e#&^~G}u``sDN6TRu!HW*WIs(CIfayfF}EBR(A z1-XqYLuI$W2^J6b+y)<++D9ngXBU+{-RIHg2~}OOX2Xq5Z~W+ppkCOR6WcDQ+%W+KjA-Bb&}5#&aV^G;*PhSPoUOt}UXD zQxpF=*CZc=^PUB@wVwXdFB@K435mYTvcP(;SFf8v=Oi(eg!;3em`zj9$cQ4n;~%Po z^&j=v_^Z)6=F!mBcDs!v*UzCuDvnlg*X#s#6MNES1++&>tcMX`#2dDJ+Zxc&9@qXF zJBSwp=6mnP$j1;P`_=7t$F_6Q{s?{_pXEG{#BSieU#G0<55-ez8yd^Jd+~mLX;}OA z3ZHex;E~l)1oT#AfiwhGJPn1gQY>A#Zo&L$S>>*@qcMiG zI>(R+jGk!Hy7EXHc9?yueq&7wROgnkfsvha{EY-*FeYp&N7fM1Nbp%fmWd+`Pd`r$ zZHe#R%sd#Aq)%KeC=Ni2%VgUpV1B|(!+ccq`r*4B!8vS8<1NkNiqfiW?MK>wFFa<< z8VbQ~@L{~)0Bnt)1pv!F&Idavk-fomuWxZX3>}NsL^eQ3#`Ncdi331+rsJz`><8>J z({dRwktPpl&RkDF&^xU35GF~zUJHAXB18QK$DSnvHFM8<pKKmX7Z&25El9g~l;z}*)t}7?})JZac<2K}3)V)sluO1JnC|UAqpIM4@ z#IC8_+Ci)fu8Fuj95C#L0u+VBJi~L<9&eIAhwx;wo0KicCt@$yKJF%ugc``odgT{% zuJ_nMBlP<4zfq4r0F=ZDIe9lUl1=I6F~=*$*E$yL@Y}E=(XaHtL5Kp&60gl?cr$jP@BxkdP*|%s zLnQD?!bj>p57xwCz7l+J(fBOWKimX!Fp@#1%xsxt&s=579y$a|`+e;4q;nVehod^; z)1UDNrly&-l-3-r{0KvWu(C6F)O^12cvO|Z;hgENbAv*FIP*{7mIS)%A3T^GE7SA+ zMN)&U32WEH&j~~|Z@4@-3YGME0LHpl%n8m^s}7C?U>WJdsqUYV(`rMcHv#hG*cy6; zt<*x~@a)ZQU&#oT0dF^??^+o0AVrhvXVNNvmWN6f$J5%spK2!XKWk!Elk`1!Zud@i zk`{n%m}ecZJ|Z2#d1mU!3YylFeQA?XR{l}$&~(}TXtnIT0fa&0P1Ub>A=D%z zdn5Gqc~3LR_m;1m0>f*=QAiYGqF6kq_jgsQq3T%;B`!|I=j?ji@+H!9?Rzh!=f38$ zdy>SgQ{}}-=JwC-6OHA>*qENg>6u@;`!{HfYxUisAAZ$OuzId==2@>CB2_aeyPP_< z7dL$uK+*hnol*Y(>uj5QsSL7c3a>1oOKl0FpaYeiVdRww(Lx=h$cJHgP-&T*x}rM;!a*)KA_%9-PO~lMCe;(=t#J8LVsbOe?$x| zf~yL3nL%$Dk2wmUPDA$;z(*>@bFtd6yatBg-^dLXI;l7D1gOEK9yxnS-3Zh0ZS~_jvDp#BO3XJrEgC2bHUm!gicN5 zgyF3B(imBqP1F1nC6V(&!UTze!9y~{C*H$GchSWsXCE(i8x z51#DY+!6`!IzZ5n%T1r92wPZRO7PzaSpJi}MV>kj0F6$8W_*{?9nXW_mCx%(n|cNY ze;YA1m;)s&(5(5orYm`(kaV7VtGomp(Bi96 zUPQ0^q(&vX!l5e<_U!du?^W}9+TfaE2CM{VD@!xAEO$hgrnrFC`14^4^4;dEDBoM& zz5WRVMk_i3LwMAitu1X+GT15ihH}CdqH{rs-Ck|)1DP?CirhNibszmm`USZl@v{zk zP|?)#Zk9FK&wbo1#K&+yN{_XJZT1<{z?&mAtP-p>R1PtXXZz=l40nZw!1L3_abqt< zG1cy=<+~rhm&FFiRI88emA`AIDu$;A*_O^tJ@kv75r| zPA?drvVmsWM#h_kl8G79r}trLgjyv zsIw|~&XW0uZ&0k&+NM~iVdsgwems*i51GyxM#nXv&l|`p2?{~O?56HE;~`%nA`l%`nmr?E|5>~I<^M~J z`L?&#Z@&j7#vfxwIiF&Du=HPp=jm!=df~jjWL0=wF53NDCqVK^=w`;(cAZ4D<*1Xm zd<=uOKj>%JgLTKXNLipjUvni@4ux`2#tU97SL8u6Ih%L-&Y|0;$(<}p@ zk;|a)D3c)4?21_pPM+mLNAxDkHk{RqwaXMJ>S8#Y_(nrBMNx1Q z<=Vkw)*Od#S^v_H$fyuW@Qb;96RcW`MJ4^#U)uV+dm^gpB5I;DT=_)lGf8)wBlq9E zjAm#4+QbR|%KP@j%{2QuGK`TC%MS*H*xS!S404+V3efUML zeYU*&4!1La5cim8WwxyDw6c})GjDO!qdNJ_UhRA4hw-Gjrx;nTON@9v{p=Zzq$wHh z?`4DJ95897>^6yS_m)-sR)ed?$rb&ldqij**`rj4-^I*p*2`hwg?|yOJfGfK z>r&xu2t75RzL&3RZo`?RQE$KQO}$$B+&=qR5Sg(#ynYRo-g8mL|F`lGBmHYV49fyj z7B2G|^&tk0nX44af1O z5xyFL58=SK&BUx?`9f9Dwib@+j;ye%_RCig#*IGY)AOL3w*=-4Nvi=RAhjk30=Rz4 z#2EZTJ~95=(raSl8dM)u`qg*@3$)$Ky3(Lnyxh@>A`9F!moNTb^S!7YMiHX1<3+zN zNN=XrTP0vL%0`(rqt%2wumMX*8pqA!gn8+%GN|7x!jW%jj= z>|eIkXF0fYI5O^|=#vsZuU0R$q=J|M%BzqMT8*dk5vKib)5&T&@o|*~DSaJTsW=E* z;!qQG$SYs=P8(fq>5~^j^VgF8t zZrT5x4!a;*Tqk{o_ypJq1Z%Q|mQp_~^3jj8exV?~R8y*N1n$Fq)H2iQ5e<)c%^0Rx-M|WICb7{<)OdAETd~U=b!f0-rD@WDY zk5r|UO4vA^G#`xA#3yK`L51%t1|-_`qAQALO+JMg=Fl)NCrDrH?Wf#u#T+kw=;XwS zjmK5_?I__o%*T#vrKd(=Ar-u(1uwCAWngEP91_)>60eJjdf}egjOsh`IRug=4QdbP zmRw8f7((Oe2xnz=E64nZ@J4#~0*G6{1W1}>>*|(qpVtLz75W7PUAiR4?>DABx`@(W zI_unhCV0oUtJQ;lN|U|^qZ{A}JQJTuTw=%;Vv|nB(Apr(z7%)Yy|PWH|Li870~2dq zS-Dn+Mv32k1xzHrzO7h1jYeXU7e|lW$1!kNBZ?|*@tXUsf@ZZYSpylJpV81l7Pzt2 zd~hY&%hp+Ru;!xhW3d|M+3Ti=?@E2~+z%)}x^4U)lfeSM)UBxlMq-rUYg@!L24CnN&3a8D*1aDjw zcKr9YsvA;zGVEY(JhCO|mhXsp4RAE)Y)D8N!nwrAeYVpy%z4bFXPJ9HCZ=A<6Rxph zpn(B@`$+`1FKsZSlzRN^ww5+TZYE-SD<;)jjnWiL-}V`aNpR4L)Up!u^!jP3*NN2p zo;dhYO=TTzEyQs(+;DIXRgLn5zUzX~@fm7fZ>_;bbnm8Yzt-Sx&f>@+s6(@Hb$vW8 zjEoz+qjcR>#%U-kz$xts$B%*Crj?|g!mbg)r7pM;`PA^S#pk<43Oc) zv1BKy26UC9kA9!*)JgT2*bJJ{%F`4w~S*@3Zy$*g8?L+h;;O zT?VyT6SiBV;hg7BOpu zuhq-P5;7JCdpHP{zs2{C$2CA^6p~b6*b=pam>iB37@)fE!G*U3H2xaVAPpfiZ<`x| ze|$IN9LLDbfwM*!MX(AR5HjK--`&DfKBL&oS>Ro#MKwum@{V>+QP;^zTEu%U z{B;)GK+$_{>)~`94r^?pp~FCK>R%1JSSza<>!?hzYh-F4tn+n7+3ElBbQWB3Hchu4 z+}#Q8?(QxjK#<_>5Zv9}-GWO3!68_1cTaG4cb{Q6lka)gS^NWf?yl6cuu;j%6Dp_fnrd;74e zFDq}#y|q-{%A{MAdIAK3c#{M?sFF*tUEN>2FPjR(-XRC>vX{VbUM-f1v0YrHj&!oB z9A3nJqHQI4ZwoF&)aQ2^Rx)}cJ`d^zSM`=WZPV%PlX{}CzgCz^JAGZ8OL)N`C1PF> z{>CwOe>4!vXQB|=!6GP1(^u_0p+0XO4<&m}3ny=iM*ihk0(9kVLpvRkPGfB;W_Pa{~*QoFnaXqi1Ag z<=DXlAI&d4hhKUxl50fPbB&J+Y$I%!8P4|iw-*zr&LgB=&bj4nH{ZYAG!=qT_4%BJ z2KNOHkMQe#Cal(--yR@(5k5g?z2yWx3mYrt1eY+D!ud?y4CjKLC}aQrJ`K8niFkF6 z;lyW&rnz37091MLXPZr6t(Rn`RmIX7A^C@q)rYhCr2@bGjsW#tbP8EuJX%ivjxDU! zpI*p{7=JCK>g`M?i~~MH+Mq9eUNaNUn;zjSj7nwP8JM%-CT7Kgu<9a7so(-~FTh1r zJ!PKPzUbVJPq=?wX*gmzv3AMZb7x!mo(`BOP*?nEvwewWm(hI7U()b&^=H{?yAG#X zG6o}-cV5HgPbHU`32@@dqCiCfY#f4ZylxyF# zwjvOPkmg~93ZOy=EwN@BEfGO&593r*Ye+!gLK@d5*-xM{w3kr+ga>?U`o=%Myq$It z35q(B|E?{T=J72-gez+D1(5P{C-uE?eZ|Ld7q5rqwu40MhJ64&6c4?da##~vO~V81 zh75YJ?%hbdq?3stOHQ*Z38{`QWm<*n=$hxSGOE*Uic}Fi_yT(6?}Xah+Z$Eim?p&+ zr!ut`Y-@TVD_+Ahb#XyLAou4exa&Ns=9%^W)Qu++$yOJ3^%F5Q)*Y2XNijxw0(LNs;w*9BfT6v)hR-OijtI;xBf^u5Kz#1-+tJ zip*JOO6O?d5vk;=-EM!6QTm78?7NzP=<7IRP-C`8Uj;3)OnAd9q19rwWK({ z-9#T^3ozxE_eyckqAZP*0Dfj0Jq1j1=&TidBP!4oi=H&aB+H341Y!~-!adLjW6~Cd zi%w@xCMd?Smq!cZj_HMa~ z_?eYNK)pGtgp3r_pdpzxC3$ zIn+c;#XG{*l6OZ*xBS!otLaU__CbR8G?jtFvBD`t2ZP@Nd1-Z!isY8wUmzOTvmVsh z=>-l8p@{^FQiwq0?;nMrdwLK=;z2xHtmXw0WO;RoBF@$285L59JEKy3ZFQb!Rvpge z9a!nupvM~nKC&@?dhfGqZY)Z3vAq60+s$e3Shd8oqzyEaNcpG1qTIzzWIBAI=t)WO zI`k>Z_TFFP3#Lr-g@2*oY0Md5;qi8eN_? zYTXiCsrPqR85-|?s0y24?+JZ>T|#jI!yWBFu$8oqzfb5O_t~E_q|*n^3K~gP#rlO; zG}#Yq@ifz!h4`yBj>Ee31o38B%6FplG<&>|jWhx|i~TgZPZ<;9pI&~3qPpp~1gA@- z{`s`t5r+lZ+KnW$dl@xix`y#r1u?d~5iDcLi>3%uLs}^6GKxo- zG)GlQr{~>}zphGdOrX!|L&tX`U25zPCEYA!a&2~!SuuiM6J8XLlvG+a>@24CKjHu2 zRdb~F5)8w>#SY#qLlLndw4@pwea3&AuB6A34#|R(1oB24jX5xT$!LYU+VOXXP-#^( z^e+Y;xMctm;I>n@U^+-ikSe6aO?;?cVxgtx!0t-1o#fD)km$9Yb+;J^X$smdJ=>G4 zAN?9Z$~!&a10vu^pUffu+dwJ#n~uogvphzE`HkX)E0e;nTlB(4%-=AiVx)^M9>cN7 zj=YSun|r6U@^=n#u{C#c?plqJf@jpqcL-b|mP=g{{=ShB{us{e(+ZyKb+Ds@oGjTe z_IqmVD5(^rLk{wv{Eo}FUy)j8u@QWn--?P9BEmS+_7DED*FeX|rXT}a>^J&@Km&r$ zTkkJq(=QOb+Ckz}u-T`0RTEFo#YyTEho6S`!oCx&jgP|(!fZix+6EkPBB@a?R18Ac>O1YrmaRV-C24}52I{s@ zrJyfpSFO-gr@N3);h9qO+sI|v)oco_EwVL0H}uD3k6Ib&3w0pX&W9%pyfqu0A;TYi zWvYXIjLy5}y?H0Sp_xa-#T%2?+Z{CC75(N99LxO_}5lrXyuy$%ygT*X zm`zEEF^P)WMf(!jePdP^>fPPl=jnNG-S*71ZfxzNitHVntn3D!f%14|xW8W5|Lm0w99XD>MTs)8Wa`49 zg_fEm|Mcxpl$aoF&4>*wXv5tJ+BLHq5y1al!a1)_nhULhe$ru@29yy>!(O!pAQ}6v zl!HPr0;3Bb;;nZ)en8C8&o48{F0m^fO*&0hq;}0h5Qd4Cltr=@3U}u5X9SRo=1P>T zm3Czp1W&XYE5V@H%AOfy8d{MT(x4qA7UCKI&J+vNih~oI}_#iIN@*Ej>bWsJ=Ymoxw53jd6C%05f_UL-}7F zoq@>e-H1Bm_V%ZojF9?Ll!FgM_bKx&6BTgvz6~!EiSyh|d7KvJ7YRL*i}@wEgOa*6 zO1Z$gA6>4Po^WUMecXp`fxtGSKcDmWpVMAa{xEhy6g(~XtXs49AprD|v*$(d4TwOb zknFh^!Q`yJGoO)FSjsifS0aY_1xpY8yM64eW9tp*s0RSN=y7sVW5c-#Q`>W3 z_dXI~B}i*;>ht+hUv1#tb1Boi|7U-8oLW4+srRFqDe#B(F={h58luej-oW^1^|eri zVMFaGFcJg%d(wOg8V_0iu)|uU*>B2-B>$!QfaezaXj%r%<9d)&_A>T&#IJtYwi^j5 zo+QucT@7QW8k#CzPw_Lu( z8U_u6$fWZV0+D%i!-p}NZVz>24LbS8U!hYHeP_Q!f6lZ0*RqmgeL1Ux0O@A0o5i`= z+Cw;W!>VxQ71C9>scGbGy~UCy(qY)5yl`)11VPDq?6TI)nE{5gU5TiOq@se~w};@^ zkR&jH$(1E0c)>@K$$Y7?x0saHmf1wHFXyNhs1%gW(6drFPHi)Ui7V}wu)o1$Ph}g% zfg-Clv^1JwY&x<-E3LAUtGe$NPvb09UNm?FkE!S2d@{~j>vOka@2}HSmp^8PTl)G7 z2au%1ny5{6mq^31z>LcYBA`$K`Ub~PS96KQ@nkk0fPz%GL;G28Cp;vW0$JXkQ+;!V z>)Wpz*-qwl^;tp(p*K)M4GD+3`|7y-_zyHq8jW64|7m<}qh+Sscqj@OH{_3q0#`@d ziy)f@>@}is#lpT2L2lq328t6zUgi)M{UW0S^4RS&KkiGp@PL6AttDoL&_P7)sO4lB7@13t31EFB?QtMC_pAurs`>VCF-KIHFC&{`bK zUGX6N@IijjkpLm?EJ?8aK)R(hV-H2?IxZzqi?TF~(Nl={Vpu#<$y#-?BeNedGI~wS zE^`lpb&%+u(t~g^JQXx+AUqRf={`YiOaEcP-botQ2Ifm)iee4`ha@FG zEJHG7*@MwfSUqa4@UFw`yiaS?ZG5DL+G0T6eBY9c7^$N=#a zRSY*CtG^c7gh5u}1FLgYe%h5(*oL}eWJ zZ=MIvt*@po-y@x^!Blb)$8M2LvHdB$>3u#{ml~MrpGBegj9|K7ur+m{MnSDo(S0oh zk}b$h@)(d1Q{TmynL)^6U8-*&e(@UX`%I@$lk|q0o5-of%6`u}*r|;$H)X;rzS%7uQm`E zDhEk?E>r^bF20ay8@uuLK*&S~Nr5199fRDF^Hu+rpw*9kqvrx}^;>X)beNr8+>WW3 zpujfb&bZ=6@8kWHx})qzO0>epp1T3&nt)x=iK4KlwSn>Uf36aQ|K|TNBa1Kb;sJE# z4f$X7X{I^RtW~_n>6K_lh1{Q2-dMQ4r;X>#M66OA@Sw5BD~PO`3l!``lWY5MmxatO z8Cb3)F^6l)nd62h2ys2TKN=Vo_aA+*kZJRM9+VlymiicOxX9Fbb1h``QAJWiSap`I zE&y6xokFDufZ6B|FG986Ot&ZEA|kI|y{nx`s^1gZyrkq*>1{jG?Ws}f&l^meH5P6@ z;JJHa{T&C;!hN~Xz~i@~|7VJiIv=MBP!#hUyK!C74=YkJ${Lqp-qI4!V=4L2{(#Z0 zDb1?k0zIZPC1Ga2lmZIct1k!YT!fmsFz7IXnR$Z4tT}NRpZF+B<#Pv;fxqXVON3Fw z`b%PYH`N=Lwa7gl0N~soGcy3Oi^N)$wueeH18F1_eunri{yX6_(2pM~7|T8bzJD~)Y^ySyY=3lm)}FP@5m299m&F=L?XMXp6+Mk)N?9+?fC{GM z!J-eXu>YJrWLZn$Tbkn2I~|DXMeHYeCKO-h})LRwkMcQsz?6Uaf`q}C9Q zwqH1_C(SNthfazrJ3t(F&MqJs`?J-na6_SijBGS+7=Bq!-;-ZcbZtE)<(klo{r* z*9f+ege(QtsI^OdK|o&by9eq|8u5JR@I_7)Qo0yBUK`#o;SF=5CRf4wuph`? zk%rAh3U1lOH_iQwW`41H$?g8uXeN8&De>!3i;A{Z9%~20>CUY@eEN<-Z6hVKC zQUiLNa7)$Wl0pw;`LjL8?#LbC#;=Z@7N|BOK?PWVmql?RCdT(%nqAKy> zbX^dM9q=l#I^*yNibLXc)Y#B|65Xk;ld|RQZ__iiD+>R?)%G*LOc&9L-=FxD%0V+t zR2hG%g++1Yvah@P#&Lr!+`n4cB%Cg*B)j#uT5eCowRz8s+Rp@wlfH#V88baDvS>1g6WBSVzaQ2|5qi!cS5RyJJwHHR0d91?98#thw~}0NkTW$%*RT;4=op` zRtQ67@~BO(L#J{GX%g?-e^(W%yas0ggqYsx)_PA(ViguFGzu;gO_1a_@w=Rv+m5Xl z^j~^PgjGqDN*c!(Gc6vuve{3c?}u3BOm=bs!26R#Q!p?Q1XB+i-9x8NjK9KcBO z6#XqvEC|}YbKaIjD4w&&y+BX-@+E-k+G&l1Q3|pmj3rMJLNEFfQzatw<1^oaiti8P z(T;JTR6rtf4Kh*7I*b781Gbp4GRsUaW}2{nrdM65GDzVCKL4poE%P9}@m*y8a9kSh z^QibFH=txGj1wxG6}MHJMdMzL2arsp<1%w{O8+wO>4re%ohsKNd4^7PL2pO20SvQMf5?%HCDmRh-e(e@q_ccFg|u z?1PuVdHFpJ`9z%eDdibjW|Yz#KoN|W%xIE?v~7GH+Qk_yd*X|&jHlBMVX^3_yl}2P7{+^rksAj z=s1BY?y_VwR)=Y)(M^4HJX!e*;_NrHh~<__Eu|9$Zi-DztWBYu6oT&Hq6gy3b->TA zGPMb)tArH#yd7@|FA2>QEkUy0njc{JHW1hSv6$??*6Y8YFBGR5i&-sA&VSf~fV=fr zgJFF(zJ#aN);$}^cq4NV5^)b}hAx5kY?)L21So*=o5q9XZ>mpb5vHDhN9)_{iOaX7 z1iHeWJeyB9G52D~c7h0w$7EA)C$84M_VX{mzxbu$_nv8_nS&)yx+TI%}k&8PWvy> zNKIp*F*YXYANgy0qtM+Og(5zU-Wg_byKtU~PC1`S_P#B$sRFmIF_NHWQ~(fmGQ65q zQ%kn29Y$_KI?mXRr4DDAW~@bu;=CUCH}}~=kz6fe{hp65zdw=8CmAZT#+&RYGDTHX zSy)HN1SdC`NiSg|MQqUfND;%0>zhdPTAs%sn%mSur?mwnzw zodWb(EqH&7v^pGd{<1YzN=p{Nbsb?{A_ZCIbRbu!sS@X~D(6cBcHAVd5me+FghCphF%2dW{ks3tcx8nv zepBy@AR?@KsbX{Z&~}1G=h0zf6jO=zBpUWnVUSqRcEFWN=+AIX4k2^$h0r()GQ@NtO$53xoA=jyT$hBJ)x!QvN51Q$P z4mvF1EA(jSfA+4>5r8e&-=dPKEWJnM-*JQ2IiFw|n8%nRdjt0lXcMkHC?Wn~XY{40 z!s)qM{POdn?qP5@MnzwBfFoa%>EKSuGLb1rkA|tuAT*5+$1$Zj9V*AHjN;cSQQ}Gh zBqJ-$h$hKww|>Ls!i{WPU;MU*urNoCd7?KKN-ILgw;)NGV{F~3m_%5liKKTw3Kijm zui%c5@LX?Ha3(!3jlLc!Dz>X5;MRD;dLpaLS!^ZgxvN@fF#OQ~=An`}d4Gq)#CoGi z%wm;(L$*z<SJU=`q-K-FYNxZF6!A_kCjNq9uzY3HP^; z42OOrW5#|I6UK+eZ-$&J8!6}?(EAXrwPCMgsdD18ydAXRUh+!C@BU=3jFQEj=uJ_jat%n$zNQ@&r^UGLvb(sEwUQA@; z>5e$~P5j?VuKB>L2gF-7D}tkL9s3vCK}gH5$)lo0o0)rFRSQ|t2vlv>1wcp#znfpmpSTqCD-}2o?@C>=Bx#d}eT!4WeDuw`7*p=AQGuB9 zwXfK(u9(>?5%_N|fqAgjTb%p$-a)d#wdPj*@XE1qtUd~V&iuDiK43QT$hOP|>+z#c zuh#Feuqj0KXWhoQx9s<@|41Le#J9JiZwG%myPqd1tSmfgr#l|=xis(#JZF#N#**3# zrq0r^73B{!0EgtjO3+Fy!q$osEWcrD9+OTOLSa6e^sw#MUpPD<)SNBWmZuqeAopC< zIE{GMBu?I6pmHEJ8uSO4iHG&iqHw**)MRecvIlTNn&ppVMy+y95c*QyjJ79}iGOCz_Z*J;3J6%uKN| zgcRcTSNZMGHjlqhZ?b2)tJou@HqIy$lcbjSdIgEXl13Q5aJ>*BJc8I+wXyldDzD9t z3o6yPs~^kU&GyU`lm*#^W((C1Un@)LW2EGWa;NBs;}ZNm1BbJ-^CVamgX3Lh1A)|v zcI^y9=U^Je+rObq{3L*Q7ITZdl7=>j;tp}yZzO(j>>R78(bXd{nT*d>H-Y|cMaL<| zNA!mr=n;onD=eAl2XgrrvF6F}zdZ}Wtrxe*I7h1Le0X(b3cv8~{TJR6rnqZ)ENoHw zLQ@5isZv5$SsZYxd%|@brEVpj{D;yVB(xhM#jw)*rZlY&pB2#H!r-#ZoeOPAThp7_ zeGWSw5@20_EhuI8>x1Pb&_2fz83okACau!yHhj#fCjWuvBoxBmeLPU%7RrZ6v|Bey;xo$~6Uwo|bek57z=9|rgw;TE>L4JSol@g9*-PC>+`E-4cedrZZ91^{ zFp7(H_I5!uDDitXIJY+J6nPuhz9klurYD-2WK4fAe2N3OS+=q=KQcj>HSIp{u!{&! z;xZ7zD}jiuOSV3N37?=KbWd#v`o4$B40F$&L;wBUBM&wix>gjU?cGhS*mw}(XBRbO z!BAYsL*yhzwX6{IEbo7)8Whvq#BFmX?Fjuj`P%#GCy)*TJ^1DfGAE0R5(n+9xhvFG zP8fMAJc%0~npWpf(f6f>QV{VO^jmzE3#K{`iTI`dycJIt;DEsl==^n3*P-8D%~nr> zS|yJZ)4X{nBqsqw=#F=(j4uzU}Ic`>=K_vM!iMnX^X#LxsrivKiq~ z#De|2X{%dce^wge62<5nd=}t?V5eZvmod(kYOXq@wwxD~{iiatA&!QI*?(SP$H9wl ze5MeG|L8oQr(O@#Q9+Ox_q ze~_BUdz!eZy%q9kCbGjKJvhJhvM2s|->X@0;N$`l_$rK6W>2<=rYLp1KWRz$(>?y_iT%J@w5 z!hZU(ekcV|qdRR+RuK%m6cJQD_@YvoogvW3)&`FyKDp~tgTC(1_?#=wc@eN>m;Q%Y zpn@p?$R5yq@e%~+Wuz~Rg+wojK(K!Y)~)~*<1OZU*FgBv6+2T1YPV1EM;@264=|O{ ztc}6w{&qCrjBrin4#$t&$(Z8EwYkOggs3YT$A3_s5WIp={;_nkx=uVj%`6NMQCLCt zCK))O)Ekd?zOWUBYjsA&iX#K|n&0dQQ+_UuxIBs^$~umqd@@Y%Ej+SQp*`lYi3=AP z<#~UtZhC3$c|o(X{$A5f8uy!AI){k)lvVt$CKp0JRf(Dsw#qE=dN+`GRcf3W6omH{8!>RGyf57`o3qsK* z_kS%ev;Pju@Zk$++c@g_X~pM;FhzU)q#S9wk<}AJeyMA{J1SyalMa8aH;>;l{!2I| zykY7$56km1(}t|kR0-%k36Y9thw0i}p3G<*R${gwNA{YOC`Nwh>cYwE+5x+4MR?(H z-KSyt;Kxn8-eAexQ&uRBv&8ae3edD?^{fvY!lAV@5#J9}$ zSB6z#=AVK&SZvLMnYd!CgDa<}D>soyd zLSTo`j;%ct-$%ECuo^cvg>*h|x!+xER9tJ1TXuZT^`VHN;xB^|;wKBLTLto{P%m3)17bz0n`Cs6S~J?kpM{B`l~5xB zU`1p9Ha^Y7RtV=$rzRsmBi_D~d=k=AQ^he!h7Z2-KU%YC)nF(+s|84@%@E)bMEb)` zPR*D?xN?SKFRzxaL)X7k%V#&ph@J?>`$P9Qj}SesM{yTDz_a=G4Pfn84Y!zA{a zU`Roa+tb~W3&P)C*h!Tf@4scqe?9j8c;RrY)yKYmBuZxGF*(Jy4GUr64DU}#pjUbH zX{Oalp(wIMLq(oP+&+jg-a~As+to#{IVVvKE3mBgUoqQP3EzLJJ1`DFZM&hmCe==) z(&n5`CeoTGWo2f0v(P6PMKQjXT0b(9H*qv2`$&tZ?W7orxQTPdr)c+&!S6C6hCLIw z>DKb5Ao4sqEUVX0RAm?}9qsgd-|#~L4Ovfhbupm=MS(1FLGcAe^b!B@r%Cn!guucw zQ)sELFSGm-wAS6^HaB*^gr$cO^X%M>T)Mm&Nt>qD46iY+ZRCRYzVj5+JGd_+c6r_J z@n?Cn&81X3*C7bIAGBEGy$zEz2uY%)?F2v6mBU=MZ1L57*DbLZe7R_(KN$rLD!e-s z^g@IHd+z_8E=ub(XCXLFYA$W>3qDS@NT{IParV3PW|?~#YsS~63sucxys)HH_cj`D z17^>?Gq2|>Q@x^m03fnKQASb|1Jz8k1GrAAo}Qd@xQH>gf-1MK)IMqHL;?diVB!wz zjgeX$zy1|~$iC|5Ej*SW{wks8w>wbJ(;6IZ2T%v-Hi)wl*iO;%!f2PSule&{?Hj#@ z>R8c_42Zsug~RM6#``UI@r`v;x8jm;=@$xb+naw11@SjM?qFTp_ftSOmg9R@!l@(2 zqfV2(V&{(r9p$K~=7dZrM=g9egU7g=d>|mESVs9YcvRNMO~r86|C{l(L;|cMGVRys zm=4X(29*V7qSK5(B-yPnKJ3vz*f+#T1D(QYXr$do@^=xXE#Qm_z{PYWq9IY9W>g-g z2Qd@9?1Uy*!E{_SG8;X;F4cHqmoVsc1CvAJ@WQ)@2ip^WwCcQO(W+BM1sip*i1x9! zNJexpIg9nfX|eHRykJ^9-ZvC{OFUwmeHEJGBd5q?4F)-Z8eyh_CdtmI5w#J{r{wvM zP(`~5yB-funndRz2cR=aJdg9YxtZZ_^WYcq_C?^ zhpC|>sMSCrVEMqVBedy;$MDKR-Kxctk4wC2yMmZ~1 zSgZpC=o$Wyc*&30^Wg z1W;?&lgEnP+z}_7O-7)};GZFNAW`p|%()L;NmOS3`I4yHRM3pIVpAk_^2&{ewtYs11TOei&Wpb-2s>vv{OXd3o(3jO*zADe0{_X6fzgSu4pyIXpjCfxM4tNZvv|0r)jkhqVyLPwdlgP0P3&ZM@ zT7^^mEj;jH3u(c!SmA;pCu_?>pBF%93(ayp=U|Y&f_%iFw|5}a@W|=Qqqx;=#+UO+ zvu!aGK8!DriOHS7?P%#uOOWpxfQF3a)ZC+jrU4^dOew|MlH$tl5nFJQ0`I>ezaH4I=h z3F!9*?(m-Tih)w{RxNWMz3-Z4ZCHXeq0Oe>C#$nrX5dFI)n~utn)A^#DT+?oQ%S$} zYsHKf^$$w=C?2jsrQ{D?k$YCX3BtB;a3vddrN;eGloD{9^lqoPR-P&|Lvm|EGqQsj z0#{rboh;TQR3OH6dQJPO_3R687@@cpdgvquvHlnZ+-~`q0&6AzpeJe~v z04}oA_>=C@COrAYDX5tDw;wwXk0B-bPg69HDf6b5$aq4l!DpLx8O7WwCK6Z(c7qi} z0L^7R->mvcT7uu))TC-K5}QM78@ozrQhgBw*f0Y{kOq+%C>|H0zr}FWEK^LXz7&DTxf} zeYKnUC+q1f1BOyj`%bW5^w=aVkPAdH2k5zV*RzmJpwpBST675;H&wen zl};vM4@(d;-b3jUF-qSvc%Zsy{k&|F3>}G$3b&aPo^1DHU*^EVA zO0s|N7T#(rUfBE1{}$1toRS`4839m3{=>C>x&;*_28AaiJx!7!TEBpwf8zV*LCzx3 zr`8AI0eg-^Ci3roTk@q1;fo-VRGN^FnquAv)XEyta#syUWCG8ZPrYe{x48ch!==1b zPsq%VmKXcY=PF`TjeVHX8_6zOO!7et6A|4xC*5;i$5?93zjwq4^mh{&Mo%PvP1R+Q zoQN((X27U>dR}Vd9{-fgVVw$>TK)<$GECDau_KMgHMJWY)jDu$n8)!wr_4zyKjG>p zXIl>x_>4HGsy|;QKFEGIoGlhqL(9b%yxaQAS#esE3P*VWO^x{ay?Y~DL`E~3* zCvgq$fxvjpfIkq@In({InaocFp@>FSxnlgAow~b%Sg`xO${r+dqTlYn$cbWh&~|6~ zyEmMvC-LaA#<&6LY*;fu;Fn2!kF`0yM9~=G0HA{`ba@oX!1>V1=I|LO>E2VQt0irKL6wIK2=2yAUJ zg&H%C{Ls8?TZqJ|GBABfAP41^%pL+ud6M?_s;i^TQhZJf5AqIp8j24{%JYhOOTMPV zBA4;i87W27rH12QEjlUFajYHFaWIJ{gr(U0Hd-c0WyU~ZDsYPiYk>x(@)t$H7c!OV zvFqnlcy6?N3a#)$h2=6hG1@{23!^T2r2nA2!%n$9tkm0TF#7AP_^(YRj4O6T-nkoh z{6*4&*lP%QJ02bUQn;xIq1Gt9Kw#2+tvz=Rpo`F+kdzQ^6JK9sNF19EcLzR(&@BVW z)3Lls(#8|vnC~BQk5wn4{Y&m?ElXk4Cb-YW$J~a}HIeF2rhZp?3xi<>o(o|a6?}^q ziD=mG7$0B+T_te0>l65MQN6^XEE&FX(azij9LYQFL3usoR{7l*tfj~hw5}l50toHj zhg6a$MUr`p?_1;M9C?b#h?H z*#&uxd%vfpHw%ioY7ePeAdO)>*?-n@-{C{JL-w~^4nDl8&FaR}hXjG%n(b6pCU4 zaxhZ6i}aCVoyYG5ZprHrRG48O(>!hzLy}aZGC_Q6#uN4}S>y?yFMLAr&y?A&jNx7b zba1~2LX?7_&wXt~){z02j-+!)%*PD!ap0MTC&R0cep?T<0xJIU9+Q z%l5>r;(o4zl2DXmy_2I_C#b=Fi|N-ef|FsQZy6SJ^R@ZqOUjus0Amhfvok;#+2b{aC_ zEiq!;s`#A>aOz3nFEdJY$S7o^Ioo7n?<~Y(@Ho9A#=fV>GI8I|BrnF0;34M&qcv}`Qqdn;O{_s*}Q_eDOOd_St5+nw7)qZ2?v40JNZ;45}VCM3?Vkoukn z1zMk<|7sWv0;slG#ttx%cADQ=n3RZo=Y0IoCMUjKZhkb7dH6LQ7G!&W-71n4seSv& z>3zYD`?4KnkNk1#F>cuNb$epYKwCc3@}<2dcB7%e%K_21H#@#!nJaRtYrk*>eh&{x zdT?mSq$k_qI4CUDS@(cDvLy(hMWD zG*pbr{N||(iC%T6sge6TP4V+!PJNq@^zKwV47w~uTC-=H(gsikSnEi|AqJGTM3Dd zYUUVMysGkcb`%LWfH5lUU);`=97-(iIKeO3!)tj+4mNYg)6akZea`=Q`m;DYQr_-pH|?7iBj4X>GqJcbVYtglf6QIq-knou%aj<^|Zu7%)lFiyvS}KUBk* zO}fjsZsf1+>(gaRnhcHNatz1wljWJTI@Cixi&vorWp&PHj9p&T=MOqYU&e7<-j6@WY?R3 z+PMtR*oCILQy9@)yQU?Aj&(U+`UJ|-5`)kC)!x&6BMB^>b@&sMt#wqc_>DZXFiFT! zjV|?`zYm%JFaVy0*COj3eSKXo;hS&o_6vHWsUwi>SS8*Fxpt<1Lyq;kJ}c6Bt?0}7 zP1tWU?>Bq#Hg29IGU4RjjKarx6Rpm%FO2;;GjBt<+;JU*j!oiNqqt6QL zXr`VwSh6?mNkPu)xasf(gZlRs*rw&f=a==THcAO&d^`yiI&}+#GT)w{H1M((@4jo2 zFEfc-U^V~822WxoIbq3@6CvP~vV~K`@^&%=-06^ekRvm^Zml zte?UbBPOuAh9`Nn;|rtc#*17|=>;qovl52ORiZ#lyQl^76T7u|hNsTDcFZVDj7lLj z&)_Y0RI96MuDvEk%!aO^J?xp0J98e?xL~a9WvJtKl}Z@+J@^vvH0&v$OIaM(_X(1-iIZlq?ntVQPXiwrXG;uiEttj^FJn0>3<`H zVrst4lv{#86+$J%7&hp~wUBs}^ zfxE09g;1rIPgH8|aoRME9>IrzV=74 zz@}WF1`%_SP<_(J(iK2Le4BU4&O%Qc=6VW-L?2BC@h~Nhk}F^n)ABU1?=6+}7&Pxx z!M3FLM#{i8_I z-8iM~@tl7m&;ZHH)=hEswX-~Ide89r-r31l)jEP3kvg?aN92)+b>z>SVT%zA%?I?D z;tx22-QwI-zFFJ-fpnh>MWJFTN>Odu;uXGE0Z~ogg+(}TCDS+aO(8(fY zYG+_M0d+8EH999z2&bQ}a%EM9xqI=!`4ZwRYrw7FN})^u1+Lj=mm0YnOI^NXHW3b3 z8`3~n;8mUttD?PioSfI}M;Jo%yudak!I3pW!B#E9$)34bEfscU6%`3aS{_;L*$QMG z_-T3Q7mhSeF|7ehZG#pwB7}tZ*SfmBe;6R0u1J=QRk1byXmpm16fd3`65%Z2&05IQ zDly_e`=~nTHk9xERHR=#3}ubBy~(BuiUuEA=0Pdoh3NNcU}$;os+2bUYr)dU70oe3 zFsI~TUXCVH!NDU$DAd?G@zNKH4w1(#Q7{B{t=XK@q^CKYC-sjQ%uzHe2@icJ3yd_9 z6~fe{lCi=rU&Ek!r&z=>b+{NW!Ln3^A_HME@jL{PDH38iDi^~pGX=(~@FRfTC@fp) zh8z%-;L$<5Ic&-c+g{481%NA<@Q)s#_Dz&2+{C)_6#$=U=|72xt>A_1y@iwII|~ad zTmzGsWwflPhX*~oHP5yyVK zOpi$JWs@s|()rIqQob+-2uaVsDEb`_urW@;1f@70lCd9WfeqJ62=zfMMOhITR9baz!YpW1S0%K z*(4&?^r&(b=Q0l!*n+}>sP|k~tO#u8SoJRn8WYY6pVN-eZ+`n*s+`?TqH0OxVqNwi z?06s~f$7a2SCabsGg(a-1O^+bv(4(MT7&rq#jzNcTH^e&o%8xQVv0oLVpSi7kD#CZ z)A&6Y$7PB5_uJr1tboGsP&-M{$*e6QbTB`4ewHq$Zi2O>&~4ezz7Yie@ufjSq3ThV zlK~B_D+%T~cV2gOqQqmUboO_Z%+|=bXUN?AaD@R3<2p~RY!9vdOi7DU(Fd?HH;pT# zjRQDj1kNS)UyvnC-AIY^#Ed-48_!HNshTCt?jq_Bi^q{)OhbnR41dl82Gt(xF-z6qVWaVp&asNwnf`P8#h0p@}QjG5jM#m z8h%IH9SByA2r8y-fB)t*42gHF!HX0iG;n#r1F62>&zOjkK`3WA!sIMu2QLG{K_nA| zj^h7nMja&U_ViR&g!b%|M%Z5axeIt_9#WeGZT>%^-omfx@BRO$1f(rON)$mPrF$R( zA}OdycX#&&BA|dEEjf{r7t+!(8j%=X14egmY>ct(H$U(1?e_Zzp1WN;=UnHi^SEo( z@SY|_L|<2i<<)#KqZWsOM8UN_YCWK(4r(Z)mG9Mqs)Qf$j?-rR>gBXBY)lc5eiMmEg9ZZM)(=tTBc&ct!%p+ z5SF@C@=-&SDzs8>>~9CpXT$qQ>vfL)Xw8=7dnCjA>Kd4h{=}6;@#W>0GwGByH^x<| zxIUSt-lfdRaL;~fuzNm|`1$kA@U{A~jrd^^cxlG@U%~2iqTsNpd%MS@0oRN5)r~sU zTJ6<1SbuGi`^`DdQL=x_QTxAWnrSr?CV^jvMlz>M81*fXeAfXzd6Y*wsIS8>9p27e z<;4FfC2g~y??3M61V2ikowSM12fHIDI5*K^ zT^CLSZ`DR-*DR_#3OfE_#k&Waa4F*=j3_CFUhGs%0Aev)A| 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 TE?c2r9U6Yp^FNg*g8%$KQVsy& diff --git a/web/public/image/logo/logo-square.png b/web/public/image/logo/logo-square.png deleted file mode 100644 index de5feb5d54594e4dc245bfcb98a84de373efee16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6444 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 MC2hsZyH>&f1DoVox&QzG diff --git a/web/public/image/logo/logo-studio-dark.png b/web/public/image/logo/logo-studio-dark.png deleted file mode 100644 index fb07a45797acd6af2b6d8fbcc411053939047199..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9543 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 diff --git a/web/public/image/logo/logo-studio.png b/web/public/image/logo/logo-studio.png deleted file mode 100644 index 42e635ca75cd86df73fe832d2a0935a5ed7f6894..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9475 zcmZ8{1ymHkw>KrNH0;t#H|&yvbazUF#L}T4-AG8oN-ju=f~4$%NSDM?k^(Cw(z1ep zbnN5*zW2`eo$oj2+`03+Gjr~oxpU4`vXO!2LsCXk92}g7+FEKRI5@b-dmT!Qe=p~{ z)IZ|j;LRK9nW^6w*=U%Qd4=)t@K3Y_nY}k3JRrs=cn}%XOn{5`-N2530N?QC!hQP# z6KOnLT%MqP0(^o~D@chfjDUcEgqXxAc@rNWpTlrrUs3x3!2|JxBLad4*TKU6A_aD^ zeX%FKWTa&ELP@Lq)}$n4pqQiEFp#QjJ0&HRLDmr|89C+4GcvM=qk<74ii29Y$CQ-R zcQCpeOWubRRCJ;L9#K&K;nch*wcec3I2^&b)2XOvZry-_isScWAA1B<;Q2j6Lqm5V zCasWtMMuYQ_kxZ(;quOyb%rIlk|UL#0f@~br=g)g;j(7Y+X6B&Q^Z^`0DxM>=ORyr zZmocSm}ES}>j6NfyC7B-+-HkkFy9%`tpv^WS@(Rmywa>u14f0 z>$Pb$R=phiB>V1R3U-+-Vu$tVTdrbGE}q+WVRxxcw_zdJm&`Y++>!<}u_7P2G#0pd z`0uS8oZL4f{x_|u*k1XY+B|HFC>AMj_tD}uKNnjreg~JjjnBlsd3={Dht1@~#w%ch z)v!^d+8?pEev)@~;aEo}iBc@q%m`~wf2$E7m4?NtnA7E8`4X^9sn|P7Th>JEh49Ps z$I^!)%6~b6cR8c~@aXAN4%y&JcE@pmJF*mfRloiMga1Y1pr zE#}4+5Z(DDVWX5y>+Z16HB>%ev2I#ee-haW?47;7d@)wE3@h>$i*+K9$;I-+u_Afc zJ6$tQI2MwH<;}%{?mfxGil$(1G?Z8~u~;ntcMA4S){-U-%kc_J{~C)Gv1f_K0%Nf> zudsJ~PNeZzUXgz_6LGFvT7*=|1wh z9cE0Tv%6)p`UO#&Vb#ejnL; z0VW2fIJkKD_i;)3kctk##Ky%dC@d~6{#ag7NlnYp)Y974_L(!(-7_#SFf=;ub!tjF z?9H3pg0~+kYieuj5C~**Yis}T*x1DQ%59_qdn0w>P)<_;2z5s{g~>XE*=1!rgy>y-)u?{+IdR{D18KDgHNw z{qIKi>UFvE6%G#jiME=uS;!9z8sWL5#5{b2WodUx0r$m~17@{f8T9d}5>c?IVET0V zfbAY&VPNrhRMrfj?$zHHq9A@k&ako=<)7+5HBn>#49?Vb_M%8{dOrPK4}N11;|NG} zT;x{~26D%Ar4uKZCY?!{82D7J1^+PHbNrl%_piacHr;y$U#aPbCN5#Ld8R3vBjeiL z{F)f%H0Hr~(s`*;NEK1?oxMK*3$%JJU!-CDej-dp{A4<1uE(8#QF{NnkvuhW$!`^OrP$NcFQO_H34sD=k+D;e_bhmwTrM*t)=-kxC8@(5!Ye*_gJHRJ1Le?B-k_dkI8G9UDYu;@${;}|>ZW-h53WYen3de0wmzOI4 zNbU>jEao5URixHoo?INQIWsCMd>%U5b5m1XJt3|op|04t@aPh_o10q~qUa-^VipBu zXxAj>+qlE!hZoMiY})Fn2mgkwJs$c*}?pGmR1kYx8+N%yR6BN)3d<}jZN$Q_R& zCwf!fXt^v0r3>R7&gNpi)wFU2!h}sfV?A{k`tYKM5`}Oo8H)K;i7kGjyF@y%y zzp(p*3Z6EB(kfy@=XGfyh1S~e@cdr4TTn1~I07Ps0|LG^NmVDfSQBg?Tx#)X_ z*WKN3bq-{{DYPapvWEt3;lSuY@Q$XI$+vy4k}1emJk3Is9hp6Bs0@9a3!;UH;X{to z90E@2JEG!RUoO;3H+rT2`%c1sJ$}?-C|{Ws3rq^+R0UrXO9ag}tbt-M>C%rn{q|3z zsm1jdKG&01^2Am1aV(Ys9@Is$SuYf0RI8xeV_Bf~tVo85e?)+et$&AiOh>yFnXED| zc4D17d>2dL_Bm#x3A(s~cnp5}`D20pr)QvP&j)7;Wcs`?mrw)^Pz6-ciSoT&p)q`q zIy+bi|M|^1IO>I*YuG{6M7fW(f(!(LT0CQR_wx&ZCLzw5nu;-^hQ+dhH;7@@vD&mG zerPZ^N5_K!15VNIQ+jI7GWoC_gx!L8@gc>+%|_`GS?xmCVK>L$?9S%!A0F!{TAz6>6Z^ z!~m`)!6~je=2l_Hk&-~tYHl&YootefA-opSvllY<+&{cR;qRU2v7Z|ahul#uxmD%> zF+B3kN(k|QL7Zg@rq_PNdrZ^N*ln~wfO*)CLLdaa-@Noly;aX8{h?%pX=~mJ;2Zt{ z|CFbDd&UZNrb0j%qqbyY;l4 z7f-4%+klGFnGHi!08WK;(p3AOh)Q3KT+r5b+LQ7X-NkOBsT2 z^uYT{XB-9EFnL4YqY>WHCo2hWr_YhOu zX^QxH>Sx}H=Om}7xvdIP<%5diS=m6@+@x1kL#YOkodvE#&$~|+ve5J>sM9}NzLAV! zIjFfqq~sb^iAv&khy{5@dbPcmjVpeIGv|l%Jb*LJ)4sSewD*-lrq?Xw>H9jJgs}@Q z=0lCyRZI3+?ueyD_g=MUaFl^U&=c-N9I%Q8=&1&z!-wd)nd{6vqhLI3@*s3Cbpae1 z4sF=6j*85pYYOP5WK_{|fBv6KXf6U3%8y; zAxCS@)b-NinR%Q3B<+4BS5)=w?PI^2 zfn*x^sJ#IU)EynK7IeXhsW@D%$cinXhL5L(cb{Ds!Mp982t*gsg-RaZaulWc^!h(; z%jN=abLVu;%L<}`40Huub+ZN5Zc{NXV8zG#x#Kmlj_oBb(nT#EA^qUU-(qCm z5{&@Pw-vPb$8UAD$ zL1~Q<+^7_90i>|Ab3N7{wuixg_Ue9`G*CICQL#A49ALG#bf=~P6bQB&XK!LE=ni!H zckvstg;(AnRD9F5*Y@aH;YOZLnpydRxmRtimt{gU&u>Y!NOGv zN7l_$04X6Q$^mgpcr$T`^iKtDoAjr6)=U(LHgmuttt0z(Dtjw4QMo=$M?4m0l?(Ddlnic*!y?9G39xM(x_ds zD}dQQS--`bSr6z^pH<`q6-9DrmuEYt%~+djfwt#q-MOE>m5K_9IAWtHv< zH?+AA3x1?Cg{mtmjcth$I2Yf9;6hs-aS3WiXO1ykXDT;tsRJRMDzT<0X7Aqy^;bMo zXiH5lZ@fZwadl{dt{Tqf3W~%twbP@pMYvi$Pnf>$;p}H`BPv#98dDFa68B4bAsCuG zuJ`$yU&e*o+PEwOKWX$PiL>r<)L9DkUhWf&XAio`u~A{l29S@;9#@$VLGD63bH0Ggpm!PH&AAPJ?5Gwboj(S_J6HwB>Mb zWf~{y+=t5<{u5J>CO$E?s}gHc<6=sigcO-eoY%DgWnBU z3jFpl4}DR1{3$YSM#@o^SMlxBC zO4aFx5_qHP!Bjk%5>E~{9wm>S@V0?^Gc#0Dnp+MUzJJB@c|KlGO~lX(E%5Zor}Q*? zwPljQ(eX_)?nlfsa2Cf7H5a9A(GSVS-8UxMF;RhTt4U@yVC1+Cl`OBa9o=?Gx{A~gRrK_dFc7w^E0IPYFjWh zHJ7AvOtqwDTvTX$8jya+QdleLWc*2~q_*5SF?BOAnq-mW;6}VSFC~4bq}Evg#vkLk1u>St&%cr3ftJ0`qNeqWvlEP~-MDw7K z)bUV6*@ls7vNv#qPe1K3w0}L-f6*Aq3C?HbvSo5GTeL#Zavmr{E3R>k~IBgH>OhFfMw1 zMfLTg+jWX6xn!0Kc84TQVsw6Vrt-!I8xIg{I@CL%L6=)Y?a}jTAH8HfeMMp`E$bv6 z)9lPaRMg8|e@S>wUaO#z$LE3OiO)jEwyXW>g%N%a$a9xq-=nQ9@$|eP&g(tpxB_}% zuy+Hav2=YDTRcV=h+m(==X8mTDS>C&r%r)ACev8@$(jL>K z#7wUpu9X1^gbcnp%`?t;J6r-|am>C1^iQ7RaO1HwXv&Jp!<3dX+8I&2S|wMWCwVVx=%$y_*-ugIpRmejr0~DC?7E|d-qW!l-k~>Y z+XB!TpTyr9Ot*;&6C_@@{E&4xivW$Q<%lolg;)$4=ul{qmfg-4_$6ha%85selcEG1 z3 z(N)n9;Pm2>BY5*AHdU-f&rpBN$>-{6x`(`a(3y~$hVlR(j*zMbAr|!-ttm>0@P+b= zQK~-aBpT0|JgkQ#kh{?bmWz`ueGcQNdWn+mA5cF}q3KLaRRFs(QK<-(JhztqqQ>l>&VZ;Slk8Rosa5|c zrtljAtHMx!lp7L$$|g~H+Tdco6xE3LR)J1Xc7Fh*PM}U5`~@|&Q5~W7U)yNP z{VYR*TjJg~B^j7`JofOQ0Xeoj-cl^G%vtqg79!RKM&!K^-XW zBBL?HWbB2cx{}7uSHG_S4TRE2G>7l6nws2#?n~uXJ?Qgp?EzY(9#qre?{nh;TF921 zk{;-K?<@P+&&W@|VqXbc?-9V6!xyvS+5__p(`xb_`#?q3^LzlWASM_*;3s^mFC#== zwCB~#5L5$=mAIx zT0(_Tw3P&S){?wWrv2`{2aa5t$!i#izyeU_K}JL4zH&RRPEBt{Gz`)dbChggWOTtyv0b z)$Od&8X%I&Bt_-kf6)9k45+ecM(>7_Xq~hM6iPJs7lIP==bFR!qw+`1c%@`b)A84+ z9q8-&v|*z{xKJe5=)fXAknpU3eXpE;O9)rMG})NrYy9`Eqa+J; zneuq@H^!(efStG}r>Z|Y^ym6}ZC2$97M)TZmrX!mB)ns0fPNOA*)#rcZmL~=PbCcX zEgKbeU72R<6tR_Cp?Qvr9!BdYpUV_z{7`lq!XJpi@G4%8bSVWL*8aV;eh%&T;jB;* zdn~?Es$`T*MLXApS6<_i0|mXfXqzOoDhPJ+LU)b$+FpGxt7)ROdY=UwIaJE4@g^Cyu*y(yFX4>T83t%B z^BkI3lYLM(sZv$g+xh6d%8fj6TZ<}^;Neu&7@+LPT(0DYv6z+1x>K6=av=^^&T)Pb zJ?nka!wV_Lye5~Izoso>;iRr$&vlul5Uhiu5?$inT3BrZPEbGfr57L;9J7e!q(+;g zfMjm)gubZ6jsBVH!YpUGr)eyv7{{YcEg7uJ!@{{1U#EhCem7U8;YF~Gg8P3F8Ci8$ zdc7$h6Nd^Y;KPa)(dn4a2&_`fqWF2&6}a+wKHz&gVewYh z)=+!LP+4xBEr8vD8nxA~tYb&&^oVkp$Rq8AW5c2nB1DtY znH04%1JWWUEy=Mpga|aI7Id&2ds{p}$&c9S4i?Rl|J%>d;QYfCx;&b-r2-<;vH(Jc z_l@A1U(cD5M#L(4+(TtEOJHjJ-7*WOc;h&5*-^!6!*$dBQc~5he}UWa?t&Yok}$rn zwp~!JBE{Cq!3EvIL2>*>&ZUP~fKE94)509#;g9N{c7O#6Uwn8WbV=m+9<|Ej)T0mY zDp75DzBU~xc3}eT2rAC*d{m-}IjlzPl!PD1nYfBNl&mqqnS9g9ll|H(oYJ69nAp0n z;KY25ZFQx2PVldI&6c-(6~YE|Mh}_4okRjx8EsBBj0>N87Ml0E=1hHj<0>#aJFsi% zZxbeYScUp4%(wBEsn9>|V+aYd#W}5RfW1n#gv1Bd^-Tcy%li?K>e}%YxiUvwU(bul zjxTGiO(C-klaZ&k)Wh~a^wMG?cjV3${*pt_g7X8?Y}jpx+o>z^RTHmrG`B-dT-sHJ zmUy0dYq+YfWPsI&gom4M1R}|;c?#=QgY3KW_LQ0(CH6OdgAVT)gJHwe*Rc;j=H;dg za)HD8tVurU)V02?A%_wJqu!}ECnv8=WaWBj#`C{@$XGb0z2m^rmeY{zv?@}f7;T&) z6nA#9|4H%gE1Q7R^;zFf?4Ja=)vnvH#EYL6bWK(7+bnt^PHT5(UrLlCt{*hd7Cqhe zE(S2Dn0=}f&31N7Nb892upR}a9%q1?*otgCD0q4_pJ}AS{S}P2H}LcP~(Gf zvIp*NLG0nX35Jew_NZmyr-a#SFaV6KgW37p=4%eG4*JK3RD=khO8;>gnQf% zOW1Ts1(g*m$7{Vf`-trDyort9wxPa{AF+UXnHkHFiJ{5Dl|-+Hbv~NNeD|FzU}a^5 z(*$sS(LUveOD-7)M!37X*Wi{8e*gFNsq^|C>M#4k7KsqtR^7Un^-+_`9d^|J=w>iA z1pvC11l^ApN{u!Sdvejw!r+_e3N7i8bU7P#P652j2H>F%uxo5(#Rmy#u7@`4C!IG< zYzE;%-kelNoulp6d*8I9Sb@cD$dN~qkTaqCr=C06<99l#(7(!<+2mP*`2B4EvpG}4KzKNu zmIN&w^>0f2d<`p%(pv~Sb%_|W=J}ZCF$yMKSLcUhFr#6BRHY$=jGLl=GR$yCzdKiz zo*1OXC2KDcfiL^Akg8vr9MSx3-TXKMfl8c@m~D>Vwa^RZT+b(>|eO>kA?u}zN#E@39V>7JJenG^7;%Ubli!XaAY*Z9DM z7VDdnAn$?Tb|xlZD)(CJA|9if%POj^)+`O5cmHgaSsCPU)qT<^9D4Wzc^g7Mj6P-H z6;ZgAxW6V{#i*Q(c2hLjh}=vjWFlGMF3O$+5H$-H{Kojjh3J2jS-1Pqu78?;U*NO) zvCfGm*Zn#XBbNR>BK+-#2IXo+q5uBzdo1mEMYKt0a_D^&m0)X z*YfZ4ZBt$+*v2O;G7A-`oqUZhpFH{28S2&_-#kbge{Q1_>9*>&3NBSxJD(2CX4npu zv5{qS-e!3$sM_?R>J{byNLZR#IGWi+O&*U3asETYkZ|IJu#9PB3x6>!D3Y0KrYOq! zC$`NWF(8}AmVmHd-`cG3r*hIY`fB@?zT@cR_~ZU;>MP6ZX>t5D){im^4}EQWmlCd; z$%+1b=D1+k79$qvE4EGhyzyb;RTx#)*6IzmP+S)T7KaG^-8j0CjxVgS6Sdqhr6n@g z_x53%^)@B5YGSm9JK)e_)Ws1l^Hk&naj`Ghm^9mH9uX*)ib16jR7wAiy-a?^;!lM( zCU@;pdG~pwVdk0s42k77$>&<08;RjWCoT^;ZB%A-z((5I+7RWBq}5JUqHRyrk`sUu z&jTnDIAK~Z8aEP+9T>4hSN+b;>q?@_Y+^f8Gw+kULt6H{A+(ND9RqL=a*f? zLHtF|(%ybyh{aqYC!*k^kC0@vy3gcP7}15rmAFUd@J4^4$$lc8g`~6H}3RQgUT(_J^<%AV_u$=-xZvi z?|gojI`_s2HS_Ka@k=XEl^~^ob|B8Do$mr**~!qp9ci**=R=D}yYDypuDWDl;o|d0 z7r^eI;R#hEkz}TeZNV{a9ZCboN+HrrIfyF_Misuxb-r^od>WDbM&GnT$< z(Vs0?PtIghO^G$PkZNE=3bH3$yo?dz&B7H2=l1E%V=^>0zN%Hb{=xB5bAEEU^PCJ3 zsRfQ({5{$!wV#U;xuvP~Z?jdv62@2|(|FMqP#MPbA3{22Q;byTT76@IUtF$~OG`G9 z-EWMBS*X!2oL-lpFFBK?tb01}Q3|fJtN;;>iZ6fcgNWL4(YP)pb*U!CM2Q1RlN1t? zYMTbS`GPfBRBCyOvcHB<$zbSOR7)#>g{K9M_tcpX=JUxSiNq9&GSj~`;jzMh>&Ktz zkq2=Do)2ysrk0ui;%GrJpU0qswny0ZTL#W(e%Dz#SX(sob~S7JFXq3 zpBs|bPk+HjwTSXxkF(u z2JQLt+?|9jX(YMM&xCeHDwoK;9ht@eMQbsQA!Nrp&mZBU*VosN*7FE;xWoqM{NT>% zw1HGr(WKfWE>$QnJ z6Fnyy3Xg2$&zlUk^L>9c9sl;NLH_L4tLRnfnRQCc*1<~)RWm=p8$+A-{|A^;`p$PG WUs`u%_3!_D;ApEGs5PiKy!tOLlNCh( diff --git a/web/public/image/logo/logo.png b/web/public/image/logo/logo.png index 7139bb21b1ae1281bb46bbc77c3f692a6f883ee7..d8cb1ce8b552836099ff509abe458c8f6e2dbfc4 100644 GIT binary patch delta 5495 zcmZu#WmFX0+8u@gLApVZPLUWwLL`RnZX_IPq|>1s2I&wfNdX-iX;8#r0BIR&XrxmG zX$h&1_pW=_`+oQP&RWme>+EOmwV$)r`E_19wh2FuGtg5fyM5<2001D<)KD?H!3Q@b zMnrhCGmP^O+yKZ$NmmH~s7offa3HujvqLqEbOC^1UH~9G5&-yfLxq0_00Kk-fS>jN zfNa){(>t%lP%cF{gziBhJ^*y1z`H52n*#hkTX#vpFaUx5DN`r(x7q)(;2!RtezHz* zSXvbx1L6jqMZm(MBAoJntK|G)&d#!yMoxZS?g9QVFHd`d>Mg|H)zSl$R9!XU@MnY3Fz{h(<1gDgc1SKvPA@ z#Fb#siX`#wM9|d+=45BgjhXq|w5_O!a$Q{#|8(>dR~tzWC74zvFS1dI7{)bPrb-+i zs~FVy%B0F5hcLm+u#ht@2ZST~(#cW#p@Z;ScwG`+TdQr&)G=n)ZRgF8lS`M4o%tlo zRf&|~M!!vLw%^qu0&%@{v55^ zPl)WFKJ3d8{`!z~Ch&E4xS_m@%=X9e$5vY0iZqZaD-{Dls=QtWJh_Lg#%*Nuds`;s7eaykb@Pgo%J3m0ZjlzI@(Y%+Jfs{xLZJ0HFWK@pzJEcBw$Lj( zc;N^ittA7RGVTPwjGV<}whe!I)tr5*f5*cw{D|>O7v;qVCk>lR<)T^P!i{P3J95f# zI{T!p3!4G%EWZ9d?A$cqGC)S5GXo`~0n(@do~ZFO?~GQ-l!U7+>{?!;>DBHJH)2=T zFXP>cS87Fqh;NkWOG*J#()3V0e|NO2PK(MniIP1o&1+d6_E8HOzMiks!6UuZ7|Ki4 zdq{g{Dg4ag<63tmLXR1V?2-Fo`rwj^!^#zF>l8Sgpnhtuy@Z3Et{sJnw7hbpDGRz4 zUipB1;yFi@Aqtnud4yTNUW2BlXJxYH&w?Xv+5O%_7=g*SiTT&xzBr~F0U4hIFLtbX zLIb0c=?)JGH45*9i7>L3^mMxHfLn-MlX4|>l!D?JxEjtc#p9EeA z%Hk}>R2+=64jN22RifF)j^Z%B*_1Ju{hWuL8i*N|2Scn)U5<~)l<`1 zt|n!KuK6%(Lw0<*Q59ka&gb6Sx83PJJsfX(b@lE*!7FiPXou7@FGW?nJZS<#npV18 z5u>DlG{(fFxI^erRx&$=Wst(pra4(9sP~_I4p&0ab?$h#3^DI?np#aN{q(a|e~C_Q4~Z zD?<97vF5;c`#_9kc;>@pO9^h$J=$MFO>oZF2g{db7poq9>r(9ffbULa(!x$x?-i8) zP+ZZK(?^G@8B_&SYc!Wi>4_ZULl?#F^(Iia38BglM+WO6aj`P`M!|JeOrLnP`k})U*eX}*-DzH!5%ma&*A$iZKQ2n^!O=p* z`T?8;(6Hd>M>7jKc&F{;z(BH5JQXcld}$o3zu`a;Nw&^)SsL(oqZxLT>4YWUVHpIk5t> zxbzu%wc+&Ft!4}vr`5sRc1h3hH)M5JG`M)_q5?ABJW`!I4+aj*;uxJdDM=8fbfZQh z2}Wj9$yPS|uwW;`tQi;ub1)>)q(}*tHB{iJvgXvGH2*{UDRVNMJknM&V&rgb<;!I@ zct(1FwXAB_qvXR#BB`z(CZh!2$IQg(FKfZ?H%>HT1F9y^8k9d@i+ih6AZR#Wl}&qL zLF%;@Xzh_GFcCSIcaNxQ_1Nj)VM@_>nxbq1l~QJkB73uh#) z2bIi5jVi{O);aumz2%rjlz*|g=SqbL(PiyeU}dp9+I5ApW<8!!ViOk6c*o?Z*~Sx9 zvUd4QP&g1t)POy55C!E3mXK(xF)+Bg?GY;oq+OX_$CCOjhV{1!s(uuB=C z`nYc)Sm+DljQk?Sa5JXI0NXIg$;(TN|KP0??uFAY8=nM8wyg(W-{Kr!8)bf?@@Bke zMHPiL1lmic-@W}xv4t03^WztOI{CKpcx&MS`@KX-uPx50inL~LR^>v}|*x{=-oSD1Hd&s+bNHFhE0?>_su3mQ@;)brE#Zg`8} zDKq&@es<9y%-~38kh%<%&Q)%%Rw1F}{3j;ae(#WZr2eTadKG#kz(%|g!&7#m)xV*^+ zFNtH+s7@t|7_dse<+XA~L(6gwW_aJ5;sNugfaHcOKeQFBMFt8CcWsdw(^tS230^ zPDsnumtjW?)fDl(Wca*2ro|R^U_!TOm7-$)E*yHj$6__?6*FbWzI3NUkb{dKx$Kv< z)SakibYO%YxVTNgy*aGvI}m7cydxzH^>asWMdfqZ8*O*J%e>bHM}_a4)LYDI`PJ}9 zf5PYWuI_WUSWPZtekHkmXSj5x0_J zON?etp@f>y&jWbjDjf+?xeT{)ANCD#TZpoR)TXPb`h=-}a`?ojMura=g-oQhUz@L) zFm5g&)LJ$HQ^SR1mF4Dl^(}uJEQ^c|rWoADShlol<}|Xf7;~i`=vOy*NE@Hv4dUkj zXd-}I{8~dWL0p#fQN>Y1ibI4Td%!5fek@_{mfoldlgdgHf4*D`b8TGhshX8TP@o%L z4IB1}4^6`}P2Qcp6I`mt1F~?%y+^%SwLuKYzzOy9)-c=ft4+7UU&7zk2{Iy~gfFQQYAOjP*=H`aD}0NlAqyl){~~3kpS0tB=f&5b++AF>+0ZYWhI5R=KM(@r z_pnCk(#CE>r0c*PvHZOEWOCWG1LKpgITz+~yXl0mJrjxmdTtY!I7vsq*?mZb;Oz=8 z@zGXE>J{xwLJOQ>cZY~V=U#pjf9CsiA{)1imm7>5#9y@48D6ONK|y;=ux!4|mmaJq zK8!Nag>und@BLn}%u`&?f0X7T*+k^pwc($rU$+^ z+fhAW74Sz&Nh#gaS&6m#;vtwT+WegPseaoFtX6!RSJvH76Z}PA{cLN9x`%ThNOdu_ z2Q5MTW5oxEBK_jL5wmVglz>&iX*#yh7q*yj$wg6wi7E-b z*0o#qX_SZFl1Gz@L4zoaCpfL?ozGAQKObHsXPHu{e@S|}_O-vX2fU?jqe6V74BZ9@%>pZCiqe>IfXBausHN?NHG zl)LLkGxSI**;?eJ&SgaXC#^V0_A{8vWU?EH6NTwmG=W*(et7Ps25g8>@>ggi^N`9* z7WA@x>$$z@`c1hrxmeMrO-v}re-3dX!-~GU%4C>iXZ?LKN6*caw1ia$6!FWdD?_`4 zMKPaeH0rp0%L1u_861S@{Fzn8i|*SLJ58Y244O4kE45Q>@@m(#-nXHyc1xmh^R*QK zYJ>Ux&BEN_?hFqUWD}bxY-8NDhqCQgd8_cUOfwe2WsP=$U@-+aWyhAnIwDkvpY<(a zv#xK>GP-UuJ$^!|ZYZ&P^|N>6>%dprkDH@Y@-EWMNltjDG-};* zkp(rLr3;zpwmR==Ww77Lqi(UQT*lF~y2@*9-k<5|4xfHz@l1UDNBNLCk*jUGLN**&PhYIz<@K%*g{(iwj}=IfC&`lik49J4Ip~ zinkRVn(+Aru&E(|uYY%26+V#G=YPMDpf5eI_T4U2&k0zH+E!L4+>ffkB8X&w+E#cr5T{9&wosAJNDSM7V9Ml6a?9{I{2!V zp!V|*KpkqTg&wbXsCSQ(&p3L1C;FX;_9hgZB7}Lexm1S zcG=@XJraA><>|8T|0n2);8-a7=a}yf9DkDau(>K>-*Qgr>3f&N;Skj G;(q|W-ASwf delta 12453 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 = Options2 & { /** @@ -1575,3 +1575,85 @@ export const surveyV1ResultsAnonymous = (op url: '/api/v1/survey/{id}/anonymous/results', ...options }); + +/** + * Get Allocation + */ +export const tutorV1GetAllocation = (options?: Options) => (options?.client ?? client).get({ + responseType: 'json', + url: '/api/v1/tutor/allocation', + ...options +}); + +/** + * Get Allocation Stats + */ +export const tutorV1GetAllocationStats = (options?: Options) => (options?.client ?? client).get({ + responseType: 'json', + url: '/api/v1/tutor/allocation/stats', + ...options +}); + +/** + * Get Exam Grades + */ +export const tutorV1GetExamGrades = (options: Options) => (options.client ?? client).get({ + responseType: 'json', + url: '/api/v1/tutor/exam/{id}/grade', + ...options +}); + +/** + * Get Exam Grade Paper + */ +export const tutorV1GetExamGradePaper = (options: Options) => (options.client ?? client).get({ + responseType: 'json', + url: '/api/v1/tutor/exam/{id}/grade/{grade_id}', + ...options +}); + +/** + * Complete Exam Grade + */ +export const tutorV1CompleteExamGrade = (options: Options) => (options.client ?? client).post({ + responseType: 'json', + url: '/api/v1/tutor/exam/{id}/grade/{grade_id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Get Exam Appeals + */ +export const tutorV1GetExamAppeals = (options: Options) => (options.client ?? client).get({ + responseType: 'json', + url: '/api/v1/tutor/exam/{id}/appeal', + ...options +}); + +/** + * Review Exam Appeals + */ +export const tutorV1ReviewExamAppeals = (options: Options) => (options.client ?? client).post({ + url: '/api/v1/tutor/exam/{id}/appeal', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Update Exam Question Solution + */ +export const tutorV1UpdateExamQuestionSolution = (options: Options) => (options.client ?? client).post({ + url: '/api/v1/tutor/exam/{id}/question/{question_id}/solution', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); diff --git a/web/src/api/types.gen.ts b/web/src/api/types.gen.ts index fee35f1..a3550d3 100644 --- a/web/src/api/types.gen.ts +++ b/web/src/api/types.gen.ts @@ -360,10 +360,6 @@ export type AppealSchema = { * Review */ review: string; - /** - * Closed - */ - closed: string | null; /** * Path */ @@ -1815,6 +1811,7 @@ export type CourseSchema = { */ export type CourseSessionSchema = { accessDate: AccessDateSchema; + gradingDate: GradingDateSchema; course: CourseSchema; engagement?: CourseEngagementSchema; /** @@ -2277,7 +2274,7 @@ export type DiscussionEarnedDetailsSchema = { /** * Tutorassessment */ - tutorAssessment: number; + tutorAssessment: number | null; }; /** @@ -5997,6 +5994,18 @@ export type CourseRelationSpec = { * CourseSpec */ export type CourseSpec = { + /** + * Gradeduedays + */ + gradeDueDays: number; + /** + * Appealdeadlinedays + */ + appealDeadlineDays: number; + /** + * Confirmduedays + */ + confirmDueDays: number; /** * Created */ @@ -6197,6 +6206,18 @@ export type CourseSaveSpec = { */ effortHours: number; level: LevelChoices; + /** + * Gradeduedays + */ + gradeDueDays: number; + /** + * Appealdeadlinedays + */ + appealDeadlineDays: number; + /** + * Confirmduedays + */ + confirmDueDays: number; /** * Honorcodeid */ @@ -6515,6 +6536,358 @@ export type SurveyAnswersSchema = { [key: string]: string; }; +/** + * AllocationSchema + */ +export type AllocationSchema = { + /** + * Id + */ + id: number; + content: TutorContentSchema; + contentType: ContentTypeSchema; +}; + +/** + * PaginatedResponse[AllocationSchema] + */ +export type PaginatedResponseAllocationSchema = { + /** + * Items + */ + items: Array; + /** + * Count + */ + count: number; + /** + * Size + */ + size: number; + /** + * Page + */ + page: number; + /** + * Pages + */ + pages: number; +}; + +/** + * TutorContentSchema + */ +export type TutorContentSchema = { + /** + * Id + */ + id: string; + /** + * Created + */ + created: string; + /** + * Title + */ + title: string; + /** + * Lastgrading + */ + lastGrading: string | null; + /** + * Submissioncount + */ + submissionCount: number; + /** + * Gradecompletedcount + */ + gradeCompletedCount: number; + /** + * Gradeconfirmedcount + */ + gradeConfirmedCount: number; + /** + * Appealcount + */ + appealCount: number; + /** + * Appealopencount + */ + appealOpenCount: number; +}; + +/** + * AllocationStatsSchema + */ +export type AllocationStatsSchema = { + /** + * Allocationcount + */ + allocationCount: number; + /** + * Submissioncount + */ + submissionCount: number; + /** + * Gradecompletedcount + */ + gradeCompletedCount: number; + /** + * Gradeconfirmedcount + */ + gradeConfirmedCount: number; + /** + * Appealcount + */ + appealCount: number; + /** + * Appealopencount + */ + appealOpenCount: number; +}; + +/** + * GradingDate + */ +export type GradingDate = { + /** + * Gradedue + */ + gradeDue: string; + /** + * Appealdeadline + */ + appealDeadline: string; + /** + * Confirmdue + */ + confirmDue: string; +}; + +/** + * PagedTutorExamGradeSchema + */ +export type PagedTutorExamGradeSchema = { + /** + * Items + */ + items: Array; + /** + * Count + */ + count: number; + /** + * Size + */ + size: number; + /** + * Page + */ + page: number; + /** + * Pages + */ + pages: number; +}; + +/** + * TutorExamGradeSchema + */ +export type TutorExamGradeSchema = { + /** + * Id + */ + id: number; + /** + * Created + */ + created: string; + /** + * Score + */ + score: number; + /** + * Passed + */ + passed: boolean; + /** + * Completed + */ + completed: string | null; + /** + * Confirmed + */ + confirmed: string | null; + /** + * Attemptretry + */ + attemptRetry: number; + gradingDate: GradingDate; +}; + +/** + * TutorExamGradePaperSchema + */ +export type TutorExamGradePaperSchema = { + /** + * Id + */ + id: number; + /** + * Earneddetails + */ + earnedDetails: { + [key: string]: number | null; + }; + /** + * Answers + */ + answers: { + [key: string]: string; + }; + /** + * Feedback + */ + feedback: { + [key: string]: string; + }; + grader: OwnerSchema | null; + /** + * Questions + */ + questions: Array; + /** + * Analysis + */ + analysis: { + [key: string]: { + [key: string]: number; + }; + }; +}; + +/** + * TutorExamQuestionSchema + */ +export type TutorExamQuestionSchema = { + /** + * Id + */ + id: number; + format: ExamQuestionFormatChoices; + /** + * Options + */ + options: Array; + /** + * Question + */ + question: string; + /** + * Supplement + */ + supplement: string; + /** + * Point + */ + point: number; + solution: ExamSolutionSchema | null; +}; + +/** + * TutorExamGradeSavedSchema + */ +export type TutorExamGradeSavedSchema = { + /** + * Score + */ + score: number; + /** + * Passed + */ + passed: boolean; + /** + * Completed + */ + completed: string | null; +}; + +/** + * TutorExamGradeSaveSchema + */ +export type TutorExamGradeSaveSchema = { + /** + * Earneddetails + */ + earnedDetails: { + [key: string]: number | null; + }; + /** + * Feedback + */ + feedback: { + [key: string]: string; + }; +}; + +/** + * PagedAppealSchema + */ +export type PagedAppealSchema = { + /** + * Items + */ + items: Array; + /** + * Count + */ + count: number; + /** + * Size + */ + size: number; + /** + * Page + */ + page: number; + /** + * Pages + */ + pages: number; +}; + +/** + * TutorExamAppealSaveSchema + */ +export type TutorExamAppealSaveSchema = { + /** + * Review + */ + review: string; + /** + * Appealids + */ + appealIds: Array; +}; + +/** + * TutorExamQuestionSolutionSchema + */ +export type TutorExamQuestionSolutionSchema = { + /** + * Correctanswers + */ + correctAnswers: Array; + /** + * Correctcriteria + */ + correctCriteria: string; + /** + * Explanation + */ + explanation: string; +}; + export type MinimaApiHealthData = { body?: never; path?: never; @@ -6788,6 +7161,7 @@ export type AssignmentV1GetSessionData = { id: string; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -6812,8 +7186,8 @@ export type AssignmentV1StartAttemptData = { id: string; }; query?: { - media?: string; mode?: string; + media?: string; course?: string; }; url: '/api/v1/assignment/{id}/attempt'; @@ -6851,6 +7225,7 @@ export type AssignmentV1SubmitAttemptData = { id: string; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -6875,6 +7250,7 @@ export type AssignmentV1DeactivateAttemptData = { id: string; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -7173,6 +7549,7 @@ export type ContentV1GetMediaData = { id: string; }; query?: { + mode?: string; media?: string; }; url: '/api/v1/content/media/{id}'; @@ -7196,6 +7573,7 @@ export type ContentV1GetSubtitlesData = { id: string; }; query?: { + mode?: string; media?: string; }; url: '/api/v1/content/media/{id}/subtitle'; @@ -7221,6 +7599,7 @@ export type ContentV1DeleteMediaWatchData = { id: string; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -7243,6 +7622,7 @@ export type ContentV1GetMediaWatchData = { id: string; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -7267,8 +7647,8 @@ export type ContentV1UpdateMediaWatchData = { id: string; }; query?: { - media?: string; mode?: string; + media?: string; course?: string; }; url: '/api/v1/content/media/{id}/watch'; @@ -7290,6 +7670,7 @@ export type ContentV1GetMediaNoteData = { id: string; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -7328,6 +7709,7 @@ export type ContentV1SaveMediaNoteData = { id: string; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -7445,6 +7827,7 @@ export type CourseV1GetSessionData = { id: string; }; query?: { + mode?: string; media?: string; }; url: '/api/v1/course/{id}/session'; @@ -7468,8 +7851,8 @@ export type CourseV1StartEngagementData = { id: string; }; query?: { - media?: string; mode?: string; + media?: string; }; url: '/api/v1/course/{id}/engage'; }; @@ -7534,6 +7917,7 @@ export type DiscussionV1GetSessionData = { id: string; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -7558,8 +7942,8 @@ export type DiscussionV1StartAttemptData = { id: string; }; query?: { - media?: string; mode?: string; + media?: string; course?: string; }; url: '/api/v1/discussion/{id}/attempt'; @@ -7583,6 +7967,7 @@ export type DiscussionV1DeactivateAttemptData = { id: string; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -7613,6 +7998,7 @@ export type DiscussionV1GetPostsData = { * Size */ size?: number; + mode?: string; media?: string; course?: string; }; @@ -7659,6 +8045,7 @@ export type DiscussionV1CreatePostData = { id: string; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -7683,6 +8070,7 @@ export type DiscussionV1GetOwnPostsData = { id: string; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -7713,6 +8101,7 @@ export type DiscussionV1DeletePostData = { post_id: number; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -7761,6 +8150,7 @@ export type DiscussionV1UpdatePostData = { post_id: number; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -7785,6 +8175,7 @@ export type ExamV1GetSessionData = { id: string; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -7809,8 +8200,8 @@ export type ExamV1StartAttemptData = { id: string; }; query?: { - media?: string; mode?: string; + media?: string; course?: string; }; url: '/api/v1/exam/{id}/attempt'; @@ -7834,6 +8225,7 @@ export type ExamV1SaveAnswersData = { id: string; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -7856,6 +8248,7 @@ export type ExamV1SubmitAttemptData = { id: string; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -7880,6 +8273,7 @@ export type ExamV1DeactivateAttemptData = { id: string; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -8631,6 +9025,7 @@ export type QuizV1GetSessionData = { id: string; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -8655,9 +9050,9 @@ export type QuizV1StartAttemptData = { id: string; }; query?: { + mode?: string; media?: string; course?: string; - mode?: string; }; url: '/api/v1/quiz/{id}/attempt'; }; @@ -8680,6 +9075,7 @@ export type QuizV1SubmitAttemptData = { id: string; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -8704,6 +9100,7 @@ export type QuizV1DeactivateAttemptData = { id: string; }; query?: { + mode?: string; media?: string; course?: string; }; @@ -10252,6 +10649,7 @@ export type SurveyV1GetSurveyData = { id: string; }; query?: { + mode?: string; media?: string; }; url: '/api/v1/survey/{id}'; @@ -10275,8 +10673,8 @@ export type SurveyV1SubmitData = { id: string; }; query?: { - media?: string; mode?: string; + media?: string; course?: string; }; url: '/api/v1/survey/{id}/submit'; @@ -10298,6 +10696,7 @@ export type SurveyV1ResultsData = { id: string; }; query?: { + mode?: string; media?: string; }; url: '/api/v1/survey/{id}/results'; @@ -10386,3 +10785,196 @@ export type SurveyV1ResultsAnonymousResponses = { }; export type SurveyV1ResultsAnonymousResponse = SurveyV1ResultsAnonymousResponses[keyof SurveyV1ResultsAnonymousResponses]; + +export type TutorV1GetAllocationData = { + body?: never; + path?: never; + query?: { + /** + * Page + */ + page?: number; + /** + * Size + */ + size?: number; + }; + url: '/api/v1/tutor/allocation'; +}; + +export type TutorV1GetAllocationResponses = { + /** + * OK + */ + 200: PaginatedResponseAllocationSchema; +}; + +export type TutorV1GetAllocationResponse = TutorV1GetAllocationResponses[keyof TutorV1GetAllocationResponses]; + +export type TutorV1GetAllocationStatsData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/tutor/allocation/stats'; +}; + +export type TutorV1GetAllocationStatsResponses = { + /** + * OK + */ + 200: AllocationStatsSchema; +}; + +export type TutorV1GetAllocationStatsResponse = TutorV1GetAllocationStatsResponses[keyof TutorV1GetAllocationStatsResponses]; + +export type TutorV1GetExamGradesData = { + body?: never; + path: { + /** + * Id + */ + id: string; + }; + query?: { + /** + * Page + */ + page?: number; + /** + * Size + */ + size?: number; + }; + url: '/api/v1/tutor/exam/{id}/grade'; +}; + +export type TutorV1GetExamGradesResponses = { + /** + * OK + */ + 200: PagedTutorExamGradeSchema; +}; + +export type TutorV1GetExamGradesResponse = TutorV1GetExamGradesResponses[keyof TutorV1GetExamGradesResponses]; + +export type TutorV1GetExamGradePaperData = { + body?: never; + path: { + /** + * Id + */ + id: string; + /** + * Grade Id + */ + grade_id: number; + }; + query?: never; + url: '/api/v1/tutor/exam/{id}/grade/{grade_id}'; +}; + +export type TutorV1GetExamGradePaperResponses = { + /** + * OK + */ + 200: TutorExamGradePaperSchema; +}; + +export type TutorV1GetExamGradePaperResponse = TutorV1GetExamGradePaperResponses[keyof TutorV1GetExamGradePaperResponses]; + +export type TutorV1CompleteExamGradeData = { + body: TutorExamGradeSaveSchema; + path: { + /** + * Id + */ + id: string; + /** + * Grade Id + */ + grade_id: number; + }; + query?: never; + url: '/api/v1/tutor/exam/{id}/grade/{grade_id}'; +}; + +export type TutorV1CompleteExamGradeResponses = { + /** + * OK + */ + 200: TutorExamGradeSavedSchema; +}; + +export type TutorV1CompleteExamGradeResponse = TutorV1CompleteExamGradeResponses[keyof TutorV1CompleteExamGradeResponses]; + +export type TutorV1GetExamAppealsData = { + body?: never; + path: { + /** + * Id + */ + id: string; + }; + query?: { + /** + * Page + */ + page?: number; + /** + * Size + */ + size?: number; + }; + url: '/api/v1/tutor/exam/{id}/appeal'; +}; + +export type TutorV1GetExamAppealsResponses = { + /** + * OK + */ + 200: PagedAppealSchema; +}; + +export type TutorV1GetExamAppealsResponse = TutorV1GetExamAppealsResponses[keyof TutorV1GetExamAppealsResponses]; + +export type TutorV1ReviewExamAppealsData = { + body: TutorExamAppealSaveSchema; + path: { + /** + * Id + */ + id: string; + }; + query?: never; + url: '/api/v1/tutor/exam/{id}/appeal'; +}; + +export type TutorV1ReviewExamAppealsResponses = { + /** + * OK + */ + 200: unknown; +}; + +export type TutorV1UpdateExamQuestionSolutionData = { + body: TutorExamQuestionSolutionSchema; + path: { + /** + * Id + */ + id: string; + /** + * Question Id + */ + question_id: number; + }; + query?: never; + url: '/api/v1/tutor/exam/{id}/question/{question_id}/solution'; +}; + +export type TutorV1UpdateExamQuestionSolutionResponses = { + /** + * OK + */ + 200: unknown; +}; diff --git a/web/src/api/valibot.gen.ts b/web/src/api/valibot.gen.ts index bf60246..24accc6 100644 --- a/web/src/api/valibot.gen.ts +++ b/web/src/api/valibot.gen.ts @@ -173,7 +173,6 @@ export const vAppealSchema = v.object({ questionId: v.pipe(v.number(), v.integer()), explanation: v.string(), review: v.string(), - closed: v.nullable(v.pipe(v.string(), v.isoTimestamp())), path: v.string() }); @@ -877,6 +876,7 @@ export const vCourseSchema = v.object({ */ export const vCourseSessionSchema = v.object({ accessDate: vAccessDateSchema, + gradingDate: vGradingDateSchema, course: vCourseSchema, engagement: v.optional(vCourseEngagementSchema), otpToken: v.optional(v.string()), @@ -1011,7 +1011,7 @@ export const vCourseCertificateRequestSchema = v.object({ export const vDiscussionEarnedDetailsSchema = v.object({ post: v.pipe(v.number(), v.integer()), reply: v.pipe(v.number(), v.integer()), - tutorAssessment: v.pipe(v.number(), v.integer()) + tutorAssessment: v.nullable(v.pipe(v.number(), v.integer())) }); /** @@ -2633,6 +2633,9 @@ export const vCourseAssetsSpec = v.object({ * CourseSpec */ export const vCourseSpec = v.object({ + gradeDueDays: v.pipe(v.number(), v.integer()), + appealDeadlineDays: v.pipe(v.number(), v.integer()), + confirmDueDays: v.pipe(v.number(), v.integer()), created: v.pipe(v.string(), v.isoTimestamp()), modified: v.pipe(v.string(), v.isoTimestamp()), title: v.string(), @@ -2673,6 +2676,9 @@ export const vCourseSaveSpec = v.object({ previewUrl: v.pipe(v.string(), v.url(), v.minLength(1), v.maxLength(2083)), effortHours: v.pipe(v.number(), v.integer()), level: vLevelChoices, + gradeDueDays: v.pipe(v.number(), v.integer()), + appealDeadlineDays: v.pipe(v.number(), v.integer()), + confirmDueDays: v.pipe(v.number(), v.integer()), honorCodeId: v.pipe(v.number(), v.integer()), faqId: v.pipe(v.number(), v.integer()), gradingPolicy: vGradingPolicySpec @@ -2827,6 +2833,158 @@ export const vSurveySchema = v.object({ */ export const vSurveyAnswersSchema = v.record(v.string(), v.pipe(v.string(), v.minLength(1))); +/** + * TutorContentSchema + */ +export const vTutorContentSchema = v.object({ + id: v.string(), + created: v.pipe(v.string(), v.isoTimestamp()), + title: v.string(), + lastGrading: v.nullable(v.pipe(v.string(), v.isoTimestamp())), + submissionCount: v.pipe(v.number(), v.integer()), + gradeCompletedCount: v.pipe(v.number(), v.integer()), + gradeConfirmedCount: v.pipe(v.number(), v.integer()), + appealCount: v.pipe(v.number(), v.integer()), + appealOpenCount: v.pipe(v.number(), v.integer()) +}); + +/** + * AllocationSchema + */ +export const vAllocationSchema = v.object({ + id: v.pipe(v.number(), v.integer()), + content: vTutorContentSchema, + contentType: vContentTypeSchema +}); + +/** + * PaginatedResponse[AllocationSchema] + */ +export const vPaginatedResponseAllocationSchema = v.object({ + items: v.array(vAllocationSchema), + count: v.pipe(v.number(), v.integer()), + size: v.pipe(v.number(), v.integer()), + page: v.pipe(v.number(), v.integer()), + pages: v.pipe(v.number(), v.integer()) +}); + +/** + * AllocationStatsSchema + */ +export const vAllocationStatsSchema = v.object({ + allocationCount: v.pipe(v.number(), v.integer()), + submissionCount: v.pipe(v.number(), v.integer()), + gradeCompletedCount: v.pipe(v.number(), v.integer()), + gradeConfirmedCount: v.pipe(v.number(), v.integer()), + appealCount: v.pipe(v.number(), v.integer()), + appealOpenCount: v.pipe(v.number(), v.integer()) +}); + +/** + * GradingDate + */ +export const vGradingDate = v.object({ + gradeDue: v.pipe(v.string(), v.isoTimestamp()), + appealDeadline: v.pipe(v.string(), v.isoTimestamp()), + confirmDue: v.pipe(v.string(), v.isoTimestamp()) +}); + +/** + * TutorExamGradeSchema + */ +export const vTutorExamGradeSchema = v.object({ + id: v.pipe(v.number(), v.integer()), + created: v.pipe(v.string(), v.isoTimestamp()), + score: v.number(), + passed: v.boolean(), + completed: v.nullable(v.pipe(v.string(), v.isoTimestamp())), + confirmed: v.nullable(v.pipe(v.string(), v.isoTimestamp())), + attemptRetry: v.pipe(v.number(), v.integer()), + gradingDate: vGradingDate +}); + +/** + * PagedTutorExamGradeSchema + */ +export const vPagedTutorExamGradeSchema = v.object({ + items: v.array(vTutorExamGradeSchema), + count: v.pipe(v.number(), v.integer()), + size: v.pipe(v.number(), v.integer()), + page: v.pipe(v.number(), v.integer()), + pages: v.pipe(v.number(), v.integer()) +}); + +/** + * TutorExamQuestionSchema + */ +export const vTutorExamQuestionSchema = v.object({ + id: v.pipe(v.number(), v.integer()), + format: vExamQuestionFormatChoices, + options: v.array(v.string()), + question: v.string(), + supplement: v.string(), + point: v.pipe(v.number(), v.integer()), + solution: v.nullable(vExamSolutionSchema) +}); + +/** + * TutorExamGradePaperSchema + */ +export const vTutorExamGradePaperSchema = v.object({ + id: v.pipe(v.number(), v.integer()), + earnedDetails: v.object({}), + answers: v.record(v.string(), v.string()), + feedback: v.record(v.string(), v.string()), + grader: v.nullable(vOwnerSchema), + questions: v.array(vTutorExamQuestionSchema), + analysis: v.record(v.string(), v.record(v.string(), v.pipe(v.number(), v.integer()))) +}); + +/** + * TutorExamGradeSavedSchema + */ +export const vTutorExamGradeSavedSchema = v.object({ + score: v.number(), + passed: v.boolean(), + completed: v.nullable(v.pipe(v.string(), v.isoTimestamp())) +}); + +/** + * TutorExamGradeSaveSchema + */ +export const vTutorExamGradeSaveSchema = v.object({ + earnedDetails: v.object({}), + feedback: v.record(v.string(), v.string()) +}); + +/** + * PagedAppealSchema + */ +export const vPagedAppealSchema = v.object({ + items: v.array(vAppealSchema), + count: v.pipe(v.number(), v.integer()), + size: v.pipe(v.number(), v.integer()), + page: v.pipe(v.number(), v.integer()), + pages: v.pipe(v.number(), v.integer()) +}); + +/** + * TutorExamAppealSaveSchema + */ +export const vTutorExamAppealSaveSchema = v.object({ + review: v.pipe(v.string(), v.minLength(1)), + appealIds: v.array(v.pipe(v.number(), v.integer())) +}); + +/** + * TutorExamQuestionSolutionSchema + */ +export const vTutorExamQuestionSolutionSchema = v.object({ + correctAnswers: v.array(v.string()), + correctCriteria: v.string(), + explanation: v.string() +}); + export const vMinimaApiHealthData = v.object({ body: v.optional(v.never()), path: v.optional(v.never()), @@ -2974,6 +3132,7 @@ export const vAssignmentV1GetSessionData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -2990,8 +3149,8 @@ export const vAssignmentV1StartAttemptData = v.object({ id: v.string() }), query: v.optional(v.object({ - media: v.optional(v.string()), mode: v.optional(v.string()), + media: v.optional(v.string()), course: v.optional(v.string()) })) }); @@ -3010,6 +3169,7 @@ export const vAssignmentV1SubmitAttemptData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -3026,6 +3186,7 @@ export const vAssignmentV1DeactivateAttemptData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -3186,6 +3347,7 @@ export const vContentV1GetMediaData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()) })) }); @@ -3201,6 +3363,7 @@ export const vContentV1GetSubtitlesData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()) })) }); @@ -3218,6 +3381,7 @@ export const vContentV1DeleteMediaWatchData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -3229,6 +3393,7 @@ export const vContentV1GetMediaWatchData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -3245,8 +3410,8 @@ export const vContentV1UpdateMediaWatchData = v.object({ id: v.string() }), query: v.optional(v.object({ - media: v.optional(v.string()), mode: v.optional(v.string()), + media: v.optional(v.string()), course: v.optional(v.string()) })) }); @@ -3257,6 +3422,7 @@ export const vContentV1GetMediaNoteData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -3276,6 +3442,7 @@ export const vContentV1SaveMediaNoteData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -3340,6 +3507,7 @@ export const vCourseV1GetSessionData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()) })) }); @@ -3355,8 +3523,8 @@ export const vCourseV1StartEngagementData = v.object({ id: v.string() }), query: v.optional(v.object({ - media: v.optional(v.string()), - mode: v.optional(v.string()) + mode: v.optional(v.string()), + media: v.optional(v.string()) })) }); @@ -3397,6 +3565,7 @@ export const vDiscussionV1GetSessionData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -3413,8 +3582,8 @@ export const vDiscussionV1StartAttemptData = v.object({ id: v.string() }), query: v.optional(v.object({ - media: v.optional(v.string()), mode: v.optional(v.string()), + media: v.optional(v.string()), course: v.optional(v.string()) })) }); @@ -3430,6 +3599,7 @@ export const vDiscussionV1DeactivateAttemptData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -3443,6 +3613,7 @@ export const vDiscussionV1GetPostsData = v.object({ query: v.optional(v.object({ page: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 1), size: v.optional(v.pipe(v.number(), v.integer(), v.maxValue(100)), 24), + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -3464,6 +3635,7 @@ export const vDiscussionV1CreatePostData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -3480,6 +3652,7 @@ export const vDiscussionV1GetOwnPostsData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -3499,6 +3672,7 @@ export const vDiscussionV1DeletePostData = v.object({ post_id: v.pipe(v.number(), v.integer()) }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -3516,6 +3690,7 @@ export const vDiscussionV1UpdatePostData = v.object({ post_id: v.pipe(v.number(), v.integer()) }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -3532,6 +3707,7 @@ export const vExamV1GetSessionData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -3548,8 +3724,8 @@ export const vExamV1StartAttemptData = v.object({ id: v.string() }), query: v.optional(v.object({ - media: v.optional(v.string()), mode: v.optional(v.string()), + media: v.optional(v.string()), course: v.optional(v.string()) })) }); @@ -3565,6 +3741,7 @@ export const vExamV1SaveAnswersData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -3576,6 +3753,7 @@ export const vExamV1SubmitAttemptData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -3592,6 +3770,7 @@ export const vExamV1DeactivateAttemptData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -3978,6 +4157,7 @@ export const vQuizV1GetSessionData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -3994,9 +4174,9 @@ export const vQuizV1StartAttemptData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), - course: v.optional(v.string()), - mode: v.optional(v.string()) + course: v.optional(v.string()) })) }); @@ -4011,6 +4191,7 @@ export const vQuizV1SubmitAttemptData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -4027,6 +4208,7 @@ export const vQuizV1DeactivateAttemptData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()), course: v.optional(v.string()) })) @@ -4871,6 +5053,7 @@ export const vSurveyV1GetSurveyData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()) })) }); @@ -4886,8 +5069,8 @@ export const vSurveyV1SubmitData = v.object({ id: v.string() }), query: v.optional(v.object({ - media: v.optional(v.string()), mode: v.optional(v.string()), + media: v.optional(v.string()), course: v.optional(v.string()) })) }); @@ -4898,6 +5081,7 @@ export const vSurveyV1ResultsData = v.object({ id: v.string() }), query: v.optional(v.object({ + mode: v.optional(v.string()), media: v.optional(v.string()) })) }); @@ -4946,3 +5130,105 @@ export const vSurveyV1ResultsAnonymousData = v.object({ * OK */ export const vSurveyV1ResultsAnonymousResponse = v.record(v.string(), v.record(v.string(), v.pipe(v.number(), v.integer()))); + +export const vTutorV1GetAllocationData = v.object({ + body: v.optional(v.never()), + path: v.optional(v.never()), + query: v.optional(v.object({ + page: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 1), + size: v.optional(v.pipe(v.number(), v.integer(), v.maxValue(100)), 24) + })) +}); + +/** + * OK + */ +export const vTutorV1GetAllocationResponse = vPaginatedResponseAllocationSchema; + +export const vTutorV1GetAllocationStatsData = v.object({ + body: v.optional(v.never()), + path: v.optional(v.never()), + query: v.optional(v.never()) +}); + +/** + * OK + */ +export const vTutorV1GetAllocationStatsResponse = vAllocationStatsSchema; + +export const vTutorV1GetExamGradesData = v.object({ + body: v.optional(v.never()), + path: v.object({ + id: v.string() + }), + query: v.optional(v.object({ + page: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 1), + size: v.optional(v.pipe(v.number(), v.integer(), v.maxValue(100)), 24) + })) +}); + +/** + * OK + */ +export const vTutorV1GetExamGradesResponse = vPagedTutorExamGradeSchema; + +export const vTutorV1GetExamGradePaperData = v.object({ + body: v.optional(v.never()), + path: v.object({ + id: v.string(), + grade_id: v.pipe(v.number(), v.integer()) + }), + query: v.optional(v.never()) +}); + +/** + * OK + */ +export const vTutorV1GetExamGradePaperResponse = vTutorExamGradePaperSchema; + +export const vTutorV1CompleteExamGradeData = v.object({ + body: vTutorExamGradeSaveSchema, + path: v.object({ + id: v.string(), + grade_id: v.pipe(v.number(), v.integer()) + }), + query: v.optional(v.never()) +}); + +/** + * OK + */ +export const vTutorV1CompleteExamGradeResponse = vTutorExamGradeSavedSchema; + +export const vTutorV1GetExamAppealsData = v.object({ + body: v.optional(v.never()), + path: v.object({ + id: v.string() + }), + query: v.optional(v.object({ + page: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 1), + size: v.optional(v.pipe(v.number(), v.integer(), v.maxValue(100)), 24) + })) +}); + +/** + * OK + */ +export const vTutorV1GetExamAppealsResponse = vPagedAppealSchema; + +export const vTutorV1ReviewExamAppealsData = v.object({ + body: vTutorExamAppealSaveSchema, + path: v.object({ + id: v.string() + }), + query: v.optional(v.never()) +}); + +export const vTutorV1UpdateExamQuestionSolutionData = v.object({ + body: vTutorExamQuestionSolutionSchema, + path: v.object({ + id: v.string(), + question_id: v.pipe(v.number(), v.integer()) + }), + query: v.optional(v.never()) +}); diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index 0f0caca..8dc8b34 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -9,10 +9,12 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' +import { Route as TutorRouteRouteImport } from './routes/tutor/route' import { Route as StudioRouteRouteImport } from './routes/studio/route' import { Route as authRouteRouteImport } from './routes/(auth)/route' import { Route as appRouteRouteImport } from './routes/(app)/route' import { Route as IndexRouteImport } from './routes/index' +import { Route as TutorIndexRouteImport } from './routes/tutor/index' import { Route as StudioIndexRouteImport } from './routes/studio/index' import { Route as authPasswordChangeRouteImport } from './routes/(auth)/password-change' import { Route as authLoginRouteImport } from './routes/(auth)/login' @@ -37,11 +39,17 @@ import { Route as appAccountLinkRouteImport } from './routes/(app)/account/link' import { Route as appAccountGroupRouteImport } from './routes/(app)/account/group' import { Route as appAccountEmailChangeRouteImport } from './routes/(app)/account/email-change' import { Route as appAccountDeviceRouteImport } from './routes/(app)/account/device' +import { Route as TutorExamIdGradingRouteImport } from './routes/tutor/exam/$id.grading' import { Route as appExamIdSessionRouteImport } from './routes/(app)/exam/$id.session' import { Route as appDiscussionIdSessionRouteImport } from './routes/(app)/discussion/$id.session' import { Route as appCourseIdSessionRouteImport } from './routes/(app)/course/$id.session' import { Route as appAssignmentIdSessionRouteImport } from './routes/(app)/assignment/$id.session' +const TutorRouteRoute = TutorRouteRouteImport.update({ + id: '/tutor', + path: '/tutor', + getParentRoute: () => rootRouteImport, +} as any) const StudioRouteRoute = StudioRouteRouteImport.update({ id: '/studio', path: '/studio', @@ -60,6 +68,11 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) +const TutorIndexRoute = TutorIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => TutorRouteRoute, +} as any) const StudioIndexRoute = StudioIndexRouteImport.update({ id: '/', path: '/', @@ -181,6 +194,11 @@ const appAccountDeviceRoute = appAccountDeviceRouteImport.update({ path: '/device', getParentRoute: () => appAccountRouteRoute, } as any) +const TutorExamIdGradingRoute = TutorExamIdGradingRouteImport.update({ + id: '/exam/$id/grading', + path: '/exam/$id/grading', + getParentRoute: () => TutorRouteRoute, +} as any) const appExamIdSessionRoute = appExamIdSessionRouteImport.update({ id: '/exam/$id/session', path: '/exam/$id/session', @@ -205,6 +223,7 @@ const appAssignmentIdSessionRoute = appAssignmentIdSessionRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/studio': typeof StudioRouteRouteWithChildren + '/tutor': typeof TutorRouteRouteWithChildren '/account': typeof appAccountRouteRouteWithChildren '/dashboard': typeof appDashboardRouteRouteWithChildren '/activate': typeof authActivateRoute @@ -212,6 +231,7 @@ export interface FileRoutesByFullPath { '/login': typeof authLoginRoute '/password-change': typeof authPasswordChangeRoute '/studio/': typeof StudioIndexRoute + '/tutor/': typeof TutorIndexRoute '/account/device': typeof appAccountDeviceRoute '/account/email-change': typeof appAccountEmailChangeRoute '/account/group': typeof appAccountGroupRoute @@ -233,6 +253,7 @@ export interface FileRoutesByFullPath { '/course/$id/session': typeof appCourseIdSessionRoute '/discussion/$id/session': typeof appDiscussionIdSessionRoute '/exam/$id/session': typeof appExamIdSessionRoute + '/tutor/exam/$id/grading': typeof TutorExamIdGradingRoute } export interface FileRoutesByTo { '/': typeof IndexRoute @@ -242,6 +263,7 @@ export interface FileRoutesByTo { '/login': typeof authLoginRoute '/password-change': typeof authPasswordChangeRoute '/studio': typeof StudioIndexRoute + '/tutor': typeof TutorIndexRoute '/account/device': typeof appAccountDeviceRoute '/account/email-change': typeof appAccountEmailChangeRoute '/account/group': typeof appAccountGroupRoute @@ -263,6 +285,7 @@ export interface FileRoutesByTo { '/course/$id/session': typeof appCourseIdSessionRoute '/discussion/$id/session': typeof appDiscussionIdSessionRoute '/exam/$id/session': typeof appExamIdSessionRoute + '/tutor/exam/$id/grading': typeof TutorExamIdGradingRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -270,6 +293,7 @@ export interface FileRoutesById { '/(app)': typeof appRouteRouteWithChildren '/(auth)': typeof authRouteRouteWithChildren '/studio': typeof StudioRouteRouteWithChildren + '/tutor': typeof TutorRouteRouteWithChildren '/(app)/account': typeof appAccountRouteRouteWithChildren '/(app)/dashboard': typeof appDashboardRouteRouteWithChildren '/(auth)/activate': typeof authActivateRoute @@ -277,6 +301,7 @@ export interface FileRoutesById { '/(auth)/login': typeof authLoginRoute '/(auth)/password-change': typeof authPasswordChangeRoute '/studio/': typeof StudioIndexRoute + '/tutor/': typeof TutorIndexRoute '/(app)/account/device': typeof appAccountDeviceRoute '/(app)/account/email-change': typeof appAccountEmailChangeRoute '/(app)/account/group': typeof appAccountGroupRoute @@ -298,12 +323,14 @@ export interface FileRoutesById { '/(app)/course/$id/session': typeof appCourseIdSessionRoute '/(app)/discussion/$id/session': typeof appDiscussionIdSessionRoute '/(app)/exam/$id/session': typeof appExamIdSessionRoute + '/tutor/exam/$id/grading': typeof TutorExamIdGradingRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/' | '/studio' + | '/tutor' | '/account' | '/dashboard' | '/activate' @@ -311,6 +338,7 @@ export interface FileRouteTypes { | '/login' | '/password-change' | '/studio/' + | '/tutor/' | '/account/device' | '/account/email-change' | '/account/group' @@ -332,6 +360,7 @@ export interface FileRouteTypes { | '/course/$id/session' | '/discussion/$id/session' | '/exam/$id/session' + | '/tutor/exam/$id/grading' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -341,6 +370,7 @@ export interface FileRouteTypes { | '/login' | '/password-change' | '/studio' + | '/tutor' | '/account/device' | '/account/email-change' | '/account/group' @@ -362,12 +392,14 @@ export interface FileRouteTypes { | '/course/$id/session' | '/discussion/$id/session' | '/exam/$id/session' + | '/tutor/exam/$id/grading' id: | '__root__' | '/' | '/(app)' | '/(auth)' | '/studio' + | '/tutor' | '/(app)/account' | '/(app)/dashboard' | '/(auth)/activate' @@ -375,6 +407,7 @@ export interface FileRouteTypes { | '/(auth)/login' | '/(auth)/password-change' | '/studio/' + | '/tutor/' | '/(app)/account/device' | '/(app)/account/email-change' | '/(app)/account/group' @@ -396,6 +429,7 @@ export interface FileRouteTypes { | '/(app)/course/$id/session' | '/(app)/discussion/$id/session' | '/(app)/exam/$id/session' + | '/tutor/exam/$id/grading' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -403,11 +437,19 @@ export interface RootRouteChildren { appRouteRoute: typeof appRouteRouteWithChildren authRouteRoute: typeof authRouteRouteWithChildren StudioRouteRoute: typeof StudioRouteRouteWithChildren + TutorRouteRoute: typeof TutorRouteRouteWithChildren publicSurveyIdRoute: typeof publicSurveyIdRoute } declare module '@tanstack/solid-router' { interface FileRoutesByPath { + '/tutor': { + id: '/tutor' + path: '/tutor' + fullPath: '/tutor' + preLoaderRoute: typeof TutorRouteRouteImport + parentRoute: typeof rootRouteImport + } '/studio': { id: '/studio' path: '/studio' @@ -436,6 +478,13 @@ declare module '@tanstack/solid-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/tutor/': { + id: '/tutor/' + path: '/' + fullPath: '/tutor/' + preLoaderRoute: typeof TutorIndexRouteImport + parentRoute: typeof TutorRouteRoute + } '/studio/': { id: '/studio/' path: '/' @@ -604,6 +653,13 @@ declare module '@tanstack/solid-router' { preLoaderRoute: typeof appAccountDeviceRouteImport parentRoute: typeof appAccountRouteRoute } + '/tutor/exam/$id/grading': { + id: '/tutor/exam/$id/grading' + path: '/exam/$id/grading' + fullPath: '/tutor/exam/$id/grading' + preLoaderRoute: typeof TutorExamIdGradingRouteImport + parentRoute: typeof TutorRouteRoute + } '/(app)/exam/$id/session': { id: '/(app)/exam/$id/session' path: '/exam/$id/session' @@ -738,11 +794,26 @@ const StudioRouteRouteWithChildren = StudioRouteRoute._addFileChildren( StudioRouteRouteChildren, ) +interface TutorRouteRouteChildren { + TutorIndexRoute: typeof TutorIndexRoute + TutorExamIdGradingRoute: typeof TutorExamIdGradingRoute +} + +const TutorRouteRouteChildren: TutorRouteRouteChildren = { + TutorIndexRoute: TutorIndexRoute, + TutorExamIdGradingRoute: TutorExamIdGradingRoute, +} + +const TutorRouteRouteWithChildren = TutorRouteRoute._addFileChildren( + TutorRouteRouteChildren, +) + const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, appRouteRoute: appRouteRouteWithChildren, authRouteRoute: authRouteRouteWithChildren, StudioRouteRoute: StudioRouteRouteWithChildren, + TutorRouteRoute: TutorRouteRouteWithChildren, publicSurveyIdRoute: publicSurveyIdRoute, } export const routeTree = rootRouteImport diff --git a/web/src/routes/(app)/-shared/AccountButton.tsx b/web/src/routes/(app)/-shared/AccountButton.tsx index 02f4b47..7fec2c3 100644 --- a/web/src/routes/(app)/-shared/AccountButton.tsx +++ b/web/src/routes/(app)/-shared/AccountButton.tsx @@ -1,5 +1,5 @@ import { IconListDetails, IconLogout, IconPuzzle2, IconUser } from '@tabler/icons-solidjs' -import { useNavigate } from '@tanstack/solid-router' +import { useNavigate, useRouter } from '@tanstack/solid-router' import { Show } from 'solid-js' import { accountStore } from '@/routes/(app)/account/-store' import { Avatar } from '@/shared/Avatar' @@ -8,6 +8,7 @@ import { logout } from './logout' export const AccountButton = () => { const { t } = useTranslation() + const router = useRouter() const navigate = useNavigate() const closeDropdown = () => { @@ -17,6 +18,7 @@ export const AccountButton = () => { const handleLogout = async () => { closeDropdown() await logout() + router.invalidate() } const goTo = (to: string) => { @@ -64,10 +66,10 @@ export const AccountButton = () => { diff --git a/web/src/routes/(app)/-shared/Inquiry.tsx b/web/src/routes/(app)/-shared/Inquiry.tsx index 2f381bc..7a86b7b 100644 --- a/web/src/routes/(app)/-shared/Inquiry.tsx +++ b/web/src/routes/(app)/-shared/Inquiry.tsx @@ -71,10 +71,7 @@ export const Inquiry = (props: Props) => { const [inquiries, setObserverEl, { setStore, refetch }] = createCachedInfiniteStore( 'operationV1GetInquiries', () => ({ query: { appLabel: props.appLabel, model: props.model, contentId: props.contentId } }), - async (options, page) => { - const { data } = await operationV1GetInquiries({ ...options, query: { ...options.query, page } }) - return data - }, + async (options, page) => (await operationV1GetInquiries({ ...options, query: { ...options.query, page } })).data, ) onMount(() => props.setRefreshHandler(() => refetch)) diff --git a/web/src/routes/(app)/-shared/Notification.tsx b/web/src/routes/(app)/-shared/Notification.tsx index e15ee94..129a467 100644 --- a/web/src/routes/(app)/-shared/Notification.tsx +++ b/web/src/routes/(app)/-shared/Notification.tsx @@ -18,16 +18,13 @@ export const Notification = () => { const [messages, setObserverEl, { setStore, refetch }] = createCachedInfiniteStore( 'operationV1GetUnreadMessages', () => (enabled() ? {} : undefined), - async (options, page) => { - const { data } = await operationV1GetUnreadMessages({ ...options, query: { page } }) - return data - }, + async (options, page) => (await operationV1GetUnreadMessages({ ...options, query: { page } })).data, ) onMount(() => { setTimeout(() => { setEnabled(true) - }, 1000 * 0.3) + }, 1000 * 1) }) const [unreadCount, setUnreadCount] = createSignal(0) diff --git a/web/src/routes/(app)/-shared/SearchBox.tsx b/web/src/routes/(app)/-shared/SearchBox.tsx index 7e79185..b117983 100644 --- a/web/src/routes/(app)/-shared/SearchBox.tsx +++ b/web/src/routes/(app)/-shared/SearchBox.tsx @@ -26,10 +26,7 @@ export const SearchBox = () => { if (!input_ || hasIncompleteKorean(input_)) return return { query: { q: input_, limit: 20 } } }, - async (options) => { - const { data } = await contentV1SearchSuggestion(options) - return data - }, + async (options) => (await contentV1SearchSuggestion(options)).data, ) const search = (q: string) => { diff --git a/web/src/routes/(app)/-shared/aichat/Chat.tsx b/web/src/routes/(app)/-shared/aichat/Chat.tsx index 6b23dd0..c984f61 100644 --- a/web/src/routes/(app)/-shared/aichat/Chat.tsx +++ b/web/src/routes/(app)/-shared/aichat/Chat.tsx @@ -18,10 +18,7 @@ export const Chat = () => { const chatListStore = createCachedStore( 'assistantV1GetChats', () => (open() ? {} : undefined), - async (options) => { - const { data } = await assistantV1GetChats(options) - return data - }, + async (options) => (await assistantV1GetChats(options)).data, ) const activeChat = () => chatListStore[0].data?.chats.find((chat) => chat.active) @@ -29,10 +26,7 @@ export const Chat = () => { const chatMessageStore = createCachedInfiniteStore( 'assistantV1GetChatMessages', () => (activeChat() ? { path: { id: activeChat()!.id } } : undefined), - async (options, page) => { - const { data } = await assistantV1GetChatMessages({ ...options, query: { page } }) - return data - }, + async (options, page) => (await assistantV1GetChatMessages({ ...options, query: { page } })).data, ) return ( diff --git a/web/src/routes/(app)/-shared/goal/CategorySelect.tsx b/web/src/routes/(app)/-shared/goal/CategorySelect.tsx index 7d4364d..d61ac81 100644 --- a/web/src/routes/(app)/-shared/goal/CategorySelect.tsx +++ b/web/src/routes/(app)/-shared/goal/CategorySelect.tsx @@ -23,10 +23,7 @@ export const CategorySelect = (props: Props) => { const [classifications] = createCachedStore( 'competencyV1GetClassificationTree', () => ({}), - async () => { - const { data } = await competencyV1GetClassificationTree() - return data - }, + async () => (await competencyV1GetClassificationTree()).data, ) const [selection, setSelection] = createStore({ firstLevel: 0, diff --git a/web/src/routes/(app)/-shared/goal/GoalForm.tsx b/web/src/routes/(app)/-shared/goal/GoalForm.tsx index 13939fb..6b11b02 100644 --- a/web/src/routes/(app)/-shared/goal/GoalForm.tsx +++ b/web/src/routes/(app)/-shared/goal/GoalForm.tsx @@ -31,10 +31,7 @@ export const GoalForm = (props: Props) => { const [skills] = createCachedStore( 'competencyV1GetClassificationSkillsData', () => (props.classIdForSkills ? { path: { id: props.classIdForSkills } } : undefined), - async (options) => { - const { data } = await competencyV1GetClassificationSkillsData(options) - return data - }, + async (options) => (await competencyV1GetClassificationSkillsData(options)).data, ) const form = createForm>({ diff --git a/web/src/routes/(app)/-shared/grading/Appeal.tsx b/web/src/routes/(app)/-shared/grading/Appeal.tsx index f87091e..38078b1 100644 --- a/web/src/routes/(app)/-shared/grading/Appeal.tsx +++ b/web/src/routes/(app)/-shared/grading/Appeal.tsx @@ -109,11 +109,11 @@ export const Appeal = (props: Props) => { - {appeal()!.closed ? t('Reviewed') : t('Pending')} + {appeal()!.review ? t('Reviewed') : t('Pending')} diff --git a/web/src/routes/(app)/-shared/grading/SessionStart.tsx b/web/src/routes/(app)/-shared/grading/SessionStart.tsx index 3738c08..1292b22 100644 --- a/web/src/routes/(app)/-shared/grading/SessionStart.tsx +++ b/web/src/routes/(app)/-shared/grading/SessionStart.tsx @@ -1,4 +1,4 @@ -import { createEffect, createSignal, type JSX, Show } from 'solid-js' +import { createEffect, createSignal, type JSX, onCleanup, Show } from 'solid-js' import { OTP_VERIFICATION_EXPIRY_SECONDS } from '@/config' import { ContentViewer } from '@/shared/ContentViewer' import { SubmitButton } from '@/shared/SubmitButton' @@ -41,7 +41,7 @@ export const SessionStart = (props: Props) => { }, OTP_VERIFICATION_EXPIRY_SECONDS * 1000 * 0.8, ) - return () => clearTimeout(timer) + onCleanup(() => clearTimeout(timer)) }) return ( diff --git a/web/src/routes/(app)/-shared/quiz/QuizDialog.tsx b/web/src/routes/(app)/-shared/quiz/QuizDialog.tsx index 839e368..0bd2758 100644 --- a/web/src/routes/(app)/-shared/quiz/QuizDialog.tsx +++ b/web/src/routes/(app)/-shared/quiz/QuizDialog.tsx @@ -22,10 +22,7 @@ export const QuizDialog = (props: Props) => { const [session, { setStore }] = createCachedStore( 'quizV1GetSession', () => ({ path: { id: props.id }, query: { ...props.inlineContext } }), - async (options) => { - const { data } = await quizV1GetSession(options) - return data - }, + async (options) => (await quizV1GetSession(options)).data, ) const s = () => session.data diff --git a/web/src/routes/(app)/-shared/thread/Thread.tsx b/web/src/routes/(app)/-shared/thread/Thread.tsx index 2f074b1..a2c585d 100644 --- a/web/src/routes/(app)/-shared/thread/Thread.tsx +++ b/web/src/routes/(app)/-shared/thread/Thread.tsx @@ -19,10 +19,7 @@ export const Thread = (props: ThreadContextValue['context']) => { const threadStore = createCachedStore( 'operationV1GetThread', () => ({ path: { app_label: appLabel, model, subject_id: subjectId } }), - async (options) => { - const { data } = await operationV1GetThread(options) - return data - }, + async (options) => (await operationV1GetThread(options)).data, ) const thread = () => threadStore[0].data @@ -30,10 +27,7 @@ export const Thread = (props: ThreadContextValue['context']) => { const commentStore = createCachedInfiniteStore( 'operationV1GetThreadComments', () => (thread() ? { path: { id: thread()!.id } } : undefined), - async (options, page) => { - const { data } = await operationV1GetThreadComments({ ...options, query: { page } }) - return data - }, + async (options, page) => (await operationV1GetThreadComments({ ...options, query: { page } })).data, ) const disabled = () => props.options?.readOnly || !!thread()?.closed diff --git a/web/src/routes/(app)/account/device.tsx b/web/src/routes/(app)/account/device.tsx index 3e489a8..69ae91a 100644 --- a/web/src/routes/(app)/account/device.tsx +++ b/web/src/routes/(app)/account/device.tsx @@ -18,10 +18,7 @@ function RouteComponent() { const [devices, { setStore }] = createCachedStore( 'operationV1GetDevices', () => ({}), - async () => { - const { data } = await operationV1GetDevices() - return data - }, + async () => (await operationV1GetDevices()).data, ) const deleteDevice = async (deviceId: number) => { diff --git a/web/src/routes/(app)/account/group.tsx b/web/src/routes/(app)/account/group.tsx index e3df1ed..5c5bec9 100644 --- a/web/src/routes/(app)/account/group.tsx +++ b/web/src/routes/(app)/account/group.tsx @@ -16,10 +16,7 @@ function RouteComponent() { const [members] = createCachedStore( 'partnerV1MemberInfos', () => ({}), - async () => { - const { data } = await partnerV1MemberInfos() - return data - }, + async () => (await partnerV1MemberInfos()).data, ) return ( diff --git a/web/src/routes/(app)/account/link.tsx b/web/src/routes/(app)/account/link.tsx index fb97cd7..7942eae 100644 --- a/web/src/routes/(app)/account/link.tsx +++ b/web/src/routes/(app)/account/link.tsx @@ -29,10 +29,7 @@ function RouteComponent() { const [accounts, { setStore }] = createCachedStore( 'ssoV1GetAccounts', () => ({}), - async () => { - const { data } = await ssoV1GetAccounts() - return data - }, + async () => (await ssoV1GetAccounts()).data, ) const accountMap = createMemo( diff --git a/web/src/routes/(app)/assignment/$id.session.tsx b/web/src/routes/(app)/assignment/$id.session.tsx index 0a490bc..92febd4 100644 --- a/web/src/routes/(app)/assignment/$id.session.tsx +++ b/web/src/routes/(app)/assignment/$id.session.tsx @@ -25,10 +25,7 @@ function RouteComponent() { const store = createCachedStore( 'assignmentV1GetSession', () => ({ path: { id: params().id }, query: accessContextParam() }), - async (options) => { - const { data } = await assignmentV1GetSession(options) - return data - }, + async (options) => (await assignmentV1GetSession(options)).data, ) const s = () => store[0].data diff --git a/web/src/routes/(app)/assignment/-session/GradingReview.tsx b/web/src/routes/(app)/assignment/-session/GradingReview.tsx index 4694c73..b531d41 100644 --- a/web/src/routes/(app)/assignment/-session/GradingReview.tsx +++ b/web/src/routes/(app)/assignment/-session/GradingReview.tsx @@ -66,7 +66,7 @@ export const GradingReview = () => {
diff --git a/web/src/routes/(app)/course/$id.session.tsx b/web/src/routes/(app)/course/$id.session.tsx index 18d697e..44c60c7 100644 --- a/web/src/routes/(app)/course/$id.session.tsx +++ b/web/src/routes/(app)/course/$id.session.tsx @@ -36,10 +36,7 @@ function RouteComponent() { const store = createCachedStore( 'courseV1GetSession', () => ({ path: { id: courseId } }), - async (options) => { - const { data } = await courseV1GetSession(options) - return data - }, + async (options) => (await courseV1GetSession(options)).data, ) const s = () => store[0].data diff --git a/web/src/routes/(app)/course/-session/Achievement.tsx b/web/src/routes/(app)/course/-session/Achievement.tsx index 705ab72..88c6908 100644 --- a/web/src/routes/(app)/course/-session/Achievement.tsx +++ b/web/src/routes/(app)/course/-session/Achievement.tsx @@ -17,10 +17,7 @@ export const Achievement = () => { const [certificates] = createCachedStore( 'competencyV1GetCertificates', () => ({ query: { courseId: s().course.id } }), - async (options) => { - const { data } = await competencyV1GetCertificates(options) - return data - }, + async (options) => (await competencyV1GetCertificates(options)).data, ) return ( diff --git a/web/src/routes/(app)/course/-session/CourseDetail.tsx b/web/src/routes/(app)/course/-session/CourseDetail.tsx index 079233d..3ef8e0b 100644 --- a/web/src/routes/(app)/course/-session/CourseDetail.tsx +++ b/web/src/routes/(app)/course/-session/CourseDetail.tsx @@ -17,10 +17,7 @@ export const CourseDetail = () => { const [courseDetail] = createCachedStore( 'courseV1GetDetail', () => ({ path: { id: s().course.id } }), - async (options) => { - const { data } = await courseV1GetDetail(options) - return data - }, + async (options) => (await courseV1GetDetail(options)).data, ) return ( diff --git a/web/src/routes/(app)/course/-session/GettingStarted.tsx b/web/src/routes/(app)/course/-session/GettingStarted.tsx index 5ddff06..e6f2f8e 100644 --- a/web/src/routes/(app)/course/-session/GettingStarted.tsx +++ b/web/src/routes/(app)/course/-session/GettingStarted.tsx @@ -51,6 +51,14 @@ export const GettingStarted = (props: Props) => { {t('Review')} {`${new Date(s().accessDate.archive).toLocaleDateString()}`} + + {t('Grading Due')} + {`${new Date(s().gradingDate.gradeDue).toLocaleDateString()}`} + + + {t('Grade Confirm Due')} + {`${new Date(s().gradingDate.confirmDue).toLocaleDateString()}`} + diff --git a/web/src/routes/(app)/course/-session/Schedule.tsx b/web/src/routes/(app)/course/-session/Schedule.tsx index 02197b2..5d903a8 100644 --- a/web/src/routes/(app)/course/-session/Schedule.tsx +++ b/web/src/routes/(app)/course/-session/Schedule.tsx @@ -102,6 +102,8 @@ export const Schedule = () => { const courseStart = new Date(s().accessDate.start) const courseEnd = new Date(s().accessDate.end) const courseArchive = new Date(s().accessDate.archive) + const gradingDue = new Date(s().gradingDate.gradeDue) + const confirmDue = new Date(s().gradingDate.confirmDue) const now = new Date() const monthMarkers = createMemo(() => { @@ -152,6 +154,28 @@ export const Schedule = () => {
{t('End')}

+
  • +
    +
    {t(gradingDue.toLocaleDateString())}
    +
    + gradingDue} fallback={}> + + +
    +
    {t('Grading')}
    +
    +
  • +
  • +
    +
    {t(confirmDue.toLocaleDateString())}
    +
    + confirmDue} fallback={}> + + +
    +
    {t('Grading Confirm')}
    +
    +

  • {t(courseArchive.toLocaleDateString())}
    diff --git a/web/src/routes/(app)/dashboard/achievement.tsx b/web/src/routes/(app)/dashboard/achievement.tsx index 9eb88ed..ed28999 100644 --- a/web/src/routes/(app)/dashboard/achievement.tsx +++ b/web/src/routes/(app)/dashboard/achievement.tsx @@ -19,10 +19,7 @@ function RouteComponent() { const [certificates, setObserverEl, { refetch }] = createCachedInfiniteStore( 'competencyV1GetCertificateAwards', () => ({}), - async (options, page) => { - const { data } = await competencyV1GetCertificateAwards({ ...options, query: { page } }) - return data - }, + async (options, page) => (await competencyV1GetCertificateAwards({ ...options, query: { page } })).data, ) onMount(() => setRefreshHandler(() => refetch)) diff --git a/web/src/routes/(app)/dashboard/announcement.tsx b/web/src/routes/(app)/dashboard/announcement.tsx index cea3ddb..73eb388 100644 --- a/web/src/routes/(app)/dashboard/announcement.tsx +++ b/web/src/routes/(app)/dashboard/announcement.tsx @@ -21,10 +21,7 @@ function RouteComponent() { const [announcements, setObserverEl, { setStore, refetch }] = createCachedInfiniteStore( 'operationV1GetAnnouncements', () => ({}), - async (options, page) => { - const { data } = await operationV1GetAnnouncements({ ...options, query: { page } }) - return data - }, + async (options, page) => (await operationV1GetAnnouncements({ ...options, query: { page } })).data, ) const { setRefreshHandler } = useDashboard() diff --git a/web/src/routes/(app)/dashboard/catalog.tsx b/web/src/routes/(app)/dashboard/catalog.tsx index 0492d6a..bf20aec 100644 --- a/web/src/routes/(app)/dashboard/catalog.tsx +++ b/web/src/routes/(app)/dashboard/catalog.tsx @@ -30,10 +30,7 @@ function RouteComponent() { const [catalogs] = createCachedStore( 'learningV1GetCatalogs', () => ({}), - async (params) => { - const { data } = await learningV1GetCatalogs(params) - return data - }, + async (params) => (await learningV1GetCatalogs(params)).data, ) return ( @@ -137,10 +134,7 @@ const ItemList = (props: ItemListProps) => { const [items, setObserverEl, { setStore, refetch }] = createCachedInfiniteStore( 'learningV1GetCatalogItems', () => (props.open ? { path: { id: props.catalog.id } } : undefined), - async (options, page) => { - const { data } = await learningV1GetCatalogItems({ ...options, query: { page } }) - return data - }, + async (options, page) => (await learningV1GetCatalogItems({ ...options, query: { page } })).data, ) return ( diff --git a/web/src/routes/(app)/dashboard/goal.tsx b/web/src/routes/(app)/dashboard/goal.tsx index 91bf250..e8f78c9 100644 --- a/web/src/routes/(app)/dashboard/goal.tsx +++ b/web/src/routes/(app)/dashboard/goal.tsx @@ -20,10 +20,7 @@ function RouteComponent() { const [goals, { setStore }] = createCachedStore( 'competencyV1GetCompetencyGoals', () => ({}), - async () => { - const { data } = await competencyV1GetCompetencyGoals() - return data - }, + async () => (await competencyV1GetCompetencyGoals()).data, ) const existingGoalClassIds = () => goals.data?.map((g) => g.classification.id) @@ -32,7 +29,7 @@ function RouteComponent() { return (
    -
    {t('Competency goals')}
    +
    {t('Competency goals')}
    @@ -77,7 +74,7 @@ function RouteComponent() { {formatDistanceToNow(item.modified, { addSuffix: true })} -
    +
    ) } diff --git a/web/src/routes/(app)/media/$id.tsx b/web/src/routes/(app)/media/$id.tsx index 53d6b3c..56b1dfa 100644 --- a/web/src/routes/(app)/media/$id.tsx +++ b/web/src/routes/(app)/media/$id.tsx @@ -40,10 +40,7 @@ function RouteComponent() { const [media] = createCachedStore( 'contentV1GetMedia', () => ({ path: { id: params().id }, query: accessContextParam() }), - async (options) => { - const { data } = await contentV1GetMedia(options) - return data - }, + async (options) => (await contentV1GetMedia(options)).data, ) const [currentTime, setCurrentTime] = createSignal(0) diff --git a/web/src/routes/(app)/media/-media/Note.tsx b/web/src/routes/(app)/media/-media/Note.tsx index 6983509..9e605cf 100644 --- a/web/src/routes/(app)/media/-media/Note.tsx +++ b/web/src/routes/(app)/media/-media/Note.tsx @@ -27,10 +27,7 @@ export const Note = (props: Props) => { const [note, { setStore }] = createCachedStore( 'contentV1GetMediaNote', () => ({ path: { id: props.mediaId }, query: accessContextParam() }), - async (options) => { - const { data } = await contentV1GetMediaNote(options) - return data - }, + async (options) => (await contentV1GetMediaNote(options)).data, ) createEffect(() => { diff --git a/web/src/routes/(app)/route.tsx b/web/src/routes/(app)/route.tsx index 253dbda..8edd0f3 100644 --- a/web/src/routes/(app)/route.tsx +++ b/web/src/routes/(app)/route.tsx @@ -1,4 +1,4 @@ -import { createFileRoute, Outlet, redirect } from '@tanstack/solid-router' +import { createFileRoute, Outlet } from '@tanstack/solid-router' import { createEffect, onMount, Suspense } from 'solid-js' import * as v from 'valibot' import { learningV1GetRecords, operationV1RegisterDevice } from '@/api' @@ -10,6 +10,7 @@ import { NavbarLogo } from '@/shared/NavbarLogo' import { createCachedStore } from '@/shared/solid/cached-store' import { ThemeButton } from '@/shared/ThemeButton' import { getDeviceName } from '@/shared/utils' +import { protectedRoute } from '../protected' import { currentDevice, setCurrentDevice } from './-device' import { AccountButton } from './-shared/AccountButton' import { Chat } from './-shared/aichat/Chat' @@ -21,23 +22,11 @@ const searchSchema = v.object({ export const Route = createFileRoute('/(app)')({ validateSearch: searchSchema, - beforeLoad: async () => { - if (!accountStore.user) { - const nextPath = location.pathname + location.search - const shouldIgnoreNext = location.search.includes('token=') - - throw redirect({ - to: '/login', - search: shouldIgnoreNext ? undefined : { next: nextPath }, - }) - } - }, + beforeLoad: protectedRoute, component: RouteComponent, }) function RouteComponent() { - const navigate = Route.useNavigate() - // local database onMount(async () => { createCachedStore( @@ -51,19 +40,6 @@ function RouteComponent() { ) }) - // protected route - createEffect(() => { - if (!accountStore.user && !location.pathname.startsWith('/login')) { - const nextPath = location.pathname + location.search - const shouldIgnoreNext = location.search.includes('token=') - - navigate({ - to: '/login', - search: shouldIgnoreNext ? undefined : { next: nextPath }, - }) - } - }) - createEffect(async () => { if (currentDevice()) return @@ -90,6 +66,7 @@ function RouteComponent() { - -
    - -
    + + + +
    diff --git a/web/src/routes/studio/-course/data.ts b/web/src/routes/studio/-course/data.ts index 5495d61..8ed1a10 100644 --- a/web/src/routes/studio/-course/data.ts +++ b/web/src/routes/studio/-course/data.ts @@ -23,6 +23,9 @@ export const EmptyCourse = (): CourseSpec => { effortHours: -1, level: '' as LevelChoices, published: null, + gradeDueDays: -1, + appealDeadlineDays: -1, + confirmDueDays: -1, honorCodeId: -1, faqId: -1, gradingPolicy: { assessmentWeight: -1, completionWeight: -1, completionPassingPoint: -1 }, @@ -51,6 +54,9 @@ export const vCourseEditingSpec = v.object({ featured: v.boolean(), passingPoint: v.pipe(v.number(), v.integer(), v.minValue(0, AT_LEAST_ZERO)), maxAttempts: v.pipe(v.number(), v.integer(), v.minValue(0, AT_LEAST_ZERO)), + gradeDueDays: v.pipe(v.number(), v.integer(), v.minValue(0, AT_LEAST_ZERO)), + appealDeadlineDays: v.pipe(v.number(), v.integer(), v.minValue(0, AT_LEAST_ZERO)), + confirmDueDays: v.pipe(v.number(), v.integer(), v.minValue(0, AT_LEAST_ZERO)), verificationRequired: v.boolean(), objective: v.pipe(v.string(), v.nonEmpty(REQUIRED)), previewUrl: v.pipe(v.string(), v.url('URL address')), diff --git a/web/src/routes/studio/-studio/NavbarLogo.tsx b/web/src/routes/studio/-studio/NavbarLogo.tsx deleted file mode 100644 index f26aef2..0000000 --- a/web/src/routes/studio/-studio/NavbarLogo.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { useNavigate } from '@tanstack/solid-router' - -export const NavbarLogo = () => { - const navigate = useNavigate() - - return ( -
    navigate({ to: '/dashboard' })}> - Logo - -
    - ) -} diff --git a/web/src/routes/studio/route.tsx b/web/src/routes/studio/route.tsx index 60cd114..5b57e89 100644 --- a/web/src/routes/studio/route.tsx +++ b/web/src/routes/studio/route.tsx @@ -1,29 +1,16 @@ -import { createFileRoute, Outlet, redirect } from '@tanstack/solid-router' -import { createEffect } from 'solid-js' -import { accountStore } from '@/routes/(app)/account/-store' +import { createFileRoute, Outlet } from '@tanstack/solid-router' import { GoToTop } from '@/shared/GoToTop' +import { NavbarLogo } from '@/shared/NavbarLogo' import { ThemeButton } from '@/shared/ThemeButton' import { AccountButton } from '../(app)/-shared/AccountButton' -import { NavbarLogo } from './-studio/NavbarLogo' +import { protectedRoute } from '../protected' export const Route = createFileRoute('/studio')({ - beforeLoad: async () => { - if (!accountStore.user?.roles.includes('editor')) { - throw redirect({ to: '/dashboard' }) - } - }, + beforeLoad: protectedRoute, component: RouteComponent, }) function RouteComponent() { - const navigate = Route.useNavigate() - - createEffect(() => { - if (!accountStore.user?.roles.includes('editor')) { - navigate({ to: '/dashboard', replace: true }) - } - }) - return ( <>