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 a171a6a4dd69..96398770abdc 100644 --- a/src/main/webapp/app/exam/manage/exam-management.module.ts +++ b/src/main/webapp/app/exam/manage/exam-management.module.ts @@ -25,7 +25,7 @@ import { ArtemisMarkdownEditorModule } from 'app/shared/markdown-editor/markdown import { DurationPipe } from 'app/shared/pipes/artemis-duration.pipe'; import { StudentExamStatusComponent } from 'app/exam/manage/student-exams/student-exam-status/student-exam-status.component'; import { StudentExamSummaryComponent } from 'app/exam/manage/student-exams/student-exam-summary.component'; -import { ArtemisParticipationSummaryModule } from 'app/exam/participate/summary/exam-participation-summary.module'; +import { ArtemisParticipationSummaryModule } from 'app/exam/participate/summary/exam-result-summary.module'; import { ExamExerciseRowButtonsComponent } from 'app/exercises/shared/exam-exercise-row-buttons/exam-exercise-row-buttons.component'; import { ArtemisProgrammingExerciseStatusModule } from 'app/exercises/programming/manage/status/programming-exercise-status.module'; import { TestRunManagementComponent } from 'app/exam/manage/test-runs/test-run-management.component'; diff --git a/src/main/webapp/app/exam/participate/exam-cover/exam-participation-cover.component.html b/src/main/webapp/app/exam/participate/exam-cover/exam-participation-cover.component.html index 499d671098a9..172042596251 100644 --- a/src/main/webapp/app/exam/participate/exam-cover/exam-participation-cover.component.html +++ b/src/main/webapp/app/exam/participate/exam-cover/exam-participation-cover.component.html @@ -27,7 +27,7 @@


- +
{{ 'artemisApp.studentExam.submissionNotInTime' | artemisTranslate }} diff --git a/src/main/webapp/app/exam/participate/exam-participation.module.ts b/src/main/webapp/app/exam/participate/exam-participation.module.ts index 3ba594471fd3..e2b72942f5c7 100644 --- a/src/main/webapp/app/exam/participate/exam-participation.module.ts +++ b/src/main/webapp/app/exam/participate/exam-participation.module.ts @@ -22,7 +22,7 @@ import { ArtemisResultModule } from 'app/exercises/shared/result/result.module'; import { ArtemisProgrammingExerciseActionsModule } from 'app/exercises/programming/shared/actions/programming-exercise-actions.module'; import { ArtemisProgrammingExerciseInstructionsRenderModule } from 'app/exercises/programming/shared/instructions-render/programming-exercise-instructions-render.module'; import { ArtemisCoursesModule } from 'app/overview/courses.module'; -import { ArtemisParticipationSummaryModule } from 'app/exam/participate/summary/exam-participation-summary.module'; +import { ArtemisParticipationSummaryModule } from 'app/exam/participate/summary/exam-result-summary.module'; import { ExamTimerComponent } from './timer/exam-timer.component'; import { ArtemisExerciseButtonsModule } from 'app/overview/exercise-details/exercise-buttons.module'; import { ArtemisHeaderExercisePageWithDetailsModule } from 'app/exercises/shared/exercise-headers/exercise-headers.module'; diff --git a/src/main/webapp/app/exam/participate/general-information/exam-general-information.component.html b/src/main/webapp/app/exam/participate/general-information/exam-general-information.component.html new file mode 100644 index 000000000000..9dca2cfe0571 --- /dev/null +++ b/src/main/webapp/app/exam/participate/general-information/exam-general-information.component.html @@ -0,0 +1,98 @@ +
+

+ {{ 'artemisApp.exam.examSummary.generalInformation' | artemisTranslate }} +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'artemisApp.examManagement.testExam.examMode' | artemisTranslate }}: + {{ 'artemisApp.examManagement.testExam.testExam' | artemisTranslate }} +
{{ 'artemisApp.examManagement.moduleNumber' | artemisTranslate }}:{{ exam.moduleNumber }}
{{ 'artemisApp.exam.course' | artemisTranslate }}:{{ exam.courseName }}
{{ 'artemisApp.examManagement.examiner' | artemisTranslate }}:{{ exam.examiner }}
{{ 'artemisApp.exam.time' | artemisTranslate }}: + {{ exam.startDate | artemisDate: 'long-date' }} {{ exam.startDate | artemisDate: 'time' }}  - {{ examEndDate | artemisDate: 'long-date' }} + {{ examEndDate | artemisDate: 'time' }} +
{{ 'artemisApp.exam.date' | artemisTranslate }}:{{ exam.startDate | artemisDate: 'long-date' }}
{{ 'artemisApp.exam.workingTime' | artemisTranslate }}:{{ exam.startDate | artemisDate: 'time' }} - {{ examEndDate | artemisDate: 'time' }}
{{ 'artemisApp.exam.duration' | artemisTranslate }}:
{{ 'artemisApp.exam.points' | artemisTranslate }}:{{ exam.examMaxPoints }}
{{ 'artemisApp.exam.exercises' | artemisTranslate }}:{{ exam.numberOfExercisesInExam }}
{{ 'artemisApp.exam.examStudentReviewTimespan' | artemisTranslate }}:  + {{ exam.examStudentReviewStart | artemisDate }} +  -  + {{ exam.examStudentReviewEnd | artemisDate }} +
{{ 'artemisApp.exam.studentReviewEnabled' | artemisTranslate }}
+
{{ 'artemisApp.exam.examinedStudent' | artemisTranslate }}:{{ studentExam!.user!.name }}
{{ 'artemisApp.exam.date' | artemisTranslate }}:{{ currentDate! | artemisDate: 'long-date' }}
{{ 'artemisApp.exam.workingTime' | artemisTranslate }}:{{ exam.workingTime! | artemisDurationFromSeconds }}
{{ 'artemisApp.exam.overview.testExam.workingTimeCalculated' | artemisTranslate }} : + +
+
+
diff --git a/src/main/webapp/app/exam/participate/general-information/exam-general-information.component.scss b/src/main/webapp/app/exam/participate/general-information/exam-general-information.component.scss new file mode 100644 index 000000000000..da1940bc4929 --- /dev/null +++ b/src/main/webapp/app/exam/participate/general-information/exam-general-information.component.scss @@ -0,0 +1,9 @@ +table { + width: fit-content; +} + +th, +td { + text-align: start; + padding: 0.25em; +} diff --git a/src/main/webapp/app/exam/participate/information/exam-information.component.ts b/src/main/webapp/app/exam/participate/general-information/exam-general-information.component.ts similarity index 67% rename from src/main/webapp/app/exam/participate/information/exam-information.component.ts rename to src/main/webapp/app/exam/participate/general-information/exam-general-information.component.ts index 8057e1b504cf..60c32214d1c9 100644 --- a/src/main/webapp/app/exam/participate/information/exam-information.component.ts +++ b/src/main/webapp/app/exam/participate/general-information/exam-general-information.component.ts @@ -5,12 +5,18 @@ import { endTime, getAdditionalWorkingTime, isExamOverMultipleDays, normalWorkin import dayjs from 'dayjs/esm'; @Component({ - selector: 'jhi-exam-information', - templateUrl: './exam-information.component.html', + selector: 'jhi-exam-general-information', + styleUrls: ['./exam-general-information.component.scss'], + templateUrl: './exam-general-information.component.html', }) -export class ExamInformationComponent implements OnInit { +export class ExamGeneralInformationComponent implements OnInit { @Input() exam: Exam; @Input() studentExam: StudentExam; + @Input() reviewIsOpen?: boolean = false; + + /** The exam cover will contain e.g. the number of exercises which is hidden in the exam summary as + * the information is shown in the {@link ExamResultOverviewComponent} */ + @Input() displayOnExamCover?: boolean = false; examEndDate?: dayjs.Dayjs; normalWorkingTime?: number; diff --git a/src/main/webapp/app/exam/participate/information/exam-information.component.html b/src/main/webapp/app/exam/participate/information/exam-information.component.html deleted file mode 100644 index 9638b73f29ee..000000000000 --- a/src/main/webapp/app/exam/participate/information/exam-information.component.html +++ /dev/null @@ -1,88 +0,0 @@ -
-
- {{ 'artemisApp.examManagement.testExam.examMode' | artemisTranslate }}: -
{{ 'artemisApp.examManagement.testExam.realExam' | artemisTranslate }}
-
{{ 'artemisApp.examManagement.testExam.testExam' | artemisTranslate }}
-
- -
- - {{ 'artemisApp.exam.date' | artemisTranslate }}: - {{ currentDate! | artemisDate: 'long-date' }} - - - {{ 'artemisApp.exam.workingTime' | artemisTranslate }}: - {{ exam.workingTime! | artemisDurationFromSeconds }} - - - {{ 'artemisApp.exam.overview.testExam.workingTimeCalculated' | artemisTranslate }} : - - -
-
-
- - {{ 'artemisApp.exam.time' | artemisTranslate }}: - {{ exam.startDate | artemisDate: 'long-date' }} {{ exam.startDate | artemisDate: 'time' }} - - - {{ examEndDate | artemisDate: 'long-date' }} {{ examEndDate | artemisDate: 'time' }} - -
- - - {{ 'artemisApp.exam.date' | artemisTranslate }}: - {{ exam.startDate | artemisDate: 'long-date' }} - - - {{ 'artemisApp.exam.time' | artemisTranslate }}: - {{ exam.startDate | artemisDate: 'time' }} - - - {{ examEndDate | artemisDate: 'time' }} - - -
-
- - {{ 'artemisApp.exam.duration' | artemisTranslate }}: - - - - -
-
- - {{ 'artemisApp.exam.examStudentReviewStart' | artemisTranslate }}: - {{ exam.examStudentReviewStart | artemisDate }} - -
-
- - {{ 'artemisApp.exam.examStudentReviewEnd' | artemisTranslate }}: - {{ exam.examStudentReviewEnd | artemisDate }} - -
-
- - {{ 'artemisApp.examManagement.examiner' | artemisTranslate }}: - {{ exam.examiner }} - - - {{ 'artemisApp.examManagement.moduleNumber' | artemisTranslate }}: - {{ exam.moduleNumber }} - - - {{ 'artemisApp.exam.course' | artemisTranslate }}: - {{ exam.courseName }} - -
-
- - {{ 'artemisApp.exam.exercises' | artemisTranslate }}: - {{ exam.numberOfExercisesInExam }} - - - {{ 'artemisApp.exam.points' | artemisTranslate }}: - {{ exam.examMaxPoints }} - -
-
diff --git a/src/main/webapp/app/exam/participate/summary/exam-participation-summary.component.html b/src/main/webapp/app/exam/participate/summary/exam-result-summary.component.html similarity index 88% rename from src/main/webapp/app/exam/participate/summary/exam-participation-summary.component.html rename to src/main/webapp/app/exam/participate/summary/exam-result-summary.component.html index 8ecb12c0db03..29e19ac2acc7 100644 --- a/src/main/webapp/app/exam/participate/summary/exam-participation-summary.component.html +++ b/src/main/webapp/app/exam/participate/summary/exam-result-summary.component.html @@ -1,10 +1,7 @@

- - {{ 'artemisApp.exam.examSummary.yourSubmissionTo' | artemisTranslate: { examTitle: studentExam?.exam?.title, studentName: studentExam?.user?.name } }} - - - {{ 'artemisApp.exam.examSummary.studentSubmissionTo' | artemisTranslate: { examTitle: studentExam?.exam?.title, studentName: studentExam?.user?.name } }} + + {{ 'artemisApp.exam.examSummary.examResults' | artemisTranslate }}

- -
-
- -
-
-
- + +
+
@@ -91,7 +90,7 @@
{{ exercise?.exerciseGroup?.title ?? '-' }} - [{{ exercise.maxPoints }} {{ 'artemisApp.examParticipation.points' | artemisTranslate }}, {{ exercise.bonusPoints }} {{ 'artemisApp.examParticipation.bonus' | artemisTranslate }}] this.themeService.print()); } + private expandExercisesAndGradingKeysBeforePrinting() { + this.collapsedExerciseIds = []; + this.isGradingKeyCollapsed = false; + this.isBonusGradingKeyCollapsed = false; + } + public generateLink(exercise: Exercise) { if (exercise?.studentParticipations?.length) { return ['/courses', this.courseId, `${exercise.type}-exercises`, exercise.id, 'participate', exercise.studentParticipations[0].id]; @@ -248,4 +252,6 @@ export class ExamParticipationSummaryComponent implements OnInit { } return false; } + + protected readonly getIcon = getIcon; } diff --git a/src/main/webapp/app/exam/participate/summary/exam-participation-summary.module.ts b/src/main/webapp/app/exam/participate/summary/exam-result-summary.module.ts similarity index 83% rename from src/main/webapp/app/exam/participate/summary/exam-participation-summary.module.ts rename to src/main/webapp/app/exam/participate/summary/exam-result-summary.module.ts index fda2b8af3abe..9cf61c27e9df 100644 --- a/src/main/webapp/app/exam/participate/summary/exam-participation-summary.module.ts +++ b/src/main/webapp/app/exam/participate/summary/exam-result-summary.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { ExamParticipationSummaryComponent } from 'app/exam/participate/summary/exam-participation-summary.component'; +import { ExamResultSummaryComponent } from 'app/exam/participate/summary/exam-result-summary.component'; import { ProgrammingExamSummaryComponent } from 'app/exam/participate/summary/exercises/programming-exam-summary/programming-exam-summary.component'; import { ModelingExamSummaryComponent } from 'app/exam/participate/summary/exercises/modeling-exam-summary/modeling-exam-summary.component'; import { FileUploadExamSummaryComponent } from 'app/exam/participate/summary/exercises/file-upload-exam-summary/file-upload-exam-summary.component'; @@ -14,8 +14,8 @@ import { ArtemisFullscreenModule } from 'app/shared/fullscreen/fullscreen.module import { ArtemisResultModule } from 'app/exercises/shared/result/result.module'; import { ArtemisCoursesModule } from 'app/overview/courses.module'; import { ArtemisComplaintsModule } from 'app/complaints/complaints.module'; -import { ExamInformationComponent } from 'app/exam/participate/information/exam-information.component'; -import { ExamPointsSummaryComponent } from 'app/exam/participate/summary/points-summary/exam-points-summary.component'; +import { ExamGeneralInformationComponent } from 'app/exam/participate/general-information/exam-general-information.component'; +import { ExamResultOverviewComponent } from 'app/exam/participate/summary/result-overview/exam-result-overview.component'; import { ArtemisProgrammingExerciseInstructionsRenderModule } from 'app/exercises/programming/shared/instructions-render/programming-exercise-instructions-render.module'; import { TestRunRibbonComponent } from 'app/exam/manage/test-runs/test-run-ribbon.component'; import { ArtemisHeaderExercisePageWithDetailsModule } from 'app/exercises/shared/exercise-headers/exercise-headers.module'; @@ -25,6 +25,7 @@ import { ArtemisExamSharedModule } from 'app/exam/shared/exam-shared.module'; import { ExampleSolutionComponent } from 'app/exercises/shared/example-solution/example-solution.component'; import { ArtemisProgrammingExerciseManagementModule } from 'app/exercises/programming/manage/programming-exercise-management.module'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { GradingKeyOverviewModule } from 'app/grading-system/grading-key-overview/grading-key-overview.module'; @NgModule({ imports: [ @@ -45,19 +46,20 @@ import { ArtemisSharedComponentModule } from 'app/shared/components/shared-compo SubmissionResultStatusModule, ArtemisExamSharedModule, ArtemisSharedComponentModule, + GradingKeyOverviewModule, ], declarations: [ - ExamParticipationSummaryComponent, + ExamResultSummaryComponent, ProgrammingExamSummaryComponent, ModelingExamSummaryComponent, FileUploadExamSummaryComponent, TextExamSummaryComponent, QuizExamSummaryComponent, - ExamInformationComponent, - ExamPointsSummaryComponent, + ExamGeneralInformationComponent, + ExamResultOverviewComponent, TestRunRibbonComponent, ExampleSolutionComponent, ], - exports: [ExamParticipationSummaryComponent, ExamInformationComponent, TestRunRibbonComponent], + exports: [ExamResultSummaryComponent, ExamGeneralInformationComponent, TestRunRibbonComponent], }) export class ArtemisParticipationSummaryModule {} diff --git a/src/main/webapp/app/exam/participate/summary/points-summary/exam-points-summary.component.html b/src/main/webapp/app/exam/participate/summary/points-summary/exam-points-summary.component.html deleted file mode 100644 index d240e4003bcf..000000000000 --- a/src/main/webapp/app/exam/participate/summary/points-summary/exam-points-summary.component.html +++ /dev/null @@ -1,136 +0,0 @@ -
-

