From 6b6a64650e8f1dc498d885e8cd4fcea01bc2f360 Mon Sep 17 00:00:00 2001 From: Ramona Beinstingel Date: Fri, 10 May 2024 17:43:10 +0200 Subject: [PATCH 01/23] Add Instructor Actions --- .../overview/course-overview.component.html | 39 ++++- .../course-exercise-details.component.html | 130 +++++--------- .../course-exercise-details.component.ts | 159 ++++++++++++++++-- .../webapp/i18n/de/student-dashboard.json | 2 +- .../webapp/i18n/en/student-dashboard.json | 4 +- 5 files changed, 225 insertions(+), 109 deletions(-) diff --git a/src/main/webapp/app/overview/course-overview.component.html b/src/main/webapp/app/overview/course-overview.component.html index 2cd6c2c5c4ee..b30269a5171c 100644 --- a/src/main/webapp/app/overview/course-overview.component.html +++ b/src/main/webapp/app/overview/course-overview.component.html @@ -198,10 +198,41 @@ - } - - - +
+ @if (isNotManagementView && course.isAtLeastTutor) { + + } + @if (showRefreshButton) { + + } +
+ +
+ @if (!hasSidebar) { + + } + +
+ +
+
+ } + + } @else { diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html index 29582939ece9..6a5a2ffe77a9 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html @@ -1,5 +1,47 @@ @if (exercise) {
+
+
{{ exercise.title }}
+
+ @if (exercise.isAtLeastTutor) { +
+ +
+ @for (instructorActionItem of instructorActionItems; track instructorActionItem) { + + } +
+
+ } + + @if (plagiarismCaseInfo && plagiarismCaseInfo.verdict !== PlagiarismVerdict.NO_PLAGIARISM) { + + @if (!plagiarismCaseInfo?.createdByContinuousPlagiarismControl) { + + } + @if (plagiarismCaseInfo?.createdByContinuousPlagiarismControl) { + + } + + } +
+
+ +
+ {{ exercise.title }}
- - @if (plagiarismCaseInfo && plagiarismCaseInfo.verdict !== PlagiarismVerdict.NO_PLAGIARISM) { - - @if (!plagiarismCaseInfo?.createdByContinuousPlagiarismControl) { - - } - @if (plagiarismCaseInfo?.createdByContinuousPlagiarismControl) { - - } - - }
- @if (exercise.isAtLeastTutor) { -
- {{ - 'artemisApp.courseOverview.exerciseDetails.instructorActions.title' + (exercise.isAtLeastInstructor ? '' : 'Tutor') | artemisTranslate - }} -
- - - - - - - - - - @if (exercise.type !== QUIZ) { - - - - - } - @if (exercise.type === QUIZ) { - - - - - } - @if (exercise.type === QUIZ) { - - - - - } - @if (exercise.isAtLeastEditor) { - @if (exercise.type === QUIZ) { - - - - - } - @if (exercise.type === MODELING) { - - - - - } - @if (exercise.type === PROGRAMMING) { - - - - - } - @if (!QUIZ_ENDED_STATUS.includes(quizExerciseStatus)) { - - - - - } - @if (QUIZ_ENDED_STATUS.includes(quizExerciseStatus) && exercise.isAtLeastInstructor) { - - - - - } - } -
-
- }
@@ -213,6 +169,8 @@

} } + + @if ( exercise && diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts index a8c94911d1cc..d77535faa67d 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts @@ -29,7 +29,7 @@ import { ComplaintService } from 'app/complaints/complaint.service'; import { Complaint } from 'app/entities/complaint.model'; import { SubmissionPolicy } from 'app/entities/submission-policy.model'; import { ArtemisMarkdownService } from 'app/shared/markdown.service'; -import { faAngleDown, faAngleUp, faBook, faEye, faFileSignature, faListAlt, faSignal, faTable, faWrench } from '@fortawesome/free-solid-svg-icons'; +import { IconDefinition, faAngleDown, faAngleUp, faBook, faEye, faFileSignature, faListAlt, faSignal, faTable, faWrench } from '@fortawesome/free-solid-svg-icons'; import { ExerciseHintService } from 'app/exercises/shared/exercise-hint/shared/exercise-hint.service'; import { ExerciseHint } from 'app/entities/hestia/exercise-hint.model'; import { PlagiarismVerdict } from 'app/exercises/shared/plagiarism/types/PlagiarismVerdict'; @@ -45,6 +45,11 @@ import { ScienceEventType } from 'app/shared/science/science.model'; import { PROFILE_IRIS } from 'app/app.constants'; import { ChatServiceMode } from 'app/iris/iris-chat.service'; +interface InstructorActionItem { + routerLink: string; + icon?: IconDefinition; + translation: string; +} @Component({ selector: 'jhi-course-exercise-details', templateUrl: './course-exercise-details.component.html', @@ -56,6 +61,7 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp readonly PlagiarismVerdict = PlagiarismVerdict; readonly QuizStatus = QuizStatus; readonly QUIZ_ENDED_STATUS: (QuizStatus | undefined)[] = [QuizStatus.CLOSED, QuizStatus.OPEN_FOR_PRACTICE]; + readonly QUIZ_EDITABLE_STATUS: (QuizStatus | undefined)[] = [QuizStatus.VISIBLE, QuizStatus.INVISIBLE]; readonly QUIZ = ExerciseType.QUIZ; readonly PROGRAMMING = ExerciseType.PROGRAMMING; readonly MODELING = ExerciseType.MODELING; @@ -100,6 +106,7 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp isProduction = true; isTestServer = false; isGeneratingFeedback: boolean = false; + instructorActionItems: InstructorActionItem[] = []; exampleSolutionInfo?: ExampleSolutionInfo; @@ -165,21 +172,6 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp }); } - ngOnDestroy() { - if (this.participationUpdateListener) { - this.participationUpdateListener.unsubscribe(); - if (this.studentParticipations) { - this.studentParticipations.forEach((participation) => { - this.participationWebsocketService.unsubscribeForLatestResultOfParticipation(participation.id!, this.exercise!); - }); - } - } - this.teamAssignmentUpdateListener?.unsubscribe(); - this.submissionSubscription?.unsubscribe(); - this.paramsSubscription?.unsubscribe(); - this.profileSubscription?.unsubscribe(); - } - loadExercise() { this.irisSettings = undefined; this.studentParticipations = this.participationWebsocketService.getParticipationsForExercise(this.exerciseId); @@ -224,6 +216,8 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp this.subscribeToTeamAssignmentUpdates(); this.baseResource = `/course-management/${this.courseId}/${this.exercise.type}-exercises/${this.exercise.id}/`; + + this.createInstructorActions(); } /** @@ -444,4 +438,137 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp } setIsGeneratingFeedback() {} + + // INSTRUCTOR ACTIONS + createInstructorActions() { + if (this.exercise?.isAtLeastTutor) { + this.instructorActionItems = this.createTutorActions(); + } + if (this.exercise?.isAtLeastEditor) { + const editorItems = this.createEditorActions(); + editorItems.forEach((editorItem) => this.instructorActionItems.push(editorItem)); + } + if (this.exercise?.isAtLeastInstructor && this.QUIZ_ENDED_STATUS.includes(this.quizExerciseStatus)) { + const reEvaluateItem: InstructorActionItem = this.getReEvaluateItem(); + this.instructorActionItems.push(reEvaluateItem); + } + } + createTutorActions(): InstructorActionItem[] { + const instructorActionItems = this.getDefaultItems(); + if (this.exercise?.type === ExerciseType.QUIZ) { + const quizItems: InstructorActionItem[] = this.getQuizItems(); + quizItems.forEach((quizItem) => instructorActionItems.push(quizItem)); + } else { + const participationsItem: InstructorActionItem = this.getParticipationItem(); + instructorActionItems.push(participationsItem); + } + return instructorActionItems; + } + + getDefaultItems(): InstructorActionItem[] { + const exercisesItem: InstructorActionItem = { + routerLink: `${this.baseResource}`, + icon: faEye, + translation: 'entity.action.view', + }; + + const statisticsItem: InstructorActionItem = { + routerLink: `${this.baseResource}scores`, + icon: faTable, + translation: 'entity.action.scores', + }; + + return [exercisesItem, statisticsItem]; + } + + getQuizItems(): InstructorActionItem[] { + const previewItem: InstructorActionItem = { + routerLink: `${this.baseResource}preview`, + icon: faEye, + translation: 'artemisApp.quizExercise.preview', + }; + const solutionItem: InstructorActionItem = { + routerLink: `${this.baseResource}solution`, + icon: faEye, + translation: 'artemisApp.quizExercise.solution', + }; + + return [previewItem, solutionItem]; + } + + getParticipationItem(): InstructorActionItem { + return { + routerLink: `${this.baseResource}participations`, + icon: faListAlt, + translation: 'artemisApp.exercise.participations', + }; + } + + createEditorActions(): InstructorActionItem[] { + const editorItems: InstructorActionItem[] = []; + if (this.exercise?.type === ExerciseType.QUIZ) { + const statisticItem: InstructorActionItem = this.getStatisticItem('quiz-point-statistic'); + editorItems.push(statisticItem); + } + if (this.exercise?.type === ExerciseType.MODELING) { + const statisticItem: InstructorActionItem = this.getStatisticItem('exercise-statistics'); + editorItems.push(statisticItem); + } + if (this.exercise?.type === ExerciseType.PROGRAMMING) { + const gradingItem: InstructorActionItem = this.getGradingItem(); + editorItems.push(gradingItem); + } + if (this.QUIZ_EDITABLE_STATUS.includes(this.quizExerciseStatus)) { + const quizEditItem: InstructorActionItem = this.getQuizEditItem(); + editorItems.push(quizEditItem); + } + return editorItems; + } + + getStatisticItem(routerLink: string): InstructorActionItem { + return { + routerLink: `${this.baseResource}${routerLink}`, + icon: faSignal, + translation: 'artemisApp.courseOverview.exerciseDetails.instructorActions.statistics', + }; + } + + getGradingItem(): InstructorActionItem { + return { + routerLink: `${this.baseResource}grading/test-cases`, + icon: faFileSignature, + translation: 'artemisApp.programmingExercise.configureGrading.shortTitle', + }; + } + + getQuizEditItem(): InstructorActionItem { + return { + routerLink: `${this.baseResource}edit`, + icon: faWrench, + translation: 'entity.action.edit', + }; + } + + getReEvaluateItem(): InstructorActionItem { + return { + routerLink: `${this.baseResource}re-evaluate`, + icon: faWrench, + translation: 'entity.action.re-evaluate', + }; + } + + ngOnDestroy() { + if (this.participationUpdateListener) { + this.participationUpdateListener.unsubscribe(); + if (this.studentParticipations) { + this.studentParticipations.forEach((participation) => { + this.participationWebsocketService.unsubscribeForLatestResultOfParticipation(participation.id!, this.exercise!); + }); + } + } + this.teamAssignmentUpdateListener?.unsubscribe(); + this.submissionSubscription?.unsubscribe(); + this.paramsSubscription?.unsubscribe(); + this.profileSubscription?.unsubscribe(); + } } diff --git a/src/main/webapp/i18n/de/student-dashboard.json b/src/main/webapp/i18n/de/student-dashboard.json index 3c15b1c3987c..e3b10a241abe 100644 --- a/src/main/webapp/i18n/de/student-dashboard.json +++ b/src/main/webapp/i18n/de/student-dashboard.json @@ -282,7 +282,7 @@ "problemStatement": "Problemstellung:", "noResults": "Du hast noch keine Ergebnisse", "instructorActions": { - "title": "Aktionen für Lehrende:", + "title": "Aktionen für Lehrende", "titleTutor": "Aktionen für Tutor:innen", "scores": "Ergebnisse", "statistics": "Statistiken" diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index 1e51fba7c618..d90120adab6a 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -282,8 +282,8 @@ "problemStatement": "Problem statement:", "noResults": "You have no results yet", "instructorActions": { - "title": "Instructor actions:", - "titleTutor": "Tutor actions:", + "title": "Instructor actions", + "titleTutor": "Tutor actions", "scores": "Scores", "statistics": "Statistics" }, From 9f4f80cdcb3a019b40c55432a829b5b1428b0e60 Mon Sep 17 00:00:00 2001 From: Ramona Beinstingel Date: Mon, 13 May 2024 10:24:30 +0200 Subject: [PATCH 02/23] change icons in sidebar --- ...-exercise-page-with-details.component.html | 96 ++++----- ...er-exercise-page-with-details.component.ts | 196 +++++++++++++++++- .../app/overview/course-overview.component.ts | 6 +- .../course-exercise-details.component.html | 8 +- .../course-exercise-details.component.ts | 10 +- ...ise-details-student-actions.component.html | 1 + .../submission-result-status.component.html | 36 ++-- .../not-released-tag.component.html | 2 +- .../components/not-released-tag.component.ts | 2 +- .../difficulty-level.component.html | 5 + .../difficulty-level.component.scss | 4 + .../difficulty-level.component.spec.ts | 25 +++ .../difficulty-level.component.ts | 44 ++++ .../exercise-categories.component.html | 22 +- .../exercise-categories.component.ts | 1 + .../information-box.component.html | 5 + .../information-box.component.ts | 12 ++ src/main/webapp/app/shared/shared.module.ts | 9 + .../webapp/i18n/de/programmingExercise.json | 6 +- .../webapp/i18n/de/student-dashboard.json | 45 ++-- .../webapp/i18n/en/programmingExercise.json | 6 +- .../webapp/i18n/en/student-dashboard.json | 43 ++-- .../shared/information-box.component.spec.ts | 25 +++ 23 files changed, 464 insertions(+), 145 deletions(-) create mode 100644 src/main/webapp/app/shared/difficulty-level/difficulty-level.component.html create mode 100644 src/main/webapp/app/shared/difficulty-level/difficulty-level.component.scss create mode 100644 src/main/webapp/app/shared/difficulty-level/difficulty-level.component.spec.ts create mode 100644 src/main/webapp/app/shared/difficulty-level/difficulty-level.component.ts create mode 100644 src/test/javascript/spec/component/shared/information-box.component.spec.ts diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.html b/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.html index 2ed3b9d18422..4ad0e362f258 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.html +++ b/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.html @@ -1,4 +1,38 @@ @if (exercise) { +
+ + @for (informationBoxItem of informationBoxItems; track informationBoxItem) { + + @if (informationBoxItem.contentComponent === 'difficultyLevel') { + + } + @if (informationBoxItem.contentComponent === 'categories') { + + } + @if (informationBoxItem.contentType === 'timeAgo') { + {{ informationBoxItem.content | artemisTimeAgo }} + } + + @if (informationBoxItem.contentComponent === 'submissionStatus') { + + } + + } +
@@ -10,7 +44,7 @@
@if ((exercise.releaseDate && dayjs(exercise.releaseDate).isAfter(dayjs())) || exercise.difficulty || exerciseCategories?.length) { - + }
@if (exercise.maxPoints || (exercise.assessmentType && exercise.type === ExerciseType.PROGRAMMING)) { @@ -32,7 +66,7 @@ } } - @if (exercise.assessmentType && exercise.type === ExerciseType.PROGRAMMING) { +
} - @if (submissionPolicy && submissionPolicy.active) { +

- @if (!nextRelevantDateLabel || (nextRelevantDateLabel !== 'releaseDate' && nextRelevantDateLabel !== 'startDate')) { - } @else { - @if (nextRelevantDate && (!exam || !isTestRun)) { -
- {{ 'artemisApp.courseOverview.exerciseDetails.' + nextRelevantDateLabel | artemisTranslate }} - - {{ nextRelevantDate | artemisTimeAgo }} - -
- } - @if (exercise.presentationScoreEnabled) { -
- @if (course?.presentationScore) { - {{ 'artemisApp.courseOverview.exerciseDetails.presented' | artemisTranslate }} - @if ((studentParticipation?.presentationScore ?? 0) > 0) { - - } - @if ((studentParticipation?.presentationScore ?? 0) <= 0) { - - } - } @else { - {{ 'artemisApp.courseOverview.exerciseDetails.presentation' | artemisTranslate }} - @if (studentParticipation?.presentationScore) { - - {{ studentParticipation!.presentationScore + '%' }} - - } - @if (!studentParticipation?.presentationScore) { - - } - } -
- } - } - @if (dueDate) { -
- {{ 'artemisApp.courseOverview.exerciseDetails.submissionDue' | artemisTranslate }} - - {{ dueDate | artemisTimeAgo }} - -
- } @if (!nextRelevantDateLabel || (nextRelevantDateLabel !== 'assessmentDue' && nextRelevantDateLabel !== 'complaintDue')) { } @else { @if (nextRelevantDate && (!exam || !isTestRun)) { diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.ts b/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.ts index b89694846761..7da9d6e7db59 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.ts @@ -16,11 +16,27 @@ import { ComplaintService } from 'app/complaints/complaint.service'; import { SubmissionType } from 'app/entities/submission.model'; import { ProgrammingSubmission } from 'app/entities/programming/programming-submission.model'; import { roundValueSpecifiedByCourseSettings } from 'app/shared/util/utils'; +import { TranslateService } from '@ngx-translate/core'; +export interface InformationBox { + title: string; + content: string | number | any; + contentType?: string; + isContentComponent?: boolean; + contentComponent?: any; + icon?: IconProp; + tooltip?: string; + contentColor?: string; + tooltipParams?: any; +} @Component({ selector: 'jhi-header-exercise-page-with-details', templateUrl: './header-exercise-page-with-details.component.html', styleUrls: ['./header-exercise-page-with-details.component.scss'], + // Our tsconfig file has `preserveWhitespaces: 'true'` which causes whitespace to affect content projection. + // We need to set it to 'false 'for this component, otherwise the components with the selecotor [contentComponent] + // will not be projected into their specific slot of the "InformationBoxComponent" component. + preserveWhitespaces: false, }) export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit { readonly IncludedInOverallScore = IncludedInOverallScore; @@ -50,13 +66,17 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit public canComplainLaterOn: boolean; public achievedPoints?: number; public numberOfSubmissions: number; + public informationBoxItems: InformationBox[] = []; icon: IconProp; // Icons faQuestionCircle = faQuestionCircle; - constructor(private sortService: SortService) {} + constructor( + private sortService: SortService, + private translateService: TranslateService, + ) {} ngOnInit() { this.exerciseCategories = this.exercise.categories || []; @@ -90,15 +110,173 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit } if (this.dueDate) { - this.dueDateStatusBadge = dayjs().isBefore(this.dueDate) ? 'bg-success' : 'bg-danger'; + // If the due date is less than a day away, the color change to red + this.dueDateStatusBadge = this.dueDate.isBetween(dayjs().add(1, 'day'), dayjs()) ? 'danger' : 'body-color'; } + this.createInformationBoxItems(); + } + + createInformationBoxItems() { + const notReleased = this.exercise.releaseDate && dayjs(this.exercise.releaseDate).isAfter(dayjs()); + if (this.exercise.maxPoints) this.informationBoxItems.push(this.getMaxPointsItem()); + this.informationBoxItems.push(this.getDueDateItem()); + this.informationBoxItems.push(this.getDifficultyItem()); + // (exercise.releaseDate && dayjs(exercise.releaseDate).isAfter(dayjs())) + if (notReleased || this.exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY || this.exercise.categories?.length) + this.informationBoxItems.push(this.getCategoryItems()); + // this.informationBoxItems.push(this.getNextRelevantDateItem()); + // if (this.submissionPolicy?.active) this.informationBoxItems.push(this.getSubmissionPolicyItem()); + this.informationBoxItems.push(this.getSubmissionStatusItem()); + + if (this.exercise.assessmentType && this.exercise.type === ExerciseType.PROGRAMMING) this.informationBoxItems.push(this.getAssessmentTypeItem()); } + getDueDateItem(): InformationBox { + return { + title: 'artemisApp.courseOverview.exerciseDetails.submissionDue', + // less than a day make time relative to now + content: this.dueDate, + // content: this.dueDate?.format('lll') ?? '-', + // icon: this.icon, + isContentComponent: true, + contentType: 'timeAgo', + tooltip: 'artemisApp.courseOverview.exerciseDetails.submissionDueTooltip', + contentColor: this.dueDateStatusBadge, + tooltipParams: { date: this.dueDate?.format('lll') }, + }; + } + // Status: Not released, no graded, graded, submitted, reviewed, assessed, complaint, complaint response, complaint applied, complaint resolved + // getStatusItem(): InformationBox { + + // } + + getAssessmentTypeItem(): InformationBox { + return { + title: 'artemisApp.courseOverview.exerciseDetails.assessmentType', + content: this.capitalize(this.exercise?.assessmentType), + tooltip: 'artemisApp.AssessmentType.tooltip.' + this.exercise.assessmentType, + }; + } + + capitalize(title?: string) { + if (!title) return '-'; + return title.toString().charAt(0).toUpperCase() + title.slice(1).toLowerCase(); + } + getDifficultyItem(): InformationBox { + return { + title: 'artemisApp.courseOverview.exerciseDetails.difficulty', + content: this.exercise.difficulty, + contentComponent: 'difficultyLevel', + isContentComponent: true, + }; + } + getSubmissionStatusItem(): InformationBox { + return { + title: 'artemisApp.courseOverview.exerciseDetails.submissionStatus', + content: this.studentParticipation, + contentComponent: 'submissionStatus', + isContentComponent: true, + }; + } + getCategoryItems(): InformationBox { + return { + title: 'artemisApp.courseOverview.exerciseDetails.categories', + content: this.exercise, + contentComponent: 'categories', + isContentComponent: true, + }; + } + + getSubmissionPolicyItem(): InformationBox { + console.log(this.submissionPolicy); + // {{ 'artemisApp.programmingExercise.submissionPolicy.submissionLimitTitle' | artemisTranslate }}: + // {{ + // numberOfSubmissions + + // '/' + + // submissionPolicy.submissionLimit + + // (submissionPolicy.exceedingPenalty + // ? ('artemisApp.programmingExercise.submissionPolicy.submissionPenalty.penaltyInfoLabel' + // | artemisTranslate: { points: submissionPolicy.exceedingPenalty }) + // : '') + // }} + + // TODO Make Red if submissionLimit is (nearly) reached + return { + title: 'artemisApp.programmingExercise.submissionPolicy.submissionLimitTitle', + content: this.numberOfSubmissions + ' / ' + this.submissionPolicy?.submissionLimit, + // content: + // this.numberOfSubmissions + + // '/' + + // this.submissionPolicy?.submissionLimit + + // (this.submissionPolicy?.exceedingPenalty + // ? ' ' + this.translateService.instant('artemisApp.programmingExercise.submissionPolicy.submissionPenalty.penaltyInfoLabel', { + // points: this.submissionPolicy.exceedingPenalty, + // }) + // : ''), + tooltip: 'artemisApp.programmingExercise.submissionPolicy.submissionPolicyType.' + this.submissionPolicy?.type + '.tooltip', + }; + } + + getExceedingPenalty() { + return { + title: 'artemisApp.programmingExercise.submissionPolicy.submissionPolicyType.submission_penalty.title', + content: '-' + this.submissionPolicy?.exceedingPenalty + ' Points', + }; + } + + // Can be visible in the tooltip above a status + // getNextRelevantDateItem(): InformationBox { + // console.log('get Next Relevant Date Item') + // console.log(this.nextRelevantDateLabel) + // // {{ 'artemisApp.courseOverview.exerciseDetails.' + nextRelevantDateLabel | artemisTranslate }} + // return { + // title: this.nextRelevantDateLabel ? this.nextRelevantDateLabel : 'Next Relevant Date', + // content: this.nextRelevantDate?.format('lll') ?? '-', + // icon: faQuestionCircle, + // }; + + // } + + getMaxPointsItem(): InformationBox { + if (this.exercise.bonusPoints) { + const pointsAndBonusTitle = 'artemisApp.courseOverview.exerciseDetails.pointsAndBonus'; + const pointsAndBonusContent = this.exercise.maxPoints + ` + ${this.exercise.bonusPoints}`; + return { + title: pointsAndBonusTitle, + content: this.achievedPoints !== undefined ? this.achievedPoints + ' / ' + pointsAndBonusContent : pointsAndBonusContent, + }; + } + return { + title: 'artemisApp.courseOverview.exerciseDetails.points', + content: this.achievedPoints !== undefined ? this.achievedPoints + ' / ' + this.exercise.maxPoints : this.exercise.maxPoints ?? '-', + }; + } + // getDefaultItems(): InformationBox[] { + // const exercisesItem: InformationBox = { + // title: `${this.baseResource}`, + // icon: faEye, + // content: 'entity.action.view', + // }; + + // const statisticsItem: InformationBox = { + // routerLink: `${this.baseResource}scores`, + // icon: faTable, + // translation: 'entity.action.scores', + // }; + + // return [exercisesItem, statisticsItem]; + // } + ngOnChanges() { this.course = this.course ?? getCourseFromExercise(this.exercise); if (this.submissionPolicy?.active) { + console.log('Changes Submission'); this.countSubmissions(); + this.informationBoxItems.push(this.getSubmissionPolicyItem()); + if (this.submissionPolicy?.exceedingPenalty) { + this.informationBoxItems.push(this.getExceedingPenalty()); + } } if (this.studentParticipation?.results?.length) { // The updated participation by the websocket is not guaranteed to be sorted, find the newest result (highest id) @@ -125,6 +303,7 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit * Determines the next date of the course exercise cycle. If none exists the latest date in the past is determined */ private determineNextRelevantDateCourseMode() { + console.log('Hi'); const possibleDates = [this.exercise.releaseDate, this.exercise.startDate, this.exercise.assessmentDueDate, this.individualComplaintDueDate]; const possibleDatesLabels = ['releaseDate', 'startDate', 'assessmentDue', 'complaintDue']; @@ -143,21 +322,31 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit this.nextRelevantDate = undefined; this.nextRelevantDateLabel = undefined; this.nextRelevantDateStatusBadge = undefined; - + console.log('Determine Next Date'); + console.log(dates); + console.log(dateLabels); for (let i = 0; i < dates.length; i++) { if (dates[i] && now.isBefore(dates[i])) { + console.log('If Satetment'); this.nextRelevantDate = dates[i]!; this.nextRelevantDateLabel = dateLabels[i]; this.nextRelevantDateStatusBadge = 'bg-success'; return; } } + + console.log(this.nextRelevantDateLabel); if (this.canComplainLaterOn) { return; } for (let i = dates.length - 1; i >= 0; i--) { + console.log(i); if (dates[i]) { + console.log(i); + console.log('If Satetment3'); if (this.dueDate && this.dueDate.isAfter(dates[i])) { + console.log('If Satetment2'); + console.log(this.nextRelevantDateLabel); return; } @@ -167,6 +356,7 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit return; } } + console.log('Haaaallo'); } private countSubmissions() { diff --git a/src/main/webapp/app/overview/course-overview.component.ts b/src/main/webapp/app/overview/course-overview.component.ts index fd9485c0efe3..f2b4a67a6a0e 100644 --- a/src/main/webapp/app/overview/course-overview.component.ts +++ b/src/main/webapp/app/overview/course-overview.component.ts @@ -20,6 +20,7 @@ import { faChalkboardUser, faChartBar, faChevronLeft, + faChartColumn, faChevronRight, faCircleNotch, faClipboard, @@ -179,6 +180,7 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit facSidebar = facSidebar; faEllipsis = faEllipsis; faQuestion = faQuestion; + faChartColumn = faChartColumn; FeatureToggle = FeatureToggle; CachingStrategy = CachingStrategy; @@ -496,7 +498,7 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit } const exercisesItem: SidebarItem = { routerLink: 'exercises', - icon: faListCheck, + icon: faListAlt, title: 'Exercises', translation: 'artemisApp.courseOverview.menu.exercises', hidden: false, @@ -504,7 +506,7 @@ export class CourseOverviewComponent implements OnInit, OnDestroy, AfterViewInit const statisticsItem: SidebarItem = { routerLink: 'statistics', - icon: faListAlt, + icon: faChartColumn, title: 'Statistics', translation: 'artemisApp.courseOverview.menu.statistics', hasInOrionProperty: true, diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html index 6a5a2ffe77a9..e7403063f46d 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html @@ -1,7 +1,12 @@ @if (exercise) {
-
{{ exercise.title }}
+
+ +
{{ exercise.title }}
+
@if (exercise.isAtLeastTutor) {
@@ -50,6 +55,7 @@
{{ exercise.title }}
> {{ exercise.title }} +
diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts index d77535faa67d..8e6022407d6d 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts @@ -11,7 +11,7 @@ import { GuidedTourService } from 'app/guided-tour/guided-tour.service'; import { programmingExerciseFail, programmingExerciseSuccess } from 'app/guided-tour/tours/course-exercise-detail-tour'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { Participation } from 'app/entities/participation/participation.model'; -import { Exercise, ExerciseType } from 'app/entities/exercise.model'; +import { Exercise, ExerciseType, getIcon, getIconTooltip } from 'app/entities/exercise.model'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; import { ExampleSolutionInfo, ExerciseDetailsType, ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { AssessmentType } from 'app/entities/assessment-type.model'; @@ -44,6 +44,7 @@ import { ScienceService } from 'app/shared/science/science.service'; import { ScienceEventType } from 'app/shared/science/science.model'; import { PROFILE_IRIS } from 'app/app.constants'; import { ChatServiceMode } from 'app/iris/iris-chat.service'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; interface InstructorActionItem { routerLink: string; @@ -107,6 +108,8 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp isTestServer = false; isGeneratingFeedback: boolean = false; instructorActionItems: InstructorActionItem[] = []; + iconTooltip: string; + exerciseIcon: IconProp; exampleSolutionInfo?: ExampleSolutionInfo; @@ -216,7 +219,10 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp this.subscribeToTeamAssignmentUpdates(); this.baseResource = `/course-management/${this.courseId}/${this.exercise.type}-exercises/${this.exercise.id}/`; - + if (this.exercise?.type) { + this.iconTooltip = getIconTooltip(this.exercise?.type); + this.exerciseIcon = getIcon(this.exercise?.type); + } this.createInstructorActions(); } diff --git a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html index d23e8a057abf..8c5075e398fb 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html +++ b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html @@ -1,5 +1,6 @@
@switch (exercise.type) { + @case (ExerciseType.QUIZ) {
diff --git a/src/main/webapp/app/overview/submission-result-status.component.html b/src/main/webapp/app/overview/submission-result-status.component.html index 2cc0eaac36b4..69d80a4356d9 100644 --- a/src/main/webapp/app/overview/submission-result-status.component.html +++ b/src/main/webapp/app/overview/submission-result-status.component.html @@ -1,4 +1,5 @@
+ {{ shouldShowResult }} @if (shouldShowResult) { @@ -18,30 +19,27 @@ } @else {
@if (exercise.teamMode && exercise.studentAssignedTeamIdComputed && !exercise.studentAssignedTeamId) { - - } - @if (uninitialized) { - - } - @if (exerciseMissedDueDate) { - - } - @if (notSubmitted) { - - } - @if (!notSubmitted && studentParticipation?.initializationState === InitializationState.FINISHED) { - - } - @if (studentParticipation?.initializationState === InitializationState.INITIALIZED && exercise.type === ExerciseType.QUIZ) { - - } - @if (quizNotStarted) { - + {{ 'artemisApp.courseOverview.exerciseList.userNotAssignedToTeamShort' | artemisTranslate }} + } @else if (uninitialized) { + {{ 'artemisApp.courseOverview.exerciseList.userNotStartedExerciseShort' | artemisTranslate }} + } @else if (exerciseMissedDueDate) { + {{ 'artemisApp.courseOverview.exerciseList.exerciseMissedDueDateShort' | artemisTranslate }} + } @else if (notSubmitted) { + {{ 'artemisApp.courseOverview.exerciseList.exerciseNotSubmittedShort' | artemisTranslate }} + } @else if (!notSubmitted && studentParticipation?.initializationState === InitializationState.FINISHED) { + {{ 'artemisApp.courseOverview.exerciseList.userSubmittedShort' | artemisTranslate }} + } @else if (studentParticipation?.initializationState === InitializationState.INITIALIZED && exercise.type === ExerciseType.QUIZ) { + {{ 'artemisApp.courseOverview.exerciseList.userParticipatingShort' | artemisTranslate }} + } @else if (quizNotStarted) { + {{ 'artemisApp.courseOverview.exerciseList.quizNotStartedShort' | artemisTranslate }} + } @else { + - }
} @if (exercise.type === ExerciseType.PROGRAMMING && studentParticipation) { + missed }
diff --git a/src/main/webapp/app/shared/components/not-released-tag.component.html b/src/main/webapp/app/shared/components/not-released-tag.component.html index 3a2f4c880ba1..44276b4dabaf 100644 --- a/src/main/webapp/app/shared/components/not-released-tag.component.html +++ b/src/main/webapp/app/shared/components/not-released-tag.component.html @@ -1,6 +1,6 @@ @if (exercise.releaseDate && dayjs(exercise.releaseDate).isAfter(dayjs())) { +
+
+
+
diff --git a/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.scss b/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.scss new file mode 100644 index 000000000000..e621c444014f --- /dev/null +++ b/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.scss @@ -0,0 +1,4 @@ +.skill-bar { + width: 1.2rem; + height: 0.6rem; +} diff --git a/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.spec.ts b/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.spec.ts new file mode 100644 index 000000000000..b2577461c8de --- /dev/null +++ b/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.spec.ts @@ -0,0 +1,25 @@ +/* tslint:disable:no-unused-variable */ +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; + +import { DifficultyLevelComponent } from './difficulty-level.component'; + +describe('DifficultyLevelComponent', () => { + let component: DifficultyLevelComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [DifficultyLevelComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DifficultyLevelComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.ts b/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.ts new file mode 100644 index 000000000000..8c175a538e42 --- /dev/null +++ b/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.ts @@ -0,0 +1,44 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { DifficultyLevel } from 'app/entities/exercise.model'; +import { Subscription } from 'rxjs'; + +export interface ColoredDifficultyLevel { + label: string; + color: string[]; +} +@Component({ + selector: 'jhi-difficulty-level', + templateUrl: './difficulty-level.component.html', + styleUrls: ['./difficulty-level.component.scss'], +}) +export class DifficultyLevelComponent implements OnInit, OnDestroy { + private translateSubscription: Subscription; + @Input() difficultyLevel: string; + coloredDifficultyLevel: ColoredDifficultyLevel = { label: '', color: [] }; + + constructor(private translateService: TranslateService) {} + + ngOnInit(): void { + this.translateSubscription = this.translateService.onLangChange.subscribe(() => { + this.mapDifficultyLevelToColors(this.difficultyLevel); + }); + this.coloredDifficultyLevel = this.mapDifficultyLevelToColors(this.difficultyLevel); + } + + mapDifficultyLevelToColors(difficultyLevel: string): ColoredDifficultyLevel { + switch (difficultyLevel) { + case DifficultyLevel.EASY: + return { label: this.translateService.instant('artemisApp.exercise.easy'), color: [...Array(1).fill('success'), ...Array(2).fill('body')] }; + case DifficultyLevel.MEDIUM: + return { label: this.translateService.instant('artemisApp.exercise.medium'), color: [...Array(2).fill('warning'), ...Array(1).fill('body')] }; + case DifficultyLevel.HARD: + return { label: this.translateService.instant('artemisApp.exercise.hard'), color: [...Array(3).fill('danger')] }; + } + return { label: this.translateService.instant('artemisApp.exercise.noLevel'), color: [...Array(3).fill('body')] }; + } + + ngOnDestroy(): void { + this.translateSubscription?.unsubscribe(); + } +} diff --git a/src/main/webapp/app/shared/exercise-categories/exercise-categories.component.html b/src/main/webapp/app/shared/exercise-categories/exercise-categories.component.html index f811a3f4c4e7..49b8cfe9756b 100644 --- a/src/main/webapp/app/shared/exercise-categories/exercise-categories.component.html +++ b/src/main/webapp/app/shared/exercise-categories/exercise-categories.component.html @@ -1,27 +1,29 @@ @if (exercise.releaseDate && dayjs(exercise.releaseDate).isAfter(dayjs()) && showTags.notReleased) { -

+ -

+ } @if (asQuizExercise(exercise).isActiveQuiz && showTags.quizLive) { -

- -

+ + {{ 'artemisApp.courseOverview.exerciseList.live' | artemisTranslate }} + } @for (category of exercise.categories; track category) { - + + {{ category.category }} + } @if (exercise.difficulty && showTags.difficulty) { -

+ -

+ } @if (exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY && showTags.includedInScore) { -

+ -

+ } diff --git a/src/main/webapp/app/shared/exercise-categories/exercise-categories.component.ts b/src/main/webapp/app/shared/exercise-categories/exercise-categories.component.ts index 43ca30b06c3e..065c59b801fc 100644 --- a/src/main/webapp/app/shared/exercise-categories/exercise-categories.component.ts +++ b/src/main/webapp/app/shared/exercise-categories/exercise-categories.component.ts @@ -21,6 +21,7 @@ export class ExerciseCategoriesComponent { readonly dayjs = dayjs; @Input() exercise: Exercise; + @Input() isSmall: boolean = false; @Input() showTags: ShowTagsConfig = { diff --git a/src/main/webapp/app/shared/information-box/information-box.component.html b/src/main/webapp/app/shared/information-box/information-box.component.html index 93a62b96655d..c4f606ecc857 100644 --- a/src/main/webapp/app/shared/information-box/information-box.component.html +++ b/src/main/webapp/app/shared/information-box/information-box.component.html @@ -5,6 +5,11 @@ ngbTooltip="{{ informationBoxData.tooltip | artemisTranslate: informationBoxData.tooltipParams }}" >
+ + + @if (informationBoxData.contentComponent) { } @else { diff --git a/src/main/webapp/app/shared/information-box/information-box.component.ts b/src/main/webapp/app/shared/information-box/information-box.component.ts index 3e909eb46ee9..1af8b1135dad 100644 --- a/src/main/webapp/app/shared/information-box/information-box.component.ts +++ b/src/main/webapp/app/shared/information-box/information-box.component.ts @@ -1,6 +1,7 @@ import { Component, Input } from '@angular/core'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; export interface InformationBox { title: string; @@ -10,6 +11,17 @@ export interface InformationBox { tooltipParams?: Record; contentColor?: string; } +// export interface InformationBox { +// title: string; +// content: string | number | any; +// contentType?: string; +// isContentComponent?: boolean; +// contentComponent?: any; +// icon?: IconProp; +// tooltip?: string; +// contentColor?: string; +// tooltipParams?: any; +// } @Component({ standalone: true, diff --git a/src/main/webapp/app/shared/shared.module.ts b/src/main/webapp/app/shared/shared.module.ts index 9c0e71a1bc90..0969d4da17bd 100644 --- a/src/main/webapp/app/shared/shared.module.ts +++ b/src/main/webapp/app/shared/shared.module.ts @@ -27,6 +27,9 @@ import { StickyPopoverDirective } from 'app/shared/sticky-popover/sticky-popover import { ConfirmEntityNameComponent } from 'app/shared/confirm-entity-name/confirm-entity-name.component'; import { DetailOverviewNavigationBarComponent } from 'app/shared/detail-overview-navigation-bar/detail-overview-navigation-bar.component'; import { ScienceDirective } from 'app/shared/science/science.directive'; +import { SearchFilterComponent } from './search-filter/search-filter.component'; +import { InformationBoxComponent } from './information-box/information-box.component'; +import { DifficultyLevelComponent } from './difficulty-level/difficulty-level.component'; @NgModule({ imports: [ArtemisSharedLibsModule, ArtemisSharedCommonModule, ArtemisSharedPipesModule, RouterModule], @@ -55,6 +58,9 @@ import { ScienceDirective } from 'app/shared/science/science.directive'; AssessmentWarningComponent, StickyPopoverDirective, ScienceDirective, + SearchFilterComponent, + InformationBoxComponent, + DifficultyLevelComponent, ], exports: [ ArtemisSharedLibsModule, @@ -85,6 +91,9 @@ import { ScienceDirective } from 'app/shared/science/science.directive'; CompetencySelectionComponent, StickyPopoverDirective, ScienceDirective, + SearchFilterComponent, + InformationBoxComponent, + DifficultyLevelComponent, ], }) export class ArtemisSharedModule {} diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index ceae33b2a043..92b6e12c314d 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -587,7 +587,7 @@ "pattern": "Der Punktabzug muss eine Nummer größer als 0 sein.", "required": "Der Punktabzug muss gesetzt sein." }, - "penaltyInfoLabel": "(Punktabzug je Überschreitung: {{points}} Punkte)", + "penaltyInfoLabel": "(Je Überschreitung: - {{points}} Punkte)", "triggerAllInformation": "Wird der Punktabzug je Überschreitung des Abgabelimits oder das Abgabelimit selbst angepasst, muss nach dem Speichern der Abgaberichtlinie die 'Erneut Bewerten' Funktionalität genutzt werden, um die Ergebnisse der Teilnehmenden zu aktualisieren." }, "submissionPolicyType": { @@ -601,10 +601,10 @@ }, "submission_penalty": { "title": "Punktabzug je Überschreitung", - "tooltip": "Das System zieht für jede Abgabe, die das Abgabelimit überschreitet, Punkte vom Gesamtergebnis ab. Versuche, innerhalb der erlaubten Abgaben zu bleiben, um Abzüge zu vermeiden!" + "tooltip": "Das System zieht für jede Abgabe, die das Abgabelimit überschreitet, Punkte vom Gesamtergebnis ab." } }, - "submissionLimitTitle": "Abgabelimit", + "submissionLimitTitle": "Abgaben", "submissionLimitDescription": "Die Anzahl von erlaubten Abgaben, bis das System die ausgewählte Abgaberichtlinie durchsetzt.", "editInGradingInformation": "Das Abgabelimit kann nur in der Bewertungssicht der Programmieraufgabe bearbeitet und (de)aktiviert werden.", "goToGradingToEditInformation": "Gehe zur Bewertungsseite, um die Abgaberichtlinie zu bearbeiten.", diff --git a/src/main/webapp/i18n/de/student-dashboard.json b/src/main/webapp/i18n/de/student-dashboard.json index e3b10a241abe..4b84d3d71804 100644 --- a/src/main/webapp/i18n/de/student-dashboard.json +++ b/src/main/webapp/i18n/de/student-dashboard.json @@ -269,17 +269,18 @@ "selectLecture": "Bitte wähle eine Vorlesung aus." }, "exerciseDetails": { - "dueDate": "Einreichungsfrist:", - "runTestsAfterDueDate": "Testdurchlauf nach Einreichungsfrist:", - "complaintDueDate": "Beschwerdefrist:", - "categories": "Kategorien:", - "type": "Typ:", - "difficulty": "Schwierigkeit:", - "exerciseStatus": "Aufgabenstatus:", - "points": "Punkte:", - "bonus": "Bonus:", + "dueDate": "Einreichungsfrist", + "runTestsAfterDueDate": "Testdurchlauf nach Einreichungsfrist", + "complaintDueDate": "Beschwerdefrist", + "categories": "Kategorien", + "type": "Typ", + "difficulty": "Schwierigkeit", + "exerciseStatus": "Aufgabenstatus", + "points": "Punkte", + "bonus": "Bonus", + "pointsAndBonus": "Punkte + Bonus", "of": " von ", - "problemStatement": "Problemstellung:", + "problemStatement": "Problemstellung", "noResults": "Du hast noch keine Ergebnisse", "instructorActions": { "title": "Aktionen für Lehrende", @@ -288,34 +289,34 @@ "statistics": "Statistiken" }, "notParticipated": "Nicht teilgenommen", - "yourGradedResult": "Dein gewertetes Ergebnis:", - "recentResults": "Aktuelle Ergebnisse:", + "yourGradedResult": "Dein gewertetes Ergebnis", + "recentResults": "Aktuelle Ergebnisse", "noGradedResult": "Du hast noch kein gewertetes Ergebnis.", - "allResults": "Alle Ergebnisse:", + "allResults": "Alle Ergebnisse", "hideResults": "Ergebnisse verbergen", "showResults": "Alle Ergebnisse anzeigen", "graded": "Bewertet", "notGraded": "Nicht bewertet", "rated": "Bewertet", "practice": "Übung", - "assessmentType": "Bewertung:", - "releaseDate": "Veröffentlichungsdatum:", + "assessmentType": "Bewertung", + "releaseDate": "Veröffentlichungsdatum", "releaseDateTooltip": "Studierende können diese Aufgabe ab {{date}} sehen.", "startDate": "Startdatum:", "startDateTooltip": "Studierende können diese Aufgabe ab {{date}} bearbeiten.", "submissionDue": "Abgabe bis:", "submissionDueTooltip": "Die Abgabe ist bis {{date}} möglich.", - "exampleSolutionPublicationDate": "Veröffentlichungsdatum der Beispiellösung:", + "exampleSolutionPublicationDate": "Veröffentlichungsdatum der Beispiellösung", "exampleSolutionPublicationDateTooltip": "Die Beispiellösung wird am {{date}} veröffentlicht.", - "assessmentDue": "Bewertung bis:", + "assessmentDue": "Bewertung bis", "assessmentDueTooltip": "Die Frist für die Bewertung ist am {{date}}.", - "complaintDue": "Beschwerde bis:", + "complaintDue": "Beschwerde bis", "complaintDueTooltip": "Du kannst eine Beschwerde bis {{date}} einreichen.", - "complaintPossible": "Beschwerde möglich:", + "complaintPossible": "Beschwerde möglich", "complaintPossibleTooltip": "Nach Erhalt einer Bewertung wirst du {{days}} Tage zum Einreichen einer Beschwerde haben.", - "presented": "Präsentiert:", - "presentation": "Präsentation:", - "selectExercise": "Bitte wähle eine Aufgabe aus." + "presented": "Präsentiert", + "presentation": "Präsentation", + "submissionStatus": "Einreichungsstatus" } } } diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index df85bd7c0911..b44ec1d64999 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -587,7 +587,7 @@ "pattern": "The penalty must be a number greater than 0.", "required": "The penalty must be set." }, - "penaltyInfoLabel": "(Exceeding Penalty: {{points}} points)", + "penaltyInfoLabel": "(Per Exceeding: - {{points}} points)", "triggerAllInformation": "If the exceeding submission limit penalty or the submission limit is updated, the 'Re-evaluate' functionality must be used to update the participants' results." }, "submissionPolicyType": { @@ -601,10 +601,10 @@ }, "submission_penalty": { "title": "Submission Penalty", - "tooltip": "The system deducts points from your score for each submission that exceeds the submission limit. Try to stay within the submission limit to avoid penalties!" + "tooltip": "The system deducts points from your score for each submission that exceeds the submission limit." } }, - "submissionLimitTitle": "Submission limit", + "submissionLimitTitle": "Submissions", "submissionLimitDescription": "The number of submissions a participant can make before the system enforces the selected policy.", "editInGradingInformation": "The submission policy can only be edited and toggled on the grading page of the programming exercise!", "goToGradingToEditInformation": "Go to the grading page to edit submission policy.", diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index d90120adab6a..6f81743387bd 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -269,15 +269,16 @@ "selectLecture": " Please select a Lecture." }, "exerciseDetails": { - "dueDate": "Due date:", + "dueDate": "Due date", "runTestsAfterDueDate": "Tests run after due date:", "complaintDueDate": "Complaint due date:", - "categories": "Categories:", + "categories": "Categories", "type": "Type:", - "difficulty": "Difficulty:", - "exerciseStatus": "Exercise status:", - "points": "Points:", - "bonus": "Bonus:", + "difficulty": "Difficulty", + "exerciseStatus": "Exercise status", + "points": "Points", + "bonus": "Bonus", + "pointsAndBonus": "Points + Bonus", "of": " of ", "problemStatement": "Problem statement:", "noResults": "You have no results yet", @@ -288,34 +289,34 @@ "statistics": "Statistics" }, "notParticipated": "Not participated", - "yourGradedResult": "Your graded result:", - "recentResults": "Recent results:", + "yourGradedResult": "Your graded result", + "recentResults": "Recent results", "noGradedResult": "You have no graded result.", - "allResults": "All results:", + "allResults": "All results", "hideResults": "Hide results", "showResults": "Show all results", "graded": "Graded", "notGraded": "Not graded", "rated": "Graded", "practice": "Practice", - "assessmentType": "Assessment:", - "releaseDate": "Release date:", + "assessmentType": "Assessment", + "releaseDate": "Release date", "releaseDateTooltip": "Students can see this exercise from {{date}}.", - "startDate": "Start date:", + "startDate": "Start date", "startDateTooltip": "Students can work on this exercise from {{date}}.", - "submissionDue": "Submission due:", + "submissionDue": "Submission due", "submissionDueTooltip": "The submission is due by {{date}}.", - "exampleSolutionPublicationDate": "Example solution publication date:", + "exampleSolutionPublicationDate": "Example solution publication date", "exampleSolutionPublicationDateTooltip": "The example solution is published at {{date}}.", - "assessmentDue": "Assessment due:", + "assessmentDue": "Assessment due", "assessmentDueTooltip": "The assessment is due by {{date}}.", - "complaintDue": "Complaint due:", - "complaintDueTooltip": "You can write a complaint until {{date}}.", - "complaintPossible": "Complaint possible:", + "complaintDue": "Complaint due", + "complaintDueTooltip": "You can write a complain until {{date}}.", + "complaintPossible": "Complaint possible", "complaintPossibleTooltip": "After receiving an assessment, you will have {{days}} days to file a complaint.", - "presented": "Presented:", - "presentation": "Presentation:", - "selectExercise": "Please select an Exercise." + "presented": "Presented", + "presentation": "Presentation", + "submissionStatus": "Submission Status" } } } diff --git a/src/test/javascript/spec/component/shared/information-box.component.spec.ts b/src/test/javascript/spec/component/shared/information-box.component.spec.ts new file mode 100644 index 000000000000..61ede367a3cf --- /dev/null +++ b/src/test/javascript/spec/component/shared/information-box.component.spec.ts @@ -0,0 +1,25 @@ +/* tslint:disable:no-unused-variable */ +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; + +import { InformationBoxComponent } from '../../../../../main/webapp/app/shared/information-box/information-box.component'; + +describe('InformationBoxComponent', () => { + let component: InformationBoxComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [InformationBoxComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(InformationBoxComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); From fdc60bb1584ad293fd84d1a4da8ba6032b3c64ac Mon Sep 17 00:00:00 2001 From: Ramona Beinstingel Date: Mon, 3 Jun 2024 14:44:09 +0200 Subject: [PATCH 03/23] information box --- .../header-exercise-page-with-details.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.ts b/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.ts index 7da9d6e7db59..df7828249855 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.ts @@ -27,7 +27,7 @@ export interface InformationBox { icon?: IconProp; tooltip?: string; contentColor?: string; - tooltipParams?: any; + tooltipParams?: Record; } @Component({ selector: 'jhi-header-exercise-page-with-details', From a4bf4c88cb6edd8b5df782ee0c7f5c4faccd3d95 Mon Sep 17 00:00:00 2001 From: Ramona Beinstingel Date: Tue, 4 Jun 2024 17:09:30 +0200 Subject: [PATCH 04/23] combine submission policy remove assessment type --- .../example-solution.component.html | 1 + ...-exercise-page-with-details.component.html | 95 +------------------ ...er-exercise-page-with-details.component.ts | 86 ++++++++--------- .../shared/result/result.component.html | 27 +++--- .../submission-result-status.component.html | 2 - .../sidebar-card-item.component.html | 2 +- .../webapp/i18n/de/programmingExercise.json | 2 +- .../webapp/i18n/de/student-dashboard.json | 2 +- .../webapp/i18n/en/programmingExercise.json | 2 +- .../webapp/i18n/en/student-dashboard.json | 2 +- 10 files changed, 68 insertions(+), 153 deletions(-) diff --git a/src/main/webapp/app/exercises/shared/example-solution/example-solution.component.html b/src/main/webapp/app/exercises/shared/example-solution/example-solution.component.html index d6e797f436e0..17be32b388e1 100644 --- a/src/main/webapp/app/exercises/shared/example-solution/example-solution.component.html +++ b/src/main/webapp/app/exercises/shared/example-solution/example-solution.component.html @@ -1,4 +1,5 @@ @if (exercise) { +
@if (displayHeader) { diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.html b/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.html index 4ad0e362f258..8934d4eadfab 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.html +++ b/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.html @@ -15,9 +15,12 @@ [isSmall]="true" /> } - @if (informationBoxItem.contentType === 'timeAgo') { + @if (informationBoxItem.contentComponent === 'timeAgo') { {{ informationBoxItem.content | artemisTimeAgo }} } + @if (informationBoxItem.contentComponent === 'dateTime') { + {{ informationBoxItem.content | artemisDate }} + } @if (informationBoxItem.contentComponent === 'submissionStatus') { } }
-
-
-
- @if (exercise.type) { - - } -   - -
- @if ((exercise.releaseDate && dayjs(exercise.releaseDate).isAfter(dayjs())) || exercise.difficulty || exerciseCategories?.length) { - - } -
- @if (exercise.maxPoints || (exercise.assessmentType && exercise.type === ExerciseType.PROGRAMMING)) { -
- @if (exercise.maxPoints) { - - - {{ 'artemisApp.courseOverview.exerciseDetails.points' | artemisTranslate }} - @if (achievedPoints !== undefined) { - {{ achievedPoints + ('artemisApp.courseOverview.exerciseDetails.of' | artemisTranslate) }} - } - {{ exercise.maxPoints }} - @if (exercise.bonusPoints) { - ({{ 'artemisApp.courseOverview.exerciseDetails.bonus' | artemisTranslate }} {{ exercise.bonusPoints }}) - } - - @if (exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY) { - - } - - } - -
- } - -
@if (!nextRelevantDateLabel || (nextRelevantDateLabel !== 'assessmentDue' && nextRelevantDateLabel !== 'complaintDue')) { } @else { - @if (nextRelevantDate && (!exam || !isTestRun)) { -
- {{ 'artemisApp.courseOverview.exerciseDetails.' + nextRelevantDateLabel | artemisTranslate }} - - {{ nextRelevantDate | artemisTimeAgo }} - -
- } @if (exercise.presentationScoreEnabled) {
@if (course?.presentationScore) { @@ -140,16 +65,6 @@
} } - @if (!nextRelevantDate && canComplainLaterOn) { -
- {{ 'artemisApp.courseOverview.exerciseDetails.complaintPossible' | artemisTranslate }} - -
- }
} diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.ts b/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.ts index df7828249855..45ee937f3bbd 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.ts @@ -22,7 +22,6 @@ export interface InformationBox { title: string; content: string | number | any; contentType?: string; - isContentComponent?: boolean; contentComponent?: any; icon?: IconProp; tooltip?: string; @@ -67,6 +66,7 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit public achievedPoints?: number; public numberOfSubmissions: number; public informationBoxItems: InformationBox[] = []; + public shouldDisplayDueDateRelative = false; icon: IconProp; @@ -112,6 +112,8 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit if (this.dueDate) { // If the due date is less than a day away, the color change to red this.dueDateStatusBadge = this.dueDate.isBetween(dayjs().add(1, 'day'), dayjs()) ? 'danger' : 'body-color'; + // If the due date is less than a week away, text is displayed relativley e.g. 'in 2 days' + this.shouldDisplayDueDateRelative = this.dueDate.isBetween(dayjs().add(1, 'week'), dayjs()) ? true : false; } this.createInformationBoxItems(); } @@ -119,7 +121,9 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit createInformationBoxItems() { const notReleased = this.exercise.releaseDate && dayjs(this.exercise.releaseDate).isAfter(dayjs()); if (this.exercise.maxPoints) this.informationBoxItems.push(this.getMaxPointsItem()); - this.informationBoxItems.push(this.getDueDateItem()); + if (this.exercise.bonusPoints) this.informationBoxItems.push(this.getBonusPointsItem()); + + if (this.exercise.dueDate) this.informationBoxItems.push(this.getDueDateItem()); this.informationBoxItems.push(this.getDifficultyItem()); // (exercise.releaseDate && dayjs(exercise.releaseDate).isAfter(dayjs())) if (notReleased || this.exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY || this.exercise.categories?.length) @@ -128,19 +132,26 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit // if (this.submissionPolicy?.active) this.informationBoxItems.push(this.getSubmissionPolicyItem()); this.informationBoxItems.push(this.getSubmissionStatusItem()); - if (this.exercise.assessmentType && this.exercise.type === ExerciseType.PROGRAMMING) this.informationBoxItems.push(this.getAssessmentTypeItem()); + // if (this.exercise.assessmentType && this.exercise.type === ExerciseType.PROGRAMMING) this.informationBoxItems.push(this.getAssessmentTypeItem()); } getDueDateItem(): InformationBox { + const isDueDateInThePast = this.dueDate?.isBefore(dayjs()); + + if (isDueDateInThePast) { + return { + title: 'artemisApp.courseOverview.exerciseDetails.submissionDueOver', + content: this.dueDate, + contentComponent: 'dateTime', + }; + } + return { title: 'artemisApp.courseOverview.exerciseDetails.submissionDue', - // less than a day make time relative to now + // less than a week make time relative to now content: this.dueDate, - // content: this.dueDate?.format('lll') ?? '-', - // icon: this.icon, - isContentComponent: true, - contentType: 'timeAgo', - tooltip: 'artemisApp.courseOverview.exerciseDetails.submissionDueTooltip', + contentComponent: this.shouldDisplayDueDateRelative ? 'timeAgo' : 'dateTime', + tooltip: this.shouldDisplayDueDateRelative ? 'artemisApp.courseOverview.exerciseDetails.submissionDueTooltip' : undefined, contentColor: this.dueDateStatusBadge, tooltipParams: { date: this.dueDate?.format('lll') }, }; @@ -167,7 +178,6 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit title: 'artemisApp.courseOverview.exerciseDetails.difficulty', content: this.exercise.difficulty, contentComponent: 'difficultyLevel', - isContentComponent: true, }; } getSubmissionStatusItem(): InformationBox { @@ -175,7 +185,6 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit title: 'artemisApp.courseOverview.exerciseDetails.submissionStatus', content: this.studentParticipation, contentComponent: 'submissionStatus', - isContentComponent: true, }; } getCategoryItems(): InformationBox { @@ -183,27 +192,15 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit title: 'artemisApp.courseOverview.exerciseDetails.categories', content: this.exercise, contentComponent: 'categories', - isContentComponent: true, }; } getSubmissionPolicyItem(): InformationBox { - console.log(this.submissionPolicy); - // {{ 'artemisApp.programmingExercise.submissionPolicy.submissionLimitTitle' | artemisTranslate }}: - // {{ - // numberOfSubmissions + - // '/' + - // submissionPolicy.submissionLimit + - // (submissionPolicy.exceedingPenalty - // ? ('artemisApp.programmingExercise.submissionPolicy.submissionPenalty.penaltyInfoLabel' - // | artemisTranslate: { points: submissionPolicy.exceedingPenalty }) - // : '') - // }} - - // TODO Make Red if submissionLimit is (nearly) reached return { title: 'artemisApp.programmingExercise.submissionPolicy.submissionLimitTitle', content: this.numberOfSubmissions + ' / ' + this.submissionPolicy?.submissionLimit, + + contentColor: this.submissionPolicy?.submissionLimit ? this.getSubmissionColor() : 'body-color', // content: // this.numberOfSubmissions + // '/' + @@ -214,14 +211,16 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit // }) // : ''), tooltip: 'artemisApp.programmingExercise.submissionPolicy.submissionPolicyType.' + this.submissionPolicy?.type + '.tooltip', + tooltipParams: { points: this.submissionPolicy?.exceedingPenalty?.toString() }, }; } - getExceedingPenalty() { - return { - title: 'artemisApp.programmingExercise.submissionPolicy.submissionPolicyType.submission_penalty.title', - content: '-' + this.submissionPolicy?.exceedingPenalty + ' Points', - }; + getSubmissionColor() { + const submissionsLeft = this.submissionPolicy?.submissionLimit ? this.submissionPolicy?.submissionLimit - this.numberOfSubmissions : 2; + if (submissionsLeft > 1) return 'body-color'; + else { + return submissionsLeft <= 0 ? 'danger' : 'warning'; + } } // Can be visible in the tooltip above a status @@ -237,18 +236,19 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit // } + // separate Points and Bonus Points + // DO one function with input getMaxPointsItem(): InformationBox { - if (this.exercise.bonusPoints) { - const pointsAndBonusTitle = 'artemisApp.courseOverview.exerciseDetails.pointsAndBonus'; - const pointsAndBonusContent = this.exercise.maxPoints + ` + ${this.exercise.bonusPoints}`; - return { - title: pointsAndBonusTitle, - content: this.achievedPoints !== undefined ? this.achievedPoints + ' / ' + pointsAndBonusContent : pointsAndBonusContent, - }; - } return { title: 'artemisApp.courseOverview.exerciseDetails.points', - content: this.achievedPoints !== undefined ? this.achievedPoints + ' / ' + this.exercise.maxPoints : this.exercise.maxPoints ?? '-', + content: this.achievedPoints !== undefined ? this.achievedPoints + ' / ' + this.exercise.maxPoints : '0 / ' + this.exercise.maxPoints, + }; + } + + getBonusPointsItem(): InformationBox { + return { + title: 'artemisApp.courseOverview.exerciseDetails.bonus', + content: this.achievedPoints !== undefined ? this.achievedPoints + ' / ' + this.exercise.bonusPoints : '0 / ' + this.exercise.bonusPoints, }; } // getDefaultItems(): InformationBox[] { @@ -270,13 +270,13 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit ngOnChanges() { this.course = this.course ?? getCourseFromExercise(this.exercise); - if (this.submissionPolicy?.active) { + if (this.submissionPolicy?.active && this.submissionPolicy?.submissionLimit) { console.log('Changes Submission'); this.countSubmissions(); this.informationBoxItems.push(this.getSubmissionPolicyItem()); - if (this.submissionPolicy?.exceedingPenalty) { - this.informationBoxItems.push(this.getExceedingPenalty()); - } + // if (this.submissionPolicy?.exceedingPenalty) { + // this.informationBoxItems.push(this.getExceedingPenalty()); + // } } if (this.studentParticipation?.results?.length) { // The updated participation by the websocket is not guaranteed to be sorted, find the newest result (highest id) diff --git a/src/main/webapp/app/exercises/shared/result/result.component.html b/src/main/webapp/app/exercises/shared/result/result.component.html index 65c4be9ca9de..e7027337e302 100644 --- a/src/main/webapp/app/exercises/shared/result/result.component.html +++ b/src/main/webapp/app/exercises/shared/result/result.component.html @@ -97,19 +97,20 @@ } @case (ResultTemplateStatus.MISSING) { -   - @switch (missingResultInfo) { - @case (MissingResultInfo.FAILED_PROGRAMMING_SUBMISSION_ONLINE_IDE) { - - } - @case (MissingResultInfo.FAILED_PROGRAMMING_SUBMISSION_OFFLINE_IDE) { - + + + @if (!isInSidebarCard) { + @switch (missingResultInfo) { + @case (MissingResultInfo.FAILED_PROGRAMMING_SUBMISSION_ONLINE_IDE) { + {{ + 'artemisApp.result.missing.programmingFailedSubmission.message' | artemisTranslate + }} + } + @case (MissingResultInfo.FAILED_PROGRAMMING_SUBMISSION_OFFLINE_IDE) { + {{ + 'artemisApp.result.missing.programmingFailedSubmission.message' | artemisTranslate + }} + } } } @if (result && exercise?.type !== ExerciseType.QUIZ) { diff --git a/src/main/webapp/app/overview/submission-result-status.component.html b/src/main/webapp/app/overview/submission-result-status.component.html index 69d80a4356d9..118d8fd5150a 100644 --- a/src/main/webapp/app/overview/submission-result-status.component.html +++ b/src/main/webapp/app/overview/submission-result-status.component.html @@ -1,5 +1,4 @@
- {{ shouldShowResult }} @if (shouldShowResult) { @@ -39,7 +38,6 @@ } @if (exercise.type === ExerciseType.PROGRAMMING && studentParticipation) { - missed }
diff --git a/src/main/webapp/app/shared/sidebar/sidebar-card-item/sidebar-card-item.component.html b/src/main/webapp/app/shared/sidebar/sidebar-card-item/sidebar-card-item.component.html index 15f1ecdd6e90..3d7486a70d90 100644 --- a/src/main/webapp/app/shared/sidebar/sidebar-card-item/sidebar-card-item.component.html +++ b/src/main/webapp/app/shared/sidebar/sidebar-card-item/sidebar-card-item.component.html @@ -73,7 +73,7 @@ {{ sidebarItem.subtitleLeft }} - @if (sidebarType === 'exercise' && sidebarItem.studentParticipation && sidebarItem.exercise) { + @if (sidebarType === 'exercise' && sidebarItem.exercise) { Date: Tue, 4 Jun 2024 23:22:35 +0200 Subject: [PATCH 05/23] create separate component --- .../example-solution.component.html | 1 - ...xercise-headers-information.component.html | 39 +++ ...xercise-headers-information.component.scss | 0 .../exercise-headers-information.component.ts | 291 ++++++++++++++++++ ...-exercise-page-with-details.component.html | 182 ++++++++--- ...er-exercise-page-with-details.component.ts | 198 +----------- .../overview/course-overview.component.html | 4 +- .../webapp/app/overview/course-overview.scss | 10 - .../discussion-section.component.scss | 2 +- .../course-exercise-details.component.html | 39 +-- .../course-exercise-details.component.scss | 8 - .../course-exercise-details.module.ts | 4 + ...ise-details-student-actions.component.html | 4 +- .../code-button/code-button.component.html | 2 +- .../code-button/code-button.component.ts | 1 + .../exercise-action-button.component.html | 2 +- .../not-released-tag.component.html | 1 + .../open-code-editor-button.component.html | 2 +- .../open-code-editor-button.component.ts | 2 + .../webapp/i18n/de/programmingExercise.json | 2 +- .../webapp/i18n/en/programmingExercise.json | 2 +- .../exercises/ExerciseResultPage.ts | 24 ++ ...cise-headers-information.component.spec.ts | 26 ++ .../exercises/ExerciseResultPage.ts | 1 + .../ProgrammingExerciseOverviewPage.ts | 2 + 25 files changed, 563 insertions(+), 286 deletions(-) create mode 100644 src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html create mode 100644 src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.scss create mode 100644 src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts create mode 100644 src/test/cypress/support/pageobjects/exercises/ExerciseResultPage.ts create mode 100644 src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts diff --git a/src/main/webapp/app/exercises/shared/example-solution/example-solution.component.html b/src/main/webapp/app/exercises/shared/example-solution/example-solution.component.html index 17be32b388e1..d6e797f436e0 100644 --- a/src/main/webapp/app/exercises/shared/example-solution/example-solution.component.html +++ b/src/main/webapp/app/exercises/shared/example-solution/example-solution.component.html @@ -1,5 +1,4 @@ @if (exercise) { -
@if (displayHeader) { diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html new file mode 100644 index 000000000000..2b35759efe26 --- /dev/null +++ b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html @@ -0,0 +1,39 @@ +@if (exercise) { +
+ + @for (informationBoxItem of informationBoxItems; track informationBoxItem) { + + @if (informationBoxItem.contentComponent === 'difficultyLevel') { + + } + @if (informationBoxItem.contentComponent === 'categories') { + + } + @if (informationBoxItem.contentComponent === 'timeAgo') { + {{ informationBoxItem.content | artemisTimeAgo }} + } + @if (informationBoxItem.contentComponent === 'dateTime') { + {{ informationBoxItem.content | artemisDate }} + } + + @if (informationBoxItem.contentComponent === 'submissionStatus') { + + } + + } +
+} diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.scss b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts new file mode 100644 index 000000000000..b98c123b841f --- /dev/null +++ b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts @@ -0,0 +1,291 @@ +import { Component, Input, OnChanges, OnInit } from '@angular/core'; +import { SortService } from 'app/shared/service/sort.service'; +import dayjs from 'dayjs/esm'; +import { Exercise, ExerciseType, IncludedInOverallScore, getCourseFromExercise, getIcon, getIconTooltip } from 'app/entities/exercise.model'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { ExerciseCategory } from 'app/entities/exercise-category.model'; +import { SubmissionPolicy } from 'app/entities/submission-policy.model'; +import { StudentParticipation } from 'app/entities/participation/student-participation.model'; +import { getExerciseDueDate } from 'app/exercises/shared/exercise/exercise.utils'; +import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; +import { Course } from 'app/entities/course.model'; +// import { AssessmentType } from 'app/entities/assessment-type.model'; +import { SubmissionType } from 'app/entities/submission.model'; +import { ProgrammingSubmission } from 'app/entities/programming-submission.model'; +import { roundValueSpecifiedByCourseSettings } from 'app/shared/util/utils'; +import { TranslateService } from '@ngx-translate/core'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; +import { SubmissionResultStatusModule } from 'app/overview/submission-result-status.module'; +import { ExerciseCategoriesModule } from 'app/shared/exercise-categories/exercise-categories.module'; + +export interface InformationBox { + title: string; + content: string | number | any; + contentType?: string; + contentComponent?: any; + icon?: IconProp; + tooltip?: string; + contentColor?: string; + tooltipParams?: Record; +} +@Component({ + selector: 'jhi-exercise-headers-information', + templateUrl: './exercise-headers-information.component.html', + standalone: true, + imports: [ArtemisSharedModule, ArtemisSharedComponentModule, SubmissionResultStatusModule, ExerciseCategoriesModule], + styleUrls: ['./exercise-headers-information.component.scss'], + // Our tsconfig file has `preserveWhitespaces: 'true'` which causes whitespace to affect content projection. + // We need to set it to 'false 'for this component, otherwise the components with the selector [contentComponent] + // will not be projected into their specific slot of the "InformationBoxComponent" component. + preserveWhitespaces: false, +}) +export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { + readonly IncludedInOverallScore = IncludedInOverallScore; + // readonly AssessmentType = AssessmentType; + readonly ExerciseType = ExerciseType; + readonly getIcon = getIcon; + readonly getIconTooltip = getIconTooltip; + readonly dayjs = dayjs; + + @Input() exercise: Exercise; + @Input() studentParticipation?: StudentParticipation; + @Input() title: string; + @Input() course?: Course; + @Input() isTestRun = false; + @Input() submissionPolicy?: SubmissionPolicy; + + exerciseCategories: ExerciseCategory[]; + dueDate?: dayjs.Dayjs; + isBeforeStartDate: boolean; + programmingExercise?: ProgrammingExercise; + individualComplaintDueDate?: dayjs.Dayjs; + // public nextRelevantDate?: dayjs.Dayjs; + // public nextRelevantDateLabel?: string; + // public nextRelevantDateStatusBadge?: string; + dueDateStatusBadge?: string; + canComplainLaterOn: boolean; + achievedPoints?: number; + numberOfSubmissions: number; + informationBoxItems: InformationBox[] = []; + shouldDisplayDueDateRelative = false; + + icon: IconProp; + + constructor( + private sortService: SortService, + private translateService: TranslateService, + ) {} + + ngOnInit() { + // this.exerciseCategories = this.exercise.categories || []; + + if (this.exercise.type) { + this.icon = getIcon(this.exercise.type); + } + + this.dueDate = getExerciseDueDate(this.exercise, this.studentParticipation); + // this.isBeforeStartDate = this.exercise.startDate ? this.exercise.startDate.isAfter(dayjs()) : !!this.exercise.releaseDate?.isAfter(dayjs()); + // if (this.course?.maxComplaintTimeDays) { + // this.individualComplaintDueDate = ComplaintService.getIndividualComplaintDueDate( + // this.exercise, + // this.course.maxComplaintTimeDays, + // this.studentParticipation?.results?.last(), + // this.studentParticipation, + // ); + // } + // // There is a submission where the student did not have the chance to complain yet + // this.canComplainLaterOn = + // !!this.studentParticipation?.submissionCount && + // !this.individualComplaintDueDate && + // (this.exercise.allowComplaintsForAutomaticAssessments || this.exercise.assessmentType !== AssessmentType.AUTOMATIC); + + if (this.dueDate) { + // If the due date is less than a day away, the color change to red + this.dueDateStatusBadge = this.dueDate.isBetween(dayjs().add(1, 'day'), dayjs()) ? 'danger' : 'body-color'; + // If the due date is less than a week away, text is displayed relativley e.g. 'in 2 days' + this.shouldDisplayDueDateRelative = this.dueDate.isBetween(dayjs().add(1, 'week'), dayjs()) ? true : false; + } + this.createInformationBoxItems(); + } + + createInformationBoxItems() { + const notReleased = this.exercise.releaseDate && dayjs(this.exercise.releaseDate).isAfter(dayjs()); + if (this.exercise.maxPoints) this.informationBoxItems.push(this.getPointsItem(this.exercise.maxPoints, 'points')); + if (this.exercise.bonusPoints) this.informationBoxItems.push(this.getPointsItem(this.exercise.bonusPoints, 'bonus')); + + if (this.exercise.dueDate) this.informationBoxItems.push(this.getDueDateItem()); + this.informationBoxItems.push(this.getDifficultyItem()); + // (exercise.releaseDate && dayjs(exercise.releaseDate).isAfter(dayjs())) + if (notReleased || this.exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY || this.exercise.categories?.length) + this.informationBoxItems.push(this.getCategoryItems()); + // this.informationBoxItems.push(this.getNextRelevantDateItem()); + // if (this.submissionPolicy?.active) this.informationBoxItems.push(this.getSubmissionPolicyItem()); + this.informationBoxItems.push(this.getSubmissionStatusItem()); + + // if (this.exercise.assessmentType && this.exercise.type === ExerciseType.PROGRAMMING) this.informationBoxItems.push(this.getAssessmentTypeItem()); + } + + getDueDateItem(): InformationBox { + const isDueDateInThePast = this.dueDate?.isBefore(dayjs()); + + if (isDueDateInThePast) { + return { + title: 'artemisApp.courseOverview.exerciseDetails.submissionDueOver', + content: this.dueDate, + contentComponent: 'dateTime', + }; + } + + return { + title: 'artemisApp.courseOverview.exerciseDetails.submissionDue', + // less than a week make time relative to now + content: this.dueDate, + contentComponent: this.shouldDisplayDueDateRelative ? 'timeAgo' : 'dateTime', + tooltip: this.shouldDisplayDueDateRelative ? 'artemisApp.courseOverview.exerciseDetails.submissionDueTooltip' : undefined, + contentColor: this.dueDateStatusBadge, + tooltipParams: { date: this.dueDate?.format('lll') }, + }; + } + // Status: Not released, no graded, graded, submitted, reviewed, assessed, complaint, complaint response, complaint applied, complaint resolved + // getStatusItem(): InformationBox { + + // } + + // getAssessmentTypeItem(): InformationBox { + // return { + // title: 'artemisApp.courseOverview.exerciseDetails.assessmentType', + // content: this.capitalize(this.exercise?.assessmentType), + // tooltip: 'artemisApp.AssessmentType.tooltip.' + this.exercise.assessmentType, + // }; + // } + + capitalize(title?: string) { + if (!title) return '-'; + return title.toString().charAt(0).toUpperCase() + title.slice(1).toLowerCase(); + } + getDifficultyItem(): InformationBox { + return { + title: 'artemisApp.courseOverview.exerciseDetails.difficulty', + content: this.exercise.difficulty, + contentComponent: 'difficultyLevel', + }; + } + getSubmissionStatusItem(): InformationBox { + return { + title: 'artemisApp.courseOverview.exerciseDetails.submissionStatus', + content: this.studentParticipation, + contentComponent: 'submissionStatus', + }; + } + getCategoryItems(): InformationBox { + return { + title: 'artemisApp.courseOverview.exerciseDetails.categories', + content: this.exercise, + contentComponent: 'categories', + }; + } + + getSubmissionPolicyItem(): InformationBox { + return { + title: 'artemisApp.programmingExercise.submissionPolicy.submissionLimitTitle', + content: this.numberOfSubmissions + ' / ' + this.submissionPolicy?.submissionLimit, + + contentColor: this.submissionPolicy?.submissionLimit ? this.getSubmissionColor() : 'body-color', + // content: + // this.numberOfSubmissions + + // '/' + + // this.submissionPolicy?.submissionLimit + + // (this.submissionPolicy?.exceedingPenalty + // ? ' ' + this.translateService.instant('artemisApp.programmingExercise.submissionPolicy.submissionPenalty.penaltyInfoLabel', { + // points: this.submissionPolicy.exceedingPenalty, + // }) + // : ''), + tooltip: 'artemisApp.programmingExercise.submissionPolicy.submissionPolicyType.' + this.submissionPolicy?.type + '.tooltip', + tooltipParams: { points: this.submissionPolicy?.exceedingPenalty?.toString() }, + }; + } + + getSubmissionColor() { + const submissionsLeft = this.submissionPolicy?.submissionLimit ? this.submissionPolicy?.submissionLimit - this.numberOfSubmissions : 2; + if (submissionsLeft > 1) return 'body-color'; + else { + return submissionsLeft <= 0 ? 'danger' : 'warning'; + } + } + + // Can be visible in the tooltip above a status + // getNextRelevantDateItem(): InformationBox { + // console.log('get Next Relevant Date Item') + // console.log(this.nextRelevantDateLabel) + // // {{ 'artemisApp.courseOverview.exerciseDetails.' + nextRelevantDateLabel | artemisTranslate }} + // return { + // title: this.nextRelevantDateLabel ? this.nextRelevantDateLabel : 'Next Relevant Date', + // content: this.nextRelevantDate?.format('lll') ?? '-', + // icon: faQuestionCircle, + // }; + + // } + + // separate Points and Bonus Points + // DO one function with input + getPointsItem(points: number | undefined, title: string): InformationBox { + return { + title: 'artemisApp.courseOverview.exerciseDetails.' + title, + content: this.achievedPoints !== undefined ? this.achievedPoints + ' / ' + points : '0 / ' + points, + }; + } + + // getDefaultItems(): InformationBox[] { + // const exercisesItem: InformationBox = { + // title: `${this.baseResource}`, + // icon: faEye, + // content: 'entity.action.view', + // }; + + // const statisticsItem: InformationBox = { + // routerLink: `${this.baseResource}scores`, + // icon: faTable, + // translation: 'entity.action.scores', + // }; + + // return [exercisesItem, statisticsItem]; + // } + + // Check what I really need + + ngOnChanges() { + this.course = this.course ?? getCourseFromExercise(this.exercise); + + if (this.submissionPolicy?.active && this.submissionPolicy?.submissionLimit) { + console.log('Changes Submission'); + this.countSubmissions(); + // need to push and pop the submission policy item to update the number of submissions + this.informationBoxItems.push(this.getSubmissionPolicyItem()); + // if (this.submissionPolicy?.exceedingPenalty) { + // this.informationBoxItems.push(this.getExceedingPenalty()); + // } + } + if (this.studentParticipation?.results?.length) { + // The updated participation by the websocket is not guaranteed to be sorted, find the newest result (highest id) + this.sortService.sortByProperty(this.studentParticipation.results, 'id', false); + + const latestRatedResult = this.studentParticipation.results.filter((result) => result.rated).first(); + if (latestRatedResult) { + this.achievedPoints = roundValueSpecifiedByCourseSettings((latestRatedResult.score! * this.exercise.maxPoints!) / 100, this.course); + } + } + } + + private countSubmissions() { + const commitHashSet = new Set(); + + this.studentParticipation?.results + ?.map((result) => result.submission) + .filter((submission) => submission?.type === SubmissionType.MANUAL) + .map((submission) => (submission as ProgrammingSubmission).commitHash) + .forEach((commitHash: string) => commitHashSet.add(commitHash)); + + this.numberOfSubmissions = commitHashSet.size; + } +} diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.html b/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.html index 8934d4eadfab..378c8200c7e4 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.html +++ b/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.html @@ -1,45 +1,144 @@ @if (exercise) { -
- - @for (informationBoxItem of informationBoxItems; track informationBoxItem) { - - @if (informationBoxItem.contentComponent === 'difficultyLevel') { - - } - @if (informationBoxItem.contentComponent === 'categories') { - - } - @if (informationBoxItem.contentComponent === 'timeAgo') { - {{ informationBoxItem.content | artemisTimeAgo }} +
+
+
+
+ @if (exercise.type) { + + } +   + +
+ @if ((exercise.releaseDate && dayjs(exercise.releaseDate).isAfter(dayjs())) || exercise.difficulty || exerciseCategories?.length) { + } - @if (informationBoxItem.contentComponent === 'dateTime') { - {{ informationBoxItem.content | artemisDate }} +
+ @if (exercise.maxPoints || (exercise.assessmentType && exercise.type === ExerciseType.PROGRAMMING)) { +
+ @if (exercise.maxPoints) { + + + {{ 'artemisApp.courseOverview.exerciseDetails.points' | artemisTranslate }} + @if (achievedPoints !== undefined) { + {{ achievedPoints + ('artemisApp.courseOverview.exerciseDetails.of' | artemisTranslate) }} + } + {{ exercise.maxPoints }} + @if (exercise.bonusPoints) { + ({{ 'artemisApp.courseOverview.exerciseDetails.bonus' | artemisTranslate }} {{ exercise.bonusPoints }}) + } + + @if (exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY) { + + } + + } + @if (exercise.assessmentType && exercise.type === ExerciseType.PROGRAMMING) { + +
+ {{ 'artemisApp.courseOverview.exerciseDetails.assessmentType' | artemisTranslate }} + {{ 'artemisApp.AssessmentType.forExerciseHeader.' + exercise.assessmentType | artemisTranslate }} + +
+
+ } +
+ } + @if (submissionPolicy && submissionPolicy.active) { +
+
{{ 'artemisApp.programmingExercise.submissionPolicy.submissionLimitTitle' | artemisTranslate }}:
+
+ {{ + numberOfSubmissions + + '/' + + submissionPolicy.submissionLimit + + (submissionPolicy.exceedingPenalty + ? ('artemisApp.programmingExercise.submissionPolicy.submissionPenalty.penaltyInfoLabel' + | artemisTranslate: { points: submissionPolicy.exceedingPenalty }) + : '') + }} + +
+
+ } +
+
+ @if (!nextRelevantDateLabel || (nextRelevantDateLabel !== 'releaseDate' && nextRelevantDateLabel !== 'startDate')) { + } @else { + @if (nextRelevantDate && (!exam || !isTestRun)) { +
+ {{ 'artemisApp.courseOverview.exerciseDetails.' + nextRelevantDateLabel | artemisTranslate }} + + {{ nextRelevantDate | artemisTimeAgo }} + +
} - - @if (informationBoxItem.contentComponent === 'submissionStatus') { - + @if (exercise.presentationScoreEnabled) { +
+ @if (course?.presentationScore) { + {{ 'artemisApp.courseOverview.exerciseDetails.presented' | artemisTranslate }} + @if ((studentParticipation?.presentationScore ?? 0) > 0) { + + {{ 'global.generic.yes' | artemisTranslate }} + + } + @if ((studentParticipation?.presentationScore ?? 0) <= 0) { + + {{ 'global.generic.no' | artemisTranslate }} + + } + } @else { + {{ 'artemisApp.courseOverview.exerciseDetails.presentation' | artemisTranslate }} + @if (studentParticipation?.presentationScore) { + + {{ studentParticipation!.presentationScore + '%' }} + + } + @if (!studentParticipation?.presentationScore) { + + {{ 'global.generic.unset' | artemisTranslate }} + + } + } +
} - - } -
-
-
+ } + @if (dueDate) { +
+ {{ 'artemisApp.courseOverview.exerciseDetails.submissionDue' | artemisTranslate }} + + {{ dueDate | artemisTimeAgo }} + +
+ } @if (!nextRelevantDateLabel || (nextRelevantDateLabel !== 'assessmentDue' && nextRelevantDateLabel !== 'complaintDue')) { } @else { + @if (nextRelevantDate && (!exam || !isTestRun)) { +
+ {{ 'artemisApp.courseOverview.exerciseDetails.' + nextRelevantDateLabel | artemisTranslate }} + + {{ nextRelevantDate | artemisTimeAgo }} + +
+ } @if (exercise.presentationScoreEnabled) {
@if (course?.presentationScore) { @@ -65,6 +164,17 @@
} } + @if (!nextRelevantDate && canComplainLaterOn) { +
+ {{ 'artemisApp.courseOverview.exerciseDetails.complaintPossible' | artemisTranslate }} + + {{ 'global.generic.yes' | artemisTranslate }} + +
+ }
} diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.ts b/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.ts index 45ee937f3bbd..b89694846761 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.ts @@ -16,26 +16,11 @@ import { ComplaintService } from 'app/complaints/complaint.service'; import { SubmissionType } from 'app/entities/submission.model'; import { ProgrammingSubmission } from 'app/entities/programming/programming-submission.model'; import { roundValueSpecifiedByCourseSettings } from 'app/shared/util/utils'; -import { TranslateService } from '@ngx-translate/core'; -export interface InformationBox { - title: string; - content: string | number | any; - contentType?: string; - contentComponent?: any; - icon?: IconProp; - tooltip?: string; - contentColor?: string; - tooltipParams?: Record; -} @Component({ selector: 'jhi-header-exercise-page-with-details', templateUrl: './header-exercise-page-with-details.component.html', styleUrls: ['./header-exercise-page-with-details.component.scss'], - // Our tsconfig file has `preserveWhitespaces: 'true'` which causes whitespace to affect content projection. - // We need to set it to 'false 'for this component, otherwise the components with the selecotor [contentComponent] - // will not be projected into their specific slot of the "InformationBoxComponent" component. - preserveWhitespaces: false, }) export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit { readonly IncludedInOverallScore = IncludedInOverallScore; @@ -65,18 +50,13 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit public canComplainLaterOn: boolean; public achievedPoints?: number; public numberOfSubmissions: number; - public informationBoxItems: InformationBox[] = []; - public shouldDisplayDueDateRelative = false; icon: IconProp; // Icons faQuestionCircle = faQuestionCircle; - constructor( - private sortService: SortService, - private translateService: TranslateService, - ) {} + constructor(private sortService: SortService) {} ngOnInit() { this.exerciseCategories = this.exercise.categories || []; @@ -110,173 +90,15 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit } if (this.dueDate) { - // If the due date is less than a day away, the color change to red - this.dueDateStatusBadge = this.dueDate.isBetween(dayjs().add(1, 'day'), dayjs()) ? 'danger' : 'body-color'; - // If the due date is less than a week away, text is displayed relativley e.g. 'in 2 days' - this.shouldDisplayDueDateRelative = this.dueDate.isBetween(dayjs().add(1, 'week'), dayjs()) ? true : false; - } - this.createInformationBoxItems(); - } - - createInformationBoxItems() { - const notReleased = this.exercise.releaseDate && dayjs(this.exercise.releaseDate).isAfter(dayjs()); - if (this.exercise.maxPoints) this.informationBoxItems.push(this.getMaxPointsItem()); - if (this.exercise.bonusPoints) this.informationBoxItems.push(this.getBonusPointsItem()); - - if (this.exercise.dueDate) this.informationBoxItems.push(this.getDueDateItem()); - this.informationBoxItems.push(this.getDifficultyItem()); - // (exercise.releaseDate && dayjs(exercise.releaseDate).isAfter(dayjs())) - if (notReleased || this.exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY || this.exercise.categories?.length) - this.informationBoxItems.push(this.getCategoryItems()); - // this.informationBoxItems.push(this.getNextRelevantDateItem()); - // if (this.submissionPolicy?.active) this.informationBoxItems.push(this.getSubmissionPolicyItem()); - this.informationBoxItems.push(this.getSubmissionStatusItem()); - - // if (this.exercise.assessmentType && this.exercise.type === ExerciseType.PROGRAMMING) this.informationBoxItems.push(this.getAssessmentTypeItem()); - } - - getDueDateItem(): InformationBox { - const isDueDateInThePast = this.dueDate?.isBefore(dayjs()); - - if (isDueDateInThePast) { - return { - title: 'artemisApp.courseOverview.exerciseDetails.submissionDueOver', - content: this.dueDate, - contentComponent: 'dateTime', - }; - } - - return { - title: 'artemisApp.courseOverview.exerciseDetails.submissionDue', - // less than a week make time relative to now - content: this.dueDate, - contentComponent: this.shouldDisplayDueDateRelative ? 'timeAgo' : 'dateTime', - tooltip: this.shouldDisplayDueDateRelative ? 'artemisApp.courseOverview.exerciseDetails.submissionDueTooltip' : undefined, - contentColor: this.dueDateStatusBadge, - tooltipParams: { date: this.dueDate?.format('lll') }, - }; - } - // Status: Not released, no graded, graded, submitted, reviewed, assessed, complaint, complaint response, complaint applied, complaint resolved - // getStatusItem(): InformationBox { - - // } - - getAssessmentTypeItem(): InformationBox { - return { - title: 'artemisApp.courseOverview.exerciseDetails.assessmentType', - content: this.capitalize(this.exercise?.assessmentType), - tooltip: 'artemisApp.AssessmentType.tooltip.' + this.exercise.assessmentType, - }; - } - - capitalize(title?: string) { - if (!title) return '-'; - return title.toString().charAt(0).toUpperCase() + title.slice(1).toLowerCase(); - } - getDifficultyItem(): InformationBox { - return { - title: 'artemisApp.courseOverview.exerciseDetails.difficulty', - content: this.exercise.difficulty, - contentComponent: 'difficultyLevel', - }; - } - getSubmissionStatusItem(): InformationBox { - return { - title: 'artemisApp.courseOverview.exerciseDetails.submissionStatus', - content: this.studentParticipation, - contentComponent: 'submissionStatus', - }; - } - getCategoryItems(): InformationBox { - return { - title: 'artemisApp.courseOverview.exerciseDetails.categories', - content: this.exercise, - contentComponent: 'categories', - }; - } - - getSubmissionPolicyItem(): InformationBox { - return { - title: 'artemisApp.programmingExercise.submissionPolicy.submissionLimitTitle', - content: this.numberOfSubmissions + ' / ' + this.submissionPolicy?.submissionLimit, - - contentColor: this.submissionPolicy?.submissionLimit ? this.getSubmissionColor() : 'body-color', - // content: - // this.numberOfSubmissions + - // '/' + - // this.submissionPolicy?.submissionLimit + - // (this.submissionPolicy?.exceedingPenalty - // ? ' ' + this.translateService.instant('artemisApp.programmingExercise.submissionPolicy.submissionPenalty.penaltyInfoLabel', { - // points: this.submissionPolicy.exceedingPenalty, - // }) - // : ''), - tooltip: 'artemisApp.programmingExercise.submissionPolicy.submissionPolicyType.' + this.submissionPolicy?.type + '.tooltip', - tooltipParams: { points: this.submissionPolicy?.exceedingPenalty?.toString() }, - }; - } - - getSubmissionColor() { - const submissionsLeft = this.submissionPolicy?.submissionLimit ? this.submissionPolicy?.submissionLimit - this.numberOfSubmissions : 2; - if (submissionsLeft > 1) return 'body-color'; - else { - return submissionsLeft <= 0 ? 'danger' : 'warning'; + this.dueDateStatusBadge = dayjs().isBefore(this.dueDate) ? 'bg-success' : 'bg-danger'; } } - // Can be visible in the tooltip above a status - // getNextRelevantDateItem(): InformationBox { - // console.log('get Next Relevant Date Item') - // console.log(this.nextRelevantDateLabel) - // // {{ 'artemisApp.courseOverview.exerciseDetails.' + nextRelevantDateLabel | artemisTranslate }} - // return { - // title: this.nextRelevantDateLabel ? this.nextRelevantDateLabel : 'Next Relevant Date', - // content: this.nextRelevantDate?.format('lll') ?? '-', - // icon: faQuestionCircle, - // }; - - // } - - // separate Points and Bonus Points - // DO one function with input - getMaxPointsItem(): InformationBox { - return { - title: 'artemisApp.courseOverview.exerciseDetails.points', - content: this.achievedPoints !== undefined ? this.achievedPoints + ' / ' + this.exercise.maxPoints : '0 / ' + this.exercise.maxPoints, - }; - } - - getBonusPointsItem(): InformationBox { - return { - title: 'artemisApp.courseOverview.exerciseDetails.bonus', - content: this.achievedPoints !== undefined ? this.achievedPoints + ' / ' + this.exercise.bonusPoints : '0 / ' + this.exercise.bonusPoints, - }; - } - // getDefaultItems(): InformationBox[] { - // const exercisesItem: InformationBox = { - // title: `${this.baseResource}`, - // icon: faEye, - // content: 'entity.action.view', - // }; - - // const statisticsItem: InformationBox = { - // routerLink: `${this.baseResource}scores`, - // icon: faTable, - // translation: 'entity.action.scores', - // }; - - // return [exercisesItem, statisticsItem]; - // } - ngOnChanges() { this.course = this.course ?? getCourseFromExercise(this.exercise); - if (this.submissionPolicy?.active && this.submissionPolicy?.submissionLimit) { - console.log('Changes Submission'); + if (this.submissionPolicy?.active) { this.countSubmissions(); - this.informationBoxItems.push(this.getSubmissionPolicyItem()); - // if (this.submissionPolicy?.exceedingPenalty) { - // this.informationBoxItems.push(this.getExceedingPenalty()); - // } } if (this.studentParticipation?.results?.length) { // The updated participation by the websocket is not guaranteed to be sorted, find the newest result (highest id) @@ -303,7 +125,6 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit * Determines the next date of the course exercise cycle. If none exists the latest date in the past is determined */ private determineNextRelevantDateCourseMode() { - console.log('Hi'); const possibleDates = [this.exercise.releaseDate, this.exercise.startDate, this.exercise.assessmentDueDate, this.individualComplaintDueDate]; const possibleDatesLabels = ['releaseDate', 'startDate', 'assessmentDue', 'complaintDue']; @@ -322,31 +143,21 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit this.nextRelevantDate = undefined; this.nextRelevantDateLabel = undefined; this.nextRelevantDateStatusBadge = undefined; - console.log('Determine Next Date'); - console.log(dates); - console.log(dateLabels); + for (let i = 0; i < dates.length; i++) { if (dates[i] && now.isBefore(dates[i])) { - console.log('If Satetment'); this.nextRelevantDate = dates[i]!; this.nextRelevantDateLabel = dateLabels[i]; this.nextRelevantDateStatusBadge = 'bg-success'; return; } } - - console.log(this.nextRelevantDateLabel); if (this.canComplainLaterOn) { return; } for (let i = dates.length - 1; i >= 0; i--) { - console.log(i); if (dates[i]) { - console.log(i); - console.log('If Satetment3'); if (this.dueDate && this.dueDate.isAfter(dates[i])) { - console.log('If Satetment2'); - console.log(this.nextRelevantDateLabel); return; } @@ -356,7 +167,6 @@ export class HeaderExercisePageWithDetailsComponent implements OnChanges, OnInit return; } } - console.log('Haaaallo'); } private countSubmissions() { diff --git a/src/main/webapp/app/overview/course-overview.component.html b/src/main/webapp/app/overview/course-overview.component.html index b30269a5171c..e4ab97d1455f 100644 --- a/src/main/webapp/app/overview/course-overview.component.html +++ b/src/main/webapp/app/overview/course-overview.component.html @@ -200,14 +200,14 @@
@if (isNotManagementView && course.isAtLeastTutor) { -
+ } @if (showRefreshButton) { - diff --git a/src/main/webapp/app/overview/course-overview.scss b/src/main/webapp/app/overview/course-overview.scss index 366470e3c500..718a755e7b56 100644 --- a/src/main/webapp/app/overview/course-overview.scss +++ b/src/main/webapp/app/overview/course-overview.scss @@ -1,16 +1,6 @@ /* ========================================================================== Course Info Bar ========================================================================== */ -.tab-bar-exercise-details { - flex-wrap: wrap; - - // move instructor actions onto their own line for small/medium devices - .instructor-actions { - width: 100%; - justify-content: flex-end; - } -} - .course-body-container { position: relative; } diff --git a/src/main/webapp/app/overview/discussion-section/discussion-section.component.scss b/src/main/webapp/app/overview/discussion-section/discussion-section.component.scss index 8e950f5556dd..15214ef60aee 100644 --- a/src/main/webapp/app/overview/discussion-section/discussion-section.component.scss +++ b/src/main/webapp/app/overview/discussion-section/discussion-section.component.scss @@ -35,7 +35,7 @@ $discussion-section-card-min-width: 360px; align-items: center; background-color: var(--artemis-dark); cursor: pointer; - border-radius: 1em 1em 0 0; + border-radius: 0.25em 0.25em 0 0; .card-title { display: flex; diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html index e7403063f46d..1c9f4b0bbf56 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html @@ -2,9 +2,6 @@
-
{{ exercise.title }}
@@ -12,7 +9,9 @@
{{ exercise.title }}
@for (instructorActionItem of instructorActionItems; track instructorActionItem) { @@ -45,33 +44,19 @@
{{ exercise.title }}
-
+
+
+ +
- - {{ exercise.title }} -
-
-
-
- -
-
-
- @if ((this.sortedHistoryResults?.length && this.sortedHistoryResults.length > 1) || this.practiceStudentParticipation?.results?.length) {
diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.scss b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.scss index aa14a1bf85a8..e69de29bb2d1 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.scss +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.scss @@ -1,8 +0,0 @@ -.tab-bar-exercise-details { - display: flex; - min-height: 60px; - margin: 0 -1rem 0.5rem -1rem; - padding: 0.5rem 1rem; - border-bottom: 1px solid var(--overview-border-color); - gap: 1rem; -} diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.module.ts b/src/main/webapp/app/overview/exercise-details/course-exercise-details.module.ts index 63f0858ea8b8..6c5664b7bff2 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.module.ts +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.module.ts @@ -31,6 +31,7 @@ import { ArtemisFeedbackModule } from 'app/exercises/shared/feedback/feedback.mo import { ArtemisExerciseInfoModule } from 'app/exercises/shared/exercise-info/exercise-info.module'; import { IrisModule } from 'app/iris/iris.module'; import { DiscussionSectionComponent } from 'app/overview/discussion-section/discussion-section.component'; +import { ExerciseHeadersInformationComponent } from 'app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component'; const routes: Routes = [ { @@ -45,6 +46,8 @@ const routes: Routes = [ }, ]; +const standaloneComponents = [ExerciseHeadersInformationComponent]; + @NgModule({ imports: [ ArtemisExerciseButtonsModule, @@ -71,6 +74,7 @@ const routes: Routes = [ ArtemisExerciseInfoModule, IrisModule, DiscussionSectionComponent, + [...standaloneComponents], ], declarations: [CourseExerciseDetailsComponent, OrionCourseExerciseDetailsComponent, LtiInitializerComponent, LtiInitializerModalComponent, ProblemStatementComponent], exports: [CourseExerciseDetailsComponent, OrionCourseExerciseDetailsComponent, ProblemStatementComponent], diff --git a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html index 8c5075e398fb..97dc9bcc53e1 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html +++ b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.html @@ -1,6 +1,5 @@
@switch (exercise.type) { - @case (ExerciseType.QUIZ) {
@@ -118,6 +117,7 @@ [participations]="exercise.studentParticipations!" [courseAndExerciseNavigationUrlSegment]="['/courses', courseId, 'exercises', 'programming-exercises', exercise.id, 'code-editor']" [exercise]="exercise" + [hideLabelMobile]="true" /> } @if (programmingExercise?.allowOfflineIde) { @@ -127,7 +127,7 @@ [participations]="exercise.studentParticipations!" [exercise]="exercise" [routerLinkForRepositoryView]="repositoryLink + '/repository/' + exercise.studentParticipations![0].id" - [useParticipationVcsAccessToken]="true" + [hideLabelMobile]="true" /> } @if (theiaEnabled) { diff --git a/src/main/webapp/app/shared/components/code-button/code-button.component.html b/src/main/webapp/app/shared/components/code-button/code-button.component.html index 122d8df4e398..9b8d4f0488fd 100644 --- a/src/main/webapp/app/shared/components/code-button/code-button.component.html +++ b/src/main/webapp/app/shared/components/code-button/code-button.component.html @@ -7,7 +7,7 @@ [buttonLabel]="'artemisApp.exerciseActions.code' | artemisTranslate" [buttonLoading]="loading" [smallButton]="smallButtons" - [hideLabelMobile]="false" + [hideLabelMobile]="hideLabelMobile" [ngbPopover]="popContent" [autoClose]="'outside'" placement="right auto" diff --git a/src/main/webapp/app/shared/components/code-button/code-button.component.ts b/src/main/webapp/app/shared/components/code-button/code-button.component.ts index 5d753b661a44..a8d45ab44b46 100644 --- a/src/main/webapp/app/shared/components/code-button/code-button.component.ts +++ b/src/main/webapp/app/shared/components/code-button/code-button.component.ts @@ -33,6 +33,7 @@ export class CodeButtonComponent implements OnInit, OnChanges { @Input() routerLinkForRepositoryView?: string | (string | number)[]; @Input() participations?: ProgrammingExerciseStudentParticipation[]; @Input() exercise?: ProgrammingExercise; + @Input() hideLabelMobile = false; useSsh = false; useToken = false; diff --git a/src/main/webapp/app/shared/components/exercise-action-button.component.html b/src/main/webapp/app/shared/components/exercise-action-button.component.html index bc2b41ec1981..13516b497161 100644 --- a/src/main/webapp/app/shared/components/exercise-action-button.component.html +++ b/src/main/webapp/app/shared/components/exercise-action-button.component.html @@ -1,6 +1,6 @@
@if (buttonIcon) { - + }   {{ buttonLabel }} diff --git a/src/main/webapp/app/shared/components/not-released-tag.component.html b/src/main/webapp/app/shared/components/not-released-tag.component.html index 44276b4dabaf..7e7a9a7449ff 100644 --- a/src/main/webapp/app/shared/components/not-released-tag.component.html +++ b/src/main/webapp/app/shared/components/not-released-tag.component.html @@ -1,4 +1,5 @@ @if (exercise.releaseDate && dayjs(exercise.releaseDate).isAfter(dayjs())) { + } diff --git a/src/main/webapp/app/shared/components/open-code-editor-button/open-code-editor-button.component.ts b/src/main/webapp/app/shared/components/open-code-editor-button/open-code-editor-button.component.ts index 24438355258d..10b32d55360e 100644 --- a/src/main/webapp/app/shared/components/open-code-editor-button/open-code-editor-button.component.ts +++ b/src/main/webapp/app/shared/components/open-code-editor-button/open-code-editor-button.component.ts @@ -22,6 +22,8 @@ export class OpenCodeEditorButtonComponent implements OnChanges { courseAndExerciseNavigationUrlSegment: any[]; @Input() exercise: Exercise; + @Input() + hideLabelMobile = false; courseAndExerciseNavigationUrl: string; activeParticipation: ProgrammingExerciseStudentParticipation; diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index 72cdb72a430f..e12430f4a584 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -587,7 +587,7 @@ "pattern": "Der Punktabzug muss eine Nummer größer als 0 sein.", "required": "Der Punktabzug muss gesetzt sein." }, - "penaltyInfoLabel": "(Je Überschreitung: - {{points}} Punkte)", + "penaltyInfoLabel": "(Punktabzug je Überschreitung: {{points}} Punkte)", "triggerAllInformation": "Wird der Punktabzug je Überschreitung des Abgabelimits oder das Abgabelimit selbst angepasst, muss nach dem Speichern der Abgaberichtlinie die 'Erneut Bewerten' Funktionalität genutzt werden, um die Ergebnisse der Teilnehmenden zu aktualisieren." }, "submissionPolicyType": { diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index 3ad1a3a6d185..aafd720ef5d4 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -587,7 +587,7 @@ "pattern": "The penalty must be a number greater than 0.", "required": "The penalty must be set." }, - "penaltyInfoLabel": "(Per Exceeding: - {{points}} points)", + "penaltyInfoLabel": "(Exceeding Penalty: {{points}} points)", "triggerAllInformation": "If the exceeding submission limit penalty or the submission limit is updated, the 'Re-evaluate' functionality must be used to update the participants' results." }, "submissionPolicyType": { diff --git a/src/test/cypress/support/pageobjects/exercises/ExerciseResultPage.ts b/src/test/cypress/support/pageobjects/exercises/ExerciseResultPage.ts new file mode 100644 index 000000000000..59f1a2591460 --- /dev/null +++ b/src/test/cypress/support/pageobjects/exercises/ExerciseResultPage.ts @@ -0,0 +1,24 @@ +import { BASE_API, GET } from '../../constants'; + +/** + * A class which encapsulates UI selectors and actions for the exercise result page. + */ +export class ExerciseResultPage { + shouldShowProblemStatement(problemStatement: string) { + cy.get('#problem-statement').contains(problemStatement).should('be.visible'); + } + + shouldShowExerciseTitle(title: string) { + cy.get('#exercise-header').contains(title).should('be.visible'); + } + + clickOpenExercise(exerciseId: number) { + cy.intercept(GET, `${BASE_API}/results/*/rating`).as('getResults'); + cy.get('#open-exercise-' + exerciseId).click(); + return cy.wait('@getResults'); + } + + clickOpenCodeEditor(exerciseId: number) { + cy.get('#open-exercise-' + exerciseId).click(); + } +} diff --git a/src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts b/src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts new file mode 100644 index 000000000000..45b7f357a2c0 --- /dev/null +++ b/src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts @@ -0,0 +1,26 @@ +/* tslint:disable:no-unused-variable */ +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; + +import { ExerciseHeadersInformationComponent } from 'app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component'; + +// TODO +describe('ExerciseHeadersInformationComponent', () => { + let component: ExerciseHeadersInformationComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ExerciseHeadersInformationComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ExerciseHeadersInformationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/test/playwright/support/pageobjects/exercises/ExerciseResultPage.ts b/src/test/playwright/support/pageobjects/exercises/ExerciseResultPage.ts index 215186d44565..e1796d3dbbd7 100644 --- a/src/test/playwright/support/pageobjects/exercises/ExerciseResultPage.ts +++ b/src/test/playwright/support/pageobjects/exercises/ExerciseResultPage.ts @@ -26,6 +26,7 @@ export class ExerciseResultPage { async shouldShowScore(percentage: number) { await Commands.reloadUntilFound(this.page, this.page.locator('jhi-course-exercise-details #submission-result-graded'), 4000, 60000); + // TODO this is not the correct selector await expect(this.page.locator('.tab-bar-exercise-details').getByText(`${percentage}%`)).toBeVisible(); } diff --git a/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts b/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts index 1844f838045b..0f4dd8a55649 100644 --- a/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts +++ b/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts @@ -16,6 +16,7 @@ export class ProgrammingExerciseOverviewPage { } async getResultScore() { + // TODO this is not the correct selector const resultScore = this.page.locator('.tab-bar-exercise-details').locator('#result-score'); await resultScore.waitFor({ state: 'visible' }); return resultScore; @@ -46,6 +47,7 @@ export class ProgrammingExerciseOverviewPage { } getExerciseDetails() { + // TODO this is not the correct selector return this.page.locator('.tab-bar-exercise-details'); } } From 91a7321826b65c24e14e3b6a4c360ad1bee142b7 Mon Sep 17 00:00:00 2001 From: Ramona Beinstingel Date: Wed, 19 Jun 2024 07:39:36 +0200 Subject: [PATCH 06/23] add start date --- ...xercise-headers-information.component.html | 2 +- .../exercise-headers-information.component.ts | 63 ++++++++++--------- .../difficulty-level.component.spec.ts | 6 +- .../information-box.component.html | 8 +-- src/main/webapp/app/shared/shared.module.ts | 3 - .../content/scss/themes/theme-default.scss | 2 +- 6 files changed, 41 insertions(+), 43 deletions(-) diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html index 2b35759efe26..8bba9c751af8 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html +++ b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html @@ -1,5 +1,5 @@ @if (exercise) { -
+
@for (informationBoxItem of informationBoxItems; track informationBoxItem) { diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts index b98c123b841f..0c6678070439 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts @@ -18,6 +18,7 @@ import { ArtemisSharedModule } from 'app/shared/shared.module'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { SubmissionResultStatusModule } from 'app/overview/submission-result-status.module'; import { ExerciseCategoriesModule } from 'app/shared/exercise-categories/exercise-categories.module'; +import { InformationBoxComponent } from 'app/shared/information-box/information-box.component'; export interface InformationBox { title: string; @@ -33,7 +34,7 @@ export interface InformationBox { selector: 'jhi-exercise-headers-information', templateUrl: './exercise-headers-information.component.html', standalone: true, - imports: [ArtemisSharedModule, ArtemisSharedComponentModule, SubmissionResultStatusModule, ExerciseCategoriesModule], + imports: [ArtemisSharedModule, ArtemisSharedComponentModule, SubmissionResultStatusModule, ExerciseCategoriesModule, InformationBoxComponent], styleUrls: ['./exercise-headers-information.component.scss'], // Our tsconfig file has `preserveWhitespaces: 'true'` which causes whitespace to affect content projection. // We need to set it to 'false 'for this component, otherwise the components with the selector [contentComponent] @@ -78,8 +79,6 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { ) {} ngOnInit() { - // this.exerciseCategories = this.exercise.categories || []; - if (this.exercise.type) { this.icon = getIcon(this.exercise.type); } @@ -110,18 +109,22 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { } createInformationBoxItems() { + console.log('Create Information Box Items'); const notReleased = this.exercise.releaseDate && dayjs(this.exercise.releaseDate).isAfter(dayjs()); if (this.exercise.maxPoints) this.informationBoxItems.push(this.getPointsItem(this.exercise.maxPoints, 'points')); if (this.exercise.bonusPoints) this.informationBoxItems.push(this.getPointsItem(this.exercise.bonusPoints, 'bonus')); if (this.exercise.dueDate) this.informationBoxItems.push(this.getDueDateItem()); - this.informationBoxItems.push(this.getDifficultyItem()); + if (this.exercise.startDate && dayjs().isBefore(this.exercise.startDate)) this.informationBoxItems.push(this.getStartDateItem()); // (exercise.releaseDate && dayjs(exercise.releaseDate).isAfter(dayjs())) - if (notReleased || this.exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY || this.exercise.categories?.length) - this.informationBoxItems.push(this.getCategoryItems()); + // this.informationBoxItems.push(this.getNextRelevantDateItem()); // if (this.submissionPolicy?.active) this.informationBoxItems.push(this.getSubmissionPolicyItem()); this.informationBoxItems.push(this.getSubmissionStatusItem()); + if (this.submissionPolicy?.active && this.submissionPolicy?.submissionLimit) this.informationBoxItems.push(this.getSubmissionPolicyItem()); + this.informationBoxItems.push(this.getDifficultyItem()); + if (notReleased || this.exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY || this.exercise.categories?.length) + this.informationBoxItems.push(this.getCategoryItems()); // if (this.exercise.assessmentType && this.exercise.type === ExerciseType.PROGRAMMING) this.informationBoxItems.push(this.getAssessmentTypeItem()); } @@ -139,7 +142,6 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { return { title: 'artemisApp.courseOverview.exerciseDetails.submissionDue', - // less than a week make time relative to now content: this.dueDate, contentComponent: this.shouldDisplayDueDateRelative ? 'timeAgo' : 'dateTime', tooltip: this.shouldDisplayDueDateRelative ? 'artemisApp.courseOverview.exerciseDetails.submissionDueTooltip' : undefined, @@ -147,6 +149,15 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { tooltipParams: { date: this.dueDate?.format('lll') }, }; } + getStartDateItem(): InformationBox { + return { + title: 'artemisApp.courseOverview.exerciseDetails.startDate', + // less than a week make time relative to now + content: this.exercise.startDate, + contentComponent: 'dateTime', + tooltip: this.shouldDisplayDueDateRelative ? 'artemisApp.exerciseActions.startExerciseBeforeStartDate' : undefined, + }; + } // Status: Not released, no graded, graded, submitted, reviewed, assessed, complaint, complaint response, complaint applied, complaint resolved // getStatusItem(): InformationBox { @@ -164,6 +175,7 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { if (!title) return '-'; return title.toString().charAt(0).toUpperCase() + title.slice(1).toLowerCase(); } + getDifficultyItem(): InformationBox { return { title: 'artemisApp.courseOverview.exerciseDetails.difficulty', @@ -227,7 +239,6 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { // } - // separate Points and Bonus Points // DO one function with input getPointsItem(points: number | undefined, title: string): InformationBox { return { @@ -236,36 +247,26 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { }; } - // getDefaultItems(): InformationBox[] { - // const exercisesItem: InformationBox = { - // title: `${this.baseResource}`, - // icon: faEye, - // content: 'entity.action.view', - // }; - - // const statisticsItem: InformationBox = { - // routerLink: `${this.baseResource}scores`, - // icon: faTable, - // translation: 'entity.action.scores', - // }; - - // return [exercisesItem, statisticsItem]; - // } - - // Check what I really need - - ngOnChanges() { - this.course = this.course ?? getCourseFromExercise(this.exercise); - + updateSubmissionPolicyItem() { if (this.submissionPolicy?.active && this.submissionPolicy?.submissionLimit) { - console.log('Changes Submission'); this.countSubmissions(); + // need to push and pop the submission policy item to update the number of submissions - this.informationBoxItems.push(this.getSubmissionPolicyItem()); + const submissionItemIndex = this.informationBoxItems.findIndex((item) => item.title === 'artemisApp.programmingExercise.submissionPolicy.submissionLimitTitle'); + if (submissionItemIndex !== -1) { + this.informationBoxItems.splice(submissionItemIndex, 1, this.getSubmissionPolicyItem()); + } // if (this.submissionPolicy?.exceedingPenalty) { // this.informationBoxItems.push(this.getExceedingPenalty()); // } } + } + + ngOnChanges() { + this.course = this.course ?? getCourseFromExercise(this.exercise); + + this.updateSubmissionPolicyItem(); + if (this.studentParticipation?.results?.length) { // The updated participation by the websocket is not guaranteed to be sorted, find the newest result (highest id) this.sortService.sortByProperty(this.studentParticipation.results, 'id', false); diff --git a/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.spec.ts b/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.spec.ts index b2577461c8de..5922de68ebf9 100644 --- a/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.spec.ts +++ b/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.spec.ts @@ -1,5 +1,5 @@ /* tslint:disable:no-unused-variable */ -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DifficultyLevelComponent } from './difficulty-level.component'; @@ -7,11 +7,11 @@ describe('DifficultyLevelComponent', () => { let component: DifficultyLevelComponent; let fixture: ComponentFixture; - beforeEach(async(() => { + beforeEach(() => { TestBed.configureTestingModule({ declarations: [DifficultyLevelComponent], }).compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(DifficultyLevelComponent); diff --git a/src/main/webapp/app/shared/information-box/information-box.component.html b/src/main/webapp/app/shared/information-box/information-box.component.html index c4f606ecc857..d3297334f362 100644 --- a/src/main/webapp/app/shared/information-box/information-box.component.html +++ b/src/main/webapp/app/shared/information-box/information-box.component.html @@ -1,15 +1,15 @@ @if (informationBoxData) {
- - - + + @if (informationBoxData.contentComponent) { } @else { diff --git a/src/main/webapp/app/shared/shared.module.ts b/src/main/webapp/app/shared/shared.module.ts index 0969d4da17bd..3cca168d2b56 100644 --- a/src/main/webapp/app/shared/shared.module.ts +++ b/src/main/webapp/app/shared/shared.module.ts @@ -28,7 +28,6 @@ import { ConfirmEntityNameComponent } from 'app/shared/confirm-entity-name/confi import { DetailOverviewNavigationBarComponent } from 'app/shared/detail-overview-navigation-bar/detail-overview-navigation-bar.component'; import { ScienceDirective } from 'app/shared/science/science.directive'; import { SearchFilterComponent } from './search-filter/search-filter.component'; -import { InformationBoxComponent } from './information-box/information-box.component'; import { DifficultyLevelComponent } from './difficulty-level/difficulty-level.component'; @NgModule({ @@ -59,7 +58,6 @@ import { DifficultyLevelComponent } from './difficulty-level/difficulty-level.co StickyPopoverDirective, ScienceDirective, SearchFilterComponent, - InformationBoxComponent, DifficultyLevelComponent, ], exports: [ @@ -92,7 +90,6 @@ import { DifficultyLevelComponent } from './difficulty-level/difficulty-level.co StickyPopoverDirective, ScienceDirective, SearchFilterComponent, - InformationBoxComponent, DifficultyLevelComponent, ], }) diff --git a/src/main/webapp/content/scss/themes/theme-default.scss b/src/main/webapp/content/scss/themes/theme-default.scss index 182f24e32e94..19e5381fbeac 100644 --- a/src/main/webapp/content/scss/themes/theme-default.scss +++ b/src/main/webapp/content/scss/themes/theme-default.scss @@ -49,5 +49,5 @@ html { } .btn-outline-primary:not(.list-group-item):hover { - color: black; + color: #fff; } From 6cd6de624fa78aadf9a4ed8f036e119bcc76f65a Mon Sep 17 00:00:00 2001 From: Ramona Beinstingel Date: Tue, 16 Jul 2024 10:23:51 +0200 Subject: [PATCH 07/23] restructure exercise-header-information --- ...xercise-headers-information.component.html | 2 +- .../exercise-headers-information.component.ts | 177 +++++++++--------- .../open-code-editor-button.component.html | 4 +- .../webapp/i18n/de/student-dashboard.json | 2 +- .../webapp/i18n/en/student-dashboard.json | 2 +- 5 files changed, 95 insertions(+), 92 deletions(-) diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html index 8bba9c751af8..791b44bbed35 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html +++ b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html @@ -1,5 +1,5 @@ @if (exercise) { -
+
@for (informationBoxItem of informationBoxItems; track informationBoxItem) { diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts index 0c6678070439..fcb52abce235 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts @@ -9,7 +9,7 @@ import { StudentParticipation } from 'app/entities/participation/student-partici import { getExerciseDueDate } from 'app/exercises/shared/exercise/exercise.utils'; import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; import { Course } from 'app/entities/course.model'; -// import { AssessmentType } from 'app/entities/assessment-type.model'; +import { AssessmentType } from 'app/entities/assessment-type.model'; import { SubmissionType } from 'app/entities/submission.model'; import { ProgrammingSubmission } from 'app/entities/programming-submission.model'; import { roundValueSpecifiedByCourseSettings } from 'app/shared/util/utils'; @@ -19,6 +19,7 @@ import { ArtemisSharedComponentModule } from 'app/shared/components/shared-compo import { SubmissionResultStatusModule } from 'app/overview/submission-result-status.module'; import { ExerciseCategoriesModule } from 'app/shared/exercise-categories/exercise-categories.module'; import { InformationBoxComponent } from 'app/shared/information-box/information-box.component'; +import { ComplaintService } from 'app/complaints/complaint.service'; export interface InformationBox { title: string; @@ -43,7 +44,7 @@ export interface InformationBox { }) export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { readonly IncludedInOverallScore = IncludedInOverallScore; - // readonly AssessmentType = AssessmentType; + readonly AssessmentType = AssessmentType; readonly ExerciseType = ExerciseType; readonly getIcon = getIcon; readonly getIconTooltip = getIconTooltip; @@ -61,11 +62,7 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { isBeforeStartDate: boolean; programmingExercise?: ProgrammingExercise; individualComplaintDueDate?: dayjs.Dayjs; - // public nextRelevantDate?: dayjs.Dayjs; - // public nextRelevantDateLabel?: string; - // public nextRelevantDateStatusBadge?: string; dueDateStatusBadge?: string; - canComplainLaterOn: boolean; achievedPoints?: number; numberOfSubmissions: number; informationBoxItems: InformationBox[] = []; @@ -84,20 +81,6 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { } this.dueDate = getExerciseDueDate(this.exercise, this.studentParticipation); - // this.isBeforeStartDate = this.exercise.startDate ? this.exercise.startDate.isAfter(dayjs()) : !!this.exercise.releaseDate?.isAfter(dayjs()); - // if (this.course?.maxComplaintTimeDays) { - // this.individualComplaintDueDate = ComplaintService.getIndividualComplaintDueDate( - // this.exercise, - // this.course.maxComplaintTimeDays, - // this.studentParticipation?.results?.last(), - // this.studentParticipation, - // ); - // } - // // There is a submission where the student did not have the chance to complain yet - // this.canComplainLaterOn = - // !!this.studentParticipation?.submissionCount && - // !this.individualComplaintDueDate && - // (this.exercise.allowComplaintsForAutomaticAssessments || this.exercise.assessmentType !== AssessmentType.AUTOMATIC); if (this.dueDate) { // If the due date is less than a day away, the color change to red @@ -105,28 +88,62 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { // If the due date is less than a week away, text is displayed relativley e.g. 'in 2 days' this.shouldDisplayDueDateRelative = this.dueDate.isBetween(dayjs().add(1, 'week'), dayjs()) ? true : false; } + if (this.course?.maxComplaintTimeDays) { + this.individualComplaintDueDate = ComplaintService.getIndividualComplaintDueDate( + this.exercise, + this.course.maxComplaintTimeDays, + this.studentParticipation?.results?.last(), + this.studentParticipation, + ); + } this.createInformationBoxItems(); } createInformationBoxItems() { - console.log('Create Information Box Items'); - const notReleased = this.exercise.releaseDate && dayjs(this.exercise.releaseDate).isAfter(dayjs()); - if (this.exercise.maxPoints) this.informationBoxItems.push(this.getPointsItem(this.exercise.maxPoints, 'points')); - if (this.exercise.bonusPoints) this.informationBoxItems.push(this.getPointsItem(this.exercise.bonusPoints, 'bonus')); - - if (this.exercise.dueDate) this.informationBoxItems.push(this.getDueDateItem()); - if (this.exercise.startDate && dayjs().isBefore(this.exercise.startDate)) this.informationBoxItems.push(this.getStartDateItem()); - // (exercise.releaseDate && dayjs(exercise.releaseDate).isAfter(dayjs())) + this.addPointsItems(); + this.addDueDateItems(); + this.addStartDateItem(); + this.addSubmissionStatusItem(); + this.addSubmissionPolicyItem(); + this.addDifficultyItem(); + this.addCategoryItems(); + } - // this.informationBoxItems.push(this.getNextRelevantDateItem()); - // if (this.submissionPolicy?.active) this.informationBoxItems.push(this.getSubmissionPolicyItem()); - this.informationBoxItems.push(this.getSubmissionStatusItem()); - if (this.submissionPolicy?.active && this.submissionPolicy?.submissionLimit) this.informationBoxItems.push(this.getSubmissionPolicyItem()); - this.informationBoxItems.push(this.getDifficultyItem()); - if (notReleased || this.exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY || this.exercise.categories?.length) - this.informationBoxItems.push(this.getCategoryItems()); + addPointsItems() { + const { maxPoints, bonusPoints } = this.exercise; + if (maxPoints) { + this.informationBoxItems.push(this.getPointsItem(maxPoints, 'points')); + } + if (bonusPoints) { + this.informationBoxItems.push(this.getPointsItem(bonusPoints, 'bonus')); + } + } - // if (this.exercise.assessmentType && this.exercise.type === ExerciseType.PROGRAMMING) this.informationBoxItems.push(this.getAssessmentTypeItem()); + addDueDateItems() { + const now = dayjs(); + if (this.dueDate) { + this.informationBoxItems.push(this.getDueDateItem()); + } + // If the due date is in the past and the assessment due date is in the future, show the assessment due date + if (this.dueDate?.isBefore(now) && this.exercise.assessmentDueDate?.isAfter(now)) { + const assessmentDueItem = { + title: 'artemisApp.courseOverview.exerciseDetails.assessmentDue', + content: this.exercise.assessmentDueDate, + contentComponent: 'dateTime', + tooltip: 'artemisApp.courseOverview.exerciseDetails.assessmentDueTooltip', + }; + this.informationBoxItems.push(assessmentDueItem); + } + // If the assessment due date is in the past and the complaint due date is in the future, show the complaint due date + if (this.exercise.assessmentDueDate?.isBefore(now) && this.individualComplaintDueDate?.isAfter(now)) { + const complaintDueItem = { + title: 'artemisApp.courseOverview.exerciseDetails.complaintDue', + content: this.individualComplaintDueDate, + contentComponent: 'dateTime', + tooltip: 'artemisApp.courseOverview.exerciseDetails.complaintDueTooltip', + }; + this.informationBoxItems.push(complaintDueItem); + } } getDueDateItem(): InformationBox { @@ -149,56 +166,57 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { tooltipParams: { date: this.dueDate?.format('lll') }, }; } - getStartDateItem(): InformationBox { - return { - title: 'artemisApp.courseOverview.exerciseDetails.startDate', - // less than a week make time relative to now - content: this.exercise.startDate, - contentComponent: 'dateTime', - tooltip: this.shouldDisplayDueDateRelative ? 'artemisApp.exerciseActions.startExerciseBeforeStartDate' : undefined, - }; - } - // Status: Not released, no graded, graded, submitted, reviewed, assessed, complaint, complaint response, complaint applied, complaint resolved - // getStatusItem(): InformationBox { - - // } - - // getAssessmentTypeItem(): InformationBox { - // return { - // title: 'artemisApp.courseOverview.exerciseDetails.assessmentType', - // content: this.capitalize(this.exercise?.assessmentType), - // tooltip: 'artemisApp.AssessmentType.tooltip.' + this.exercise.assessmentType, - // }; - // } - capitalize(title?: string) { - if (!title) return '-'; - return title.toString().charAt(0).toUpperCase() + title.slice(1).toLowerCase(); + addStartDateItem() { + if (this.exercise.startDate && dayjs().isBefore(this.exercise.startDate)) { + const startDateItem = { + title: 'artemisApp.courseOverview.exerciseDetails.startDate', + // less than a week make time relative to now + content: this.exercise.startDate, + contentComponent: 'dateTime', + tooltip: this.shouldDisplayDueDateRelative ? 'artemisApp.exerciseActions.startExerciseBeforeStartDate' : undefined, + }; + this.informationBoxItems.push(startDateItem); + } } - getDifficultyItem(): InformationBox { - return { + addDifficultyItem() { + const difficultyItem = { title: 'artemisApp.courseOverview.exerciseDetails.difficulty', content: this.exercise.difficulty, contentComponent: 'difficultyLevel', }; + this.informationBoxItems.push(difficultyItem); } - getSubmissionStatusItem(): InformationBox { - return { - title: 'artemisApp.courseOverview.exerciseDetails.submissionStatus', + + addSubmissionStatusItem() { + const submissionStatusItem = { + title: 'artemisApp.courseOverview.exerciseDetails.status', content: this.studentParticipation, contentComponent: 'submissionStatus', }; + this.informationBoxItems.push(submissionStatusItem); } - getCategoryItems(): InformationBox { - return { - title: 'artemisApp.courseOverview.exerciseDetails.categories', - content: this.exercise, - contentComponent: 'categories', - }; + addCategoryItems() { + const notReleased = this.exercise.releaseDate && dayjs(this.exercise.releaseDate).isAfter(dayjs()); + + if (notReleased || this.exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY || this.exercise.categories?.length) { + const categoryItem = { + title: 'artemisApp.courseOverview.exerciseDetails.categories', + content: this.exercise, + contentComponent: 'categories', + }; + this.informationBoxItems.push(categoryItem); + } } - getSubmissionPolicyItem(): InformationBox { + addSubmissionPolicyItem() { + if (this.submissionPolicy?.active && this.submissionPolicy?.submissionLimit) { + this.informationBoxItems.push(this.getSubmissionPolicyItem()); + } + } + + getSubmissionPolicyItem() { return { title: 'artemisApp.programmingExercise.submissionPolicy.submissionLimitTitle', content: this.numberOfSubmissions + ' / ' + this.submissionPolicy?.submissionLimit, @@ -225,21 +243,6 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { return submissionsLeft <= 0 ? 'danger' : 'warning'; } } - - // Can be visible in the tooltip above a status - // getNextRelevantDateItem(): InformationBox { - // console.log('get Next Relevant Date Item') - // console.log(this.nextRelevantDateLabel) - // // {{ 'artemisApp.courseOverview.exerciseDetails.' + nextRelevantDateLabel | artemisTranslate }} - // return { - // title: this.nextRelevantDateLabel ? this.nextRelevantDateLabel : 'Next Relevant Date', - // content: this.nextRelevantDate?.format('lll') ?? '-', - // icon: faQuestionCircle, - // }; - - // } - - // DO one function with input getPointsItem(points: number | undefined, title: string): InformationBox { return { title: 'artemisApp.courseOverview.exerciseDetails.' + title, diff --git a/src/main/webapp/app/shared/components/open-code-editor-button/open-code-editor-button.component.html b/src/main/webapp/app/shared/components/open-code-editor-button/open-code-editor-button.component.html index f3cd9603c5a5..47eafc60ec58 100644 --- a/src/main/webapp/app/shared/components/open-code-editor-button/open-code-editor-button.component.html +++ b/src/main/webapp/app/shared/components/open-code-editor-button/open-code-editor-button.component.html @@ -22,7 +22,7 @@ [buttonLabel]="'artemisApp.exerciseActions.openCodeEditor' | artemisTranslate" [buttonLoading]="loading" [smallButton]="smallButtons" - [hideLabelMobile]="false" + [hideLabelMobile]="hideLabelMobile" [ngbPopover]="popContent" [autoClose]="'outside'" placement="right auto" @@ -44,7 +44,7 @@ [buttonLabel]="'artemisApp.exerciseActions.' + (isPracticeMode ? 'openPracticeCodeEditor' : 'openGradedCodeEditor') | artemisTranslate" [buttonLoading]="loading" [smallButton]="smallButtons" - [hideLabelMobile]="false" + [hideLabelMobile]="hideLabelMobile" [routerLink]="[courseAndExerciseNavigationUrl, activeParticipation.id]" > diff --git a/src/main/webapp/i18n/de/student-dashboard.json b/src/main/webapp/i18n/de/student-dashboard.json index 73a2b105fb5b..8e309845e6b6 100644 --- a/src/main/webapp/i18n/de/student-dashboard.json +++ b/src/main/webapp/i18n/de/student-dashboard.json @@ -316,7 +316,7 @@ "complaintPossibleTooltip": "Nach Erhalt einer Bewertung wirst du {{days}} Tage zum Einreichen einer Beschwerde haben.", "presented": "Präsentiert", "presentation": "Präsentation", - "submissionStatus": "Einreichungsstatus" + "status": "Status" } } } diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index 95181187bbae..046b0b241c90 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -316,7 +316,7 @@ "complaintPossibleTooltip": "After receiving an assessment, you will have {{days}} days to file a complaint.", "presented": "Presented", "presentation": "Presentation", - "submissionStatus": "Submission Status" + "status": "Status" } } } From f38e4474b195ffbe62454678d53190e914b67cf0 Mon Sep 17 00:00:00 2001 From: Ramona Beinstingel Date: Tue, 16 Jul 2024 17:03:04 +0200 Subject: [PATCH 08/23] refactor and make type safe --- .../exam-start-information.component.html | 13 +- .../exam-start-information.component.ts | 59 +++-- ...xercise-headers-information.component.html | 22 +- .../exercise-headers-information.component.ts | 212 +++++++++--------- .../shared/result/result.component.html | 25 +-- .../shared/result/result.component.ts | 1 + .../result/updating-result.component.html | 1 + .../result/updating-result.component.ts | 1 + .../course-exercise-details.component.html | 6 +- .../submission-result-status.component.html | 1 + .../submission-result-status.component.ts | 1 + .../difficulty-level.component.spec.ts | 25 --- .../information-box.component.html | 13 +- .../information-box.component.ts | 42 ++-- src/main/webapp/app/utils/date.utils.ts | 4 + ...cise-headers-information.component.spec.ts | 82 ++++++- .../course-exercise-details.component.spec.ts | 2 + .../shared/difficulty-level.component.spec.ts | 44 ++++ .../shared/information-box.component.spec.ts | 46 +++- 19 files changed, 378 insertions(+), 222 deletions(-) delete mode 100644 src/main/webapp/app/shared/difficulty-level/difficulty-level.component.spec.ts create mode 100644 src/test/javascript/spec/component/shared/difficulty-level.component.spec.ts diff --git a/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.html b/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.html index e7d9752a2f15..7629165ad45f 100644 --- a/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.html +++ b/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.html @@ -3,14 +3,11 @@
- @if (informationBoxData.contentComponent === 'formatedDate') { - - {{ informationBoxData.content | artemisDate: 'long-date' }} - - - {{ informationBoxData.content | artemisDate: 'time' }} - - } @else if (informationBoxData.contentComponent === 'workingTime') { - + @if (informationBoxData.content.type === 'dateTime') { + {{ informationBoxData.content.value | artemisDate }} + } + @if (informationBoxData.content.type === 'workingTime') { + } diff --git a/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts b/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts index 33237b989106..01e22802405e 100644 --- a/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts +++ b/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts @@ -1,8 +1,8 @@ import { Component, Input, OnInit } from '@angular/core'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; -import { InformationBox, InformationBoxComponent } from 'app/shared/information-box/information-box.component'; -import { Exam } from 'app/entities/exam/exam.model'; +import { InformationBox, InformationBoxComponent, InformationBoxContent } from 'app/shared/information-box/information-box.component'; +import { Exam } from 'app/entities/exam.model'; import { StudentExam } from 'app/entities/student-exam.model'; import { ArtemisExamSharedModule } from 'app/exam/shared/exam-shared.module'; import dayjs from 'dayjs/esm'; @@ -45,45 +45,78 @@ export class ExamStartInformationComponent implements OnInit { this.prepareInformationBoxData(); } - buildInformationBox(boxTitle: string, boxContent: string | number, boxContentComponent?: string): InformationBox { + buildInformationBox(boxTitle: string, boxContent: InformationBoxContent, isContentComponent?: boolean): InformationBox { const examInformationBoxData: InformationBox = { title: boxTitle ?? '', - content: boxContent ?? '', - contentComponent: boxContentComponent, + content: boxContent, + isContentComponent: isContentComponent ?? false, }; return examInformationBoxData; } prepareInformationBoxData(): void { if (this.moduleNumber) { - const informationBoxModuleNumber = this.buildInformationBox('artemisApp.exam.moduleNumber', this.moduleNumber!); + const boxContentModuleNumber: InformationBoxContent = { + type: 'string', + value: this.moduleNumber, + }; + const informationBoxModuleNumber = this.buildInformationBox('artemisApp.exam.moduleNumber', boxContentModuleNumber); this.examInformationBoxData.push(informationBoxModuleNumber); } if (this.courseName) { - const informationBoxCourseName = this.buildInformationBox('artemisApp.exam.course', this.courseName!); + const boxContentCourseName: InformationBoxContent = { + type: 'string', + value: this.courseName, + }; + const informationBoxCourseName = this.buildInformationBox('artemisApp.exam.course', boxContentCourseName); this.examInformationBoxData.push(informationBoxCourseName); } if (this.examiner) { - const informationBoxExaminer = this.buildInformationBox('artemisApp.examManagement.examiner', this.examiner!); + const boxContentExaminer: InformationBoxContent = { + type: 'string', + value: this.examiner, + }; + const informationBoxExaminer = this.buildInformationBox('artemisApp.examManagement.examiner', boxContentExaminer); this.examInformationBoxData.push(informationBoxExaminer); } if (this.examinedStudent) { - const informationBoxExaminedStudent = this.buildInformationBox('artemisApp.exam.examinedStudent', this.examinedStudent!); + const boxContentExaminedStudent: InformationBoxContent = { + type: 'string', + value: this.examinedStudent, + }; + const informationBoxExaminedStudent = this.buildInformationBox('artemisApp.exam.examinedStudent', boxContentExaminedStudent); this.examInformationBoxData.push(informationBoxExaminedStudent); } if (this.startDate) { - const informationBoxStartDate = this.buildInformationBox('artemisApp.exam.date', this.startDate.toString(), 'formatedDate'); + const boxContentStartDate: InformationBoxContent = { + type: 'dateTime', + value: this.startDate, + }; + const informationBoxStartDate = this.buildInformationBox('artemisApp.exam.date', boxContentStartDate, true); this.examInformationBoxData.push(informationBoxStartDate); } - const informationBoxTotalWorkingTime = this.buildInformationBox('artemisApp.exam.workingTime', this.exam.workingTime!, 'workingTime'); + const boxContentExamWorkingTime: InformationBoxContent = { + type: 'workingTime', + value: this.studentExam, + }; + + const informationBoxTotalWorkingTime = this.buildInformationBox('artemisApp.exam.workingTime', boxContentExamWorkingTime, true); this.examInformationBoxData.push(informationBoxTotalWorkingTime); + const boxContentTotalPoints: InformationBoxContent = { + type: 'dateTime', + value: this.totalPoints!.toString(), + }; - const informationBoxTotalPoints = this.buildInformationBox('artemisApp.exam.points', this.totalPoints!.toString()); + const informationBoxTotalPoints = this.buildInformationBox('artemisApp.exam.points', boxContentTotalPoints); this.examInformationBoxData.push(informationBoxTotalPoints); if (this.numberOfExercisesInExam) { - const informationBoxNumberOfExercises = this.buildInformationBox('artemisApp.exam.exercises', this.numberOfExercisesInExam!.toString()); + const boxContent: InformationBoxContent = { + type: 'string', + value: this.numberOfExercisesInExam!.toString(), + }; + const informationBoxNumberOfExercises = this.buildInformationBox('artemisApp.exam.exercises', boxContent); this.examInformationBoxData.push(informationBoxNumberOfExercises); } } diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html index 791b44bbed35..64ebaec50e0b 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html +++ b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html @@ -3,33 +3,33 @@ @for (informationBoxItem of informationBoxItems; track informationBoxItem) { - @if (informationBoxItem.contentComponent === 'difficultyLevel') { - + @if (informationBoxItem.content.type === 'difficultyLevel') { + } - @if (informationBoxItem.contentComponent === 'categories') { + @if (informationBoxItem.content.type === 'categories') { } - @if (informationBoxItem.contentComponent === 'timeAgo') { - {{ informationBoxItem.content | artemisTimeAgo }} + @if (informationBoxItem.content.type === 'timeAgo') { + {{ informationBoxItem.content.value | artemisTimeAgo }} } - @if (informationBoxItem.contentComponent === 'dateTime') { - {{ informationBoxItem.content | artemisDate }} + @if (informationBoxItem.content.type === 'dateTime') { + {{ informationBoxItem.content.value | artemisDate }} } - @if (informationBoxItem.contentComponent === 'submissionStatus') { + @if (informationBoxItem.content.type === 'submissionStatus') { } diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts index fcb52abce235..bf6c8d6011fb 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts @@ -1,93 +1,55 @@ import { Component, Input, OnChanges, OnInit } from '@angular/core'; import { SortService } from 'app/shared/service/sort.service'; import dayjs from 'dayjs/esm'; -import { Exercise, ExerciseType, IncludedInOverallScore, getCourseFromExercise, getIcon, getIconTooltip } from 'app/entities/exercise.model'; -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { ExerciseCategory } from 'app/entities/exercise-category.model'; +import { Exercise, IncludedInOverallScore, getCourseFromExercise } from 'app/entities/exercise.model'; import { SubmissionPolicy } from 'app/entities/submission-policy.model'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; import { getExerciseDueDate } from 'app/exercises/shared/exercise/exercise.utils'; import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; import { Course } from 'app/entities/course.model'; -import { AssessmentType } from 'app/entities/assessment-type.model'; import { SubmissionType } from 'app/entities/submission.model'; import { ProgrammingSubmission } from 'app/entities/programming-submission.model'; import { roundValueSpecifiedByCourseSettings } from 'app/shared/util/utils'; -import { TranslateService } from '@ngx-translate/core'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { SubmissionResultStatusModule } from 'app/overview/submission-result-status.module'; import { ExerciseCategoriesModule } from 'app/shared/exercise-categories/exercise-categories.module'; -import { InformationBoxComponent } from 'app/shared/information-box/information-box.component'; +import { InformationBox, InformationBoxComponent } from 'app/shared/information-box/information-box.component'; import { ComplaintService } from 'app/complaints/complaint.service'; +import { isDateLessThanAWeekAway } from 'app/utils/date.utils'; -export interface InformationBox { - title: string; - content: string | number | any; - contentType?: string; - contentComponent?: any; - icon?: IconProp; - tooltip?: string; - contentColor?: string; - tooltipParams?: Record; -} @Component({ selector: 'jhi-exercise-headers-information', templateUrl: './exercise-headers-information.component.html', standalone: true, imports: [ArtemisSharedModule, ArtemisSharedComponentModule, SubmissionResultStatusModule, ExerciseCategoriesModule, InformationBoxComponent], styleUrls: ['./exercise-headers-information.component.scss'], - // Our tsconfig file has `preserveWhitespaces: 'true'` which causes whitespace to affect content projection. - // We need to set it to 'false 'for this component, otherwise the components with the selector [contentComponent] - // will not be projected into their specific slot of the "InformationBoxComponent" component. + /* Our tsconfig file has `preserveWhitespaces: 'true'` which causes whitespace to affect content projection. + We need to set it to 'false 'for this component, otherwise the components with the selector [contentComponent] + will not be projected into their specific slot of the "InformationBoxComponent" component.*/ preserveWhitespaces: false, }) export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { readonly IncludedInOverallScore = IncludedInOverallScore; - readonly AssessmentType = AssessmentType; - readonly ExerciseType = ExerciseType; - readonly getIcon = getIcon; - readonly getIconTooltip = getIconTooltip; readonly dayjs = dayjs; @Input() exercise: Exercise; @Input() studentParticipation?: StudentParticipation; - @Input() title: string; @Input() course?: Course; - @Input() isTestRun = false; @Input() submissionPolicy?: SubmissionPolicy; - exerciseCategories: ExerciseCategory[]; dueDate?: dayjs.Dayjs; - isBeforeStartDate: boolean; programmingExercise?: ProgrammingExercise; individualComplaintDueDate?: dayjs.Dayjs; - dueDateStatusBadge?: string; achievedPoints?: number; numberOfSubmissions: number; informationBoxItems: InformationBox[] = []; - shouldDisplayDueDateRelative = false; - - icon: IconProp; - constructor( - private sortService: SortService, - private translateService: TranslateService, - ) {} + constructor(private sortService: SortService) {} ngOnInit() { - if (this.exercise.type) { - this.icon = getIcon(this.exercise.type); - } - this.dueDate = getExerciseDueDate(this.exercise, this.studentParticipation); - if (this.dueDate) { - // If the due date is less than a day away, the color change to red - this.dueDateStatusBadge = this.dueDate.isBetween(dayjs().add(1, 'day'), dayjs()) ? 'danger' : 'body-color'; - // If the due date is less than a week away, text is displayed relativley e.g. 'in 2 days' - this.shouldDisplayDueDateRelative = this.dueDate.isBetween(dayjs().add(1, 'week'), dayjs()) ? true : false; - } if (this.course?.maxComplaintTimeDays) { this.individualComplaintDueDate = ComplaintService.getIndividualComplaintDueDate( this.exercise, @@ -121,90 +83,126 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { addDueDateItems() { const now = dayjs(); - if (this.dueDate) { - this.informationBoxItems.push(this.getDueDateItem()); + + const dueDateItem = this.getDueDateItem(); + if (dueDateItem) { + this.informationBoxItems.push(dueDateItem); } // If the due date is in the past and the assessment due date is in the future, show the assessment due date if (this.dueDate?.isBefore(now) && this.exercise.assessmentDueDate?.isAfter(now)) { - const assessmentDueItem = { + const assessmentDueItem: InformationBox = { title: 'artemisApp.courseOverview.exerciseDetails.assessmentDue', - content: this.exercise.assessmentDueDate, - contentComponent: 'dateTime', + content: { + type: 'dateTime', + value: this.exercise.assessmentDueDate, + }, + isContentComponent: true, tooltip: 'artemisApp.courseOverview.exerciseDetails.assessmentDueTooltip', }; this.informationBoxItems.push(assessmentDueItem); } - // If the assessment due date is in the past and the complaint due date is in the future, show the complaint due date + // // If the assessment due date is in the past and the complaint due date is in the future, show the complaint due date if (this.exercise.assessmentDueDate?.isBefore(now) && this.individualComplaintDueDate?.isAfter(now)) { - const complaintDueItem = { + const complaintDueItem: InformationBox = { title: 'artemisApp.courseOverview.exerciseDetails.complaintDue', - content: this.individualComplaintDueDate, - contentComponent: 'dateTime', + content: { + type: 'dateTime', + value: this.individualComplaintDueDate, + }, + isContentComponent: true, tooltip: 'artemisApp.courseOverview.exerciseDetails.complaintDueTooltip', }; this.informationBoxItems.push(complaintDueItem); } } - getDueDateItem(): InformationBox { - const isDueDateInThePast = this.dueDate?.isBefore(dayjs()); + getDueDateItem(): InformationBox | undefined { + if (this.dueDate) { + const isDueDateInThePast = this.dueDate.isBefore(dayjs()); + // If the due date is less than a day away, the color change to red + const dueDateStatusBadge = this.dueDate.isBetween(dayjs().add(1, 'day'), dayjs()) ? 'danger' : 'body-color'; + // If the due date is less than a week away, text is displayed relatively e.g. 'in 2 days' + const shouldDisplayDueDateRelative = isDateLessThanAWeekAway(this.dueDate); + + if (isDueDateInThePast) { + return { + title: 'artemisApp.courseOverview.exerciseDetails.submissionDueOver', + content: { + type: 'dateTime', + value: this.dueDate, + }, + isContentComponent: true, + }; + } - if (isDueDateInThePast) { return { - title: 'artemisApp.courseOverview.exerciseDetails.submissionDueOver', - content: this.dueDate, - contentComponent: 'dateTime', + title: 'artemisApp.courseOverview.exerciseDetails.submissionDue', + content: { + type: shouldDisplayDueDateRelative ? 'timeAgo' : 'dateTime', + value: this.dueDate, + }, + isContentComponent: true, + tooltip: shouldDisplayDueDateRelative ? 'artemisApp.courseOverview.exerciseDetails.submissionDueTooltip' : undefined, + contentColor: dueDateStatusBadge, + tooltipParams: { date: this.dueDate?.format('lll') }, }; } - - return { - title: 'artemisApp.courseOverview.exerciseDetails.submissionDue', - content: this.dueDate, - contentComponent: this.shouldDisplayDueDateRelative ? 'timeAgo' : 'dateTime', - tooltip: this.shouldDisplayDueDateRelative ? 'artemisApp.courseOverview.exerciseDetails.submissionDueTooltip' : undefined, - contentColor: this.dueDateStatusBadge, - tooltipParams: { date: this.dueDate?.format('lll') }, - }; } addStartDateItem() { if (this.exercise.startDate && dayjs().isBefore(this.exercise.startDate)) { - const startDateItem = { + // If the start date is less than a week away, text is displayed relatively e.g. 'in 2 days' + const shouldDisplayStartDateRelative = isDateLessThanAWeekAway(this.exercise.startDate); + const startDateItem: InformationBox = { title: 'artemisApp.courseOverview.exerciseDetails.startDate', - // less than a week make time relative to now - content: this.exercise.startDate, - contentComponent: 'dateTime', - tooltip: this.shouldDisplayDueDateRelative ? 'artemisApp.exerciseActions.startExerciseBeforeStartDate' : undefined, + content: { + type: shouldDisplayStartDateRelative ? 'timeAgo' : 'dateTime', + value: this.exercise.startDate, + }, + isContentComponent: true, + tooltip: shouldDisplayStartDateRelative ? 'artemisApp.exerciseActions.startExerciseBeforeStartDate' : undefined, }; this.informationBoxItems.push(startDateItem); } } addDifficultyItem() { - const difficultyItem = { - title: 'artemisApp.courseOverview.exerciseDetails.difficulty', - content: this.exercise.difficulty, - contentComponent: 'difficultyLevel', - }; - this.informationBoxItems.push(difficultyItem); + if (this.exercise.difficulty) { + const difficultyItem: InformationBox = { + title: 'artemisApp.courseOverview.exerciseDetails.difficulty', + content: { + type: 'difficultyLevel', + value: this.exercise.difficulty, + }, + isContentComponent: true, + }; + this.informationBoxItems.push(difficultyItem); + } } addSubmissionStatusItem() { - const submissionStatusItem = { + const submissionStatusItem: InformationBox = { title: 'artemisApp.courseOverview.exerciseDetails.status', - content: this.studentParticipation, - contentComponent: 'submissionStatus', + content: { + type: 'submissionStatus', + value: this.exercise, + }, + isContentComponent: true, }; this.informationBoxItems.push(submissionStatusItem); } + addCategoryItems() { const notReleased = this.exercise.releaseDate && dayjs(this.exercise.releaseDate).isAfter(dayjs()); if (notReleased || this.exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY || this.exercise.categories?.length) { - const categoryItem = { + const categoryItem: InformationBox = { title: 'artemisApp.courseOverview.exerciseDetails.categories', - content: this.exercise, - contentComponent: 'categories', + content: { + type: 'categories', + value: this.exercise, + }, + isContentComponent: true, }; this.informationBoxItems.push(categoryItem); } @@ -216,21 +214,14 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { } } - getSubmissionPolicyItem() { + getSubmissionPolicyItem(): InformationBox { return { title: 'artemisApp.programmingExercise.submissionPolicy.submissionLimitTitle', - content: this.numberOfSubmissions + ' / ' + this.submissionPolicy?.submissionLimit, - + content: { + type: 'string', + value: `${this.numberOfSubmissions} / ${this.submissionPolicy?.submissionLimit}`, + }, contentColor: this.submissionPolicy?.submissionLimit ? this.getSubmissionColor() : 'body-color', - // content: - // this.numberOfSubmissions + - // '/' + - // this.submissionPolicy?.submissionLimit + - // (this.submissionPolicy?.exceedingPenalty - // ? ' ' + this.translateService.instant('artemisApp.programmingExercise.submissionPolicy.submissionPenalty.penaltyInfoLabel', { - // points: this.submissionPolicy.exceedingPenalty, - // }) - // : ''), tooltip: 'artemisApp.programmingExercise.submissionPolicy.submissionPolicyType.' + this.submissionPolicy?.type + '.tooltip', tooltipParams: { points: this.submissionPolicy?.exceedingPenalty?.toString() }, }; @@ -246,30 +237,29 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { getPointsItem(points: number | undefined, title: string): InformationBox { return { title: 'artemisApp.courseOverview.exerciseDetails.' + title, - content: this.achievedPoints !== undefined ? this.achievedPoints + ' / ' + points : '0 / ' + points, + content: { + type: 'string', + value: this.achievedPoints ? `${this.achievedPoints} / ${points}` : `0 / ${points}`, + }, }; } updateSubmissionPolicyItem() { - if (this.submissionPolicy?.active && this.submissionPolicy?.submissionLimit) { - this.countSubmissions(); + this.countSubmissions(); - // need to push and pop the submission policy item to update the number of submissions - const submissionItemIndex = this.informationBoxItems.findIndex((item) => item.title === 'artemisApp.programmingExercise.submissionPolicy.submissionLimitTitle'); - if (submissionItemIndex !== -1) { - this.informationBoxItems.splice(submissionItemIndex, 1, this.getSubmissionPolicyItem()); - } - // if (this.submissionPolicy?.exceedingPenalty) { - // this.informationBoxItems.push(this.getExceedingPenalty()); - // } + // need to push and pop the submission policy item to update the number of submissions + const submissionItemIndex = this.informationBoxItems.findIndex((item) => item.title === 'artemisApp.programmingExercise.submissionPolicy.submissionLimitTitle'); + if (submissionItemIndex !== -1) { + this.informationBoxItems.splice(submissionItemIndex, 1, this.getSubmissionPolicyItem()); } } ngOnChanges() { this.course = this.course ?? getCourseFromExercise(this.exercise); - this.updateSubmissionPolicyItem(); - + if (this.submissionPolicy?.active && this.submissionPolicy?.submissionLimit) { + this.updateSubmissionPolicyItem(); + } if (this.studentParticipation?.results?.length) { // The updated participation by the websocket is not guaranteed to be sorted, find the newest result (highest id) this.sortService.sortByProperty(this.studentParticipation.results, 'id', false); diff --git a/src/main/webapp/app/exercises/shared/result/result.component.html b/src/main/webapp/app/exercises/shared/result/result.component.html index e7027337e302..dd61f529bddf 100644 --- a/src/main/webapp/app/exercises/shared/result/result.component.html +++ b/src/main/webapp/app/exercises/shared/result/result.component.html @@ -60,7 +60,7 @@ } - @if (!isInSidebarCard) { + @if (!isInSidebarCard && showCompletion) { ({{ result!.completionDate | artemisTimeAgo }}) } @@ -98,19 +98,16 @@ @case (ResultTemplateStatus.MISSING) { - - @if (!isInSidebarCard) { - @switch (missingResultInfo) { - @case (MissingResultInfo.FAILED_PROGRAMMING_SUBMISSION_ONLINE_IDE) { - {{ - 'artemisApp.result.missing.programmingFailedSubmission.message' | artemisTranslate - }} - } - @case (MissingResultInfo.FAILED_PROGRAMMING_SUBMISSION_OFFLINE_IDE) { - {{ - 'artemisApp.result.missing.programmingFailedSubmission.message' | artemisTranslate - }} - } + @switch (missingResultInfo) { + @case (MissingResultInfo.FAILED_PROGRAMMING_SUBMISSION_ONLINE_IDE) { + {{ + 'artemisApp.result.missing.programmingFailedSubmission.message' | artemisTranslate + }} + } + @case (MissingResultInfo.FAILED_PROGRAMMING_SUBMISSION_OFFLINE_IDE) { + {{ + 'artemisApp.result.missing.programmingFailedSubmission.message' | artemisTranslate + }} } } @if (result && exercise?.type !== ExerciseType.QUIZ) { diff --git a/src/main/webapp/app/exercises/shared/result/result.component.ts b/src/main/webapp/app/exercises/shared/result/result.component.ts index 6d1c2d1e9cc2..87a444e579b2 100644 --- a/src/main/webapp/app/exercises/shared/result/result.component.ts +++ b/src/main/webapp/app/exercises/shared/result/result.component.ts @@ -61,6 +61,7 @@ export class ResultComponent implements OnInit, OnChanges, OnDestroy { @Input() showBadge = false; @Input() showIcon = true; @Input() isInSidebarCard = false; + @Input() showCompletion = true; @Input() missingResultInfo = MissingResultInformation.NONE; @Input() exercise?: Exercise; diff --git a/src/main/webapp/app/exercises/shared/result/updating-result.component.html b/src/main/webapp/app/exercises/shared/result/updating-result.component.html index a26e0b4a62bd..b6998df616a5 100644 --- a/src/main/webapp/app/exercises/shared/result/updating-result.component.html +++ b/src/main/webapp/app/exercises/shared/result/updating-result.component.html @@ -10,4 +10,5 @@ [showIcon]="showIcon" [missingResultInfo]="missingResultInfo" [isInSidebarCard]="isInSidebarCard" + [showCompletion]="showCompletion" /> 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 640b02f38bff..ac2f1cc9ae4d 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 @@ -34,6 +34,7 @@ export class UpdatingResultComponent implements OnChanges, OnDestroy { @Input() showBadge = false; @Input() showIcon = true; @Input() isInSidebarCard = false; + @Input() showCompletion = true; @Output() showResult = new EventEmitter(); /** * @property personalParticipation Whether the participation belongs to the user (by being a student) or not (by being an instructor) diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html index 1c9f4b0bbf56..28596e449e15 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html @@ -2,7 +2,7 @@
-
{{ exercise.title }}
+
{{ exercise.title }}
@if (exercise.isAtLeastTutor) { @@ -45,7 +45,7 @@
{{ exercise.title }}

-
+
{{ exercise.title }} />
-
+
diff --git a/src/main/webapp/app/overview/submission-result-status.component.html b/src/main/webapp/app/overview/submission-result-status.component.html index 118d8fd5150a..20d880b949c1 100644 --- a/src/main/webapp/app/overview/submission-result-status.component.html +++ b/src/main/webapp/app/overview/submission-result-status.component.html @@ -11,6 +11,7 @@ [showBadge]="showBadge" [showIcon]="showIcon" [isInSidebarCard]="isInSidebarCard" + [showCompletion]="showCompletion" [short]="short" [personalParticipation]="true" /> diff --git a/src/main/webapp/app/overview/submission-result-status.component.ts b/src/main/webapp/app/overview/submission-result-status.component.ts index 5fe0027a5adb..2677fcf51883 100644 --- a/src/main/webapp/app/overview/submission-result-status.component.ts +++ b/src/main/webapp/app/overview/submission-result-status.component.ts @@ -31,6 +31,7 @@ export class SubmissionResultStatusComponent implements OnChanges { @Input() showUngradedResults = false; @Input() showIcon = true; @Input() isInSidebarCard = false; + @Input() showCompletion = true; @Input() short = true; @Input() triggerLastGraded = true; diff --git a/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.spec.ts b/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.spec.ts deleted file mode 100644 index 5922de68ebf9..000000000000 --- a/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* tslint:disable:no-unused-variable */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DifficultyLevelComponent } from './difficulty-level.component'; - -describe('DifficultyLevelComponent', () => { - let component: DifficultyLevelComponent; - let fixture: ComponentFixture; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [DifficultyLevelComponent], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(DifficultyLevelComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/main/webapp/app/shared/information-box/information-box.component.html b/src/main/webapp/app/shared/information-box/information-box.component.html index d3297334f362..f2cf35f5195c 100644 --- a/src/main/webapp/app/shared/information-box/information-box.component.html +++ b/src/main/webapp/app/shared/information-box/information-box.component.html @@ -4,17 +4,12 @@ class="mw-content text-nowrap rounded-3 py-1 px-2 border border-1 bg-module mb-3 small fw-semibold" ngbTooltip="{{ informationBoxData.tooltip | artemisTranslate: informationBoxData.tooltipParams }}" > -
- - - - @if (informationBoxData.contentComponent) { +
+ @if (informationBoxData.isContentComponent) { } @else { -
- {{ informationBoxData.content }} +
+ {{ informationBoxData.content.value }}
}
diff --git a/src/main/webapp/app/shared/information-box/information-box.component.ts b/src/main/webapp/app/shared/information-box/information-box.component.ts index 1af8b1135dad..21b554456b49 100644 --- a/src/main/webapp/app/shared/information-box/information-box.component.ts +++ b/src/main/webapp/app/shared/information-box/information-box.component.ts @@ -1,27 +1,41 @@ import { Component, Input } from '@angular/core'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; -import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { DifficultyLevel, Exercise } from 'app/entities/exercise.model'; +import { DateType } from '../pipes/artemis-date.pipe'; +import { StudentExam } from 'app/entities/student-exam.model'; export interface InformationBox { title: string; - content: string | number; - contentComponent?: string; + content: InformationBoxContent; + isContentComponent?: boolean; tooltip?: string; tooltipParams?: Record; contentColor?: string; } -// export interface InformationBox { -// title: string; -// content: string | number | any; -// contentType?: string; -// isContentComponent?: boolean; -// contentComponent?: any; -// icon?: IconProp; -// tooltip?: string; -// contentColor?: string; -// tooltipParams?: any; -// } + +interface StudentExamContent { + type: 'workingTime'; + value: StudentExam; +} + +interface DateContent { + type: 'timeAgo' | 'dateTime'; + value: DateType; +} +interface DifficultyLevelContent { + type: 'difficultyLevel'; + value: DifficultyLevel; +} +interface ExerciseContent { + type: 'submissionStatus' | 'categories'; + value: Exercise; +} +interface StringContent { + type: 'string'; + value: string; +} +export type InformationBoxContent = StudentExamContent | DateContent | ExerciseContent | DifficultyLevelContent | StringContent; @Component({ standalone: true, diff --git a/src/main/webapp/app/utils/date.utils.ts b/src/main/webapp/app/utils/date.utils.ts index 37ec993c27aa..65443b5b161e 100644 --- a/src/main/webapp/app/utils/date.utils.ts +++ b/src/main/webapp/app/utils/date.utils.ts @@ -74,3 +74,7 @@ export function dayOfWeekZeroSundayToZeroMonday(dayOfWeekZeroSunday: number): nu } return (dayOfWeekZeroSunday + 6) % 7; } + +export function isDateLessThanAWeekAway(date: dayjs.Dayjs): boolean { + return date.isBetween(dayjs().add(1, 'week'), dayjs()); +} diff --git a/src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts b/src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts index 45b7f357a2c0..5b8579e11782 100644 --- a/src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts +++ b/src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts @@ -1,26 +1,92 @@ -/* tslint:disable:no-unused-variable */ -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; import { ExerciseHeadersInformationComponent } from 'app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component'; +import { MockProvider } from 'ng-mocks'; +import { InformationBoxComponent } from 'app/shared/information-box/information-box.component'; +import { ArtemisTestModule } from '../../../../test.module'; +import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; +import { DifficultyLevel, Exercise, ExerciseType } from 'app/entities/exercise.model'; +import { of } from 'rxjs'; +import dayjs from 'dayjs/esm'; +import { ArtemisDatePipe } from '../../../../../../../main/webapp/app/shared/pipes/artemis-date.pipe'; -// TODO describe('ExerciseHeadersInformationComponent', () => { let component: ExerciseHeadersInformationComponent; let fixture: ComponentFixture; + // let exerciseService: MockExerciseService; + let exerciseService: ExerciseService; + let getExerciseDetailsMock: jest.SpyInstance; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ExerciseHeadersInformationComponent], - }).compileComponents(); - })); + const exercise = { id: 42, type: ExerciseType.TEXT, studentParticipations: [], course: {}, dueDate: dayjs().subtract(1, 'weeks') } as unknown as Exercise; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ExerciseHeadersInformationComponent, ArtemisTestModule, TranslateModule.forRoot(), NgbTooltipModule, InformationBoxComponent], + providers: [MockProvider(ExerciseService), MockProvider(ArtemisDatePipe)], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(ExerciseHeadersInformationComponent); + component = fixture.componentInstance; + // mock exerciseService + exerciseService = fixture.debugElement.injector.get(ExerciseService); + getExerciseDetailsMock = jest.spyOn(exerciseService, 'getExerciseDetails'); + getExerciseDetailsMock.mockReturnValue(of({ body: { exercise: exercise } })); + }); + }); beforeEach(() => { fixture = TestBed.createComponent(ExerciseHeadersInformationComponent); component = fixture.componentInstance; + component.exercise = { ...exercise }; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should display the information box items', () => { + component.informationBoxItems = [ + { + title: 'Test Title 1', + tooltip: 'Test Tooltip 1', + tooltipParams: {}, + isContentComponent: false, + content: { type: 'string', value: 'Test Content' }, + contentColor: 'primary', + }, + { + title: 'Test Title 2', + tooltip: 'Test Tooltip 2', + tooltipParams: {}, + isContentComponent: false, + content: { type: 'string', value: 'Test Content' }, + }, + ]; + fixture.detectChanges(); + + const compiled = fixture.nativeElement as HTMLElement; + const informationBoxes = compiled.querySelectorAll('jhi-information-box'); + expect(informationBoxes).toHaveLength(2); + }); + + it('should display difficulty level component when content type is difficultyLevel', () => { + component.informationBoxItems = [ + { + title: 'Difficulty Level', + tooltip: 'Difficulty Tooltip', + tooltipParams: {}, + isContentComponent: true, + content: { type: 'difficultyLevel', value: DifficultyLevel.EASY }, + }, + ]; + fixture.detectChanges(); + + const compiled = fixture.nativeElement as HTMLElement; + const difficultyLevelComponent = compiled.querySelector('jhi-difficulty-level'); + expect(difficultyLevelComponent).toBeTruthy(); + }); }); diff --git a/src/test/javascript/spec/component/overview/exercise-details/course-exercise-details.component.spec.ts b/src/test/javascript/spec/component/overview/exercise-details/course-exercise-details.component.spec.ts index efb66d508837..4f2a2c9b8331 100644 --- a/src/test/javascript/spec/component/overview/exercise-details/course-exercise-details.component.spec.ts +++ b/src/test/javascript/spec/component/overview/exercise-details/course-exercise-details.component.spec.ts @@ -64,6 +64,7 @@ import { ProgrammingExerciseExampleSolutionRepoDownloadComponent } from 'app/exe import { ResetRepoButtonComponent } from 'app/shared/components/reset-repo-button/reset-repo-button.component'; import { ProblemStatementComponent } from 'app/overview/exercise-details/problem-statement/problem-statement.component'; import { ExerciseInfoComponent } from 'app/exercises/shared/exercise-info/exercise-info.component'; +import { ExerciseHeadersInformationComponent } from 'app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component'; import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.service'; import { IrisSettings } from 'app/entities/iris/settings/iris-settings.model'; import { ScienceService } from 'app/shared/science/science.service'; @@ -164,6 +165,7 @@ describe('CourseExerciseDetailsComponent', () => { MockComponent(LtiInitializerComponent), MockComponent(ModelingEditorComponent), MockComponent(ExerciseInfoComponent), + MockComponent(ExerciseHeadersInformationComponent), ], providers: [ provideHttpClient(), diff --git a/src/test/javascript/spec/component/shared/difficulty-level.component.spec.ts b/src/test/javascript/spec/component/shared/difficulty-level.component.spec.ts new file mode 100644 index 000000000000..a907c0a03316 --- /dev/null +++ b/src/test/javascript/spec/component/shared/difficulty-level.component.spec.ts @@ -0,0 +1,44 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; + +import { DifficultyLevelComponent } from 'app/shared/difficulty-level/difficulty-level.component'; +import { ArtemisTestModule } from '../../test.module'; + +describe('DifficultyLevelComponent', () => { + let component: DifficultyLevelComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DifficultyLevelComponent], + imports: [NgbTooltipModule, ArtemisTestModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DifficultyLevelComponent); + component = fixture.componentInstance; + component.coloredDifficultyLevel = { + label: 'Easy', + color: ['success', 'body', 'body'], + }; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display the correct tooltip label', () => { + const tooltipElement: HTMLElement = fixture.nativeElement.querySelector('[ngbTooltip]'); + expect(tooltipElement.getAttribute('ngbTooltip')).toBe('Easy'); + }); + + it('should apply the correct classes for difficulty colors', () => { + const skillBars: NodeListOf = fixture.nativeElement.querySelectorAll('.skill-bar'); + expect(skillBars).toHaveLength(3); + expect(skillBars[0].classList).toContain('bg-success'); + expect(skillBars[1].classList).toContain('bg-body'); + expect(skillBars[2].classList).toContain('bg-body'); + }); +}); diff --git a/src/test/javascript/spec/component/shared/information-box.component.spec.ts b/src/test/javascript/spec/component/shared/information-box.component.spec.ts index 61ede367a3cf..970a7f04c90f 100644 --- a/src/test/javascript/spec/component/shared/information-box.component.spec.ts +++ b/src/test/javascript/spec/component/shared/information-box.component.spec.ts @@ -1,17 +1,19 @@ -/* tslint:disable:no-unused-variable */ -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { InformationBoxComponent } from '../../../../../main/webapp/app/shared/information-box/information-box.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { ArtemisTestModule } from '../../test.module'; describe('InformationBoxComponent', () => { let component: InformationBoxComponent; let fixture: ComponentFixture; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [InformationBoxComponent], + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InformationBoxComponent, ArtemisTestModule, TranslateModule.forRoot(), NgbTooltipModule], }).compileComponents(); - })); + }); beforeEach(() => { fixture = TestBed.createComponent(InformationBoxComponent); @@ -22,4 +24,36 @@ describe('InformationBoxComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should display the title', () => { + component.informationBoxData = { + title: 'Test Title', + tooltip: 'Test Tooltip', + tooltipParams: {}, + isContentComponent: false, + content: { type: 'string', value: 'Test Content' }, + contentColor: 'primary', + }; + fixture.detectChanges(); + + const compiled = fixture.nativeElement as HTMLElement; + const titleElement = compiled.querySelector('#test-title'); + expect(titleElement?.textContent).toContain('Test Title'); + }); + + it('should display the content', () => { + component.informationBoxData = { + title: 'Test Title', + tooltip: 'Test Tooltip', + tooltipParams: {}, + isContentComponent: false, + content: { type: 'string', value: 'Test Content' }, + contentColor: 'primary', + }; + fixture.detectChanges(); + + const compiled = fixture.nativeElement as HTMLElement; + const contentElement = compiled.querySelector('#test-text'); + expect(contentElement?.textContent).toContain('Test Content'); + }); }); From 17eb4d409b99b3160dd75a2b12fa74b56668a8ae Mon Sep 17 00:00:00 2001 From: Ramona Beinstingel Date: Fri, 19 Jul 2024 13:51:33 +0200 Subject: [PATCH 09/23] Clean Up --- ...xercise-headers-information.component.html | 1 - .../exercise-headers-information.component.ts | 3 +- .../course-exercise-details.component.html | 4 +- .../course-exercise-details.component.ts | 81 ++++++++----------- .../difficulty-level.component.ts | 4 + src/main/webapp/app/shared/shared.module.ts | 3 - ...cise-headers-information.component.spec.ts | 1 - 7 files changed, 42 insertions(+), 55 deletions(-) diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html index 64ebaec50e0b..32e2402927dc 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html +++ b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html @@ -1,6 +1,5 @@ @if (exercise) {
- @for (informationBoxItem of informationBoxItems; track informationBoxItem) { @if (informationBoxItem.content.type === 'difficultyLevel') { diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts index bf6c8d6011fb..cc3f02224345 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts @@ -17,12 +17,13 @@ import { ExerciseCategoriesModule } from 'app/shared/exercise-categories/exercis import { InformationBox, InformationBoxComponent } from 'app/shared/information-box/information-box.component'; import { ComplaintService } from 'app/complaints/complaint.service'; import { isDateLessThanAWeekAway } from 'app/utils/date.utils'; +import { DifficultyLevelComponent } from 'app/shared/difficulty-level/difficulty-level.component'; @Component({ selector: 'jhi-exercise-headers-information', templateUrl: './exercise-headers-information.component.html', standalone: true, - imports: [ArtemisSharedModule, ArtemisSharedComponentModule, SubmissionResultStatusModule, ExerciseCategoriesModule, InformationBoxComponent], + imports: [ArtemisSharedModule, ArtemisSharedComponentModule, SubmissionResultStatusModule, ExerciseCategoriesModule, InformationBoxComponent, DifficultyLevelComponent], styleUrls: ['./exercise-headers-information.component.scss'], /* Our tsconfig file has `preserveWhitespaces: 'true'` which causes whitespace to affect content projection. We need to set it to 'false 'for this component, otherwise the components with the selector [contentComponent] diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html index 28596e449e15..747dd937c25d 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html @@ -2,7 +2,7 @@
-
{{ exercise.title }}
+
{{ exercise.title }}
@if (exercise.isAtLeastTutor) { @@ -160,8 +160,6 @@

} } - - @if ( exercise && diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts index 8e6022407d6d..c2209f47c840 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts @@ -11,7 +11,7 @@ import { GuidedTourService } from 'app/guided-tour/guided-tour.service'; import { programmingExerciseFail, programmingExerciseSuccess } from 'app/guided-tour/tours/course-exercise-detail-tour'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { Participation } from 'app/entities/participation/participation.model'; -import { Exercise, ExerciseType, getIcon, getIconTooltip } from 'app/entities/exercise.model'; +import { Exercise, ExerciseType, getIcon } from 'app/entities/exercise.model'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; import { ExampleSolutionInfo, ExerciseDetailsType, ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { AssessmentType } from 'app/entities/assessment-type.model'; @@ -108,7 +108,6 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp isTestServer = false; isGeneratingFeedback: boolean = false; instructorActionItems: InstructorActionItem[] = []; - iconTooltip: string; exerciseIcon: IconProp; exampleSolutionInfo?: ExampleSolutionInfo; @@ -220,7 +219,6 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp this.baseResource = `/course-management/${this.courseId}/${this.exercise.type}-exercises/${this.exercise.id}/`; if (this.exercise?.type) { - this.iconTooltip = getIconTooltip(this.exercise?.type); this.exerciseIcon = getIcon(this.exercise?.type); } this.createInstructorActions(); @@ -451,55 +449,50 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp this.instructorActionItems = this.createTutorActions(); } if (this.exercise?.isAtLeastEditor) { - const editorItems = this.createEditorActions(); - editorItems.forEach((editorItem) => this.instructorActionItems.push(editorItem)); + this.instructorActionItems.push(...this.createEditorActions()); } if (this.exercise?.isAtLeastInstructor && this.QUIZ_ENDED_STATUS.includes(this.quizExerciseStatus)) { - const reEvaluateItem: InstructorActionItem = this.getReEvaluateItem(); - this.instructorActionItems.push(reEvaluateItem); + this.instructorActionItems.push(this.getReEvaluateItem()); } } createTutorActions(): InstructorActionItem[] { - const instructorActionItems = this.getDefaultItems(); + const tutorActionItems = [...this.getDefaultItems()]; if (this.exercise?.type === ExerciseType.QUIZ) { - const quizItems: InstructorActionItem[] = this.getQuizItems(); - quizItems.forEach((quizItem) => instructorActionItems.push(quizItem)); + tutorActionItems.push(...this.getQuizItems()); } else { - const participationsItem: InstructorActionItem = this.getParticipationItem(); - instructorActionItems.push(participationsItem); + tutorActionItems.push(this.getParticipationItem()); } - return instructorActionItems; + return tutorActionItems; } getDefaultItems(): InstructorActionItem[] { - const exercisesItem: InstructorActionItem = { - routerLink: `${this.baseResource}`, - icon: faEye, - translation: 'entity.action.view', - }; - - const statisticsItem: InstructorActionItem = { - routerLink: `${this.baseResource}scores`, - icon: faTable, - translation: 'entity.action.scores', - }; - - return [exercisesItem, statisticsItem]; + return [ + { + routerLink: `${this.baseResource}`, + icon: faEye, + translation: 'entity.action.view', + }, + { + routerLink: `${this.baseResource}scores`, + icon: faTable, + translation: 'entity.action.scores', + }, + ]; } getQuizItems(): InstructorActionItem[] { - const previewItem: InstructorActionItem = { - routerLink: `${this.baseResource}preview`, - icon: faEye, - translation: 'artemisApp.quizExercise.preview', - }; - const solutionItem: InstructorActionItem = { - routerLink: `${this.baseResource}solution`, - icon: faEye, - translation: 'artemisApp.quizExercise.solution', - }; - - return [previewItem, solutionItem]; + return [ + { + routerLink: `${this.baseResource}preview`, + icon: faEye, + translation: 'artemisApp.quizExercise.preview', + }, + { + routerLink: `${this.baseResource}solution`, + icon: faEye, + translation: 'artemisApp.quizExercise.solution', + }, + ]; } getParticipationItem(): InstructorActionItem { @@ -513,20 +506,16 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp createEditorActions(): InstructorActionItem[] { const editorItems: InstructorActionItem[] = []; if (this.exercise?.type === ExerciseType.QUIZ) { - const statisticItem: InstructorActionItem = this.getStatisticItem('quiz-point-statistic'); - editorItems.push(statisticItem); + editorItems.push(this.getStatisticItem('quiz-point-statistic')); } if (this.exercise?.type === ExerciseType.MODELING) { - const statisticItem: InstructorActionItem = this.getStatisticItem('exercise-statistics'); - editorItems.push(statisticItem); + editorItems.push(this.getStatisticItem('exercise-statistics')); } if (this.exercise?.type === ExerciseType.PROGRAMMING) { - const gradingItem: InstructorActionItem = this.getGradingItem(); - editorItems.push(gradingItem); + editorItems.push(this.getGradingItem()); } if (this.QUIZ_EDITABLE_STATUS.includes(this.quizExerciseStatus)) { - const quizEditItem: InstructorActionItem = this.getQuizEditItem(); - editorItems.push(quizEditItem); + editorItems.push(this.getQuizEditItem()); } return editorItems; } diff --git a/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.ts b/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.ts index 8c175a538e42..69af618315e3 100644 --- a/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.ts +++ b/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.ts @@ -2,6 +2,8 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { DifficultyLevel } from 'app/entities/exercise.model'; import { Subscription } from 'rxjs'; +import { ArtemisSharedModule } from '../shared.module'; +import { ArtemisSharedComponentModule } from '../components/shared-component.module'; export interface ColoredDifficultyLevel { label: string; @@ -11,6 +13,8 @@ export interface ColoredDifficultyLevel { selector: 'jhi-difficulty-level', templateUrl: './difficulty-level.component.html', styleUrls: ['./difficulty-level.component.scss'], + standalone: true, + imports: [ArtemisSharedModule, ArtemisSharedComponentModule], }) export class DifficultyLevelComponent implements OnInit, OnDestroy { private translateSubscription: Subscription; diff --git a/src/main/webapp/app/shared/shared.module.ts b/src/main/webapp/app/shared/shared.module.ts index 3cca168d2b56..ed2bf4cc9dfd 100644 --- a/src/main/webapp/app/shared/shared.module.ts +++ b/src/main/webapp/app/shared/shared.module.ts @@ -28,7 +28,6 @@ import { ConfirmEntityNameComponent } from 'app/shared/confirm-entity-name/confi import { DetailOverviewNavigationBarComponent } from 'app/shared/detail-overview-navigation-bar/detail-overview-navigation-bar.component'; import { ScienceDirective } from 'app/shared/science/science.directive'; import { SearchFilterComponent } from './search-filter/search-filter.component'; -import { DifficultyLevelComponent } from './difficulty-level/difficulty-level.component'; @NgModule({ imports: [ArtemisSharedLibsModule, ArtemisSharedCommonModule, ArtemisSharedPipesModule, RouterModule], @@ -58,7 +57,6 @@ import { DifficultyLevelComponent } from './difficulty-level/difficulty-level.co StickyPopoverDirective, ScienceDirective, SearchFilterComponent, - DifficultyLevelComponent, ], exports: [ ArtemisSharedLibsModule, @@ -90,7 +88,6 @@ import { DifficultyLevelComponent } from './difficulty-level/difficulty-level.co StickyPopoverDirective, ScienceDirective, SearchFilterComponent, - DifficultyLevelComponent, ], }) export class ArtemisSharedModule {} diff --git a/src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts b/src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts index 5b8579e11782..7c763c94c3a3 100644 --- a/src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts +++ b/src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts @@ -15,7 +15,6 @@ import { ArtemisDatePipe } from '../../../../../../../main/webapp/app/shared/pip describe('ExerciseHeadersInformationComponent', () => { let component: ExerciseHeadersInformationComponent; let fixture: ComponentFixture; - // let exerciseService: MockExerciseService; let exerciseService: ExerciseService; let getExerciseDetailsMock: jest.SpyInstance; From da91995aba27993cb701b1742c37cbbe610e2818 Mon Sep 17 00:00:00 2001 From: Ramona Beinstingel Date: Fri, 19 Jul 2024 14:28:26 +0200 Subject: [PATCH 10/23] update difficulty-level spec --- .../difficulty-level.component.html | 2 +- .../shared/difficulty-level.component.spec.ts | 66 ++++++++++++++----- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.html b/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.html index ca7b406627c3..9f61483533b1 100644 --- a/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.html +++ b/src/main/webapp/app/shared/difficulty-level/difficulty-level.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/test/javascript/spec/component/shared/difficulty-level.component.spec.ts b/src/test/javascript/spec/component/shared/difficulty-level.component.spec.ts index a907c0a03316..cc53280d6721 100644 --- a/src/test/javascript/spec/component/shared/difficulty-level.component.spec.ts +++ b/src/test/javascript/spec/component/shared/difficulty-level.component.spec.ts @@ -1,6 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; - import { DifficultyLevelComponent } from 'app/shared/difficulty-level/difficulty-level.component'; import { ArtemisTestModule } from '../../test.module'; @@ -8,33 +7,70 @@ describe('DifficultyLevelComponent', () => { let component: DifficultyLevelComponent; let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [DifficultyLevelComponent], - imports: [NgbTooltipModule, ArtemisTestModule], - }).compileComponents(); - }); - beforeEach(() => { + TestBed.configureTestingModule({ + imports: [DifficultyLevelComponent, NgbTooltipModule, ArtemisTestModule], + }).compileComponents(); fixture = TestBed.createComponent(DifficultyLevelComponent); component = fixture.componentInstance; - component.coloredDifficultyLevel = { - label: 'Easy', - color: ['success', 'body', 'body'], - }; - fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + it('should set coloredDifficultyLevel correctly for EASY', () => { + component.difficultyLevel = 'EASY'; + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.coloredDifficultyLevel.label).toBe('artemisApp.exercise.easy'); + expect(component.coloredDifficultyLevel.color).toEqual(['success', 'body', 'body']); + }); + + it('should set coloredDifficultyLevel correctly for MEDIUM', () => { + component.difficultyLevel = 'MEDIUM'; + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.coloredDifficultyLevel.label).toBe('artemisApp.exercise.medium'); + expect(component.coloredDifficultyLevel.color).toEqual(['warning', 'warning', 'body']); + }); + + it('should set coloredDifficultyLevel correctly for HARD', () => { + component.difficultyLevel = 'HARD'; + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.coloredDifficultyLevel.label).toBe('artemisApp.exercise.hard'); + expect(component.coloredDifficultyLevel.color).toEqual(['danger', 'danger', 'danger']); + }); + + it('should set coloredDifficultyLevel correctly for no level', () => { + component.difficultyLevel = 'UNKNOWN'; + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.coloredDifficultyLevel.label).toBe('artemisApp.exercise.noLevel'); + expect(component.coloredDifficultyLevel.color).toEqual(['body', 'body', 'body']); + }); it('should display the correct tooltip label', () => { - const tooltipElement: HTMLElement = fixture.nativeElement.querySelector('[ngbTooltip]'); - expect(tooltipElement.getAttribute('ngbTooltip')).toBe('Easy'); + component.difficultyLevel = 'EASY'; + component.ngOnInit(); + fixture.detectChanges(); + + const tooltipElement: HTMLElement = fixture.nativeElement.querySelector('#difficulty-level'); + tooltipElement.dispatchEvent(new Event('mouseenter')); + fixture.detectChanges(); + + const tooltipContent = document.querySelector('.tooltip-inner'); + expect(tooltipContent?.textContent).toBe('artemisApp.exercise.easy'); }); it('should apply the correct classes for difficulty colors', () => { + component.difficultyLevel = 'EASY'; + component.ngOnInit(); + fixture.detectChanges(); const skillBars: NodeListOf = fixture.nativeElement.querySelectorAll('.skill-bar'); expect(skillBars).toHaveLength(3); expect(skillBars[0].classList).toContain('bg-success'); From d6b7604319f198af2e2e57471d40b931a3484d9e Mon Sep 17 00:00:00 2001 From: Ramona Beinstingel Date: Fri, 19 Jul 2024 14:33:16 +0200 Subject: [PATCH 11/23] remove unused variable --- src/main/webapp/app/overview/course-overview.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/webapp/app/overview/course-overview.component.ts b/src/main/webapp/app/overview/course-overview.component.ts index f2b4a67a6a0e..23ac1ce08689 100644 --- a/src/main/webapp/app/overview/course-overview.component.ts +++ b/src/main/webapp/app/overview/course-overview.component.ts @@ -31,7 +31,6 @@ import { faFlag, faGraduationCap, faListAlt, - faListCheck, faNetworkWired, faPersonChalkboard, faQuestion, From e9a06df0d4869e15c4b5ab15da4c448719cb0204 Mon Sep 17 00:00:00 2001 From: Ramona Beinstingel Date: Thu, 25 Jul 2024 10:34:09 +0200 Subject: [PATCH 12/23] Feedback PR --- .../exam-start-information.component.ts | 4 +- ...xercise-headers-information.component.html | 40 +++++++++++++++++-- .../exercise-headers-information.component.ts | 1 + .../course-exercise-details.component.ts | 5 ++- .../information-box.component.ts | 4 ++ ...cise-headers-information.component.spec.ts | 9 +---- .../shared/information-box.component.spec.ts | 3 -- .../exercises/ExerciseResultPage.ts | 1 + .../ProgrammingExerciseOverviewPage.ts | 1 + 9 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts b/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts index 01e22802405e..9ee44430c900 100644 --- a/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts +++ b/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts @@ -105,7 +105,7 @@ export class ExamStartInformationComponent implements OnInit { this.examInformationBoxData.push(informationBoxTotalWorkingTime); const boxContentTotalPoints: InformationBoxContent = { type: 'dateTime', - value: this.totalPoints!.toString(), + value: this.totalPoints?.toString(), }; const informationBoxTotalPoints = this.buildInformationBox('artemisApp.exam.points', boxContentTotalPoints); @@ -114,7 +114,7 @@ export class ExamStartInformationComponent implements OnInit { if (this.numberOfExercisesInExam) { const boxContent: InformationBoxContent = { type: 'string', - value: this.numberOfExercisesInExam!.toString(), + value: this.numberOfExercisesInExam?.toString(), }; const informationBoxNumberOfExercises = this.buildInformationBox('artemisApp.exam.exercises', boxContent); this.examInformationBoxData.push(informationBoxNumberOfExercises); diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html index 32e2402927dc..56916946fc6a 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html +++ b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html @@ -2,7 +2,39 @@
@for (informationBoxItem of informationBoxItems; track informationBoxItem) { - @if (informationBoxItem.content.type === 'difficultyLevel') { + @switch (informationBoxItem.content.type) { + @case ('difficultyLevel') { + + } + @case ('categories') { + + } + @case ('timeAgo') { + {{ informationBoxItem.content.value | artemisTimeAgo }} + } + @case ('dateTime') { + {{ informationBoxItem.content.value | artemisDate }} + } + @case ('submissionStatus') { + + } + } + + }
diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts index cc3f02224345..96f33950eaae 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts @@ -235,6 +235,7 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { return submissionsLeft <= 0 ? 'danger' : 'warning'; } } + getPointsItem(points: number | undefined, title: string): InformationBox { return { title: 'artemisApp.courseOverview.exerciseDetails.' + title, diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts index c2209f47c840..eab7db624454 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts @@ -455,6 +455,7 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp this.instructorActionItems.push(this.getReEvaluateItem()); } } + createTutorActions(): InstructorActionItem[] { const tutorActionItems = [...this.getDefaultItems()]; if (this.exercise?.type === ExerciseType.QUIZ) { @@ -557,7 +558,9 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp this.participationUpdateListener.unsubscribe(); if (this.studentParticipations) { this.studentParticipations.forEach((participation) => { - this.participationWebsocketService.unsubscribeForLatestResultOfParticipation(participation.id!, this.exercise!); + if (participation.id && this.exercise) { + this.participationWebsocketService.unsubscribeForLatestResultOfParticipation(participation.id, this.exercise); + } }); } } diff --git a/src/main/webapp/app/shared/information-box/information-box.component.ts b/src/main/webapp/app/shared/information-box/information-box.component.ts index 21b554456b49..ec79beb1fd0e 100644 --- a/src/main/webapp/app/shared/information-box/information-box.component.ts +++ b/src/main/webapp/app/shared/information-box/information-box.component.ts @@ -23,18 +23,22 @@ interface DateContent { type: 'timeAgo' | 'dateTime'; value: DateType; } + interface DifficultyLevelContent { type: 'difficultyLevel'; value: DifficultyLevel; } + interface ExerciseContent { type: 'submissionStatus' | 'categories'; value: Exercise; } + interface StringContent { type: 'string'; value: string; } + export type InformationBoxContent = StudentExamContent | DateContent | ExerciseContent | DifficultyLevelContent | StringContent; @Component({ diff --git a/src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts b/src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts index 7c763c94c3a3..50f2f2a315c8 100644 --- a/src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts +++ b/src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts @@ -33,16 +33,11 @@ describe('ExerciseHeadersInformationComponent', () => { exerciseService = fixture.debugElement.injector.get(ExerciseService); getExerciseDetailsMock = jest.spyOn(exerciseService, 'getExerciseDetails'); getExerciseDetailsMock.mockReturnValue(of({ body: { exercise: exercise } })); + component.exercise = { ...exercise }; + fixture.detectChanges(); }); }); - beforeEach(() => { - fixture = TestBed.createComponent(ExerciseHeadersInformationComponent); - component = fixture.componentInstance; - component.exercise = { ...exercise }; - fixture.detectChanges(); - }); - it('should create', () => { expect(component).toBeTruthy(); }); diff --git a/src/test/javascript/spec/component/shared/information-box.component.spec.ts b/src/test/javascript/spec/component/shared/information-box.component.spec.ts index 970a7f04c90f..2a69d6587e8f 100644 --- a/src/test/javascript/spec/component/shared/information-box.component.spec.ts +++ b/src/test/javascript/spec/component/shared/information-box.component.spec.ts @@ -13,9 +13,6 @@ describe('InformationBoxComponent', () => { await TestBed.configureTestingModule({ imports: [InformationBoxComponent, ArtemisTestModule, TranslateModule.forRoot(), NgbTooltipModule], }).compileComponents(); - }); - - beforeEach(() => { fixture = TestBed.createComponent(InformationBoxComponent); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/src/test/playwright/support/pageobjects/exercises/ExerciseResultPage.ts b/src/test/playwright/support/pageobjects/exercises/ExerciseResultPage.ts index e1796d3dbbd7..745a08f0f473 100644 --- a/src/test/playwright/support/pageobjects/exercises/ExerciseResultPage.ts +++ b/src/test/playwright/support/pageobjects/exercises/ExerciseResultPage.ts @@ -27,6 +27,7 @@ export class ExerciseResultPage { async shouldShowScore(percentage: number) { await Commands.reloadUntilFound(this.page, this.page.locator('jhi-course-exercise-details #submission-result-graded'), 4000, 60000); // TODO this is not the correct selector + await expect(this.page.locator('.tab-bar-exercise-details').getByText(`${percentage}%`)).toBeVisible(); } diff --git a/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts b/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts index 0f4dd8a55649..db77afa04097 100644 --- a/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts +++ b/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts @@ -17,6 +17,7 @@ export class ProgrammingExerciseOverviewPage { async getResultScore() { // TODO this is not the correct selector + const resultScore = this.page.locator('.tab-bar-exercise-details').locator('#result-score'); await resultScore.waitFor({ state: 'visible' }); return resultScore; From dd674249ca2e47ed08d1cbd766c2320c7a44dcbc Mon Sep 17 00:00:00 2001 From: Ramona Beinstingel Date: Fri, 26 Jul 2024 16:15:19 +0200 Subject: [PATCH 13/23] PR Feedback - e2e test adaption missing --- ...xercise-headers-information.component.html | 33 +---------------- .../exercise-headers-information.component.ts | 36 +++++++++---------- .../course-exercise-details.component.html | 2 +- .../exercise-categories.component.html | 8 +++-- .../content/scss/themes/theme-default.scss | 2 +- .../shared/difficulty-level.component.spec.ts | 6 ---- .../exercises/ExerciseResultPage.ts | 4 +-- .../ProgrammingExerciseOverviewPage.ts | 7 ++-- 8 files changed, 29 insertions(+), 69 deletions(-) diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html index 56916946fc6a..fa98ad524bc3 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html +++ b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html @@ -1,5 +1,5 @@ @if (exercise) { -
+
@for (informationBoxItem of informationBoxItems; track informationBoxItem) { @switch (informationBoxItem.content.type) { @@ -33,37 +33,6 @@ /> } } - - }
diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts index 96f33950eaae..6c77b2bc0b68 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts @@ -72,6 +72,23 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { this.addCategoryItems(); } + ngOnChanges() { + this.course = this.course ?? getCourseFromExercise(this.exercise); + + if (this.submissionPolicy?.active && this.submissionPolicy?.submissionLimit) { + this.updateSubmissionPolicyItem(); + } + if (this.studentParticipation?.results?.length) { + // The updated participation by the websocket is not guaranteed to be sorted, find the newest result (highest id) + this.sortService.sortByProperty(this.studentParticipation.results, 'id', false); + + const latestRatedResult = this.studentParticipation.results.filter((result) => result.rated).first(); + if (latestRatedResult) { + this.achievedPoints = roundValueSpecifiedByCourseSettings((latestRatedResult.score! * this.exercise.maxPoints!) / 100, this.course); + } + } + } + addPointsItems() { const { maxPoints, bonusPoints } = this.exercise; if (maxPoints) { @@ -228,7 +245,7 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { }; } - getSubmissionColor() { + getSubmissionColor(): string { const submissionsLeft = this.submissionPolicy?.submissionLimit ? this.submissionPolicy?.submissionLimit - this.numberOfSubmissions : 2; if (submissionsLeft > 1) return 'body-color'; else { @@ -256,23 +273,6 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { } } - ngOnChanges() { - this.course = this.course ?? getCourseFromExercise(this.exercise); - - if (this.submissionPolicy?.active && this.submissionPolicy?.submissionLimit) { - this.updateSubmissionPolicyItem(); - } - if (this.studentParticipation?.results?.length) { - // The updated participation by the websocket is not guaranteed to be sorted, find the newest result (highest id) - this.sortService.sortByProperty(this.studentParticipation.results, 'id', false); - - const latestRatedResult = this.studentParticipation.results.filter((result) => result.rated).first(); - if (latestRatedResult) { - this.achievedPoints = roundValueSpecifiedByCourseSettings((latestRatedResult.score! * this.exercise.maxPoints!) / 100, this.course); - } - } - } - private countSubmissions() { const commitHashSet = new Set(); diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html index 747dd937c25d..425edf13f3c3 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html @@ -2,7 +2,7 @@
-
{{ exercise.title }}
+
{{ exercise.title }}
@if (exercise.isAtLeastTutor) { diff --git a/src/main/webapp/app/shared/exercise-categories/exercise-categories.component.html b/src/main/webapp/app/shared/exercise-categories/exercise-categories.component.html index 49b8cfe9756b..c073e9ebf5b9 100644 --- a/src/main/webapp/app/shared/exercise-categories/exercise-categories.component.html +++ b/src/main/webapp/app/shared/exercise-categories/exercise-categories.component.html @@ -11,9 +11,11 @@ } @for (category of exercise.categories; track category) { - - {{ category.category }} - + @if (category.category) { + + {{ category.category | truncate:30 }} + + } } @if (exercise.difficulty && showTags.difficulty) { diff --git a/src/main/webapp/content/scss/themes/theme-default.scss b/src/main/webapp/content/scss/themes/theme-default.scss index 19e5381fbeac..dba63eaa7c91 100644 --- a/src/main/webapp/content/scss/themes/theme-default.scss +++ b/src/main/webapp/content/scss/themes/theme-default.scss @@ -49,5 +49,5 @@ html { } .btn-outline-primary:not(.list-group-item):hover { - color: #fff; + color: $white; } diff --git a/src/test/javascript/spec/component/shared/difficulty-level.component.spec.ts b/src/test/javascript/spec/component/shared/difficulty-level.component.spec.ts index cc53280d6721..7fbad5566f3e 100644 --- a/src/test/javascript/spec/component/shared/difficulty-level.component.spec.ts +++ b/src/test/javascript/spec/component/shared/difficulty-level.component.spec.ts @@ -21,7 +21,6 @@ describe('DifficultyLevelComponent', () => { it('should set coloredDifficultyLevel correctly for EASY', () => { component.difficultyLevel = 'EASY'; - component.ngOnInit(); fixture.detectChanges(); expect(component.coloredDifficultyLevel.label).toBe('artemisApp.exercise.easy'); @@ -30,7 +29,6 @@ describe('DifficultyLevelComponent', () => { it('should set coloredDifficultyLevel correctly for MEDIUM', () => { component.difficultyLevel = 'MEDIUM'; - component.ngOnInit(); fixture.detectChanges(); expect(component.coloredDifficultyLevel.label).toBe('artemisApp.exercise.medium'); @@ -39,7 +37,6 @@ describe('DifficultyLevelComponent', () => { it('should set coloredDifficultyLevel correctly for HARD', () => { component.difficultyLevel = 'HARD'; - component.ngOnInit(); fixture.detectChanges(); expect(component.coloredDifficultyLevel.label).toBe('artemisApp.exercise.hard'); @@ -48,7 +45,6 @@ describe('DifficultyLevelComponent', () => { it('should set coloredDifficultyLevel correctly for no level', () => { component.difficultyLevel = 'UNKNOWN'; - component.ngOnInit(); fixture.detectChanges(); expect(component.coloredDifficultyLevel.label).toBe('artemisApp.exercise.noLevel'); @@ -56,7 +52,6 @@ describe('DifficultyLevelComponent', () => { }); it('should display the correct tooltip label', () => { component.difficultyLevel = 'EASY'; - component.ngOnInit(); fixture.detectChanges(); const tooltipElement: HTMLElement = fixture.nativeElement.querySelector('#difficulty-level'); @@ -69,7 +64,6 @@ describe('DifficultyLevelComponent', () => { it('should apply the correct classes for difficulty colors', () => { component.difficultyLevel = 'EASY'; - component.ngOnInit(); fixture.detectChanges(); const skillBars: NodeListOf = fixture.nativeElement.querySelectorAll('.skill-bar'); expect(skillBars).toHaveLength(3); diff --git a/src/test/playwright/support/pageobjects/exercises/ExerciseResultPage.ts b/src/test/playwright/support/pageobjects/exercises/ExerciseResultPage.ts index 745a08f0f473..4b72c998fde9 100644 --- a/src/test/playwright/support/pageobjects/exercises/ExerciseResultPage.ts +++ b/src/test/playwright/support/pageobjects/exercises/ExerciseResultPage.ts @@ -26,9 +26,7 @@ export class ExerciseResultPage { async shouldShowScore(percentage: number) { await Commands.reloadUntilFound(this.page, this.page.locator('jhi-course-exercise-details #submission-result-graded'), 4000, 60000); - // TODO this is not the correct selector - - await expect(this.page.locator('.tab-bar-exercise-details').getByText(`${percentage}%`)).toBeVisible(); + await expect(this.page.locator('#exercise-headers-information').getByText(`${percentage}%`)).toBeVisible(); } async clickOpenExercise(exerciseId: number) { diff --git a/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts b/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts index db77afa04097..14fffcf6a354 100644 --- a/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts +++ b/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts @@ -16,9 +16,7 @@ export class ProgrammingExerciseOverviewPage { } async getResultScore() { - // TODO this is not the correct selector - - const resultScore = this.page.locator('.tab-bar-exercise-details').locator('#result-score'); + const resultScore = this.page.locator('#exercise-headers-information').locator('#result-score'); await resultScore.waitFor({ state: 'visible' }); return resultScore; } @@ -48,7 +46,6 @@ export class ProgrammingExerciseOverviewPage { } getExerciseDetails() { - // TODO this is not the correct selector - return this.page.locator('.tab-bar-exercise-details'); + return this.page.locator('#exercise-headers-information'); } } From bc0930b9ad75588ca9f79d89c33e5498576652a0 Mon Sep 17 00:00:00 2001 From: Ramona Beinstingel Date: Tue, 30 Jul 2024 20:07:16 +0200 Subject: [PATCH 14/23] add client tests --- ...xercise-headers-information.component.html | 28 +-- .../exercise-headers-information.component.ts | 27 ++- .../course-exercise-details.component.html | 2 +- .../exercise-categories.component.html | 2 +- .../information-box.component.ts | 14 +- ...cise-headers-information.component.spec.ts | 220 +++++++++++++++++- .../javascript/spec/util/date.utils.spec.ts | 26 ++- .../ProgrammingExerciseOverviewPage.ts | 2 +- 8 files changed, 276 insertions(+), 45 deletions(-) diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html index fa98ad524bc3..178b835ccdbe 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html +++ b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.html @@ -8,12 +8,12 @@ } @case ('categories') { + contentComponent + [exercise]="informationBoxItem.content.value" + [showTags]="{ difficulty: false, notReleased: true, includedInScore: true }" + [ngClass]="'badge-row'" + [isSmall]="true" + /> } @case ('timeAgo') { {{ informationBoxItem.content.value | artemisTimeAgo }} @@ -23,14 +23,14 @@ } @case ('submissionStatus') { + contentComponent + class="text-truncate result" + [exercise]="informationBoxItem.content.value" + [studentParticipation]="studentParticipation" + [triggerLastGraded]="false" + [showCompletion]="false" + [showBadge]="false" + /> } } diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts index 6c77b2bc0b68..5b4fef0f83a2 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts @@ -10,20 +10,19 @@ import { Course } from 'app/entities/course.model'; import { SubmissionType } from 'app/entities/submission.model'; import { ProgrammingSubmission } from 'app/entities/programming-submission.model'; import { roundValueSpecifiedByCourseSettings } from 'app/shared/util/utils'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { SubmissionResultStatusModule } from 'app/overview/submission-result-status.module'; import { ExerciseCategoriesModule } from 'app/shared/exercise-categories/exercise-categories.module'; import { InformationBox, InformationBoxComponent } from 'app/shared/information-box/information-box.component'; import { ComplaintService } from 'app/complaints/complaint.service'; import { isDateLessThanAWeekAway } from 'app/utils/date.utils'; import { DifficultyLevelComponent } from 'app/shared/difficulty-level/difficulty-level.component'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; @Component({ selector: 'jhi-exercise-headers-information', templateUrl: './exercise-headers-information.component.html', standalone: true, - imports: [ArtemisSharedModule, ArtemisSharedComponentModule, SubmissionResultStatusModule, ExerciseCategoriesModule, InformationBoxComponent, DifficultyLevelComponent], + imports: [SubmissionResultStatusModule, ExerciseCategoriesModule, InformationBoxComponent, DifficultyLevelComponent, ArtemisSharedCommonModule], styleUrls: ['./exercise-headers-information.component.scss'], /* Our tsconfig file has `preserveWhitespaces: 'true'` which causes whitespace to affect content projection. We need to set it to 'false 'for this component, otherwise the components with the selector [contentComponent] @@ -62,16 +61,6 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { this.createInformationBoxItems(); } - createInformationBoxItems() { - this.addPointsItems(); - this.addDueDateItems(); - this.addStartDateItem(); - this.addSubmissionStatusItem(); - this.addSubmissionPolicyItem(); - this.addDifficultyItem(); - this.addCategoryItems(); - } - ngOnChanges() { this.course = this.course ?? getCourseFromExercise(this.exercise); @@ -89,6 +78,16 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { } } + createInformationBoxItems() { + this.addPointsItems(); + this.addDueDateItems(); + this.addStartDateItem(); + this.addSubmissionStatusItem(); + this.addSubmissionPolicyItem(); + this.addDifficultyItem(); + this.addCategoryItems(); + } + addPointsItems() { const { maxPoints, bonusPoints } = this.exercise; if (maxPoints) { @@ -273,7 +272,7 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { } } - private countSubmissions() { + countSubmissions() { const commitHashSet = new Set(); this.studentParticipation?.results diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html index 425edf13f3c3..a9724ca5fb8b 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html @@ -1,5 +1,5 @@ @if (exercise) { -
+
{{ exercise.title }}
diff --git a/src/main/webapp/app/shared/exercise-categories/exercise-categories.component.html b/src/main/webapp/app/shared/exercise-categories/exercise-categories.component.html index c073e9ebf5b9..b83db09553e5 100644 --- a/src/main/webapp/app/shared/exercise-categories/exercise-categories.component.html +++ b/src/main/webapp/app/shared/exercise-categories/exercise-categories.component.html @@ -13,7 +13,7 @@ @for (category of exercise.categories; track category) { @if (category.category) { - {{ category.category | truncate:30 }} + {{ category.category | truncate: 30 }} } } diff --git a/src/main/webapp/app/shared/information-box/information-box.component.ts b/src/main/webapp/app/shared/information-box/information-box.component.ts index ec79beb1fd0e..5be6212f7277 100644 --- a/src/main/webapp/app/shared/information-box/information-box.component.ts +++ b/src/main/webapp/app/shared/information-box/information-box.component.ts @@ -14,32 +14,32 @@ export interface InformationBox { contentColor?: string; } -interface StudentExamContent { +export interface StudentExamContent { type: 'workingTime'; value: StudentExam; } -interface DateContent { +export interface DateContent { type: 'timeAgo' | 'dateTime'; value: DateType; } -interface DifficultyLevelContent { +export interface DifficultyLevelContent { type: 'difficultyLevel'; value: DifficultyLevel; } -interface ExerciseContent { +export interface ExerciseContent { type: 'submissionStatus' | 'categories'; value: Exercise; } -interface StringContent { +export interface StringNumberContent { type: 'string'; - value: string; + value: string | number; } -export type InformationBoxContent = StudentExamContent | DateContent | ExerciseContent | DifficultyLevelContent | StringContent; +export type InformationBoxContent = StudentExamContent | DateContent | ExerciseContent | DifficultyLevelContent | StringNumberContent; @Component({ standalone: true, diff --git a/src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts b/src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts index 50f2f2a315c8..d665b478a9cf 100644 --- a/src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts +++ b/src/test/javascript/spec/component/exercises/shared/headers/exercise-headers-information.component.spec.ts @@ -4,13 +4,20 @@ import { TranslateModule } from '@ngx-translate/core'; import { ExerciseHeadersInformationComponent } from 'app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component'; import { MockProvider } from 'ng-mocks'; -import { InformationBoxComponent } from 'app/shared/information-box/information-box.component'; import { ArtemisTestModule } from '../../../../test.module'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { DifficultyLevel, Exercise, ExerciseType } from 'app/entities/exercise.model'; import { of } from 'rxjs'; import dayjs from 'dayjs/esm'; -import { ArtemisDatePipe } from '../../../../../../../main/webapp/app/shared/pipes/artemis-date.pipe'; +import { Course } from 'app/entities/course.model'; +import { Result } from 'app/entities/result.model'; +import { StudentParticipation } from 'app/entities/participation/student-participation.model'; +import { SubmissionPolicy } from 'app/entities/submission-policy.model'; +import { ComplaintService } from 'app/complaints/complaint.service'; +import { SubmissionType } from 'app/entities/submission.model'; +import { ProgrammingSubmission } from 'app/entities/programming-submission.model'; +import { LockRepositoryPolicy } from 'app/entities/submission-policy.model'; +import { DateContent, InformationBox, StringNumberContent } from 'app/shared/information-box/information-box.component'; describe('ExerciseHeadersInformationComponent', () => { let component: ExerciseHeadersInformationComponent; @@ -18,18 +25,24 @@ describe('ExerciseHeadersInformationComponent', () => { let exerciseService: ExerciseService; let getExerciseDetailsMock: jest.SpyInstance; - const exercise = { id: 42, type: ExerciseType.TEXT, studentParticipations: [], course: {}, dueDate: dayjs().subtract(1, 'weeks') } as unknown as Exercise; + const exercise = { + id: 42, + type: ExerciseType.TEXT, + studentParticipations: [], + course: {}, + dueDate: dayjs().subtract(1, 'weeks'), + assessmentDueDate: dayjs().add(1, 'weeks'), + } as unknown as Exercise; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ExerciseHeadersInformationComponent, ArtemisTestModule, TranslateModule.forRoot(), NgbTooltipModule, InformationBoxComponent], - providers: [MockProvider(ExerciseService), MockProvider(ArtemisDatePipe)], + imports: [ExerciseHeadersInformationComponent, ArtemisTestModule, TranslateModule.forRoot(), NgbTooltipModule], + providers: [MockProvider(ExerciseService), MockProvider(ComplaintService)], }) .compileComponents() .then(() => { fixture = TestBed.createComponent(ExerciseHeadersInformationComponent); component = fixture.componentInstance; - // mock exerciseService exerciseService = fixture.debugElement.injector.get(ExerciseService); getExerciseDetailsMock = jest.spyOn(exerciseService, 'getExerciseDetails'); getExerciseDetailsMock.mockReturnValue(of({ body: { exercise: exercise } })); @@ -83,4 +96,199 @@ describe('ExerciseHeadersInformationComponent', () => { const difficultyLevelComponent = compiled.querySelector('jhi-difficulty-level'); expect(difficultyLevelComponent).toBeTruthy(); }); + + it('should set individualComplaintDueDate if course.maxComplaintTimeDays is defined', () => { + const course: Course = { id: 1, maxComplaintTimeDays: 7 } as Course; + const result: Result = { id: 1, completionDate: dayjs().subtract(2, 'day') } as Result; + const studentParticipation: StudentParticipation = { id: 1, results: [result] } as StudentParticipation; + + component.course = course; + component.studentParticipation = studentParticipation; + + const expectedDueDate = dayjs().add(7, 'days'); + jest.spyOn(ComplaintService, 'getIndividualComplaintDueDate').mockReturnValue(expectedDueDate); + + if (component.course?.maxComplaintTimeDays) { + component.individualComplaintDueDate = ComplaintService.getIndividualComplaintDueDate( + component.exercise, + component.course.maxComplaintTimeDays, + component.studentParticipation?.results?.last(), + component.studentParticipation, + ); + } + }); + + it('should add points item to informationBoxItems', () => { + const maxPoints = 10; + const pointsContent: StringNumberContent = { type: 'string', value: maxPoints }; + const pointsItem: InformationBox = { title: 'Points', content: pointsContent }; + + jest.spyOn(component, 'getPointsItem').mockReturnValue(pointsItem); + + component.informationBoxItems = []; + component.informationBoxItems.push(component.getPointsItem(maxPoints, 'points')); + + expect(component.informationBoxItems).toHaveLength(1); + expect(component.informationBoxItems[0]).toEqual(pointsItem); + }); + + it('should add bonus points item to informationBoxItems', () => { + const bonusPoints = 5; + const pointsContent: StringNumberContent = { type: 'string', value: bonusPoints }; + const pointsItem: InformationBox = { title: 'Bonus Points', content: pointsContent }; + + jest.spyOn(component, 'getPointsItem').mockReturnValue(pointsItem); + + component.informationBoxItems = []; + component.informationBoxItems.push(component.getPointsItem(bonusPoints, 'points')); + + expect(component.informationBoxItems).toHaveLength(1); + expect(component.informationBoxItems[0]).toEqual(pointsItem); + }); + + it('should add start date item to informationBoxItems if startDate is in the future', () => { + const exercise = { + id: 43, + type: ExerciseType.TEXT, + studentParticipations: [], + course: {}, + dueDate: dayjs().add(1, 'weeks'), + assessmentDueDate: dayjs().add(1, 'weeks'), + startDate: dayjs().add(3, 'days'), + } as unknown as Exercise; + + component.exercise = { ...exercise }; + const startDateContent: DateContent = { + type: 'dateTime', + value: dayjs().add(3, 'days'), + }; + const startDateItem: InformationBox = { + title: 'artemisApp.courseOverview.exerciseDetails.startDate', + content: startDateContent, + isContentComponent: true, + }; + component.informationBoxItems = []; + + fixture.detectChanges(); + + if (component.exercise.startDate && dayjs().isBefore(component.exercise.startDate)) { + component.informationBoxItems.push(startDateItem); + } + + expect(component.informationBoxItems).toHaveLength(1); + expect(component.informationBoxItems[0]).toEqual(startDateItem); + }); + + it('should return correct submission color based on submissions left', () => { + const submissionPolicyWithLimit = { submissionLimit: 3 } as SubmissionPolicy; + const submissionPolicyWithoutLimit = { submissionLimit: undefined } as SubmissionPolicy; + + // Case 1: More than 1 submission left + component.submissionPolicy = submissionPolicyWithLimit; + component.numberOfSubmissions = 1; + expect(component.getSubmissionColor()).toBe('body-color'); + + // Case 2: Exactly 1 submission left + component.numberOfSubmissions = 2; + expect(component.getSubmissionColor()).toBe('warning'); + + // Case 3: No submissions left + component.numberOfSubmissions = 3; + expect(component.getSubmissionColor()).toBe('danger'); + + // Case 4: One more than the limit + component.numberOfSubmissions = 4; + expect(component.getSubmissionColor()).toBe('danger'); + + // Case 5: Unlimited submissions left + component.submissionPolicy = submissionPolicyWithoutLimit; + component.numberOfSubmissions = 0; + expect(component.getSubmissionColor()).toBe('body-color'); + + // Case 6: Unlimited submissions with 1 submission done + component.numberOfSubmissions = 1; + expect(component.getSubmissionColor()).toBe('body-color'); + + // Case 7: Unlimited submissions with 2 submissions done + component.numberOfSubmissions = 2; + expect(component.getSubmissionColor()).toBe('body-color'); + }); + + it('should add assessment due date item to informationBoxItems if dueDate is in the past and assessmentDueDate is in the future', () => { + const now = dayjs(); + const dueDate = now.subtract(1, 'day'); + + const assessmentDueContent: DateContent = { type: 'dateTime', value: dayjs().add(1, 'weeks') }; + const assessmentDueItem: InformationBox = { + title: 'artemisApp.courseOverview.exerciseDetails.assessmentDue', + content: assessmentDueContent, + isContentComponent: true, + tooltip: 'artemisApp.courseOverview.exerciseDetails.assessmentDueTooltip', + }; + component.dueDate = dueDate; + component.informationBoxItems = []; + + if (component.dueDate?.isBefore(now) && component.exercise.assessmentDueDate?.isAfter(now)) { + component.informationBoxItems.push(assessmentDueItem); + } + + expect(component.informationBoxItems).toHaveLength(1); + expect(component.informationBoxItems[0]).toEqual(assessmentDueItem); + }); + + it('should update submission policy item in informationBoxItems', () => { + // Mock the countSubmissions method + jest.spyOn(component, 'countSubmissions').mockImplementation(() => {}); + + // Mock the getSubmissionPolicyItem method + const mockSubmissionPolicyItem: InformationBox = { + title: 'artemisApp.programmingExercise.submissionPolicy.submissionLimitTitle', + content: { type: 'string', value: 'Updated Item' } as StringNumberContent, + }; + jest.spyOn(component, 'getSubmissionPolicyItem').mockReturnValue(mockSubmissionPolicyItem); + + // Initialize informationBoxItems with a mock item + component.informationBoxItems = [ + { title: 'artemisApp.programmingExercise.submissionPolicy.submissionLimitTitle', content: { type: 'string', value: 'Original Item' } as StringNumberContent }, + ]; + + // Call the function + component.updateSubmissionPolicyItem(); + + // Verify that countSubmissions was called + expect(component.countSubmissions).toHaveBeenCalled(); + + // Verify that the item in informationBoxItems was updated + expect(component.informationBoxItems).toHaveLength(1); + expect(component.informationBoxItems[0]).toEqual(mockSubmissionPolicyItem); + }); + + it('should correctly count unique manual submissions', () => { + const mockResults: Result[] = [ + { submission: { type: SubmissionType.MANUAL, commitHash: 'hash1' } as ProgrammingSubmission } as Result, + { submission: { type: SubmissionType.MANUAL, commitHash: 'hash2' } as ProgrammingSubmission } as Result, + { submission: { type: SubmissionType.MANUAL, commitHash: 'hash1' } as ProgrammingSubmission } as Result, // Duplicate commit hash + { submission: { type: SubmissionType.INSTRUCTOR, commitHash: 'hash3' } as ProgrammingSubmission } as Result, // Different submission type + ]; + + const mockStudentParticipation: StudentParticipation = { + results: mockResults, + } as StudentParticipation; + + component.studentParticipation = mockStudentParticipation; + component.countSubmissions(); + expect(component.numberOfSubmissions).toBe(2); + }); + + it('should call updateSubmissionPolicyItem if submissionPolicy is active and has a submission limit', () => { + component.submissionPolicy = new LockRepositoryPolicy(); + component.submissionPolicy.active = true; + component.submissionPolicy.submissionLimit = 5; + + const updateSubmissionPolicyItemSpy = jest.spyOn(component, 'updateSubmissionPolicyItem'); + + component.ngOnChanges(); + + expect(updateSubmissionPolicyItemSpy).toHaveBeenCalled(); + }); }); diff --git a/src/test/javascript/spec/util/date.utils.spec.ts b/src/test/javascript/spec/util/date.utils.spec.ts index 1c8bed2cb753..b96d2fbd3a47 100644 --- a/src/test/javascript/spec/util/date.utils.spec.ts +++ b/src/test/javascript/spec/util/date.utils.spec.ts @@ -1,4 +1,11 @@ -import { convertDateFromClient, convertDateFromServer, dayOfWeekZeroSundayToZeroMonday, toISO8601DateString, toISO8601DateTimeString } from 'app/utils/date.utils'; +import { + convertDateFromClient, + convertDateFromServer, + dayOfWeekZeroSundayToZeroMonday, + isDateLessThanAWeekAway, + toISO8601DateString, + toISO8601DateTimeString, +} from 'app/utils/date.utils'; import dayjs from 'dayjs/esm'; describe('DateUtils', () => { @@ -69,4 +76,21 @@ describe('DateUtils', () => { expect(() => dayOfWeekZeroSundayToZeroMonday(7)).toThrow(); }); }); + + describe('isDateLessThanAWeekAway', () => { + it('should return true if date is less than a week away', () => { + const date = dayjs().add(6, 'days'); + expect(isDateLessThanAWeekAway(date)).toBeTrue(); + }); + + it('should return false if date is more than a week away', () => { + const date = dayjs().add(8, 'days'); + expect(isDateLessThanAWeekAway(date)).toBeFalse(); + }); + + it('should return false if date is more than a week ago', () => { + const date = dayjs().subtract(8, 'days'); + expect(isDateLessThanAWeekAway(date)).toBeFalse(); + }); + }); }); diff --git a/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts b/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts index 14fffcf6a354..cc7243718a96 100644 --- a/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts +++ b/src/test/playwright/support/pageobjects/exercises/programming/ProgrammingExerciseOverviewPage.ts @@ -46,6 +46,6 @@ export class ProgrammingExerciseOverviewPage { } getExerciseDetails() { - return this.page.locator('#exercise-headers-information'); + return this.page.locator('#course-exercise-details'); } } From 7d975953939b8b2b38e4245d09ed8bc8dedbbf45 Mon Sep 17 00:00:00 2001 From: Ramona Beinstingel Date: Fri, 2 Aug 2024 11:11:31 +0200 Subject: [PATCH 15/23] PR Feedback Johannes --- .../exam-start-information.component.ts | 6 +- .../exercise-headers-information.component.ts | 44 ++- ...-exercise-page-with-details.component.html | 17 +- .../shared/result/result.component.html | 14 +- .../overview/course-overview.component.html | 353 ++++++++---------- .../overview/course-overview.component.scss | 14 +- .../course-exercise-details.component.html | 6 +- .../course-exercise-details.component.ts | 29 +- .../submission-result-status.component.html | 14 +- .../not-released-tag.component.html | 1 - .../components/not-released-tag.component.ts | 2 +- .../exercise-categories.component.html | 14 +- .../information-box.component.html | 4 +- .../app/shared/layouts/navbar/navbar.scss | 3 +- src/main/webapp/app/shared/shared.module.ts | 4 - .../app/shared/sidebar/sidebar.component.html | 4 +- src/main/webapp/app/utils/date.utils.ts | 2 +- src/main/webapp/content/scss/global.scss | 2 +- .../content/scss/themes/_dark-variables.scss | 7 +- .../scss/themes/_default-variables.scss | 7 +- ...cise-headers-information.component.spec.ts | 6 +- .../javascript/spec/util/date.utils.spec.ts | 10 +- 22 files changed, 263 insertions(+), 300 deletions(-) diff --git a/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts b/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts index 9ee44430c900..9a594acf8d4e 100644 --- a/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts +++ b/src/main/webapp/app/exam/participate/exam-start-information/exam-start-information.component.ts @@ -2,7 +2,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { InformationBox, InformationBoxComponent, InformationBoxContent } from 'app/shared/information-box/information-box.component'; -import { Exam } from 'app/entities/exam.model'; +import { Exam } from 'app/entities/exam/exam.model'; import { StudentExam } from 'app/entities/student-exam.model'; import { ArtemisExamSharedModule } from 'app/exam/shared/exam-shared.module'; import dayjs from 'dayjs/esm'; @@ -45,11 +45,11 @@ export class ExamStartInformationComponent implements OnInit { this.prepareInformationBoxData(); } - buildInformationBox(boxTitle: string, boxContent: InformationBoxContent, isContentComponent?: boolean): InformationBox { + buildInformationBox(boxTitle: string, boxContent: InformationBoxContent, isContentComponent = false): InformationBox { const examInformationBoxData: InformationBox = { title: boxTitle ?? '', content: boxContent, - isContentComponent: isContentComponent ?? false, + isContentComponent: isContentComponent, }; return examInformationBoxData; } diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts index 5b4fef0f83a2..547009a136bd 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts +++ b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts @@ -5,16 +5,16 @@ import { Exercise, IncludedInOverallScore, getCourseFromExercise } from 'app/ent import { SubmissionPolicy } from 'app/entities/submission-policy.model'; import { StudentParticipation } from 'app/entities/participation/student-participation.model'; import { getExerciseDueDate } from 'app/exercises/shared/exercise/exercise.utils'; -import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; +import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model'; import { Course } from 'app/entities/course.model'; import { SubmissionType } from 'app/entities/submission.model'; -import { ProgrammingSubmission } from 'app/entities/programming-submission.model'; +import { ProgrammingSubmission } from 'app/entities/programming/programming-submission.model'; import { roundValueSpecifiedByCourseSettings } from 'app/shared/util/utils'; import { SubmissionResultStatusModule } from 'app/overview/submission-result-status.module'; import { ExerciseCategoriesModule } from 'app/shared/exercise-categories/exercise-categories.module'; import { InformationBox, InformationBoxComponent } from 'app/shared/information-box/information-box.component'; import { ComplaintService } from 'app/complaints/complaint.service'; -import { isDateLessThanAWeekAway } from 'app/utils/date.utils'; +import { isDateLessThanAWeekInTheFuture } from 'app/utils/date.utils'; import { DifficultyLevelComponent } from 'app/shared/difficulty-level/difficulty-level.component'; import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; @@ -41,7 +41,7 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { dueDate?: dayjs.Dayjs; programmingExercise?: ProgrammingExercise; individualComplaintDueDate?: dayjs.Dayjs; - achievedPoints?: number; + achievedPoints: number = 0; numberOfSubmissions: number; informationBoxItems: InformationBox[] = []; @@ -73,7 +73,7 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { const latestRatedResult = this.studentParticipation.results.filter((result) => result.rated).first(); if (latestRatedResult) { - this.achievedPoints = roundValueSpecifiedByCourseSettings((latestRatedResult.score! * this.exercise.maxPoints!) / 100, this.course); + this.achievedPoints = roundValueSpecifiedByCourseSettings((latestRatedResult.score! * this.exercise.maxPoints!) / 100, this.course) ?? 0; } } } @@ -91,10 +91,18 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { addPointsItems() { const { maxPoints, bonusPoints } = this.exercise; if (maxPoints) { - this.informationBoxItems.push(this.getPointsItem(maxPoints, 'points')); - } - if (bonusPoints) { - this.informationBoxItems.push(this.getPointsItem(bonusPoints, 'bonus')); + if (bonusPoints) { + let achievedBonusPoints: number = 0; + // If the student has more points than the max points, the bonus points are calculated + if (this.achievedPoints > maxPoints) { + achievedBonusPoints = roundValueSpecifiedByCourseSettings(this.achievedPoints - maxPoints, this.course); + } + const achievedPoints = this.achievedPoints - achievedBonusPoints; + this.informationBoxItems.push(this.getPointsItem('points', maxPoints, achievedPoints)); + this.informationBoxItems.push(this.getPointsItem('bonus', bonusPoints, achievedBonusPoints)); + } else { + this.informationBoxItems.push(this.getPointsItem('points', maxPoints, this.achievedPoints)); + } } } @@ -139,7 +147,7 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { // If the due date is less than a day away, the color change to red const dueDateStatusBadge = this.dueDate.isBetween(dayjs().add(1, 'day'), dayjs()) ? 'danger' : 'body-color'; // If the due date is less than a week away, text is displayed relatively e.g. 'in 2 days' - const shouldDisplayDueDateRelative = isDateLessThanAWeekAway(this.dueDate); + const shouldDisplayDueDateRelative = isDateLessThanAWeekInTheFuture(this.dueDate); if (isDueDateInThePast) { return { @@ -169,7 +177,7 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { addStartDateItem() { if (this.exercise.startDate && dayjs().isBefore(this.exercise.startDate)) { // If the start date is less than a week away, text is displayed relatively e.g. 'in 2 days' - const shouldDisplayStartDateRelative = isDateLessThanAWeekAway(this.exercise.startDate); + const shouldDisplayStartDateRelative = isDateLessThanAWeekInTheFuture(this.exercise.startDate); const startDateItem: InformationBox = { title: 'artemisApp.courseOverview.exerciseDetails.startDate', content: { @@ -245,19 +253,21 @@ export class ExerciseHeadersInformationComponent implements OnInit, OnChanges { } getSubmissionColor(): string { + // default color should be 'body-color', thats why the default submissionsLeft is 2 const submissionsLeft = this.submissionPolicy?.submissionLimit ? this.submissionPolicy?.submissionLimit - this.numberOfSubmissions : 2; - if (submissionsLeft > 1) return 'body-color'; - else { - return submissionsLeft <= 0 ? 'danger' : 'warning'; - } + let submissionColor = 'body-color'; + if (submissionsLeft === 1) submissionColor = 'warning'; + // 0 submissions left or limit is already reached + else if (submissionsLeft <= 0) submissionColor = 'danger'; + return submissionColor; } - getPointsItem(points: number | undefined, title: string): InformationBox { + getPointsItem(title: string, maxPoints: number, achievedPoints: number): InformationBox { return { title: 'artemisApp.courseOverview.exerciseDetails.' + title, content: { type: 'string', - value: this.achievedPoints ? `${this.achievedPoints} / ${points}` : `0 / ${points}`, + value: `${achievedPoints} / ${maxPoints}`, }, }; } diff --git a/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.html b/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.html index 378c8200c7e4..2ed3b9d18422 100644 --- a/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.html +++ b/src/main/webapp/app/exercises/shared/exercise-headers/header-exercise-page-with-details.component.html @@ -86,14 +86,10 @@ @if (course?.presentationScore) { {{ 'artemisApp.courseOverview.exerciseDetails.presented' | artemisTranslate }} @if ((studentParticipation?.presentationScore ?? 0) > 0) { - - {{ 'global.generic.yes' | artemisTranslate }} - + } @if ((studentParticipation?.presentationScore ?? 0) <= 0) { - - {{ 'global.generic.no' | artemisTranslate }} - + } } @else { {{ 'artemisApp.courseOverview.exerciseDetails.presentation' | artemisTranslate }} @@ -103,9 +99,7 @@ } @if (!studentParticipation?.presentationScore) { - - {{ 'global.generic.unset' | artemisTranslate }} - + } }
@@ -170,9 +164,8 @@ - {{ 'global.generic.yes' | artemisTranslate }} - + jhiTranslate="global.generic.yes" + >
}
diff --git a/src/main/webapp/app/exercises/shared/result/result.component.html b/src/main/webapp/app/exercises/shared/result/result.component.html index dd61f529bddf..01207f3c5052 100644 --- a/src/main/webapp/app/exercises/shared/result/result.component.html +++ b/src/main/webapp/app/exercises/shared/result/result.component.html @@ -100,14 +100,16 @@ @switch (missingResultInfo) { @case (MissingResultInfo.FAILED_PROGRAMMING_SUBMISSION_ONLINE_IDE) { - {{ - 'artemisApp.result.missing.programmingFailedSubmission.message' | artemisTranslate - }} + } @case (MissingResultInfo.FAILED_PROGRAMMING_SUBMISSION_OFFLINE_IDE) { - {{ - 'artemisApp.result.missing.programmingFailedSubmission.message' | artemisTranslate - }} + } } @if (result && exercise?.type !== ExerciseType.QUIZ) { diff --git a/src/main/webapp/app/overview/course-overview.component.html b/src/main/webapp/app/overview/course-overview.component.html index e4ab97d1455f..e06da3b07e97 100644 --- a/src/main/webapp/app/overview/course-overview.component.html +++ b/src/main/webapp/app/overview/course-overview.component.html @@ -1,213 +1,178 @@ -@if (!isShownViaLti) { -