+
@for (informationBoxData of examInformationBoxData; track informationBoxData) {
-
-
-
- @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..3f02da6bab2b 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,7 +1,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 } from 'app/shared/information-box/information-box.component';
+import { InformationBox, InformationBoxComponent, InformationBoxContent } from 'app/shared/information-box/information-box.component';
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';
@@ -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 = false): InformationBox {
const examInformationBoxData: InformationBox = {
title: boxTitle ?? '',
- content: boxContent ?? '',
- contentComponent: boxContentComponent,
+ content: boxContent,
+ isContentComponent: isContentComponent,
};
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: 'string',
+ 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
new file mode 100644
index 000000000000..30e26e951231
--- /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) {
+
+}
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..547009a136bd
--- /dev/null
+++ b/src/main/webapp/app/exercises/shared/exercise-headers/exercise-headers-information/exercise-headers-information.component.ts
@@ -0,0 +1,296 @@
+import { Component, Input, OnChanges, OnInit } from '@angular/core';
+import { SortService } from 'app/shared/service/sort.service';
+import dayjs from 'dayjs/esm';
+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/programming-exercise.model';
+import { Course } from 'app/entities/course.model';
+import { SubmissionType } from 'app/entities/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 { isDateLessThanAWeekInTheFuture } 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: [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]
+ will not be projected into their specific slot of the "InformationBoxComponent" component.*/
+ preserveWhitespaces: false,
+})
+export class ExerciseHeadersInformationComponent implements OnInit, OnChanges {
+ readonly IncludedInOverallScore = IncludedInOverallScore;
+ readonly dayjs = dayjs;
+
+ @Input() exercise: Exercise;
+ @Input() studentParticipation?: StudentParticipation;
+ @Input() course?: Course;
+ @Input() submissionPolicy?: SubmissionPolicy;
+
+ dueDate?: dayjs.Dayjs;
+ programmingExercise?: ProgrammingExercise;
+ individualComplaintDueDate?: dayjs.Dayjs;
+ achievedPoints: number = 0;
+ numberOfSubmissions: number;
+ informationBoxItems: InformationBox[] = [];
+
+ constructor(private sortService: SortService) {}
+
+ ngOnInit() {
+ this.dueDate = getExerciseDueDate(this.exercise, this.studentParticipation);
+
+ if (this.course?.maxComplaintTimeDays) {
+ this.individualComplaintDueDate = ComplaintService.getIndividualComplaintDueDate(
+ this.exercise,
+ this.course.maxComplaintTimeDays,
+ this.studentParticipation?.results?.last(),
+ this.studentParticipation,
+ );
+ }
+ this.createInformationBoxItems();
+ }
+
+ 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) ?? 0;
+ }
+ }
+ }
+
+ createInformationBoxItems() {
+ this.addPointsItems();
+ this.addDueDateItems();
+ this.addStartDateItem();
+ this.addSubmissionStatusItem();
+ this.addSubmissionPolicyItem();
+ this.addDifficultyItem();
+ this.addCategoryItems();
+ }
+
+ addPointsItems() {
+ const { maxPoints, bonusPoints } = this.exercise;
+ if (maxPoints) {
+ 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));
+ }
+ }
+ }
+
+ addDueDateItems() {
+ const now = dayjs();
+
+ 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: InformationBox = {
+ title: 'artemisApp.courseOverview.exerciseDetails.assessmentDue',
+ 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 (this.exercise.assessmentDueDate?.isBefore(now) && this.individualComplaintDueDate?.isAfter(now)) {
+ const complaintDueItem: InformationBox = {
+ title: 'artemisApp.courseOverview.exerciseDetails.complaintDue',
+ content: {
+ type: 'dateTime',
+ value: this.individualComplaintDueDate,
+ },
+ isContentComponent: true,
+ tooltip: 'artemisApp.courseOverview.exerciseDetails.complaintDueTooltip',
+ };
+ this.informationBoxItems.push(complaintDueItem);
+ }
+ }
+
+ 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 = isDateLessThanAWeekInTheFuture(this.dueDate);
+
+ if (isDueDateInThePast) {
+ return {
+ title: 'artemisApp.courseOverview.exerciseDetails.submissionDueOver',
+ content: {
+ type: 'dateTime',
+ value: this.dueDate,
+ },
+ isContentComponent: true,
+ };
+ }
+
+ return {
+ 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') },
+ };
+ }
+ }
+
+ 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 = isDateLessThanAWeekInTheFuture(this.exercise.startDate);
+ const startDateItem: InformationBox = {
+ title: 'artemisApp.courseOverview.exerciseDetails.startDate',
+ content: {
+ type: shouldDisplayStartDateRelative ? 'timeAgo' : 'dateTime',
+ value: this.exercise.startDate,
+ },
+ isContentComponent: true,
+ tooltip: shouldDisplayStartDateRelative ? 'artemisApp.exerciseActions.startExerciseBeforeStartDate' : undefined,
+ };
+ this.informationBoxItems.push(startDateItem);
+ }
+ }
+
+ addDifficultyItem() {
+ 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: InformationBox = {
+ title: 'artemisApp.courseOverview.exerciseDetails.status',
+ 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: InformationBox = {
+ title: 'artemisApp.courseOverview.exerciseDetails.categories',
+ content: {
+ type: 'categories',
+ value: this.exercise,
+ },
+ isContentComponent: true,
+ };
+ this.informationBoxItems.push(categoryItem);
+ }
+ }
+
+ addSubmissionPolicyItem() {
+ if (this.submissionPolicy?.active && this.submissionPolicy?.submissionLimit) {
+ this.informationBoxItems.push(this.getSubmissionPolicyItem());
+ }
+ }
+
+ getSubmissionPolicyItem(): InformationBox {
+ return {
+ title: 'artemisApp.programmingExercise.submissionPolicy.submissionLimitTitle',
+ content: {
+ type: 'string',
+ value: `${this.numberOfSubmissions} / ${this.submissionPolicy?.submissionLimit}`,
+ },
+ contentColor: this.submissionPolicy?.submissionLimit ? this.getSubmissionColor() : 'body-color',
+ tooltip: 'artemisApp.programmingExercise.submissionPolicy.submissionPolicyType.' + this.submissionPolicy?.type + '.tooltip',
+ tooltipParams: { points: this.submissionPolicy?.exceedingPenalty?.toString() },
+ };
+ }
+
+ 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;
+ 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(title: string, maxPoints: number, achievedPoints: number): InformationBox {
+ return {
+ title: 'artemisApp.courseOverview.exerciseDetails.' + title,
+ content: {
+ type: 'string',
+ value: `${achievedPoints} / ${maxPoints}`,
+ },
+ };
+ }
+
+ updateSubmissionPolicyItem() {
+ 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());
+ }
+ }
+
+ 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/result/result.component.html b/src/main/webapp/app/exercises/shared/result/result.component.html
index 65c4be9ca9de..01207f3c5052 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 }})
}
@@ -97,7 +97,7 @@
}
@case (ResultTemplateStatus.MISSING) {
-
+
@switch (missingResultInfo) {
@case (MissingResultInfo.FAILED_PROGRAMMING_SUBMISSION_ONLINE_IDE) {
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/course-conversations/course-conversations.component.scss b/src/main/webapp/app/overview/course-conversations/course-conversations.component.scss
index 4feac4ecf258..e2919dcc3388 100644
--- a/src/main/webapp/app/overview/course-conversations/course-conversations.component.scss
+++ b/src/main/webapp/app/overview/course-conversations/course-conversations.component.scss
@@ -20,8 +20,8 @@
}
.scrollable-content {
- --message-input-height-dev: 193px;
- --message-input-height-prod: 177px;
+ --message-input-height-dev: 164px;
+ --message-input-height-prod: 148px;
overflow-y: auto;
overflow-x: hidden;
max-height: calc(100vh - var(--header-height) - var(--message-input-height-prod));
diff --git a/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.scss b/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.scss
index d6900d231537..83aa144d2d9e 100644
--- a/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.scss
+++ b/src/main/webapp/app/overview/course-conversations/layout/conversation-messages/conversation-messages.component.scss
@@ -3,8 +3,8 @@
@import 'bootstrap/scss/mixins';
.conversation-messages {
- --message-input-height-prod: 171px;
- --message-input-height-dev: 187px;
+ --message-input-height-prod: 142px;
+ --message-input-height-dev: 158px;
--search-height: 52px;
--channel-header-height: 52px;
diff --git a/src/main/webapp/app/overview/course-overview.component.html b/src/main/webapp/app/overview/course-overview.component.html
index 2cd6c2c5c4ee..5fac3f273643 100644
--- a/src/main/webapp/app/overview/course-overview.component.html
+++ b/src/main/webapp/app/overview/course-overview.component.html
@@ -1,223 +1,230 @@
-@if (!isShownViaLti) {
-