diff --git a/core/apps/assignment/admin.py b/core/apps/assignment/admin.py index c945618..b808e7f 100644 --- a/core/apps/assignment/admin.py +++ b/core/apps/assignment/admin.py @@ -19,7 +19,6 @@ QuestionPool, Rubric, RubricCriterion, - Solution, Submission, ) from apps.common.admin import ( @@ -43,22 +42,12 @@ class QuestionInline(TabularInline[Question]): @admin.register(Question) class QuestionAdmin(HiddenModelAdmin[Question]): - class SolutionInline(TabularInline[Solution]): - model = Solution - - inlines = (SolutionInline,) - def formfield_for_dbfield(self, db_field, request, **kwargs: object): if db_field.name == "supplement": kwargs["widget"] = WysiwygWidget() return super().formfield_for_dbfield(db_field, request, **kwargs) -@admin.register(Solution) -class SolutionAdmin(HiddenModelAdmin[Solution]): - pass - - @admin.register(Rubric) class RubricAdmin(ModelAdmin[Rubric]): class RubricCriterionInline(TabularInline[RubricCriterion]): @@ -158,14 +147,11 @@ class GradeEventInline(ReadOnlyTabularInline[Grade.pgh_event_model]): @action(description=_("Grade"), permissions=["grade"]) # type: ignore # gettext not working def grade(self, request: HttpRequest, obj: Grade): - grade = Grade.objects.select_related("attempt__assignment", "attempt__question__solution__rubric").get( - pk=obj.pk - ) + grade = Grade.objects.select_related("attempt__assignment", "attempt__question").get(pk=obj.pk) prefetch_related_objects( - [grade.attempt], + [grade.attempt.assignment], Prefetch( - "question__solution__rubric__rubric_criteria__performance_levels", - queryset=PerformanceLevel.objects.order_by("point"), + "rubric__rubric_criteria__performance_levels", queryset=PerformanceLevel.objects.order_by("point") ), ) async_to_sync(grade.grade)(grader_id=cast(str, request.user.pk) if request.user else None) diff --git a/core/apps/assignment/api/schema.py b/core/apps/assignment/api/schema.py index ba1625c..eb2ca8e 100644 --- a/core/apps/assignment/api/schema.py +++ b/core/apps/assignment/api/schema.py @@ -22,6 +22,8 @@ class AssignmentSchema(LearningObjectMixinSchema): id: str owner: OwnerSchema honor_code: HonorCodeSchema + rubric_data: RubricSchema | None + sample_attachment: str | None class AssignmentSubmissionSchema(TimeStampedMixinSchema): @@ -39,9 +41,7 @@ class AssignmentQuestionSchema(Schema): supplement: str attachment_file_count: int attachment_file_types: list[str] - sample_attachment: str | None plagiarism_threshold: int - solution: "AssignmentSolutionSchema" @staticmethod def resolve_supplement(obj: Question): @@ -58,12 +58,6 @@ class AssignmentGradeSchema(GradeFieldMixinSchema, TimeStampedMixinSchema): id: int -class AssignmentSolutionSchema(Schema): - id: int - rubric_data: "RubricSchema" - explanation: str - - class RubricSchema(Schema): name: str description: str diff --git a/core/apps/assignment/fixtures/generic_rubric_en.json b/core/apps/assignment/fixtures/generic_rubric_en.json new file mode 100644 index 0000000..79f0a8e --- /dev/null +++ b/core/apps/assignment/fixtures/generic_rubric_en.json @@ -0,0 +1,167 @@ +[ + { + "model": "assignment.rubric", + "pk": 1, + "fields": { + "name": "General Rubric", + "description": "A general-purpose rubric for evaluating assignments." + } + }, + { + "model": "assignment.rubriccriterion", + "pk": 1, + "fields": { + "rubric": 1, + "name": "Content", + "description": "Evaluates the accuracy, relevance, and depth of the content." + } + }, + { + "model": "assignment.rubriccriterion", + "pk": 2, + "fields": { + "rubric": 1, + "name": "Organization", + "description": "Evaluates the logical structure and flow of the response." + } + }, + { + "model": "assignment.rubriccriterion", + "pk": 3, + "fields": { + "rubric": 1, + "name": "Expression", + "description": "Evaluates clarity, grammar, and overall quality of writing." + } + }, + { + "model": "assignment.performancelevel", + "pk": 1, + "fields": { + "criterion": 1, + "name": "Poor", + "description": "Content is largely inaccurate, irrelevant, or missing.", + "point": 1 + } + }, + { + "model": "assignment.performancelevel", + "pk": 2, + "fields": { + "criterion": 1, + "name": "Developing", + "description": "Content is partially accurate but lacks depth or relevance.", + "point": 2 + } + }, + { + "model": "assignment.performancelevel", + "pk": 3, + "fields": { + "criterion": 1, + "name": "Proficient", + "description": "Content is mostly accurate and relevant with adequate depth.", + "point": 3 + } + }, + { + "model": "assignment.performancelevel", + "pk": 4, + "fields": { + "criterion": 1, + "name": "Excellent", + "description": "Content is accurate, highly relevant, and demonstrates strong depth.", + "point": 4 + } + }, + { + "model": "assignment.performancelevel", + "pk": 5, + "fields": { + "criterion": 2, + "name": "Poor", + "description": "Response lacks clear structure and is difficult to follow.", + "point": 1 + } + }, + { + "model": "assignment.performancelevel", + "pk": 6, + "fields": { + "criterion": 2, + "name": "Developing", + "description": "Some structure is present but transitions and flow are inconsistent.", + "point": 2 + } + }, + { + "model": "assignment.performancelevel", + "pk": 7, + "fields": { + "criterion": 2, + "name": "Proficient", + "description": "Response is well-organized with clear transitions.", + "point": 3 + } + }, + { + "model": "assignment.performancelevel", + "pk": 8, + "fields": { + "criterion": 2, + "name": "Excellent", + "description": "Response is exceptionally well-structured and easy to follow.", + "point": 4 + } + }, + { + "model": "assignment.performancelevel", + "pk": 9, + "fields": { + "criterion": 3, + "name": "Poor", + "description": "Writing is unclear with frequent grammatical errors.", + "point": 1 + } + }, + { + "model": "assignment.performancelevel", + "pk": 10, + "fields": { + "criterion": 3, + "name": "Developing", + "description": "Writing is somewhat clear but has noticeable errors.", + "point": 2 + } + }, + { + "model": "assignment.performancelevel", + "pk": 11, + "fields": { + "criterion": 3, + "name": "Proficient", + "description": "Writing is clear and mostly free of grammatical errors.", + "point": 3 + } + }, + { + "model": "assignment.performancelevel", + "pk": 12, + "fields": { + "criterion": 3, + "name": "Excellent", + "description": "Writing is exceptionally clear, polished, and error-free.", + "point": 4 + } + }, + { + "model": "assignment.performancelevel", + "pk": 13, + "fields": { + "criterion": 3, + "name": "Outstanding", + "description": "Writing demonstrates exceptional style, precision, and sophistication.", + "point": 5 + } + } +] diff --git a/core/apps/assignment/fixtures/generic_rubric_ko.json b/core/apps/assignment/fixtures/generic_rubric_ko.json new file mode 100644 index 0000000..3e628a7 --- /dev/null +++ b/core/apps/assignment/fixtures/generic_rubric_ko.json @@ -0,0 +1,167 @@ +[ + { + "model": "assignment.rubric", + "pk": 1, + "fields": { + "name": "일반 루브릭", + "description": "과제 평가를 위한 일반적인 루브릭입니다." + } + }, + { + "model": "assignment.rubriccriterion", + "pk": 1, + "fields": { + "rubric": 1, + "name": "내용", + "description": "내용의 정확성, 관련성, 깊이를 평가합니다." + } + }, + { + "model": "assignment.rubriccriterion", + "pk": 2, + "fields": { + "rubric": 1, + "name": "구성", + "description": "답변의 논리적 구조와 흐름을 평가합니다." + } + }, + { + "model": "assignment.rubriccriterion", + "pk": 3, + "fields": { + "rubric": 1, + "name": "표현", + "description": "명확성, 문법, 전반적인 글쓰기 품질을 평가합니다." + } + }, + { + "model": "assignment.performancelevel", + "pk": 1, + "fields": { + "criterion": 1, + "name": "미흡", + "description": "내용이 대부분 부정확하거나 관련성이 없거나 누락되어 있습니다.", + "point": 1 + } + }, + { + "model": "assignment.performancelevel", + "pk": 2, + "fields": { + "criterion": 1, + "name": "보통", + "description": "내용이 부분적으로 정확하나 깊이나 관련성이 부족합니다.", + "point": 2 + } + }, + { + "model": "assignment.performancelevel", + "pk": 3, + "fields": { + "criterion": 1, + "name": "우수", + "description": "내용이 대체로 정확하고 관련성이 있으며 적절한 깊이를 갖추고 있습니다.", + "point": 3 + } + }, + { + "model": "assignment.performancelevel", + "pk": 4, + "fields": { + "criterion": 1, + "name": "탁월", + "description": "내용이 정확하고 매우 관련성이 높으며 뛰어난 깊이를 보여줍니다.", + "point": 4 + } + }, + { + "model": "assignment.performancelevel", + "pk": 5, + "fields": { + "criterion": 2, + "name": "미흡", + "description": "명확한 구조가 없어 내용을 따라가기 어렵습니다.", + "point": 1 + } + }, + { + "model": "assignment.performancelevel", + "pk": 6, + "fields": { + "criterion": 2, + "name": "보통", + "description": "구조가 일부 있으나 전환과 흐름이 일관되지 않습니다.", + "point": 2 + } + }, + { + "model": "assignment.performancelevel", + "pk": 7, + "fields": { + "criterion": 2, + "name": "우수", + "description": "명확한 전환이 있는 잘 구성된 답변입니다.", + "point": 3 + } + }, + { + "model": "assignment.performancelevel", + "pk": 8, + "fields": { + "criterion": 2, + "name": "탁월", + "description": "매우 체계적으로 구성되어 있어 읽기 쉽습니다.", + "point": 4 + } + }, + { + "model": "assignment.performancelevel", + "pk": 9, + "fields": { + "criterion": 3, + "name": "미흡", + "description": "글쓰기가 불명확하고 문법 오류가 잦습니다.", + "point": 1 + } + }, + { + "model": "assignment.performancelevel", + "pk": 10, + "fields": { + "criterion": 3, + "name": "보통", + "description": "글쓰기가 어느 정도 명확하나 눈에 띄는 오류가 있습니다.", + "point": 2 + } + }, + { + "model": "assignment.performancelevel", + "pk": 11, + "fields": { + "criterion": 3, + "name": "우수", + "description": "글쓰기가 명확하고 문법 오류가 거의 없습니다.", + "point": 3 + } + }, + { + "model": "assignment.performancelevel", + "pk": 12, + "fields": { + "criterion": 3, + "name": "탁월", + "description": "글쓰기가 매우 명확하고 세련되며 오류가 없습니다.", + "point": 4 + } + }, + { + "model": "assignment.performancelevel", + "pk": 13, + "fields": { + "criterion": 3, + "name": "최우수", + "description": "뛰어난 문체, 정확성, 세련됨을 갖춘 글쓰기입니다.", + "point": 5 + } + } +] diff --git a/core/apps/assignment/migrations/0004_remove_solution_question_remove_solution_rubric_and_more.py b/core/apps/assignment/migrations/0004_remove_solution_question_remove_solution_rubric_and_more.py new file mode 100644 index 0000000..e856db7 --- /dev/null +++ b/core/apps/assignment/migrations/0004_remove_solution_question_remove_solution_rubric_and_more.py @@ -0,0 +1,82 @@ +# Generated by Django 6.0.3 on 2026-03-06 09:25 + +import django.db.models.deletion +import pgtrigger.compiler +import pgtrigger.migrations +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assignment', '0003_remove_assignment_insert_insert_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='solution', + name='question', + ), + migrations.RemoveField( + model_name='solution', + name='rubric', + ), + migrations.RemoveField( + model_name='solutionevent', + name='pgh_obj', + ), + migrations.RemoveField( + model_name='solutionevent', + name='pgh_context', + ), + migrations.RemoveField( + model_name='solutionevent', + name='question', + ), + migrations.RemoveField( + model_name='solutionevent', + name='rubric', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='assignment', + name='insert_insert', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='assignment', + name='update_update', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='assignment', + name='delete_delete', + ), + migrations.AddField( + model_name='assignment', + name='rubric', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='assignment.rubric', verbose_name='Rubric'), + preserve_default=False, + ), + migrations.AddField( + model_name='assignmentevent', + name='rubric', + field=models.ForeignKey(db_constraint=False, default=1, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', related_query_name='+', to='assignment.rubric', verbose_name='Rubric'), + preserve_default=False, + ), + pgtrigger.migrations.AddTrigger( + model_name='assignment', + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "assignment_assignmentevent" ("appeal_deadline_days", "audience", "confirm_due_days", "created", "description", "duration", "featured", "format", "grade_due_days", "honor_code_id", "id", "max_attempts", "modified", "owner_id", "passing_point", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "published", "question_pool_id", "rubric_id", "thumbnail", "title", "verification_required") VALUES (NEW."appeal_deadline_days", NEW."audience", NEW."confirm_due_days", NEW."created", NEW."description", NEW."duration", NEW."featured", NEW."format", NEW."grade_due_days", NEW."honor_code_id", NEW."id", NEW."max_attempts", NEW."modified", NEW."owner_id", NEW."passing_point", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."published", NEW."question_pool_id", NEW."rubric_id", NEW."thumbnail", NEW."title", NEW."verification_required"); RETURN NULL;', hash='1cd6f2ed24dcbe90b52e9632cc7b10b27259160e', operation='INSERT', pgid='pgtrigger_insert_insert_e3a8d', table='assignment_assignment', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='assignment', + 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."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."max_attempts" IS DISTINCT FROM (NEW."max_attempts") OR OLD."owner_id" IS DISTINCT FROM (NEW."owner_id") OR OLD."passing_point" IS DISTINCT FROM (NEW."passing_point") OR OLD."published" IS DISTINCT FROM (NEW."published") OR OLD."question_pool_id" IS DISTINCT FROM (NEW."question_pool_id") OR OLD."rubric_id" IS DISTINCT FROM (NEW."rubric_id") 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 "assignment_assignmentevent" ("appeal_deadline_days", "audience", "confirm_due_days", "created", "description", "duration", "featured", "format", "grade_due_days", "honor_code_id", "id", "max_attempts", "modified", "owner_id", "passing_point", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "published", "question_pool_id", "rubric_id", "thumbnail", "title", "verification_required") VALUES (NEW."appeal_deadline_days", NEW."audience", NEW."confirm_due_days", NEW."created", NEW."description", NEW."duration", NEW."featured", NEW."format", NEW."grade_due_days", NEW."honor_code_id", NEW."id", NEW."max_attempts", NEW."modified", NEW."owner_id", NEW."passing_point", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."published", NEW."question_pool_id", NEW."rubric_id", NEW."thumbnail", NEW."title", NEW."verification_required"); RETURN NULL;', hash='9303206dde629794b00824d3a5367925f1fdd159', operation='UPDATE', pgid='pgtrigger_update_update_629e4', table='assignment_assignment', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='assignment', + trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "assignment_assignmentevent" ("appeal_deadline_days", "audience", "confirm_due_days", "created", "description", "duration", "featured", "format", "grade_due_days", "honor_code_id", "id", "max_attempts", "modified", "owner_id", "passing_point", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "published", "question_pool_id", "rubric_id", "thumbnail", "title", "verification_required") VALUES (OLD."appeal_deadline_days", OLD."audience", OLD."confirm_due_days", OLD."created", OLD."description", OLD."duration", OLD."featured", OLD."format", OLD."grade_due_days", OLD."honor_code_id", OLD."id", OLD."max_attempts", OLD."modified", OLD."owner_id", OLD."passing_point", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."published", OLD."question_pool_id", OLD."rubric_id", OLD."thumbnail", OLD."title", OLD."verification_required"); RETURN NULL;', hash='da98a79a403fb93d0acea3abcf35aa374ae18916', operation='DELETE', pgid='pgtrigger_delete_delete_1054f', table='assignment_assignment', when='AFTER')), + ), + migrations.DeleteModel( + name='Solution', + ), + migrations.DeleteModel( + name='SolutionEvent', + ), + ] diff --git a/core/apps/assignment/migrations/0005_remove_assignment_insert_insert_and_more.py b/core/apps/assignment/migrations/0005_remove_assignment_insert_insert_and_more.py new file mode 100644 index 0000000..d7973d7 --- /dev/null +++ b/core/apps/assignment/migrations/0005_remove_assignment_insert_insert_and_more.py @@ -0,0 +1,81 @@ +# Generated by Django 6.0.3 on 2026-03-06 10:50 + +import pgtrigger.compiler +import pgtrigger.migrations +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assignment', '0004_remove_solution_question_remove_solution_rubric_and_more'), + ] + + operations = [ + pgtrigger.migrations.RemoveTrigger( + model_name='assignment', + name='insert_insert', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='assignment', + name='update_update', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='assignment', + name='delete_delete', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='question', + name='insert_insert', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='question', + name='update_update', + ), + pgtrigger.migrations.RemoveTrigger( + model_name='question', + name='delete_delete', + ), + migrations.RemoveField( + model_name='question', + name='sample_attachment', + ), + migrations.RemoveField( + model_name='questionevent', + name='sample_attachment', + ), + migrations.AddField( + model_name='assignment', + name='sample_attachment', + field=models.FileField(blank=True, max_length=255, null=True, upload_to='', verbose_name='Sample Attachment'), + ), + migrations.AddField( + model_name='assignmentevent', + name='sample_attachment', + field=models.FileField(blank=True, max_length=255, null=True, upload_to='', verbose_name='Sample Attachment'), + ), + pgtrigger.migrations.AddTrigger( + model_name='assignment', + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "assignment_assignmentevent" ("appeal_deadline_days", "audience", "confirm_due_days", "created", "description", "duration", "featured", "format", "grade_due_days", "honor_code_id", "id", "max_attempts", "modified", "owner_id", "passing_point", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "published", "question_pool_id", "rubric_id", "sample_attachment", "thumbnail", "title", "verification_required") VALUES (NEW."appeal_deadline_days", NEW."audience", NEW."confirm_due_days", NEW."created", NEW."description", NEW."duration", NEW."featured", NEW."format", NEW."grade_due_days", NEW."honor_code_id", NEW."id", NEW."max_attempts", NEW."modified", NEW."owner_id", NEW."passing_point", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."published", NEW."question_pool_id", NEW."rubric_id", NEW."sample_attachment", NEW."thumbnail", NEW."title", NEW."verification_required"); RETURN NULL;', hash='21ac5a1dbcdaca2a2780136fbf794154a596c62c', operation='INSERT', pgid='pgtrigger_insert_insert_e3a8d', table='assignment_assignment', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='assignment', + 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."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."max_attempts" IS DISTINCT FROM (NEW."max_attempts") OR OLD."owner_id" IS DISTINCT FROM (NEW."owner_id") OR OLD."passing_point" IS DISTINCT FROM (NEW."passing_point") OR OLD."published" IS DISTINCT FROM (NEW."published") OR OLD."question_pool_id" IS DISTINCT FROM (NEW."question_pool_id") OR OLD."rubric_id" IS DISTINCT FROM (NEW."rubric_id") OR OLD."sample_attachment" IS DISTINCT FROM (NEW."sample_attachment") 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 "assignment_assignmentevent" ("appeal_deadline_days", "audience", "confirm_due_days", "created", "description", "duration", "featured", "format", "grade_due_days", "honor_code_id", "id", "max_attempts", "modified", "owner_id", "passing_point", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "published", "question_pool_id", "rubric_id", "sample_attachment", "thumbnail", "title", "verification_required") VALUES (NEW."appeal_deadline_days", NEW."audience", NEW."confirm_due_days", NEW."created", NEW."description", NEW."duration", NEW."featured", NEW."format", NEW."grade_due_days", NEW."honor_code_id", NEW."id", NEW."max_attempts", NEW."modified", NEW."owner_id", NEW."passing_point", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."published", NEW."question_pool_id", NEW."rubric_id", NEW."sample_attachment", NEW."thumbnail", NEW."title", NEW."verification_required"); RETURN NULL;', hash='31e50859d395027d1fe45fdebb78d526da55e3b8', operation='UPDATE', pgid='pgtrigger_update_update_629e4', table='assignment_assignment', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='assignment', + trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "assignment_assignmentevent" ("appeal_deadline_days", "audience", "confirm_due_days", "created", "description", "duration", "featured", "format", "grade_due_days", "honor_code_id", "id", "max_attempts", "modified", "owner_id", "passing_point", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "published", "question_pool_id", "rubric_id", "sample_attachment", "thumbnail", "title", "verification_required") VALUES (OLD."appeal_deadline_days", OLD."audience", OLD."confirm_due_days", OLD."created", OLD."description", OLD."duration", OLD."featured", OLD."format", OLD."grade_due_days", OLD."honor_code_id", OLD."id", OLD."max_attempts", OLD."modified", OLD."owner_id", OLD."passing_point", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."published", OLD."question_pool_id", OLD."rubric_id", OLD."sample_attachment", OLD."thumbnail", OLD."title", OLD."verification_required"); RETURN NULL;', hash='bd3967fe3520f4bc3eb34aa5628ee090a3020e92', operation='DELETE', pgid='pgtrigger_delete_delete_1054f', table='assignment_assignment', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='question', + trigger=pgtrigger.compiler.Trigger(name='insert_insert', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "assignment_questionevent" ("attachment_file_count", "attachment_file_types", "id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "plagiarism_threshold", "pool_id", "question", "supplement") VALUES (NEW."attachment_file_count", NEW."attachment_file_types", NEW."id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."plagiarism_threshold", NEW."pool_id", NEW."question", NEW."supplement"); RETURN NULL;', hash='ac65eeb67c670d74ad31e29e2cdec60661b4dedf', operation='INSERT', pgid='pgtrigger_insert_insert_ec4a4', table='assignment_question', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='question', + trigger=pgtrigger.compiler.Trigger(name='update_update', sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD.* IS DISTINCT FROM NEW.*)', func='INSERT INTO "assignment_questionevent" ("attachment_file_count", "attachment_file_types", "id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "plagiarism_threshold", "pool_id", "question", "supplement") VALUES (NEW."attachment_file_count", NEW."attachment_file_types", NEW."id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."plagiarism_threshold", NEW."pool_id", NEW."question", NEW."supplement"); RETURN NULL;', hash='eff17638cb2cc8f38a72a5929750b25e7ab7272f', operation='UPDATE', pgid='pgtrigger_update_update_6ef6c', table='assignment_question', when='AFTER')), + ), + pgtrigger.migrations.AddTrigger( + model_name='question', + trigger=pgtrigger.compiler.Trigger(name='delete_delete', sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "assignment_questionevent" ("attachment_file_count", "attachment_file_types", "id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "plagiarism_threshold", "pool_id", "question", "supplement") VALUES (OLD."attachment_file_count", OLD."attachment_file_types", OLD."id", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."plagiarism_threshold", OLD."pool_id", OLD."question", OLD."supplement"); RETURN NULL;', hash='3ec3e58da7ff73b07683a37d8a7c7348328ba765', operation='DELETE', pgid='pgtrigger_delete_delete_dc36d', table='assignment_question', when='AFTER')), + ), + ] diff --git a/core/apps/assignment/models.py b/core/apps/assignment/models.py index ffaa364..779a862 100644 --- a/core/apps/assignment/models.py +++ b/core/apps/assignment/models.py @@ -108,85 +108,24 @@ class Question(AttachmentMixin): supplement = TextField(_("Supplement"), blank=True, default="") attachment_file_count = PositiveSmallIntegerField(_("Attachment File Count"), default=1) attachment_file_types = ArrayField(CharField(max_length=10), blank=True, default=list, verbose_name=_("Attachment File Types")) # fmt: off - sample_attachment = FileField(_("Sample Attachment"), max_length=255, blank=True, null=True) plagiarism_threshold = PositiveSmallIntegerField(_("Plagiarism Threshold Percentage")) class Meta: verbose_name = _("Question") verbose_name_plural = _("Questions") - if TYPE_CHECKING: - solution: "Solution" - @property def cleaned_supplement(self): return self.update_attachment_urls(content=self.supplement) - @property - def sample_attachment_url(self): - if self.sample_attachment: - return self.sample_attachment.url - - -@pghistory.track() -class Solution(Model): - question = OneToOneField(Question, CASCADE, verbose_name=_("Question")) - rubric = ForeignKey("Rubric", CASCADE, verbose_name=_("Rubric")) - explanation = TextField(_("Explanation"), blank=True, default="") - - class Meta: - verbose_name = _("Solution") - verbose_name_plural = _("Solutions") - - @property - def rubric_data(self): - return getattr(self, "_rubric_data", None) - - @rubric_data.setter - def rubric_data(self, data: RubricDataDict): - self._rubric_data = data - - async def get_rubric_data(self) -> RubricDataDict: - criteria_dict: dict[int, RubricCriterionDataDict] = {} - max_points_by_criterion: dict[int, dict[str, int]] = {} - - async for criterion in self.rubric.rubric_criteria.all(): - criterion_id = criterion.pk - criteria_dict[criterion_id] = { - "id": criterion_id, - "name": criterion.name, - "description": criterion.description, - "performance_levels": [], - } - max_points_by_criterion[criterion_id] = {"max_point": 0} - - async for level in criterion.performance_levels.all(): - max_points_by_criterion[criterion_id]["max_point"] = max( - max_points_by_criterion[criterion_id]["max_point"], level.point - ) - criteria_dict[criterion_id]["performance_levels"].append({ - "id": level.pk, - "name": level.name, - "description": level.description, - "point": level.point, - }) - - possible_point = sum(data["max_point"] for data in max_points_by_criterion.values()) - - return { - "id": self.rubric.pk, - "name": self.rubric.name, - "description": self.rubric.description, - "possible_point": possible_point, - "criteria": list(criteria_dict.values()), - } - @pghistory.track() class Assignment(LearningObjectMixin, GradeWorkflowMixin): owner = ForeignKey(User, CASCADE, verbose_name=_("Owner")) honor_code = ForeignKey(HonorCode, CASCADE, verbose_name=_("Honor Code")) question_pool = ForeignKey(QuestionPool, CASCADE, verbose_name=_("Question Pool")) + rubric = ForeignKey("Rubric", CASCADE, verbose_name=_("Rubric")) + sample_attachment = FileField(_("Sample Attachment"), max_length=255, blank=True, null=True) class Meta(LearningObjectMixin.Meta, GradeWorkflowMixin.Meta): verbose_name = _("Assignment") @@ -197,6 +136,19 @@ class Meta(LearningObjectMixin.Meta, GradeWorkflowMixin.Meta): question_pool_id: int pk: str + @property + def sample_attachment_url(self): + if self.sample_attachment: + return self.sample_attachment.url + + @property + def rubric_data(self): + return getattr(self, "_rubric_data", None) + + @rubric_data.setter + def rubric_data(self, data: RubricDataDict): + self._rubric_data = data + @classmethod async def get_session(cls, *, assignment_id: str, learner_id: str, context: str, access_date: AccessDate): assignment = await Assignment.objects.select_related("owner", "honor_code", "question_pool").aget( @@ -212,8 +164,7 @@ async def get_session(cls, *, assignment_id: str, learner_id: str, context: str, attempt = ( await Attempt.objects .filter(assignment_id=assignment_id, learner_id=learner_id, context=context, active=True) - .select_related("assignment", "submission", "grade", "question__solution__rubric") - .prefetch_related("question__solution__rubric__rubric_criteria__performance_levels") + .select_related("assignment", "submission", "grade") .prefetch_related("question__attachments") .alast() ) @@ -227,8 +178,14 @@ async def get_session(cls, *, assignment_id: str, learner_id: str, context: str, ) return session - # cf exam solution logic - attempt.question.solution.rubric_data = await attempt.question.solution.get_rubric_data() + await aprefetch_related_objects( + [assignment], + Prefetch( + "rubric__rubric_criteria__performance_levels", queryset=PerformanceLevel.objects.order_by("point") + ), + ) + assignment.rubric_data = await assignment.get_rubric_data() + session["attempt"] = attempt if not hasattr(attempt, "submission"): @@ -270,6 +227,41 @@ async def analyze_answers(self, question_ids: Sequence[int]): return await sync_to_async(SubmissionDocument.analyze_answers)(question_ids=question_ids) + async def get_rubric_data(self) -> RubricDataDict: + criteria_dict: dict[int, RubricCriterionDataDict] = {} + max_points_by_criterion: dict[int, dict[str, int]] = {} + + async for criterion in self.rubric.rubric_criteria.all(): + criterion_id = criterion.pk + criteria_dict[criterion_id] = { + "id": criterion_id, + "name": criterion.name, + "description": criterion.description, + "performance_levels": [], + } + max_points_by_criterion[criterion_id] = {"max_point": 0} + + async for level in criterion.performance_levels.all(): + max_points_by_criterion[criterion_id]["max_point"] = max( + max_points_by_criterion[criterion_id]["max_point"], level.point + ) + criteria_dict[criterion_id]["performance_levels"].append({ + "id": level.pk, + "name": level.name, + "description": level.description, + "point": level.point, + }) + + possible_point = sum(data["max_point"] for data in max_points_by_criterion.values()) + + return { + "id": self.rubric.pk, + "name": self.rubric.name, + "description": self.rubric.description, + "possible_point": possible_point, + "criteria": list(criteria_dict.values()), + } + @pghistory.track() class Attempt(AttemptMixin): @@ -306,16 +298,16 @@ async def start(cls, *, assignment_id: str, learner_id: str, context: str, mode: if not await OtpLog.check_otp_verification(user_id=learner_id, consumer=assignment): raise ValueError(ErrorCode.OTP_VERIFICATION_REQUIRED) - question = await QuestionPool(id=assignment.question_pool_id).select_question() await aprefetch_related_objects( - [question], - "attachments", + [assignment], Prefetch( - "solution__rubric__rubric_criteria__performance_levels", - queryset=PerformanceLevel.objects.order_by("point"), + "rubric__rubric_criteria__performance_levels", queryset=PerformanceLevel.objects.order_by("point") ), ) - question.solution.rubric_data = await question.solution.get_rubric_data() + assignment.rubric_data = await assignment.get_rubric_data() + + question = await QuestionPool(id=assignment.question_pool_id).select_question() + await aprefetch_related_objects([question], "attachments") try: attempt = await Attempt.objects.acreate( @@ -338,15 +330,16 @@ async def start(cls, *, assignment_id: str, learner_id: str, context: str, mode: async def submit( cls, *, assignment_id: str, learner_id: str, context: str, answer: str, files: Sequence[File] | None ): - attempt = await cls.objects.select_related("assignment", "question__solution__rubric").aget( - assignment_id=assignment_id, learner_id=learner_id, context=context, active=True - ) - await aprefetch_related_objects( - [attempt], - Prefetch( - "question__solution__rubric__rubric_criteria__performance_levels", - queryset=PerformanceLevel.objects.order_by("point"), - ), + attempt = ( + await cls.objects + .select_related("assignment", "question") + .prefetch_related( + Prefetch( + "assignment__rubric__rubric_criteria__performance_levels", + queryset=PerformanceLevel.objects.order_by("point"), + ) + ) + .aget(assignment_id=assignment_id, learner_id=learner_id, context=context, active=True) ) file_count = attempt.question.attachment_file_count @@ -448,7 +441,7 @@ class Meta(GradeFieldMixin.Meta, TimeStampedMixin.Meta): pgh_event_model: type[Model] async def grade(self, grader_id: str | None = None): - rubric_data = await self.attempt.question.solution.get_rubric_data() + rubric_data = await self.attempt.assignment.get_rubric_data() default_details = {criterion["name"]: None for criterion in rubric_data["criteria"]} self.earned_details = default_details | (self.earned_details or {}) self.possible_point = rubric_data["possible_point"] diff --git a/core/apps/assignment/tests/factories.py b/core/apps/assignment/tests/factories.py index 3c8b72d..17e8f6f 100644 --- a/core/apps/assignment/tests/factories.py +++ b/core/apps/assignment/tests/factories.py @@ -21,7 +21,6 @@ QuestionPool, Rubric, RubricCriterion, - Solution, Submission, ) from apps.common.tests.factories import GradeFieldFactory, GradeWorkflowFactory, LearningObjectFactory, dummy_html @@ -112,9 +111,6 @@ class QuestionFactory(DjangoModelFactory[Question]): question = Sequence(lambda n: f"{generic.text.text(quantity=generic.random.randint(1, 3))} {n}") supplement = LazyFunction(lambda: dummy_html()) attachment_file_types = ["docx"] - sample_attachment = Sequence( - lambda n: ContentFile(generic.binaryfile.document(file_type=DocumentFile.DOCX), name=f"sample.{n}.docx") - ) plagiarism_threshold = FactoryField("choice", items=[80, 100]) class Meta: @@ -127,23 +123,15 @@ def post_generation(self, create: bool, extracted: object, **kwargs: object): if not create: return - SolutionFactory.create(question=self) - - -class SolutionFactory(DjangoModelFactory[Solution]): - question = SubFactory(QuestionFactory) - rubric = SubFactory(RubricFactory) - explanation = FactoryField("text") - - class Meta: - model = Solution - django_get_or_create = ("question",) - class AssignmentFactory(LearningObjectFactory[Assignment], GradeWorkflowFactory[Assignment]): passing_point = FactoryField("choice", items=[60, 80]) max_attempts = 1 verification_required = True + rubric = SubFactory(RubricFactory) + sample_attachment = Sequence( + lambda n: ContentFile(generic.binaryfile.document(file_type=DocumentFile.DOCX), name=f"sample.{n}.docx") + ) owner = LazyFunction(lambda: UserFactory(email=test_user_email)) question_pool = SubFactory(QuestionPoolFactory, owner=owner) @@ -212,17 +200,14 @@ def create(cls, **kwargs: object): grade = Grade.objects.get(attempt=kwargs["attempt"]) except Grade.DoesNotExist: grade = super().build(**kwargs) - attempt = Attempt.objects.select_related("question__solution__rubric", "assignment").get( - id=grade.attempt_id - ) + attempt = Attempt.objects.select_related("question", "assignment").get(id=grade.attempt_id) prefetch_related_objects( - [attempt], + [attempt.assignment], Prefetch( - "question__solution__rubric__rubric_criteria__performance_levels", - queryset=PerformanceLevel.objects.order_by("point"), + "rubric__rubric_criteria__performance_levels", queryset=PerformanceLevel.objects.order_by("point") ), ) - rubric_data = async_to_sync(attempt.question.solution.get_rubric_data)() + rubric_data = async_to_sync(attempt.assignment.get_rubric_data)() grade.earned_details = { criterion["name"]: generic.random.choice([level["point"] for level in criterion["performance_levels"]]) for criterion in rubric_data["criteria"] diff --git a/core/apps/common/error.py b/core/apps/common/error.py index c027725..e04a703 100644 --- a/core/apps/common/error.py +++ b/core/apps/common/error.py @@ -9,6 +9,7 @@ class ErrorCode: ATTACHMENT_TOO_MANY = "ATTACHMENT_TOO_MANY" ATTEMPT_ALREADY_STARTED = "ATTEMPT_ALREADY_STARTED" ATTEMPT_ALREADY_SUBMITTED = "ATTEMPT_ALREADY_SUBMITTED" + ATTEMPT_EXISTS = "ATTEMPT_EXISTS" ATTEMPT_HAS_EXPIRED = "ATTEMPT_HAS_EXPIRED" CERTIFICATE_ALREADY_ISSUED = "CERTIFICATE_ALREADY_ISSUED" CERTIFICATE_GENERATION_FAILED = "CERTIFICATE_GENERATION_FAILED" diff --git a/core/apps/operation/migrations/0003_alter_faq_name_alter_honorcode_title.py b/core/apps/operation/migrations/0003_alter_faq_name_alter_honorcode_title.py new file mode 100644 index 0000000..9797b40 --- /dev/null +++ b/core/apps/operation/migrations/0003_alter_faq_name_alter_honorcode_title.py @@ -0,0 +1,23 @@ +# Generated by Django 6.0.3 on 2026-03-06 09:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('operation', '0002_alter_faq_name'), + ] + + operations = [ + migrations.AlterField( + model_name='faq', + name='name', + field=models.CharField(max_length=255, unique=True, verbose_name='Name'), + ), + migrations.AlterField( + model_name='honorcode', + name='title', + field=models.CharField(max_length=255, unique=True, verbose_name='Title'), + ), + ] diff --git a/core/apps/studio/api/v1/assignment.py b/core/apps/studio/api/v1/assignment.py index 5d0ef9d..62a06cc 100644 --- a/core/apps/studio/api/v1/assignment.py +++ b/core/apps/studio/api/v1/assignment.py @@ -9,7 +9,15 @@ from ninja.params import functions from pydantic import RootModel -from apps.assignment.models import Assignment, Attempt, Question, QuestionPool +from apps.assignment.models import ( + Assignment, + Attempt, + PerformanceLevel, + Question, + QuestionPool, + Rubric, + RubricCriterion, +) from apps.common.error import ErrorCode from apps.common.schema import ( FileSizeValidator, @@ -18,7 +26,7 @@ LearningObjectMixinSchema, Schema, ) -from apps.common.util import HttpRequest +from apps.common.util import HttpRequest, ModeChoices from apps.studio.decorator import editor_required, track_draft @@ -33,25 +41,45 @@ class AssignmentQuestionSaveSpec(Schema): class AssignmentQuestionSpec(AssignmentQuestionSaveSpec): id: int - sample_attachment: str | None @staticmethod def resolve_supplement(obj: Question): return obj.cleaned_supplement -class AssignmentQuestionsSaveSpec(RootModel[list[AssignmentQuestionSaveSpec]]): - pass +# RootModel not working with multipart +class AssignmentQuestionsSaveSpec(Schema): + data: list[AssignmentQuestionSaveSpec] class AssignmentQuestionsSpec(RootModel[list[AssignmentQuestionSpec]]): pass +class RubricCriterionSpec(Schema): + name: str + description: str + performance_levels: "list[PerformanceLevelSchema]" + + +class PerformanceLevelSchema(Schema): + name: str + description: str + point: int + + class AssignmentSpec(LearningObjectMixinSchema, GradeWorkflowMixinSchema): id: str honor_code_id: int + rubric_criteria: list[RubricCriterionSpec] questions: AssignmentQuestionsSpec + sample_attachment: str | None + + @staticmethod + def resolve_rubric_criteria(obj: Assignment): + if not obj.rubric_data: + return [] + return obj.rubric_data["criteria"] @staticmethod def resolve_questions(obj: Assignment): @@ -79,9 +107,19 @@ class AssignmentSaveSpec(Schema): @router.get("/assignment/{id}", response=AssignmentSpec) @editor_required() async def get_assignment(request: HttpRequest, id: str): - assignment = await Assignment.objects.prefetch_related( - Prefetch("question_pool__questions", queryset=Question.objects.prefetch_related("attachments").order_by("id")) - ).aget(id=id, owner_id=request.auth) + assignment = ( + await Assignment.objects + .prefetch_related( + Prefetch( + "question_pool__questions", queryset=Question.objects.prefetch_related("attachments").order_by("id") + ) + ) + .prefetch_related( + Prefetch("rubric__rubric_criteria__performance_levels", queryset=PerformanceLevel.objects.order_by("point")) + ) + .aget(id=id, owner_id=request.auth) + ) + assignment.rubric_data = await assignment.get_rubric_data() return assignment @@ -96,6 +134,10 @@ async def save_assignment( Annotated[UploadedFile, FileSizeValidator(), FileTypeValidator()], functions.File(None, description=f"Max size: {settings.ATTACHMENT_MAX_SIZE_MB}MB"), ], + sample_attachment: Annotated[ + Annotated[UploadedFile, FileSizeValidator(), FileTypeValidator()], + functions.File(None, description=f"Max size: {settings.ATTACHMENT_MAX_SIZE_MB}MB", alias="sampleAttachment"), + ], ): assignment_dict = data.model_dump(exclude_unset=True) assignment_id = assignment_dict.pop("id", None) @@ -103,6 +145,9 @@ async def save_assignment( if thumbnail: assignment_dict["thumbnail"] = thumbnail + if sample_attachment: + assignment_dict["sample_attachment"] = sample_attachment + if assignment_id: assignment = await aget_object_or_404(Assignment, id=assignment_id, owner_id=request.auth) for key, value in assignment_dict.items(): @@ -116,7 +161,10 @@ async def save_assignment( def create_new(): try: pool = QuestionPool.objects.create(owner_id=request.auth, title=data.title) - assignment = Assignment.objects.create(**assignment_dict, question_pool=pool, owner_id=request.auth) + rubric = Rubric.objects.create(name=data.title) + assignment = Assignment.objects.create( + **assignment_dict, question_pool=pool, rubric=rubric, owner_id=request.auth + ) except IntegrityError: # both title conflict raise ValueError(ErrorCode.TITLE_ALREADY_EXISTS) @@ -127,53 +175,70 @@ def create_new(): return assignment.id +@router.delete("/assignment/{id}") +@editor_required() +@track_draft(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(): + raise ValueError(ErrorCode.ATTEMPT_EXISTS) + await Assignment.objects.filter(id=id, owner_id=request.auth, published__isnull=True).adelete() + + @router.get("/assignment/{id}/question", response=list[AssignmentQuestionSpec]) @editor_required() async def get_assignment_questions(request: HttpRequest, id: str): return [ q - async for q in Question.objects - .select_related("solution") - .prefetch_related("attachments") - .filter(pool__assignment__id=id, pool__assignment__owner_id=request.auth) + async for q in Question.objects.prefetch_related("attachments").filter( + pool__assignment__id=id, pool__assignment__owner_id=request.auth + ) ] -@router.post("/assignment/{id}/question", response=int) +@router.post("/assignment/{id}/question", response=list[int]) @editor_required() @track_draft(Assignment, id_field="id") -async def save_assignment_question( +async def save_assignment_questions( request: HttpRequest, id: str, - data: AssignmentQuestionSaveSpec, + data: AssignmentQuestionsSaveSpec, files: Annotated[ list[Annotated[UploadedFile, FileSizeValidator(), FileTypeValidator()]], functions.File(None, description=f"Max size: {settings.ATTACHMENT_MAX_SIZE_MB}MB"), ], - sample: Annotated[ - Annotated[UploadedFile, FileSizeValidator(), FileTypeValidator()], - functions.File(None, description=f"Max size: {settings.ATTACHMENT_MAX_SIZE_MB}MB"), - ], ): assignment = await aget_object_or_404(Assignment, id=id, owner_id=request.auth) - defaults: dict = { - "question": data.question, - "supplement": data.supplement, - "attachment_file_count": data.attachment_file_count, - "attachment_file_types": data.attachment_file_types, - "plagiarism_threshold": data.plagiarism_threshold, - } - - if sample: - defaults["sample_attachment"] = sample - - question, _ = await Question.objects.aupdate_or_create( - id=None if not data.id else data.id, pool_id=assignment.question_pool_id, defaults=defaults + questions, is_new = [], [] + + dumped = data.model_dump()["data"] + for question_data in dumped: + is_new.append(not question_data["id"]) + + if not question_data["id"]: + question_data["id"] = None + + question = Question(pool_id=assignment.question_pool_id, **question_data) + questions.append(question) + + await Question.objects.abulk_create( + questions, + update_conflicts=True, + unique_fields=["id"], + update_fields=[ + "question", + "supplement", + "attachment_file_count", + "attachment_file_types", + "plagiarism_threshold", + ], ) - await question.update_attachments(files=files, owner_id=request.auth, content=question.supplement) + for question, new in zip(questions, is_new): + if new: + question._prefetched_objects_cache = {"attachments": []} + await question.update_attachments(files=files, owner_id=request.auth, content=question.supplement) - return question.pk + return [q.id for q in questions] @router.delete("/assignment/{id}/question/{question_id}") @@ -188,3 +253,38 @@ async def delete_assignment_quesion(request: HttpRequest, id: str, question_id: count, _ = await Question.objects.filter(id=question_id, pool__assignment__id=id).adelete() if count < 1: raise ValueError(ErrorCode.NOT_FOUND) + + +@router.get("/assignment/{id}/rubric", response=list[RubricCriterionSpec]) +@editor_required() +async def get_assignment_rubric(request: HttpRequest, id: str): + assignment = await aget_object_or_404( + Assignment.objects.prefetch_related("rubric__rubric_criteria__performance_levels"), id=id, owner_id=request.auth + ) + return (await assignment.get_rubric_data())["criteria"] + + +@router.post("/assignment/{id}/rubric") +@editor_required() +@track_draft(Assignment, id_field="id") +async def save_assignment_rubric(request: HttpRequest, id: str, data: RootModel[list[RubricCriterionSpec]]): + assignment = await aget_object_or_404(Assignment, id=id, owner_id=request.auth) + + @sync_to_async + @transaction.atomic + def update_rubric(): + RubricCriterion.objects.filter(rubric_id=assignment.rubric_id).delete() + + criteria_data = data.model_dump() + criteria = RubricCriterion.objects.bulk_create([ + RubricCriterion(rubric_id=assignment.rubric_id, **{k: v for k, v in c.items() if k != "performance_levels"}) + for c in criteria_data + ]) + + PerformanceLevel.objects.bulk_create([ + PerformanceLevel(criterion=criterion, **level) + for criterion, c in zip(criteria, criteria_data) + for level in c["performance_levels"] + ]) + + await update_rubric() diff --git a/core/apps/studio/api/v1/course.py b/core/apps/studio/api/v1/course.py index 7096eb6..1dc80cc 100644 --- a/core/apps/studio/api/v1/course.py +++ b/core/apps/studio/api/v1/course.py @@ -21,7 +21,7 @@ LearningObjectMixinSchema, Schema, ) -from apps.common.util import HttpRequest +from apps.common.util import HttpRequest, ModeChoices from apps.course.models import ( ASSESSIBLE_MODELS, Assessment, @@ -31,6 +31,7 @@ CourseInstructor, CourseRelation, CourseSurvey, + Engagement, GradingPolicy, Lesson, LessonMedia, @@ -238,6 +239,15 @@ def create_new(): return course.id +@router.delete("/course/{id}") +@editor_required() +@track_draft(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(): + raise ValueError(ErrorCode.ATTEMPT_EXISTS) + await Course.objects.filter(id=id, owner_id=request.auth, published__isnull=True).adelete() + + class CourseSurveySaveSpec(CourseSurveySpec): id: Annotated[int, Field(None)] diff --git a/core/apps/studio/api/v1/discussion.py b/core/apps/studio/api/v1/discussion.py index 90a0242..8ce5486 100644 --- a/core/apps/studio/api/v1/discussion.py +++ b/core/apps/studio/api/v1/discussion.py @@ -17,7 +17,7 @@ LearningObjectMixinSchema, Schema, ) -from apps.common.util import HttpRequest +from apps.common.util import HttpRequest, ModeChoices from apps.discussion.models import Attempt, Discussion, Question, QuestionPool from apps.studio.decorator import editor_required, track_draft @@ -128,6 +128,15 @@ def create_new(): return discussion.id +@router.delete("/discussion/{id}") +@editor_required() +@track_draft(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(): + raise ValueError(ErrorCode.ATTEMPT_EXISTS) + await Discussion.objects.filter(id=id, owner_id=request.auth, published__isnull=True).adelete() + + @router.get("/discussion/{id}/question", response=list[DiscussionQuestionSpec]) @editor_required() async def get_discussion_questions(request: HttpRequest, id: str): @@ -142,7 +151,7 @@ async def get_discussion_questions(request: HttpRequest, id: str): @router.post("/discussion/{id}/question", response=list[int]) @editor_required() @track_draft(Discussion, id_field="id") -async def save_discussion_question( +async def save_discussion_questions( request: HttpRequest, id: str, data: DiscussionQuestionsSaveSpec, diff --git a/core/apps/studio/api/v1/exam.py b/core/apps/studio/api/v1/exam.py index 2e43ab4..6ca1bed 100644 --- a/core/apps/studio/api/v1/exam.py +++ b/core/apps/studio/api/v1/exam.py @@ -17,7 +17,7 @@ LearningObjectMixinSchema, Schema, ) -from apps.common.util import HttpRequest +from apps.common.util import HttpRequest, ModeChoices from apps.exam.models import Attempt, Exam, Question, QuestionPool, Solution from apps.studio.decorator import editor_required, track_draft @@ -148,6 +148,15 @@ def create_new(): return exam.id +@router.delete("/exam/{id}") +@editor_required() +@track_draft(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(): + raise ValueError(ErrorCode.ATTEMPT_EXISTS) + await Exam.objects.filter(id=id, owner_id=request.auth, published__isnull=True).adelete() + + @router.get("/exam/{id}/question", response=list[ExamQuestionSpec]) @editor_required() async def get_exam_questions(request: HttpRequest, id: str): diff --git a/core/apps/studio/api/v1/media.py b/core/apps/studio/api/v1/media.py index 98a8e15..83f9e6c 100644 --- a/core/apps/studio/api/v1/media.py +++ b/core/apps/studio/api/v1/media.py @@ -11,8 +11,8 @@ from apps.common.error import ErrorCode from apps.common.schema import FileSizeValidator, FileTypeValidator, LearningObjectMixinSchema, Schema -from apps.common.util import HttpRequest -from apps.content.models import Media, Subtitle +from apps.common.util import HttpRequest, ModeChoices +from apps.content.models import Media, Subtitle, Watch from apps.quiz.models import Quiz from apps.studio.decorator import editor_required, track_draft @@ -115,6 +115,15 @@ async def save_media( return media.id +@router.delete("/media/{id}") +@editor_required() +@track_draft(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(): + raise ValueError(ErrorCode.ATTEMPT_EXISTS) + await Media.objects.filter(id=id, owner_id=request.auth, published__isnull=True).adelete() + + @router.post("/media/{id}/subtitle") @editor_required() @track_draft(Media, id_field="id") diff --git a/core/apps/studio/api/v1/quiz.py b/core/apps/studio/api/v1/quiz.py index 5936834..84a9573 100644 --- a/core/apps/studio/api/v1/quiz.py +++ b/core/apps/studio/api/v1/quiz.py @@ -11,7 +11,7 @@ from apps.common.error import ErrorCode from apps.common.schema import FileSizeValidator, FileTypeValidator, LearningObjectMixinSchema, Schema -from apps.common.util import HttpRequest +from apps.common.util import HttpRequest, ModeChoices from apps.quiz.models import Attempt, Question, QuestionPool, Quiz, Solution from apps.studio.decorator import editor_required, track_draft @@ -131,6 +131,15 @@ def create_new(): return quiz.id +@router.delete("/quiz/{id}") +@editor_required() +@track_draft(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(): + raise ValueError(ErrorCode.ATTEMPT_EXISTS) + await Quiz.objects.filter(id=id, owner_id=request.auth, published__isnull=True).adelete() + + @router.get("/quiz/{id}/question", response=list[QuizQuestionSpec]) @editor_required() async def get_quiz_questions(request: HttpRequest, id: str): diff --git a/core/apps/studio/api/v1/survey.py b/core/apps/studio/api/v1/survey.py index 4903775..42503ad 100644 --- a/core/apps/studio/api/v1/survey.py +++ b/core/apps/studio/api/v1/survey.py @@ -11,9 +11,9 @@ from apps.common.error import ErrorCode from apps.common.schema import FileSizeValidator, FileTypeValidator, LearningObjectMixinSchema, Schema -from apps.common.util import HttpRequest +from apps.common.util import HttpRequest, ModeChoices from apps.studio.decorator import editor_required, track_draft -from apps.survey.models import Question, QuestionPool, Survey +from apps.survey.models import Question, QuestionPool, Submission, Survey class SurveyQuestionSaveSpec(Schema): @@ -118,6 +118,15 @@ def create_new(): return survey.id +@router.delete("/survey/{id}") +@editor_required() +@track_draft(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(): + raise ValueError(ErrorCode.ATTEMPT_EXISTS) + await Survey.objects.filter(id=id, owner_id=request.auth, published__isnull=True).adelete() + + @router.get("/survey/{id}/question", response=list[SurveyQuestionSpec]) @editor_required() async def get_survey_questions(request: HttpRequest, id: str): diff --git a/core/apps/studio/tasks.py b/core/apps/studio/tasks.py index a8dc631..e69fc10 100644 --- a/core/apps/studio/tasks.py +++ b/core/apps/studio/tasks.py @@ -4,21 +4,20 @@ from django.utils import timezone from apps.assignment.models import Attempt as AssignmentAttempt -from apps.content.models import Watch as MediaWatch from apps.course.models import Engagement as CourseEngagement from apps.discussion.models import Attempt as DiscussionAttempt from apps.exam.models import Attempt as ExamAttempt from apps.quiz.models import Attempt as QuizAttempt from apps.survey.models import Submission as SurveySubmission -preview_data_models = [ +PREVIEW_DATA_MODELS = [ QuizAttempt, SurveySubmission, ExamAttempt, AssignmentAttempt, DiscussionAttempt, CourseEngagement, - MediaWatch, + # MediaWatch, ] CLEANUP_THRESHOLD_HOURS = 1 @@ -29,8 +28,8 @@ def cleanup_preview_data(): threshold = timezone.now() - timedelta(hours=CLEANUP_THRESHOLD_HOURS) deleted = {} - for M in preview_data_models: - num, model_num = M.objects.filter(mode="preview", start__lte=threshold).delete() + for M in PREVIEW_DATA_MODELS: + num, model_num = M.objects.filter(mode="preview", started__lte=threshold).delete() deleted[M._meta.model_name] = num, model_num return deleted diff --git a/core/apps/studio/tests/test_studio_assignment_api.py b/core/apps/studio/tests/test_studio_assignment_api.py index 1c4f4fb..92ff5b6 100644 --- a/core/apps/studio/tests/test_studio_assignment_api.py +++ b/core/apps/studio/tests/test_studio_assignment_api.py @@ -3,6 +3,7 @@ import pytest from django.test.client import Client +from apps.assignment.models import Attempt from apps.assignment.tests.factories import AssignmentFactory from conftest import AdminUser @@ -12,14 +13,13 @@ def test_studio_assignment_flow(client: Client, admin_user: AdminUser): admin_user.login() - AssignmentFactory(owner=admin_user.get_user()) + assignment = AssignmentFactory(owner=admin_user.get_user(), published=None) + assignment_id = assignment.id # get content suggestions res = client.get("/api/v1/studio/suggestion/content?kind=assignment") assert res.status_code == 200, "get content suggestions" - assignment_id = res.json()[0]["id"] - # get assignment res = client.get(f"/api/v1/studio/assignment/{assignment_id}") assert res.status_code == 200, "get assignment" @@ -27,6 +27,8 @@ def test_studio_assignment_flow(client: Client, admin_user: AdminUser): data = res.json() questions = data["questions"][:3] del data["questions"] + rubric_criteria = data["rubricCriteria"] + del data["rubricCriteria"] # save assignment res = client.post("/api/v1/studio/assignment", data={"data": json.dumps(data)}, format="multipart") @@ -39,15 +41,38 @@ def test_studio_assignment_flow(client: Client, admin_user: AdminUser): res = client.post("/api/v1/studio/assignment", data={"data": json.dumps(data)}, format="multipart") assert res.status_code == 200, "create new assignment" - question = questions[0] - del question["id"] + # get assignment questions + res = client.get(f"/api/v1/studio/assignment/{assignment_id}/question") + assert res.status_code == 200, "get assignment questions" + + for question in questions: + del question["id"] - # save assignment question + # save assignment questions res = client.post( - f"/api/v1/studio/assignment/{assignment_id}/question", data={"data": json.dumps(question)}, format="multipart" + f"/api/v1/studio/assignment/{assignment_id}/question", + data={"data": json.dumps({"data": questions})}, + format="multipart", ) - assert res.status_code == 200, "save assignment question" + assert res.status_code == 200, "save assignment questions" # delete assignment question - res = client.delete(f"/api/v1/studio/assignment/{assignment_id}/question/{res.json()}") + res = client.delete(f"/api/v1/studio/assignment/{assignment_id}/question/{res.json()[0]}") assert res.status_code == 200, "delete assignment question" + + # get assignment rubric criteria + res = client.get(f"/api/v1/studio/assignment/{assignment_id}/rubric") + assert res.status_code == 200, "get assignment rubric criteria" + + # save rubric criteria + res = client.post( + f"/api/v1/studio/assignment/{assignment_id}/rubric", + data=json.dumps(rubric_criteria), + content_type="application/json", + ) + assert res.status_code == 200, "save rubric criteria" + + # delete assignment + Attempt.objects.filter(assignment_id=assignment_id).delete() + res = client.delete(f"/api/v1/studio/assignment/{assignment_id}") + assert res.status_code == 200, "delete assignment" diff --git a/core/apps/studio/tests/test_studio_course_api.py b/core/apps/studio/tests/test_studio_course_api.py index 560ddbd..99d587d 100644 --- a/core/apps/studio/tests/test_studio_course_api.py +++ b/core/apps/studio/tests/test_studio_course_api.py @@ -3,7 +3,7 @@ import pytest from django.test.client import Client -from apps.course.models import CourseRelation +from apps.course.models import CourseRelation, Engagement from apps.course.tests.factories import CourseFactory from conftest import AdminUser @@ -17,12 +17,12 @@ def test_studio_course_flow(client: Client, admin_user: AdminUser): CourseRelation.objects.get_or_create(course=c1, related_course=c2, defaults={"label": c2.title, "ordering": 0}) CourseRelation.objects.get_or_create(course=c2, related_course=c1, defaults={"label": c1.title, "ordering": 0}) + course_id = c2.id + # get content suggestions res = client.get("/api/v1/studio/suggestion/content?kind=course") assert res.status_code == 200, "get content suggestions" - course_id = c2.id - # get course res = client.get(f"/api/v1/studio/course/{course_id}") assert res.status_code == 200, "get course" @@ -189,3 +189,8 @@ def test_studio_course_flow(client: Client, admin_user: AdminUser): content_type="application/json", ) assert res.status_code == 200, "add instructor" + + # delete course + Engagement.objects.filter(course_id=course_id).delete() + res = client.delete(f"/api/v1/studio/course/{course_id}") + assert res.status_code == 200, "delete course" diff --git a/core/apps/studio/tests/test_studio_discussion_api.py b/core/apps/studio/tests/test_studio_discussion_api.py index 1cb62e7..4b2d8b3 100644 --- a/core/apps/studio/tests/test_studio_discussion_api.py +++ b/core/apps/studio/tests/test_studio_discussion_api.py @@ -3,6 +3,7 @@ import pytest from django.test.client import Client +from apps.discussion.models import Attempt from apps.discussion.tests.factories import DiscussionFactory from conftest import AdminUser @@ -12,14 +13,13 @@ def test_studio_discussion_flow(client: Client, admin_user: AdminUser): admin_user.login() - DiscussionFactory(owner=admin_user.get_user()) + discussion = DiscussionFactory(owner=admin_user.get_user()) + discussion_id = discussion.id # get content suggestions res = client.get("/api/v1/studio/suggestion/content?kind=discussion") assert res.status_code == 200, "get content suggestions" - discussion_id = res.json()[0]["id"] - # get discussion res = client.get(f"/api/v1/studio/discussion/{discussion_id}") assert res.status_code == 200, "get discussion" @@ -57,3 +57,8 @@ def test_studio_discussion_flow(client: Client, admin_user: AdminUser): # delete discussion question res = client.delete(f"/api/v1/studio/discussion/{discussion_id}/question/{res.json()[0]}") assert res.status_code == 200, "delete discussion question" + + # delete discussion + Attempt.objects.filter(discussion_id=discussion_id).delete() + res = client.delete(f"/api/v1/studio/discussion/{discussion_id}") + assert res.status_code == 200, "delete discussion" diff --git a/core/apps/studio/tests/test_studio_exam_api.py b/core/apps/studio/tests/test_studio_exam_api.py index 0d781f4..0ec26c9 100644 --- a/core/apps/studio/tests/test_studio_exam_api.py +++ b/core/apps/studio/tests/test_studio_exam_api.py @@ -14,14 +14,12 @@ def test_studio_exam_flow(client: Client, admin_user: AdminUser): admin_user.login() exam = ExamFactory(owner=admin_user.get_user()) - Attempt.objects.filter(exam=exam).delete() + exam_id = exam.id # get content suggestions res = client.get("/api/v1/studio/suggestion/content?kind=exam") assert res.status_code == 200, "get content suggestions" - exam_id = res.json()[0]["id"] - # get exam res = client.get(f"/api/v1/studio/exam/{exam_id}") assert res.status_code == 200, "get exam" @@ -57,3 +55,8 @@ def test_studio_exam_flow(client: Client, admin_user: AdminUser): # delete exam question res = client.delete(f"/api/v1/studio/exam/{exam_id}/question/{res.json()[0]}") assert res.status_code == 200, "delete exam question" + + # delete exam + Attempt.objects.filter(exam_id=exam_id).delete() + res = client.delete(f"/api/v1/studio/exam/{exam_id}") + assert res.status_code == 200, "delete exam" diff --git a/core/apps/studio/tests/test_studio_media_api.py b/core/apps/studio/tests/test_studio_media_api.py index fe1f73c..a8928b7 100644 --- a/core/apps/studio/tests/test_studio_media_api.py +++ b/core/apps/studio/tests/test_studio_media_api.py @@ -3,6 +3,7 @@ import pytest from django.test.client import Client +from apps.content.models import Watch from apps.content.tests.factories import MediaFactory from conftest import AdminUser @@ -12,14 +13,13 @@ def test_studio_media_flow(client: Client, admin_user: AdminUser): admin_user.login() - MediaFactory(owner=admin_user.get_user()) + media = MediaFactory(owner=admin_user.get_user()) + media_id = media.id # get content suggestions res = client.get("/api/v1/studio/suggestion/content?kind=media") assert res.status_code == 200, "get content suggestions" - media_id = res.json()[0]["id"] - # get media res = client.get(f"/api/v1/studio/media/{media_id}") assert res.status_code == 200, "get media" @@ -36,3 +36,8 @@ def test_studio_media_flow(client: Client, admin_user: AdminUser): # create new media res = client.post("/api/v1/studio/media", data={"data": json.dumps(data)}, format="multipart") assert res.status_code == 200, "create new media" + + # delete media + Watch.objects.filter(media_id=media_id).delete() + res = client.delete(f"/api/v1/studio/media/{media_id}") + assert res.status_code == 200, "delete media" diff --git a/core/apps/studio/tests/test_studio_quiz_api.py b/core/apps/studio/tests/test_studio_quiz_api.py index 6335249..02944ff 100644 --- a/core/apps/studio/tests/test_studio_quiz_api.py +++ b/core/apps/studio/tests/test_studio_quiz_api.py @@ -13,15 +13,13 @@ def test_studio_quiz_flow(client: Client, admin_user: AdminUser): admin_user.login() - quiz = QuizFactory(owner=admin_user.get_user()) - Attempt.objects.filter(quiz=quiz).delete() + quiz = QuizFactory(owner=admin_user.get_user(), published=None) + quiz_id = quiz.id # get content suggestions res = client.get("/api/v1/studio/suggestion/content?kind=quiz") assert res.status_code == 200, "get content suggestions" - quiz_id = res.json()[0]["id"] - # get quiz res = client.get(f"/api/v1/studio/quiz/{quiz_id}") assert res.status_code == 200, "get quiz" @@ -57,3 +55,8 @@ def test_studio_quiz_flow(client: Client, admin_user: AdminUser): # delete quiz question res = client.delete(f"/api/v1/studio/quiz/{quiz_id}/question/{res.json()[0]}") assert res.status_code == 200, "delete quiz question" + + # delete quiz + Attempt.objects.filter(quiz=quiz).delete() + res = client.delete(f"/api/v1/studio/quiz/{quiz_id}") + assert res.status_code == 200, "delete quiz" diff --git a/core/apps/studio/tests/test_studio_survey_api.py b/core/apps/studio/tests/test_studio_survey_api.py index 3c116dd..4ec5151 100644 --- a/core/apps/studio/tests/test_studio_survey_api.py +++ b/core/apps/studio/tests/test_studio_survey_api.py @@ -3,6 +3,7 @@ import pytest from django.test.client import Client +from apps.survey.models import Submission from apps.survey.tests.factories import SurveyFactory from conftest import AdminUser @@ -12,14 +13,13 @@ def test_studio_survey_flow(client: Client, admin_user: AdminUser): admin_user.login() - SurveyFactory(owner=admin_user.get_user()) + survey = SurveyFactory(owner=admin_user.get_user()) + survey_id = survey.id # get content suggestions for survey res = client.get("/api/v1/studio/suggestion/content?kind=survey") assert res.status_code == 200, "get content suggestions for survey" - survey_id = res.json()[0]["id"] - # get survey res = client.get(f"/api/v1/studio/survey/{survey_id}") assert res.status_code == 200, "get survey" @@ -57,3 +57,8 @@ def test_studio_survey_flow(client: Client, admin_user: AdminUser): # delete survey question res = client.delete(f"/api/v1/studio/survey/{survey_id}/question/{res.json()[0]}") assert res.status_code == 200, "delete survey question" + + # delete survey + Submission.objects.filter(survey_id=survey_id).delete() + res = client.delete(f"/api/v1/studio/survey/{survey_id}") + assert res.status_code == 200, "delete survey" diff --git a/core/dev.py b/core/dev.py index f27acb3..f84e70b 100644 --- a/core/dev.py +++ b/core/dev.py @@ -19,12 +19,13 @@ def up(): subprocess.run(["docker", "compose", "up", "-d"]) -LANG = os.environ.get("LANGUAGE_CODE", "en-us") +LANG = os.environ.get("LANGUAGE_CODE", "en-us").lower() @app.command() def bootstrap(tty: bool = True): - category_fixture = "ncs_category_ko.json" if LANG.lower() == "ko-kr" else "ncs_category_en.json" + category_fixture = "ncs_category_ko.json" if LANG == "ko-kr" else "ncs_category_en.json" + rubric_fixture = "generic_rubric_ko.json" if LANG == "ko-kr" else "generic_rubric_en.json" commands = [ "python manage.py migrate", @@ -36,6 +37,7 @@ def bootstrap(tty: bool = True): "python manage.py setup_base_operation_data", "python manage.py load_ncs_data", f"python manage.py loaddata {category_fixture}", + f"python manage.py loaddata {rubric_fixture}", ] tty_flag: list[str] = [] if tty else ["-T"] subprocess.run(["docker", "compose", "exec", *tty_flag, "minima", "sh", "-c", " && ".join(commands)]) diff --git a/core/uv.lock b/core/uv.lock index cf81eb9..c2eb927 100644 --- a/core/uv.lock +++ b/core/uv.lock @@ -154,15 +154,15 @@ wheels = [ [[package]] name = "boto3-stubs" -version = "1.42.61" +version = "1.42.62" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore-stubs" }, { name = "types-s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/f6/07d823217af2023f4a5ac1ef0b7370f1375d31d40d2208438b2e7446670c/boto3_stubs-1.42.61.tar.gz", hash = "sha256:e8c87eeb4a27932e02d647c795693ac075e6eb148084d00959c61f2f64e30e78", size = 100995, upload-time = "2026-03-04T21:01:25.278Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/f3/3ae7094d0f5c33fb0f80cb96a991abdbe87d49477cc8cdfe3f2648e81382/boto3_stubs-1.42.61-py3-none-any.whl", hash = "sha256:f4cfd8cb90302824cf299253db6162f27b1281e528939ee3e32ddba19a697924", size = 69825, upload-time = "2026-03-04T21:01:12.318Z" }, + { 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" }, ] [package.optional-dependencies] @@ -273,27 +273,27 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232, upload-time = "2026-03-06T06:02:01.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688, upload-time = "2026-03-06T06:02:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833, upload-time = "2026-03-06T06:02:05.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879, upload-time = "2026-03-06T06:02:06.446Z" }, + { url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764, upload-time = "2026-03-06T06:02:08.763Z" }, + { url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728, upload-time = "2026-03-06T06:02:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937, upload-time = "2026-03-06T06:02:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040, upload-time = "2026-03-06T06:02:13.489Z" }, + { url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107, upload-time = "2026-03-06T06:02:14.83Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310, upload-time = "2026-03-06T06:02:16.165Z" }, + { url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918, upload-time = "2026-03-06T06:02:18.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615, upload-time = "2026-03-06T06:02:19.821Z" }, + { url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784, upload-time = "2026-03-06T06:02:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009, upload-time = "2026-03-06T06:02:23.289Z" }, + { url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511, upload-time = "2026-03-06T06:02:26.195Z" }, + { url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775, upload-time = "2026-03-06T06:02:27.473Z" }, + { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" }, ] [[package]] diff --git a/web/package-lock.json b/web/package-lock.json index 976410b..78a7947 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -17,7 +17,6 @@ "@tailwindcss/vite": "^4.2.1", "@tanstack/router-plugin": "^1.166.2", "@tanstack/solid-router": "^1.166.2", - "@tanstack/solid-virtual": "^3.13.20", "@tiptap/core": "^3.20.0", "@tiptap/extension-dropcursor": "^3.20.0", "@tiptap/extension-file-handler": "^3.20.0", @@ -4110,22 +4109,6 @@ "solid-js": "^1.6.0" } }, - "node_modules/@tanstack/solid-virtual": { - "version": "3.13.20", - "resolved": "https://registry.npmjs.org/@tanstack/solid-virtual/-/solid-virtual-3.13.20.tgz", - "integrity": "sha512-9ThhJ7X1d25lx/uVfTNTk5Wyt/OLnKLRaxb8Indx5uxw2rxokcGHFpRXEaSYyxLtRURcGdJ4+M5P0D6lVH4eDg==", - "license": "MIT", - "dependencies": { - "@tanstack/virtual-core": "3.13.20" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "solid-js": "^1.3.0" - } - }, "node_modules/@tanstack/store": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.1.tgz", @@ -4136,16 +4119,6 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, - "node_modules/@tanstack/virtual-core": { - "version": "3.13.20", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.20.tgz", - "integrity": "sha512-Djnq7ujPWcRAKyDpwqL4JDe6ZTN9AWAqE2wLstBlsEu4OnO7Im0p8KsHzLU7TPIvLQgNKpkn9EmgBH6xs8yjfA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, "node_modules/@tanstack/virtual-file-routes": { "version": "1.161.4", "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.161.4.tgz", diff --git a/web/package.json b/web/package.json index 57f846d..1c91d3c 100644 --- a/web/package.json +++ b/web/package.json @@ -26,7 +26,6 @@ "@tailwindcss/vite": "^4.2.1", "@tanstack/router-plugin": "^1.166.2", "@tanstack/solid-router": "^1.166.2", - "@tanstack/solid-virtual": "^3.13.20", "@tiptap/core": "^3.20.0", "@tiptap/extension-dropcursor": "^3.20.0", "@tiptap/extension-file-handler": "^3.20.0", diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 3e0e27b..d32e29c 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { accountV1Activate, accountV1ApplyEmailChange, accountV1ApplyPasswordChange, accountV1CompleteOtpSetup, accountV1GetMe, accountV1Join, accountV1Login, accountV1Logout, accountV1RequestActivation, accountV1RequestEmailChange, accountV1RequestPasswordChange, accountV1ResetOtp, accountV1SetupOtp, accountV1UpdateMe, accountV1UploadAvatar, accountV1VerifyOtp, assignmentV1DeactivateAttempt, assignmentV1GetSession, assignmentV1StartAttempt, assignmentV1SubmitAttempt, assistantV1ChatMessage, assistantV1DeleteChat, assistantV1GetChatMessages, assistantV1GetChats, assistantV1SaveAssistantNote, assistantV1UpdateChatMessage, competencyV1DeleteCompetencyGoal, competencyV1GetCertificateAwards, competencyV1GetCertificates, competencyV1GetClassificationSkillsData, competencyV1GetClassificationTree, competencyV1GetCompetencyGoals, competencyV1SaveCompetencyGoal, contentV1DeleteMediaWatch, contentV1GetMedia, contentV1GetMediaNote, contentV1GetMediaWatch, contentV1GetSubtitles, contentV1GetWatchMedias, contentV1SaveMediaNote, contentV1Search, contentV1SearchSuggestion, contentV1UpdateMediaWatch, courseV1GetDetail, courseV1GetSession, courseV1RequestCertificate, courseV1StartEngagement, discussionV1CreatePost, discussionV1DeactivateAttempt, discussionV1DeletePost, discussionV1GetOwnPosts, discussionV1GetPosts, discussionV1GetSession, discussionV1StartAttempt, discussionV1UpdatePost, examV1DeactivateAttempt, examV1GetSession, examV1GetTimestamp, examV1SaveAnswers, examV1StartAttempt, examV1SubmitAttempt, learningV1EnrollCatalogItem, learningV1GetCatalogItems, learningV1GetCatalogs, learningV1GetEnrolled, learningV1GetRecords, learningV1GetReport, learningV1Unenroll, minimaApiHealth, operationV1AgreePolicies, operationV1CreateAppeal, operationV1CreateInquiry, operationV1CreateThread, operationV1DeleteComment, operationV1DeleteDevice, operationV1EffectivePolicies, operationV1GetAnnouncements, operationV1GetComments, operationV1GetDevices, operationV1GetInquiries, operationV1GetThread, operationV1GetThreadComments, operationV1GetUnreadMessages, operationV1ReadAnnouncement, operationV1ReadMessage, operationV1RegisterDevice, operationV1SaveComment, operationV1ToggleDeviceActive, operationV1UpdateInquiry, type Options, partnerV1MemberInfos, quizV1DeactivateAttempt, quizV1GetSession, quizV1StartAttempt, quizV1SubmitAttempt, ssoV1Authorize, ssoV1Callback, ssoV1DeleteAccount, ssoV1GetAccounts, ssoV1Link, studioV1AssessmentSuggestions, studioV1Content, studioV1ContentSuggestions, studioV1CreateMediaQuiz, studioV1DeleteAssignmentQuesion, studioV1DeleteDiscussionQuesion, studioV1DeleteExamQuesion, studioV1DeleteMediaSubtitle, studioV1DeleteQuizQuesion, studioV1DeleteSurveyQuesion, studioV1GetAssignment, studioV1GetAssignmentQuestions, studioV1GetCourse, studioV1GetDiscussion, studioV1GetDiscussionQuestions, studioV1GetExam, studioV1GetExamQuestions, studioV1GetMedia, studioV1GetQuiz, studioV1GetQuizQuestions, studioV1GetSurvey, studioV1GetSurveyQuestions, studioV1InlineSuggestions, studioV1RemoveCourseAssessment, studioV1RemoveCourseCategory, studioV1RemoveCourseCertificate, studioV1RemoveCourseInstructor, studioV1RemoveCourseLesson, studioV1RemoveCourseRelation, studioV1RemoveCourseSurvey, studioV1SaveAssignment, studioV1SaveAssignmentQuestion, studioV1SaveCourse, studioV1SaveCourseAssessments, studioV1SaveCourseCategories, studioV1SaveCourseCertificates, studioV1SaveCourseInstructors, studioV1SaveCourseLessons, studioV1SaveCourseRelations, studioV1SaveCourseSurveys, studioV1SaveDiscussion, studioV1SaveDiscussionQuestion, studioV1SaveExam, studioV1SaveExamQuestions, studioV1SaveMedia, studioV1SaveMediaSubtitle, studioV1SaveQuiz, studioV1SaveQuizQuestions, studioV1SaveSurvey, studioV1SaveSurveyQuestions, surveyV1GetAnonymousSurvey, surveyV1GetSurvey, surveyV1Results, surveyV1ResultsAnonymous, surveyV1Submit, surveyV1SubmitAnonymous } from './sdk.gen'; -export type { AccessDateSchema, AccountActivatedSchema, AccountActivateSchema, AccountV1ActivateData, AccountV1ActivateResponse, AccountV1ActivateResponses, AccountV1ApplyEmailChangeData, AccountV1ApplyEmailChangeResponses, AccountV1ApplyPasswordChangeData, AccountV1ApplyPasswordChangeResponses, AccountV1CompleteOtpSetupData, AccountV1CompleteOtpSetupResponse, AccountV1CompleteOtpSetupResponses, AccountV1GetMeData, AccountV1GetMeResponse, AccountV1GetMeResponses, AccountV1JoinData, AccountV1JoinResponses, AccountV1LoginData, AccountV1LoginResponse, AccountV1LoginResponses, AccountV1LogoutData, AccountV1LogoutResponses, AccountV1RequestActivationData, AccountV1RequestActivationResponses, AccountV1RequestEmailChangeData, AccountV1RequestEmailChangeResponses, AccountV1RequestPasswordChangeData, AccountV1RequestPasswordChangeResponses, AccountV1ResetOtpData, AccountV1ResetOtpResponses, AccountV1SetupOtpData, AccountV1SetupOtpResponse, AccountV1SetupOtpResponses, AccountV1UpdateMeData, AccountV1UpdateMeResponse, AccountV1UpdateMeResponses, AccountV1UploadAvatarData, AccountV1UploadAvatarResponse, AccountV1UploadAvatarResponses, AccountV1VerifyOtpData, AccountV1VerifyOtpResponses, AnnounceSchema, AppealCreateSchema, AppealSchema, ApplyEmailChangeSchema, ApplyPasswordChangeSchema, AssessmentSaveSpec, AssessmentSpec, AssessmentSuggestionSpec, AssignmentAttemptSchema, AssignmentGradeSchema, AssignmentQuestionSaveSpec, AssignmentQuestionSchema, AssignmentQuestionSpec, AssignmentQuestionsSpec, AssignmentSaveSpec, AssignmentSchema, AssignmentSessionSchema, AssignmentSolutionSchema, AssignmentSpec, AssignmentSubmissionSchema, AssignmentSubmitSchema, AssignmentV1DeactivateAttemptData, AssignmentV1DeactivateAttemptResponses, AssignmentV1GetSessionData, AssignmentV1GetSessionResponse, AssignmentV1GetSessionResponses, AssignmentV1StartAttemptData, AssignmentV1StartAttemptResponse, AssignmentV1StartAttemptResponses, AssignmentV1SubmitAttemptData, AssignmentV1SubmitAttemptResponse, AssignmentV1SubmitAttemptResponses, AssistantNoteSaveSchema, AssistantV1ChatMessageData, AssistantV1ChatMessageResponses, AssistantV1DeleteChatData, AssistantV1DeleteChatResponses, AssistantV1GetChatMessagesData, AssistantV1GetChatMessagesResponse, AssistantV1GetChatMessagesResponses, AssistantV1GetChatsData, AssistantV1GetChatsResponse, AssistantV1GetChatsResponses, AssistantV1SaveAssistantNoteData, AssistantV1SaveAssistantNoteResponses, AssistantV1UpdateChatMessageData, AssistantV1UpdateChatMessageResponses, AuthorizeResponseSchema, AuthorizeSchema, CatalogContentSchema, CatalogItemEnrollSchema, CatalogItemSchema, CatalogSchema, CertificateAwardSchema, CertificateEndorsementSchema, CertificateFilterSchema, CertificateSchema, CertificateSkillSchema, ChatListSchema, ChatMessageCreateSchema, ChatMessageSchema, ChatMessageUpdateSchema, ChatSchema, ClassificationSchema, ClassificationTreeNodeSchema, ClientOptions, CohortSchema, CohortStaffSchema, CommentBriefSchema, CommentNestedSchema, CommentSavedSchema, CommentSaveSchema, CommentSchema, CommentThreadSchema, CompetencyFactorySchema, CompetencyGoalSaveSchema, CompetencyGoalSchema, CompetencySkillSchema, CompetencyV1DeleteCompetencyGoalData, CompetencyV1DeleteCompetencyGoalResponses, CompetencyV1GetCertificateAwardsData, CompetencyV1GetCertificateAwardsResponse, CompetencyV1GetCertificateAwardsResponses, CompetencyV1GetCertificatesData, CompetencyV1GetCertificatesResponse, CompetencyV1GetCertificatesResponses, CompetencyV1GetClassificationSkillsDataData, CompetencyV1GetClassificationSkillsDataResponse, CompetencyV1GetClassificationSkillsDataResponses, CompetencyV1GetClassificationTreeData, CompetencyV1GetClassificationTreeResponse, CompetencyV1GetClassificationTreeResponses, CompetencyV1GetCompetencyGoalsData, CompetencyV1GetCompetencyGoalsResponse, CompetencyV1GetCompetencyGoalsResponses, CompetencyV1SaveCompetencyGoalData, CompetencyV1SaveCompetencyGoalResponse, CompetencyV1SaveCompetencyGoalResponses, ContentSuggestionSpec, ContentTypeSchema, ContentV1DeleteMediaWatchData, ContentV1DeleteMediaWatchResponses, ContentV1GetMediaData, ContentV1GetMediaNoteData, ContentV1GetMediaNoteResponse, ContentV1GetMediaNoteResponses, ContentV1GetMediaResponse, ContentV1GetMediaResponses, ContentV1GetMediaWatchData, ContentV1GetMediaWatchResponse, ContentV1GetMediaWatchResponses, ContentV1GetSubtitlesData, ContentV1GetSubtitlesResponse, ContentV1GetSubtitlesResponses, ContentV1GetWatchMediasData, ContentV1GetWatchMediasResponse, ContentV1GetWatchMediasResponses, ContentV1SaveMediaNoteData, ContentV1SaveMediaNoteResponse, ContentV1SaveMediaNoteResponses, ContentV1SearchData, ContentV1SearchResponse, ContentV1SearchResponses, ContentV1SearchSuggestionData, ContentV1SearchSuggestionResponse, ContentV1SearchSuggestionResponses, ContentV1UpdateMediaWatchData, ContentV1UpdateMediaWatchResponses, CourseAssetsSpec, CourseCategorySaveSpec, CourseCategorySchema, CourseCategorySpec, CourseCertificateItemSchema, CourseCertificateRequestSchema, CourseCertificateSaveSpec, CourseCertificateSchema, CourseCertificateSpec, CourseDetailSchema, CourseEngagementSchema, CourseGradebookSchema, CourseInstructorItemSchema, CourseInstructorSaveSpec, CourseInstructorSchema, CourseInstructorSpec, CourseRelationItemSchema, CourseRelationSaveSpec, CourseRelationSchema, CourseRelationSpec, CourseSaveSpec, CourseSchema, CourseSessionSchema, CourseSpec, CourseSurveyItemSchema, CourseSurveySaveSpec, CourseSurveySchema, CourseSurveySpec, CourseV1GetDetailData, CourseV1GetDetailResponse, CourseV1GetDetailResponses, CourseV1GetSessionData, CourseV1GetSessionResponse, CourseV1GetSessionResponses, CourseV1RequestCertificateData, CourseV1RequestCertificateResponse, CourseV1RequestCertificateResponses, CourseV1StartEngagementData, CourseV1StartEngagementResponse, CourseV1StartEngagementResponses, DeviceRegisterSchema, DeviceSchema, DiscussionAttemptSchema, DiscussionEarnedDetailsSchema, DiscussionFeedbackSchema, DiscussionGradeSchema, DiscussionOwnPostSchema, DiscussionPostCountSchema, DiscussionPostNestedSchema, DiscussionPostSaveSchema, DiscussionPostSchema, DiscussionPostWithCountSchema, DiscussionQuestionSaveSpec, DiscussionQuestionSchema, DiscussionQuestionSpec, DiscussionQuestionsSaveSpec, DiscussionQuestionsSpec, DiscussionSaveSpec, DiscussionSchema, DiscussionSessionSchema, DiscussionSpec, DiscussionV1CreatePostData, DiscussionV1CreatePostResponse, DiscussionV1CreatePostResponses, DiscussionV1DeactivateAttemptData, DiscussionV1DeactivateAttemptResponses, DiscussionV1DeletePostData, DiscussionV1DeletePostResponses, DiscussionV1GetOwnPostsData, DiscussionV1GetOwnPostsResponse, DiscussionV1GetOwnPostsResponses, DiscussionV1GetPostsData, DiscussionV1GetPostsResponse, DiscussionV1GetPostsResponses, DiscussionV1GetSessionData, DiscussionV1GetSessionResponse, DiscussionV1GetSessionResponses, DiscussionV1StartAttemptData, DiscussionV1StartAttemptResponse, DiscussionV1StartAttemptResponses, DiscussionV1UpdatePostData, DiscussionV1UpdatePostResponse, DiscussionV1UpdatePostResponses, EnrollmentContentSchema, EnrollmentSchema, EnrollmentSuccessSchema, ExamAttemptAnswersSchema, ExamAttemptSchema, ExamGradeSchema, ExamQuestionFormatChoices, ExamQuestionPoolSchema, ExamQuestionPoolSpec, ExamQuestionSaveSpec, ExamQuestionSchema, ExamQuestionSolutionSpec, ExamQuestionSpec, ExamQuestionsSaveSpec, ExamQuestionsSpec, ExamSaveSpec, ExamSchema, ExamSessionSchema, ExamSolutionSchema, ExamSpec, ExamSubmissionSchema, ExamV1DeactivateAttemptData, ExamV1DeactivateAttemptResponses, ExamV1GetSessionData, ExamV1GetSessionResponse, ExamV1GetSessionResponses, ExamV1GetTimestampData, ExamV1GetTimestampResponse, ExamV1GetTimestampResponses, ExamV1SaveAnswersData, ExamV1SaveAnswersResponses, ExamV1StartAttemptData, ExamV1StartAttemptResponse, ExamV1StartAttemptResponses, ExamV1SubmitAttemptData, ExamV1SubmitAttemptResponse, ExamV1SubmitAttemptResponses, FactoryDataSchema, FaqItemSchema, FaqSchema, GradingCriterionSchema, GradingDateSchema, GradingPolicySpec, HonorCodeSchema, InlineSuggestionSpec, Input, InquiryCreateSchema, InquiryFilterSchema, InquiryResponseSchema, InquirySavedSchema, InquirySchema, InquiryUpdateSchema, JoinSchema, KindChoices, LearningRecordSchema, LearningReportSchema, LearningSessionStep, LearningV1EnrollCatalogItemData, LearningV1EnrollCatalogItemResponse, LearningV1EnrollCatalogItemResponses, LearningV1GetCatalogItemsData, LearningV1GetCatalogItemsResponse, LearningV1GetCatalogItemsResponses, LearningV1GetCatalogsData, LearningV1GetCatalogsResponse, LearningV1GetCatalogsResponses, LearningV1GetEnrolledData, LearningV1GetEnrolledResponse, LearningV1GetEnrolledResponses, LearningV1GetRecordsData, LearningV1GetRecordsResponse, LearningV1GetRecordsResponses, LearningV1GetReportData, LearningV1GetReportResponse, LearningV1GetReportResponses, LearningV1UnenrollData, LearningV1UnenrollResponses, LessonMediaItemSchema, LessonMediaSchema, LessonSaveSpec, LessonSchema, LessonSpec, LevelChoices, LoginSchema, MatchedLineSchema, MediaFormatChoices, MediaSaveSpec, MediaSchema, MediaSpec, MessageDataSchema, MessageSchema, MinimaApiHealthData, MinimaApiHealthResponses, NoteSaveSchema, NoteSchema, OperationV1AgreePoliciesData, OperationV1AgreePoliciesResponses, OperationV1CreateAppealData, OperationV1CreateAppealResponse, OperationV1CreateAppealResponses, OperationV1CreateInquiryData, OperationV1CreateInquiryResponse, OperationV1CreateInquiryResponses, OperationV1CreateThreadData, OperationV1CreateThreadResponse, OperationV1CreateThreadResponses, OperationV1DeleteCommentData, OperationV1DeleteCommentResponses, OperationV1DeleteDeviceData, OperationV1DeleteDeviceResponses, OperationV1EffectivePoliciesData, OperationV1EffectivePoliciesResponse, OperationV1EffectivePoliciesResponses, OperationV1GetAnnouncementsData, OperationV1GetAnnouncementsResponse, OperationV1GetAnnouncementsResponses, OperationV1GetCommentsData, OperationV1GetCommentsResponse, OperationV1GetCommentsResponses, OperationV1GetDevicesData, OperationV1GetDevicesResponse, OperationV1GetDevicesResponses, OperationV1GetInquiriesData, OperationV1GetInquiriesResponse, OperationV1GetInquiriesResponses, OperationV1GetThreadCommentsData, OperationV1GetThreadCommentsResponse, OperationV1GetThreadCommentsResponses, OperationV1GetThreadData, OperationV1GetThreadResponse, OperationV1GetThreadResponses, OperationV1GetUnreadMessagesData, OperationV1GetUnreadMessagesResponse, OperationV1GetUnreadMessagesResponses, OperationV1ReadAnnouncementData, OperationV1ReadAnnouncementResponses, OperationV1ReadMessageData, OperationV1ReadMessageResponses, OperationV1RegisterDeviceData, OperationV1RegisterDeviceResponse, OperationV1RegisterDeviceResponses, OperationV1SaveCommentData, OperationV1SaveCommentResponse, OperationV1SaveCommentResponses, OperationV1ToggleDeviceActiveData, OperationV1ToggleDeviceActiveResponses, OperationV1UpdateInquiryData, OperationV1UpdateInquiryResponse, OperationV1UpdateInquiryResponses, OtpSetupCompleteSchema, OtpSetupSchema, OtpVerifySchema, OwnerSchema, PagedAnnounceSchema, PagedCertificateAwardSchema, PagedChatMessageSchema, PagedCommentBriefSchema, PagedCommentNestedSchema, PagedDiscussionPostNestedSchema, PagedInquirySchema, PagedMessageSchema, PagedWatchedMediaSchema, PaginatedResponseCatalogItemSchema, PaginatedResponseEnrollmentSchema, PaginatedResponseSearchedMediaSchema, PaginatedResponseStudioContentSpec, PartnerGroupMemberSchema, PartnerGroupSchema, PartnerSchema, PartnerV1MemberInfosData, PartnerV1MemberInfosResponse, PartnerV1MemberInfosResponses, PerformanceLevelSchema, PolicyVersionAgreementSchema, QuizAttemptAnswersSchema, QuizAttemptSchema, QuizGradeSchema, QuizQuestionPoolSpec, QuizQuestionSaveSpec, QuizQuestionSchema, QuizQuestionSolutionSpec, QuizQuestionSpec, QuizQuestionsSaveSpec, QuizQuestionsSpec, QuizSaveSpec, QuizSchema, QuizSessionSchema, QuizSolutionSchema, QuizSpec, QuizSubmissionSchema, QuizV1DeactivateAttemptData, QuizV1DeactivateAttemptResponses, QuizV1GetSessionData, QuizV1GetSessionResponse, QuizV1GetSessionResponses, QuizV1StartAttemptData, QuizV1StartAttemptResponse, QuizV1StartAttemptResponses, QuizV1SubmitAttemptData, QuizV1SubmitAttemptResponse, QuizV1SubmitAttemptResponses, RequestActivationSchema, RequestEmailChangeSchema, RequestPasswordChangeSchema, RoleChoices, RootModelListAssessmentSaveSpec, RootModelListCourseCategorySaveSpec, RootModelListCourseCertificateSaveSpec, RootModelListCourseInstructorSaveSpec, RootModelListCourseRelationSaveSpec, RootModelListCourseSurveySaveSpec, RootModelListLessonSaveSpec, RubricCriterionSchema, RubricSchema, ScoreStatsSchema, SearchedMediaSchema, SitePolicySchema, SitePolicyVersionSchema, SkillDataSchema, SsoAccountSchema, SsoV1AuthorizeData, SsoV1AuthorizeResponse, SsoV1AuthorizeResponses, SsoV1CallbackData, SsoV1CallbackResponses, SsoV1DeleteAccountData, SsoV1DeleteAccountResponses, SsoV1GetAccountsData, SsoV1GetAccountsResponse, SsoV1GetAccountsResponses, SsoV1LinkData, SsoV1LinkResponse, SsoV1LinkResponses, StudioContentSpec, StudioV1AssessmentSuggestionsData, StudioV1AssessmentSuggestionsResponse, StudioV1AssessmentSuggestionsResponses, StudioV1ContentData, StudioV1ContentResponse, StudioV1ContentResponses, StudioV1ContentSuggestionsData, StudioV1ContentSuggestionsResponse, StudioV1ContentSuggestionsResponses, StudioV1CreateMediaQuizData, StudioV1CreateMediaQuizResponse, StudioV1CreateMediaQuizResponses, StudioV1DeleteAssignmentQuesionData, StudioV1DeleteAssignmentQuesionResponses, StudioV1DeleteDiscussionQuesionData, StudioV1DeleteDiscussionQuesionResponses, StudioV1DeleteExamQuesionData, StudioV1DeleteExamQuesionResponses, StudioV1DeleteMediaSubtitleData, StudioV1DeleteMediaSubtitleResponses, StudioV1DeleteQuizQuesionData, StudioV1DeleteQuizQuesionResponses, StudioV1DeleteSurveyQuesionData, StudioV1DeleteSurveyQuesionResponses, StudioV1GetAssignmentData, StudioV1GetAssignmentQuestionsData, StudioV1GetAssignmentQuestionsResponse, StudioV1GetAssignmentQuestionsResponses, StudioV1GetAssignmentResponse, StudioV1GetAssignmentResponses, StudioV1GetCourseData, StudioV1GetCourseResponse, StudioV1GetCourseResponses, StudioV1GetDiscussionData, StudioV1GetDiscussionQuestionsData, StudioV1GetDiscussionQuestionsResponse, StudioV1GetDiscussionQuestionsResponses, StudioV1GetDiscussionResponse, StudioV1GetDiscussionResponses, StudioV1GetExamData, StudioV1GetExamQuestionsData, StudioV1GetExamQuestionsResponse, StudioV1GetExamQuestionsResponses, StudioV1GetExamResponse, StudioV1GetExamResponses, StudioV1GetMediaData, StudioV1GetMediaResponse, StudioV1GetMediaResponses, StudioV1GetQuizData, StudioV1GetQuizQuestionsData, StudioV1GetQuizQuestionsResponse, StudioV1GetQuizQuestionsResponses, StudioV1GetQuizResponse, StudioV1GetQuizResponses, StudioV1GetSurveyData, StudioV1GetSurveyQuestionsData, StudioV1GetSurveyQuestionsResponse, StudioV1GetSurveyQuestionsResponses, StudioV1GetSurveyResponse, StudioV1GetSurveyResponses, StudioV1InlineSuggestionsData, StudioV1InlineSuggestionsResponse, StudioV1InlineSuggestionsResponses, StudioV1RemoveCourseAssessmentData, StudioV1RemoveCourseAssessmentResponses, StudioV1RemoveCourseCategoryData, StudioV1RemoveCourseCategoryResponses, StudioV1RemoveCourseCertificateData, StudioV1RemoveCourseCertificateResponses, StudioV1RemoveCourseInstructorData, StudioV1RemoveCourseInstructorResponses, StudioV1RemoveCourseLessonData, StudioV1RemoveCourseLessonResponses, StudioV1RemoveCourseRelationData, StudioV1RemoveCourseRelationResponses, StudioV1RemoveCourseSurveyData, StudioV1RemoveCourseSurveyResponses, StudioV1SaveAssignmentData, StudioV1SaveAssignmentQuestionData, StudioV1SaveAssignmentQuestionResponse, StudioV1SaveAssignmentQuestionResponses, StudioV1SaveAssignmentResponse, StudioV1SaveAssignmentResponses, StudioV1SaveCourseAssessmentsData, StudioV1SaveCourseAssessmentsResponse, StudioV1SaveCourseAssessmentsResponses, StudioV1SaveCourseCategoriesData, StudioV1SaveCourseCategoriesResponse, StudioV1SaveCourseCategoriesResponses, StudioV1SaveCourseCertificatesData, StudioV1SaveCourseCertificatesResponse, StudioV1SaveCourseCertificatesResponses, StudioV1SaveCourseData, StudioV1SaveCourseInstructorsData, StudioV1SaveCourseInstructorsResponse, StudioV1SaveCourseInstructorsResponses, StudioV1SaveCourseLessonsData, StudioV1SaveCourseLessonsResponse, StudioV1SaveCourseLessonsResponses, StudioV1SaveCourseRelationsData, StudioV1SaveCourseRelationsResponse, StudioV1SaveCourseRelationsResponses, StudioV1SaveCourseResponse, StudioV1SaveCourseResponses, StudioV1SaveCourseSurveysData, StudioV1SaveCourseSurveysResponse, StudioV1SaveCourseSurveysResponses, StudioV1SaveDiscussionData, StudioV1SaveDiscussionQuestionData, StudioV1SaveDiscussionQuestionResponse, StudioV1SaveDiscussionQuestionResponses, StudioV1SaveDiscussionResponse, StudioV1SaveDiscussionResponses, StudioV1SaveExamData, StudioV1SaveExamQuestionsData, StudioV1SaveExamQuestionsResponse, StudioV1SaveExamQuestionsResponses, StudioV1SaveExamResponse, StudioV1SaveExamResponses, StudioV1SaveMediaData, StudioV1SaveMediaResponse, StudioV1SaveMediaResponses, StudioV1SaveMediaSubtitleData, StudioV1SaveMediaSubtitleResponses, StudioV1SaveQuizData, StudioV1SaveQuizQuestionsData, StudioV1SaveQuizQuestionsResponse, StudioV1SaveQuizQuestionsResponses, StudioV1SaveQuizResponse, StudioV1SaveQuizResponses, StudioV1SaveSurveyData, StudioV1SaveSurveyQuestionsData, StudioV1SaveSurveyQuestionsResponse, StudioV1SaveSurveyQuestionsResponses, StudioV1SaveSurveyResponse, StudioV1SaveSurveyResponses, SubtitleSchema, SubtitleSpec, SurveyAnswersSchema, SurveyQuestionFormatChoices, SurveyQuestionSaveSpec, SurveyQuestionSchema, SurveyQuestionSpec, SurveyQuestionsSaveSpec, SurveyQuestionsSpec, SurveySaveSpec, SurveySchema, SurveySpec, SurveyV1GetAnonymousSurveyData, SurveyV1GetAnonymousSurveyResponse, SurveyV1GetAnonymousSurveyResponses, SurveyV1GetSurveyData, SurveyV1GetSurveyResponse, SurveyV1GetSurveyResponses, SurveyV1ResultsAnonymousData, SurveyV1ResultsAnonymousResponse, SurveyV1ResultsAnonymousResponses, SurveyV1ResultsData, SurveyV1ResultsResponse, SurveyV1ResultsResponses, SurveyV1SubmitAnonymousData, SurveyV1SubmitAnonymousResponses, SurveyV1SubmitData, SurveyV1SubmitResponses, ThreadCreateSchema, ThreadSchema, TotpDeviceSchema, UserSchema, UserUpdateSchema, WatchedMediaSchema, WatchInSchema, WatchOutSchema } from './types.gen'; +export { accountV1Activate, accountV1ApplyEmailChange, accountV1ApplyPasswordChange, accountV1CompleteOtpSetup, accountV1GetMe, accountV1Join, accountV1Login, accountV1Logout, accountV1RequestActivation, accountV1RequestEmailChange, accountV1RequestPasswordChange, accountV1ResetOtp, accountV1SetupOtp, accountV1UpdateMe, accountV1UploadAvatar, accountV1VerifyOtp, assignmentV1DeactivateAttempt, assignmentV1GetSession, assignmentV1StartAttempt, assignmentV1SubmitAttempt, assistantV1ChatMessage, assistantV1DeleteChat, assistantV1GetChatMessages, assistantV1GetChats, assistantV1SaveAssistantNote, assistantV1UpdateChatMessage, competencyV1DeleteCompetencyGoal, competencyV1GetCertificateAwards, competencyV1GetCertificates, competencyV1GetClassificationSkillsData, competencyV1GetClassificationTree, competencyV1GetCompetencyGoals, competencyV1SaveCompetencyGoal, contentV1DeleteMediaWatch, contentV1GetMedia, contentV1GetMediaNote, contentV1GetMediaWatch, contentV1GetSubtitles, contentV1GetWatchMedias, contentV1SaveMediaNote, contentV1Search, contentV1SearchSuggestion, contentV1UpdateMediaWatch, courseV1GetDetail, courseV1GetSession, courseV1RequestCertificate, courseV1StartEngagement, discussionV1CreatePost, discussionV1DeactivateAttempt, discussionV1DeletePost, discussionV1GetOwnPosts, discussionV1GetPosts, discussionV1GetSession, discussionV1StartAttempt, discussionV1UpdatePost, examV1DeactivateAttempt, examV1GetSession, examV1GetTimestamp, examV1SaveAnswers, examV1StartAttempt, examV1SubmitAttempt, learningV1EnrollCatalogItem, learningV1GetCatalogItems, learningV1GetCatalogs, learningV1GetEnrolled, learningV1GetRecords, learningV1GetReport, learningV1Unenroll, minimaApiHealth, operationV1AgreePolicies, operationV1CreateAppeal, operationV1CreateInquiry, operationV1CreateThread, operationV1DeleteComment, operationV1DeleteDevice, operationV1EffectivePolicies, operationV1GetAnnouncements, operationV1GetComments, operationV1GetDevices, operationV1GetInquiries, operationV1GetThread, operationV1GetThreadComments, operationV1GetUnreadMessages, operationV1ReadAnnouncement, operationV1ReadMessage, operationV1RegisterDevice, operationV1SaveComment, operationV1ToggleDeviceActive, operationV1UpdateInquiry, type Options, partnerV1MemberInfos, quizV1DeactivateAttempt, quizV1GetSession, quizV1StartAttempt, quizV1SubmitAttempt, ssoV1Authorize, ssoV1Callback, ssoV1DeleteAccount, ssoV1GetAccounts, ssoV1Link, studioV1AssessmentSuggestions, studioV1Content, studioV1ContentSuggestions, studioV1CreateMediaQuiz, studioV1DeleteAssignment, studioV1DeleteAssignmentQuesion, studioV1DeleteCourse, studioV1DeleteDiscussion, studioV1DeleteDiscussionQuesion, studioV1DeleteExam, studioV1DeleteExamQuesion, studioV1DeleteMedia, studioV1DeleteMediaSubtitle, studioV1DeleteQuiz, studioV1DeleteQuizQuesion, studioV1DeleteSurvey, studioV1DeleteSurveyQuesion, studioV1GetAssignment, studioV1GetAssignmentQuestions, studioV1GetAssignmentRubric, studioV1GetCourse, studioV1GetDiscussion, studioV1GetDiscussionQuestions, studioV1GetExam, studioV1GetExamQuestions, studioV1GetMedia, studioV1GetQuiz, studioV1GetQuizQuestions, studioV1GetSurvey, studioV1GetSurveyQuestions, studioV1InlineSuggestions, studioV1RemoveCourseAssessment, studioV1RemoveCourseCategory, studioV1RemoveCourseCertificate, studioV1RemoveCourseInstructor, studioV1RemoveCourseLesson, studioV1RemoveCourseRelation, studioV1RemoveCourseSurvey, studioV1SaveAssignment, studioV1SaveAssignmentQuestions, studioV1SaveAssignmentRubric, studioV1SaveCourse, studioV1SaveCourseAssessments, studioV1SaveCourseCategories, studioV1SaveCourseCertificates, studioV1SaveCourseInstructors, studioV1SaveCourseLessons, studioV1SaveCourseRelations, studioV1SaveCourseSurveys, studioV1SaveDiscussion, studioV1SaveDiscussionQuestions, studioV1SaveExam, studioV1SaveExamQuestions, studioV1SaveMedia, studioV1SaveMediaSubtitle, studioV1SaveQuiz, studioV1SaveQuizQuestions, studioV1SaveSurvey, studioV1SaveSurveyQuestions, surveyV1GetAnonymousSurvey, surveyV1GetSurvey, surveyV1Results, surveyV1ResultsAnonymous, surveyV1Submit, surveyV1SubmitAnonymous } from './sdk.gen'; +export type { AccessDateSchema, AccountActivatedSchema, AccountActivateSchema, AccountV1ActivateData, AccountV1ActivateResponse, AccountV1ActivateResponses, AccountV1ApplyEmailChangeData, AccountV1ApplyEmailChangeResponses, AccountV1ApplyPasswordChangeData, AccountV1ApplyPasswordChangeResponses, AccountV1CompleteOtpSetupData, AccountV1CompleteOtpSetupResponse, AccountV1CompleteOtpSetupResponses, AccountV1GetMeData, AccountV1GetMeResponse, AccountV1GetMeResponses, AccountV1JoinData, AccountV1JoinResponses, AccountV1LoginData, AccountV1LoginResponse, AccountV1LoginResponses, AccountV1LogoutData, AccountV1LogoutResponses, AccountV1RequestActivationData, AccountV1RequestActivationResponses, AccountV1RequestEmailChangeData, AccountV1RequestEmailChangeResponses, AccountV1RequestPasswordChangeData, AccountV1RequestPasswordChangeResponses, AccountV1ResetOtpData, AccountV1ResetOtpResponses, AccountV1SetupOtpData, AccountV1SetupOtpResponse, AccountV1SetupOtpResponses, AccountV1UpdateMeData, AccountV1UpdateMeResponse, AccountV1UpdateMeResponses, AccountV1UploadAvatarData, AccountV1UploadAvatarResponse, AccountV1UploadAvatarResponses, AccountV1VerifyOtpData, AccountV1VerifyOtpResponses, AnnounceSchema, AppealCreateSchema, AppealSchema, ApplyEmailChangeSchema, ApplyPasswordChangeSchema, AssessmentSaveSpec, AssessmentSpec, AssessmentSuggestionSpec, AssignmentAttemptSchema, AssignmentGradeSchema, AssignmentQuestionSaveSpec, AssignmentQuestionSchema, AssignmentQuestionSpec, AssignmentQuestionsSaveSpec, AssignmentQuestionsSpec, AssignmentSaveSpec, AssignmentSchema, AssignmentSessionSchema, AssignmentSpec, AssignmentSubmissionSchema, AssignmentSubmitSchema, AssignmentV1DeactivateAttemptData, AssignmentV1DeactivateAttemptResponses, AssignmentV1GetSessionData, AssignmentV1GetSessionResponse, AssignmentV1GetSessionResponses, AssignmentV1StartAttemptData, AssignmentV1StartAttemptResponse, AssignmentV1StartAttemptResponses, AssignmentV1SubmitAttemptData, AssignmentV1SubmitAttemptResponse, AssignmentV1SubmitAttemptResponses, AssistantNoteSaveSchema, AssistantV1ChatMessageData, AssistantV1ChatMessageResponses, AssistantV1DeleteChatData, AssistantV1DeleteChatResponses, AssistantV1GetChatMessagesData, AssistantV1GetChatMessagesResponse, AssistantV1GetChatMessagesResponses, AssistantV1GetChatsData, AssistantV1GetChatsResponse, AssistantV1GetChatsResponses, AssistantV1SaveAssistantNoteData, AssistantV1SaveAssistantNoteResponses, AssistantV1UpdateChatMessageData, AssistantV1UpdateChatMessageResponses, AuthorizeResponseSchema, AuthorizeSchema, CatalogContentSchema, CatalogItemEnrollSchema, CatalogItemSchema, CatalogSchema, CertificateAwardSchema, CertificateEndorsementSchema, CertificateFilterSchema, CertificateSchema, CertificateSkillSchema, ChatListSchema, ChatMessageCreateSchema, ChatMessageSchema, ChatMessageUpdateSchema, ChatSchema, ClassificationSchema, ClassificationTreeNodeSchema, ClientOptions, CohortSchema, CohortStaffSchema, CommentBriefSchema, CommentNestedSchema, CommentSavedSchema, CommentSaveSchema, CommentSchema, CommentThreadSchema, CompetencyFactorySchema, CompetencyGoalSaveSchema, CompetencyGoalSchema, CompetencySkillSchema, CompetencyV1DeleteCompetencyGoalData, CompetencyV1DeleteCompetencyGoalResponses, CompetencyV1GetCertificateAwardsData, CompetencyV1GetCertificateAwardsResponse, CompetencyV1GetCertificateAwardsResponses, CompetencyV1GetCertificatesData, CompetencyV1GetCertificatesResponse, CompetencyV1GetCertificatesResponses, CompetencyV1GetClassificationSkillsDataData, CompetencyV1GetClassificationSkillsDataResponse, CompetencyV1GetClassificationSkillsDataResponses, CompetencyV1GetClassificationTreeData, CompetencyV1GetClassificationTreeResponse, CompetencyV1GetClassificationTreeResponses, CompetencyV1GetCompetencyGoalsData, CompetencyV1GetCompetencyGoalsResponse, CompetencyV1GetCompetencyGoalsResponses, CompetencyV1SaveCompetencyGoalData, CompetencyV1SaveCompetencyGoalResponse, CompetencyV1SaveCompetencyGoalResponses, ContentSuggestionSpec, ContentTypeSchema, ContentV1DeleteMediaWatchData, ContentV1DeleteMediaWatchResponses, ContentV1GetMediaData, ContentV1GetMediaNoteData, ContentV1GetMediaNoteResponse, ContentV1GetMediaNoteResponses, ContentV1GetMediaResponse, ContentV1GetMediaResponses, ContentV1GetMediaWatchData, ContentV1GetMediaWatchResponse, ContentV1GetMediaWatchResponses, ContentV1GetSubtitlesData, ContentV1GetSubtitlesResponse, ContentV1GetSubtitlesResponses, ContentV1GetWatchMediasData, ContentV1GetWatchMediasResponse, ContentV1GetWatchMediasResponses, ContentV1SaveMediaNoteData, ContentV1SaveMediaNoteResponse, ContentV1SaveMediaNoteResponses, ContentV1SearchData, ContentV1SearchResponse, ContentV1SearchResponses, ContentV1SearchSuggestionData, ContentV1SearchSuggestionResponse, ContentV1SearchSuggestionResponses, ContentV1UpdateMediaWatchData, ContentV1UpdateMediaWatchResponses, CourseAssetsSpec, CourseCategorySaveSpec, CourseCategorySchema, CourseCategorySpec, CourseCertificateItemSchema, CourseCertificateRequestSchema, CourseCertificateSaveSpec, CourseCertificateSchema, CourseCertificateSpec, CourseDetailSchema, CourseEngagementSchema, CourseGradebookSchema, CourseInstructorItemSchema, CourseInstructorSaveSpec, CourseInstructorSchema, CourseInstructorSpec, CourseRelationItemSchema, CourseRelationSaveSpec, CourseRelationSchema, CourseRelationSpec, CourseSaveSpec, CourseSchema, CourseSessionSchema, CourseSpec, CourseSurveyItemSchema, CourseSurveySaveSpec, CourseSurveySchema, CourseSurveySpec, CourseV1GetDetailData, CourseV1GetDetailResponse, CourseV1GetDetailResponses, CourseV1GetSessionData, CourseV1GetSessionResponse, CourseV1GetSessionResponses, CourseV1RequestCertificateData, CourseV1RequestCertificateResponse, CourseV1RequestCertificateResponses, CourseV1StartEngagementData, CourseV1StartEngagementResponse, CourseV1StartEngagementResponses, DeviceRegisterSchema, DeviceSchema, DiscussionAttemptSchema, DiscussionEarnedDetailsSchema, DiscussionFeedbackSchema, DiscussionGradeSchema, DiscussionOwnPostSchema, DiscussionPostCountSchema, DiscussionPostNestedSchema, DiscussionPostSaveSchema, DiscussionPostSchema, DiscussionPostWithCountSchema, DiscussionQuestionSaveSpec, DiscussionQuestionSchema, DiscussionQuestionSpec, DiscussionQuestionsSaveSpec, DiscussionQuestionsSpec, DiscussionSaveSpec, DiscussionSchema, DiscussionSessionSchema, DiscussionSpec, DiscussionV1CreatePostData, DiscussionV1CreatePostResponse, DiscussionV1CreatePostResponses, DiscussionV1DeactivateAttemptData, DiscussionV1DeactivateAttemptResponses, DiscussionV1DeletePostData, DiscussionV1DeletePostResponses, DiscussionV1GetOwnPostsData, DiscussionV1GetOwnPostsResponse, DiscussionV1GetOwnPostsResponses, DiscussionV1GetPostsData, DiscussionV1GetPostsResponse, DiscussionV1GetPostsResponses, DiscussionV1GetSessionData, DiscussionV1GetSessionResponse, DiscussionV1GetSessionResponses, DiscussionV1StartAttemptData, DiscussionV1StartAttemptResponse, DiscussionV1StartAttemptResponses, DiscussionV1UpdatePostData, DiscussionV1UpdatePostResponse, DiscussionV1UpdatePostResponses, EnrollmentContentSchema, EnrollmentSchema, EnrollmentSuccessSchema, ExamAttemptAnswersSchema, ExamAttemptSchema, ExamGradeSchema, ExamQuestionFormatChoices, ExamQuestionPoolSchema, ExamQuestionPoolSpec, ExamQuestionSaveSpec, ExamQuestionSchema, ExamQuestionSolutionSpec, ExamQuestionSpec, ExamQuestionsSaveSpec, ExamQuestionsSpec, ExamSaveSpec, ExamSchema, ExamSessionSchema, ExamSolutionSchema, ExamSpec, ExamSubmissionSchema, ExamV1DeactivateAttemptData, ExamV1DeactivateAttemptResponses, ExamV1GetSessionData, ExamV1GetSessionResponse, ExamV1GetSessionResponses, ExamV1GetTimestampData, ExamV1GetTimestampResponse, ExamV1GetTimestampResponses, ExamV1SaveAnswersData, ExamV1SaveAnswersResponses, ExamV1StartAttemptData, ExamV1StartAttemptResponse, ExamV1StartAttemptResponses, ExamV1SubmitAttemptData, ExamV1SubmitAttemptResponse, ExamV1SubmitAttemptResponses, FactoryDataSchema, FaqItemSchema, FaqSchema, GradingCriterionSchema, GradingDateSchema, GradingPolicySpec, HonorCodeSchema, InlineSuggestionSpec, Input, InquiryCreateSchema, InquiryFilterSchema, InquiryResponseSchema, InquirySavedSchema, InquirySchema, InquiryUpdateSchema, JoinSchema, KindChoices, LearningRecordSchema, LearningReportSchema, LearningSessionStep, LearningV1EnrollCatalogItemData, LearningV1EnrollCatalogItemResponse, LearningV1EnrollCatalogItemResponses, LearningV1GetCatalogItemsData, LearningV1GetCatalogItemsResponse, LearningV1GetCatalogItemsResponses, LearningV1GetCatalogsData, LearningV1GetCatalogsResponse, LearningV1GetCatalogsResponses, LearningV1GetEnrolledData, LearningV1GetEnrolledResponse, LearningV1GetEnrolledResponses, LearningV1GetRecordsData, LearningV1GetRecordsResponse, LearningV1GetRecordsResponses, LearningV1GetReportData, LearningV1GetReportResponse, LearningV1GetReportResponses, LearningV1UnenrollData, LearningV1UnenrollResponses, LessonMediaItemSchema, LessonMediaSchema, LessonSaveSpec, LessonSchema, LessonSpec, LevelChoices, LoginSchema, MatchedLineSchema, MediaFormatChoices, MediaSaveSpec, MediaSchema, MediaSpec, MessageDataSchema, MessageSchema, MinimaApiHealthData, MinimaApiHealthResponses, NoteSaveSchema, NoteSchema, OperationV1AgreePoliciesData, OperationV1AgreePoliciesResponses, OperationV1CreateAppealData, OperationV1CreateAppealResponse, OperationV1CreateAppealResponses, OperationV1CreateInquiryData, OperationV1CreateInquiryResponse, OperationV1CreateInquiryResponses, OperationV1CreateThreadData, OperationV1CreateThreadResponse, OperationV1CreateThreadResponses, OperationV1DeleteCommentData, OperationV1DeleteCommentResponses, OperationV1DeleteDeviceData, OperationV1DeleteDeviceResponses, OperationV1EffectivePoliciesData, OperationV1EffectivePoliciesResponse, OperationV1EffectivePoliciesResponses, OperationV1GetAnnouncementsData, OperationV1GetAnnouncementsResponse, OperationV1GetAnnouncementsResponses, OperationV1GetCommentsData, OperationV1GetCommentsResponse, OperationV1GetCommentsResponses, OperationV1GetDevicesData, OperationV1GetDevicesResponse, OperationV1GetDevicesResponses, OperationV1GetInquiriesData, OperationV1GetInquiriesResponse, OperationV1GetInquiriesResponses, OperationV1GetThreadCommentsData, OperationV1GetThreadCommentsResponse, OperationV1GetThreadCommentsResponses, OperationV1GetThreadData, OperationV1GetThreadResponse, OperationV1GetThreadResponses, OperationV1GetUnreadMessagesData, OperationV1GetUnreadMessagesResponse, OperationV1GetUnreadMessagesResponses, OperationV1ReadAnnouncementData, OperationV1ReadAnnouncementResponses, OperationV1ReadMessageData, OperationV1ReadMessageResponses, OperationV1RegisterDeviceData, OperationV1RegisterDeviceResponse, OperationV1RegisterDeviceResponses, OperationV1SaveCommentData, OperationV1SaveCommentResponse, OperationV1SaveCommentResponses, OperationV1ToggleDeviceActiveData, OperationV1ToggleDeviceActiveResponses, OperationV1UpdateInquiryData, OperationV1UpdateInquiryResponse, OperationV1UpdateInquiryResponses, OtpSetupCompleteSchema, OtpSetupSchema, OtpVerifySchema, OwnerSchema, PagedAnnounceSchema, PagedCertificateAwardSchema, PagedChatMessageSchema, PagedCommentBriefSchema, PagedCommentNestedSchema, PagedDiscussionPostNestedSchema, PagedInquirySchema, PagedMessageSchema, PagedWatchedMediaSchema, PaginatedResponseCatalogItemSchema, PaginatedResponseEnrollmentSchema, PaginatedResponseSearchedMediaSchema, PaginatedResponseStudioContentSpec, PartnerGroupMemberSchema, PartnerGroupSchema, PartnerSchema, PartnerV1MemberInfosData, PartnerV1MemberInfosResponse, PartnerV1MemberInfosResponses, PerformanceLevelSchema, PolicyVersionAgreementSchema, QuizAttemptAnswersSchema, QuizAttemptSchema, QuizGradeSchema, QuizQuestionPoolSpec, QuizQuestionSaveSpec, QuizQuestionSchema, QuizQuestionSolutionSpec, QuizQuestionSpec, QuizQuestionsSaveSpec, QuizQuestionsSpec, QuizSaveSpec, QuizSchema, QuizSessionSchema, QuizSolutionSchema, QuizSpec, QuizSubmissionSchema, QuizV1DeactivateAttemptData, QuizV1DeactivateAttemptResponses, QuizV1GetSessionData, QuizV1GetSessionResponse, QuizV1GetSessionResponses, QuizV1StartAttemptData, QuizV1StartAttemptResponse, QuizV1StartAttemptResponses, QuizV1SubmitAttemptData, QuizV1SubmitAttemptResponse, QuizV1SubmitAttemptResponses, RequestActivationSchema, RequestEmailChangeSchema, RequestPasswordChangeSchema, RoleChoices, RootModelListAssessmentSaveSpec, RootModelListCourseCategorySaveSpec, RootModelListCourseCertificateSaveSpec, RootModelListCourseInstructorSaveSpec, RootModelListCourseRelationSaveSpec, RootModelListCourseSurveySaveSpec, RootModelListLessonSaveSpec, RootModelListRubricCriterionSpec, RubricCriterionSchema, RubricCriterionSpec, RubricSchema, ScoreStatsSchema, SearchedMediaSchema, SitePolicySchema, SitePolicyVersionSchema, SkillDataSchema, SsoAccountSchema, SsoV1AuthorizeData, SsoV1AuthorizeResponse, SsoV1AuthorizeResponses, SsoV1CallbackData, SsoV1CallbackResponses, SsoV1DeleteAccountData, SsoV1DeleteAccountResponses, SsoV1GetAccountsData, SsoV1GetAccountsResponse, SsoV1GetAccountsResponses, SsoV1LinkData, SsoV1LinkResponse, SsoV1LinkResponses, StudioContentSpec, StudioV1AssessmentSuggestionsData, StudioV1AssessmentSuggestionsResponse, StudioV1AssessmentSuggestionsResponses, StudioV1ContentData, StudioV1ContentResponse, StudioV1ContentResponses, StudioV1ContentSuggestionsData, StudioV1ContentSuggestionsResponse, StudioV1ContentSuggestionsResponses, StudioV1CreateMediaQuizData, StudioV1CreateMediaQuizResponse, StudioV1CreateMediaQuizResponses, StudioV1DeleteAssignmentData, StudioV1DeleteAssignmentQuesionData, StudioV1DeleteAssignmentQuesionResponses, StudioV1DeleteAssignmentResponses, StudioV1DeleteCourseData, StudioV1DeleteCourseResponses, StudioV1DeleteDiscussionData, StudioV1DeleteDiscussionQuesionData, StudioV1DeleteDiscussionQuesionResponses, StudioV1DeleteDiscussionResponses, StudioV1DeleteExamData, StudioV1DeleteExamQuesionData, StudioV1DeleteExamQuesionResponses, StudioV1DeleteExamResponses, StudioV1DeleteMediaData, StudioV1DeleteMediaResponses, StudioV1DeleteMediaSubtitleData, StudioV1DeleteMediaSubtitleResponses, StudioV1DeleteQuizData, StudioV1DeleteQuizQuesionData, StudioV1DeleteQuizQuesionResponses, StudioV1DeleteQuizResponses, StudioV1DeleteSurveyData, StudioV1DeleteSurveyQuesionData, StudioV1DeleteSurveyQuesionResponses, StudioV1DeleteSurveyResponses, StudioV1GetAssignmentData, StudioV1GetAssignmentQuestionsData, StudioV1GetAssignmentQuestionsResponse, StudioV1GetAssignmentQuestionsResponses, StudioV1GetAssignmentResponse, StudioV1GetAssignmentResponses, StudioV1GetAssignmentRubricData, StudioV1GetAssignmentRubricResponse, StudioV1GetAssignmentRubricResponses, StudioV1GetCourseData, StudioV1GetCourseResponse, StudioV1GetCourseResponses, StudioV1GetDiscussionData, StudioV1GetDiscussionQuestionsData, StudioV1GetDiscussionQuestionsResponse, StudioV1GetDiscussionQuestionsResponses, StudioV1GetDiscussionResponse, StudioV1GetDiscussionResponses, StudioV1GetExamData, StudioV1GetExamQuestionsData, StudioV1GetExamQuestionsResponse, StudioV1GetExamQuestionsResponses, StudioV1GetExamResponse, StudioV1GetExamResponses, StudioV1GetMediaData, StudioV1GetMediaResponse, StudioV1GetMediaResponses, StudioV1GetQuizData, StudioV1GetQuizQuestionsData, StudioV1GetQuizQuestionsResponse, StudioV1GetQuizQuestionsResponses, StudioV1GetQuizResponse, StudioV1GetQuizResponses, StudioV1GetSurveyData, StudioV1GetSurveyQuestionsData, StudioV1GetSurveyQuestionsResponse, StudioV1GetSurveyQuestionsResponses, StudioV1GetSurveyResponse, StudioV1GetSurveyResponses, StudioV1InlineSuggestionsData, StudioV1InlineSuggestionsResponse, StudioV1InlineSuggestionsResponses, StudioV1RemoveCourseAssessmentData, StudioV1RemoveCourseAssessmentResponses, StudioV1RemoveCourseCategoryData, StudioV1RemoveCourseCategoryResponses, StudioV1RemoveCourseCertificateData, StudioV1RemoveCourseCertificateResponses, StudioV1RemoveCourseInstructorData, StudioV1RemoveCourseInstructorResponses, StudioV1RemoveCourseLessonData, StudioV1RemoveCourseLessonResponses, StudioV1RemoveCourseRelationData, StudioV1RemoveCourseRelationResponses, StudioV1RemoveCourseSurveyData, StudioV1RemoveCourseSurveyResponses, StudioV1SaveAssignmentData, StudioV1SaveAssignmentQuestionsData, StudioV1SaveAssignmentQuestionsResponse, StudioV1SaveAssignmentQuestionsResponses, StudioV1SaveAssignmentResponse, StudioV1SaveAssignmentResponses, StudioV1SaveAssignmentRubricData, StudioV1SaveAssignmentRubricResponses, StudioV1SaveCourseAssessmentsData, StudioV1SaveCourseAssessmentsResponse, StudioV1SaveCourseAssessmentsResponses, StudioV1SaveCourseCategoriesData, StudioV1SaveCourseCategoriesResponse, StudioV1SaveCourseCategoriesResponses, StudioV1SaveCourseCertificatesData, StudioV1SaveCourseCertificatesResponse, StudioV1SaveCourseCertificatesResponses, StudioV1SaveCourseData, StudioV1SaveCourseInstructorsData, StudioV1SaveCourseInstructorsResponse, StudioV1SaveCourseInstructorsResponses, StudioV1SaveCourseLessonsData, StudioV1SaveCourseLessonsResponse, StudioV1SaveCourseLessonsResponses, StudioV1SaveCourseRelationsData, StudioV1SaveCourseRelationsResponse, StudioV1SaveCourseRelationsResponses, StudioV1SaveCourseResponse, StudioV1SaveCourseResponses, StudioV1SaveCourseSurveysData, StudioV1SaveCourseSurveysResponse, StudioV1SaveCourseSurveysResponses, StudioV1SaveDiscussionData, StudioV1SaveDiscussionQuestionsData, StudioV1SaveDiscussionQuestionsResponse, StudioV1SaveDiscussionQuestionsResponses, StudioV1SaveDiscussionResponse, StudioV1SaveDiscussionResponses, StudioV1SaveExamData, StudioV1SaveExamQuestionsData, StudioV1SaveExamQuestionsResponse, StudioV1SaveExamQuestionsResponses, StudioV1SaveExamResponse, StudioV1SaveExamResponses, StudioV1SaveMediaData, StudioV1SaveMediaResponse, StudioV1SaveMediaResponses, StudioV1SaveMediaSubtitleData, StudioV1SaveMediaSubtitleResponses, StudioV1SaveQuizData, StudioV1SaveQuizQuestionsData, StudioV1SaveQuizQuestionsResponse, StudioV1SaveQuizQuestionsResponses, StudioV1SaveQuizResponse, StudioV1SaveQuizResponses, StudioV1SaveSurveyData, StudioV1SaveSurveyQuestionsData, StudioV1SaveSurveyQuestionsResponse, StudioV1SaveSurveyQuestionsResponses, StudioV1SaveSurveyResponse, StudioV1SaveSurveyResponses, SubtitleSchema, SubtitleSpec, SurveyAnswersSchema, SurveyQuestionFormatChoices, SurveyQuestionSaveSpec, SurveyQuestionSchema, SurveyQuestionSpec, SurveyQuestionsSaveSpec, SurveyQuestionsSpec, SurveySaveSpec, SurveySchema, SurveySpec, SurveyV1GetAnonymousSurveyData, SurveyV1GetAnonymousSurveyResponse, SurveyV1GetAnonymousSurveyResponses, SurveyV1GetSurveyData, SurveyV1GetSurveyResponse, SurveyV1GetSurveyResponses, SurveyV1ResultsAnonymousData, SurveyV1ResultsAnonymousResponse, SurveyV1ResultsAnonymousResponses, SurveyV1ResultsData, SurveyV1ResultsResponse, SurveyV1ResultsResponses, SurveyV1SubmitAnonymousData, SurveyV1SubmitAnonymousResponses, SurveyV1SubmitData, SurveyV1SubmitResponses, ThreadCreateSchema, ThreadSchema, TotpDeviceSchema, UserSchema, UserUpdateSchema, WatchedMediaSchema, WatchInSchema, WatchOutSchema } from './types.gen'; diff --git a/web/src/api/sdk.gen.ts b/web/src/api/sdk.gen.ts index 72ff612..dabe4ac 100644 --- a/web/src/api/sdk.gen.ts +++ b/web/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import { type Client, formDataBodySerializer, type Options as Options2, type TDataShape } from './client'; import { client } from './client.gen'; -import type { AccountV1ActivateData, AccountV1ActivateResponses, AccountV1ApplyEmailChangeData, AccountV1ApplyEmailChangeResponses, AccountV1ApplyPasswordChangeData, AccountV1ApplyPasswordChangeResponses, AccountV1CompleteOtpSetupData, AccountV1CompleteOtpSetupResponses, AccountV1GetMeData, AccountV1GetMeResponses, AccountV1JoinData, AccountV1JoinResponses, AccountV1LoginData, AccountV1LoginResponses, AccountV1LogoutData, AccountV1LogoutResponses, AccountV1RequestActivationData, AccountV1RequestActivationResponses, AccountV1RequestEmailChangeData, AccountV1RequestEmailChangeResponses, AccountV1RequestPasswordChangeData, AccountV1RequestPasswordChangeResponses, AccountV1ResetOtpData, AccountV1ResetOtpResponses, AccountV1SetupOtpData, AccountV1SetupOtpResponses, AccountV1UpdateMeData, AccountV1UpdateMeResponses, AccountV1UploadAvatarData, AccountV1UploadAvatarResponses, AccountV1VerifyOtpData, AccountV1VerifyOtpResponses, AssignmentV1DeactivateAttemptData, AssignmentV1DeactivateAttemptResponses, AssignmentV1GetSessionData, AssignmentV1GetSessionResponses, AssignmentV1StartAttemptData, AssignmentV1StartAttemptResponses, AssignmentV1SubmitAttemptData, AssignmentV1SubmitAttemptResponses, AssistantV1ChatMessageData, AssistantV1ChatMessageResponses, AssistantV1DeleteChatData, AssistantV1DeleteChatResponses, AssistantV1GetChatMessagesData, AssistantV1GetChatMessagesResponses, AssistantV1GetChatsData, AssistantV1GetChatsResponses, AssistantV1SaveAssistantNoteData, AssistantV1SaveAssistantNoteResponses, AssistantV1UpdateChatMessageData, AssistantV1UpdateChatMessageResponses, CompetencyV1DeleteCompetencyGoalData, CompetencyV1DeleteCompetencyGoalResponses, CompetencyV1GetCertificateAwardsData, CompetencyV1GetCertificateAwardsResponses, CompetencyV1GetCertificatesData, CompetencyV1GetCertificatesResponses, CompetencyV1GetClassificationSkillsDataData, CompetencyV1GetClassificationSkillsDataResponses, CompetencyV1GetClassificationTreeData, CompetencyV1GetClassificationTreeResponses, CompetencyV1GetCompetencyGoalsData, CompetencyV1GetCompetencyGoalsResponses, CompetencyV1SaveCompetencyGoalData, CompetencyV1SaveCompetencyGoalResponses, ContentV1DeleteMediaWatchData, ContentV1DeleteMediaWatchResponses, ContentV1GetMediaData, ContentV1GetMediaNoteData, ContentV1GetMediaNoteResponses, ContentV1GetMediaResponses, ContentV1GetMediaWatchData, ContentV1GetMediaWatchResponses, ContentV1GetSubtitlesData, ContentV1GetSubtitlesResponses, ContentV1GetWatchMediasData, ContentV1GetWatchMediasResponses, ContentV1SaveMediaNoteData, ContentV1SaveMediaNoteResponses, ContentV1SearchData, ContentV1SearchResponses, ContentV1SearchSuggestionData, ContentV1SearchSuggestionResponses, ContentV1UpdateMediaWatchData, ContentV1UpdateMediaWatchResponses, CourseV1GetDetailData, CourseV1GetDetailResponses, CourseV1GetSessionData, CourseV1GetSessionResponses, CourseV1RequestCertificateData, CourseV1RequestCertificateResponses, CourseV1StartEngagementData, CourseV1StartEngagementResponses, DiscussionV1CreatePostData, DiscussionV1CreatePostResponses, DiscussionV1DeactivateAttemptData, DiscussionV1DeactivateAttemptResponses, DiscussionV1DeletePostData, DiscussionV1DeletePostResponses, DiscussionV1GetOwnPostsData, DiscussionV1GetOwnPostsResponses, DiscussionV1GetPostsData, DiscussionV1GetPostsResponses, DiscussionV1GetSessionData, DiscussionV1GetSessionResponses, DiscussionV1StartAttemptData, DiscussionV1StartAttemptResponses, DiscussionV1UpdatePostData, DiscussionV1UpdatePostResponses, ExamV1DeactivateAttemptData, ExamV1DeactivateAttemptResponses, ExamV1GetSessionData, ExamV1GetSessionResponses, ExamV1GetTimestampData, ExamV1GetTimestampResponses, ExamV1SaveAnswersData, ExamV1SaveAnswersResponses, ExamV1StartAttemptData, ExamV1StartAttemptResponses, ExamV1SubmitAttemptData, ExamV1SubmitAttemptResponses, LearningV1EnrollCatalogItemData, LearningV1EnrollCatalogItemResponses, LearningV1GetCatalogItemsData, LearningV1GetCatalogItemsResponses, LearningV1GetCatalogsData, LearningV1GetCatalogsResponses, LearningV1GetEnrolledData, LearningV1GetEnrolledResponses, LearningV1GetRecordsData, LearningV1GetRecordsResponses, LearningV1GetReportData, LearningV1GetReportResponses, LearningV1UnenrollData, LearningV1UnenrollResponses, MinimaApiHealthData, MinimaApiHealthResponses, OperationV1AgreePoliciesData, OperationV1AgreePoliciesResponses, OperationV1CreateAppealData, OperationV1CreateAppealResponses, OperationV1CreateInquiryData, OperationV1CreateInquiryResponses, OperationV1CreateThreadData, OperationV1CreateThreadResponses, OperationV1DeleteCommentData, OperationV1DeleteCommentResponses, OperationV1DeleteDeviceData, OperationV1DeleteDeviceResponses, OperationV1EffectivePoliciesData, OperationV1EffectivePoliciesResponses, OperationV1GetAnnouncementsData, OperationV1GetAnnouncementsResponses, OperationV1GetCommentsData, OperationV1GetCommentsResponses, OperationV1GetDevicesData, OperationV1GetDevicesResponses, OperationV1GetInquiriesData, OperationV1GetInquiriesResponses, OperationV1GetThreadCommentsData, OperationV1GetThreadCommentsResponses, OperationV1GetThreadData, OperationV1GetThreadResponses, OperationV1GetUnreadMessagesData, OperationV1GetUnreadMessagesResponses, OperationV1ReadAnnouncementData, OperationV1ReadAnnouncementResponses, OperationV1ReadMessageData, OperationV1ReadMessageResponses, OperationV1RegisterDeviceData, OperationV1RegisterDeviceResponses, OperationV1SaveCommentData, OperationV1SaveCommentResponses, OperationV1ToggleDeviceActiveData, OperationV1ToggleDeviceActiveResponses, OperationV1UpdateInquiryData, OperationV1UpdateInquiryResponses, PartnerV1MemberInfosData, PartnerV1MemberInfosResponses, QuizV1DeactivateAttemptData, QuizV1DeactivateAttemptResponses, QuizV1GetSessionData, QuizV1GetSessionResponses, QuizV1StartAttemptData, QuizV1StartAttemptResponses, QuizV1SubmitAttemptData, QuizV1SubmitAttemptResponses, SsoV1AuthorizeData, SsoV1AuthorizeResponses, SsoV1CallbackData, SsoV1CallbackResponses, SsoV1DeleteAccountData, SsoV1DeleteAccountResponses, SsoV1GetAccountsData, SsoV1GetAccountsResponses, SsoV1LinkData, SsoV1LinkResponses, StudioV1AssessmentSuggestionsData, StudioV1AssessmentSuggestionsResponses, StudioV1ContentData, StudioV1ContentResponses, StudioV1ContentSuggestionsData, StudioV1ContentSuggestionsResponses, StudioV1CreateMediaQuizData, StudioV1CreateMediaQuizResponses, StudioV1DeleteAssignmentQuesionData, StudioV1DeleteAssignmentQuesionResponses, StudioV1DeleteDiscussionQuesionData, StudioV1DeleteDiscussionQuesionResponses, StudioV1DeleteExamQuesionData, StudioV1DeleteExamQuesionResponses, StudioV1DeleteMediaSubtitleData, StudioV1DeleteMediaSubtitleResponses, StudioV1DeleteQuizQuesionData, StudioV1DeleteQuizQuesionResponses, StudioV1DeleteSurveyQuesionData, StudioV1DeleteSurveyQuesionResponses, StudioV1GetAssignmentData, StudioV1GetAssignmentQuestionsData, StudioV1GetAssignmentQuestionsResponses, StudioV1GetAssignmentResponses, StudioV1GetCourseData, StudioV1GetCourseResponses, StudioV1GetDiscussionData, StudioV1GetDiscussionQuestionsData, StudioV1GetDiscussionQuestionsResponses, StudioV1GetDiscussionResponses, StudioV1GetExamData, StudioV1GetExamQuestionsData, StudioV1GetExamQuestionsResponses, StudioV1GetExamResponses, StudioV1GetMediaData, StudioV1GetMediaResponses, StudioV1GetQuizData, StudioV1GetQuizQuestionsData, StudioV1GetQuizQuestionsResponses, StudioV1GetQuizResponses, StudioV1GetSurveyData, StudioV1GetSurveyQuestionsData, StudioV1GetSurveyQuestionsResponses, StudioV1GetSurveyResponses, StudioV1InlineSuggestionsData, StudioV1InlineSuggestionsResponses, StudioV1RemoveCourseAssessmentData, StudioV1RemoveCourseAssessmentResponses, StudioV1RemoveCourseCategoryData, StudioV1RemoveCourseCategoryResponses, StudioV1RemoveCourseCertificateData, StudioV1RemoveCourseCertificateResponses, StudioV1RemoveCourseInstructorData, StudioV1RemoveCourseInstructorResponses, StudioV1RemoveCourseLessonData, StudioV1RemoveCourseLessonResponses, StudioV1RemoveCourseRelationData, StudioV1RemoveCourseRelationResponses, StudioV1RemoveCourseSurveyData, StudioV1RemoveCourseSurveyResponses, StudioV1SaveAssignmentData, StudioV1SaveAssignmentQuestionData, StudioV1SaveAssignmentQuestionResponses, StudioV1SaveAssignmentResponses, StudioV1SaveCourseAssessmentsData, StudioV1SaveCourseAssessmentsResponses, StudioV1SaveCourseCategoriesData, StudioV1SaveCourseCategoriesResponses, StudioV1SaveCourseCertificatesData, StudioV1SaveCourseCertificatesResponses, StudioV1SaveCourseData, StudioV1SaveCourseInstructorsData, StudioV1SaveCourseInstructorsResponses, StudioV1SaveCourseLessonsData, StudioV1SaveCourseLessonsResponses, StudioV1SaveCourseRelationsData, StudioV1SaveCourseRelationsResponses, StudioV1SaveCourseResponses, StudioV1SaveCourseSurveysData, StudioV1SaveCourseSurveysResponses, StudioV1SaveDiscussionData, StudioV1SaveDiscussionQuestionData, StudioV1SaveDiscussionQuestionResponses, StudioV1SaveDiscussionResponses, StudioV1SaveExamData, StudioV1SaveExamQuestionsData, StudioV1SaveExamQuestionsResponses, StudioV1SaveExamResponses, StudioV1SaveMediaData, StudioV1SaveMediaResponses, StudioV1SaveMediaSubtitleData, StudioV1SaveMediaSubtitleResponses, StudioV1SaveQuizData, StudioV1SaveQuizQuestionsData, StudioV1SaveQuizQuestionsResponses, StudioV1SaveQuizResponses, StudioV1SaveSurveyData, StudioV1SaveSurveyQuestionsData, StudioV1SaveSurveyQuestionsResponses, StudioV1SaveSurveyResponses, SurveyV1GetAnonymousSurveyData, SurveyV1GetAnonymousSurveyResponses, SurveyV1GetSurveyData, SurveyV1GetSurveyResponses, SurveyV1ResultsAnonymousData, SurveyV1ResultsAnonymousResponses, SurveyV1ResultsData, SurveyV1ResultsResponses, SurveyV1SubmitAnonymousData, SurveyV1SubmitAnonymousResponses, SurveyV1SubmitData, SurveyV1SubmitResponses } from './types.gen'; +import type { AccountV1ActivateData, AccountV1ActivateResponses, AccountV1ApplyEmailChangeData, AccountV1ApplyEmailChangeResponses, AccountV1ApplyPasswordChangeData, AccountV1ApplyPasswordChangeResponses, AccountV1CompleteOtpSetupData, AccountV1CompleteOtpSetupResponses, AccountV1GetMeData, AccountV1GetMeResponses, AccountV1JoinData, AccountV1JoinResponses, AccountV1LoginData, AccountV1LoginResponses, AccountV1LogoutData, AccountV1LogoutResponses, AccountV1RequestActivationData, AccountV1RequestActivationResponses, AccountV1RequestEmailChangeData, AccountV1RequestEmailChangeResponses, AccountV1RequestPasswordChangeData, AccountV1RequestPasswordChangeResponses, AccountV1ResetOtpData, AccountV1ResetOtpResponses, AccountV1SetupOtpData, AccountV1SetupOtpResponses, AccountV1UpdateMeData, AccountV1UpdateMeResponses, AccountV1UploadAvatarData, AccountV1UploadAvatarResponses, AccountV1VerifyOtpData, AccountV1VerifyOtpResponses, AssignmentV1DeactivateAttemptData, AssignmentV1DeactivateAttemptResponses, AssignmentV1GetSessionData, AssignmentV1GetSessionResponses, AssignmentV1StartAttemptData, AssignmentV1StartAttemptResponses, AssignmentV1SubmitAttemptData, AssignmentV1SubmitAttemptResponses, AssistantV1ChatMessageData, AssistantV1ChatMessageResponses, AssistantV1DeleteChatData, AssistantV1DeleteChatResponses, AssistantV1GetChatMessagesData, AssistantV1GetChatMessagesResponses, AssistantV1GetChatsData, AssistantV1GetChatsResponses, AssistantV1SaveAssistantNoteData, AssistantV1SaveAssistantNoteResponses, AssistantV1UpdateChatMessageData, AssistantV1UpdateChatMessageResponses, CompetencyV1DeleteCompetencyGoalData, CompetencyV1DeleteCompetencyGoalResponses, CompetencyV1GetCertificateAwardsData, CompetencyV1GetCertificateAwardsResponses, CompetencyV1GetCertificatesData, CompetencyV1GetCertificatesResponses, CompetencyV1GetClassificationSkillsDataData, CompetencyV1GetClassificationSkillsDataResponses, CompetencyV1GetClassificationTreeData, CompetencyV1GetClassificationTreeResponses, CompetencyV1GetCompetencyGoalsData, CompetencyV1GetCompetencyGoalsResponses, CompetencyV1SaveCompetencyGoalData, CompetencyV1SaveCompetencyGoalResponses, ContentV1DeleteMediaWatchData, ContentV1DeleteMediaWatchResponses, ContentV1GetMediaData, ContentV1GetMediaNoteData, ContentV1GetMediaNoteResponses, ContentV1GetMediaResponses, ContentV1GetMediaWatchData, ContentV1GetMediaWatchResponses, ContentV1GetSubtitlesData, ContentV1GetSubtitlesResponses, ContentV1GetWatchMediasData, ContentV1GetWatchMediasResponses, ContentV1SaveMediaNoteData, ContentV1SaveMediaNoteResponses, ContentV1SearchData, ContentV1SearchResponses, ContentV1SearchSuggestionData, ContentV1SearchSuggestionResponses, ContentV1UpdateMediaWatchData, ContentV1UpdateMediaWatchResponses, CourseV1GetDetailData, CourseV1GetDetailResponses, CourseV1GetSessionData, CourseV1GetSessionResponses, CourseV1RequestCertificateData, CourseV1RequestCertificateResponses, CourseV1StartEngagementData, CourseV1StartEngagementResponses, DiscussionV1CreatePostData, DiscussionV1CreatePostResponses, DiscussionV1DeactivateAttemptData, DiscussionV1DeactivateAttemptResponses, DiscussionV1DeletePostData, DiscussionV1DeletePostResponses, DiscussionV1GetOwnPostsData, DiscussionV1GetOwnPostsResponses, DiscussionV1GetPostsData, DiscussionV1GetPostsResponses, DiscussionV1GetSessionData, DiscussionV1GetSessionResponses, DiscussionV1StartAttemptData, DiscussionV1StartAttemptResponses, DiscussionV1UpdatePostData, DiscussionV1UpdatePostResponses, ExamV1DeactivateAttemptData, ExamV1DeactivateAttemptResponses, ExamV1GetSessionData, ExamV1GetSessionResponses, ExamV1GetTimestampData, ExamV1GetTimestampResponses, ExamV1SaveAnswersData, ExamV1SaveAnswersResponses, ExamV1StartAttemptData, ExamV1StartAttemptResponses, ExamV1SubmitAttemptData, ExamV1SubmitAttemptResponses, LearningV1EnrollCatalogItemData, LearningV1EnrollCatalogItemResponses, LearningV1GetCatalogItemsData, LearningV1GetCatalogItemsResponses, LearningV1GetCatalogsData, LearningV1GetCatalogsResponses, LearningV1GetEnrolledData, LearningV1GetEnrolledResponses, LearningV1GetRecordsData, LearningV1GetRecordsResponses, LearningV1GetReportData, LearningV1GetReportResponses, LearningV1UnenrollData, LearningV1UnenrollResponses, MinimaApiHealthData, MinimaApiHealthResponses, OperationV1AgreePoliciesData, OperationV1AgreePoliciesResponses, OperationV1CreateAppealData, OperationV1CreateAppealResponses, OperationV1CreateInquiryData, OperationV1CreateInquiryResponses, OperationV1CreateThreadData, OperationV1CreateThreadResponses, OperationV1DeleteCommentData, OperationV1DeleteCommentResponses, OperationV1DeleteDeviceData, OperationV1DeleteDeviceResponses, OperationV1EffectivePoliciesData, OperationV1EffectivePoliciesResponses, OperationV1GetAnnouncementsData, OperationV1GetAnnouncementsResponses, OperationV1GetCommentsData, OperationV1GetCommentsResponses, OperationV1GetDevicesData, OperationV1GetDevicesResponses, OperationV1GetInquiriesData, OperationV1GetInquiriesResponses, OperationV1GetThreadCommentsData, OperationV1GetThreadCommentsResponses, OperationV1GetThreadData, OperationV1GetThreadResponses, OperationV1GetUnreadMessagesData, OperationV1GetUnreadMessagesResponses, OperationV1ReadAnnouncementData, OperationV1ReadAnnouncementResponses, OperationV1ReadMessageData, OperationV1ReadMessageResponses, OperationV1RegisterDeviceData, OperationV1RegisterDeviceResponses, OperationV1SaveCommentData, OperationV1SaveCommentResponses, OperationV1ToggleDeviceActiveData, OperationV1ToggleDeviceActiveResponses, OperationV1UpdateInquiryData, OperationV1UpdateInquiryResponses, PartnerV1MemberInfosData, PartnerV1MemberInfosResponses, QuizV1DeactivateAttemptData, QuizV1DeactivateAttemptResponses, QuizV1GetSessionData, QuizV1GetSessionResponses, QuizV1StartAttemptData, QuizV1StartAttemptResponses, QuizV1SubmitAttemptData, QuizV1SubmitAttemptResponses, SsoV1AuthorizeData, SsoV1AuthorizeResponses, SsoV1CallbackData, SsoV1CallbackResponses, SsoV1DeleteAccountData, SsoV1DeleteAccountResponses, SsoV1GetAccountsData, SsoV1GetAccountsResponses, SsoV1LinkData, SsoV1LinkResponses, StudioV1AssessmentSuggestionsData, StudioV1AssessmentSuggestionsResponses, StudioV1ContentData, StudioV1ContentResponses, StudioV1ContentSuggestionsData, StudioV1ContentSuggestionsResponses, StudioV1CreateMediaQuizData, StudioV1CreateMediaQuizResponses, StudioV1DeleteAssignmentData, StudioV1DeleteAssignmentQuesionData, StudioV1DeleteAssignmentQuesionResponses, StudioV1DeleteAssignmentResponses, StudioV1DeleteCourseData, StudioV1DeleteCourseResponses, StudioV1DeleteDiscussionData, StudioV1DeleteDiscussionQuesionData, StudioV1DeleteDiscussionQuesionResponses, StudioV1DeleteDiscussionResponses, StudioV1DeleteExamData, StudioV1DeleteExamQuesionData, StudioV1DeleteExamQuesionResponses, StudioV1DeleteExamResponses, StudioV1DeleteMediaData, StudioV1DeleteMediaResponses, StudioV1DeleteMediaSubtitleData, StudioV1DeleteMediaSubtitleResponses, StudioV1DeleteQuizData, StudioV1DeleteQuizQuesionData, StudioV1DeleteQuizQuesionResponses, StudioV1DeleteQuizResponses, StudioV1DeleteSurveyData, StudioV1DeleteSurveyQuesionData, StudioV1DeleteSurveyQuesionResponses, StudioV1DeleteSurveyResponses, StudioV1GetAssignmentData, StudioV1GetAssignmentQuestionsData, StudioV1GetAssignmentQuestionsResponses, StudioV1GetAssignmentResponses, StudioV1GetAssignmentRubricData, StudioV1GetAssignmentRubricResponses, StudioV1GetCourseData, StudioV1GetCourseResponses, StudioV1GetDiscussionData, StudioV1GetDiscussionQuestionsData, StudioV1GetDiscussionQuestionsResponses, StudioV1GetDiscussionResponses, StudioV1GetExamData, StudioV1GetExamQuestionsData, StudioV1GetExamQuestionsResponses, StudioV1GetExamResponses, StudioV1GetMediaData, StudioV1GetMediaResponses, StudioV1GetQuizData, StudioV1GetQuizQuestionsData, StudioV1GetQuizQuestionsResponses, StudioV1GetQuizResponses, StudioV1GetSurveyData, StudioV1GetSurveyQuestionsData, StudioV1GetSurveyQuestionsResponses, StudioV1GetSurveyResponses, StudioV1InlineSuggestionsData, StudioV1InlineSuggestionsResponses, StudioV1RemoveCourseAssessmentData, StudioV1RemoveCourseAssessmentResponses, StudioV1RemoveCourseCategoryData, StudioV1RemoveCourseCategoryResponses, StudioV1RemoveCourseCertificateData, StudioV1RemoveCourseCertificateResponses, StudioV1RemoveCourseInstructorData, StudioV1RemoveCourseInstructorResponses, StudioV1RemoveCourseLessonData, StudioV1RemoveCourseLessonResponses, StudioV1RemoveCourseRelationData, StudioV1RemoveCourseRelationResponses, StudioV1RemoveCourseSurveyData, StudioV1RemoveCourseSurveyResponses, StudioV1SaveAssignmentData, StudioV1SaveAssignmentQuestionsData, StudioV1SaveAssignmentQuestionsResponses, StudioV1SaveAssignmentResponses, StudioV1SaveAssignmentRubricData, StudioV1SaveAssignmentRubricResponses, StudioV1SaveCourseAssessmentsData, StudioV1SaveCourseAssessmentsResponses, StudioV1SaveCourseCategoriesData, StudioV1SaveCourseCategoriesResponses, StudioV1SaveCourseCertificatesData, StudioV1SaveCourseCertificatesResponses, StudioV1SaveCourseData, StudioV1SaveCourseInstructorsData, StudioV1SaveCourseInstructorsResponses, StudioV1SaveCourseLessonsData, StudioV1SaveCourseLessonsResponses, StudioV1SaveCourseRelationsData, StudioV1SaveCourseRelationsResponses, StudioV1SaveCourseResponses, StudioV1SaveCourseSurveysData, StudioV1SaveCourseSurveysResponses, StudioV1SaveDiscussionData, StudioV1SaveDiscussionQuestionsData, StudioV1SaveDiscussionQuestionsResponses, StudioV1SaveDiscussionResponses, StudioV1SaveExamData, StudioV1SaveExamQuestionsData, StudioV1SaveExamQuestionsResponses, StudioV1SaveExamResponses, StudioV1SaveMediaData, StudioV1SaveMediaResponses, StudioV1SaveMediaSubtitleData, StudioV1SaveMediaSubtitleResponses, StudioV1SaveQuizData, StudioV1SaveQuizQuestionsData, StudioV1SaveQuizQuestionsResponses, StudioV1SaveQuizResponses, StudioV1SaveSurveyData, StudioV1SaveSurveyQuestionsData, StudioV1SaveSurveyQuestionsResponses, StudioV1SaveSurveyResponses, SurveyV1GetAnonymousSurveyData, SurveyV1GetAnonymousSurveyResponses, SurveyV1GetSurveyData, SurveyV1GetSurveyResponses, SurveyV1ResultsAnonymousData, SurveyV1ResultsAnonymousResponses, SurveyV1ResultsData, SurveyV1ResultsResponses, SurveyV1SubmitAnonymousData, SurveyV1SubmitAnonymousResponses, SurveyV1SubmitData, SurveyV1SubmitResponses } from './types.gen'; export type Options = Options2 & { /** @@ -1007,6 +1007,11 @@ export const studioV1InlineSuggestions = (o ...options }); +/** + * Delete Exam + */ +export const studioV1DeleteExam = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/studio/exam/{id}', ...options }); + /** * Get Exam */ @@ -1058,6 +1063,11 @@ export const studioV1SaveExamQuestions = (o */ export const studioV1DeleteExamQuesion = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/studio/exam/{id}/question/{question_id}', ...options }); +/** + * Delete Quiz + */ +export const studioV1DeleteQuiz = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/studio/quiz/{id}', ...options }); + /** * Get Quiz */ @@ -1109,6 +1119,11 @@ export const studioV1SaveQuizQuestions = (o */ export const studioV1DeleteQuizQuesion = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/studio/quiz/{id}/question/{question_id}', ...options }); +/** + * Delete Survey + */ +export const studioV1DeleteSurvey = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/studio/survey/{id}', ...options }); + /** * Get Survey */ @@ -1160,6 +1175,11 @@ export const studioV1SaveSurveyQuestions = */ export const studioV1DeleteSurveyQuesion = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/studio/survey/{id}/question/{question_id}', ...options }); +/** + * Delete Discussion + */ +export const studioV1DeleteDiscussion = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/studio/discussion/{id}', ...options }); + /** * Get Discussion */ @@ -1193,9 +1213,9 @@ export const studioV1GetDiscussionQuestions = (options: Options) => (options.client ?? client).post({ +export const studioV1SaveDiscussionQuestions = (options: Options) => (options.client ?? client).post({ ...formDataBodySerializer, responseType: 'json', url: '/api/v1/studio/discussion/{id}/question', @@ -1211,6 +1231,11 @@ export const studioV1SaveDiscussionQuestion = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/studio/discussion/{id}/question/{question_id}', ...options }); +/** + * Delete Assignment + */ +export const studioV1DeleteAssignment = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/studio/assignment/{id}', ...options }); + /** * Get Assignment */ @@ -1244,9 +1269,9 @@ export const studioV1GetAssignmentQuestions = (options: Options) => (options.client ?? client).post({ +export const studioV1SaveAssignmentQuestions = (options: Options) => (options.client ?? client).post({ ...formDataBodySerializer, responseType: 'json', url: '/api/v1/studio/assignment/{id}/question', @@ -1262,6 +1287,32 @@ export const studioV1SaveAssignmentQuestion = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/studio/assignment/{id}/question/{question_id}', ...options }); +/** + * Get Assignment Rubric + */ +export const studioV1GetAssignmentRubric = (options: Options) => (options.client ?? client).get({ + responseType: 'json', + url: '/api/v1/studio/assignment/{id}/rubric', + ...options +}); + +/** + * Save Assignment Rubric + */ +export const studioV1SaveAssignmentRubric = (options: Options) => (options.client ?? client).post({ + url: '/api/v1/studio/assignment/{id}/rubric', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Delete Media + */ +export const studioV1DeleteMedia = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/studio/media/{id}', ...options }); + /** * Get Media */ @@ -1311,6 +1362,11 @@ export const studioV1CreateMediaQuiz = (opt ...options }); +/** + * Delete Course + */ +export const studioV1DeleteCourse = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/studio/course/{id}', ...options }); + /** * Get Course */ diff --git a/web/src/api/types.gen.ts b/web/src/api/types.gen.ts index 50d6d93..fee35f1 100644 --- a/web/src/api/types.gen.ts +++ b/web/src/api/types.gen.ts @@ -479,15 +479,10 @@ export type AssignmentQuestionSchema = { * Attachmentfiletypes */ attachmentFileTypes: Array; - /** - * Sampleattachment - */ - sampleAttachment: string | null; /** * Plagiarismthreshold */ plagiarismThreshold: number; - solution: AssignmentSolutionSchema; }; /** @@ -552,6 +547,11 @@ export type AssignmentSchema = { id: string; owner: OwnerSchema; honorCode: HonorCodeSchema; + rubricData: RubricSchema | null; + /** + * Sampleattachment + */ + sampleAttachment: string | null; }; /** @@ -581,21 +581,6 @@ export type AssignmentSessionSchema = { otpToken?: string; }; -/** - * AssignmentSolutionSchema - */ -export type AssignmentSolutionSchema = { - /** - * Id - */ - id: number; - rubricData: RubricSchema; - /** - * Explanation - */ - explanation: string; -}; - /** * AssignmentSubmissionSchema */ @@ -5520,10 +5505,6 @@ export type AssignmentQuestionSpec = { * Plagiarismthreshold */ plagiarismThreshold: number; - /** - * Sampleattachment - */ - sampleAttachment: string | null; }; /** @@ -5607,7 +5588,33 @@ export type AssignmentSpec = { * Honorcodeid */ honorCodeId: number; + /** + * Rubriccriteria + */ + rubricCriteria: Array; questions: AssignmentQuestionsSpec; + /** + * Sampleattachment + */ + sampleAttachment: string | null; +}; + +/** + * RubricCriterionSpec + */ +export type RubricCriterionSpec = { + /** + * Name + */ + name: string; + /** + * Description + */ + description: string; + /** + * Performancelevels + */ + performanceLevels: Array; }; /** @@ -5694,6 +5701,21 @@ export type AssignmentQuestionSaveSpec = { plagiarismThreshold: number; }; +/** + * AssignmentQuestionsSaveSpec + */ +export type AssignmentQuestionsSaveSpec = { + /** + * Data + */ + data: Array; +}; + +/** + * RootModel[list[RubricCriterionSpec]] + */ +export type RootModelListRubricCriterionSpec = Array; + /** * MediaSpec */ @@ -8899,6 +8921,25 @@ export type StudioV1InlineSuggestionsResponses = { export type StudioV1InlineSuggestionsResponse = StudioV1InlineSuggestionsResponses[keyof StudioV1InlineSuggestionsResponses]; +export type StudioV1DeleteExamData = { + body?: never; + path: { + /** + * Id + */ + id: string; + }; + query?: never; + url: '/api/v1/studio/exam/{id}'; +}; + +export type StudioV1DeleteExamResponses = { + /** + * OK + */ + 200: unknown; +}; + export type StudioV1GetExamData = { body?: never; path: { @@ -9029,6 +9070,25 @@ export type StudioV1DeleteExamQuesionResponses = { 200: unknown; }; +export type StudioV1DeleteQuizData = { + body?: never; + path: { + /** + * Id + */ + id: string; + }; + query?: never; + url: '/api/v1/studio/quiz/{id}'; +}; + +export type StudioV1DeleteQuizResponses = { + /** + * OK + */ + 200: unknown; +}; + export type StudioV1GetQuizData = { body?: never; path: { @@ -9159,6 +9219,25 @@ export type StudioV1DeleteQuizQuesionResponses = { 200: unknown; }; +export type StudioV1DeleteSurveyData = { + body?: never; + path: { + /** + * Id + */ + id: string; + }; + query?: never; + url: '/api/v1/studio/survey/{id}'; +}; + +export type StudioV1DeleteSurveyResponses = { + /** + * OK + */ + 200: unknown; +}; + export type StudioV1GetSurveyData = { body?: never; path: { @@ -9289,6 +9368,25 @@ export type StudioV1DeleteSurveyQuesionResponses = { 200: unknown; }; +export type StudioV1DeleteDiscussionData = { + body?: never; + path: { + /** + * Id + */ + id: string; + }; + query?: never; + url: '/api/v1/studio/discussion/{id}'; +}; + +export type StudioV1DeleteDiscussionResponses = { + /** + * OK + */ + 200: unknown; +}; + export type StudioV1GetDiscussionData = { body?: never; path: { @@ -9362,7 +9460,7 @@ export type StudioV1GetDiscussionQuestionsResponses = { export type StudioV1GetDiscussionQuestionsResponse = StudioV1GetDiscussionQuestionsResponses[keyof StudioV1GetDiscussionQuestionsResponses]; -export type StudioV1SaveDiscussionQuestionData = { +export type StudioV1SaveDiscussionQuestionsData = { /** * MultiPartBodyParams */ @@ -9385,7 +9483,7 @@ export type StudioV1SaveDiscussionQuestionData = { url: '/api/v1/studio/discussion/{id}/question'; }; -export type StudioV1SaveDiscussionQuestionResponses = { +export type StudioV1SaveDiscussionQuestionsResponses = { /** * Response * @@ -9394,7 +9492,7 @@ export type StudioV1SaveDiscussionQuestionResponses = { 200: Array; }; -export type StudioV1SaveDiscussionQuestionResponse = StudioV1SaveDiscussionQuestionResponses[keyof StudioV1SaveDiscussionQuestionResponses]; +export type StudioV1SaveDiscussionQuestionsResponse = StudioV1SaveDiscussionQuestionsResponses[keyof StudioV1SaveDiscussionQuestionsResponses]; export type StudioV1DeleteDiscussionQuesionData = { body?: never; @@ -9419,6 +9517,25 @@ export type StudioV1DeleteDiscussionQuesionResponses = { 200: unknown; }; +export type StudioV1DeleteAssignmentData = { + body?: never; + path: { + /** + * Id + */ + id: string; + }; + query?: never; + url: '/api/v1/studio/assignment/{id}'; +}; + +export type StudioV1DeleteAssignmentResponses = { + /** + * OK + */ + 200: unknown; +}; + export type StudioV1GetAssignmentData = { body?: never; path: { @@ -9451,6 +9568,12 @@ export type StudioV1SaveAssignmentData = { * Max size: 3MB */ thumbnail?: Blob | File; + /** + * Sampleattachment + * + * Max size: 3MB + */ + sampleAttachment?: Blob | File; data: AssignmentSaveSpec; }; path?: never; @@ -9492,7 +9615,7 @@ export type StudioV1GetAssignmentQuestionsResponses = { export type StudioV1GetAssignmentQuestionsResponse = StudioV1GetAssignmentQuestionsResponses[keyof StudioV1GetAssignmentQuestionsResponses]; -export type StudioV1SaveAssignmentQuestionData = { +export type StudioV1SaveAssignmentQuestionsData = { /** * MultiPartBodyParams */ @@ -9503,13 +9626,7 @@ export type StudioV1SaveAssignmentQuestionData = { * Max size: 3MB */ files?: Array; - /** - * Sample - * - * Max size: 3MB - */ - sample?: Blob | File; - data: AssignmentQuestionSaveSpec; + data: AssignmentQuestionsSaveSpec; }; path: { /** @@ -9521,16 +9638,16 @@ export type StudioV1SaveAssignmentQuestionData = { url: '/api/v1/studio/assignment/{id}/question'; }; -export type StudioV1SaveAssignmentQuestionResponses = { +export type StudioV1SaveAssignmentQuestionsResponses = { /** * Response * * OK */ - 200: number; + 200: Array; }; -export type StudioV1SaveAssignmentQuestionResponse = StudioV1SaveAssignmentQuestionResponses[keyof StudioV1SaveAssignmentQuestionResponses]; +export type StudioV1SaveAssignmentQuestionsResponse = StudioV1SaveAssignmentQuestionsResponses[keyof StudioV1SaveAssignmentQuestionsResponses]; export type StudioV1DeleteAssignmentQuesionData = { body?: never; @@ -9555,6 +9672,67 @@ export type StudioV1DeleteAssignmentQuesionResponses = { 200: unknown; }; +export type StudioV1GetAssignmentRubricData = { + body?: never; + path: { + /** + * Id + */ + id: string; + }; + query?: never; + url: '/api/v1/studio/assignment/{id}/rubric'; +}; + +export type StudioV1GetAssignmentRubricResponses = { + /** + * Response + * + * OK + */ + 200: Array; +}; + +export type StudioV1GetAssignmentRubricResponse = StudioV1GetAssignmentRubricResponses[keyof StudioV1GetAssignmentRubricResponses]; + +export type StudioV1SaveAssignmentRubricData = { + body: RootModelListRubricCriterionSpec; + path: { + /** + * Id + */ + id: string; + }; + query?: never; + url: '/api/v1/studio/assignment/{id}/rubric'; +}; + +export type StudioV1SaveAssignmentRubricResponses = { + /** + * OK + */ + 200: unknown; +}; + +export type StudioV1DeleteMediaData = { + body?: never; + path: { + /** + * Id + */ + id: string; + }; + query?: never; + url: '/api/v1/studio/media/{id}'; +}; + +export type StudioV1DeleteMediaResponses = { + /** + * OK + */ + 200: unknown; +}; + export type StudioV1GetMediaData = { body?: never; path: { @@ -9674,6 +9852,25 @@ export type StudioV1CreateMediaQuizResponses = { export type StudioV1CreateMediaQuizResponse = StudioV1CreateMediaQuizResponses[keyof StudioV1CreateMediaQuizResponses]; +export type StudioV1DeleteCourseData = { + body?: never; + path: { + /** + * Id + */ + id: string; + }; + query?: never; + url: '/api/v1/studio/course/{id}'; +}; + +export type StudioV1DeleteCourseResponses = { + /** + * OK + */ + 200: unknown; +}; + export type StudioV1GetCourseData = { body?: never; path: { diff --git a/web/src/api/valibot.gen.ts b/web/src/api/valibot.gen.ts index 889621d..bf60246 100644 --- a/web/src/api/valibot.gen.ts +++ b/web/src/api/valibot.gen.ts @@ -194,6 +194,31 @@ export const vAssignmentGradeSchema = v.object({ id: v.pipe(v.number(), v.integer()) }); +/** + * AssignmentQuestionSchema + */ +export const vAssignmentQuestionSchema = v.object({ + id: v.pipe(v.number(), v.integer()), + question: v.string(), + supplement: v.string(), + attachmentFileCount: v.pipe(v.number(), v.integer()), + attachmentFileTypes: v.array(v.string()), + plagiarismThreshold: v.pipe(v.number(), v.integer()) +}); + +/** + * AssignmentAttemptSchema + */ +export const vAssignmentAttemptSchema = v.object({ + started: v.pipe(v.string(), v.isoTimestamp()), + active: v.boolean(), + context: v.string(), + mode: v.string(), + id: v.pipe(v.number(), v.integer()), + question: vAssignmentQuestionSchema, + retry: v.pipe(v.number(), v.integer()) +}); + /** * AssignmentSubmissionSchema */ @@ -240,28 +265,6 @@ export const vOwnerSchema = v.object({ nickname: v.string() }); -/** - * AssignmentSchema - */ -export const vAssignmentSchema = v.object({ - created: v.pipe(v.string(), v.isoTimestamp()), - modified: v.pipe(v.string(), v.isoTimestamp()), - title: v.string(), - description: v.string(), - audience: v.string(), - thumbnail: v.nullable(v.string()), - featured: v.boolean(), - format: v.string(), - durationSeconds: v.nullable(v.number()), - passingPoint: v.pipe(v.number(), v.integer()), - maxAttempts: v.pipe(v.number(), v.integer()), - verificationRequired: v.boolean(), - published: v.nullable(v.pipe(v.string(), v.isoTimestamp())), - id: v.string(), - owner: vOwnerSchema, - honorCode: vHonorCodeSchema -}); - /** * PerformanceLevelSchema */ @@ -291,39 +294,27 @@ export const vRubricSchema = v.object({ }); /** - * AssignmentSolutionSchema - */ -export const vAssignmentSolutionSchema = v.object({ - id: v.pipe(v.number(), v.integer()), - rubricData: vRubricSchema, - explanation: v.string() -}); - -/** - * AssignmentQuestionSchema - */ -export const vAssignmentQuestionSchema = v.object({ - id: v.pipe(v.number(), v.integer()), - question: v.string(), - supplement: v.string(), - attachmentFileCount: v.pipe(v.number(), v.integer()), - attachmentFileTypes: v.array(v.string()), - sampleAttachment: v.nullable(v.string()), - plagiarismThreshold: v.pipe(v.number(), v.integer()), - solution: vAssignmentSolutionSchema -}); - -/** - * AssignmentAttemptSchema + * AssignmentSchema */ -export const vAssignmentAttemptSchema = v.object({ - started: v.pipe(v.string(), v.isoTimestamp()), - active: v.boolean(), - context: v.string(), - mode: v.string(), - id: v.pipe(v.number(), v.integer()), - question: vAssignmentQuestionSchema, - retry: v.pipe(v.number(), v.integer()) +export const vAssignmentSchema = v.object({ + created: v.pipe(v.string(), v.isoTimestamp()), + modified: v.pipe(v.string(), v.isoTimestamp()), + title: v.string(), + description: v.string(), + audience: v.string(), + thumbnail: v.nullable(v.string()), + featured: v.boolean(), + format: v.string(), + durationSeconds: v.nullable(v.number()), + passingPoint: v.pipe(v.number(), v.integer()), + maxAttempts: v.pipe(v.number(), v.integer()), + verificationRequired: v.boolean(), + published: v.nullable(v.pipe(v.string(), v.isoTimestamp())), + id: v.string(), + owner: vOwnerSchema, + honorCode: vHonorCodeSchema, + rubricData: v.nullable(vRubricSchema), + sampleAttachment: v.nullable(v.string()) }); /** @@ -2409,8 +2400,7 @@ export const vAssignmentQuestionSpec = v.object({ supplement: v.string(), attachmentFileCount: v.pipe(v.number(), v.integer()), attachmentFileTypes: v.array(v.string()), - plagiarismThreshold: v.pipe(v.number(), v.integer()), - sampleAttachment: v.nullable(v.string()) + plagiarismThreshold: v.pipe(v.number(), v.integer()) }); /** @@ -2418,6 +2408,15 @@ export const vAssignmentQuestionSpec = v.object({ */ export const vAssignmentQuestionsSpec = v.array(vAssignmentQuestionSpec); +/** + * RubricCriterionSpec + */ +export const vRubricCriterionSpec = v.object({ + name: v.string(), + description: v.string(), + performanceLevels: v.array(vPerformanceLevelSchema) +}); + /** * AssignmentSpec */ @@ -2440,7 +2439,9 @@ export const vAssignmentSpec = v.object({ published: v.nullable(v.pipe(v.string(), v.isoTimestamp())), id: v.string(), honorCodeId: v.pipe(v.number(), v.integer()), - questions: vAssignmentQuestionsSpec + rubricCriteria: v.array(vRubricCriterionSpec), + questions: vAssignmentQuestionsSpec, + sampleAttachment: v.nullable(v.string()) }); /** @@ -2473,6 +2474,18 @@ export const vAssignmentQuestionSaveSpec = v.object({ plagiarismThreshold: v.pipe(v.number(), v.integer()) }); +/** + * AssignmentQuestionsSaveSpec + */ +export const vAssignmentQuestionsSaveSpec = v.object({ + data: v.array(vAssignmentQuestionSaveSpec) +}); + +/** + * RootModel[list[RubricCriterionSpec]] + */ +export const vRootModelListRubricCriterionSpec = v.array(vRubricCriterionSpec); + /** * SubtitleSpec */ @@ -4158,6 +4171,14 @@ export const vStudioV1InlineSuggestionsData = v.object({ */ export const vStudioV1InlineSuggestionsResponse = v.array(vInlineSuggestionSpec); +export const vStudioV1DeleteExamData = v.object({ + body: v.optional(v.never()), + path: v.object({ + id: v.string() + }), + query: v.optional(v.never()) +}); + export const vStudioV1GetExamData = v.object({ body: v.optional(v.never()), path: v.object({ @@ -4229,6 +4250,14 @@ export const vStudioV1DeleteExamQuesionData = v.object({ query: v.optional(v.never()) }); +export const vStudioV1DeleteQuizData = v.object({ + body: v.optional(v.never()), + path: v.object({ + id: v.string() + }), + query: v.optional(v.never()) +}); + export const vStudioV1GetQuizData = v.object({ body: v.optional(v.never()), path: v.object({ @@ -4300,6 +4329,14 @@ export const vStudioV1DeleteQuizQuesionData = v.object({ query: v.optional(v.never()) }); +export const vStudioV1DeleteSurveyData = v.object({ + body: v.optional(v.never()), + path: v.object({ + id: v.string() + }), + query: v.optional(v.never()) +}); + export const vStudioV1GetSurveyData = v.object({ body: v.optional(v.never()), path: v.object({ @@ -4371,6 +4408,14 @@ export const vStudioV1DeleteSurveyQuesionData = v.object({ query: v.optional(v.never()) }); +export const vStudioV1DeleteDiscussionData = v.object({ + body: v.optional(v.never()), + path: v.object({ + id: v.string() + }), + query: v.optional(v.never()) +}); + export const vStudioV1GetDiscussionData = v.object({ body: v.optional(v.never()), path: v.object({ @@ -4415,7 +4460,7 @@ export const vStudioV1GetDiscussionQuestionsData = v.object({ */ export const vStudioV1GetDiscussionQuestionsResponse = v.array(vDiscussionQuestionSpec); -export const vStudioV1SaveDiscussionQuestionData = v.object({ +export const vStudioV1SaveDiscussionQuestionsData = v.object({ body: v.object({ files: v.optional(v.array(v.string())), data: vDiscussionQuestionsSaveSpec @@ -4431,7 +4476,7 @@ export const vStudioV1SaveDiscussionQuestionData = v.object({ * * OK */ -export const vStudioV1SaveDiscussionQuestionResponse = v.array(v.pipe(v.number(), v.integer())); +export const vStudioV1SaveDiscussionQuestionsResponse = v.array(v.pipe(v.number(), v.integer())); export const vStudioV1DeleteDiscussionQuesionData = v.object({ body: v.optional(v.never()), @@ -4442,6 +4487,14 @@ export const vStudioV1DeleteDiscussionQuesionData = v.object({ query: v.optional(v.never()) }); +export const vStudioV1DeleteAssignmentData = v.object({ + body: v.optional(v.never()), + path: v.object({ + id: v.string() + }), + query: v.optional(v.never()) +}); + export const vStudioV1GetAssignmentData = v.object({ body: v.optional(v.never()), path: v.object({ @@ -4458,6 +4511,7 @@ export const vStudioV1GetAssignmentResponse = vAssignmentSpec; export const vStudioV1SaveAssignmentData = v.object({ body: v.object({ thumbnail: v.optional(v.string()), + sampleAttachment: v.optional(v.string()), data: vAssignmentSaveSpec }), path: v.optional(v.never()), @@ -4486,11 +4540,10 @@ export const vStudioV1GetAssignmentQuestionsData = v.object({ */ export const vStudioV1GetAssignmentQuestionsResponse = v.array(vAssignmentQuestionSpec); -export const vStudioV1SaveAssignmentQuestionData = v.object({ +export const vStudioV1SaveAssignmentQuestionsData = v.object({ body: v.object({ files: v.optional(v.array(v.string())), - sample: v.optional(v.string()), - data: vAssignmentQuestionSaveSpec + data: vAssignmentQuestionsSaveSpec }), path: v.object({ id: v.string() @@ -4503,7 +4556,7 @@ export const vStudioV1SaveAssignmentQuestionData = v.object({ * * OK */ -export const vStudioV1SaveAssignmentQuestionResponse = v.pipe(v.number(), v.integer()); +export const vStudioV1SaveAssignmentQuestionsResponse = v.array(v.pipe(v.number(), v.integer())); export const vStudioV1DeleteAssignmentQuesionData = v.object({ body: v.optional(v.never()), @@ -4514,6 +4567,37 @@ export const vStudioV1DeleteAssignmentQuesionData = v.object({ query: v.optional(v.never()) }); +export const vStudioV1GetAssignmentRubricData = v.object({ + body: v.optional(v.never()), + path: v.object({ + id: v.string() + }), + query: v.optional(v.never()) +}); + +/** + * Response + * + * OK + */ +export const vStudioV1GetAssignmentRubricResponse = v.array(vRubricCriterionSpec); + +export const vStudioV1SaveAssignmentRubricData = v.object({ + body: vRootModelListRubricCriterionSpec, + path: v.object({ + id: v.string() + }), + query: v.optional(v.never()) +}); + +export const vStudioV1DeleteMediaData = v.object({ + body: v.optional(v.never()), + path: v.object({ + id: v.string() + }), + query: v.optional(v.never()) +}); + export const vStudioV1GetMediaData = v.object({ body: v.optional(v.never()), path: v.object({ @@ -4576,6 +4660,14 @@ export const vStudioV1CreateMediaQuizData = v.object({ */ export const vStudioV1CreateMediaQuizResponse = v.string(); +export const vStudioV1DeleteCourseData = v.object({ + body: v.optional(v.never()), + path: v.object({ + id: v.string() + }), + query: v.optional(v.never()) +}); + export const vStudioV1GetCourseData = v.object({ body: v.optional(v.never()), path: v.object({ diff --git a/web/src/routes/(app)/assignment/-session/GradingReview.tsx b/web/src/routes/(app)/assignment/-session/GradingReview.tsx index fa5fd80..4694c73 100644 --- a/web/src/routes/(app)/assignment/-session/GradingReview.tsx +++ b/web/src/routes/(app)/assignment/-session/GradingReview.tsx @@ -25,8 +25,7 @@ export const GradingReview = () => { const assignment = s().assignment const grade = s().grade! - const solution = s().attempt!.question.solution - const rubricData = solution.rubricData + const rubricData = assignment.rubricData! const possiblePoint = grade.possiblePoint const passingPoint = assignment.passingPoint ?? 0 @@ -58,11 +57,6 @@ export const GradingReview = () => {
- -
{t('Explanation')}
-
{solution.explanation}
-
- { const [session, { setStore }] = useSession() const s = () => session.data! + const assignment = s().assignment const [formState, { Form, Field }] = createForm>({ initialValues: { answer: s().submission?.answer ?? '' }, @@ -31,7 +32,7 @@ export const Submission = () => { const { data } = await assignmentV1SubmitAttempt({ body: { ...values, files: files() }, - path: { id: s().assignment.id }, + path: { id: assignment.id }, }) setStore('data', 'submission', data) @@ -51,7 +52,7 @@ export const Submission = () => {
{t('Question')}
- {t('{{count}} point', { count: question.solution?.rubricData.possiblePoint })} + {t('{{count}} point', { count: assignment.rubricData!.possiblePoint })}
@@ -64,7 +65,7 @@ export const Submission = () => {
{t('Assessment Criteria')}
    - + {(criterion) => (
  • {criterion.name} @@ -101,9 +102,9 @@ export const Submission = () => { size: Math.floor(ASSIGNMENT_ATTACHMENT_MAX_SIZE / 1024 / 1024), })}
  • - +
  • - + {t('View sample file')}
  • diff --git a/web/src/routes/-SitePolicy.tsx b/web/src/routes/-SitePolicy.tsx index a70afef..c0b412d 100644 --- a/web/src/routes/-SitePolicy.tsx +++ b/web/src/routes/-SitePolicy.tsx @@ -107,7 +107,7 @@ export const SitePolicy = (props: SitePolicyProps) => {
    {policy.title} - {t('Optional')}}> + {t('(Optional)')}}> * diff --git a/web/src/routes/studio/-assignment/App.tsx b/web/src/routes/studio/-assignment/App.tsx index 63eefbf..222e0c5 100644 --- a/web/src/routes/studio/-assignment/App.tsx +++ b/web/src/routes/studio/-assignment/App.tsx @@ -1,13 +1,14 @@ import { Show } from 'solid-js' -import { type AssignmentSpec, studioV1GetAssignment } from '@/api' +import { type AssignmentSpec, studioV1DeleteAssignment, studioV1GetAssignment } from '@/api' import { LoadingOverlay } from '@/shared/LoadingOverlay' import { CollapseProvider } from '../-context/CollapseContext' import { EditingProvider, EMPTY_CONTENT_ID } from '../-context/editing' import { type ContentEntry, initEditing } from '../-studio/initEditing' -import { PublishBadge } from '../-studio/PublishBadge' +import { PublishStatus } from '../-studio/PublishStatus' import { Assignment } from './Assignment' import { EmptyAssignment } from './data' import { QuestionPool } from './QuestionPool' +import { Rubric } from './Rubric' const restorableRegistry: Record> = {} @@ -23,10 +24,11 @@ export const App = (props: { id: string }) => { return (
    }> - + + diff --git a/web/src/routes/studio/-assignment/Assignment.tsx b/web/src/routes/studio/-assignment/Assignment.tsx index 1fec9bf..7e66342 100644 --- a/web/src/routes/studio/-assignment/Assignment.tsx +++ b/web/src/routes/studio/-assignment/Assignment.tsx @@ -7,7 +7,7 @@ import { type AssignmentSpec, studioV1InlineSuggestions, studioV1SaveAssignment import { useTranslation } from '@/shared/solid/i18n' import { EMPTY_CONTENT_ID, useEditing } from '../-context/editing' import { DataAction } from '../-studio/DataAction' -import { BooleanField, DataBindField, NumberField, TextField, ThumbnailField } from '../-studio/field' +import { AttachmentField, BooleanField, DataBindField, NumberField, TextField, ThumbnailField } from '../-studio/field' import { Paper } from '../-studio/Paper' import { vAssignmentEditingSpec } from './data' @@ -24,12 +24,14 @@ export const Assignment = (props: Props) => { const schema = vAssignmentEditingSpec.entries const [thumbnail, setThumbnail] = createSignal() + const [sampleFile, setSampleFile] = createSignal() const saveAssignment = async (validated: v.InferOutput) => { const { data: id } = await studioV1SaveAssignment({ body: { data: { id: staging.id === EMPTY_CONTENT_ID ? undefined : staging.id, ...validated }, thumbnail: thumbnail(), + sampleAttachment: sampleFile(), }, }) setThumbnail(undefined) @@ -39,12 +41,22 @@ export const Assignment = (props: Props) => { if (staging.id === EMPTY_CONTENT_ID) { navigate({ to: `/studio/assignment/${id}`, replace: true }) } else { - modifyMutable(source, reconcile(structuredClone(unwrap({ ...staging, questions: source.questions })))) + modifyMutable( + source, + reconcile( + structuredClone(unwrap({ ...staging, questions: source.questions, rubricCriteria: source.rubricCriteria })), + ), + ) } } return ( - + {(status, actions) => (
    @@ -81,6 +93,15 @@ export const Assignment = (props: Props) => {
    + + +
    + [0]> path={['honorCodeId']} label={t('Honor code')} diff --git a/web/src/routes/studio/-assignment/Question.tsx b/web/src/routes/studio/-assignment/Question.tsx index 33340fb..7926739 100644 --- a/web/src/routes/studio/-assignment/Question.tsx +++ b/web/src/routes/studio/-assignment/Question.tsx @@ -1,11 +1,11 @@ import { batch, createSignal } from 'solid-js' import { unwrap } from 'solid-js/store' import * as v from 'valibot' -import { type AssignmentSpec, studioV1DeleteAssignmentQuesion, studioV1SaveAssignmentQuestion } from '@/api' +import { type AssignmentSpec, studioV1DeleteAssignmentQuesion, studioV1SaveAssignmentQuestions } from '@/api' import { useTranslation } from '@/shared/solid/i18n' import { useEditing } from '../-context/editing' import { DataAction } from '../-studio/DataAction' -import { AttachmentField, CommaSeparatedField, NumberField, RichTextField, TextField } from '../-studio/field' +import { CommaSeparatedField, NumberField, RichTextField, TextField } from '../-studio/field' import { Paper } from '../-studio/Paper' import { vAssignmentQuestionEditingSpec } from './data' @@ -21,16 +21,15 @@ export const Question = (props: Props) => { const question = () => staging.questions[props.index]! const [files, setFiles] = createSignal([]) - const [sampleFile, setSampleFile] = createSignal() const saveQuestion = async (validated: v.InferOutput) => { - const { data } = await studioV1SaveAssignmentQuestion({ + const { data } = await studioV1SaveAssignmentQuestions({ path: { id: staging.id }, - body: { data: validated, files: files(), sample: sampleFile() }, + body: { data: { data: [validated] }, files: files() }, }) batch(() => { - staging.questions[props.index]!.id = data + staging.questions[props.index]!.id = data[0]! source.questions[props.index] = structuredClone(unwrap(staging.questions[props.index]!)) }) } @@ -56,7 +55,7 @@ export const Question = (props: Props) => { return ( {(status, actions) => ( @@ -100,14 +99,8 @@ export const Question = (props: Props) => { />
    - -
    +
    diff --git a/web/src/routes/studio/-assignment/QuestionPool.tsx b/web/src/routes/studio/-assignment/QuestionPool.tsx index 8352750..0f36f38 100644 --- a/web/src/routes/studio/-assignment/QuestionPool.tsx +++ b/web/src/routes/studio/-assignment/QuestionPool.tsx @@ -1,20 +1,27 @@ -import { IconPlus } from '@tabler/icons-solidjs' +import { IconPlus, IconSearch } from '@tabler/icons-solidjs' import { createMemo, For, Show } from 'solid-js' import * as v from 'valibot' -import type { AssignmentSpec } from '@/api' +import { + type AssignmentSpec, + studioV1ContentSuggestions, + studioV1GetAssignmentQuestions, + studioV1SaveAssignmentQuestions, +} from '@/api' import { CollapseButton } from '@/shared/CollapseButton' import { useTranslation } from '@/shared/solid/i18n' import { useCollapse } from '../-context/CollapseContext' import { useEditing } from '../-context/editing' import { DataAction } from '../-studio/DataAction' import { scrollToLastPaper } from '../-studio/helper' +import { InlineSuggestion } from '../-studio/InlineSuggestion' +import { makeCopyQuestionPool, makeSaveQuestions } from '../-studio/questionPool' import { EmptyQuestion, vAssignmentQuestionEditingSpec } from './data' import { Question } from './Question' export const QuestionPool = () => { const { t } = useTranslation() - const { staging } = useEditing() + const { source, staging, fieldState } = useEditing() const questions = () => staging.questions @@ -23,6 +30,9 @@ export const QuestionPool = () => { scrollToLastPaper() } + const saveAllQuestions = makeSaveQuestions(staging, source, fieldState, studioV1SaveAssignmentQuestions) + const copyQuestionPool = makeCopyQuestionPool(staging, studioV1GetAssignmentQuestions) + const completePercentage = createMemo((): number => { return questions().length > 0 ? 100 : 0 }) @@ -61,16 +71,29 @@ export const QuestionPool = () => {
    + + [0]> + placeholder={t('Copy question pool')} + cacheKey="studioV1ContentSuggestions" + fetchParams={() => ({ query: { kind: 'assignment' } })} + fetchFn={async (options) => (await studioV1ContentSuggestions(options)).data} + excludeIds={() => [staging.id]} + onCommit={copyQuestionPool} + icon={} + inputClass="bg-transparent" + /> + +
    diff --git a/web/src/routes/studio/-assignment/Rubric.tsx b/web/src/routes/studio/-assignment/Rubric.tsx new file mode 100644 index 0000000..8dac097 --- /dev/null +++ b/web/src/routes/studio/-assignment/Rubric.tsx @@ -0,0 +1,187 @@ +import { IconMinus, IconPlus, IconSearch } from '@tabler/icons-solidjs' +import { For, Show } from 'solid-js' +import { unwrap } from 'solid-js/store' +import * as v from 'valibot' +import { + type AssignmentSpec, + type ContentSuggestionSpec, + studioV1ContentSuggestions, + studioV1GetAssignmentRubric, + studioV1SaveAssignmentRubric, +} from '@/api' +import { useTranslation } from '@/shared/solid/i18n' +import { useEditing } from '../-context/editing' +import { DataAction } from '../-studio/DataAction' +import { NumberField, TextField } from '../-studio/field' +import { InlineSuggestion } from '../-studio/InlineSuggestion' +import { Paper } from '../-studio/Paper' +import { EmptyPerformanceLevel, EmptyRubricCriterion, vRubricCriteriaEditingSpec } from './data' + +export const Rubric = () => { + const { t } = useTranslation() + + const { source, staging, fieldState } = useEditing() + + const saveCriteria = async (validated: v.InferOutput) => { + await studioV1SaveAssignmentRubric({ path: { id: staging.id }, body: validated }) + source.rubricCriteria = structuredClone(unwrap(staging.rubricCriteria)) + } + + const addCriterion = async () => { + staging.rubricCriteria.push(EmptyRubricCriterion()) + } + + const removeCriteria = async (criteriaIdx: number) => { + staging.rubricCriteria.splice(criteriaIdx, 1) + fieldState.rubricCriteria.splice(criteriaIdx, 1) + } + + const addPerformanceLevel = async (criteriaIdx: number) => { + const newLevel = EmptyPerformanceLevel() + newLevel.point = (staging.rubricCriteria[criteriaIdx]!.performanceLevels.at(-1)?.point ?? 0) + 1 + staging.rubricCriteria[criteriaIdx]!.performanceLevels.push(newLevel) + } + + const removePerformanceLevel = async (criteriaIdx: number, levelIdx: number) => { + staging.rubricCriteria[criteriaIdx]!.performanceLevels.splice(levelIdx, 1) + fieldState.rubricCriteria[criteriaIdx]!.performanceLevels.splice(levelIdx, 1) + } + + const copyCriteria = async (suggestion: ContentSuggestionSpec) => { + const { data } = await studioV1GetAssignmentRubric({ path: { id: suggestion.id } }) + staging.rubricCriteria = data + } + + return ( + + {(status, actions) => ( +
    +
    + + +
    + + {t('Rubric criteria')}
    }> +
    {t('Rubric criteria')}
    + + + {(_, i) => ( + <> + +
    + +
    +
    + + + + + +
    +
    + + {(_, j) => ( +
    + +
    + +
    + + +
    + +
    + + + +
    +
    + )} + + + +
    +
    + + )} + + + + +
    + +
    + [0]> + placeholder={t('Copy rubric criteria')} + cacheKey="studioV1ContentSuggestions" + fetchParams={() => ({ query: { kind: 'assignment' } })} + fetchFn={async (options) => (await studioV1ContentSuggestions(options)).data} + excludeIds={() => [staging.id]} + onCommit={copyCriteria} + icon={} + inputClass="bg-transparent" + /> + + + +
    + +
    + )} + + ) +} diff --git a/web/src/routes/studio/-assignment/data.ts b/web/src/routes/studio/-assignment/data.ts index 11d3d1d..764de60 100644 --- a/web/src/routes/studio/-assignment/data.ts +++ b/web/src/routes/studio/-assignment/data.ts @@ -1,5 +1,5 @@ import * as v from 'valibot' -import type { AssignmentQuestionSpec, AssignmentSpec } from '@/api' +import type { AssignmentQuestionSpec, AssignmentSpec, PerformanceLevelSchema, RubricCriterionSchema } from '@/api' import { lazyT } from '@/shared/solid/i18n' import { EMPTY_CONTENT_ID } from '../-context/editing' @@ -23,10 +23,28 @@ export const EmptyAssignment = (): AssignmentSpec => { confirmDueDays: -1, honorCodeId: -1, published: null, + sampleAttachment: '', + rubricCriteria: [EmptyRubricCriterion()], questions: [], } } +export const EmptyRubricCriterion = (): RubricCriterionSchema => { + return { + name: '', + description: '', + performanceLevels: [EmptyPerformanceLevel()], + } +} + +export const EmptyPerformanceLevel = (): PerformanceLevelSchema => { + return { + name: '', + description: '', + point: 1, + } +} + let questionSequence = 0 export const EmptyQuestion = (): AssignmentQuestionSpec => { @@ -36,7 +54,6 @@ export const EmptyQuestion = (): AssignmentQuestionSpec => { supplement: '', attachmentFileCount: -1, attachmentFileTypes: [], - sampleAttachment: '', plagiarismThreshold: -1, } } @@ -67,7 +84,30 @@ export const vAssignmentQuestionEditingSpec = v.pipe( supplement: v.string(), attachmentFileCount: v.pipe(v.number(), v.integer(), v.minValue(0, AT_LEAST_ZERO)), attachmentFileTypes: v.pipe(v.array(v.pipe(v.string(), v.nonEmpty(REQUIRED))), v.minLength(1, AT_LEAST_ONE)), - sampleAttachment: v.pipe(v.string(), v.nonEmpty(REQUIRED)), plagiarismThreshold: v.pipe(v.number(), v.integer(), v.minValue(0, AT_LEAST_ZERO), v.maxValue(100, AT_MOST_100)), }), ) + +export const vRubricCriteriaEditingSpec = v.pipe( + v.array( + v.object({ + name: v.pipe(v.string(), v.nonEmpty(REQUIRED)), + description: v.pipe(v.string(), v.nonEmpty(REQUIRED)), + performanceLevels: v.pipe( + v.array( + v.object({ + name: v.pipe(v.string(), v.nonEmpty(REQUIRED)), + description: v.pipe(v.string(), v.nonEmpty(REQUIRED)), + point: v.pipe(v.number(), v.integer(), v.minValue(1, AT_LEAST_ONE)), + }), + ), + v.check((levels) => new Set(levels.map((l) => l.point)).size === levels.length, lazyT('point must be unique')), + v.check((levels) => new Set(levels.map((l) => l.name)).size === levels.length, lazyT('name must be unique')), + ), + }), + ), + v.check( + (criteria) => new Set(criteria.map((c) => c.name)).size === criteria.length, + lazyT('criterion name must be unique'), + ), +) diff --git a/web/src/routes/studio/-course/App.tsx b/web/src/routes/studio/-course/App.tsx index 5e2185b..da6caed 100644 --- a/web/src/routes/studio/-course/App.tsx +++ b/web/src/routes/studio/-course/App.tsx @@ -1,9 +1,9 @@ import { Show } from 'solid-js' -import { type CourseSpec, studioV1GetCourse } from '@/api' +import { type CourseSpec, studioV1DeleteCourse, studioV1GetCourse } from '@/api' import { LoadingOverlay } from '@/shared/LoadingOverlay' import { EditingProvider, EMPTY_CONTENT_ID } from '../-context/editing' import { type ContentEntry, initEditing } from '../-studio/initEditing' -import { PublishBadge } from '../-studio/PublishBadge' +import { PublishStatus } from '../-studio/PublishStatus' import { Assessments } from './Assessments' import { Categories } from './Categories' import { Certificates } from './Certificates' @@ -28,8 +28,8 @@ export const App = (props: { id: string }) => { return (
    }> - + diff --git a/web/src/routes/studio/-discussion/App.tsx b/web/src/routes/studio/-discussion/App.tsx index cc3a054..b0063aa 100644 --- a/web/src/routes/studio/-discussion/App.tsx +++ b/web/src/routes/studio/-discussion/App.tsx @@ -1,10 +1,10 @@ import { Show } from 'solid-js' -import { type DiscussionSpec, studioV1GetDiscussion } from '@/api' +import { type DiscussionSpec, studioV1DeleteDiscussion, studioV1GetDiscussion } from '@/api' import { LoadingOverlay } from '@/shared/LoadingOverlay' import { CollapseProvider } from '../-context/CollapseContext' import { EditingProvider, EMPTY_CONTENT_ID } from '../-context/editing' import { type ContentEntry, initEditing } from '../-studio/initEditing' -import { PublishBadge } from '../-studio/PublishBadge' +import { PublishStatus } from '../-studio/PublishStatus' import { Discussion } from './Discussion' import { EmptyDiscussion } from './data' import { QuestionPool } from './QuestionPool' @@ -23,8 +23,8 @@ export const App = (props: { id: string }) => { return (
    }> - + diff --git a/web/src/routes/studio/-discussion/Question.tsx b/web/src/routes/studio/-discussion/Question.tsx index 0a3edfb..4d7ad28 100644 --- a/web/src/routes/studio/-discussion/Question.tsx +++ b/web/src/routes/studio/-discussion/Question.tsx @@ -1,7 +1,7 @@ import { batch, createSignal } from 'solid-js' import { unwrap } from 'solid-js/store' import type * as v from 'valibot' -import { type DiscussionSpec, studioV1DeleteDiscussionQuesion, studioV1SaveDiscussionQuestion } from '@/api' +import { type DiscussionSpec, studioV1DeleteDiscussionQuesion, studioV1SaveDiscussionQuestions } from '@/api' import { useTranslation } from '@/shared/solid/i18n' import { useEditing } from '../-context/editing' import { DataAction } from '../-studio/DataAction' @@ -23,7 +23,7 @@ export const Question = (props: Props) => { const [files, setFiles] = createSignal([]) const saveQuestion = async (validated: v.InferOutput) => { - const { data } = await studioV1SaveDiscussionQuestion({ + const { data } = await studioV1SaveDiscussionQuestions({ path: { id: staging.id }, body: { data: { data: [validated] }, files: files() }, }) diff --git a/web/src/routes/studio/-discussion/QuestionPool.tsx b/web/src/routes/studio/-discussion/QuestionPool.tsx index f4c3f3c..6bbd38a 100644 --- a/web/src/routes/studio/-discussion/QuestionPool.tsx +++ b/web/src/routes/studio/-discussion/QuestionPool.tsx @@ -1,20 +1,27 @@ -import { IconPlus } from '@tabler/icons-solidjs' +import { IconPlus, IconSearch } from '@tabler/icons-solidjs' import { createMemo, For, Show } from 'solid-js' import * as v from 'valibot' -import type { DiscussionSpec } from '@/api' +import { + type DiscussionSpec, + studioV1ContentSuggestions, + studioV1GetDiscussionQuestions, + studioV1SaveDiscussionQuestions, +} from '@/api' import { CollapseButton } from '@/shared/CollapseButton' import { useTranslation } from '@/shared/solid/i18n' import { useCollapse } from '../-context/CollapseContext' import { useEditing } from '../-context/editing' import { DataAction } from '../-studio/DataAction' import { scrollToLastPaper } from '../-studio/helper' +import { InlineSuggestion } from '../-studio/InlineSuggestion' +import { makeCopyQuestionPool, makeSaveQuestions } from '../-studio/questionPool' import { EmptyQuestion, vDiscussionQuestionEditingSpec } from './data' import { Question } from './Question' export const QuestionPool = () => { const { t } = useTranslation() - const { staging } = useEditing() + const { source, staging, fieldState } = useEditing() const questions = () => staging.questions @@ -23,6 +30,9 @@ export const QuestionPool = () => { scrollToLastPaper() } + const saveAllQuestions = makeSaveQuestions(staging, source, fieldState, studioV1SaveDiscussionQuestions) + const copyQuestionPool = makeCopyQuestionPool(staging, studioV1GetDiscussionQuestions) + const completePercentage = createMemo((): number => { return questions().length >= 1 ? 100 : 0 }) @@ -61,16 +71,29 @@ export const QuestionPool = () => {
    + + [0]> + placeholder={t('Copy question pool')} + cacheKey="studioV1ContentSuggestions" + fetchParams={() => ({ query: { kind: 'discussion' } })} + fetchFn={async (options) => (await studioV1ContentSuggestions(options)).data} + excludeIds={() => [staging.id]} + onCommit={copyQuestionPool} + icon={} + inputClass="bg-transparent" + /> + +
    diff --git a/web/src/routes/studio/-exam/App.tsx b/web/src/routes/studio/-exam/App.tsx index a077b5b..86f191a 100644 --- a/web/src/routes/studio/-exam/App.tsx +++ b/web/src/routes/studio/-exam/App.tsx @@ -1,10 +1,10 @@ import { Show } from 'solid-js' -import { type ExamSpec, studioV1GetExam } from '@/api' +import { type ExamSpec, studioV1DeleteExam, studioV1GetExam } from '@/api' import { LoadingOverlay } from '@/shared/LoadingOverlay' import { CollapseProvider } from '../-context/CollapseContext' import { EditingProvider, EMPTY_CONTENT_ID } from '../-context/editing' import { type ContentEntry, initEditing } from '../-studio/initEditing' -import { PublishBadge } from '../-studio/PublishBadge' +import { PublishStatus } from '../-studio/PublishStatus' import { EmptyExam } from './data' import { Exam } from './Exam' import { QuestionPool } from './QuestionPool' @@ -23,8 +23,8 @@ export const App = (props: { id: string }) => { return (
    }> - + diff --git a/web/src/routes/studio/-exam/Question.tsx b/web/src/routes/studio/-exam/Question.tsx index 4f7a485..5e1f9d1 100644 --- a/web/src/routes/studio/-exam/Question.tsx +++ b/web/src/routes/studio/-exam/Question.tsx @@ -93,7 +93,7 @@ export const Question = (props: Props) => { {(_, index) => ( 1 ? t('Optional') : ''}`} + label={`${t('Option {{num}}', { num: index() + 1 })} ${index() > 1 ? t('(Optional)') : ''}`} schema={index() < 2 ? v.pipe(v.string(), v.nonEmpty(t('required'))) : v.string()} multiline /> diff --git a/web/src/routes/studio/-exam/QuestionPool.tsx b/web/src/routes/studio/-exam/QuestionPool.tsx index eb40ca6..66e98fb 100644 --- a/web/src/routes/studio/-exam/QuestionPool.tsx +++ b/web/src/routes/studio/-exam/QuestionPool.tsx @@ -1,9 +1,7 @@ import { IconPlus, IconSearch } from '@tabler/icons-solidjs' -import { batch, createMemo, For, Show, Suspense } from 'solid-js' -import { unwrap } from 'solid-js/store' +import { createMemo, For, Show } from 'solid-js' import * as v from 'valibot' import { - type ContentSuggestionSpec, type ExamQuestionFormatChoices, type ExamSpec, studioV1ContentSuggestions, @@ -15,9 +13,9 @@ import { useTranslation } from '@/shared/solid/i18n' import { useCollapse } from '../-context/CollapseContext' import { useEditing } from '../-context/editing' import { DataAction } from '../-studio/DataAction' -import { collectBlobFiles } from '../-studio/field' -import { checkTree, getNestedState, scrollToLastPaper } from '../-studio/helper' +import { scrollToLastPaper } from '../-studio/helper' import { InlineSuggestion } from '../-studio/InlineSuggestion' +import { makeCopyQuestionPool, makeSaveQuestions } from '../-studio/questionPool' import { EmptyQuestion, questionFormats, vExamQuestionEditingSpec } from './data' import { Question } from './Question' @@ -34,42 +32,8 @@ export const QuestionPool = () => { scrollToLastPaper() } - const saveAllQuestions = async (d: v.InferOutput[]) => { - const changedIndices: number[] = [] - d.forEach((q, i) => { - if (!q.id) { - changedIndices.push(i) - return - } - const node = getNestedState(fieldState, ['questions', i]) - if (node && checkTree(node, new Set()).dirty) changedIndices.push(i) - }) - - if (changedIndices.length === 0) return - - const changedQuestions = changedIndices.map((i) => d[i]!) - - const supplements = changedIndices.map((i) => staging.questions[i]?.supplement ?? '').join('') - - const files = await collectBlobFiles(supplements) - - const { data: savedIds } = await studioV1SaveExamQuestions({ - path: { id: staging.id }, - body: { data: { data: changedQuestions }, files }, - }) - - batch(() => { - for (let i = 0; i < changedQuestions.length; i++) { - const oldId = changedQuestions[i]!.id - const newId = savedIds[i]! - const index = staging.questions.findIndex((q) => q.id === oldId) - if (index >= 0 && oldId !== newId) { - staging.questions[index]!.id = newId - } - } - source.questions = structuredClone(unwrap(staging.questions)) - }) - } + const saveAllQuestions = makeSaveQuestions(staging, source, fieldState, studioV1SaveExamQuestions) + const copyQuestionPool = makeCopyQuestionPool(staging, studioV1GetExamQuestions) const completePercentage = createMemo((): number => { const composition = questionPool()!.composition @@ -84,15 +48,6 @@ export const QuestionPool = () => { return Math.round((completedCount / selectionCount) * 100) }) - const copyQuestionPool = async (suggestion: ContentSuggestionSpec) => { - const { data } = await studioV1GetExamQuestions({ path: { id: suggestion.id } }) - const filteredQuestions = data - .filter((q) => staging.questions.findIndex((sq) => sq.question === q.question) < 0) - .map((q) => ({ ...q, id: 0 })) - if (filteredQuestions.length === 0) return - staging.questions.push(...filteredQuestions) - } - const collapseAll = useCollapse() return ( @@ -166,9 +121,7 @@ export const QuestionPool = () => { {(question, index) => ( - - - + )} diff --git a/web/src/routes/studio/-media/App.tsx b/web/src/routes/studio/-media/App.tsx index 8f6f020..d279d8e 100644 --- a/web/src/routes/studio/-media/App.tsx +++ b/web/src/routes/studio/-media/App.tsx @@ -1,9 +1,9 @@ import { Show } from 'solid-js' -import { type MediaSpec, studioV1GetMedia } from '@/api' +import { type MediaSpec, studioV1DeleteMedia, studioV1GetMedia } from '@/api' import { LoadingOverlay } from '@/shared/LoadingOverlay' import { EditingProvider, EMPTY_CONTENT_ID } from '../-context/editing' import { type ContentEntry, initEditing } from '../-studio/initEditing' -import { PublishBadge } from '../-studio/PublishBadge' +import { PublishStatus } from '../-studio/PublishStatus' import { EmptyMedia } from './data' import { Media } from './Media' import { SubtitleSet } from './SubtitleSet' @@ -22,8 +22,8 @@ export const App = (props: { id: string }) => { return (
    }> - + diff --git a/web/src/routes/studio/-quiz/App.tsx b/web/src/routes/studio/-quiz/App.tsx index aefa24e..3d5ebdf 100644 --- a/web/src/routes/studio/-quiz/App.tsx +++ b/web/src/routes/studio/-quiz/App.tsx @@ -1,10 +1,10 @@ import { Show } from 'solid-js' -import { type QuizSpec, studioV1GetQuiz } from '@/api' +import { type QuizSpec, studioV1DeleteQuiz, studioV1GetQuiz } from '@/api' import { LoadingOverlay } from '@/shared/LoadingOverlay' import { CollapseProvider } from '../-context/CollapseContext' import { EditingProvider, EMPTY_CONTENT_ID } from '../-context/editing' import { type ContentEntry, initEditing } from '../-studio/initEditing' -import { PublishBadge } from '../-studio/PublishBadge' +import { PublishStatus } from '../-studio/PublishStatus' import { EmptyQuiz } from './data' import { QuestionPool } from './QuestionPool' import { Quiz } from './Quiz' @@ -23,8 +23,8 @@ export const App = (props: { id: string }) => { return (
    }> - + diff --git a/web/src/routes/studio/-quiz/Question.tsx b/web/src/routes/studio/-quiz/Question.tsx index 50fb0e0..7117181 100644 --- a/web/src/routes/studio/-quiz/Question.tsx +++ b/web/src/routes/studio/-quiz/Question.tsx @@ -92,7 +92,7 @@ export const Question = (props: Props) => { {(_, index) => ( 1 ? t('Optional') : ''}`} + label={`${t('Option {{num}}', { num: index() + 1 })} ${index() > 1 ? t('(Optional)') : ''}`} schema={index() < 2 ? v.pipe(v.string(), v.nonEmpty(t('required'))) : v.string()} multiline /> diff --git a/web/src/routes/studio/-quiz/QuestionPool.tsx b/web/src/routes/studio/-quiz/QuestionPool.tsx index a1cdf0d..c906d3e 100644 --- a/web/src/routes/studio/-quiz/QuestionPool.tsx +++ b/web/src/routes/studio/-quiz/QuestionPool.tsx @@ -1,22 +1,15 @@ import { IconPlus, IconSearch } from '@tabler/icons-solidjs' -import { batch, createMemo, For, Show, Suspense } from 'solid-js' -import { unwrap } from 'solid-js/store' +import { createMemo, For, Show } from 'solid-js' import * as v from 'valibot' -import { - type ContentSuggestionSpec, - type QuizSpec, - studioV1ContentSuggestions, - studioV1GetQuizQuestions, - studioV1SaveQuizQuestions, -} from '@/api' +import { type QuizSpec, studioV1ContentSuggestions, studioV1GetQuizQuestions, studioV1SaveQuizQuestions } from '@/api' import { CollapseButton } from '@/shared/CollapseButton' import { useTranslation } from '@/shared/solid/i18n' import { useCollapse } from '../-context/CollapseContext' import { useEditing } from '../-context/editing' import { DataAction } from '../-studio/DataAction' -import { collectBlobFiles } from '../-studio/field' -import { checkTree, getNestedState, scrollToLastPaper } from '../-studio/helper' +import { scrollToLastPaper } from '../-studio/helper' import { InlineSuggestion } from '../-studio/InlineSuggestion' +import { makeCopyQuestionPool, makeSaveQuestions } from '../-studio/questionPool' import { EmptyQuestion, vQuizQuestionEditingSpec } from './data' import { Question } from './Question' @@ -32,42 +25,8 @@ export const QuestionPool = () => { scrollToLastPaper() } - const saveAllQuestions = async (d: v.InferOutput[]) => { - const changedIndices: number[] = [] - d.forEach((q, i) => { - if (!q.id) { - changedIndices.push(i) - return - } - const node = getNestedState(fieldState, ['questions', i]) - if (node && checkTree(node, new Set()).dirty) changedIndices.push(i) - }) - - if (changedIndices.length === 0) return - - const changedQuestions = changedIndices.map((i) => d[i]!) - - const supplements = changedIndices.map((i) => staging.questions[i]?.supplement ?? '').join('') - - const files = await collectBlobFiles(supplements) - - const { data: savedIds } = await studioV1SaveQuizQuestions({ - path: { id: staging.id }, - body: { data: { data: changedQuestions }, files }, - }) - - batch(() => { - for (let i = 0; i < changedQuestions.length; i++) { - const oldId = changedQuestions[i]!.id - const newId = savedIds[i]! - const index = staging.questions.findIndex((q) => q.id === oldId) - if (index >= 0 && oldId !== newId) { - staging.questions[index]!.id = newId - } - } - source.questions = structuredClone(unwrap(staging.questions)) - }) - } + const saveAllQuestions = makeSaveQuestions(staging, source, fieldState, studioV1SaveQuizQuestions) + const copyQuestionPool = makeCopyQuestionPool(staging, studioV1GetQuizQuestions) const completePercentage = createMemo((): number => { const selectCount = staging.questionPool.selectCount @@ -77,15 +36,6 @@ export const QuestionPool = () => { return Math.round((completedCount / selectCount) * 100) }) - const copyQuestionPool = async (suggestion: ContentSuggestionSpec) => { - const { data } = await studioV1GetQuizQuestions({ path: { id: suggestion.id } }) - const filteredQuestions = data - .filter((q) => staging.questions.findIndex((sq) => sq.question === q.question) < 0) - .map((q) => ({ ...q, id: 0 })) - if (filteredQuestions.length === 0) return - staging.questions.push(...filteredQuestions) - } - const collapseAll = useCollapse() return ( @@ -155,9 +105,7 @@ export const QuestionPool = () => { {(question, index) => ( - - - + )} diff --git a/web/src/routes/studio/-studio/DataAction.tsx b/web/src/routes/studio/-studio/DataAction.tsx index a830a92..66b50f4 100644 --- a/web/src/routes/studio/-studio/DataAction.tsx +++ b/web/src/routes/studio/-studio/DataAction.tsx @@ -8,7 +8,7 @@ import { useTranslation } from '@/shared/solid/i18n' import { showToast } from '@/shared/toast/store' import { forceDownload } from '@/shared/utils' import { type ContentType, useEditing } from '../-context/editing' -import { checkTree, getNestedState, getNestedValue, type Paths, setNestedState } from './helper' +import { checkArrayLengths, checkTree, getNestedState, getNestedValue, type Paths, setNestedState } from './helper' interface Props { rootKey?: Paths | [] @@ -47,7 +47,11 @@ export const DataAction = (props: const treeState = createMemo(() => { if (props.rootKey === undefined) return { error: false, dirty: false } const node = props.rootKey.length === 0 ? fieldState : getNestedState(fieldState, props.rootKey) - return checkTree(node, excludeSet()) + const sourceNode = props.rootKey.length === 0 ? source : getNestedValue(source, props.rootKey) + const stagingNode = props.rootKey.length === 0 ? staging : getNestedValue(staging, props.rootKey) + const tree = checkTree(node, excludeSet()) + const lengthDirty = checkArrayLengths(sourceNode, stagingNode, excludeSet(), node) + return { ...tree, dirty: tree.dirty || lengthDirty } }) const isDirty = () => treeState().dirty @@ -90,7 +94,7 @@ export const DataAction = (props: ), HasError: () => ( -
    +
    ), } diff --git a/web/src/routes/studio/-studio/Menu.tsx b/web/src/routes/studio/-studio/Menu.tsx index 37f8d05..779ad5d 100644 --- a/web/src/routes/studio/-studio/Menu.tsx +++ b/web/src/routes/studio/-studio/Menu.tsx @@ -42,8 +42,17 @@ export const Menu = (props: Props) => { return ( <>
    + { - const { t } = useTranslation() - - return ( - {t('Unpublished')}
    } - > -
    - {t('Published at {{date}}', { date: new Date(props.published!).toLocaleString() })} -
    - - ) -} diff --git a/web/src/routes/studio/-studio/PublishStatus.tsx b/web/src/routes/studio/-studio/PublishStatus.tsx new file mode 100644 index 0000000..60e5a32 --- /dev/null +++ b/web/src/routes/studio/-studio/PublishStatus.tsx @@ -0,0 +1,51 @@ +import { IconX } from '@tabler/icons-solidjs' +import { useNavigate, useParams } from '@tanstack/solid-router' +import { Show } from 'solid-js' +import { clearCachedInfiniteStoreBy } from '@/shared/solid/cached-infinite-store' +import { clearCachedStoreBy } from '@/shared/solid/cached-store' +import { useTranslation } from '@/shared/solid/i18n' +import { EMPTY_CONTENT_ID, useEditing } from '../-context/editing' + +interface Props { + class?: string + deleteFn?: (options: { path: { id: string } }) => Promise +} + +export const PublishStatus = (props: Props) => { + const { t } = useTranslation() + const params = useParams({ from: '/studio/$app/$id' }) + const navigate = useNavigate() + + const { source } = useEditing() + + const deleteContent = async () => { + if (!props.deleteFn) return + const { id, app } = params() + if (!id || !app) return + if (!confirm(`Are you sure you want to delete this content?`)) return + await props.deleteFn({ path: { id: params().id } }) + clearCachedStoreBy('studioV1Content') + clearCachedInfiniteStoreBy('studioV1Content') + navigate({ to: '/studio/$app/$id', params: { ...params(), id: EMPTY_CONTENT_ID }, replace: true }) + } + + return ( + + {t('Published at {{date}}', { date: new Date(source.published!).toLocaleString() })} +
    + } + > +
    +
    {t('Unpublished')}
    + +
    + {t('Delete')} +
    +
    +
    + + ) +} diff --git a/web/src/routes/studio/-studio/field.tsx b/web/src/routes/studio/-studio/field.tsx index 555f460..10db5c0 100644 --- a/web/src/routes/studio/-studio/field.tsx +++ b/web/src/routes/studio/-studio/field.tsx @@ -71,7 +71,7 @@ export const TextField = (props: TextFieldProps) => { const val = draft() const result = v.safeParse(props.schema, val) setNestedState(fieldState, props.path, { - dirty: val !== (getNestedValue(source, props.path) ?? ''), + dirty: val !== getNestedValue(source, props.path), error: result.success ? '' : result.issues[0].message, }) }) @@ -396,7 +396,10 @@ export const ThumbnailField = (props: ThumbnailFieldProps) => { onMount(() => { const url = value() if (!url?.startsWith('blob:')) return + + // no need to rstore filename if (!isDirty()) return + const filename = blobFilenameMap.get(url) ?? filenameFromUrl(url) fetch(url) .then((r) => r.blob()) @@ -522,13 +525,15 @@ export const AttachmentField = (props: AttachmentFieldProps) => { onMount(() => { const url = value() if (!url?.startsWith('blob:')) return - if (!isDirty()) return + const filename = blobFilenameMap.get(url) ?? filenameFromUrl(url) + setFilename(filename) + if (!isDirty()) return + fetch(url) .then((r) => r.blob()) .then((blob) => { props.onFileSelect(new File([blob], filename, { type: blob.type })) - setFilename(filename) }) }) diff --git a/web/src/routes/studio/-studio/helper.ts b/web/src/routes/studio/-studio/helper.ts index b095b75..1aeb2be 100644 --- a/web/src/routes/studio/-studio/helper.ts +++ b/web/src/routes/studio/-studio/helper.ts @@ -76,6 +76,25 @@ export const checkTree = (node: unknown, exclude: Set): { error: boolea return result } +export const checkArrayLengths = (src: unknown, stg: unknown, exclude?: Set, node?: unknown): boolean => { + if (exclude?.has(node)) return false + if (Array.isArray(src) && Array.isArray(stg)) { + if (src.length !== stg.length) return true + return src.some((_, i) => checkArrayLengths(src[i], stg[i], exclude, Array.isArray(node) ? node[i] : undefined)) + } + if (src && stg && typeof src === 'object' && typeof stg === 'object') { + return Object.keys(src).some((k) => + checkArrayLengths( + (src as Record)[k], + (stg as Record)[k], + exclude, + (node as Record | undefined)?.[k], + ), + ) + } + return false +} + export const scrollToLastPaper = () => { requestAnimationFrame(() => { const papers = document.querySelectorAll('[data-paper]') diff --git a/web/src/routes/studio/-studio/initEditing.ts b/web/src/routes/studio/-studio/initEditing.ts index 2a9a979..835cf5c 100644 --- a/web/src/routes/studio/-studio/initEditing.ts +++ b/web/src/routes/studio/-studio/initEditing.ts @@ -1,8 +1,8 @@ import { useNavigate, useParams } from '@tanstack/solid-router' import { batch, createEffect, onCleanup } from 'solid-js' import { createMutable, modifyMutable, reconcile, unwrap } from 'solid-js/store' -import { createCachedStore, initCachedStore } from '@/shared/solid/cached-store' -import { useTranslation } from '@/shared/solid/i18n' +import { clearCachedInfiniteStoreBy } from '@/shared/solid/cached-infinite-store' +import { clearCachedStoreBy, createCachedStore, initCachedStore } from '@/shared/solid/cached-store' import { type ContentType, EMPTY_CONTENT_ID, type FieldState } from '../-context/editing' export type ContentEntry = { @@ -19,7 +19,6 @@ interface Config { } export const initEditing = (config: Config) => { - const { t } = useTranslation() const { restorableRegistry, id } = config const params = useParams({ from: '/studio/$app/$id' }) const navigate = useNavigate() @@ -34,6 +33,12 @@ export const initEditing = (config: Config) => { async (options) => (await config.fetchFn(options)).data, ) + createEffect(() => { + if (rawSourceData.error?.status === 404) { + navigate({ to: '/studio/$app/$id', params: { ...params(), id: EMPTY_CONTENT_ID } }) + } + }) + // init edit staging const source = createMutable(config.emptyFactory()) const staging = createMutable(config.emptyFactory()) @@ -67,11 +72,10 @@ export const initEditing = (config: Config) => { }) const onSave = async (id: string) => { - // TODO - // const suggestion = { id, title: staging.title, modified: new Date().toISOString() } - // setSuggestions('data', (prev) => [suggestion, ...(prev?.filter((e) => e.id !== id) ?? [])]) - if (staging.id === EMPTY_CONTENT_ID) { + clearCachedStoreBy('studioV1Content') + clearCachedInfiniteStoreBy('studioV1Content') + batch(() => { initCachedStore(config.cacheKey, { path: { id } }, structuredClone(unwrap({ ...staging, id }))) modifyMutable(staging, reconcile(structuredClone(config.emptyFactory()))) @@ -79,21 +83,5 @@ export const initEditing = (config: Config) => { } } - const onCopy = async () => { - if (staging.id === EMPTY_CONTENT_ID) return - restorableRegistry[EMPTY_CONTENT_ID] = { - source: structuredClone(unwrap(config.emptyFactory())), - staging: structuredClone( - unwrap({ - ...staging, - id: EMPTY_CONTENT_ID, - title: t('Copy of {{title}}', { title: staging.title }), - thumbnail: undefined, - }), - ), - } - navigate({ to: '/studio/$app/$id', params: { ...params(), id: EMPTY_CONTENT_ID } }) - } - - return { source, staging, fieldState, onSave, onCopy, loading: () => rawSourceData.loading } + return { source, staging, fieldState, onSave, loading: () => rawSourceData.loading } } diff --git a/web/src/routes/studio/-studio/questionPool.ts b/web/src/routes/studio/-studio/questionPool.ts new file mode 100644 index 0000000..0b1d878 --- /dev/null +++ b/web/src/routes/studio/-studio/questionPool.ts @@ -0,0 +1,70 @@ +import { batch } from 'solid-js' +import { unwrap } from 'solid-js/store' +import type { ContentSuggestionSpec } from '@/api' +import { collectBlobFiles } from './field' +import { checkTree, getNestedState } from './helper' + +type SaveFn = (opts: { + path: { id: string } + body: { data: { data: Q[] }; files?: Array } +}) => Promise<{ data: number[] }> + +type GetFn = (opts: { path: { id: string } }) => Promise<{ data: Q[] }> + +export const makeSaveQuestions = + ( + staging: { id: string; questions: Q[] }, + source: { questions: Q[] }, + fieldState: object, + saveFn: SaveFn, + ) => + async (d: Q[]) => { + const changedIndices: number[] = [] + d.forEach((q, i) => { + if (!q.id) { + changedIndices.push(i) + return + } + const node = getNestedState(fieldState, ['questions', i]) + if (node && checkTree(node, new Set()).dirty) changedIndices.push(i) + }) + + if (changedIndices.length === 0) return + + const changedQuestions = changedIndices.map((i) => d[i]!) + const supplements = changedIndices.map((i) => staging.questions[i]?.supplement ?? '').join('') + const files = await collectBlobFiles(supplements) + + const { data: savedIds } = await saveFn({ + path: { id: staging.id }, + body: { data: { data: changedQuestions }, files }, + }) + + batch(() => { + for (let i = 0; i < changedQuestions.length; i++) { + const oldId = changedQuestions[i]!.id + const newId = savedIds[i]! + const index = staging.questions.findIndex((q) => q.id === oldId) + if (index >= 0 && oldId !== newId) { + staging.questions[index]!.id = newId + } + } + source.questions = structuredClone(unwrap(staging.questions)) + }) + } + +export const makeCopyQuestionPool = + (staging: { questions: Q[] }, getFn: GetFn) => + async (suggestion: ContentSuggestionSpec) => { + const { data } = await getFn({ path: { id: suggestion.id } }) + const filteredQuestions = data + .filter( + (q) => + staging.questions.findIndex( + (sq) => (sq.question && sq.question === q.question) || (sq.directive && sq.directive === q.directive), + ) < 0, + ) + .map((q) => ({ ...q, id: 0 })) + if (filteredQuestions.length === 0) return + staging.questions.push(...filteredQuestions) + } diff --git a/web/src/routes/studio/-survey/App.tsx b/web/src/routes/studio/-survey/App.tsx index 27c3d9e..9f784e1 100644 --- a/web/src/routes/studio/-survey/App.tsx +++ b/web/src/routes/studio/-survey/App.tsx @@ -1,10 +1,10 @@ import { Show } from 'solid-js' -import { type SurveySpec, studioV1GetSurvey } from '@/api' +import { type SurveySpec, studioV1DeleteSurvey, studioV1GetSurvey } from '@/api' import { LoadingOverlay } from '@/shared/LoadingOverlay' import { CollapseProvider } from '../-context/CollapseContext' import { EditingProvider, EMPTY_CONTENT_ID } from '../-context/editing' import { type ContentEntry, initEditing } from '../-studio/initEditing' -import { PublishBadge } from '../-studio/PublishBadge' +import { PublishStatus } from '../-studio/PublishStatus' import { EmptySurvey } from './data' import { QuestionPool } from './QuestionPool' import { Survey } from './Survey' @@ -23,8 +23,8 @@ export const App = (props: { id: string }) => { return (
    }> - + diff --git a/web/src/routes/studio/-survey/Question.tsx b/web/src/routes/studio/-survey/Question.tsx index 3989563..1fe5bf7 100644 --- a/web/src/routes/studio/-survey/Question.tsx +++ b/web/src/routes/studio/-survey/Question.tsx @@ -87,7 +87,7 @@ export const Question = (props: Props) => { {(_, index) => ( 1 ? t('Optional') : ''}`} + label={`${t('Option {{num}}', { num: index() + 1 })} ${index() > 1 ? t('(Optional)') : ''}`} schema={index() < 2 ? v.pipe(v.string(), v.nonEmpty(t('required'))) : v.string()} multiline /> diff --git a/web/src/routes/studio/-survey/QuestionPool.tsx b/web/src/routes/studio/-survey/QuestionPool.tsx index 6613033..6d79e94 100644 --- a/web/src/routes/studio/-survey/QuestionPool.tsx +++ b/web/src/routes/studio/-survey/QuestionPool.tsx @@ -1,9 +1,7 @@ import { IconPlus, IconSearch } from '@tabler/icons-solidjs' -import { batch, For, Show, Suspense } from 'solid-js' -import { unwrap } from 'solid-js/store' +import { For, Show } from 'solid-js' import * as v from 'valibot' import { - type ContentSuggestionSpec, type SurveyQuestionFormatChoices, type SurveySpec, studioV1ContentSuggestions, @@ -15,9 +13,9 @@ import { useTranslation } from '@/shared/solid/i18n' import { useCollapse } from '../-context/CollapseContext' import { useEditing } from '../-context/editing' import { DataAction } from '../-studio/DataAction' -import { collectBlobFiles } from '../-studio/field' -import { checkTree, getNestedState, scrollToLastPaper } from '../-studio/helper' +import { scrollToLastPaper } from '../-studio/helper' import { InlineSuggestion } from '../-studio/InlineSuggestion' +import { makeCopyQuestionPool, makeSaveQuestions } from '../-studio/questionPool' import { EmptyQuestion, questionFormats, vSurveyQuestionEditingSpec } from './data' import { Question } from './Question' @@ -33,51 +31,8 @@ export const QuestionPool = () => { scrollToLastPaper() } - const saveAllQuestions = async (d: v.InferOutput[]) => { - const changedIndices: number[] = [] - d.forEach((q, i) => { - if (!q.id) { - changedIndices.push(i) - return - } - const node = getNestedState(fieldState, ['questions', i]) - if (node && checkTree(node, new Set()).dirty) changedIndices.push(i) - }) - - if (changedIndices.length === 0) return - - const changedQuestions = changedIndices.map((i) => d[i]!) - - const supplements = changedIndices.map((i) => staging.questions[i]?.supplement ?? '').join('') - - const files = await collectBlobFiles(supplements) - - const { data: savedIds } = await studioV1SaveSurveyQuestions({ - path: { id: staging.id }, - body: { data: { data: changedQuestions }, files }, - }) - - batch(() => { - for (let i = 0; i < changedQuestions.length; i++) { - const oldId = changedQuestions[i]!.id - const newId = savedIds[i]! - const index = staging.questions.findIndex((q) => q.id === oldId) - if (index >= 0 && oldId !== newId) { - staging.questions[index]!.id = newId - } - } - source.questions = structuredClone(unwrap(staging.questions)) - }) - } - - const copyQuestionPool = async (suggestion: ContentSuggestionSpec) => { - const { data } = await studioV1GetSurveyQuestions({ path: { id: suggestion.id } }) - const filteredQuestions = data - .filter((q) => staging.questions.findIndex((sq) => sq.question === q.question) < 0) - .map((q) => ({ ...q, id: 0 })) - if (filteredQuestions.length === 0) return - staging.questions.push(...filteredQuestions) - } + const saveAllQuestions = makeSaveQuestions(staging, source, fieldState, studioV1SaveSurveyQuestions) + const copyQuestionPool = makeCopyQuestionPool(staging, studioV1GetSurveyQuestions) const collapseAll = useCollapse() @@ -142,9 +97,7 @@ export const QuestionPool = () => { {(question, index) => ( - - - + )} diff --git a/web/src/shared/solid/cached-infinite-store.ts b/web/src/shared/solid/cached-infinite-store.ts index 41786f4..3339300 100644 --- a/web/src/shared/solid/cached-infinite-store.ts +++ b/web/src/shared/solid/cached-infinite-store.ts @@ -62,6 +62,13 @@ export const initCachedInfiniteStore = ( }) } +export const clearCachedInfiniteStoreBy = (matcher: string | RegExp): void => { + for (const key of cache.keys()) { + const matches = typeof matcher === 'string' ? key.startsWith(matcher) : matcher.test(key) + if (matches) cache.delete(key) + } +} + export const createCachedInfiniteStore = ( prefix: string, getParams: () => P | undefined,