From caa1767144b00a0976c1b95c97a897a940067e3d Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Sun, 2 Jul 2023 00:34:18 +0200 Subject: [PATCH 01/75] initial user interface --- .../app/exam/manage/exam-management.module.ts | 6 ++ .../app/exam/manage/exam-management.route.ts | 15 +++- .../student-exam-detail.component.html | 45 ++++++----- .../student-exam-timeline.component.html | 57 ++++++++++++++ .../student-exam-timeline.component.scss | 0 .../student-exam-timeline.component.ts | 78 +++++++++++++++++++ .../exam-navigation-bar.component.html | 5 +- .../exam-navigation-bar.component.ts | 18 ++++- .../exam-navigation-bar.module.ts | 12 +++ .../participate/exam-participation.module.ts | 30 +++---- ...exam-exercise-update-highlighter.module.ts | 11 +++ .../exercises/exam-page.component.ts | 3 +- .../exam-submission-components.module.ts | 46 +++++++++++ .../exercises/exam-submission.component.ts | 3 + .../modeling-exam-submission.component.html | 1 + ...programming-exam-submission.component.html | 3 +- .../quiz/quiz-exam-submission.component.html | 3 + .../text/text-exam-submission.component.html | 2 +- .../participate/timer/exam-timer.module.ts | 11 +++ .../shared/layouts/navbar/navbar.component.ts | 1 + src/main/webapp/i18n/de/exam.json | 1 + src/main/webapp/i18n/en/exam.json | 1 + .../ExerciseScoresChartIntegrationTest.java | 6 +- 23 files changed, 304 insertions(+), 54 deletions(-) create mode 100644 src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html create mode 100644 src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.scss create mode 100644 src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.ts create mode 100644 src/main/webapp/app/exam/participate/exam-navigation-bar/exam-navigation-bar.module.ts create mode 100644 src/main/webapp/app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.module.ts create mode 100644 src/main/webapp/app/exam/participate/exercises/exam-submission-components.module.ts create mode 100644 src/main/webapp/app/exam/participate/timer/exam-timer.module.ts diff --git a/src/main/webapp/app/exam/manage/exam-management.module.ts b/src/main/webapp/app/exam/manage/exam-management.module.ts index 5d606f30618e..aea786a03870 100644 --- a/src/main/webapp/app/exam/manage/exam-management.module.ts +++ b/src/main/webapp/app/exam/manage/exam-management.module.ts @@ -52,7 +52,10 @@ import { ExamExerciseImportComponent } from 'app/exam/manage/exams/exam-exercise import { FeatureToggleModule } from 'app/shared/feature-toggle/feature-toggle.module'; import { BonusComponent } from 'app/grading-system/bonus/bonus.component'; import { ArtemisModePickerModule } from 'app/exercises/shared/mode-picker/mode-picker.module'; +import { StudentExamTimelineComponent } from './student-exams/student-exam-timeline/student-exam-timeline.component'; import { TitleChannelNameModule } from 'app/shared/form/title-channel-name/title-channel-name.module'; +import { ArtemisExamNavigationBarModule } from 'app/exam/participate/exam-navigation-bar/exam-navigation-bar.module'; +import { ArtemisExamSubmissionComponentsModule } from 'app/exam/participate/exercises/exam-submission-components.module'; const ENTITY_STATES = [...examManagementState]; @@ -87,6 +90,8 @@ const ENTITY_STATES = [...examManagementState]; ArtemisModePickerModule, StudentsUploadImagesModule, TitleChannelNameModule, + ArtemisExamNavigationBarModule, + ArtemisExamSubmissionComponentsModule, ], declarations: [ ExamManagementComponent, @@ -115,6 +120,7 @@ const ENTITY_STATES = [...examManagementState]; ExamImportComponent, ExamExerciseImportComponent, BonusComponent, + StudentExamTimelineComponent, ], }) export class ArtemisExamManagementModule {} diff --git a/src/main/webapp/app/exam/manage/exam-management.route.ts b/src/main/webapp/app/exam/manage/exam-management.route.ts index 1026a9df462f..67b1564e54d3 100644 --- a/src/main/webapp/app/exam/manage/exam-management.route.ts +++ b/src/main/webapp/app/exam/manage/exam-management.route.ts @@ -55,6 +55,7 @@ import { FileUploadExerciseManagementResolve } from 'app/exercises/file-upload/m import { ModelingExerciseResolver } from 'app/exercises/modeling/manage/modeling-exercise-resolver.service'; import { ExamResolve, ExerciseGroupResolve, StudentExamResolve } from 'app/exam/manage/exam-management-resolve.service'; import { BonusComponent } from 'app/grading-system/bonus/bonus.component'; +import { StudentExamTimelineComponent } from 'app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component'; export const examManagementRoute: Routes = [ { @@ -276,6 +277,18 @@ export const examManagementRoute: Routes = [ }, canActivate: [UserRouteAccessService], }, + { + path: ':examId/student-exams/:studentExamId/timeline', + component: StudentExamTimelineComponent, + resolve: { + studentExam: StudentExamResolve, + }, + data: { + authorities: [Authority.INSTRUCTOR, Authority.ADMIN], + pageTitle: 'artemisApp.examManagement.title', + }, + canActivate: [UserRouteAccessService], + }, { path: ':examId/student-exams/:studentExamId/summary/overview/grading-key', component: GradingKeyOverviewComponent, @@ -307,7 +320,7 @@ export const examManagementRoute: Routes = [ }, { path: ':examId/test-runs/:studentExamId/summary', - component: StudentExamSummaryComponent, + component: StudentExamTimelineComponent, resolve: { studentExam: StudentExamResolve, }, diff --git a/src/main/webapp/app/exam/manage/student-exams/student-exam-detail.component.html b/src/main/webapp/app/exam/manage/student-exams/student-exam-detail.component.html index f1e26aeca990..1cbc2a80af62 100644 --- a/src/main/webapp/app/exam/manage/student-exams/student-exam-detail.component.html +++ b/src/main/webapp/app/exam/manage/student-exams/student-exam-detail.component.html @@ -11,25 +11,32 @@

-
Student
-
    -
  1. - Name: - {{ student.name }} -
  2. -
  3. - Login: - {{ student.login }} -
  4. -
  5. - Email: - {{ student.email }} -
  6. -
  7. - Matriculation number: - {{ student.visibleRegistrationNumber }} -
  8. -
+
+
+
Student
+
    +
  1. + Name: + {{ student.name }} +
  2. +
  3. + Login: + {{ student.login }} +
  4. +
  5. + Email: + {{ student.email }} +
  6. +
  7. + Matriculation number: + {{ student.visibleRegistrationNumber }} +
  8. +
+
+ + Timeline + +
diff --git a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html new file mode 100644 index 000000000000..c17138ad7b3b --- /dev/null +++ b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html @@ -0,0 +1,57 @@ +

+ {{ 'Klausurverlauf von ' + studentExam?.user?.login }} +

+
+ + + + + + +
+ + + + + +
+
+
+
+
+
+ diff --git a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.scss b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.ts b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.ts new file mode 100644 index 000000000000..722123c197b0 --- /dev/null +++ b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.ts @@ -0,0 +1,78 @@ +import { AfterViewInit, Component, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { StudentExamService } from 'app/exam/manage/student-exams/student-exam.service'; +import { StudentExam } from 'app/entities/student-exam.model'; +import { Exercise, ExerciseType } from 'app/entities/exercise.model'; +import { ExamPage } from 'app/entities/exam-page.model'; +import { ExamPageComponent } from 'app/exam/participate/exercises/exam-page.component'; +import { ExamSubmissionComponent } from 'app/exam/participate/exercises/exam-submission.component'; +import { ExamNavigationBarComponent } from 'app/exam/participate/exam-navigation-bar/exam-navigation-bar.component'; + +@Component({ + selector: 'jhi-student-exam-timeline', + templateUrl: './student-exam-timeline.component.html', + styleUrls: ['./student-exam-timeline.component.scss'], +}) +export class StudentExamTimelineComponent implements OnInit, AfterViewInit { + readonly TEXT = ExerciseType.TEXT; + readonly QUIZ = ExerciseType.QUIZ; + readonly MODELING = ExerciseType.MODELING; + readonly PROGRAMMING = ExerciseType.PROGRAMMING; + readonly FILEUPLOAD = ExerciseType.FILE_UPLOAD; + // determines if component was once drawn visited + pageComponentVisited: boolean[]; + + studentExam: StudentExam; + exerciseIndex: number; + activeExamPage = new ExamPage(); + courseId: number; + + @ViewChildren(ExamSubmissionComponent) currentPageComponents: QueryList; + @ViewChild('examNavigationBar') examNavigationBarComponent: ExamNavigationBarComponent; + + constructor(private studentExamService: StudentExamService, private activatedRoute: ActivatedRoute) {} + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ studentExam: studentExamWithGrade }) => { + this.studentExam = studentExamWithGrade.studentExam; + this.courseId = this.studentExam.exam?.course?.id!; + }); + this.exerciseIndex = 0; + this.pageComponentVisited = new Array(this.studentExam.exercises!.length).fill(false); + } + ngAfterViewInit(): void { + this.examNavigationBarComponent.changePage(false, this.exerciseIndex, false); + } + + onPageChange(exerciseChange: { overViewChange: boolean; exercise?: Exercise; forceSave: boolean }): void { + const activeComponent = this.activePageComponent; + if (activeComponent) { + activeComponent.onDeactivate(); + } + this.initializeExercise(exerciseChange.exercise!); + } + + initializeExercise(exercise: Exercise) { + this.activeExamPage.exercise = exercise; + // set current exercise Index + this.exerciseIndex = this.studentExam.exercises!.findIndex((exercise1) => exercise1.id === exercise.id); + this.activateActiveComponent(); + } + + private activateActiveComponent() { + this.pageComponentVisited[this.activePageIndex] = true; + const activeComponent = this.activePageComponent; + if (activeComponent) { + activeComponent.onActivate(); + } + } + + get activePageIndex(): number { + return this.studentExam.exercises!.findIndex((examExercise) => examExercise.id === this.activeExamPage.exercise?.id); + } + + get activePageComponent(): ExamPageComponent | undefined { + // we have to find the current component based on the activeExercise because the queryList might not be full yet (e.g. only 2 of 5 components initialized) + return this.currentPageComponents.find((submissionComponent) => (submissionComponent as ExamSubmissionComponent).getExercise().id === this.activeExamPage.exercise?.id); + } +} diff --git a/src/main/webapp/app/exam/participate/exam-navigation-bar/exam-navigation-bar.component.html b/src/main/webapp/app/exam/participate/exam-navigation-bar/exam-navigation-bar.component.html index 9e35bb2e7522..690a82308803 100644 --- a/src/main/webapp/app/exam/participate/exam-navigation-bar/exam-navigation-bar.component.html +++ b/src/main/webapp/app/exam/participate/exam-navigation-bar/exam-navigation-bar.component.html @@ -13,6 +13,7 @@
-
+
Online Code Editor

[exercise]="exercise" [participation]="studentParticipation" [btnSize]="ButtonSize.MEDIUM" + *ngIf="!readonly" > [fnOnSelection]="onSelectionChanged.bind(this)" [submittedQuizExercise]="exercise" [questionIndex]="i + 1" + [clickDisabled]="readonly" > (mappingsChange)="dragAndDropMappings.set(question.id!, $event)" [onMappingUpdate]="onSelectionChanged.bind(this)" [questionIndex]="i + 1" + [clickDisabled]="readonly" > (submittedTextsChange)="shortAnswerSubmittedTexts.set(question.id!, $event)" [fnOnSubmittedTextUpdate]="onSelectionChanged.bind(this)" [questionIndex]="i + 1" + [clickDisabled]="readonly" > diff --git a/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.html b/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.html index 1e4f902e6ecc..0b137eaaa6a4 100644 --- a/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.html +++ b/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.html @@ -25,7 +25,7 @@ class="text-editor-textarea" [maxLength]="maxCharacterCount" [(ngModel)]="answer" - [readonly]="!studentSubmission" + [readonly]="readonly || !studentSubmission" [disabled]="!studentSubmission" (keydown.tab)="onTextEditorTab(textEditor, $event)" (input)="onTextEditorInput($event)" diff --git a/src/main/webapp/app/exam/participate/timer/exam-timer.module.ts b/src/main/webapp/app/exam/participate/timer/exam-timer.module.ts new file mode 100644 index 000000000000..b04ff5ab7a30 --- /dev/null +++ b/src/main/webapp/app/exam/participate/timer/exam-timer.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ExamTimerComponent } from 'app/exam/participate/timer/exam-timer.component'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; + +@NgModule({ + declarations: [ExamTimerComponent], + imports: [CommonModule, ArtemisSharedCommonModule], + exports: [ExamTimerComponent], +}) +export class ArtemisExamTimerModule {} diff --git a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts index e0f5904b4bf4..8a952f18405b 100644 --- a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts +++ b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts @@ -331,6 +331,7 @@ export class NavbarComponent implements OnInit, OnDestroy { privacy_statement: 'artemisApp.legal.privacyStatement.title', imprint: 'artemisApp.legal.imprint.title', edit_build_plan: 'artemisApp.programmingExercise.buildPlanEditor', + exam_timeline: 'artemisApp.exam.studentExams.examTimeline', }; studentPathBreadcrumbTranslations = { diff --git a/src/main/webapp/i18n/de/exam.json b/src/main/webapp/i18n/de/exam.json index 895e454151fc..39cf401fed6a 100644 --- a/src/main/webapp/i18n/de/exam.json +++ b/src/main/webapp/i18n/de/exam.json @@ -300,6 +300,7 @@ "submitted": "Abgegeben", "submissionDate": "Abgabedatum", "examSessions": "Sitzungen", + "examTimeline": "Klausur Verlauf", "grade": "Note", "gradeBeforeBonus": "Note vor Bonus", "gradeAfterBonus": "Note nach Bonus", diff --git a/src/main/webapp/i18n/en/exam.json b/src/main/webapp/i18n/en/exam.json index ed141ea1e75e..3425657245ef 100644 --- a/src/main/webapp/i18n/en/exam.json +++ b/src/main/webapp/i18n/en/exam.json @@ -316,6 +316,7 @@ "assessment": "Assessment", "id": "ID", "exam": "Exam", + "examTimeline": "Exam timeline", "studentExamGenerationSuccess": "{{number}} student exams successfully generated!", "studentExamGenerationError": "There was an error during student exam generation:\n {{message}}", "missingStudentExamGenerationSuccess": "{{number}} missing student exams successfully generated!", diff --git a/src/test/java/de/tum/in/www1/artemis/assessment/ExerciseScoresChartIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/assessment/ExerciseScoresChartIntegrationTest.java index 4bd0854348d9..110d8eab9d0b 100644 --- a/src/test/java/de/tum/in/www1/artemis/assessment/ExerciseScoresChartIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/assessment/ExerciseScoresChartIntegrationTest.java @@ -7,9 +7,7 @@ import java.util.List; import java.util.Set; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; @@ -119,7 +117,7 @@ void tearDown() { ParticipantScoreScheduleService.DEFAULT_WAITING_TIME_FOR_SCHEDULED_TASKS = 500; } - @Test + @RepeatedTest(3000) @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void getCourseExerciseScores_asStudent_shouldReturnCorrectIndividualAverageAndMaxScores() throws Exception { List exerciseScores = request.getList(getEndpointUrl(idOfCourse), HttpStatus.OK, ExerciseScoresDTO.class); From 12af27e000d84204d089c4064888b6e7cabffdc2 Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Sun, 2 Jul 2023 21:16:35 +0200 Subject: [PATCH 02/75] continue implementation --- package-lock.json | 29 ++++ package.json | 1 + .../in/www1/artemis/domain/Submission.java | 10 +- .../SubmissionVersionRepository.java | 3 + .../web/rest/ModelingSubmissionResource.java | 33 +++-- .../web/rest/QuizSubmissionResource.java | 31 ++++- .../artemis/web/rest/SubmissionResource.java | 14 +- .../web/rest/TextSubmissionResource.java | 31 ++++- .../app/entities/submission-version.model.ts | 10 ++ .../app/exam/manage/exam-management.module.ts | 3 +- .../student-exam-timeline.component.html | 4 +- .../student-exam-timeline.component.ts | 130 +++++++++++++++++- ...file-upload-exam-submission.component.html | 2 +- .../modeling-exam-submission.component.ts | 25 +++- .../quiz/quiz-exam-submission.component.html | 2 +- .../quiz/quiz-exam-submission.component.ts | 3 +- .../text/text-exam-submission.component.ts | 1 + .../shared/submission/submission.service.ts | 11 ++ 18 files changed, 309 insertions(+), 34 deletions(-) create mode 100644 src/main/webapp/app/entities/submission-version.model.ts diff --git a/package-lock.json b/package-lock.json index 9aa832c55086..273e285747c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,7 @@ "lodash-es": "4.17.21", "mobile-drag-drop": "3.0.0-rc.0", "ngx-infinite-scroll": "16.0.0", + "ngx-slider-v2": "16.0.2", "ngx-webstorage": "12.0.0", "papaparse": "5.3.2", "process": "0.11.10", @@ -9132,6 +9133,11 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/detect-it/-/detect-it-4.0.1.tgz", + "integrity": "sha512-dg5YBTJYvogK1+dA2mBUDKzOWfYZtHVba89SyZUhc4+e3i2tzgjANFg5lDRCd3UOtRcw00vUTMK8LELcMdicug==" + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -9148,6 +9154,14 @@ "dev": true, "peer": true }, + "node_modules/detect-passive-events": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-passive-events/-/detect-passive-events-2.0.3.tgz", + "integrity": "sha512-QN/1X65Axis6a9D8qg8Py9cwY/fkWAmAH/edTbmLMcv4m5dboLJ7LcAi8CfaCON2tjk904KwKX/HTdsHC6yeRg==", + "dependencies": { + "detect-it": "^4.0.1" + } + }, "node_modules/diff-match-patch-typescript": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/diff-match-patch-typescript/-/diff-match-patch-typescript-1.0.8.tgz", @@ -15638,6 +15652,21 @@ "@angular/core": ">=16.0.0 <17.0.0" } }, + "node_modules/ngx-slider-v2": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/ngx-slider-v2/-/ngx-slider-v2-16.0.2.tgz", + "integrity": "sha512-Lpl7SlErL+tJJvTRZYdyZoXTThKN8Ro1z3vscJQ1O5azHXwvbv3pnTcsOwY4ltfaP+dpzY27KL1QXyDr6QMaxQ==", + "dependencies": { + "detect-passive-events": "^2.0.3", + "rxjs": "^7.4.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^16.0.0", + "@angular/core": "^16.0.0", + "@angular/forms": "^16.0.0" + } + }, "node_modules/ngx-webstorage": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/ngx-webstorage/-/ngx-webstorage-12.0.0.tgz", diff --git a/package.json b/package.json index 019099e8baab..f8300bfc8e65 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@angular/platform-browser-dynamic": "16.1.3", "@angular/router": "16.1.3", "@angular/service-worker": "16.1.3", + "ngx-slider-v2": "16.0.2", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "16.0.1", "@fingerprintjs/fingerprintjs": "3.4.1", diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Submission.java b/src/main/java/de/tum/in/www1/artemis/domain/Submission.java index d9c0704f7ab8..d2cc5dbc77be 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Submission.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Submission.java @@ -58,9 +58,9 @@ public abstract class Submission extends DomainObject implements Comparable versions = new HashSet<>(); /** @@ -300,6 +300,14 @@ public void setExampleSubmission(Boolean exampleSubmission) { this.exampleSubmission = exampleSubmission; } + public Set getVersions() { + return versions; + } + + public void setVersions(Set versions) { + this.versions = versions; + } + /** * determine whether a submission is empty, i.e. the student did not work properly on the corresponding exercise * diff --git a/src/main/java/de/tum/in/www1/artemis/repository/SubmissionVersionRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/SubmissionVersionRepository.java index 6e0fd32e81ae..066b7d5e3654 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/SubmissionVersionRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/SubmissionVersionRepository.java @@ -1,5 +1,6 @@ package de.tum.in.www1.artemis.repository; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -18,4 +19,6 @@ public interface SubmissionVersionRepository extends JpaRepository findLatestVersion(@Param("submissionId") long submissionId); + List findSubmissionVersionBySubmissionIdOrderByCreatedDateAsc(long submissionId); + } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingSubmissionResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingSubmissionResource.java index 0953258457f4..425d59af247c 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingSubmissionResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ModelingSubmissionResource.java @@ -20,6 +20,7 @@ import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.security.Role; +import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastTutor; import de.tum.in.www1.artemis.service.AuthorizationCheckService; @@ -38,7 +39,7 @@ * REST controller for managing ModelingSubmission. */ @RestController -@RequestMapping("/api") +@RequestMapping("api/") public class ModelingSubmissionResource extends AbstractSubmissionResource { private final Logger log = LoggerFactory.getLogger(ModelingSubmissionResource.class); @@ -62,10 +63,12 @@ public class ModelingSubmissionResource extends AbstractSubmissionResource { private final PlagiarismService plagiarismService; + private final SubmissionVersionRepository submissionVersionRepository; + public ModelingSubmissionResource(SubmissionRepository submissionRepository, ResultService resultService, ModelingSubmissionService modelingSubmissionService, ModelingExerciseRepository modelingExerciseRepository, AuthorizationCheckService authCheckService, UserRepository userRepository, ExerciseRepository exerciseRepository, GradingCriterionRepository gradingCriterionRepository, ExamSubmissionService examSubmissionService, StudentParticipationRepository studentParticipationRepository, - ModelingSubmissionRepository modelingSubmissionRepository, PlagiarismService plagiarismService) { + ModelingSubmissionRepository modelingSubmissionRepository, PlagiarismService plagiarismService, SubmissionVersionRepository submissionVersionRepository) { super(submissionRepository, resultService, authCheckService, userRepository, exerciseRepository, modelingSubmissionService, studentParticipationRepository); this.modelingSubmissionService = modelingSubmissionService; this.modelingExerciseRepository = modelingExerciseRepository; @@ -73,6 +76,7 @@ public ModelingSubmissionResource(SubmissionRepository submissionRepository, Res this.examSubmissionService = examSubmissionService; this.modelingSubmissionRepository = modelingSubmissionRepository; this.plagiarismService = plagiarismService; + this.submissionVersionRepository = submissionVersionRepository; } /** @@ -83,7 +87,7 @@ public ModelingSubmissionResource(SubmissionRepository submissionRepository, Res * @param modelingSubmission the modelingSubmission to create * @return the ResponseEntity with status 200 (OK) and the Result as its body, or with status 4xx if the request is invalid */ - @PostMapping("/exercises/{exerciseId}/modeling-submissions") + @PostMapping("exercises/{exerciseId}/modeling-submissions") @EnforceAtLeastStudent public ResponseEntity createModelingSubmission(@PathVariable long exerciseId, @Valid @RequestBody ModelingSubmission modelingSubmission) { log.debug("REST request to create modeling submission: {}", modelingSubmission.getModel()); @@ -103,7 +107,7 @@ public ResponseEntity createModelingSubmission(@PathVariable * @return the ResponseEntity with status 200 (OK) and with body the updated modelingSubmission, or with status 400 (Bad Request) if the modelingSubmission is not valid, or * with status 500 (Internal Server Error) if the modelingSubmission couldn't be updated */ - @PutMapping("/exercises/{exerciseId}/modeling-submissions") + @PutMapping("exercises/{exerciseId}/modeling-submissions") @EnforceAtLeastStudent public ResponseEntity updateModelingSubmission(@PathVariable long exerciseId, @Valid @RequestBody ModelingSubmission modelingSubmission) { log.debug("REST request to update modeling submission: {}", modelingSubmission.getModel()); @@ -149,7 +153,7 @@ private ResponseEntity handleModelingSubmission(Long exercis @ResponseStatus(HttpStatus.OK) @ApiResponses({ @ApiResponse(code = 200, message = GET_200_SUBMISSIONS_REASON, response = ModelingSubmission.class, responseContainer = "List"), @ApiResponse(code = 403, message = ErrorConstants.REQ_403_REASON), @ApiResponse(code = 404, message = ErrorConstants.REQ_404_REASON), }) - @GetMapping(value = "/exercises/{exerciseId}/modeling-submissions") + @GetMapping(value = "exercises/{exerciseId}/modeling-submissions") @EnforceAtLeastTutor public ResponseEntity> getAllModelingSubmissions(@PathVariable Long exerciseId, @RequestParam(defaultValue = "false") boolean submittedOnly, @RequestParam(defaultValue = "false") boolean assessedByTutor, @RequestParam(value = "correction-round", defaultValue = "0") int correctionRound) { @@ -170,7 +174,7 @@ public ResponseEntity> getAllModelingSubmissions(@PathVariable * @return the ResponseEntity with status 200 (OK) and with body the modelingSubmission for the given id, or with status 404 (Not Found) if the modelingSubmission could not be * found */ - @GetMapping("/modeling-submissions/{submissionId}") + @GetMapping("modeling-submissions/{submissionId}") @EnforceAtLeastStudent public ResponseEntity getModelingSubmission(@PathVariable Long submissionId, @RequestParam(value = "correction-round", defaultValue = "0") int correctionRound, @RequestParam(value = "resultId", required = false) Long resultId, @@ -233,7 +237,7 @@ public ResponseEntity getModelingSubmission(@PathVariable Lo * @param correctionRound correctionRound for which submissions without a result should be returned * @return the ResponseEntity with status 200 (OK) and a modeling submission without assessment in body */ - @GetMapping(value = "/exercises/{exerciseId}/modeling-submission-without-assessment") + @GetMapping(value = "exercises/{exerciseId}/modeling-submission-without-assessment") @EnforceAtLeastTutor public ResponseEntity getModelingSubmissionWithoutAssessment(@PathVariable Long exerciseId, @RequestParam(value = "lock", defaultValue = "false") boolean lockSubmission, @RequestParam(value = "correction-round", defaultValue = "0") int correctionRound) { @@ -276,7 +280,7 @@ public ResponseEntity getModelingSubmissionWithoutAssessment * @param participationId the participationId for which to find the submission and data for the modeling editor * @return the ResponseEntity with the submission as body */ - @GetMapping("/participations/{participationId}/latest-modeling-submission") + @GetMapping("participations/{participationId}/latest-modeling-submission") @EnforceAtLeastStudent public ResponseEntity getLatestSubmissionForModelingEditor(@PathVariable long participationId) { StudentParticipation participation = studentParticipationRepository.findByIdWithLegalSubmissionsResultsFeedbackElseThrow(participationId); @@ -339,4 +343,17 @@ public ResponseEntity getLatestSubmissionForModelingEditor(@ return ResponseEntity.ok(modelingSubmission); } + + /** + * Retrieve all submission versions for a given submission id + * + * @param submissionId id of the submission + * @return a list of submission versions ordered by creation date + */ + @GetMapping("modeling-submissions/{submissionId}/versions") + @EnforceAtLeastInstructor + public List getSubmissionVersions(@PathVariable long submissionId) { + return submissionVersionRepository.findSubmissionVersionBySubmissionIdOrderByCreatedDateAsc(submissionId); + + } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/QuizSubmissionResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/QuizSubmissionResource.java index fa4cc3ee2499..f59d82f16bac 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/QuizSubmissionResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/QuizSubmissionResource.java @@ -1,6 +1,7 @@ package de.tum.in.www1.artemis.web.rest; import java.time.ZonedDateTime; +import java.util.List; import javax.validation.Valid; @@ -11,6 +12,7 @@ import org.springframework.web.bind.annotation.*; import de.tum.in.www1.artemis.domain.Result; +import de.tum.in.www1.artemis.domain.SubmissionVersion; import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.domain.enumeration.AssessmentType; import de.tum.in.www1.artemis.domain.enumeration.InitializationState; @@ -20,11 +22,10 @@ import de.tum.in.www1.artemis.domain.quiz.QuizSubmission; import de.tum.in.www1.artemis.domain.quiz.SubmittedAnswer; import de.tum.in.www1.artemis.exception.QuizSubmissionException; -import de.tum.in.www1.artemis.repository.QuizExerciseRepository; -import de.tum.in.www1.artemis.repository.StudentParticipationRepository; -import de.tum.in.www1.artemis.repository.UserRepository; +import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.security.SecurityUtils; +import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastTutor; import de.tum.in.www1.artemis.service.AuthorizationCheckService; @@ -38,7 +39,7 @@ * REST controller for managing QuizSubmission. */ @RestController -@RequestMapping("/api") +@RequestMapping("api/") public class QuizSubmissionResource { private final Logger log = LoggerFactory.getLogger(QuizSubmissionResource.class); @@ -64,9 +65,11 @@ public class QuizSubmissionResource { private final ExamSubmissionService examSubmissionService; + private final SubmissionVersionRepository submissionVersionRepository; + public QuizSubmissionResource(QuizExerciseRepository quizExerciseRepository, QuizSubmissionService quizSubmissionService, ParticipationService participationService, WebsocketMessagingService messagingService, UserRepository userRepository, AuthorizationCheckService authCheckService, ExamSubmissionService examSubmissionService, - StudentParticipationRepository studentParticipationRepository) { + StudentParticipationRepository studentParticipationRepository, SubmissionVersionRepository submissionVersionRepository) { this.quizExerciseRepository = quizExerciseRepository; this.quizSubmissionService = quizSubmissionService; this.participationService = participationService; @@ -75,6 +78,7 @@ public QuizSubmissionResource(QuizExerciseRepository quizExerciseRepository, Qui this.authCheckService = authCheckService; this.examSubmissionService = examSubmissionService; this.studentParticipationRepository = studentParticipationRepository; + this.submissionVersionRepository = submissionVersionRepository; } /** @@ -84,7 +88,7 @@ public QuizSubmissionResource(QuizExerciseRepository quizExerciseRepository, Qui * @param quizSubmission the quizSubmission to submit * @return the ResponseEntity with status 200 (OK) and the Result as its body, or with status 4xx if the request is invalid */ - @PostMapping("/exercises/{exerciseId}/submissions/live") + @PostMapping("exercises/{exerciseId}/submissions/live") @EnforceAtLeastStudent public ResponseEntity submitForLiveMode(@PathVariable Long exerciseId, @Valid @RequestBody QuizSubmission quizSubmission) { log.debug("REST request to submit QuizSubmission for live mode : {}", quizSubmission); @@ -108,7 +112,7 @@ public ResponseEntity submitForLiveMode(@PathVariable Long exerc * @param quizSubmission the quizSubmission to submit * @return the ResponseEntity with status 200 (OK) and the Result as its body, or with status 4xx if the request is invalid */ - @PostMapping("/exercises/{exerciseId}/submissions/practice") + @PostMapping("exercises/{exerciseId}/submissions/practice") @EnforceAtLeastStudent public ResponseEntity submitForPractice(@PathVariable Long exerciseId, @Valid @RequestBody QuizSubmission quizSubmission) { log.debug("REST request to submit QuizSubmission for practice : {}", quizSubmission); @@ -235,4 +239,17 @@ public ResponseEntity submitQuizForExam(@PathVariable Long exerc log.info("submitQuizForExam took {}ms for exercise {} and user {}", end - start, exerciseId, user.getLogin()); return ResponseEntity.ok(updatedQuizSubmission); } + + /** + * Retrieve all submission versions for a given submission id + * + * @param submissionId id of the submission + * @return a list of submission versions ordered by creation date + */ + @GetMapping("quiz-submissions/{submissionId}/versions") + @EnforceAtLeastInstructor + public List getSubmissionVersions(@PathVariable long submissionId) { + return submissionVersionRepository.findSubmissionVersionBySubmissionIdOrderByCreatedDateAsc(submissionId); + + } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/SubmissionResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/SubmissionResource.java index c36a8b643b68..def97a172f7b 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/SubmissionResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/SubmissionResource.java @@ -59,9 +59,11 @@ public class SubmissionResource { private final BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository; + private final SubmissionVersionRepository submissionVersionRepository; + public SubmissionResource(SubmissionService submissionService, SubmissionRepository submissionRepository, BuildLogEntryService buildLogEntryService, ResultService resultService, StudentParticipationRepository studentParticipationRepository, AuthorizationCheckService authCheckService, UserRepository userRepository, - ExerciseRepository exerciseRepository, BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository) { + ExerciseRepository exerciseRepository, BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository, SubmissionVersionRepository submissionVersionRepository) { this.submissionService = submissionService; this.submissionRepository = submissionRepository; this.buildLogEntryService = buildLogEntryService; @@ -71,6 +73,7 @@ public SubmissionResource(SubmissionService submissionService, SubmissionReposit this.authCheckService = authCheckService; this.userRepository = userRepository; this.buildLogStatisticsEntryRepository = buildLogStatisticsEntryRepository; + this.submissionVersionRepository = submissionVersionRepository; } /** @@ -221,4 +224,13 @@ private Course findCourseFromSubmission(Submission submission) { return studentParticipationRepository.findByIdElseThrow(participation.getId()).getExercise().getCourseViaExerciseGroupOrCourseMember(); } + + @GetMapping("/submissions/{submissionId}/versions") + @EnforceAtLeastInstructor + public List getSubmissionVersions(@PathVariable long submissionId) { + var submission = submissionRepository.findById(submissionId).orElseThrow(); + authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.INSTRUCTOR, submission.getParticipation().getExercise(), userRepository.getUser()); + return submissionVersionRepository.findSubmissionVersionBySubmissionIdOrderByCreatedDateAsc(submissionId); + + } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/TextSubmissionResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/TextSubmissionResource.java index 0ede38fe599f..05666d8c383d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/TextSubmissionResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/TextSubmissionResource.java @@ -15,6 +15,7 @@ import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.security.Role; +import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastTutor; import de.tum.in.www1.artemis.service.AuthorizationCheckService; @@ -31,7 +32,7 @@ * REST controller for managing TextSubmission. */ @RestController -@RequestMapping("/api") +@RequestMapping("api/") public class TextSubmissionResource extends AbstractSubmissionResource { private static final String ENTITY_NAME = "textSubmission"; @@ -60,11 +61,13 @@ public class TextSubmissionResource extends AbstractSubmissionResource { private final PlagiarismService plagiarismService; + private final SubmissionVersionRepository submissionVersionRepository; + public TextSubmissionResource(SubmissionRepository submissionRepository, ResultService resultService, TextSubmissionRepository textSubmissionRepository, ExerciseRepository exerciseRepository, TextExerciseRepository textExerciseRepository, AuthorizationCheckService authCheckService, TextSubmissionService textSubmissionService, UserRepository userRepository, StudentParticipationRepository studentParticipationRepository, GradingCriterionRepository gradingCriterionRepository, TextAssessmentService textAssessmentService, Optional atheneScheduleService, - ExamSubmissionService examSubmissionService, PlagiarismService plagiarismService) { + ExamSubmissionService examSubmissionService, PlagiarismService plagiarismService, SubmissionVersionRepository submissionVersionRepository) { super(submissionRepository, resultService, authCheckService, userRepository, exerciseRepository, textSubmissionService, studentParticipationRepository); this.textSubmissionRepository = textSubmissionRepository; this.exerciseRepository = exerciseRepository; @@ -77,6 +80,7 @@ public TextSubmissionResource(SubmissionRepository submissionRepository, ResultS this.textAssessmentService = textAssessmentService; this.examSubmissionService = examSubmissionService; this.plagiarismService = plagiarismService; + this.submissionVersionRepository = submissionVersionRepository; } /** @@ -86,7 +90,7 @@ public TextSubmissionResource(SubmissionRepository submissionRepository, ResultS * @param textSubmission the textSubmission to create * @return the ResponseEntity with status 200 (OK) and the Result as its body, or with status 4xx if the request is invalid */ - @PostMapping("/exercises/{exerciseId}/text-submissions") + @PostMapping("exercises/{exerciseId}/text-submissions") @EnforceAtLeastStudent public ResponseEntity createTextSubmission(@PathVariable Long exerciseId, @Valid @RequestBody TextSubmission textSubmission) { log.debug("REST request to save text submission : {}", textSubmission); @@ -106,7 +110,7 @@ public ResponseEntity createTextSubmission(@PathVariable Long ex * @return the ResponseEntity with status 200 (OK) and with body the updated textSubmission, or with status 400 (Bad Request) if the textSubmission is not valid, or with status * 500 (Internal Server Error) if the textSubmission couldn't be updated */ - @PutMapping("/exercises/{exerciseId}/text-submissions") + @PutMapping("exercises/{exerciseId}/text-submissions") @EnforceAtLeastStudent public ResponseEntity updateTextSubmission(@PathVariable long exerciseId, @Valid @RequestBody TextSubmission textSubmission) { log.debug("REST request to update text submission: {}", textSubmission); @@ -143,7 +147,7 @@ private ResponseEntity handleTextSubmission(long exerciseId, Tex * @param submissionId the id of the textSubmission to retrieve * @return the ResponseEntity with status 200 (OK) and with body the textSubmission, or with status 404 (Not Found) */ - @GetMapping("/text-submissions/{submissionId}") + @GetMapping("text-submissions/{submissionId}") @EnforceAtLeastStudent public ResponseEntity getTextSubmissionWithResults(@PathVariable long submissionId) { log.debug("REST request to get text submission: {}", submissionId); @@ -169,7 +173,7 @@ public ResponseEntity getTextSubmissionWithResults(@PathVariable * @param assessedByTutor mark if only assessed Submissions should be returned * @return the ResponseEntity with status 200 (OK) and the list of textSubmissions in body */ - @GetMapping(value = "/exercises/{exerciseId}/text-submissions") + @GetMapping(value = "exercises/{exerciseId}/text-submissions") @EnforceAtLeastTutor public ResponseEntity> getAllTextSubmissions(@PathVariable Long exerciseId, @RequestParam(defaultValue = "false") boolean submittedOnly, @RequestParam(defaultValue = "false") boolean assessedByTutor, @RequestParam(value = "correction-round", defaultValue = "0") int correctionRound) { @@ -187,7 +191,7 @@ public ResponseEntity> getAllTextSubmissions(@PathVariable Long * @param lockSubmission optional value to define if the submission should be locked and has the value of false if not set manually * @return the ResponseEntity with status 200 (OK) and the list of textSubmissions in body */ - @GetMapping(value = "/exercises/{exerciseId}/text-submission-without-assessment") + @GetMapping(value = "exercises/{exerciseId}/text-submission-without-assessment") @EnforceAtLeastTutor public ResponseEntity getTextSubmissionWithoutAssessment(@PathVariable Long exerciseId, @RequestParam(value = "head", defaultValue = "false") boolean skipAssessmentOrderOptimization, @@ -237,4 +241,17 @@ public ResponseEntity getTextSubmissionWithoutAssessment(@PathVa return ResponseEntity.ok().body(textSubmission); } + + /** + * Retrieve all submission versions for a given submission id + * + * @param submissionId id of the submission + * @return a list of submission versions ordered by creation date + */ + @GetMapping("text-submissions/{submissionId}/versions") + @EnforceAtLeastInstructor + public List getSubmissionVersions(@PathVariable long submissionId) { + return submissionVersionRepository.findSubmissionVersionBySubmissionIdOrderByCreatedDateAsc(submissionId); + + } } diff --git a/src/main/webapp/app/entities/submission-version.model.ts b/src/main/webapp/app/entities/submission-version.model.ts new file mode 100644 index 000000000000..3e781c93294b --- /dev/null +++ b/src/main/webapp/app/entities/submission-version.model.ts @@ -0,0 +1,10 @@ +import { BaseEntity } from 'app/shared/model/base-entity'; +import { Submission } from 'app/entities/submission.model'; +import dayjs from 'dayjs/esm'; + +export class SubmissionVersion implements BaseEntity { + public id?: number; + public submission: Submission; + public createdDate: dayjs.Dayjs; + public content: string; +} diff --git a/src/main/webapp/app/exam/manage/exam-management.module.ts b/src/main/webapp/app/exam/manage/exam-management.module.ts index aea786a03870..792b8afbaec9 100644 --- a/src/main/webapp/app/exam/manage/exam-management.module.ts +++ b/src/main/webapp/app/exam/manage/exam-management.module.ts @@ -56,7 +56,7 @@ import { StudentExamTimelineComponent } from './student-exams/student-exam-timel import { TitleChannelNameModule } from 'app/shared/form/title-channel-name/title-channel-name.module'; import { ArtemisExamNavigationBarModule } from 'app/exam/participate/exam-navigation-bar/exam-navigation-bar.module'; import { ArtemisExamSubmissionComponentsModule } from 'app/exam/participate/exercises/exam-submission-components.module'; - +import { NgxSliderModule } from 'ngx-slider-v2'; const ENTITY_STATES = [...examManagementState]; @NgModule({ @@ -92,6 +92,7 @@ const ENTITY_STATES = [...examManagementState]; TitleChannelNameModule, ArtemisExamNavigationBarModule, ArtemisExamSubmissionComponentsModule, + NgxSliderModule, ], declarations: [ ExamManagementComponent, diff --git a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html index c17138ad7b3b..7a145452dc95 100644 --- a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html +++ b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html @@ -22,6 +22,7 @@ [exercise]="exercise" [studentSubmission]="exercise.studentParticipations[0].submissions![0]" [readonly]="true" + [examTimeline]="true" > - + diff --git a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.ts b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.ts index 722123c197b0..6384e63659ad 100644 --- a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.ts +++ b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; +import { AfterViewInit, ChangeDetectorRef, Component, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { StudentExamService } from 'app/exam/manage/student-exams/student-exam.service'; import { StudentExam } from 'app/entities/student-exam.model'; @@ -7,6 +7,14 @@ import { ExamPage } from 'app/entities/exam-page.model'; import { ExamPageComponent } from 'app/exam/participate/exercises/exam-page.component'; import { ExamSubmissionComponent } from 'app/exam/participate/exercises/exam-submission.component'; import { ExamNavigationBarComponent } from 'app/exam/participate/exam-navigation-bar/exam-navigation-bar.component'; +import { SubmissionService } from 'app/exercises/shared/submission/submission.service'; +import dayjs from 'dayjs/esm'; +import { SubmissionVersion } from 'app/entities/submission-version.model'; +import { Observable, catchError, combineLatest, combineLatestWith, forkJoin, map, merge, mergeMap, of, toArray } from 'rxjs'; +import { ProgrammingSubmission } from 'app/entities/programming-submission.model'; +import { Submission } from 'app/entities/submission.model'; +import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; +import { ChangeContext, LabelType, Options } from 'ngx-slider-v2'; @Component({ selector: 'jhi-student-exam-timeline', @@ -21,16 +29,36 @@ export class StudentExamTimelineComponent implements OnInit, AfterViewInit { readonly FILEUPLOAD = ExerciseType.FILE_UPLOAD; // determines if component was once drawn visited pageComponentVisited: boolean[]; + value: number; + options: Options = { + showTicks: true, + showTicksValues: true, + stepsArray: [{ value: 0 }], + translate: (value: number): string => { + return this.datePipe.transform(value, 'time', true); + }, + }; studentExam: StudentExam; exerciseIndex: number; activeExamPage = new ExamPage(); courseId: number; - + submissionTimeStamps: dayjs.Dayjs[] = []; + submissionVersions: SubmissionVersion[] = []; + programmingSubmissions: ProgrammingSubmission[] = []; + stepIndex = 0; @ViewChildren(ExamSubmissionComponent) currentPageComponents: QueryList; @ViewChild('examNavigationBar') examNavigationBarComponent: ExamNavigationBarComponent; + finalValue: dayjs.Dayjs; + readonly SubmissionVersion = SubmissionVersion; - constructor(private studentExamService: StudentExamService, private activatedRoute: ActivatedRoute) {} + constructor( + private studentExamService: StudentExamService, + private activatedRoute: ActivatedRoute, + private submissionService: SubmissionService, + private datePipe: ArtemisDatePipe, + private changeDetectorRef: ChangeDetectorRef, + ) {} ngOnInit(): void { this.activatedRoute.data.subscribe(({ studentExam: studentExamWithGrade }) => { @@ -39,7 +67,66 @@ export class StudentExamTimelineComponent implements OnInit, AfterViewInit { }); this.exerciseIndex = 0; this.pageComponentVisited = new Array(this.studentExam.exercises!.length).fill(false); + this.retrieveSubmissionDataAndTimeStamps().subscribe((results) => { + results.forEach((result) => { + if (this.isSubmissionVersion(result)) { + const submissionVersion = result as SubmissionVersion; + console.log('submitted version at ' + submissionVersion.createdDate); + this.submissionVersions.push(submissionVersion); + this.submissionTimeStamps.push(submissionVersion.createdDate); + } else { + const programmingSubmission = result as ProgrammingSubmission; + console.log('programming submission at ' + programmingSubmission.submissionDate!); + + this.programmingSubmissions.push(programmingSubmission); + this.submissionTimeStamps.push(programmingSubmission.submissionDate!); + } + }); + this.sortTimeStamps(); + this.setupRangeSlider(); + }); + } + + private setupRangeSlider() { + this.value = this.submissionTimeStamps[0]?.toDate().getTime(); + const newOptions: Options = Object.assign({}, this.options); + newOptions.stepsArray = this.submissionTimeStamps.map((date) => { + return { + value: date.toDate().getTime(), + }; + }); + this.options = newOptions; + } + + private isSubmissionVersion(object: SubmissionVersion | Submission | null) { + if (object === null) { + return false; + } + const submissionVersion = object as SubmissionVersion; + return submissionVersion.id && submissionVersion.createdDate && submissionVersion.content && submissionVersion.submission; + } + + private retrieveSubmissionDataAndTimeStamps() { + const submissionObservables: Observable[] = []; + this.studentExam.exercises?.forEach((exercise) => { + if (exercise.type !== this.PROGRAMMING) { + submissionObservables.push( + this.submissionService.findAllSubmissionVersionsOfSubmission(exercise.studentParticipations![0].submissions![0].id!).pipe( + mergeMap((versions) => versions), + toArray(), + ), + ); + } else { + submissionObservables.push(this.submissionService.findAllSubmissionsOfParticipation(exercise.studentParticipations![0].id!).pipe(map(({ body }) => body!))); + } + }); + return merge(...submissionObservables); } + + private sortTimeStamps() { + this.submissionTimeStamps = this.submissionTimeStamps.sort((date1, date2) => (date1.isAfter(date2) ? 1 : -1)); + } + ngAfterViewInit(): void { this.examNavigationBarComponent.changePage(false, this.exerciseIndex, false); } @@ -75,4 +162,41 @@ export class StudentExamTimelineComponent implements OnInit, AfterViewInit { // we have to find the current component based on the activeExercise because the queryList might not be full yet (e.g. only 2 of 5 components initialized) return this.currentPageComponents.find((submissionComponent) => (submissionComponent as ExamSubmissionComponent).getExercise().id === this.activeExamPage.exercise?.id); } + + onInputChange(changeContext: ChangeContext) { + console.log('change'); + const submission = this.findCorrespondingSubmissionForTimestamp(changeContext.value); + let exercise: Exercise | undefined; + if (this.isSubmissionVersion(submission)) { + const submissionVersion = submission as SubmissionVersion; + exercise = submissionVersion.submission.participation?.exercise; + } else { + const programmingSubmission = submission as ProgrammingSubmission; + exercise = programmingSubmission.participation?.exercise; + } + if (exercise) { + const exerciseIndex = this.studentExam.exercises!.findIndex((examExercise) => examExercise.id === exercise?.id); + this.examNavigationBarComponent.changePage(false, exerciseIndex, false); + } + // TODO find the corresponding submission for the timestamp instantiate the component with it and navigate to the respective page + } + + private findCorrespondingSubmissionForTimestamp(timestamp: number): SubmissionVersion | ProgrammingSubmission | null { + console.log('find submission for timestamp' + timestamp); + for (let i = 0; i < this.submissionVersions.length; i++) { + const comparisonObject = dayjs(timestamp); + const submissionVersion = this.submissionVersions[i]; + if (submissionVersion.createdDate.isSame(comparisonObject)) { + return submissionVersion; + } + } + for (let i = 0; i < this.programmingSubmissions.length; i++) { + const comparisonObject = dayjs(timestamp); + const programmingSubmission = this.programmingSubmissions[i]; + if (programmingSubmission.submissionDate?.isSame(comparisonObject)) { + return programmingSubmission; + } + } + return null; + } } diff --git a/src/main/webapp/app/exam/participate/exercises/file-upload/file-upload-exam-submission.component.html b/src/main/webapp/app/exam/participate/exercises/file-upload/file-upload-exam-submission.component.html index db5b14cd500d..29f1651eac3b 100644 --- a/src/main/webapp/app/exam/participate/exercises/file-upload/file-upload-exam-submission.component.html +++ b/src/main/webapp/app/exam/participate/exercises/file-upload/file-upload-exam-submission.component.html @@ -11,7 +11,7 @@
-
+
diff --git a/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts b/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts index a9d4a2633182..673471774331 100644 --- a/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts @@ -8,6 +8,7 @@ import { ExamSubmissionComponent } from 'app/exam/participate/exercises/exam-sub import { Submission } from 'app/entities/submission.model'; import { Exercise, IncludedInOverallScore } from 'app/entities/exercise.model'; import { faListAlt } from '@fortawesome/free-regular-svg-icons'; +import { SubmissionVersion } from 'app/entities/submission-version.model'; @Component({ selector: 'jhi-modeling-submission-exam', @@ -24,6 +25,10 @@ export class ModelingExamSubmissionComponent extends ExamSubmissionComponent imp // IMPORTANT: this reference must be contained in this.studentParticipation.submissions[0] otherwise the parent component will not be able to react to changes @Input() studentSubmission: ModelingSubmission; + @Input() + submissionVersion: SubmissionVersion; + @Input() + examTimeline = false; @Input() exercise: ModelingExercise; @@ -41,8 +46,12 @@ export class ModelingExamSubmissionComponent extends ExamSubmissionComponent imp } ngOnInit(): void { - // show submission answers in UI - this.updateViewFromSubmission(); + if (this.examTimeline) { + this.updateViewFromSubmissionVersion(); + } else { + // show submission answers in UI + this.updateViewFromSubmission(); + } } /** @@ -116,4 +125,16 @@ export class ModelingExamSubmissionComponent extends ExamSubmissionComponent imp this.studentSubmission.isSynced = false; this.explanationText = explanation; } + + private updateViewFromSubmissionVersion() { + if (this.submissionVersion) { + if (this.submissionVersion.content) { + // Updates the Apollon editor model state (view) with the latest modeling submission + this.umlModel = JSON.parse(this.submissionVersion.content); + } + //TODO include explanation in submission version?? + // Updates explanation text with the latest submission + this.explanationText = this.studentSubmission.explanationText ?? ''; + } + } } diff --git a/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.html b/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.html index d40bca3c0835..df38212cf163 100644 --- a/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.html +++ b/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.html @@ -1,5 +1,5 @@
-
+
diff --git a/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.ts b/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.ts index f8d960084243..6f526391daf2 100644 --- a/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.ts @@ -48,10 +48,11 @@ export class QuizExamSubmissionComponent extends ExamSubmissionComponent impleme studentSubmission: AbstractQuizSubmission; @Input() exercise: QuizExercise; + @Input() examTimeline = false; + selectedAnswerOptions = new Map(); dragAndDropMappings = new Map(); shortAnswerSubmittedTexts = new Map(); - constructor(private quizService: ArtemisQuizService, changeDetectorReference: ChangeDetectorRef) { super(changeDetectorReference); smoothscroll.polyfill(); diff --git a/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.ts b/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.ts index 6c07397fdb3b..79aaf7e414be 100644 --- a/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.ts @@ -11,6 +11,7 @@ import { ExamSubmissionComponent } from 'app/exam/participate/exercises/exam-sub import { Submission } from 'app/entities/submission.model'; import { faListAlt } from '@fortawesome/free-regular-svg-icons'; import { MAX_SUBMISSION_TEXT_LENGTH } from 'app/shared/constants/input.constants'; +import { SubmissionVersion } from 'app/entities/submission-version.model'; @Component({ selector: 'jhi-text-editor-exam', diff --git a/src/main/webapp/app/exercises/shared/submission/submission.service.ts b/src/main/webapp/app/exercises/shared/submission/submission.service.ts index af4e9549d0fd..c5d92a51aad5 100644 --- a/src/main/webapp/app/exercises/shared/submission/submission.service.ts +++ b/src/main/webapp/app/exercises/shared/submission/submission.service.ts @@ -13,6 +13,7 @@ import { AccountService } from 'app/core/auth/account.service'; import { ParticipationService } from 'app/exercises/shared/participation/participation.service'; import { convertDateFromServer } from 'app/utils/date.utils'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; +import { SubmissionVersion } from 'app/entities/submission-version.model'; export type EntityResponseType = HttpResponse; export type EntityArrayResponseType = HttpResponse; @@ -56,6 +57,10 @@ export class SubmissionService { ); } + findAllSubmissionVersionsOfSubmission(submissionId: number): Observable { + return this.http.get(`${this.resourceUrl}/${submissionId}/versions`).pipe(map((res) => this.convertCreatedDatesFromServer(res))); + } + /** * Find the submissions with complaints for a tutor for a specified exercise (complaintType == 'COMPLAINT'). * @param exerciseId @@ -264,4 +269,10 @@ export class SubmissionService { secondFeedback.text === firstFeedback.text ); } + + private convertCreatedDatesFromServer(res: SubmissionVersion[]): SubmissionVersion[] { + return res.map((version) => { + return { ...version, createdDate: convertDateFromServer(version.createdDate)! }; + }); + } } From eb425a0bf897422d0f933745bcf956f0db4dd382 Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Sun, 2 Jul 2023 21:17:34 +0200 Subject: [PATCH 03/75] continue implementation --- src/main/java/de/tum/in/www1/artemis/domain/Submission.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Submission.java b/src/main/java/de/tum/in/www1/artemis/domain/Submission.java index d2cc5dbc77be..a8a3f6727be0 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Submission.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Submission.java @@ -60,7 +60,7 @@ public abstract class Submission extends DomainObject implements Comparable versions = new HashSet<>(); /** From bacb5e86c072bac1ee0f36e1dfcebfef92c5ebe7 Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Mon, 3 Jul 2023 15:20:12 +0200 Subject: [PATCH 04/75] continue implementation --- .../student-exam-timeline.component.ts | 14 ++++++++++---- .../exercises/exam-submission.component.ts | 3 +++ .../modeling/modeling-exam-submission.component.ts | 2 -- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.ts b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.ts index 6384e63659ad..973469b3aea8 100644 --- a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.ts +++ b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.ts @@ -10,11 +10,11 @@ import { ExamNavigationBarComponent } from 'app/exam/participate/exam-navigation import { SubmissionService } from 'app/exercises/shared/submission/submission.service'; import dayjs from 'dayjs/esm'; import { SubmissionVersion } from 'app/entities/submission-version.model'; -import { Observable, catchError, combineLatest, combineLatestWith, forkJoin, map, merge, mergeMap, of, toArray } from 'rxjs'; +import { Observable, map, merge, mergeMap, toArray } from 'rxjs'; import { ProgrammingSubmission } from 'app/entities/programming-submission.model'; import { Submission } from 'app/entities/submission.model'; import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; -import { ChangeContext, LabelType, Options } from 'ngx-slider-v2'; +import { ChangeContext, Options } from 'ngx-slider-v2'; @Component({ selector: 'jhi-student-exam-timeline', @@ -46,10 +46,8 @@ export class StudentExamTimelineComponent implements OnInit, AfterViewInit { submissionTimeStamps: dayjs.Dayjs[] = []; submissionVersions: SubmissionVersion[] = []; programmingSubmissions: ProgrammingSubmission[] = []; - stepIndex = 0; @ViewChildren(ExamSubmissionComponent) currentPageComponents: QueryList; @ViewChild('examNavigationBar') examNavigationBarComponent: ExamNavigationBarComponent; - finalValue: dayjs.Dayjs; readonly SubmissionVersion = SubmissionVersion; constructor( @@ -176,6 +174,14 @@ export class StudentExamTimelineComponent implements OnInit, AfterViewInit { } if (exercise) { const exerciseIndex = this.studentExam.exercises!.findIndex((examExercise) => examExercise.id === exercise?.id); + const correspondingSubmissionComponent = this.currentPageComponents.find( + (submissionComponent) => (submissionComponent as ExamSubmissionComponent).getExercise().id === exercise?.id, + ); + if (exercise.type === ExerciseType.PROGRAMMING) { + correspondingSubmissionComponent!.submission = submission as ProgrammingSubmission; + } else { + correspondingSubmissionComponent!.submissionVersion = submission as SubmissionVersion; + } this.examNavigationBarComponent.changePage(false, exerciseIndex, false); } // TODO find the corresponding submission for the timestamp instantiate the component with it and navigate to the respective page diff --git a/src/main/webapp/app/exam/participate/exercises/exam-submission.component.ts b/src/main/webapp/app/exam/participate/exercises/exam-submission.component.ts index 325b78ac8227..9cd340b694aa 100644 --- a/src/main/webapp/app/exam/participate/exercises/exam-submission.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/exam-submission.component.ts @@ -2,6 +2,7 @@ import { Submission } from 'app/entities/submission.model'; import { Exercise } from 'app/entities/exercise.model'; import { ExamPageComponent } from 'app/exam/participate/exercises/exam-page.component'; import { Directive, Input } from '@angular/core'; +import { SubmissionVersion } from 'app/entities/submission-version.model'; @Directive() export abstract class ExamSubmissionComponent extends ExamPageComponent { @@ -27,4 +28,6 @@ export abstract class ExamSubmissionComponent extends ExamPageComponent { abstract getSubmission(): Submission | undefined; abstract getExercise(): Exercise; @Input() readonly = false; + @Input() submissionVersion: SubmissionVersion; + @Input() submission: Submission; } diff --git a/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts b/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts index 673471774331..cb903ea92832 100644 --- a/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts @@ -26,8 +26,6 @@ export class ModelingExamSubmissionComponent extends ExamSubmissionComponent imp @Input() studentSubmission: ModelingSubmission; @Input() - submissionVersion: SubmissionVersion; - @Input() examTimeline = false; @Input() From 0a78175f6bf65a9ccc7f1217eadf7303c58c0a78 Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Wed, 5 Jul 2023 21:54:45 +0200 Subject: [PATCH 05/75] debug logs --- .../student-exam-timeline.component.html | 1 + .../student-exam-timeline.component.ts | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html index 7a145452dc95..d6fca6d5c679 100644 --- a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html +++ b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html @@ -18,6 +18,7 @@
examExercise.id === exercise?.id); + + this.examNavigationBarComponent.changePage(false, exerciseIndex, false); + console.log(this.currentPageComponents); + this.currentPageComponents.forEach((component) => { + console.log('exercise id in submission component' + (component as ExamSubmissionComponent).getExercise().id); + }); const correspondingSubmissionComponent = this.currentPageComponents.find( (submissionComponent) => (submissionComponent as ExamSubmissionComponent).getExercise().id === exercise?.id, ); + if (!correspondingSubmissionComponent) { + console.log('no corresponding submission component found'); + } if (exercise.type === ExerciseType.PROGRAMMING) { correspondingSubmissionComponent!.submission = submission as ProgrammingSubmission; } else { correspondingSubmissionComponent!.submissionVersion = submission as SubmissionVersion; } - this.examNavigationBarComponent.changePage(false, exerciseIndex, false); } // TODO find the corresponding submission for the timestamp instantiate the component with it and navigate to the respective page } From ec0ba9297b0d3d2339590045303fc43fb904cdbc Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Fri, 7 Jul 2023 08:42:52 +0200 Subject: [PATCH 06/75] try to make it work for modeling --- .../student-exam-timeline.component.html | 2 + .../student-exam-timeline.component.ts | 88 ++++++++++++------- .../exam-navigation-bar.component.ts | 14 ++- .../exercises/exam-submission.component.ts | 6 +- .../file-upload-exam-submission.component.ts | 4 + .../modeling-exam-submission.component.ts | 18 ++-- .../programming-exam-submission.component.ts | 4 + .../quiz/quiz-exam-submission.component.ts | 6 ++ .../text/text-exam-submission.component.ts | 16 +++- 9 files changed, 111 insertions(+), 47 deletions(-) diff --git a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html index d6fca6d5c679..d627de9defa5 100644 --- a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html +++ b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html @@ -36,6 +36,7 @@ [exercise]="exercise" [studentSubmission]="exercise.studentParticipations[0].submissions![0]" [readonly]="true" + [examTimeline]="true" >
diff --git a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.ts b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.ts index 85bb38f01deb..898c3f4b180e 100644 --- a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.ts +++ b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.ts @@ -46,6 +46,7 @@ export class StudentExamTimelineComponent implements OnInit, AfterViewInit { submissionTimeStamps: dayjs.Dayjs[] = []; submissionVersions: SubmissionVersion[] = []; programmingSubmissions: ProgrammingSubmission[] = []; + currentExercise: Exercise | undefined; @ViewChildren(ExamSubmissionComponent) currentPageComponents: QueryList; @ViewChild('examNavigationBar') examNavigationBarComponent: ExamNavigationBarComponent; readonly SubmissionVersion = SubmissionVersion; @@ -86,7 +87,7 @@ export class StudentExamTimelineComponent implements OnInit, AfterViewInit { } private setupRangeSlider() { - this.value = this.submissionTimeStamps[0]?.toDate().getTime(); + this.value = this.submissionTimeStamps[0]?.toDate().getTime() ?? 0; const newOptions: Options = Object.assign({}, this.options); newOptions.stepsArray = this.submissionTimeStamps.map((date) => { return { @@ -96,7 +97,7 @@ export class StudentExamTimelineComponent implements OnInit, AfterViewInit { this.options = newOptions; } - private isSubmissionVersion(object: SubmissionVersion | Submission | null) { + private isSubmissionVersion(object: SubmissionVersion | Submission | undefined) { if (object === null) { return false; } @@ -126,22 +127,49 @@ export class StudentExamTimelineComponent implements OnInit, AfterViewInit { } ngAfterViewInit(): void { - this.examNavigationBarComponent.changePage(false, this.exerciseIndex, false); + this.examNavigationBarComponent.changePage(false, this.exerciseIndex, false, undefined, true); + + //TODO set correct submission version on init } - onPageChange(exerciseChange: { overViewChange: boolean; exercise?: Exercise; forceSave: boolean }): void { + onPageChange(exerciseChange: { + overViewChange: boolean; + exercise?: Exercise; + forceSave: boolean; + submission?: ProgrammingSubmission | SubmissionVersion; + initial?: boolean; + }): void { const activeComponent = this.activePageComponent; if (activeComponent) { activeComponent.onDeactivate(); } - this.initializeExercise(exerciseChange.exercise!); + this.initializeExercise(exerciseChange.exercise!, exerciseChange.submission, exerciseChange.initial); } - initializeExercise(exercise: Exercise) { + initializeExercise(exercise: Exercise, submission: Submission | SubmissionVersion | undefined, initial?: boolean) { this.activeExamPage.exercise = exercise; // set current exercise Index this.exerciseIndex = this.studentExam.exercises!.findIndex((exercise1) => exercise1.id === exercise.id); this.activateActiveComponent(); + //const activeComponent = this.activeExamPage as ExamSubmissionComponent; + const activeComponent = this.activePageComponent; + // if we show the page for the first time we need to find the submission version that was submitted first during the exam + // if (initial) { + // const submissionVersions = this.submissionVersions.filter((submissionVersion) => submissionVersion.submission!.id === exercise.studentParticipations![0].submissions![0].id)!; + // if (submissionVersions.length === 1) { + // submission = submissionVersions[0]; + // } else { + // submission = submissionVersions.reduce((previous, current) => (previous.createdDate!.isAfter(current.createdDate!) ? previous : current)); + // } + // } + if (activeComponent) { + if (this.currentExercise?.type === ExerciseType.PROGRAMMING) { + activeComponent!.submission = submission as ProgrammingSubmission; + } else { + activeComponent!.submissionVersion = submission as SubmissionVersion; + activeComponent?.updateViewFromSubmissionVersion(); + } + } } private activateActiveComponent() { @@ -156,7 +184,7 @@ export class StudentExamTimelineComponent implements OnInit, AfterViewInit { return this.studentExam.exercises!.findIndex((examExercise) => examExercise.id === this.activeExamPage.exercise?.id); } - get activePageComponent(): ExamPageComponent | undefined { + get activePageComponent(): ExamSubmissionComponent | undefined { // we have to find the current component based on the activeExercise because the queryList might not be full yet (e.g. only 2 of 5 components initialized) return this.currentPageComponents.find((submissionComponent) => (submissionComponent as ExamSubmissionComponent).getExercise().id === this.activeExamPage.exercise?.id); } @@ -164,41 +192,35 @@ export class StudentExamTimelineComponent implements OnInit, AfterViewInit { onInputChange(changeContext: ChangeContext) { console.log('change'); const submission = this.findCorrespondingSubmissionForTimestamp(changeContext.value); - let exercise: Exercise | undefined; if (this.isSubmissionVersion(submission)) { const submissionVersion = submission as SubmissionVersion; - exercise = submissionVersion.submission.participation?.exercise; + this.currentExercise = submissionVersion.submission.participation?.exercise; } else { const programmingSubmission = submission as ProgrammingSubmission; - exercise = programmingSubmission.participation?.exercise; - } - if (exercise) { - console.log('exercise id: ' + exercise.id); - const exerciseIndex = this.studentExam.exercises!.findIndex((examExercise) => examExercise.id === exercise?.id); - - this.examNavigationBarComponent.changePage(false, exerciseIndex, false); - console.log(this.currentPageComponents); - this.currentPageComponents.forEach((component) => { - console.log('exercise id in submission component' + (component as ExamSubmissionComponent).getExercise().id); - }); - const correspondingSubmissionComponent = this.currentPageComponents.find( - (submissionComponent) => (submissionComponent as ExamSubmissionComponent).getExercise().id === exercise?.id, - ); - if (!correspondingSubmissionComponent) { - console.log('no corresponding submission component found'); - } - if (exercise.type === ExerciseType.PROGRAMMING) { - correspondingSubmissionComponent!.submission = submission as ProgrammingSubmission; - } else { - correspondingSubmissionComponent!.submissionVersion = submission as SubmissionVersion; - } + this.currentExercise = programmingSubmission.participation?.exercise; } + const exerciseIndex = this.studentExam.exercises!.findIndex((examExercise) => examExercise.id === this.currentExercise?.id); + + this.examNavigationBarComponent.changePage(false, exerciseIndex, false, submission); + // this.currentPageComponents.changes.subscribe(() => { + // console.log("Item elements are now in the DOM!", this.currentPageComponents.length); + // + // }); + // console.log(this.currentPageComponents); + // this.currentPageComponents.forEach((component) => { + // console.log('exercise id in submission component' + (component as ExamSubmissionComponent).getExercise().id); + // }); + // if (!correspondingSubmissionComponent) { + // console.log('no corresponding submission component found'); + // } + // TODO find the corresponding submission for the timestamp instantiate the component with it and navigate to the respective page } - private findCorrespondingSubmissionForTimestamp(timestamp: number): SubmissionVersion | ProgrammingSubmission | null { + private findCorrespondingSubmissionForTimestamp(timestamp: number): SubmissionVersion | ProgrammingSubmission | undefined { console.log('find submission for timestamp' + timestamp); for (let i = 0; i < this.submissionVersions.length; i++) { + console.log(timestamp); const comparisonObject = dayjs(timestamp); const submissionVersion = this.submissionVersions[i]; if (submissionVersion.createdDate.isSame(comparisonObject)) { @@ -212,6 +234,6 @@ export class StudentExamTimelineComponent implements OnInit, AfterViewInit { return programmingSubmission; } } - return null; + return undefined; } } diff --git a/src/main/webapp/app/exam/participate/exam-navigation-bar/exam-navigation-bar.component.ts b/src/main/webapp/app/exam/participate/exam-navigation-bar/exam-navigation-bar.component.ts index 97493e2cff78..5e2d75c0640f 100644 --- a/src/main/webapp/app/exam/participate/exam-navigation-bar/exam-navigation-bar.component.ts +++ b/src/main/webapp/app/exam/participate/exam-navigation-bar/exam-navigation-bar.component.ts @@ -14,6 +14,8 @@ import { map } from 'rxjs/operators'; import { CodeEditorConflictStateService } from 'app/exercises/programming/shared/code-editor/service/code-editor-conflict-state.service'; import { ExamSession } from 'app/entities/exam-session.model'; import { faBars, faCheck, faEdit } from '@fortawesome/free-solid-svg-icons'; +import { ProgrammingSubmission } from 'app/entities/programming-submission.model'; +import { SubmissionVersion } from 'app/entities/submission-version.model'; @Component({ selector: 'jhi-exam-navigation-bar', @@ -27,7 +29,13 @@ export class ExamNavigationBarComponent implements OnInit { @Input() overviewPageOpen: boolean; @Input() examSessions?: ExamSession[] = []; @Input() examTimeLineView = false; - @Output() onPageChanged = new EventEmitter<{ overViewChange: boolean; exercise?: Exercise; forceSave: boolean }>(); + @Output() onPageChanged = new EventEmitter<{ + overViewChange: boolean; + exercise?: Exercise; + forceSave: boolean; + submission?: ProgrammingSubmission | SubmissionVersion; + initial?: boolean; + }>(); @Output() examAboutToEnd = new EventEmitter(); @Output() onExamHandInEarly = new EventEmitter(); @@ -112,7 +120,7 @@ export class ExamNavigationBarComponent implements OnInit { * @param exerciseIndex: index of the exercise to switch to, if it should not be used, you can pass -1 * @param forceSave: true if forceSave shall be used. */ - changePage(overviewPage: boolean, exerciseIndex: number, forceSave?: boolean): void { + changePage(overviewPage: boolean, exerciseIndex: number, forceSave?: boolean, submission?: SubmissionVersion | ProgrammingSubmission, initial?: boolean): void { if (!overviewPage) { // out of index -> do nothing if (exerciseIndex > this.exercises.length - 1 || exerciseIndex < 0) { @@ -120,7 +128,7 @@ export class ExamNavigationBarComponent implements OnInit { } // set index and emit event this.exerciseIndex = exerciseIndex; - this.onPageChanged.emit({ overViewChange: false, exercise: this.exercises[this.exerciseIndex], forceSave: !!forceSave }); + this.onPageChanged.emit({ overViewChange: false, exercise: this.exercises[this.exerciseIndex], forceSave: !!forceSave, submission: submission, initial: initial }); } else if (overviewPage) { // set index and emit event this.exerciseIndex = -1; diff --git a/src/main/webapp/app/exam/participate/exercises/exam-submission.component.ts b/src/main/webapp/app/exam/participate/exercises/exam-submission.component.ts index 9cd340b694aa..477bcbf2c6d9 100644 --- a/src/main/webapp/app/exam/participate/exercises/exam-submission.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/exam-submission.component.ts @@ -24,10 +24,12 @@ export abstract class ExamSubmissionComponent extends ExamPageComponent { * In case the submission has not been edited it is an empty submission. */ abstract updateViewFromSubmission(): void; + abstract updateViewFromSubmissionVersion(): void; abstract getSubmission(): Submission | undefined; abstract getExercise(): Exercise; @Input() readonly = false; - @Input() submissionVersion: SubmissionVersion; - @Input() submission: Submission; + submissionVersion: SubmissionVersion; + submission: Submission; + @Input() examTimeline = false; } diff --git a/src/main/webapp/app/exam/participate/exercises/file-upload/file-upload-exam-submission.component.ts b/src/main/webapp/app/exam/participate/exercises/file-upload/file-upload-exam-submission.component.ts index 4185d9aa010b..33f910856747 100644 --- a/src/main/webapp/app/exam/participate/exercises/file-upload/file-upload-exam-submission.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/file-upload/file-upload-exam-submission.component.ts @@ -166,4 +166,8 @@ export class FileUploadExamSubmissionComponent extends ExamSubmissionComponent i private onError() { this.alertService.error(this.translateService.instant('error.fileUploadSavingError')); } + + updateViewFromSubmissionVersion(): void { + // submission versions are not supported for file upload exercises + } } diff --git a/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts b/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts index cb903ea92832..cc61f6ff2f61 100644 --- a/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts @@ -25,8 +25,6 @@ export class ModelingExamSubmissionComponent extends ExamSubmissionComponent imp // IMPORTANT: this reference must be contained in this.studentParticipation.submissions[0] otherwise the parent component will not be able to react to changes @Input() studentSubmission: ModelingSubmission; - @Input() - examTimeline = false; @Input() exercise: ModelingExercise; @@ -45,6 +43,7 @@ export class ModelingExamSubmissionComponent extends ExamSubmissionComponent imp ngOnInit(): void { if (this.examTimeline) { + console.log('updateViewFromSubmissionVersion ngOninit'); this.updateViewFromSubmissionVersion(); } else { // show submission answers in UI @@ -124,15 +123,20 @@ export class ModelingExamSubmissionComponent extends ExamSubmissionComponent imp this.explanationText = explanation; } - private updateViewFromSubmissionVersion() { + updateViewFromSubmissionVersion() { + console.log('updateViewFromSubmissionVersion'); + console.log(this.submissionVersion); + if (this.submissionVersion) { if (this.submissionVersion.content) { + let model = this.submissionVersion.content.substring(0, this.submissionVersion.content.indexOf('; Explanation:')); + model = model.replace('Model:', '{"model":'); + model += '}'; + console.log(model); // Updates the Apollon editor model state (view) with the latest modeling submission - this.umlModel = JSON.parse(this.submissionVersion.content); + this.umlModel = JSON.parse(model); } - //TODO include explanation in submission version?? - // Updates explanation text with the latest submission - this.explanationText = this.studentSubmission.explanationText ?? ''; + this.explanationText = this.submissionVersion.content.substring(this.submissionVersion.content.indexOf('Explanation:') + 13) ?? ''; } } } diff --git a/src/main/webapp/app/exam/participate/exercises/programming/programming-exam-submission.component.ts b/src/main/webapp/app/exam/participate/exercises/programming/programming-exam-submission.component.ts index 57b1c864a47a..90f1f170bbb0 100644 --- a/src/main/webapp/app/exam/participate/exercises/programming/programming-exam-submission.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/programming/programming-exam-submission.component.ts @@ -130,4 +130,8 @@ export class ProgrammingExamSubmissionComponent extends ExamSubmissionComponent updateViewFromSubmission(): void { // do nothing - the code editor itself is taking care of updating the view from submission } + + updateViewFromSubmissionVersion(): void { + // do nothing - submission versions are not supported for programming exercises + } } diff --git a/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.ts b/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.ts index 6f526391daf2..fdbb332e4fa3 100644 --- a/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.ts @@ -254,4 +254,10 @@ export class QuizExamSubmissionComponent extends ExamSubmissionComponent impleme this.studentSubmission.submittedAnswers!.push(shortAnswerSubmittedAnswer); }, this); } + + updateViewFromSubmissionVersion(): void { + const quizSubmission = JSON.parse(this.submissionVersion.content); + this.studentSubmission.submittedAnswers = JSON.parse(quizSubmission); + this.updateViewFromSubmission(); + } } diff --git a/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.ts b/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.ts index 79aaf7e414be..836c788d4799 100644 --- a/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.ts @@ -48,8 +48,12 @@ export class TextExamSubmissionComponent extends ExamSubmissionComponent impleme } ngOnInit(): void { - // show submission answers in UI - this.updateViewFromSubmission(); + if (this.examTimeline) { + this.updateViewFromSubmissionVersion(); + } else { + // show submission answers in UI + this.updateViewFromSubmission(); + } } getExercise(): Exercise { @@ -104,4 +108,12 @@ export class TextExamSubmissionComponent extends ExamSubmissionComponent impleme this.studentSubmission.isSynced = false; this.textEditorInput.next((event.target).value); } + updateViewFromSubmissionVersion() { + if (this.submissionVersion) { + if (this.submissionVersion.content) { + // Updates the Apollon editor model state (view) with the latest modeling submission + this.answer = this.submissionVersion.content; + } + } + } } From aad9cdd7ec597069d54acc9dfb5428c8e827aef9 Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Sat, 8 Jul 2023 10:13:06 +0200 Subject: [PATCH 07/75] file upload --- .../student-exam-detail.component.html | 2 +- .../student-exam-timeline.component.html | 5 ++-- .../student-exam-timeline.component.ts | 29 ++++++++++++++----- .../file-upload-exam-submission.component.ts | 6 +++- .../modeling-exam-submission.component.ts | 5 ++-- .../quiz/quiz-exam-submission.component.ts | 5 +++- .../text/text-exam-submission.component.html | 2 +- .../text/text-exam-submission.component.scss | 3 ++ .../text/text-exam-submission.component.ts | 1 - src/main/webapp/i18n/de/exam.json | 5 +++- src/main/webapp/i18n/en/exam.json | 3 ++ 11 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/main/webapp/app/exam/manage/student-exams/student-exam-detail.component.html b/src/main/webapp/app/exam/manage/student-exams/student-exam-detail.component.html index 1cbc2a80af62..b3612ff2864e 100644 --- a/src/main/webapp/app/exam/manage/student-exams/student-exam-detail.component.html +++ b/src/main/webapp/app/exam/manage/student-exams/student-exam-detail.component.html @@ -33,7 +33,7 @@
Student
- + Timeline
diff --git a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html index d627de9defa5..078ef28fcecc 100644 --- a/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html +++ b/src/main/webapp/app/exam/manage/student-exams/student-exam-timeline/student-exam-timeline.component.html @@ -1,6 +1,4 @@ -

- {{ 'Klausurverlauf von ' + studentExam?.user?.login }} -

+

Exam timeline

; @ViewChild('examNavigationBar') examNavigationBarComponent: ExamNavigationBarComponent; @@ -73,6 +74,10 @@ export class StudentExamTimelineComponent implements OnInit, AfterViewInit { console.log('submitted version at ' + submissionVersion.createdDate); this.submissionVersions.push(submissionVersion); this.submissionTimeStamps.push(submissionVersion.createdDate); + } else if (this.isFileUploadSubmission(result)) { + const fileUploadSubmission = result as FileUploadSubmission; + this.fileUploadSubmissions.push(fileUploadSubmission); + this.submissionTimeStamps.push(fileUploadSubmission.submissionDate!); } else { const programmingSubmission = result as ProgrammingSubmission; console.log('programming submission at ' + programmingSubmission.submissionDate!); @@ -98,7 +103,7 @@ export class StudentExamTimelineComponent implements OnInit, AfterViewInit { } private isSubmissionVersion(object: SubmissionVersion | Submission | undefined) { - if (object === null) { + if (!object) { return false; } const submissionVersion = object as SubmissionVersion; @@ -108,15 +113,17 @@ export class StudentExamTimelineComponent implements OnInit, AfterViewInit { private retrieveSubmissionDataAndTimeStamps() { const submissionObservables: Observable[] = []; this.studentExam.exercises?.forEach((exercise) => { - if (exercise.type !== this.PROGRAMMING) { + if (exercise.type === this.PROGRAMMING) { + submissionObservables.push(this.submissionService.findAllSubmissionsOfParticipation(exercise.studentParticipations![0].id!).pipe(map(({ body }) => body!))); + } else if (exercise.type === this.FILEUPLOAD) { + submissionObservables.push(this.submissionService.findAllSubmissionsOfParticipation(exercise.studentParticipations![0].id!).pipe(map(({ body }) => body!))); + } else { submissionObservables.push( this.submissionService.findAllSubmissionVersionsOfSubmission(exercise.studentParticipations![0].submissions![0].id!).pipe( mergeMap((versions) => versions), toArray(), ), ); - } else { - submissionObservables.push(this.submissionService.findAllSubmissionsOfParticipation(exercise.studentParticipations![0].id!).pipe(map(({ body }) => body!))); } }); return merge(...submissionObservables); @@ -143,10 +150,10 @@ export class StudentExamTimelineComponent implements OnInit, AfterViewInit { if (activeComponent) { activeComponent.onDeactivate(); } - this.initializeExercise(exerciseChange.exercise!, exerciseChange.submission, exerciseChange.initial); + this.initializeExercise(exerciseChange.exercise!, exerciseChange.submission); } - initializeExercise(exercise: Exercise, submission: Submission | SubmissionVersion | undefined, initial?: boolean) { + initializeExercise(exercise: Exercise, submission: Submission | SubmissionVersion | undefined) { this.activeExamPage.exercise = exercise; // set current exercise Index this.exerciseIndex = this.studentExam.exercises!.findIndex((exercise1) => exercise1.id === exercise.id); @@ -236,4 +243,12 @@ export class StudentExamTimelineComponent implements OnInit, AfterViewInit { } return undefined; } + + private isFileUploadSubmission(object: FileUploadSubmission | SubmissionVersion | ProgrammingSubmission | undefined) { + if (!object) { + return false; + } + const fileUploadSubmission = object as FileUploadSubmission; + return !!fileUploadSubmission.id && fileUploadSubmission.submissionDate && fileUploadSubmission.filePath; + } } diff --git a/src/main/webapp/app/exam/participate/exercises/file-upload/file-upload-exam-submission.component.ts b/src/main/webapp/app/exam/participate/exercises/file-upload/file-upload-exam-submission.component.ts index 33f910856747..f541634b1f16 100644 --- a/src/main/webapp/app/exam/participate/exercises/file-upload/file-upload-exam-submission.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/file-upload/file-upload-exam-submission.component.ts @@ -130,13 +130,17 @@ export class FileUploadExamSubmissionComponent extends ExamSubmissionComponent i * Here the new filePath, which was received from the server, is used to display the name and type of the just uploaded file. */ updateViewFromSubmission(): void { - if (this.studentSubmission.isSynced && this.studentSubmission.filePath) { + console.log('updateViewFromSubmission'); + console.log(this.studentSubmission); + if ((this.studentSubmission.isSynced && this.studentSubmission.filePath) || (this.studentSubmission.filePath && this.examTimeline)) { // clear submitted file so that it is not displayed in the input (this might be confusing) this.submissionFile = undefined; const filePath = this.studentSubmission!.filePath!.split('/'); this.submittedFileName = filePath.last()!; const fileName = this.submittedFileName.split('.'); this.submittedFileExtension = fileName.last()!; + console.log(this.submittedFileName); + console.log(this.studentSubmission.filePath); } } diff --git a/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts b/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts index cc61f6ff2f61..7f74f05243d4 100644 --- a/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts @@ -130,11 +130,12 @@ export class ModelingExamSubmissionComponent extends ExamSubmissionComponent imp if (this.submissionVersion) { if (this.submissionVersion.content) { let model = this.submissionVersion.content.substring(0, this.submissionVersion.content.indexOf('; Explanation:')); - model = model.replace('Model:', '{"model":'); - model += '}'; + model = model.replace('Model: ', ''); console.log(model); // Updates the Apollon editor model state (view) with the latest modeling submission this.umlModel = JSON.parse(model); + this.modelingEditor.umlModel = this.umlModel; + this.changeDetectorReference.detectChanges(); } this.explanationText = this.submissionVersion.content.substring(this.submissionVersion.content.indexOf('Explanation:') + 13) ?? ''; } diff --git a/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.ts b/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.ts index fdbb332e4fa3..ee9132fe177d 100644 --- a/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.ts @@ -53,6 +53,7 @@ export class QuizExamSubmissionComponent extends ExamSubmissionComponent impleme selectedAnswerOptions = new Map(); dragAndDropMappings = new Map(); shortAnswerSubmittedTexts = new Map(); + constructor(private quizService: ArtemisQuizService, changeDetectorReference: ChangeDetectorRef) { super(changeDetectorReference); smoothscroll.polyfill(); @@ -76,7 +77,9 @@ export class QuizExamSubmissionComponent extends ExamSubmissionComponent impleme */ initQuiz() { // randomize order - this.quizService.randomizeOrder(this.exercise); + if (!this.examTimeline) { + this.quizService.randomizeOrder(this.exercise); + } // prepare selection arrays for each question this.selectedAnswerOptions = new Map(); this.dragAndDropMappings = new Map(); diff --git a/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.html b/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.html index 0b137eaaa6a4..088d58e0396e 100644 --- a/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.html +++ b/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.html @@ -22,7 +22,7 @@