{{ 'artemisApp.exam.examSummary.points.overview' | artemisTranslate }}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#{{ 'artemisApp.exam.examSummary.points.exercise' | artemisTranslate }}{{ 'artemisApp.exercise.includedInOverallScore' | artemisTranslate }}{{ 'artemisApp.exam.examSummary.points.yourPoints' | artemisTranslate }}{{ 'artemisApp.exam.examSummary.points.maxPoints' | artemisTranslate }}{{ 'artemisApp.exam.examSummary.points.maxBonus' | artemisTranslate }}
{{ i + 1 }} - {{ exercise?.exerciseGroup?.title ?? '-' }} - {{ exerciseService.isIncludedInScore(exercise) }} - {{ getAchievedPoints(exercise) }} - - {{ exercise.includedInOverallScore === IncludedInOverallScore.INCLUDED_AS_BONUS ? 0 : exercise.maxPoints }} - - {{ - exercise.includedInOverallScore === IncludedInOverallScore.INCLUDED_AS_BONUS ? exercise.maxPoints : exercise.bonusPoints - }} -
{{ 'artemisApp.exam.examSummary.points.total' | artemisTranslate }}- - {{ getAchievedPointsSum() }} - - {{ getMaxNormalPointsSum() }} - - {{ getMaxBonusPointsSum() }} -
-
- {{ - 'artemisApp.exam.examSummary.points.youAchievedWithBonus' - | artemisTranslate - : { - achievedPoints: getAchievedPointsSum(), - normalPoints: getMaxNormalPointsSum() - } - }} -
-
- {{ - 'artemisApp.exam.examSummary.points.youAchieved' - | artemisTranslate - : { - achievedPoints: getAchievedPointsSum(), - normalPoints: getMaxNormalPointsSum() - } - }} -
-
- {{ - 'artemisApp.exam.examSummary.points.youAchievedFromBonus.' + studentExamWithGrade.studentResult.gradeWithBonus.bonusStrategy - | artemisTranslate - : { - achievedBonus: studentExamWithGrade.studentResult.gradeWithBonus.bonusGrade, - bonusFromTitle: studentExamWithGrade.studentResult.gradeWithBonus.bonusFromTitle - } - }} -
-
- {{ - 'artemisApp.exam.examSummary.points.youAchievedPointsAfterBonus' - | artemisTranslate - : { - finalPoints: studentExamWithGrade.studentResult.gradeWithBonus?.finalPoints - } - }} -
-
- - - - - - - - - - - - - - -
{{ 'artemisApp.exam.examSummary.' + (studentExamWithGrade.studentResult.gradeWithBonus != undefined ? 'gradeBeforeBonus' : 'grade') | artemisTranslate }}: - {{ grade }} -
{{ 'artemisApp.exam.examSummary.bonus' | artemisTranslate }}:{{ grade }}
{{ 'artemisApp.exam.examSummary.gradeAfterBonus' | artemisTranslate }}: - {{ studentExamWithGrade.studentResult.gradeWithBonus.finalGrade }} -
- - - -  {{ - isBonus ? ('artemisApp.exam.examSummary.gradeKeyButtonBonus' | artemisTranslate) : ('artemisApp.exam.examSummary.gradeKeyButton' | artemisTranslate) - }} - - - - - -  {{ 'artemisApp.exam.examSummary.bonusGradeKeyButton' | artemisTranslate }} - - -
-
diff --git a/src/main/webapp/app/exam/participate/summary/points-summary/exam-points-summary.component.scss b/src/main/webapp/app/exam/participate/summary/points-summary/exam-points-summary.component.scss deleted file mode 100644 index 04135c7fc31a..000000000000 --- a/src/main/webapp/app/exam/participate/summary/points-summary/exam-points-summary.component.scss +++ /dev/null @@ -1,15 +0,0 @@ -.passing-grade { - background-color: #28a745; - color: white; - padding: 0.25em 0.5em; -} - -.failing-grade { - background-color: #ca2024; - color: white; - padding: 0.25em 0.5em; -} - -.total-footer { - border-top-width: 2px; -} diff --git a/src/main/webapp/app/exam/participate/summary/points-summary/exam-points-summary.component.ts b/src/main/webapp/app/exam/participate/summary/points-summary/exam-points-summary.component.ts deleted file mode 100644 index 625e23d0d264..000000000000 --- a/src/main/webapp/app/exam/participate/summary/points-summary/exam-points-summary.component.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; -import dayjs from 'dayjs/esm'; -import { Exercise, IncludedInOverallScore } from 'app/entities/exercise.model'; -import { ArtemisServerDateService } from 'app/shared/server-date.service'; -import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; -import { GradeType } from 'app/entities/grading-scale.model'; -import { faAward, faClipboard } from '@fortawesome/free-solid-svg-icons'; -import { StudentExamWithGradeDTO } from 'app/exam/exam-scores/exam-score-dtos.model'; -import { BonusStrategy } from 'app/entities/bonus.model'; - -@Component({ - selector: 'jhi-exam-points-summary', - styleUrls: ['./exam-points-summary.component.scss'], - templateUrl: './exam-points-summary.component.html', -}) -export class ExamPointsSummaryComponent implements OnInit { - readonly IncludedInOverallScore = IncludedInOverallScore; - readonly BonusStrategy = BonusStrategy; - @Input() studentExamWithGrade: StudentExamWithGradeDTO; - - gradingScaleExists = false; - isBonus = false; - hasPassed = false; - grade?: string; - - // Icons - faClipboard = faClipboard; - faAward = faAward; - - constructor( - private serverDateService: ArtemisServerDateService, - public exerciseService: ExerciseService, - private changeDetector: ChangeDetectorRef, - ) {} - - ngOnInit() { - if (this.isExamResultPublished()) { - this.setExamGrade(); - } - } - - /** - * The points summary table will only be shown if: - * - exam.publishResultsDate is set - * - we are after the exam.publishResultsDate - * - at least one exercise has a result - */ - show(): boolean { - return !!(this.isExamResultPublished() && this.hasAtLeastOneResult()); - } - - private isExamResultPublished() { - const exam = this.studentExamWithGrade?.studentExam?.exam; - return exam && exam.publishResultsDate && dayjs(exam.publishResultsDate).isBefore(this.serverDateService.now()); - } - - /** - * Sets the student's exam grade if a grading scale exists for the exam - */ - setExamGrade() { - if (this.studentExamWithGrade?.studentResult?.overallGrade != undefined) { - this.gradingScaleExists = true; - this.grade = this.studentExamWithGrade.studentResult.overallGrade; - this.isBonus = this.studentExamWithGrade.gradeType === GradeType.BONUS; - this.hasPassed = !!this.studentExamWithGrade.studentResult.hasPassed; - this.changeDetector.detectChanges(); - } - } - - getAchievedPointsSum() { - return this.studentExamWithGrade?.studentResult.overallPointsAchieved ?? 0; - } - - /** - * Returns the max. achievable (normal) points. It is possible to exceed this value if there are bonus points. - */ - getMaxNormalPointsSum() { - return this.studentExamWithGrade?.maxPoints ?? 0; - } - - getAchievedPoints(exercise: Exercise): number { - return this.studentExamWithGrade?.achievedPointsPerExercise?.[exercise.id!] ?? 0; - } - - /** - * Returns the max. achievable bonusPoints. - */ - getMaxBonusPointsSum(): number { - return this.studentExamWithGrade?.maxBonusPoints ?? 0; - } - - /** - * Returns the sum of max. achievable normal and bonus points. It is not possible to exceed this value. - */ - getMaxNormalAndBonusPointsSum(): number { - return this.getMaxNormalPointsSum() + this.getMaxBonusPointsSum(); - } - - private hasAtLeastOneResult(): boolean { - const exercises = this.studentExamWithGrade?.studentExam?.exercises; - // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - if (exercises?.length! > 0) { - // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - return exercises!.some((exercise) => exercise.studentParticipations?.[0]?.results?.length! > 0); - } - return false; - } -} diff --git a/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.html b/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.html new file mode 100644 index 000000000000..d79fca5055d6 --- /dev/null +++ b/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.html @@ -0,0 +1,189 @@ +
+

+ {{ 'artemisApp.exam.examSummary.points.overview' | artemisTranslate }} +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#{{ 'artemisApp.exam.examSummary.points.exercise' | artemisTranslate }}{{ 'artemisApp.exercise.includedInOverallScore' | artemisTranslate }}{{ 'artemisApp.exam.examSummary.points.yourPoints' | artemisTranslate }}{{ 'artemisApp.exam.examSummary.points.maxPoints' | artemisTranslate }}{{ 'artemisApp.exam.examSummary.points.achievedPercentage' | artemisTranslate }}{{ 'artemisApp.exam.examSummary.points.maxBonus' | artemisTranslate }}
{{ exerciseIndex + 1 }} +   +
+ +
+   + {{ exercise?.exerciseGroup?.title ?? '-' }} +
{{ exerciseService.isIncludedInScore(exercise) }} + {{ studentExamWithGrade?.achievedPointsPerExercise?.[exercise.id!] ?? 0 }} + + {{ + exercise.includedInOverallScore === IncludedInOverallScore.INCLUDED_AS_BONUS ? 0 : exercise.maxPoints + }} + + + {{ exerciseInfos[exercise.id!].achievedPercentage }}% + + - + + {{ + exercise.includedInOverallScore === IncludedInOverallScore.INCLUDED_AS_BONUS ? exercise.maxPoints : exercise.bonusPoints + }} +
{{ 'artemisApp.exam.examSummary.points.total' | artemisTranslate }}- + {{ overallAchievedPoints }} + + {{ maxPoints }} + + + {{ studentExamWithGrade.studentResult.overallScoreAchieved }}% + + + - + + + {{ studentExamWithGrade?.maxBonusPoints }} +
+
+
+
+ {{ + 'artemisApp.exam.examSummary.points.youAchievedWithBonus' + | artemisTranslate + : { + achievedPoints: overallAchievedPoints, + normalPoints: maxPoints + } + }} +
+
+ {{ + 'artemisApp.exam.examSummary.points.youAchieved' + | artemisTranslate + : { + achievedPoints: overallAchievedPoints, + normalPoints: maxPoints + } + }} +
+
+ {{ + 'artemisApp.exam.examSummary.points.youAchievedFromBonus.' + studentExamWithGrade.studentResult.gradeWithBonus.bonusStrategy + | artemisTranslate + : { + achievedBonus: studentExamWithGrade.studentResult.gradeWithBonus.bonusGrade, + bonusFromTitle: studentExamWithGrade.studentResult.gradeWithBonus.bonusFromTitle + } + }} +
+
+ +
+ + + + + + + + + + + + + +
+ {{ + 'artemisApp.exam.examSummary.' + (studentExamWithGrade.studentResult.gradeWithBonus != undefined ? 'gradeBeforeBonus' : 'grade') | artemisTranslate + }}: + + {{ grade }} +
{{ 'artemisApp.exam.examSummary.bonus' | artemisTranslate }}:{{ grade }}
{{ 'artemisApp.exam.examSummary.gradeAfterBonus' | artemisTranslate }}: + {{ studentExamWithGrade.studentResult.gradeWithBonus.finalGrade }} +
+
+
+ +
+
+
+ + +
+ +
+
+ +
+ +
+ +
+
+
+
+
+
diff --git a/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.scss b/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.scss new file mode 100644 index 000000000000..251508ae9922 --- /dev/null +++ b/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.scss @@ -0,0 +1,58 @@ +#result-overview-table tr:not(thead tr):not(tfoot tr) { + cursor: pointer; + + &:hover { + transform: scale(1.01); + transition: transform 0.1s; + } +} + +.not-included-in-score { + opacity: 0.5; +} + +.grade { + color: var(--white); + padding: 0.25em 0.5em; + margin-left: 0.25em; + border-radius: 0.25em; +} + +.passing-grade { + background-color: var(--success); +} + +.failing-grade { + background-color: var(--danger); +} + +.total-footer { + border-top-width: 2px; +} + +.chevron-position { + display: inline-block; + vertical-align: middle; +} + +.rotate-icon { + transition: transform 0.3s ease; +} + +.rotate-icon.rotated { + transform: rotate(90deg); +} + +.icon-container { + display: inline-block; + vertical-align: middle; + horiz-align: center; +} + +.icon { + display: flex; + justify-content: center; + align-items: center; + width: 24px; + height: 24px; +} diff --git a/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.ts b/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.ts new file mode 100644 index 000000000000..fd776d6e92e5 --- /dev/null +++ b/src/main/webapp/app/exam/participate/summary/result-overview/exam-result-overview.component.ts @@ -0,0 +1,236 @@ +import { ChangeDetectorRef, Component, Input, OnChanges, OnInit } from '@angular/core'; +import dayjs from 'dayjs/esm'; +import { Exercise, IncludedInOverallScore, getIcon } from 'app/entities/exercise.model'; +import { ArtemisServerDateService } from 'app/shared/server-date.service'; +import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; +import { GradeType } from 'app/entities/grading-scale.model'; +import { faAward, faClipboard } from '@fortawesome/free-solid-svg-icons'; +import { ExerciseResult, StudentExamWithGradeDTO } from 'app/exam/exam-scores/exam-score-dtos.model'; +import { BonusStrategy } from 'app/entities/bonus.model'; +import { evaluateTemplateStatus, getTextColorClass } from 'app/exercises/shared/result/result.utils'; +import { faChevronRight } from '@fortawesome/free-solid-svg-icons'; +import { roundScorePercentSpecifiedByCourseSettings } from 'app/shared/util/utils'; +import { getLatestResultOfStudentParticipation } from 'app/exercises/shared/participation/participation.utils'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; + +type ExerciseInfo = { + icon: IconProp; + achievedPercentage?: number; + colorClass?: string; +}; + +@Component({ + selector: 'jhi-exam-result-overview', + styleUrls: ['./exam-result-overview.component.scss'], + templateUrl: './exam-result-overview.component.html', +}) +export class ExamResultOverviewComponent implements OnInit, OnChanges { + readonly IncludedInOverallScore = IncludedInOverallScore; + readonly BonusStrategy = BonusStrategy; + + @Input() studentExamWithGrade: StudentExamWithGradeDTO; + @Input() isGradingKeyCollapsed: boolean = true; + @Input() isBonusGradingKeyCollapsed: boolean = true; + + gradingScaleExists = false; + isBonus = false; + hasPassed = false; + grade?: string; + + // Icons + faClipboard = faClipboard; + faAward = faAward; + faChevronRight = faChevronRight; + + showIncludedInScoreColumn = false; + /** + * the max. achievable (normal) points. It is possible to exceed this value if there are bonus points. + */ + maxPoints = 0; + overallAchievedPoints = 0; + isBonusGradingKeyDisplayed = false; + + exerciseInfos: Record; + + /** + * The points summary table will only be shown if: + * - exam.publishResultsDate is set + * - we are after the exam.publishResultsDate + * - at least one exercise has a result + */ + showResultOverview = false; + + constructor( + private serverDateService: ArtemisServerDateService, + public exerciseService: ExerciseService, + private changeDetector: ChangeDetectorRef, + ) {} + + ngOnInit() { + if (this.isExamResultPublished()) { + this.setExamGrade(); + } + + this.updateLocalVariables(); + } + + ngOnChanges() { + this.updateLocalVariables(); + } + + private updateLocalVariables() { + this.showResultOverview = !!(this.isExamResultPublished() && this.hasAtLeastOneResult()); + this.showIncludedInScoreColumn = this.containsExerciseThatIsNotIncludedCompletely(); + this.maxPoints = this.studentExamWithGrade?.maxPoints ?? 0; + this.overallAchievedPoints = this.studentExamWithGrade?.studentResult.overallPointsAchieved ?? 0; + this.isBonusGradingKeyDisplayed = this.studentExamWithGrade.studentResult.gradeWithBonus?.bonusGrade != undefined; + + this.exerciseInfos = this.getExerciseInfos(); + } + + private getExerciseInfos() { + const exerciseInfos: Record = {}; + for (const exercise of this.studentExamWithGrade?.studentExam?.exercises ?? []) { + if (exercise.id === undefined) { + console.error('Exercise id is undefined', exercise); + continue; + } + exerciseInfos[exercise.id] = { + icon: getIcon(exercise.type), + achievedPercentage: this.getAchievedPercentageByExerciseId(exercise.id), + colorClass: this.getTextColorClassByExercise(exercise), + }; + } + return exerciseInfos; + } + + /** + * If all exercises are included in the overall score, we do not need to show the column + * -> displayed if at least one exercise is not included in the overall score + */ + containsExerciseThatIsNotIncludedCompletely(): boolean { + for (const exercise of this.studentExamWithGrade?.studentExam?.exercises ?? []) { + if (exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY) { + return true; + } + } + + return false; + } + + private isExamResultPublished() { + const exam = this.studentExamWithGrade?.studentExam?.exam; + return exam && exam.publishResultsDate && dayjs(exam.publishResultsDate).isBefore(this.serverDateService.now()); + } + + /** + * Sets the student's exam grade if a grading scale exists for the exam + */ + setExamGrade() { + if (this.studentExamWithGrade?.studentResult?.overallGrade != undefined) { + this.gradingScaleExists = true; + this.grade = this.studentExamWithGrade.studentResult.overallGrade; + this.isBonus = this.studentExamWithGrade.gradeType === GradeType.BONUS; + this.hasPassed = !!this.studentExamWithGrade.studentResult.hasPassed; + this.changeDetector.detectChanges(); + } + } + + /** + * Returns the sum of max. achievable normal and bonus points. It is not possible to exceed this value. + */ + getMaxNormalAndBonusPointsSum(): number { + const maxAchievableBonusPoints = this.studentExamWithGrade?.maxBonusPoints ?? 0; + return this.maxPoints + maxAchievableBonusPoints; + } + + private getExerciseResultByExerciseId(exerciseId?: number): ExerciseResult | undefined { + if (exerciseId === undefined) { + return undefined; + } + + const exerciseGroupResultMapping = this.studentExamWithGrade?.studentResult?.exerciseGroupIdToExerciseResult; + let exerciseResult = undefined; + + for (const key in exerciseGroupResultMapping) { + if (key in exerciseGroupResultMapping && exerciseGroupResultMapping[key].exerciseId === exerciseId) { + exerciseResult = exerciseGroupResultMapping[key]; + break; + } + } + + return exerciseResult; + } + + getAchievedPercentageByExerciseId(exerciseId?: number): number | undefined { + const result = this.getExerciseResultByExerciseId(exerciseId); + if (result === undefined) { + return undefined; + } + + const course = this.studentExamWithGrade.studentExam?.exam?.course; + + if (result.achievedScore !== undefined) { + return roundScorePercentSpecifiedByCourseSettings(result.achievedScore / 100, course); + } + + const canCalculatePercentage = result.maxScore && result.achievedPoints !== undefined; + if (canCalculatePercentage) { + return roundScorePercentSpecifiedByCourseSettings(result.achievedPoints! / result.maxScore, course); + } + + return undefined; + } + + scrollToExercise(exerciseId?: number) { + if (exerciseId === undefined) { + return; + } + + const searchedId = `exercise-${exerciseId}`; + const targetElement = document.getElementById(searchedId); + + if (targetElement) { + targetElement.scrollIntoView({ + behavior: 'smooth', + block: 'start', + inline: 'nearest', + }); + } else { + console.error(`Could not find corresponding exercise with id "${searchedId}"`); + } + } + + private hasAtLeastOneResult(): boolean { + const exercises = this.studentExamWithGrade?.studentExam?.exercises; + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain + if (exercises?.length! > 0) { + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain + return exercises!.some((exercise) => exercise.studentParticipations?.[0]?.results?.length! > 0); + } + return false; + } + + getTextColorClassByExercise(exercise: Exercise) { + const participation = exercise.studentParticipations![0]; + const showUngradedResults = false; + const result = getLatestResultOfStudentParticipation(participation, showUngradedResults); + + const isBuilding = false; + const templateStatus = evaluateTemplateStatus(exercise, participation, result, isBuilding); + + return getTextColorClass(result, templateStatus); + } + + toggleGradingKey(): void { + this.isGradingKeyCollapsed = !this.isGradingKeyCollapsed; + } + + toggleBonusGradingKey(): void { + this.isBonusGradingKeyCollapsed = !this.isBonusGradingKeyCollapsed; + } + + protected readonly getIcon = getIcon; + protected readonly getTextColorClass = getTextColorClass; + protected readonly evaluateTemplateStatus = evaluateTemplateStatus; +} diff --git a/src/main/webapp/app/exercises/shared/participation/participation.utils.ts b/src/main/webapp/app/exercises/shared/participation/participation.utils.ts index b2cf18ea7dec..a55c805c1979 100644 --- a/src/main/webapp/app/exercises/shared/participation/participation.utils.ts +++ b/src/main/webapp/app/exercises/shared/participation/participation.utils.ts @@ -6,6 +6,9 @@ import { Exercise, ExerciseType } from 'app/entities/exercise.model'; import { getExerciseDueDate } from 'app/exercises/shared/exercise/exercise.utils'; import { SimpleChanges } from '@angular/core'; import dayjs from 'dayjs/esm'; +import { StudentParticipation } from 'app/entities/participation/student-participation.model'; +import { Result } from 'app/entities/result.model'; +import { orderBy as _orderBy } from 'lodash-es'; export const setBuildPlanUrlForProgrammingParticipations = (profileInfo: ProfileInfo, participations: ProgrammingExerciseStudentParticipation[], projectKey?: string) => { if (!projectKey) { @@ -105,3 +108,20 @@ export const isParticipationInDueTime = (participation: Participation, exercise: // If the submission has no submissionDate set, the submission cannot be in time. return false; }; + +/** + * Returns the latest result of a given student participation. + * + * @param participation + * @param showUngradedResults + */ +export function getLatestResultOfStudentParticipation(participation: StudentParticipation, showUngradedResults: boolean): Result | undefined { + // Sort participation results by completionDate desc. + if (participation.results) { + participation.results = _orderBy(participation.results, 'completionDate', 'desc'); + } + // The latest result is the first rated result in the sorted array (=newest) or any result if the option is active to show ungraded results. + const latestResult = participation.results?.find(({ rated }) => showUngradedResults || rated === true); + // Make sure that the participation result is connected to the newest result. + return latestResult ? { ...latestResult, participation: participation } : undefined; +} diff --git a/src/main/webapp/app/exercises/shared/result/updating-result.component.ts b/src/main/webapp/app/exercises/shared/result/updating-result.component.ts index dce6b52af890..aa73df8d8eee 100644 --- a/src/main/webapp/app/exercises/shared/result/updating-result.component.ts +++ b/src/main/webapp/app/exercises/shared/result/updating-result.component.ts @@ -1,5 +1,4 @@ import { Component, EventEmitter, Input, OnChanges, OnDestroy, Output, SimpleChanges } from '@angular/core'; -import { orderBy as _orderBy } from 'lodash-es'; import { Subscription } from 'rxjs'; import { filter, map, tap } from 'rxjs/operators'; import { ParticipationWebsocketService } from 'app/overview/participation-websocket.service'; @@ -13,7 +12,7 @@ import { Submission, SubmissionType } from 'app/entities/submission.model'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; import { Result } from 'app/entities/result.model'; import { getExerciseDueDate } from 'app/exercises/shared/exercise/exercise.utils'; -import { hasParticipationChanged } from 'app/exercises/shared/participation/participation.utils'; +import { getLatestResultOfStudentParticipation, hasParticipationChanged } from 'app/exercises/shared/participation/participation.utils'; import { MissingResultInformation } from 'app/exercises/shared/result/result.utils'; import { convertDateFromServer } from 'app/utils/date.utils'; @@ -58,14 +57,7 @@ export class UpdatingResultComponent implements OnChanges, OnDestroy { */ ngOnChanges(changes: SimpleChanges) { if (hasParticipationChanged(changes)) { - // Sort participation results by completionDate desc. - if (this.participation.results) { - this.participation.results = _orderBy(this.participation.results, 'completionDate', 'desc'); - } - // The latest result is the first rated result in the sorted array (=newest) or any result if the option is active to show ungraded results. - const latestResult = this.participation.results?.find(({ rated }) => this.showUngradedResults || rated === true); - // Make sure that the participation result is connected to the newest result. - this.result = latestResult ? { ...latestResult, participation: this.participation } : undefined; + this.result = getLatestResultOfStudentParticipation(this.participation, this.showUngradedResults); this.missingResultInfo = MissingResultInformation.NONE; this.subscribeForNewResults(); diff --git a/src/main/webapp/app/grading-system/grading-key-overview/grading-key-helper.ts b/src/main/webapp/app/grading-system/grading-key-overview/grading-key-helper.ts new file mode 100644 index 000000000000..aadaad58112e --- /dev/null +++ b/src/main/webapp/app/grading-system/grading-key-overview/grading-key-helper.ts @@ -0,0 +1,36 @@ +import { findParamInRouteHierarchy } from 'app/utils/navigation.utils'; +import { ActivatedRoute } from '@angular/router'; + +export type GradingKeyUrlParams = { + courseId: number; + examId?: number; + isExam: boolean; + forBonus: boolean; + studentGradeOrBonusPointsOrGradeBonus?: string; +}; + +export function loadGradingKeyUrlParams(route: ActivatedRoute): GradingKeyUrlParams { + // Note: This component is used in multiple routes, so it can be lazy loaded. Also, courseId and examId can be + // found on different levels of hierarchy tree (on the same level or a parent or a grandparent, etc.). + const courseId = Number(findParamInRouteHierarchy(route, 'courseId')); + let examId = undefined; + let isExam = false; + + const examIdParam = findParamInRouteHierarchy(route, 'examId'); + if (examIdParam) { + examId = Number(examIdParam); + isExam = true; + } + const forBonus = !!route.snapshot.data['forBonus']; + + /** If needed queryParam is available, it is available on {@link GradingKeyOverviewComponent} so no need to traverse the hierarchy like params above. */ + const studentGradeOrBonusPointsOrGradeBonus = route.snapshot.queryParams['grade']; + + return { + courseId, + examId, + forBonus, + isExam, + studentGradeOrBonusPointsOrGradeBonus: studentGradeOrBonusPointsOrGradeBonus, + }; +} diff --git a/src/main/webapp/app/grading-system/grading-key-overview/grading-key-overview.component.html b/src/main/webapp/app/grading-system/grading-key-overview/grading-key-overview.component.html index 9b9d2bbd47f9..cca50757cf16 100644 --- a/src/main/webapp/app/grading-system/grading-key-overview/grading-key-overview.component.html +++ b/src/main/webapp/app/grading-system/grading-key-overview/grading-key-overview.component.html @@ -5,56 +5,13 @@

{{ title }}

{{ 'artemisApp.gradingSystem.overview.info' | artemisTranslate }}
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ isBonus ? ('artemisApp.exam.examSummary.bonus' | artemisTranslate) : ('artemisApp.exam.examSummary.grade' | artemisTranslate) }}{{ 'artemisApp.exam.examSummary.interval' | artemisTranslate }}{{ 'artemisApp.exam.examSummary.intervalPoints' | artemisTranslate }}
{{ plagiarismGrade }}
{{ noParticipationGrade }}
{{ gradeStep.gradeName }} - - - -
-
-
-
{{ 'artemisApp.gradingSystem.overview.intervals.title' | artemisTranslate }}
-
    -
  • [a, b): {{ 'artemisApp.gradingSystem.overview.intervals.leftInclusiveRightExclusive' | artemisTranslate }}
  • -
  • (a, b]: {{ 'artemisApp.gradingSystem.overview.intervals.leftExclusiveRightInclusive' | artemisTranslate }}
  • -
  • [a, b]: {{ 'artemisApp.gradingSystem.overview.intervals.bothInclusive' | artemisTranslate }}
  • -
