From f3b3e94c4fd73a1b5c434afeb4235c4c22092457 Mon Sep 17 00:00:00 2001 From: Matti Lupari Date: Tue, 20 Aug 2024 17:55:58 +0300 Subject: [PATCH] CSCEXAM-000 Split large components to smaller ones --- build.sbt | 2 +- .../active/active-enrolment.component.html | 88 +-- .../active/active-enrolment.component.ts | 6 +- .../helpers/optional-sections.component.ts | 105 +++ ...exam-publication-participants.component.ts | 87 +++ .../exam-publication.component.html | 65 +- .../publication/exam-publication.component.ts | 2 + .../sections/section-question.component.html | 4 +- .../sections/section-question.component.ts | 3 +- .../exam/editor/sections/section.component.ts | 18 +- .../basequestion/question-body.component.html | 610 +++++++----------- .../basequestion/question-body.component.ts | 15 +- .../basequestion/question.component.html | 9 +- .../base-question-editor.component.ts | 1 - .../examquestion/claim-choice.component.ts | 145 +++++ .../question/examquestion/essay.component.ts | 112 ++++ .../examquestion/exam-question.component.html | 502 ++------------ .../examquestion/exam-question.component.ts | 27 +- .../examquestion/multichoice.component.ts | 139 ++++ .../weighted-multichoice.component.ts | 174 +++++ .../question/question-basic-info.component.ts | 116 ++++ .../app/question/question-usage.component.ts | 112 ++++ ui/src/app/question/question.service.ts | 26 +- .../print/printed-multi-choice.component.ts | 2 +- .../multi-choice-question.component.ts | 2 +- ui/src/assets/i18n/en.json | 2 +- ui/src/assets/i18n/fi.json | 2 +- ui/src/assets/i18n/sv.json | 2 +- ui/src/styles.scss | 34 +- 29 files changed, 1334 insertions(+), 1078 deletions(-) create mode 100644 ui/src/app/enrolment/active/helpers/optional-sections.component.ts create mode 100644 ui/src/app/exam/editor/publication/exam-publication-participants.component.ts create mode 100644 ui/src/app/question/examquestion/claim-choice.component.ts create mode 100644 ui/src/app/question/examquestion/essay.component.ts create mode 100644 ui/src/app/question/examquestion/multichoice.component.ts create mode 100644 ui/src/app/question/examquestion/weighted-multichoice.component.ts create mode 100644 ui/src/app/question/question-basic-info.component.ts create mode 100644 ui/src/app/question/question-usage.component.ts diff --git a/build.sbt b/build.sbt index fb396d465..f54900b3f 100644 --- a/build.sbt +++ b/build.sbt @@ -6,7 +6,7 @@ name := "exam" version := "6.3.0" -licenses += "EUPL 1.1" -> url("https://joinup.ec.europa.eu/software/page/eupl/licence-eupl") +licenses += "EUPL 1.2" -> url("https://joinup.ec.europa.eu/software/page/eupl/licence-eupl") scalaVersion := "3.4.0" diff --git a/ui/src/app/enrolment/active/active-enrolment.component.html b/ui/src/app/enrolment/active/active-enrolment.component.html index 5a1cb6f09..f6394377c 100644 --- a/ui/src/app/enrolment/active/active-enrolment.component.html +++ b/ui/src/app/enrolment/active/active-enrolment.component.html @@ -267,93 +267,7 @@

} @if (reservation && enrolment().optionalSections.length > 0) { -
-
- - @if (reservation && enrolment().optionalSections.length > 0) { -
- @for (section of exam.examSections; track section) { -
- @if (section.optional === false) { -
-
- {{ 'i18n_exam_section' | translate }} - ({{ 'i18n_required' | translate | lowercase }}): - {{ section.name }} -
-
-
-
- {{ section.description }} -
-
- } -
- } - @for (section of enrolment().optionalSections; track section) { -
-
-
- {{ 'i18n_exam_section' | translate }} - ({{ 'i18n_optional' | translate | lowercase }}): - {{ section.name }} -
-
-
-
- {{ section.description }} -
-
- @if (section.examMaterials.length > 0) { -
-
-
- {{ 'i18n_exam_materials' | translate }} -
-
- @for (material of section.examMaterials; track material) { -
-
- - {{ 'i18n_name' | translate | uppercase }}: {{ material.name }} - @if (material.author) { - - {{ 'i18n_author' | translate | uppercase }}: - {{ material.author }} - - } - @if (material.isbn) { - ISBN: {{ material.isbn }} - } - -
-
- } -
- } -
- } -
- } -
-
+ }
diff --git a/ui/src/app/enrolment/active/active-enrolment.component.ts b/ui/src/app/enrolment/active/active-enrolment.component.ts index a7055229d..13fb91b39 100644 --- a/ui/src/app/enrolment/active/active-enrolment.component.ts +++ b/ui/src/app/enrolment/active/active-enrolment.component.ts @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: EUPL-1.2 -import { DatePipe, LowerCasePipe, SlicePipe, UpperCasePipe } from '@angular/common'; +import { DatePipe, SlicePipe, UpperCasePipe } from '@angular/common'; import { Component, input, output, signal } from '@angular/core'; import { RouterLink } from '@angular/router'; import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap'; @@ -16,6 +16,7 @@ import { MathJaxDirective } from 'src/app/shared/math/math-jax.directive'; import { CourseCodeComponent } from 'src/app/shared/miscellaneous/course-code.component'; import { TeacherListComponent } from 'src/app/shared/user/teacher-list.component'; import { ActiveEnrolmentMenuComponent } from './helpers/active-enrolment-menu.component'; +import { OptionalSectionsComponent } from './helpers/optional-sections.component'; @Component({ selector: 'xm-active-enrolment', @@ -26,10 +27,10 @@ import { ActiveEnrolmentMenuComponent } from './helpers/active-enrolment-menu.co ActiveEnrolmentMenuComponent, CourseCodeComponent, TeacherListComponent, + OptionalSectionsComponent, NgbCollapse, MathJaxDirective, UpperCasePipe, - LowerCasePipe, SlicePipe, DatePipe, TranslateModule, @@ -43,7 +44,6 @@ export class ActiveEnrolmentComponent { showGuide = signal(false); showInstructions = signal(false); - showMaterials = signal(false); constructor( private translate: TranslateService, diff --git a/ui/src/app/enrolment/active/helpers/optional-sections.component.ts b/ui/src/app/enrolment/active/helpers/optional-sections.component.ts new file mode 100644 index 000000000..d207aa967 --- /dev/null +++ b/ui/src/app/enrolment/active/helpers/optional-sections.component.ts @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: 2024 The members of the EXAM Consortium +// +// SPDX-License-Identifier: EUPL-1.2 + +import { LowerCasePipe, UpperCasePipe } from '@angular/common'; +import { Component, input, signal } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { ExamSection } from 'src/app/exam/exam.model'; + +@Component({ + selector: 'xm-optional-sections', + standalone: true, + imports: [TranslateModule, LowerCasePipe, UpperCasePipe], + template: `
+
+ + +
+ @for (section of allSections(); track section.id) { +
+ @if (section.optional === false) { +
+
+ {{ 'i18n_exam_section' | translate }} + ({{ 'i18n_required' | translate | lowercase }}): + {{ section.name }} +
+
+
+
+ {{ section.description }} +
+
+ } +
+ } + @for (section of selectedSections(); track section.id) { +
+
+
+ {{ 'i18n_exam_section' | translate }} + ({{ 'i18n_optional' | translate | lowercase }}): + {{ section.name }} +
+
+
+
+ {{ section.description }} +
+
+ @if (section.examMaterials.length > 0) { +
+
+
+ {{ 'i18n_exam_materials' | translate }} +
+
+ @for (material of section.examMaterials; track material.id) { +
+
+ + {{ 'i18n_name' | translate | uppercase }}: {{ material.name }} + @if (material.author) { + + {{ 'i18n_author' | translate | uppercase }}: + {{ material.author }} + + } + @if (material.isbn) { + ISBN: {{ material.isbn }} + } + +
+
+ } +
+ } +
+ } +
+
+
`, +}) +export class OptionalSectionsComponent { + allSections = input.required(); + selectedSections = input.required(); + showSections = signal(false); +} diff --git a/ui/src/app/exam/editor/publication/exam-publication-participants.component.ts b/ui/src/app/exam/editor/publication/exam-publication-participants.component.ts new file mode 100644 index 000000000..902379449 --- /dev/null +++ b/ui/src/app/exam/editor/publication/exam-publication-participants.component.ts @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2024 The members of the EXAM Consortium +// +// SPDX-License-Identifier: EUPL-1.2 + +import { Component, input, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { Exam } from 'src/app/exam/exam.model'; +import { CollaborativeExamOwnerSelectorComponent } from './collaborative-exam-owner-picker.component'; +import { ExamParticipantSelectorComponent } from './exam-participant-picker.component'; +import { ExamPreParticipantSelectorComponent } from './exam-pre-participant-picker.component'; +import { OrganisationSelectorComponent } from './organisation-picker.component'; + +@Component({ + standalone: true, + imports: [ + FormsModule, + NgbPopoverModule, + TranslateModule, + ExamParticipantSelectorComponent, + ExamPreParticipantSelectorComponent, + CollaborativeExamOwnerSelectorComponent, + OrganisationSelectorComponent, + ], + selector: 'xm-exam-publication-participants', + template: ` + @if (!collaborative()) { +
+
+ {{ 'i18n_exam_add_participants_title' | translate }} +
+
+ + +
+
+ } + + @if (visibleParticipantSelector() === 'participant') { + + } + + + @if (visibleParticipantSelector() === 'pre-participant') { + + } + + @if (collaborative()) { + + + } + `, +}) +export class ExamPublicationParticipantsComponent { + collaborative = input(false); + exam = input.required(); + visibleParticipantSelector = signal('participant'); +} diff --git a/ui/src/app/exam/editor/publication/exam-publication.component.html b/ui/src/app/exam/editor/publication/exam-publication.component.html index 221337ed9..057429926 100644 --- a/ui/src/app/exam/editor/publication/exam-publication.component.html +++ b/ui/src/app/exam/editor/publication/exam-publication.component.html @@ -232,65 +232,12 @@
- @if (!collaborative()) { -
-
- {{ 'i18n_exam_add_participants_title' | translate }} -
-
- - -
-
- } - - - @if ( - exam.executionType.type !== 'PUBLIC' && !collaborative() && visibleParticipantSelector() === 'participant' - ) { - - } - - - @if ( - exam.executionType.type !== 'PUBLIC' && - !collaborative() && - visibleParticipantSelector() === 'pre-participant' - ) { - - } - - @if (collaborative()) { - - + + @if (exam.executionType.type !== 'PUBLIC') { + } diff --git a/ui/src/app/exam/editor/publication/exam-publication.component.ts b/ui/src/app/exam/editor/publication/exam-publication.component.ts index c0fc0915e..379949682 100644 --- a/ui/src/app/exam/editor/publication/exam-publication.component.ts +++ b/ui/src/app/exam/editor/publication/exam-publication.component.ts @@ -34,6 +34,7 @@ import { CollaborativeExamOwnerSelectorComponent } from './collaborative-exam-ow import { CustomDurationPickerDialogComponent } from './custom-duration-picker-dialog.component'; import { ExamParticipantSelectorComponent } from './exam-participant-picker.component'; import { ExamPreParticipantSelectorComponent } from './exam-pre-participant-picker.component'; +import { ExamPublicationParticipantsComponent } from './exam-publication-participants.component'; import { OrganisationSelectorComponent } from './organisation-picker.component'; import { PublicationDialogComponent } from './publication-dialog.component'; import { PublicationErrorDialogComponent } from './publication-error-dialog.component'; @@ -48,6 +49,7 @@ import { PublicationRevocationDialogComponent } from './publication-revocation-d FormsModule, NgbPopover, NgClass, + ExamPublicationParticipantsComponent, ExamParticipantSelectorComponent, ExamPreParticipantSelectorComponent, CollaborativeExamOwnerSelectorComponent, diff --git a/ui/src/app/exam/editor/sections/section-question.component.html b/ui/src/app/exam/editor/sections/section-question.component.html index 718d52d3f..a8d252bb9 100644 --- a/ui/src/app/exam/editor/sections/section-question.component.html +++ b/ui/src/app/exam/editor/sections/section-question.component.html @@ -109,7 +109,7 @@ } @switch (sectionQuestion.question.type) { @case ('MultipleChoiceQuestion') { - @for (option of sectionQuestion.options; track option) { + @for (option of sectionQuestion.options; track option.id) {
@if (option.option.correctOption) { @@ -123,7 +123,7 @@ } } @case ('WeightedMultipleChoiceQuestion') { - @for (option of sectionQuestion.options; track option) { + @for (option of sectionQuestion.options; track option.id) {
@if (option.score >= 0) { diff --git a/ui/src/app/exam/editor/sections/section-question.component.ts b/ui/src/app/exam/editor/sections/section-question.component.ts index 5d3723f64..dcc489067 100644 --- a/ui/src/app/exam/editor/sections/section-question.component.ts +++ b/ui/src/app/exam/editor/sections/section-question.component.ts @@ -67,7 +67,7 @@ export class SectionQuestionComponent { private Files: FileService, ) {} - calculateWeightedMaxPoints = () => this.Question.calculateWeightedMaxPoints(this.sectionQuestion); + calculateWeightedMaxPoints = () => this.Question.calculateWeightedMaxPoints(this.sectionQuestion.options); getCorrectClaimChoiceOptionScore = () => this.Question.getCorrectClaimChoiceOptionScore(this.sectionQuestion); @@ -124,6 +124,7 @@ export class SectionQuestionComponent { const modal = this.modal.open(BaseQuestionEditorComponent, { backdrop: 'static', keyboard: true, + windowClass: 'xm-xxl-modal', size: 'xl', }); modal.componentInstance.lotteryOn = this.lotteryOn; diff --git a/ui/src/app/exam/editor/sections/section.component.ts b/ui/src/app/exam/editor/sections/section.component.ts index a5cd447af..926074205 100644 --- a/ui/src/app/exam/editor/sections/section.component.ts +++ b/ui/src/app/exam/editor/sections/section.component.ts @@ -25,9 +25,10 @@ import { } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { ToastrService } from 'ngx-toastr'; -import { noop } from 'rxjs'; +import { from, noop } from 'rxjs'; import type { ExamMaterial, ExamSection, ExamSectionQuestion, Question } from 'src/app/exam/exam.model'; import { ExamService } from 'src/app/exam/exam.service'; +import { BaseQuestionEditorComponent } from 'src/app/question/examquestion/base-question-editor.component'; import { QuestionSelectorComponent } from 'src/app/question/picker/question-picker.component'; import { QuestionService } from 'src/app/question/question.service'; import { ConfirmationDialogService } from 'src/app/shared/dialogs/confirmation-dialog.service'; @@ -325,8 +326,15 @@ export class SectionComponent { } }; - private openBaseQuestionEditor = () => - this.Question.openBaseQuestionEditor(true, this.collaborative).subscribe((resp) => - this.insertExamQuestion(resp, this.section.sectionQuestions.length), - ); + private openBaseQuestionEditor = () => { + const modal = this.modal.open(BaseQuestionEditorComponent, { + backdrop: 'static', + keyboard: false, + windowClass: 'question-editor-modal', + size: 'xl', + }); + modal.componentInstance.newQuestion = true; + modal.componentInstance.collaborative = this.collaborative; + from(modal.result).subscribe((resp) => this.insertExamQuestion(resp, this.section.sectionQuestions.length)); + }; } diff --git a/ui/src/app/question/basequestion/question-body.component.html b/ui/src/app/question/basequestion/question-body.component.html index e7e348135..450646a1e 100644 --- a/ui/src/app/question/basequestion/question-body.component.html +++ b/ui/src/app/question/basequestion/question-body.component.html @@ -12,234 +12,47 @@ border-color: transparent; } -
-
- -
-
- -
-
- @if (examNames.length > 1) { -
- {{ 'i18n_exam_question_edit_instructions' | translate }} -
- } - @if (examNames.length === 1) { -
- {{ 'i18n_exam_question_edit_instructions_for_one' | translate }} -
- } - @if (examNames.length < 1) { -
- {{ 'i18n_exam_question_edit_instructions_for_none' | translate }} -
- } - @if (examNames.length > 0) { -
-
    -
  • - - {{ 'i18n_exam_question_in_use' | translate }}: {{ examNames.length }} - -
  • - - @for (name of examNames.slice(0, 5); track name) { -
  • - - {{ name }} - -
  • - } - - @for (name of examNames.slice(5); track name) { -
  • - - {{ name }} - -
  • - } - - @if (examNames.length > 5 && hideRestExams) { -
  • - -
  • - } - - @if (examNames.length > 5) { -
  • - -
  • - } -
-
- } -
-
-
- - -
+
+ + +
- @if (showWarning()) { -
- {{ 'i18n_exam_basic_information_tab' | translate }} - - - {{ 'i18n_shared_question_property_info' | translate }} - -
- } @else { -
- {{ 'i18n_exam_basic_information_tab' | translate }} -
+ @if (question.type === 'EssayQuestion') { + +
{{ 'i18n_comments' | translate }}
+ } @else if ( + question.type === 'MultipleChoiceQuestion' || + question.type === 'WeightedMultipleChoiceQuestion' || + question.type === 'ClaimChoiceQuestion' + ) { + +
{{ 'i18n_question_options' | translate }}
}
- @if (question.id) { -
-
- {{ 'i18n_question_id' | translate }} -
-
#{{ question.id }}
-
- } -
-
- {{ 'i18n_new_question_type' | translate }} - - - -
-
- - @switch (question.type) { - @case ('EssayQuestion') { - {{ 'i18n_toolbar_essay_question' | translate }} - } - @case ('ClozeTestQuestion') { - {{ 'i18n_toolbar_cloze_test_question' | translate }} - } - @case ('MultipleChoiceQuestion') { - {{ 'i18n_toolbar_multiplechoice_question' | translate }} - } - @case ('WeightedMultipleChoiceQuestion') { - {{ 'i18n_toolbar_weighted_multiplechoice_question' | translate }} - } - @case ('ClaimChoiceQuestion') { - {{ 'i18n_toolbar_claim_choice_question' | translate }} - } - } -
-
- @if (question.type) { -
-
- {{ 'i18n_question_text' | translate }} - - - -
-
- - -
-
- -
-
- @if (question.type === 'EssayQuestion') { - -
{{ 'i18n_comments' | translate }}
- } @else if ( - question.type === 'MultipleChoiceQuestion' || - question.type === 'WeightedMultipleChoiceQuestion' || - question.type === 'ClaimChoiceQuestion' - ) { - -
{{ 'i18n_question_options' | translate }}
- } -
-
- - - @switch (question.type) { - @case ('EssayQuestion') { - - } - @case ('MultipleChoiceQuestion') { - - } - @case ('WeightedMultipleChoiceQuestion') { - - } - @case ('ClaimChoiceQuestion') { - - } - } - - @if (question.type === 'EssayQuestion') { + + @switch (question.type) { + @case ('EssayQuestion') { +
{{ 'i18n_evaluation_type' | translate }}
-
+
-
-
+ @case ('MultipleChoiceQuestion') { + + } + @case ('WeightedMultipleChoiceQuestion') { + + } + @case ('ClaimChoiceQuestion') { + } + } - + @if ( + question.type === 'MultipleChoiceQuestion' || + question.type === 'ClozeTestQuestion' || + question.defaultEvaluationType === 'Points' + ) { +
-
-
{{ 'i18n_additional_info' | translate }}
+
+ {{ 'i18n_max_score' | translate }} +
+
+
+ } - -
-
- {{ 'i18n_question_owners' | translate }} - - - -
-
- @if (isUserAllowedToModifyOwners()) { -
-
-
- - -
+ +
+
+
{{ 'i18n_additional_info' | translate }}
+
+
+ + +
+
+ {{ 'i18n_question_owners' | translate }} + + + +
+
+ @if (isUserAllowedToModifyOwners()) { +
+
+
+ +
-
-
- @for (user of currentOwners; track user.id) { - {{ user.firstName }} {{ user.lastName }} - - } -
+
+
+
+ @for (user of currentOwners; track user.id) { + {{ user.firstName }} {{ user.lastName }} + + }
- } @else { - @for (user of currentOwners; track user.id) { - {{ user.firstName }} {{ user.lastName }} - } +
+ } @else { + @for (user of currentOwners; track user.id) { + {{ user.firstName }} {{ user.lastName }} } -
+ }
+
- -
-
- {{ 'i18n_question_attachment' | translate }} - - - -
-
- @if (showWarning()) { -
- - {{ 'i18n_shared_question_property_info' | translate }} -
- } - - @if (question.attachment && !question.attachment?.removed) { -
- @if (hasUploadedAttachment()) { - - - {{ question.attachment?.fileName }} - - } @else { - - - {{ question.attachment?.fileName }} - {{ getFileSize() }} - - } - - {{ 'i18n_remove_attachment' | translate }} + +
+
+ {{ 'i18n_question_attachment' | translate }} + + + +
+
+ @if (showWarning()) { +
+ + {{ 'i18n_shared_question_property_info' | translate }} +
+ } + + @if (question.attachment && !question.attachment?.removed) { +
+ @if (hasUploadedAttachment()) { + + + {{ question.attachment?.fileName }} + + } @else { + + + {{ question.attachment?.fileName }} + {{ getFileSize() }} -
- } -
+ } + + {{ 'i18n_remove_attachment' | translate }} + +
+ }
+
+ + +
+
+ {{ 'i18n_question_instruction' | translate }} + + + +
+
+ +
+
- + @if (question.type === 'EssayQuestion') { +
- {{ 'i18n_question_instruction' | translate }} + {{ 'i18n_exam_evaluation_criteria' | translate }} @@ -405,59 +262,32 @@
+ } - @if (question.type === 'EssayQuestion') { - -
-
- {{ 'i18n_exam_evaluation_criteria' | translate }} - - - -
-
- -
-
- } - - @if (!collaborative) { - - - } + @if (!collaborative) { + + + } - @if (sectionNames.length > 0) { - -
-
- {{ 'i18n_added_to_sections' | translate }} -
-
- {{ sectionNames.join(', ') }} -
+ @if (sectionNames.length > 0) { + +
+
+ {{ 'i18n_added_to_sections' | translate }}
- } +
+ {{ sectionNames.join(', ') }} +
+
}
diff --git a/ui/src/app/question/basequestion/question-body.component.ts b/ui/src/app/question/basequestion/question-body.component.ts index d4fa57e5a..ca73f4415 100644 --- a/ui/src/app/question/basequestion/question-body.component.ts +++ b/ui/src/app/question/basequestion/question-body.component.ts @@ -12,6 +12,8 @@ import type { Observable } from 'rxjs'; import { from } from 'rxjs'; import { debounceTime, distinctUntilChanged, exhaustMap, map } from 'rxjs/operators'; import type { ExamSectionQuestion, ReverseQuestion, Tag } from 'src/app/exam/exam.model'; +import { QuestionBasicInfoComponent } from 'src/app/question/question-basic-info.component'; +import { QuestionUsageComponent } from 'src/app/question/question-usage.component'; import type { QuestionDraft } from 'src/app/question/question.service'; import { QuestionService } from 'src/app/question/question.service'; import { TagPickerComponent } from 'src/app/question/tags/tag-picker.component'; @@ -36,12 +38,13 @@ import { MultipleChoiceEditorComponent } from './multiple-choice.component'; EssayEditorComponent, MultipleChoiceEditorComponent, ClaimChoiceEditorComponent, + QuestionBasicInfoComponent, + QuestionUsageComponent, NgbTypeahead, TagPickerComponent, TranslateModule, ], styleUrls: ['../question.shared.scss'], - styles: '.initial-width { width: initial !important; }', }) export class QuestionBodyComponent implements OnInit { @Input() question!: ReverseQuestion | QuestionDraft; @@ -80,11 +83,12 @@ export class QuestionBodyComponent implements OnInit { this.init(); } - setQuestionType = () => { - this.question.type = this.Question.getQuestionType(this.newType); + setQuestionType = ($event: string) => { + this.question.type = this.Question.getQuestionType($event); this.init(); this.cdr.detectChanges(); }; + setText = ($event: string) => (this.question.question = $event); showWarning = () => this.examNames.length > 1; @@ -168,8 +172,9 @@ export class QuestionBodyComponent implements OnInit { return a && (a.id || a.externalId); }; - updateEvaluationType = () => { - if (this.question.defaultEvaluationType === 'Selection') { + updateEvaluationType = ($event: string) => { + this.question.defaultEvaluationType = $event; + if ($event === 'Selection') { delete this.question.defaultMaxScore; } }; diff --git a/ui/src/app/question/basequestion/question.component.html b/ui/src/app/question/basequestion/question.component.html index 557989d56..84ad92d44 100644 --- a/ui/src/app/question/basequestion/question.component.html +++ b/ui/src/app/question/basequestion/question.component.html @@ -11,13 +11,15 @@ } - go back + + go back +
- +
-
+ @if (question) { +
+
+

{{ 'i18n_claim_choice_question_instruction' | translate }}

+

{{ 'i18n_claim_choice_options_description' | translate }}

+
    +
  • {{ 'i18n_claim_choice_correct_points_description' | translate }}
  • +
  • {{ 'i18n_claim_choice_incorrect_points_description' | translate }}
  • +
  • {{ 'i18n_claim_choice_skip_option_description' | translate }}
  • +
+
+
+
+
+
+ {{ 'i18n_question_options' | translate | uppercase }} +
+
+ + {{ 'i18n_word_points' | translate | uppercase }} + +
+
+
+
+
+ @for (opt of options(); track opt.id; let index = $index) { +
+ @if (opt.option) { +
+ +
+ } +
+ +
+
+ {{ translateDescription(opt) }} +
+
+ } +
+
+
+
+ @if (missingOptions().length > 0) { +
+ + + {{ 'i18n_claim_choice_missing_options_warning' | translate }} + @for (opt of missingOptions(); track $index; let l = $last) { + {{ opt | translate }}{{ l ? '' : ',' }} + } + +
+ } +
+
+
+ `, +}) +export class ClaimChoiceComponent { + options = input([]); + lotteryOn = input(false); + optionsChanged = output(); + + missingOptions = computed(() => + this.QuestionService.getInvalidDistributedClaimOptionTypes(this.options()) + .filter((type) => type !== 'SkipOption') + .map((optionType) => this.QuestionService.getOptionTypeTranslation(optionType)), + ); + + constructor(private QuestionService: QuestionService) {} + + updateText = (text: string, index: number) => { + const newOption = { ...this.options()[index].option, option: text }; + const next = this.options(); + next[index].option = newOption; + this.optionsChanged.emit(next); + }; + + updateScore = (score: number, index: number) => { + const newOption = { ...this.options()[index], score: score }; + const next = this.options(); + next[index] = newOption; + this.optionsChanged.emit(next); + }; + + determineOptionType = (option: ExamSectionQuestionOption) => + this.QuestionService.determineClaimOptionTypeForExamQuestionOption(option); + + translateDescription = (option: ExamSectionQuestionOption) => { + const optionType = this.determineOptionType(option); + if (!optionType) { + return ''; + } + return this.QuestionService.determineOptionDescriptionTranslation(optionType); + }; + + getOptionClass = (option: ExamSectionQuestionOption) => { + const optionType = this.determineOptionType(option); + if (!optionType) { + return ''; + } + return this.QuestionService.determineClaimChoiceOptionClass(optionType); + }; +} diff --git a/ui/src/app/question/examquestion/essay.component.ts b/ui/src/app/question/examquestion/essay.component.ts new file mode 100644 index 000000000..bc98d40b7 --- /dev/null +++ b/ui/src/app/question/examquestion/essay.component.ts @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: 2024 The members of the EXAM Consortium +// +// SPDX-License-Identifier: EUPL-1.2 + +import { Component, input, output } from '@angular/core'; +import { ControlContainer, FormsModule, NgForm } from '@angular/forms'; +import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; + +@Component({ + selector: 'xm-eq-essay', + viewProviders: [{ provide: ControlContainer, useExisting: NgForm }], + standalone: true, + imports: [FormsModule, NgbPopoverModule, TranslateModule], + styleUrls: ['../question.shared.scss'], + template: ` +
+
+
+
{{ 'i18n_comments' | translate }}
+
+
+
+
+ {{ 'i18n_essay_length_recommendation' | translate }} +
+ +
+ + + {{ 'i18n_approximately' | translate }} {{ estimateCharacters() }} + {{ 'i18n_characters' | translate }} + +
+ @if (wordCount.invalid) { +
+ + {{ 'i18n_essay_length_recommendation_bounds' | translate }} +
+ } +
+
+
+
+ {{ 'i18n_evaluation_type' | translate }} +
+
+ +
+
+
+ +
+
+ {{ 'i18n_exam_evaluation_criteria' | translate }} + + + +
+
+ +
+
+ `, +}) +export class EssayComponent { + evaluationType = input(); + expectedWordCount = input(); + evaluationCriteria = input(); + + evaluationTypeChanged = output(); + expectedWordCountChanged = output(); + evaluationCriteriaChanged = output(); + + updateEvaluationType = (type: string) => this.evaluationTypeChanged.emit(type); + updateWordCount = (count: number) => this.expectedWordCountChanged.emit(count); + updateEvaluationCriteria = (criteria: string) => this.evaluationCriteriaChanged.emit(criteria); + estimateCharacters = () => (this.expectedWordCount() || 0) * 8; +} diff --git a/ui/src/app/question/examquestion/exam-question.component.html b/ui/src/app/question/examquestion/exam-question.component.html index 22741955a..013770382 100644 --- a/ui/src/app/question/examquestion/exam-question.component.html +++ b/ui/src/app/question/examquestion/exam-question.component.html @@ -9,214 +9,29 @@
- - @if (question?.type === 'EssayQuestion') { -
-
- {{ 'i18n_exam_evaluation_criteria' | translate }} - - - -
-
- -
-
- } -
@@ -687,7 +257,7 @@
+ + } + +
+ } +
+
+
+ + +
+
+ @if (showWarning()) { +
+ {{ 'i18n_exam_basic_information_tab' | translate }} + + + {{ 'i18n_shared_question_property_info' | translate }} + +
+ } @else { +
+ {{ 'i18n_exam_basic_information_tab' | translate }} +
+ } +
+
+ `, +}) +export class QuestionUsageComponent { + examNames = input.required(); + showWarning = input(false); + limitNames = signal(false); +} diff --git a/ui/src/app/question/question.service.ts b/ui/src/app/question/question.service.ts index 5e66aae91..e76add77d 100644 --- a/ui/src/app/question/question.service.ts +++ b/ui/src/app/question/question.service.ts @@ -4,11 +4,9 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { TranslateService } from '@ngx-translate/core'; import { ToastrService } from 'ngx-toastr'; import type { Observable } from 'rxjs'; -import { from } from 'rxjs'; import { map } from 'rxjs/operators'; import type { Exam, @@ -23,7 +21,6 @@ import { SessionService } from 'src/app/session/session.service'; import { AttachmentService } from 'src/app/shared/attachment/attachment.service'; import { FileService } from 'src/app/shared/file/file.service'; import { isNumber } from 'src/app/shared/miscellaneous/helpers'; -import { BaseQuestionEditorComponent } from './examquestion/base-question-editor.component'; export type QuestionDraft = Omit & { id: undefined }; export type QuestionAmounts = { @@ -37,7 +34,6 @@ export class QuestionService { constructor( private http: HttpClient, private translate: TranslateService, - private modal: NgbModal, private toast: ToastrService, private Session: SessionService, private Files: FileService, @@ -209,8 +205,8 @@ export class QuestionService { } }; - calculateWeightedMaxPoints = (sectionQuestion: ExamSectionQuestion): number => { - const points = sectionQuestion.options.filter((o) => o.score > 0).reduce((a, b) => a + b.score, 0); + calculateWeightedMaxPoints = (options: ExamSectionQuestionOption[]): number => { + const points = options.filter((o) => o.score > 0).reduce((a, b) => a + b.score, 0); return parseFloat(points.toFixed(2)); }; @@ -229,7 +225,7 @@ export class QuestionService { return question.maxScore; } if (type === 'WeightedMultipleChoiceQuestion') { - return this.calculateWeightedMaxPoints(question); + return this.calculateWeightedMaxPoints(question.options); } if (type === 'ClaimChoiceQuestion') { return this.getCorrectClaimChoiceOptionScore(question); @@ -392,7 +388,7 @@ export class QuestionService { } }; - determineOptionDescriptionTranslation = (optionType: string) => { + determineOptionDescriptionTranslation = (optionType: string): string => { switch (optionType) { case 'CorrectOption': return this.translate.instant('i18n_claim_choice_correct_option_description'); @@ -417,7 +413,7 @@ export class QuestionService { return 'CorrectOption'; } - return null; + return ''; }; getInvalidDistributedClaimOptionTypes = (options: ExamSectionQuestionOption[]) => { @@ -462,18 +458,6 @@ export class QuestionService { return this.http.post(this.questionOwnerApi(uid), data); }; - openBaseQuestionEditor = (newQuestion: boolean, collaborative: boolean): Observable => { - const modal = this.modal.open(BaseQuestionEditorComponent, { - backdrop: 'static', - keyboard: false, - windowClass: 'question-editor-modal', - size: 'xl', - }); - modal.componentInstance.newQuestion = newQuestion; - modal.componentInstance.collaborative = collaborative; - return from(modal.result); - }; - private questionsApi = (id?: number) => (!id ? '/app/questions' : `/app/questions/${id}`); private questionOwnerApi = (id?: number) => (!id ? '/app/questions/owner' : `/app/questions/owner/${id}`); diff --git a/ui/src/app/review/assessment/print/printed-multi-choice.component.ts b/ui/src/app/review/assessment/print/printed-multi-choice.component.ts index 3b4815e9c..d367ace51 100644 --- a/ui/src/app/review/assessment/print/printed-multi-choice.component.ts +++ b/ui/src/app/review/assessment/print/printed-multi-choice.component.ts @@ -43,7 +43,7 @@ export class PrintedMultiChoiceComponent { return this.Question.scoreClaimChoiceAnswer(this.sectionQuestion, ignoreForcedScore); }; - calculateWeightedMaxPoints = () => this.Question.calculateWeightedMaxPoints(this.sectionQuestion); + calculateWeightedMaxPoints = () => this.Question.calculateWeightedMaxPoints(this.sectionQuestion.options); calculateMultiChoiceMaxPoints = () => Number.isInteger(this.sectionQuestion.maxScore) diff --git a/ui/src/app/review/assessment/questions/multi-choice-question.component.ts b/ui/src/app/review/assessment/questions/multi-choice-question.component.ts index 34933b351..e8b0f07b9 100644 --- a/ui/src/app/review/assessment/questions/multi-choice-question.component.ts +++ b/ui/src/app/review/assessment/questions/multi-choice-question.component.ts @@ -105,7 +105,7 @@ export class MultiChoiceQuestionComponent implements OnInit { ? this.sectionQuestion.maxScore : this.sectionQuestion.maxScore.toFixed(2); - calculateWeightedMaxPoints = () => this.Question.calculateWeightedMaxPoints(this.sectionQuestion); + calculateWeightedMaxPoints = () => this.Question.calculateWeightedMaxPoints(this.sectionQuestion.options); getMinimumOptionScore = () => this.Question.getMinimumOptionScore(this.sectionQuestion); diff --git a/ui/src/assets/i18n/en.json b/ui/src/assets/i18n/en.json index 5ff4c08d7..fded07f3e 100644 --- a/ui/src/assets/i18n/en.json +++ b/ui/src/assets/i18n/en.json @@ -340,7 +340,7 @@ "i18n_positive_integer_required": "A positive integer is required", "i18n_endtime_before_starttime": "End time cannot be before start time", "i18n_section_name": "Section name (visible to students)", - "i18n_write_question_below": "Write the question in the box below", + "i18n_write_question_above": "Write the question in the box above", "i18n_option_updated": "The alternative answer has been updated", "i18n_new_question_draft": "New question draft", "i18n_unpublish_exam": "Unpublish the exam", diff --git a/ui/src/assets/i18n/fi.json b/ui/src/assets/i18n/fi.json index ffccf2d50..72ccb5200 100644 --- a/ui/src/assets/i18n/fi.json +++ b/ui/src/assets/i18n/fi.json @@ -340,7 +340,7 @@ "i18n_positive_integer_required": "Arvon pitää olla positiivinen kokonaisluku (0-999999)", "i18n_endtime_before_starttime": "Lopetusaika ei voi olla ennen aloitusaikaa", "i18n_section_name": "Aihealueen nimi (näkyy opiskelijoille)", - "i18n_write_question_below": "Kirjoita tenttikysymys alla olevaan kenttään", + "i18n_write_question_above": "Kirjoita tenttikysymys yllä olevaan kenttään", "i18n_option_updated": "Vastausvaihtoehto päivitetty", "i18n_new_question_draft": "Uusi kysymysluonnos", "i18n_unpublish_exam": "Peruuta tentin julkaisu", diff --git a/ui/src/assets/i18n/sv.json b/ui/src/assets/i18n/sv.json index 3aa032ef3..303411a50 100644 --- a/ui/src/assets/i18n/sv.json +++ b/ui/src/assets/i18n/sv.json @@ -340,7 +340,7 @@ "i18n_positive_integer_required": "Värdet ska vara ett positivt heltal (0-999999)", "i18n_endtime_before_starttime": "Sluttiden kan inte vara före starttiden", "i18n_section_name": "Sektionens namn (visas för studerandena)", - "i18n_write_question_below": "Skriv tentamensfrågan i fältet nedan", + "i18n_write_question_above": "Skriv tentamensfrågan i fältet ovan", "i18n_option_updated": "Svarsalternativet är uppdaterat", "i18n_new_question_draft": "Ett nytt utkast av frågan", "i18n_unpublish_exam": "Ångra publicering av tentamen", diff --git a/ui/src/styles.scss b/ui/src/styles.scss index 559abcba2..542b90d22 100644 --- a/ui/src/styles.scss +++ b/ui/src/styles.scss @@ -67,13 +67,18 @@ .xm-xxl-modal .modal-dialog { min-width: 80vw; } + /* form validation overrides */ -form .ng-invalid { +input.ng-invalid { border: 1px solid red; } -form .exclude { - border: 0 !important; +select.ng-invalid { + border: 1px solid red; } +.ng-invalid > .cke { + border: 1px solid red; +} + /* dropdown menu that scrolls */ .xm-scrollable-menu { height: auto; @@ -118,26 +123,6 @@ form .exclude { @extend .xm-study-item-container; border-color: #238635; } -/* standard buttons that change colors on focus */ -@mixin xm-button($bgc1, $border, $color1, $bgc2, $color2: white) { - border: 1px solid $border; - background-color: $bgc1; - color: $color1; - padding: 10px 20px; - line-height: 1.5; - border-radius: 3px; - text-decoration: none; - &:disabled { - opacity: 0.3; - pointer-events: none; - cursor: not-allowed; - } - &:active, - &:hover { - background-color: $bgc2; - color: $color2; - } -} /* Standard bordered box */ .xm-bordered-area { @@ -207,7 +192,8 @@ form .exclude { left: auto; z-index: 999; } -.important-clear-focus:focus, .btn:focus{ +.important-clear-focus:focus, +.btn:focus { box-shadow: 0px 0px 0px 1.5px rgb(255, 255, 255), 0px 0px 0px 4px rgb(68, 120, 247) !important;