-
-
- - -
+ + +
diff --git a/src/main/webapp/app/grading-system/grading-key-overview/grading-key-overview.component.ts b/src/main/webapp/app/grading-system/grading-key-overview/grading-key-overview.component.ts index 77cd4f5aa702..37f9e5ecd13d 100644 --- a/src/main/webapp/app/grading-system/grading-key-overview/grading-key-overview.component.ts +++ b/src/main/webapp/app/grading-system/grading-key-overview/grading-key-overview.component.ts @@ -1,18 +1,11 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { GradingSystemService } from 'app/grading-system/grading-system.service'; -import { GradeStep, GradeStepsDTO } from 'app/entities/grade-step.model'; -import { GradeType, GradingScale } from 'app/entities/grading-scale.model'; -import { ArtemisNavigationUtilService, findParamInRouteHierarchy } from 'app/utils/navigation.utils'; +import { GradeStep } from 'app/entities/grade-step.model'; +import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { faChevronLeft, faPrint } from '@fortawesome/free-solid-svg-icons'; -import { GradeStepBoundsPipe } from 'app/shared/pipes/grade-step-bounds.pipe'; -import { GradeEditMode } from 'app/grading-system/base-grading-system/base-grading-system.component'; import { ThemeService } from 'app/core/theme/theme.service'; -import { BonusService } from 'app/grading-system/bonus/bonus.service'; -import { map } from 'rxjs/operators'; -import { Observable } from 'rxjs'; -import { ScoresStorageService } from 'app/course/course-scores/scores-storage.service'; -import { ScoreType } from 'app/shared/constants/score-type.constants'; +import { loadGradingKeyUrlParams } from 'app/grading-system/grading-key-overview/grading-key-helper'; @Component({ selector: 'jhi-grade-key-overview', @@ -20,20 +13,15 @@ import { ScoreType } from 'app/shared/constants/score-type.constants'; styleUrls: ['./grading-key-overview.scss'], }) export class GradingKeyOverviewComponent implements OnInit { - // Icons readonly faChevronLeft = faChevronLeft; readonly faPrint = faPrint; - readonly GradeEditMode = GradeEditMode; - plagiarismGrade: string; noParticipationGrade: string; constructor( private route: ActivatedRoute, private gradingSystemService: GradingSystemService, - private bonusService: BonusService, - private scoresStorageService: ScoresStorageService, private navigationUtilService: ArtemisNavigationUtilService, private themeService: ThemeService, ) {} @@ -45,71 +33,18 @@ export class GradingKeyOverviewComponent implements OnInit { title?: string; gradeSteps: GradeStep[] = []; - studentGrade?: string; + studentGradeOrBonusPointsOrGradeBonus?: string; isBonus = false; forBonus: boolean; ngOnInit(): void { - // Note: This component is used in multiple routes, so it can be lazy loaded. Also, courseId and examId can be - // found on different levels of hierarchy tree (on the same level or a parent or a grandparent, etc.). - this.courseId = Number(findParamInRouteHierarchy(this.route, 'courseId')); - const examIdParam = findParamInRouteHierarchy(this.route, 'examId'); - if (examIdParam) { - this.examId = Number(examIdParam); - this.isExam = true; - } - this.forBonus = !!this.route.snapshot.data['forBonus']; - this.findGradeSteps(this.courseId, this.examId).subscribe((gradeSteps) => { - if (gradeSteps) { - this.title = gradeSteps.title; - this.isBonus = gradeSteps.gradeType === GradeType.BONUS; - this.gradeSteps = this.gradingSystemService.sortGradeSteps(gradeSteps.gradeSteps); - this.plagiarismGrade = gradeSteps.plagiarismGrade; - this.noParticipationGrade = gradeSteps.noParticipationGrade; - if (gradeSteps.maxPoints !== undefined) { - if (!this.isExam) { - let maxPoints = 0; - const totalScoresForCourse = this.scoresStorageService.getStoredTotalScores(this.courseId!); - if (totalScoresForCourse) { - maxPoints = totalScoresForCourse[ScoreType.REACHABLE_POINTS]; - } - this.gradingSystemService.setGradePoints(this.gradeSteps, maxPoints); - } else { - // for exams the max points filed should equal the total max points (otherwise exams can't be started) - this.gradingSystemService.setGradePoints(this.gradeSteps, gradeSteps.maxPoints!); - } - } - } - }); + const { courseId, examId, forBonus, isExam, studentGradeOrBonusPointsOrGradeBonus } = loadGradingKeyUrlParams(this.route); - // Needed queryParam is available on this component so no need to traverse the hierarchy like params above. - this.studentGrade = this.route.snapshot.queryParams['grade']; - } - - private findGradeSteps(courseId: number, examId?: number): Observable { - if (!this.forBonus) { - return this.gradingSystemService.findGradeSteps(courseId, examId); - } else { - // examId must be present if forBonus is true. - return this.bonusService.findBonusForExam(courseId, examId!, true).pipe( - map((bonusResponse) => { - const source = bonusResponse.body?.sourceGradingScale; - if (!source) { - return undefined; - } - return { - title: this.gradingSystemService.getGradingScaleTitle(source)!, - gradeType: source.gradeType, - gradeSteps: source.gradeSteps, - maxPoints: this.gradingSystemService.getGradingScaleMaxPoints(source), - plagiarismGrade: source.plagiarismGrade || GradingScale.DEFAULT_PLAGIARISM_GRADE, - noParticipationGrade: source.noParticipationGrade || GradingScale.DEFAULT_NO_PARTICIPATION_GRADE, - presentationsNumber: source.presentationsNumber, - presentationsWeight: source.presentationsWeight, - }; - }), - ); - } + this.courseId = courseId; + this.examId = examId; + this.forBonus = forBonus; + this.isExam = isExam; + this.studentGradeOrBonusPointsOrGradeBonus = studentGradeOrBonusPointsOrGradeBonus; } /** @@ -131,18 +66,4 @@ export class GradingKeyOverviewComponent implements OnInit { printPDF() { setTimeout(() => this.themeService.print()); } - - /** - * @see GradingSystemService.hasPointsSet - */ - hasPointsSet(): boolean { - return this.gradingSystemService.hasPointsSet(this.gradeSteps); - } - - /** - * @see GradeStepBoundsPipe.round - */ - round(num?: number) { - return GradeStepBoundsPipe.round(num); - } } diff --git a/src/main/webapp/app/grading-system/grading-key-overview/grading-key-overview.module.ts b/src/main/webapp/app/grading-system/grading-key-overview/grading-key-overview.module.ts index 45db9939acbb..9f7deffb6073 100644 --- a/src/main/webapp/app/grading-system/grading-key-overview/grading-key-overview.module.ts +++ b/src/main/webapp/app/grading-system/grading-key-overview/grading-key-overview.module.ts @@ -2,10 +2,11 @@ import { ArtemisSharedModule } from 'app/shared/shared.module'; import { GradingKeyOverviewComponent } from 'app/grading-system/grading-key-overview/grading-key-overview.component'; import { NgModule } from '@angular/core'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { GradingKeyTableComponent } from 'app/grading-system/grading-key-overview/grading-key/grading-key-table.component'; @NgModule({ - declarations: [GradingKeyOverviewComponent], + declarations: [GradingKeyOverviewComponent, GradingKeyTableComponent], imports: [ArtemisSharedModule, ArtemisSharedComponentModule], - exports: [GradingKeyOverviewComponent], + exports: [GradingKeyOverviewComponent, GradingKeyTableComponent], }) export class GradingKeyOverviewModule {} diff --git a/src/main/webapp/app/grading-system/grading-key-overview/grading-key/grading-key-table.component.html b/src/main/webapp/app/grading-system/grading-key-overview/grading-key/grading-key-table.component.html new file mode 100644 index 000000000000..33144ad86a60 --- /dev/null +++ b/src/main/webapp/app/grading-system/grading-key-overview/grading-key/grading-key-table.component.html @@ -0,0 +1,48 @@ +
+
{{ 'artemisApp.gradingSystem.overview.info' | artemisTranslate }}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ isBonus ? ('artemisApp.exam.examSummary.bonus' | artemisTranslate) : ('artemisApp.exam.examSummary.grade' | artemisTranslate) }}{{ 'artemisApp.exam.examSummary.interval' | artemisTranslate }}{{ 'artemisApp.exam.examSummary.intervalPoints' | artemisTranslate }}
{{ plagiarismGrade }}
{{ noParticipationGrade }}
{{ gradeStep.gradeName }} + + + +
+
+
+
{{ 'artemisApp.gradingSystem.overview.intervals.title' | artemisTranslate }}
+
    +
  • [a, b): {{ 'artemisApp.gradingSystem.overview.intervals.leftInclusiveRightExclusive' | artemisTranslate }}
  • +
  • (a, b]: {{ 'artemisApp.gradingSystem.overview.intervals.leftExclusiveRightInclusive' | artemisTranslate }}
  • +
  • [a, b]: {{ 'artemisApp.gradingSystem.overview.intervals.bothInclusive' | artemisTranslate }}
  • +
+
+
diff --git a/src/main/webapp/app/grading-system/grading-key-overview/grading-key/grading-key-table.component.ts b/src/main/webapp/app/grading-system/grading-key-overview/grading-key/grading-key-table.component.ts new file mode 100644 index 000000000000..fc5d5bd07882 --- /dev/null +++ b/src/main/webapp/app/grading-system/grading-key-overview/grading-key/grading-key-table.component.ts @@ -0,0 +1,108 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { GradingSystemService } from 'app/grading-system/grading-system.service'; +import { GradeStep, GradeStepsDTO } from 'app/entities/grade-step.model'; +import { GradeType, GradingScale } from 'app/entities/grading-scale.model'; +import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; +import { GradeEditMode } from 'app/grading-system/base-grading-system/base-grading-system.component'; +import { BonusService } from 'app/grading-system/bonus/bonus.service'; +import { map } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { ScoresStorageService } from 'app/course/course-scores/scores-storage.service'; +import { ScoreType } from 'app/shared/constants/score-type.constants'; +import { ActivatedRoute } from '@angular/router'; +import { loadGradingKeyUrlParams } from 'app/grading-system/grading-key-overview/grading-key-helper'; + +@Component({ + selector: 'jhi-grade-key-table', + templateUrl: './grading-key-table.component.html', + styleUrls: ['../grading-key-overview.scss'], +}) +export class GradingKeyTableComponent implements OnInit { + readonly faChevronLeft = faChevronLeft; + + readonly GradeEditMode = GradeEditMode; + + @Input() studentGradeOrBonusPointsOrGradeBonus?: string; + @Input() forBonus?: boolean; + + constructor( + private route: ActivatedRoute, + private gradingSystemService: GradingSystemService, + private bonusService: BonusService, + private scoresStorageService: ScoresStorageService, + ) {} + + plagiarismGrade: string; + noParticipationGrade: string; + + isExam = false; + + courseId?: number; + examId?: number; + + title?: string; + gradeSteps: GradeStep[] = []; + isBonus = false; + + hasPointsSet = false; + + ngOnInit(): void { + const { courseId, examId, forBonus, isExam, studentGradeOrBonusPointsOrGradeBonus } = loadGradingKeyUrlParams(this.route); + this.courseId = courseId; + this.examId = examId; + this.forBonus = this.forBonus || forBonus; + this.isExam = isExam; + this.studentGradeOrBonusPointsOrGradeBonus = this.studentGradeOrBonusPointsOrGradeBonus || studentGradeOrBonusPointsOrGradeBonus; + + this.findGradeSteps(this.courseId, this.examId).subscribe((gradeSteps) => { + if (gradeSteps) { + this.title = gradeSteps.title; + this.isBonus = gradeSteps.gradeType === GradeType.BONUS; + this.gradeSteps = this.gradingSystemService.sortGradeSteps(gradeSteps.gradeSteps); + this.plagiarismGrade = gradeSteps.plagiarismGrade; + this.noParticipationGrade = gradeSteps.noParticipationGrade; + if (gradeSteps.maxPoints !== undefined) { + if (!this.isExam) { + let maxPoints = 0; + const totalScoresForCourse = this.scoresStorageService.getStoredTotalScores(this.courseId!); + if (totalScoresForCourse) { + maxPoints = totalScoresForCourse[ScoreType.REACHABLE_POINTS]; + } + this.gradingSystemService.setGradePoints(this.gradeSteps, maxPoints); + } else { + // for exams the max points filed should equal the total max points (otherwise exams can't be started) + this.gradingSystemService.setGradePoints(this.gradeSteps, gradeSteps.maxPoints!); + } + } + } + }); + + this.hasPointsSet = this.gradingSystemService.hasPointsSet(this.gradeSteps); + } + + private findGradeSteps(courseId: number, examId?: number): Observable { + if (!this.forBonus) { + return this.gradingSystemService.findGradeSteps(courseId, examId); + } else { + // examId must be present if forBonus is true. + return this.bonusService.findBonusForExam(courseId, examId!, true).pipe( + map((bonusResponse) => { + const source = bonusResponse.body?.sourceGradingScale; + if (!source) { + return undefined; + } + return { + title: this.gradingSystemService.getGradingScaleTitle(source)!, + gradeType: source.gradeType, + gradeSteps: source.gradeSteps, + maxPoints: this.gradingSystemService.getGradingScaleMaxPoints(source), + plagiarismGrade: source.plagiarismGrade || GradingScale.DEFAULT_PLAGIARISM_GRADE, + noParticipationGrade: source.noParticipationGrade || GradingScale.DEFAULT_NO_PARTICIPATION_GRADE, + presentationsNumber: source.presentationsNumber, + presentationsWeight: source.presentationsWeight, + }; + }), + ); + } + } +} diff --git a/src/main/webapp/i18n/de/exam.json b/src/main/webapp/i18n/de/exam.json index dbfb1f9305d1..ba61149faf0c 100644 --- a/src/main/webapp/i18n/de/exam.json +++ b/src/main/webapp/i18n/de/exam.json @@ -66,7 +66,8 @@ } }, "examSummary": { - "yourSubmissionTo": "Deine Abgabe für {{examTitle}} ({{studentName}})", + "examResults": "Prüfungsergebnisse", + "generalInformation": "Allgemeine Informationen", "exportPDF": "Als PDF exportieren", "noSubmissionFound": "Du hast keine Lösung für diese Aufgabe abgegeben.", "lastCommitHash": "Hash des letzten Commits", @@ -77,19 +78,19 @@ "missingResultNotice": "Es gibt derzeit kein Ergebnis für diese Quiz Aufgabe, obwohl die Ergebnisse schon veröffentlicht wurden. Bitte informiere die entsprechende Lehrkraft.", "points": { "exercise": "Aufgabe", - "overview": "Übersicht", + "overview": "Ergebnis-Übersicht", "total": "Gesamt", "yourPoints": "Deine Punkte", "maxPoints": "Erreichbare Punkte", + "achievedPercentage": "Erreichte Punkte in %", "maxBonus": "Erreichbare Bonuspunkte", "youAchievedWithBonus": "Du hast {{achievedPoints}} Punkte von {{normalPoints}} möglichen Punkten (einschließlich Bonuspunkten) erreicht.", - "youAchieved": "Du hast {{achievedPoints}} von möglichen {{normalPoints}} Punkten.", + "youAchieved": "Du hast {{achievedPoints}} von {{normalPoints}} möglichen Punkten erreicht.", "youAchievedFromBonus": { "GRADES_CONTINUOUS": "Du hast einen Notenbonus von {{achievedBonus}} aus {{bonusFromTitle}}.", "GRADES_DISCRETE": "Du hast einen Notenbonus von {{achievedBonus}} Stufe(n) gemäß der Notenskala erhalten, was {{gradePointDifference}} von {{bonusFromTitle}} entspricht.", "POINTS": "Du hast {{achievedBonus}} Bonuspunkte von {{bonusFromTitle}}." }, - "youAchievedPointsAfterBonus": "Deine endgültigen Punkte sind {{finalPoints}} nach dem Bonus.", "maxPointsNotSet": "Die maximale Punktzahl in der Klausur ist nicht gesetzt." }, "grade": "Note", @@ -108,6 +109,7 @@ "date": "Datum", "time": "Zeit", "workingTime": "Bearbeitungszeit", + "examinedStudent": "Prüfungsteilnehmer:in", "resultInformation": "Dein Ergebnis wird nach Abschluss der Korrektur an dieser Stelle für dich veröffentlicht. Du kommst zu dieser Seite, indem du in der Klausurenübersicht des Kurses auf diese Klausur klickst.", "course": "Kurs", "exercises": "Aufgaben", @@ -117,8 +119,10 @@ "startDate": "Beginn", "endDate": "Ende", "publishResultsDate": "Veröffentlichung der Ergebnisse", + "examStudentReviewTimespan": "Zeitraum der Klausureinsicht", "examStudentReviewStart": "Beginn der Klausureinsicht", "examStudentReviewEnd": "Ende der Klausureinsicht", + "studentReviewEnabled": "Klausureinsicht geöffnet", "exampleSolutionPublicationDate": "Veröffentlichungsdatum der Beispiellösung", "duration": "Dauer", "nrOfStudents": "Anzahl registrierter Studierender", diff --git a/src/main/webapp/i18n/en/exam.json b/src/main/webapp/i18n/en/exam.json index cab0ddfb0a41..46c4f3903ac6 100644 --- a/src/main/webapp/i18n/en/exam.json +++ b/src/main/webapp/i18n/en/exam.json @@ -66,7 +66,8 @@ } }, "examSummary": { - "yourSubmissionTo": "Your submission to {{examTitle}} ({{studentName}})", + "examResults": "Exam Results", + "generalInformation": "General Information", "exportPDF": "Export PDF", "noSubmissionFound": "You didn't submit any solution for this exercise.", "lastCommitHash": "Last Commit Hash", @@ -77,10 +78,11 @@ "missingResultNotice": "There is currently no result for this quiz exercise, although the results have already been published. Please inform your instructor.", "points": { "exercise": "Exercise", - "overview": "Overview", + "overview": "Result Overview", "total": "Total", "yourPoints": "Your Points", "maxPoints": "Achievable Points", + "achievedPercentage": "Achieved Percentage", "maxBonus": "Achievable Bonus Points", "youAchievedWithBonus": "You achieved {{achievedPoints}} out of a maximum {{normalPoints}} possible points (including bonus points).", "youAchieved": "You achieved {{achievedPoints}} out of {{normalPoints}} possible points.", @@ -89,7 +91,6 @@ "GRADES_DISCRETE": "You got a grade bonus of {{achievedBonus}} step(s) according to the grading scale which equals {{gradePointDiff}} from {{bonusFromTitle}}.", "POINTS": "You received {{achievedBonus}} additional bonus points from {{bonusFromTitle}}." }, - "youAchievedPointsAfterBonus": "Your final points are {{finalPoints}} after the bonus.", "maxPointsNotSet": "The maximum number of points in the exam is not set." }, "grade": "Grade", @@ -108,6 +109,7 @@ "date": "Date", "time": "Time", "workingTime": "Working Time", + "examinedStudent": "Examined student", "resultInformation": "Your result will be published here as soon as the correction is finished. You can get to this page by clicking on this exam in the exam overview of this course.", "course": "Course", "exercises": "Exercises", @@ -117,6 +119,7 @@ "startDate": "Start Date", "endDate": "End Date", "publishResultsDate": "Release Date of Results", + "examStudentReviewTimespan": "Review Timespan", "examStudentReviewStart": "Begin of Student Review", "examStudentReviewEnd": "End of Student Review", "studentReviewEnabled": "Review is open", diff --git a/src/test/javascript/spec/component/exam/manage/student-exams/student-exam-summary.component.spec.ts b/src/test/javascript/spec/component/exam/manage/student-exams/student-exam-summary.component.spec.ts index a863550a5f96..fe4ee6e5d5eb 100644 --- a/src/test/javascript/spec/component/exam/manage/student-exams/student-exam-summary.component.spec.ts +++ b/src/test/javascript/spec/component/exam/manage/student-exams/student-exam-summary.component.spec.ts @@ -6,7 +6,7 @@ import { of } from 'rxjs'; import { StudentExam } from 'app/entities/student-exam.model'; import { Exam } from 'app/entities/exam.model'; import { StudentExamSummaryComponent } from 'app/exam/manage/student-exams/student-exam-summary.component'; -import { ExamParticipationSummaryComponent } from 'app/exam/participate/summary/exam-participation-summary.component'; +import { ExamResultSummaryComponent } from 'app/exam/participate/summary/exam-result-summary.component'; describe('StudentExamSummaryComponent', () => { let fixture: ComponentFixture; @@ -18,7 +18,7 @@ describe('StudentExamSummaryComponent', () => { beforeEach(() => { return TestBed.configureTestingModule({ - declarations: [StudentExamSummaryComponent, MockComponent(ExamParticipationSummaryComponent)], + declarations: [StudentExamSummaryComponent, MockComponent(ExamResultSummaryComponent)], providers: [ { provide: ActivatedRoute, diff --git a/src/test/javascript/spec/component/exam/participate/exam-participation-cover.component.spec.ts b/src/test/javascript/spec/component/exam/participate/exam-participation-cover.component.spec.ts index b6043c498de3..cd383e73bca2 100644 --- a/src/test/javascript/spec/component/exam/participate/exam-participation-cover.component.spec.ts +++ b/src/test/javascript/spec/component/exam/participate/exam-participation-cover.component.spec.ts @@ -12,7 +12,7 @@ import { Exercise, ExerciseType } from 'app/entities/exercise.model'; import { StudentExam } from 'app/entities/student-exam.model'; import { ExamParticipationCoverComponent } from 'app/exam/participate/exam-cover/exam-participation-cover.component'; import { ExamParticipationService } from 'app/exam/participate/exam-participation.service'; -import { ExamInformationComponent } from 'app/exam/participate/information/exam-information.component'; +import { ExamGeneralInformationComponent } from 'app/exam/participate/general-information/exam-general-information.component'; import { ExamTimerComponent } from 'app/exam/participate/timer/exam-timer.component'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { ArtemisServerDateService } from 'app/shared/server-date.service'; @@ -54,7 +54,7 @@ describe('ExamParticipationCoverComponent', () => { MockPipe(ArtemisTranslatePipe), MockComponent(FaIconComponent), MockComponent(ExamTimerComponent), - MockComponent(ExamInformationComponent), + MockComponent(ExamGeneralInformationComponent), MockDirective(TranslateDirective), MockPipe(ArtemisDatePipe), ], diff --git a/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts b/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts index 43aa840169cd..75a36617e848 100644 --- a/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts +++ b/src/test/javascript/spec/component/exam/participate/exam-participation.component.spec.ts @@ -24,7 +24,7 @@ import { ModelingExamSubmissionComponent } from 'app/exam/participate/exercises/ import { ProgrammingExamSubmissionComponent } from 'app/exam/participate/exercises/programming/programming-exam-submission.component'; import { QuizExamSubmissionComponent } from 'app/exam/participate/exercises/quiz/quiz-exam-submission.component'; import { TextExamSubmissionComponent } from 'app/exam/participate/exercises/text/text-exam-submission.component'; -import { ExamParticipationSummaryComponent } from 'app/exam/participate/summary/exam-participation-summary.component'; +import { ExamResultSummaryComponent } from 'app/exam/participate/summary/exam-result-summary.component'; import { FileUploadSubmissionService } from 'app/exercises/file-upload/participate/file-upload-submission.service'; import { ModelingSubmissionService } from 'app/exercises/modeling/participate/modeling-submission.service'; import { ProgrammingSubmissionService, ProgrammingSubmissionState, ProgrammingSubmissionStateObj } from 'app/exercises/programming/participate/programming-submission.service'; @@ -78,7 +78,7 @@ describe('ExamParticipationComponent', () => { MockComponent(JhiConnectionStatusComponent), MockDirective(TranslateDirective), MockComponent(TestRunRibbonComponent), - MockComponent(ExamParticipationSummaryComponent), + MockComponent(ExamResultSummaryComponent), MockPipe(ArtemisDatePipe), ], providers: [ diff --git a/src/test/javascript/spec/component/exam/participate/information/exam-information.component.spec.ts b/src/test/javascript/spec/component/exam/participate/general-information/exam-general-information.component.spec.ts similarity index 91% rename from src/test/javascript/spec/component/exam/participate/information/exam-information.component.spec.ts rename to src/test/javascript/spec/component/exam/participate/general-information/exam-general-information.component.spec.ts index c5dc07843146..6124c87a02d7 100644 --- a/src/test/javascript/spec/component/exam/participate/information/exam-information.component.spec.ts +++ b/src/test/javascript/spec/component/exam/participate/general-information/exam-general-information.component.spec.ts @@ -3,7 +3,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { User } from 'app/core/user/user.model'; import { Exam } from 'app/entities/exam.model'; import { StudentExam } from 'app/entities/student-exam.model'; -import { ExamInformationComponent } from 'app/exam/participate/information/exam-information.component'; +import { ExamGeneralInformationComponent } from 'app/exam/participate/general-information/exam-general-information.component'; import { StudentExamWorkingTimeComponent } from 'app/exam/shared/student-exam-working-time/student-exam-working-time.component'; import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; import { ArtemisDurationFromSecondsPipe } from 'app/shared/pipes/artemis-duration-from-seconds.pipe'; @@ -11,8 +11,8 @@ import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import dayjs from 'dayjs/esm'; import { MockComponent, MockPipe } from 'ng-mocks'; -let fixture: ComponentFixture; -let component: ExamInformationComponent; +let fixture: ComponentFixture; +let component: ExamGeneralInformationComponent; const user = { id: 1, name: 'Test User' } as User; @@ -29,7 +29,7 @@ let exam = { let studentExam = { id: 1, exam, user, workingTime: 60, submitted: true } as StudentExam; -describe('ExamInformationComponent', () => { +describe('ExamGeneralInformationComponent', () => { beforeEach(() => { exam = { id: 1, title: 'ExamForTesting', startDate, endDate, testExam: false } as Exam; studentExam = { id: 1, exam, user, workingTime: 60, submitted: true } as StudentExam; @@ -37,7 +37,7 @@ describe('ExamInformationComponent', () => { return TestBed.configureTestingModule({ imports: [RouterTestingModule.withRoutes([])], declarations: [ - ExamInformationComponent, + ExamGeneralInformationComponent, MockComponent(StudentExamWorkingTimeComponent), MockPipe(ArtemisTranslatePipe), MockPipe(ArtemisDatePipe), @@ -46,7 +46,7 @@ describe('ExamInformationComponent', () => { }) .compileComponents() .then(() => { - fixture = TestBed.createComponent(ExamInformationComponent); + fixture = TestBed.createComponent(ExamGeneralInformationComponent); component = fixture.componentInstance; }); }); diff --git a/src/test/javascript/spec/component/exam/participate/summary/exam-participation-summary.component.spec.ts b/src/test/javascript/spec/component/exam/participate/summary/exam-participation-summary.component.spec.ts index 93cea7322980..56c40745a6c5 100644 --- a/src/test/javascript/spec/component/exam/participate/summary/exam-participation-summary.component.spec.ts +++ b/src/test/javascript/spec/component/exam/participate/summary/exam-participation-summary.component.spec.ts @@ -27,14 +27,14 @@ import { TextSubmission } from 'app/entities/text-submission.model'; import { StudentExamWithGradeDTO, StudentResult } from 'app/exam/exam-scores/exam-score-dtos.model'; import { TestRunRibbonComponent } from 'app/exam/manage/test-runs/test-run-ribbon.component'; import { ExamParticipationService } from 'app/exam/participate/exam-participation.service'; -import { ExamInformationComponent } from 'app/exam/participate/information/exam-information.component'; -import { ExamParticipationSummaryComponent } from 'app/exam/participate/summary/exam-participation-summary.component'; +import { ExamGeneralInformationComponent } from 'app/exam/participate/general-information/exam-general-information.component'; +import { ExamResultSummaryComponent } from 'app/exam/participate/summary/exam-result-summary.component'; import { FileUploadExamSummaryComponent } from 'app/exam/participate/summary/exercises/file-upload-exam-summary/file-upload-exam-summary.component'; import { ModelingExamSummaryComponent } from 'app/exam/participate/summary/exercises/modeling-exam-summary/modeling-exam-summary.component'; import { ProgrammingExamSummaryComponent } from 'app/exam/participate/summary/exercises/programming-exam-summary/programming-exam-summary.component'; import { QuizExamSummaryComponent } from 'app/exam/participate/summary/exercises/quiz-exam-summary/quiz-exam-summary.component'; import { TextExamSummaryComponent } from 'app/exam/participate/summary/exercises/text-exam-summary/text-exam-summary.component'; -import { ExamPointsSummaryComponent } from 'app/exam/participate/summary/points-summary/exam-points-summary.component'; +import { ExamResultOverviewComponent } from 'app/exam/participate/summary/result-overview/exam-result-overview.component'; import { ProgrammingExerciseInstructionComponent } from 'app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component'; import { IncludedInScoreBadgeComponent } from 'app/exercises/shared/exercise-headers/included-in-score-badge.component'; import { ResultComponent } from 'app/exercises/shared/result/result.component'; @@ -52,8 +52,8 @@ import { MockExamParticipationService } from '../../../../helpers/mocks/service/ import { MockLocalStorageService } from '../../../../helpers/mocks/service/mock-local-storage.service'; import { MockArtemisServerDateService } from '../../../../helpers/mocks/service/mock-server-date.service'; -let fixture: ComponentFixture; -let component: ExamParticipationSummaryComponent; +let fixture: ComponentFixture; +let component: ExamResultSummaryComponent; let artemisServerDateService: ArtemisServerDateService; const user = { id: 1, name: 'Test User' } as User; @@ -136,10 +136,10 @@ function sharedSetup(url: string[]) { return TestBed.configureTestingModule({ imports: [RouterTestingModule.withRoutes([]), HttpClientModule, NgbCollapseMocksModule], declarations: [ - ExamParticipationSummaryComponent, + ExamResultSummaryComponent, MockComponent(TestRunRibbonComponent), - MockComponent(ExamPointsSummaryComponent), - MockComponent(ExamInformationComponent), + MockComponent(ExamResultOverviewComponent), + MockComponent(ExamGeneralInformationComponent), MockComponent(ResultComponent), MockComponent(UpdatingResultComponent), MockComponent(ProgrammingExerciseInstructionComponent), @@ -179,7 +179,7 @@ function sharedSetup(url: string[]) { }) .compileComponents() .then(() => { - fixture = TestBed.createComponent(ExamParticipationSummaryComponent); + fixture = TestBed.createComponent(ExamResultSummaryComponent); component = fixture.componentInstance; component.studentExam = studentExam; artemisServerDateService = TestBed.inject(ArtemisServerDateService); @@ -191,7 +191,7 @@ function sharedSetup(url: string[]) { }); } -describe('ExamParticipationSummaryComponent', () => { +describe('ExamResultSummaryComponent', () => { sharedSetup(['', '']); it('should expand all exercises and call print when Export PDF is clicked', fakeAsync(() => { diff --git a/src/test/javascript/spec/component/exam/participate/summary/points-summary/exam-points-summary.component.spec.ts b/src/test/javascript/spec/component/exam/participate/summary/result-overview/exam-result-overview.component.spec.ts similarity index 57% rename from src/test/javascript/spec/component/exam/participate/summary/points-summary/exam-points-summary.component.spec.ts rename to src/test/javascript/spec/component/exam/participate/summary/result-overview/exam-result-overview.component.spec.ts index df79e0ce3c9e..8ed3b7b1de23 100644 --- a/src/test/javascript/spec/component/exam/participate/summary/points-summary/exam-points-summary.component.spec.ts +++ b/src/test/javascript/spec/component/exam/participate/summary/result-overview/exam-result-overview.component.spec.ts @@ -5,7 +5,7 @@ import { MockComponent, MockModule, MockPipe, MockProvider } from 'ng-mocks'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { User } from 'app/core/user/user.model'; import { Exam } from 'app/entities/exam.model'; -import { ExamPointsSummaryComponent } from 'app/exam/participate/summary/points-summary/exam-points-summary.component'; +import { ExamResultOverviewComponent } from 'app/exam/participate/summary/result-overview/exam-result-overview.component'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; import { ExerciseType, IncludedInOverallScore } from 'app/entities/exercise.model'; import { TextExercise } from 'app/entities/text-exercise.model'; @@ -19,10 +19,10 @@ import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service' import { GradeType } from 'app/entities/grading-scale.model'; import { Course } from 'app/entities/course.model'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; -import { StudentExamWithGradeDTO } from 'app/exam/exam-scores/exam-score-dtos.model'; +import { ExerciseResult, StudentExamWithGradeDTO } from 'app/exam/exam-scores/exam-score-dtos.model'; -let fixture: ComponentFixture; -let component: ExamPointsSummaryComponent; +let fixture: ComponentFixture; +let component: ExamResultOverviewComponent; let studentExamWithGrade: StudentExamWithGradeDTO; const visibleDate = dayjs().subtract(7, 'hours'); @@ -116,11 +116,13 @@ const programmingExerciseTwo = { } as ProgrammingExercise; const exercises = [textExercise, quizExercise, modelingExercise, programmingExercise, programmingExerciseTwo, notIncludedTextExercise, bonusTextExercise]; -describe('ExamPointsSummaryComponent', () => { +const textExerciseResult = { exerciseId: textExercise.id, achievedScore: 60, achievedPoints: 6, maxScore: textExercise.maxPoints } as ExerciseResult; + +describe('ExamResultOverviewComponent', () => { beforeEach(() => { return TestBed.configureTestingModule({ imports: [RouterTestingModule.withRoutes([]), MockModule(NgbModule), HttpClientTestingModule], - declarations: [ExamPointsSummaryComponent, MockComponent(FaIconComponent), MockPipe(ArtemisTranslatePipe)], + declarations: [ExamResultOverviewComponent, MockComponent(FaIconComponent), MockPipe(ArtemisTranslatePipe)], providers: [MockProvider(ExerciseService)], }) .compileComponents() @@ -141,12 +143,15 @@ describe('ExamPointsSummaryComponent', () => { overallGrade: '1.7', hasPassed: true, submitted: true, - exerciseGroupIdToExerciseResult: {}, + exerciseGroupIdToExerciseResult: { + [textExercise.id!]: textExerciseResult, + }, }, achievedPointsPerExercise: { + [programmingExerciseTwo.id!]: 0, [textExercise.id!]: 20, - [bonusTextExercise.id!]: 10, [notIncludedTextExercise.id!]: 10, + [bonusTextExercise.id!]: 10, [quizExercise.id!]: 2, [modelingExercise.id!]: 3.33, [programmingExercise.id!]: 0, @@ -157,7 +162,7 @@ describe('ExamPointsSummaryComponent', () => { course.id = 1; course.accuracyOfScores = 2; - fixture = TestBed.createComponent(ExamPointsSummaryComponent); + fixture = TestBed.createComponent(ExamResultOverviewComponent); component = fixture.componentInstance; exam.course = course; component.gradingScaleExists = false; @@ -192,17 +197,18 @@ describe('ExamPointsSummaryComponent', () => { it('should initialize and calculate scores correctly', () => { fixture.detectChanges(); expect(fixture).not.toBeNull(); - expect(component.getAchievedPoints(programmingExerciseTwo)).toBe(0); - expect(component.getAchievedPoints(textExercise)).toBe(20); - expect(component.getAchievedPoints(notIncludedTextExercise)).toBe(10); - expect(component.getAchievedPoints(bonusTextExercise)).toBe(10); - expect(component.getAchievedPoints(quizExercise)).toBe(2); - expect(component.getAchievedPoints(modelingExercise)).toBe(3.33); - expect(component.getAchievedPoints(programmingExercise)).toBe(0); - - expect(component.getAchievedPointsSum()).toBe(35.33); - expect(component.getMaxNormalPointsSum()).toBe(40); - expect(component.getMaxBonusPointsSum()).toBe(20); + + expect(component.studentExamWithGrade?.achievedPointsPerExercise?.[programmingExerciseTwo.id!]).toBe(0); + expect(component.studentExamWithGrade?.achievedPointsPerExercise?.[textExercise.id!]).toBe(20); + expect(component.studentExamWithGrade?.achievedPointsPerExercise?.[notIncludedTextExercise.id!]).toBe(10); + expect(component.studentExamWithGrade?.achievedPointsPerExercise?.[bonusTextExercise.id!]).toBe(10); + expect(component.studentExamWithGrade?.achievedPointsPerExercise?.[quizExercise.id!]).toBe(2); + expect(component.studentExamWithGrade?.achievedPointsPerExercise?.[modelingExercise.id!]).toBe(3.33); + expect(component.studentExamWithGrade?.achievedPointsPerExercise?.[programmingExercise.id!]).toBe(0); + + expect(component.overallAchievedPoints).toBe(35.33); + expect(component.maxPoints).toBe(40); + expect(component.studentExamWithGrade?.maxBonusPoints).toBe(20); expect(component.getMaxNormalAndBonusPointsSum()).toBe(60); }); @@ -214,9 +220,103 @@ describe('ExamPointsSummaryComponent', () => { fixture.detectChanges(); expect(fixture).not.toBeNull(); - expect(component.getAchievedPointsSum()).toBe(0); - expect(component.getMaxNormalPointsSum()).toBe(0); - expect(component.getMaxBonusPointsSum()).toBe(20); + expect(component.overallAchievedPoints).toBe(0); + expect(component.maxPoints).toBe(0); + expect(component.studentExamWithGrade?.maxBonusPoints).toBe(20); expect(component.getMaxNormalAndBonusPointsSum()).toBe(20); }); + + describe('should evaluate showIncludedInScoreColumn', () => { + it('to false if all exercises are included in the score', () => { + const onlyIncludedExercises = [textExercise, quizExercise, modelingExercise, programmingExercise]; + component.studentExamWithGrade.studentExam!.exercises = onlyIncludedExercises; + + expect(component.containsExerciseThatIsNotIncludedCompletely()).toBeFalse(); + }); + + it('to true if exercise is excluded', () => { + const onlyIncludedExercises = [textExercise, quizExercise, modelingExercise, programmingExercise, notIncludedTextExercise]; + component.studentExamWithGrade.studentExam!.exercises = onlyIncludedExercises; + + expect(component.containsExerciseThatIsNotIncludedCompletely()).toBeTrue(); + }); + + it('to true if bonus exercise is included', () => { + const onlyIncludedExercises = [textExercise, quizExercise, modelingExercise, programmingExercise, bonusTextExercise]; + component.studentExamWithGrade.studentExam!.exercises = onlyIncludedExercises; + + expect(component.containsExerciseThatIsNotIncludedCompletely()).toBeTrue(); + }); + }); + + describe('scrollToExercise', () => { + it('should scroll to the target exercise dom element', () => { + const mockElement = document.createElement('div'); + mockElement.id = 'exercise-1'; + document.body.appendChild(mockElement); + mockElement.scrollIntoView = jest.fn(); + + component.scrollToExercise(1); + + expect(mockElement.scrollIntoView).toHaveBeenCalledWith({ + behavior: 'smooth', + block: 'start', + inline: 'nearest', + }); + }); + + it('should log an error when the target exercise dom element does not exist', () => { + const consoleErrorMock = jest.spyOn(console, 'error').mockImplementation(); + const INVALID_EXERCISE_ID = 999; + + component.scrollToExercise(INVALID_EXERCISE_ID); + + expect(consoleErrorMock).toHaveBeenCalledWith(expect.stringContaining('Could not find corresponding exercise with id')); + }); + + it('should return immediately when exerciseId is undefined', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + component.scrollToExercise(undefined); + + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + }); + + describe('getAchievedPercentageByExerciseId', () => { + it('should return undefined if exercise result is undefined', () => { + component.studentExamWithGrade.studentResult.exerciseGroupIdToExerciseResult = {}; + const scoreAsPercentage = component.getAchievedPercentageByExerciseId(textExercise.id); + + expect(scoreAsPercentage).toBeUndefined(); + }); + + it('should calculate percentage based on achievedScore considering course settings', () => { + textExerciseResult.achievedScore = 60.6666; + + const scoreAsPercentage = component.getAchievedPercentageByExerciseId(textExercise.id); + + expect(scoreAsPercentage).toBe(60.67); + }); + + it('should calculate percentage based on maxScore and achievedPoints', () => { + textExerciseResult.achievedScore = undefined; + textExerciseResult.maxScore = 10; + textExerciseResult.achievedPoints = 6.066666; + component.studentExamWithGrade.studentExam!.exam!.course!.accuracyOfScores = 3; + + const scoreAsPercentage = component.getAchievedPercentageByExerciseId(textExercise.id); + + expect(scoreAsPercentage).toBe(60.667); + }); + + it('should return undefined if not set and not calculable', () => { + textExerciseResult.achievedScore = undefined; + textExerciseResult.achievedPoints = undefined; + + const scoreAsPercentage = component.getAchievedPercentageByExerciseId(textExercise.id); + + expect(scoreAsPercentage).toBeUndefined(); + }); + }); }); diff --git a/src/test/javascript/spec/component/grading-system/grading-key-overview.component.spec.ts b/src/test/javascript/spec/component/grading-system/grading-key-overview.component.spec.ts index 596965d63b16..f1e4c6e0739b 100644 --- a/src/test/javascript/spec/component/grading-system/grading-key-overview.component.spec.ts +++ b/src/test/javascript/spec/component/grading-system/grading-key-overview.component.spec.ts @@ -1,62 +1,27 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { GradingSystemService } from 'app/grading-system/grading-system.service'; import { GradingKeyOverviewComponent } from 'app/grading-system/grading-key-overview/grading-key-overview.component'; -import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; -import { MockComponent, MockDirective, MockModule, MockPipe, MockProvider } from 'ng-mocks'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { ActivatedRoute, ActivatedRouteSnapshot, Params, Router } from '@angular/router'; -import { of } from 'rxjs'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MockRouter } from '../../helpers/mocks/mock-router'; -import { GradeStep, GradeStepsDTO } from 'app/entities/grade-step.model'; -import { GradeType, GradingScale } from 'app/entities/grading-scale.model'; +import { BonusService } from 'app/grading-system/bonus/bonus.service'; +import { CourseStorageService } from 'app/course/manage/course-storage.service'; +import { ScoresStorageService } from 'app/course/course-scores/scores-storage.service'; import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; +import { MockLocalStorageService } from '../../helpers/mocks/service/mock-local-storage.service'; +import { LocalStorageService } from 'ngx-webstorage'; +import { MockComponent, MockDirective, MockPipe, MockProvider } from 'ng-mocks'; +import { ActivatedRoute, Params, Router } from '@angular/router'; +import { GradingKeyTableComponent } from 'app/grading-system/grading-key-overview/grading-key/grading-key-table.component'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { TranslateDirective } from 'app/shared/language/translate.directive'; import { SafeHtmlPipe } from 'app/shared/pipes/safe-html.pipe'; import { GradeStepBoundsPipe } from 'app/shared/pipes/grade-step-bounds.pipe'; -import { MockLocalStorageService } from '../../helpers/mocks/service/mock-local-storage.service'; -import { LocalStorageService } from 'ngx-webstorage'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { ThemeService } from 'app/core/theme/theme.service'; -import { BonusService } from 'app/grading-system/bonus/bonus.service'; -import { Bonus } from 'app/entities/bonus.model'; -import { HttpResponse } from '@angular/common/http'; -import { CourseStorageService } from 'app/course/manage/course-storage.service'; -import { ScoresStorageService } from 'app/course/course-scores/scores-storage.service'; -import { CourseScores, StudentScores } from 'app/course/course-scores/course-scores'; - -describe('GradeKeyOverviewComponent', () => { +describe('GradingKeyOverviewComponent', () => { let fixture: ComponentFixture; - let comp: GradingKeyOverviewComponent; + let component: GradingKeyOverviewComponent; let route: ActivatedRoute; - let gradingSystemService: GradingSystemService; - let bonusService: BonusService; - - const gradeStep1: GradeStep = { - gradeName: 'Fail', - lowerBoundPercentage: 0, - upperBoundPercentage: 50, - lowerBoundInclusive: true, - upperBoundInclusive: false, - isPassingGrade: false, - }; - const gradeStep2: GradeStep = { - gradeName: 'Pass', - lowerBoundPercentage: 50, - upperBoundPercentage: 100, - lowerBoundInclusive: true, - upperBoundInclusive: true, - isPassingGrade: true, - }; - const gradeStepsDto: GradeStepsDTO = { - title: 'Title', - gradeType: GradeType.BONUS, - gradeSteps: [gradeStep1, gradeStep2], - maxPoints: 100, - plagiarismGrade: GradingScale.DEFAULT_PLAGIARISM_GRADE, - noParticipationGrade: GradingScale.DEFAULT_NO_PARTICIPATION_GRADE, - }; - const studentGrade = '2.0'; beforeEach(() => { @@ -72,10 +37,10 @@ describe('GradeKeyOverviewComponent', () => { }, } as ActivatedRoute; - return TestBed.configureTestingModule({ - imports: [MockModule(NgbModule)], + TestBed.configureTestingModule({ declarations: [ GradingKeyOverviewComponent, + MockComponent(GradingKeyTableComponent), MockComponent(FaIconComponent), MockPipe(ArtemisTranslatePipe), MockDirective(TranslateDirective), @@ -96,142 +61,41 @@ describe('GradeKeyOverviewComponent', () => { .compileComponents() .then(() => { fixture = TestBed.createComponent(GradingKeyOverviewComponent); - comp = fixture.componentInstance; - gradingSystemService = fixture.debugElement.injector.get(GradingSystemService); - bonusService = fixture.debugElement.injector.get(BonusService); + component = fixture.componentInstance; }); - }); - afterEach(() => { - jest.restoreAllMocks(); + fixture = TestBed.createComponent(GradingKeyOverviewComponent); }); - function expectInitialState(grade?: string) { - jest.spyOn(gradingSystemService, 'findGradeSteps').mockReturnValue(of(gradeStepsDto)); - jest.spyOn(gradingSystemService, 'sortGradeSteps').mockReturnValue([gradeStep1, gradeStep2]); - const gradePointsSpy = jest.spyOn(gradingSystemService, 'setGradePoints').mockImplementation(); - + it('should initialize component', () => { fixture.detectChanges(); expect(fixture).toBeTruthy(); - expect(comp).toBeTruthy(); - expect(comp.examId).toBe(123); - expect(comp.courseId).toBe(345); - expect(comp.studentGrade).toBe(grade); - expect(comp.title).toBe('Title'); - expect(comp.isBonus).toBeTrue(); - expect(comp.isExam).toBeTrue(); - expect(comp.gradeSteps).toEqual([gradeStep1, gradeStep2]); - expect(gradePointsSpy).toHaveBeenCalledWith([gradeStep1, gradeStep2], 100); - } - - it('should initialize when grade queryParam is not given', () => { - route.snapshot.queryParams = {}; - - expectInitialState(undefined); - }); - - it('should initialize when params are in grandparent route', () => { - expectInitialState(studentGrade); - }); - - it('should initialize when params are in parent route', () => { - // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - route.parent!.snapshot.params = route.parent?.parent?.snapshot.params!; - route.parent!.parent!.snapshot = { params: {} } as ActivatedRouteSnapshot; - - expectInitialState(studentGrade); - }); - - it('should initialize when params are in current route', () => { - // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - route.snapshot.params = route.parent?.parent?.snapshot.params!; - route.parent!.parent!.snapshot = { params: {} } as ActivatedRouteSnapshot; - - expectInitialState(studentGrade); - }); - - it('should initialize for bonus grading scale', () => { - jest.spyOn(gradingSystemService, 'getGradingScaleTitle').mockImplementation((gradingScale) => gradingScale?.course?.title); - jest.spyOn(gradingSystemService, 'getGradingScaleMaxPoints').mockImplementation((gradingScale) => gradingScale?.course?.maxPoints ?? 0); - const bonusServiceSpy = jest.spyOn(bonusService, 'findBonusForExam').mockReturnValue( - of({ - body: { - sourceGradingScale: { - gradeSteps: gradeStepsDto.gradeSteps, - gradeType: gradeStepsDto.gradeType, - course: { title: gradeStepsDto.title, maxPoints: gradeStepsDto.maxPoints }, - }, - } as Bonus, - } as HttpResponse), - ); - - // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - route.snapshot.params = route.parent?.parent?.snapshot.params!; - route.parent!.parent!.snapshot = { params: {} } as ActivatedRouteSnapshot; - route.snapshot.data.forBonus = true; - - expectInitialState(studentGrade); - - expect(bonusServiceSpy).toHaveBeenCalledOnce(); - expect(bonusServiceSpy).toHaveBeenCalledWith(345, 123, true); - }); - - it('should initialize for courses', () => { - route.parent!.parent!.snapshot!.params.examId = undefined; - const courseId = route.parent!.parent!.snapshot!.params.courseId; - const reachablePoints = 200; - - const scoresStorageService = fixture.debugElement.injector.get(ScoresStorageService); - const getStoredScoresStub = jest.spyOn(scoresStorageService, 'getStoredTotalScores').mockReturnValue(new CourseScores(250, 200, 0, new StudentScores())); - const gradingSystemServiceSpy = jest.spyOn(gradingSystemService, 'setGradePoints'); - - jest.spyOn(gradingSystemService, 'findGradeSteps').mockReturnValue(of(gradeStepsDto)); - jest.spyOn(gradingSystemService, 'sortGradeSteps').mockReturnValue([gradeStep1, gradeStep2]); - - fixture.detectChanges(); - - expect(fixture).toBeTruthy(); - expect(comp).toBeTruthy(); - expect(comp.examId).toBeUndefined(); - expect(comp.courseId).toBe(courseId); - expect(comp.studentGrade).toBe(studentGrade); - expect(comp.title).toBe('Title'); - expect(comp.isBonus).toBeTrue(); - expect(comp.isExam).toBeFalse(); - - expect(getStoredScoresStub).toHaveBeenCalledOnce(); - expect(getStoredScoresStub).toHaveBeenCalledWith(courseId); - - expect(gradingSystemServiceSpy).toHaveBeenCalledOnce(); - expect(gradingSystemServiceSpy).toHaveBeenCalledWith([gradeStep1, gradeStep2], reachablePoints); + expect(component).toBeTruthy(); + expect(component.examId).toBe(123); + expect(component.courseId).toBe(345); + expect(component.studentGradeOrBonusPointsOrGradeBonus).toBe(studentGrade); }); it('should print PDF', fakeAsync(() => { const printSpy = jest.spyOn(TestBed.inject(ThemeService), 'print').mockImplementation(); - comp.printPDF(); + component.printPDF(); tick(); expect(printSpy).toHaveBeenCalledOnce(); })); - it('should round correctly', () => { - expect(comp.round(undefined)).toBeUndefined(); - expect(comp.round(5)).toBe(5); - expect(comp.round(3.333333333333333)).toBe(3.33); - }); - it.each([456, undefined])('should call the back method on the nav util service on previousState for examId %s', (examId) => { const navUtilService = TestBed.inject(ArtemisNavigationUtilService); const navUtilServiceSpy = jest.spyOn(navUtilService, 'navigateBack'); const courseId = 213; - comp.courseId = courseId; - comp.examId = examId; - comp.isExam = examId !== undefined; + component.courseId = courseId; + component.examId = examId; + component.isExam = examId !== undefined; - comp.previousState(); + component.previousState(); expect(navUtilServiceSpy).toHaveBeenCalledOnce(); diff --git a/src/test/javascript/spec/component/grading-system/grading-key-table.component.spec.ts b/src/test/javascript/spec/component/grading-system/grading-key-table.component.spec.ts new file mode 100644 index 000000000000..6110d07dde4f --- /dev/null +++ b/src/test/javascript/spec/component/grading-system/grading-key-table.component.spec.ts @@ -0,0 +1,206 @@ +import { GradingSystemService } from 'app/grading-system/grading-system.service'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { MockComponent, MockDirective, MockModule, MockPipe, MockProvider } from 'ng-mocks'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ActivatedRoute, ActivatedRouteSnapshot, Params, Router } from '@angular/router'; +import { of } from 'rxjs'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { MockRouter } from '../../helpers/mocks/mock-router'; +import { GradeStep, GradeStepsDTO } from 'app/entities/grade-step.model'; +import { GradeType, GradingScale } from 'app/entities/grading-scale.model'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; +import { SafeHtmlPipe } from 'app/shared/pipes/safe-html.pipe'; +import { GradeStepBoundsPipe } from 'app/shared/pipes/grade-step-bounds.pipe'; +import { MockLocalStorageService } from '../../helpers/mocks/service/mock-local-storage.service'; +import { LocalStorageService } from 'ngx-webstorage'; +import { BonusService } from 'app/grading-system/bonus/bonus.service'; +import { Bonus } from 'app/entities/bonus.model'; +import { HttpResponse } from '@angular/common/http'; +import { CourseStorageService } from 'app/course/manage/course-storage.service'; +import { ScoresStorageService } from 'app/course/course-scores/scores-storage.service'; +import { CourseScores, StudentScores } from 'app/course/course-scores/course-scores'; +import { GradingKeyTableComponent } from 'app/grading-system/grading-key-overview/grading-key/grading-key-table.component'; + +describe('GradingKeyTableComponent', () => { + let fixture: ComponentFixture; + let comp: GradingKeyTableComponent; + let route: ActivatedRoute; + + let gradingSystemService: GradingSystemService; + let bonusService: BonusService; + + const gradeStep1: GradeStep = { + gradeName: 'Fail', + lowerBoundPercentage: 0, + upperBoundPercentage: 50, + lowerBoundInclusive: true, + upperBoundInclusive: false, + isPassingGrade: false, + }; + const gradeStep2: GradeStep = { + gradeName: 'Pass', + lowerBoundPercentage: 50, + upperBoundPercentage: 100, + lowerBoundInclusive: true, + upperBoundInclusive: true, + isPassingGrade: true, + }; + const gradeStepsDto: GradeStepsDTO = { + title: 'Title', + gradeType: GradeType.BONUS, + gradeSteps: [gradeStep1, gradeStep2], + maxPoints: 100, + plagiarismGrade: GradingScale.DEFAULT_PLAGIARISM_GRADE, + noParticipationGrade: GradingScale.DEFAULT_NO_PARTICIPATION_GRADE, + }; + + const studentGrade = '2.0'; + + beforeEach(() => { + route = { + snapshot: { params: {} as Params, queryParams: { grade: studentGrade } as Params, data: {} }, + parent: { + snapshot: { params: {} }, + parent: { + snapshot: { + params: { courseId: 345, examId: 123 } as Params, + }, + }, + }, + } as ActivatedRoute; + + return TestBed.configureTestingModule({ + imports: [MockModule(NgbModule)], + declarations: [ + GradingKeyTableComponent, + MockComponent(FaIconComponent), + MockPipe(ArtemisTranslatePipe), + MockDirective(TranslateDirective), + MockPipe(SafeHtmlPipe), + MockPipe(GradeStepBoundsPipe), + ], + providers: [ + { provide: ActivatedRoute, useValue: route }, + { provide: Router, useClass: MockRouter }, + MockProvider(GradingSystemService), + MockProvider(BonusService), + MockProvider(CourseStorageService), + MockProvider(ScoresStorageService), + { provide: LocalStorageService, useClass: MockLocalStorageService }, + ], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(GradingKeyTableComponent); + comp = fixture.componentInstance; + gradingSystemService = fixture.debugElement.injector.get(GradingSystemService); + bonusService = fixture.debugElement.injector.get(BonusService); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + function expectInitialState(grade?: string) { + jest.spyOn(gradingSystemService, 'findGradeSteps').mockReturnValue(of(gradeStepsDto)); + jest.spyOn(gradingSystemService, 'sortGradeSteps').mockReturnValue([gradeStep1, gradeStep2]); + const gradePointsSpy = jest.spyOn(gradingSystemService, 'setGradePoints').mockImplementation(); + + fixture.detectChanges(); + + expect(fixture).toBeTruthy(); + expect(comp).toBeTruthy(); + expect(comp.examId).toBe(123); + expect(comp.courseId).toBe(345); + expect(comp.studentGradeOrBonusPointsOrGradeBonus).toBe(grade); + expect(comp.title).toBe('Title'); + expect(comp.isBonus).toBeTrue(); + expect(comp.isExam).toBeTrue(); + expect(comp.gradeSteps).toEqual([gradeStep1, gradeStep2]); + expect(gradePointsSpy).toHaveBeenCalledWith([gradeStep1, gradeStep2], 100); + } + + it('should initialize when grade queryParam is not given', () => { + route.snapshot.queryParams = {}; + + expectInitialState(undefined); + }); + + it('should initialize when params are in grandparent route', () => { + expectInitialState(studentGrade); + }); + + it('should initialize when params are in parent route', () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain + route.parent!.snapshot.params = route.parent?.parent?.snapshot.params!; + route.parent!.parent!.snapshot = { params: {} } as ActivatedRouteSnapshot; + + expectInitialState(studentGrade); + }); + + it('should initialize when params are in current route', () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain + route.snapshot.params = route.parent?.parent?.snapshot.params!; + route.parent!.parent!.snapshot = { params: {} } as ActivatedRouteSnapshot; + + expectInitialState(studentGrade); + }); + + it('should initialize for bonus grading scale', () => { + jest.spyOn(gradingSystemService, 'getGradingScaleTitle').mockImplementation((gradingScale) => gradingScale?.course?.title); + jest.spyOn(gradingSystemService, 'getGradingScaleMaxPoints').mockImplementation((gradingScale) => gradingScale?.course?.maxPoints ?? 0); + const bonusServiceSpy = jest.spyOn(bonusService, 'findBonusForExam').mockReturnValue( + of({ + body: { + sourceGradingScale: { + gradeSteps: gradeStepsDto.gradeSteps, + gradeType: gradeStepsDto.gradeType, + course: { title: gradeStepsDto.title, maxPoints: gradeStepsDto.maxPoints }, + }, + } as Bonus, + } as HttpResponse), + ); + + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain + route.snapshot.params = route.parent?.parent?.snapshot.params!; + route.parent!.parent!.snapshot = { params: {} } as ActivatedRouteSnapshot; + route.snapshot.data.forBonus = true; + + expectInitialState(studentGrade); + + expect(bonusServiceSpy).toHaveBeenCalledOnce(); + expect(bonusServiceSpy).toHaveBeenCalledWith(345, 123, true); + }); + + it('should initialize for courses', () => { + route.parent!.parent!.snapshot!.params.examId = undefined; + const courseId = route.parent!.parent!.snapshot!.params.courseId; + const reachablePoints = 200; + + const scoresStorageService = fixture.debugElement.injector.get(ScoresStorageService); + const getStoredScoresStub = jest.spyOn(scoresStorageService, 'getStoredTotalScores').mockReturnValue(new CourseScores(250, 200, 0, new StudentScores())); + const gradingSystemServiceSpy = jest.spyOn(gradingSystemService, 'setGradePoints'); + + jest.spyOn(gradingSystemService, 'findGradeSteps').mockReturnValue(of(gradeStepsDto)); + jest.spyOn(gradingSystemService, 'sortGradeSteps').mockReturnValue([gradeStep1, gradeStep2]); + + fixture.detectChanges(); + + expect(fixture).toBeTruthy(); + expect(comp).toBeTruthy(); + expect(comp.examId).toBeUndefined(); + expect(comp.courseId).toBe(courseId); + expect(comp.studentGradeOrBonusPointsOrGradeBonus).toBe(studentGrade); + expect(comp.title).toBe('Title'); + expect(comp.isBonus).toBeTrue(); + expect(comp.isExam).toBeFalse(); + + expect(getStoredScoresStub).toHaveBeenCalledOnce(); + expect(getStoredScoresStub).toHaveBeenCalledWith(courseId); + + expect(gradingSystemServiceSpy).toHaveBeenCalledOnce(); + expect(gradingSystemServiceSpy).toHaveBeenCalledWith([gradeStep1, gradeStep2], reachablePoints); + }); +});