From d9f2cb87d84a9b413b7f15b2456c81c7e1c213f3 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Mon, 9 Sep 2024 17:07:20 +0200 Subject: [PATCH 1/5] Development: Improve cleanup schedule for build logs --- .../artemis/service/BuildLogEntryService.java | 29 +++++++++++++++++-- .../resources/config/application-artemis.yml | 2 +- .../service/BuildLogEntryServiceTest.java | 4 --- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/BuildLogEntryService.java b/src/main/java/de/tum/in/www1/artemis/service/BuildLogEntryService.java index 9b2916219cd0..73198cbb7d52 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/BuildLogEntryService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/BuildLogEntryService.java @@ -40,15 +40,18 @@ public class BuildLogEntryService { private final ProgrammingSubmissionRepository programmingSubmissionRepository; + private final ProfileService profileService; + @Value("${artemis.continuous-integration.build-log.file-expiry-days:30}") private int expiryDays; @Value("${artemis.build-logs-path:./build-logs}") private Path buildLogsPath; - public BuildLogEntryService(BuildLogEntryRepository buildLogEntryRepository, ProgrammingSubmissionRepository programmingSubmissionRepository) { + public BuildLogEntryService(BuildLogEntryRepository buildLogEntryRepository, ProgrammingSubmissionRepository programmingSubmissionRepository, ProfileService profileService) { this.buildLogEntryRepository = buildLogEntryRepository; this.programmingSubmissionRepository = programmingSubmissionRepository; + this.profileService = profileService; } /** @@ -331,10 +334,30 @@ public FileSystemResource retrieveBuildLogsFromFileForBuildJob(String buildJobId } /** - * Deletes all build log files that are older than {@link #expiryDays} days on a schedule + * Scheduled task that deletes old build log files from the continuous integration system. + *

+ * This method runs based on the cron schedule defined in the application properties, with + * a default value of 3:00 AM every day if no custom schedule is provided. + * The task will only execute if scheduling is active, which is checked via the {@code profileService}. + *

+ *

+ * The method iterates through the files in the configured build logs directory and deletes + * files that were last modified before the configured expiry period (in days). The expiration + * period is specified by the {@code expiryDays} variable, and files older than this period are deleted. + *

+ *

+ * In case of an error during file deletion, it logs the error and continues processing. + *

+ * + * @throws IOException if an I/O error occurs while accessing the build log files directory or + * deleting files. */ - @Scheduled(cron = "${artemis.continuous-integration.build-log.cleanup-schedule:0 0 3 1 * ?}") + @Scheduled(cron = "${artemis.continuous-integration.build-log.cleanup-schedule:0 0 3 * * ?}") public void deleteOldBuildLogsFiles() { + // only execute this if scheduling is active + if (!profileService.isSchedulingActive()) { + return; + } log.info("Deleting old build log files"); ZonedDateTime now = ZonedDateTime.now(); diff --git a/src/main/resources/config/application-artemis.yml b/src/main/resources/config/application-artemis.yml index 9b2c2e3f40f7..f1501cbceba2 100644 --- a/src/main/resources/config/application-artemis.yml +++ b/src/main/resources/config/application-artemis.yml @@ -90,7 +90,7 @@ artemis: notification-plugin: "ls1tum/artemis-notification-plugin:1.0.0" # Docker image for the generic notification plugin. This value is set in an CI variable in GitLab CI. build-log: file-expiry-days: 30 # The amount of days until build log files can be deleted - cleanup-schedule: 0 0 3 1 * ? # Cron expression for schedule to delete old build log files + cleanup-schedule: 0 0 3 * * ? # Cron expression for schedule to delete old build log files git: name: Artemis email: artemis@xcit.tum.de diff --git a/src/test/java/de/tum/in/www1/artemis/service/BuildLogEntryServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/BuildLogEntryServiceTest.java index ba2befae81f0..398572dc7613 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/BuildLogEntryServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/BuildLogEntryServiceTest.java @@ -350,10 +350,6 @@ void filterOutEmptyLogs() { assertThat(result).isEmpty(); } - private List convertToBuildLogs(List content) { - return convertToBuildLogs(content.stream()); - } - private List convertToBuildLogs(String... content) { return convertToBuildLogs(Arrays.stream(content)); } From a902b4d3b8e415f358ca541fa95aa8ed6672b6f7 Mon Sep 17 00:00:00 2001 From: Lucas Welscher Date: Mon, 9 Sep 2024 23:04:59 +0200 Subject: [PATCH 2/5] Assessment: Don't allow activating presentations if no presentation scoring is set (#9251) --- .../file-upload/manage/file-upload-exercise.component.html | 4 ++-- .../modeling/manage/modeling-exercise.component.html | 4 ++-- .../programming/manage/programming-exercise.component.html | 4 ++-- .../shared/presentation-score/presentation-score.component.ts | 2 +- .../text/manage/text-exercise/text-exercise.component.html | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.html b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.html index a478cc930b75..8e0258d925a9 100644 --- a/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.html +++ b/src/main/webapp/app/exercises/file-upload/manage/file-upload-exercise.component.html @@ -16,7 +16,7 @@       - @if (course.presentationScore !== 0) { + @if (course.presentationScore) {   @@ -58,7 +58,7 @@ {{ fileUploadExercise.maxPoints }} {{ fileUploadExercise.bonusPoints }} {{ exerciseService.isIncludedInScore(fileUploadExercise) }} - @if (course.presentationScore !== 0) { + @if (course.presentationScore) { {{ fileUploadExercise.presentationScoreEnabled }} } {{ fileUploadExercise.filePattern }} diff --git a/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.html b/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.html index e76a279bbb45..7658540cd82c 100644 --- a/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.html +++ b/src/main/webapp/app/exercises/modeling/manage/modeling-exercise.component.html @@ -16,7 +16,7 @@       - @if (course.presentationScore !== 0) { + @if (course.presentationScore) {   @@ -58,7 +58,7 @@ {{ modelingExercise.maxPoints }} {{ modelingExercise.bonusPoints }} {{ exerciseService.isIncludedInScore(modelingExercise) }} - @if (course.presentationScore !== 0) { + @if (course.presentationScore) { {{ modelingExercise.presentationScoreEnabled }} } {{ modelingExercise.diagramType }} diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html index 39f4a28bbff0..098e76ae96b9 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html @@ -23,7 +23,7 @@ - @if (course.presentationScore !== 0) { + @if (course.presentationScore) {   @@ -151,7 +151,7 @@ - @if (course.presentationScore !== 0) { + @if (course.presentationScore) { {{ programmingExercise.presentationScoreEnabled }} } diff --git a/src/main/webapp/app/exercises/shared/presentation-score/presentation-score.component.ts b/src/main/webapp/app/exercises/shared/presentation-score/presentation-score.component.ts index 628e3eec033e..5b1ed0e98593 100644 --- a/src/main/webapp/app/exercises/shared/presentation-score/presentation-score.component.ts +++ b/src/main/webapp/app/exercises/shared/presentation-score/presentation-score.component.ts @@ -68,7 +68,7 @@ export class PresentationScoreComponent implements DoCheck, OnDestroy { } private isBasicPresentation(): boolean { - return !!(this.exercise.course && this.exercise.course.presentationScore !== 0); + return !!this.exercise.course?.presentationScore; } private isGradedPresentation(): boolean { diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.html b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.html index 379a4761e2e7..96efff611126 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.html +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.component.html @@ -16,7 +16,7 @@       - @if (course.presentationScore !== 0) { + @if (course.presentationScore) {   @@ -53,7 +53,7 @@ {{ textExercise.maxPoints }} {{ textExercise.bonusPoints }} {{ exerciseService.isIncludedInScore(textExercise) }} - @if (course.presentationScore !== 0) { + @if (course.presentationScore) { {{ textExercise.presentationScoreEnabled }} } From a2d6daeee133b98d2667a652ef7b00a5af032040 Mon Sep 17 00:00:00 2001 From: Lucas Welscher Date: Mon, 9 Sep 2024 23:05:12 +0200 Subject: [PATCH 3/5] Exam mode: Show exercise group title in breadcrumbs (#9254) --- .../manage/course-management.service.ts | 2 +- ...gramming-exercise-participation.service.ts | 4 +-- .../shared/exercise/exercise.service.ts | 8 ++--- .../layouts/navbar/entity-title.service.ts | 11 +++++++ .../spec/service/entity-title.service.spec.ts | 31 +++++++++++++++++++ .../spec/service/exercise.service.spec.ts | 25 ++------------- 6 files changed, 50 insertions(+), 31 deletions(-) diff --git a/src/main/webapp/app/course/manage/course-management.service.ts b/src/main/webapp/app/course/manage/course-management.service.ts index 6b1a0a567412..0f442cada5ea 100644 --- a/src/main/webapp/app/course/manage/course-management.service.ts +++ b/src/main/webapp/app/course/manage/course-management.service.ts @@ -676,7 +676,7 @@ export class CourseManagementService { this.entityTitleService.setTitle(EntityType.COURSE, [course?.id], course?.title); course?.exercises?.forEach((exercise) => { - this.entityTitleService.setTitle(EntityType.EXERCISE, [exercise.id], exercise.title); + this.entityTitleService.setExerciseTitle(exercise); }); course?.lectures?.forEach((lecture) => this.entityTitleService.setTitle(EntityType.LECTURE, [lecture.id], lecture.title)); course?.exams?.forEach((exam) => this.entityTitleService.setTitle(EntityType.EXAM, [exam.id], exam.title)); diff --git a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts index 5953571a4190..e5ece788a0fb 100644 --- a/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts +++ b/src/main/webapp/app/exercises/programming/manage/services/programming-exercise-participation.service.ts @@ -3,11 +3,11 @@ import { Injectable } from '@angular/core'; import { AccountService } from 'app/core/auth/account.service'; import { Participation } from 'app/entities/participation/participation.model'; import { ProgrammingExerciseStudentParticipation } from 'app/entities/participation/programming-exercise-student-participation.model'; +import { CommitInfo } from 'app/entities/programming/programming-submission.model'; import { Result } from 'app/entities/result.model'; import { EntityTitleService, EntityType } from 'app/shared/layouts/navbar/entity-title.service'; import { createRequestOption } from 'app/shared/util/request.util'; import { Observable, map, tap } from 'rxjs'; -import { CommitInfo } from 'app/entities/programming/programming-submission.model'; export interface IProgrammingExerciseParticipationService { getLatestResultWithFeedback: (participationId: number, withSubmission: boolean) => Observable; @@ -81,7 +81,7 @@ export class ProgrammingExerciseParticipationService implements IProgrammingExer sendTitlesToEntityTitleService(participation: Participation | undefined) { if (participation?.exercise) { const exercise = participation.exercise; - this.entityTitleService.setTitle(EntityType.EXERCISE, [exercise.id], exercise.title); + this.entityTitleService.setExerciseTitle(exercise); if (exercise.course) { const course = exercise.course; diff --git a/src/main/webapp/app/exercises/shared/exercise/exercise.service.ts b/src/main/webapp/app/exercises/shared/exercise/exercise.service.ts index a5c6b2febc1a..909aa30aa592 100644 --- a/src/main/webapp/app/exercises/shared/exercise/exercise.service.ts +++ b/src/main/webapp/app/exercises/shared/exercise/exercise.service.ts @@ -488,12 +488,8 @@ export class ExerciseService { } public sendExerciseTitleToTitleService(exercise?: Exercise) { - // we only want to show the exercise group name as exercise name to the student for exam exercises. - // for tutors and more privileged users, we want to show the exercise title - if (exercise?.exerciseGroup && !exercise?.isAtLeastTutor) { - this.entityTitleService.setTitle(EntityType.EXERCISE, [exercise?.id], exercise?.exerciseGroup.title); - } else { - this.entityTitleService.setTitle(EntityType.EXERCISE, [exercise?.id], exercise?.title); + if (exercise) { + this.entityTitleService.setExerciseTitle(exercise); } if (exercise?.course) { this.entityTitleService.setTitle(EntityType.COURSE, [exercise.course.id], exercise.course.title); diff --git a/src/main/webapp/app/shared/layouts/navbar/entity-title.service.ts b/src/main/webapp/app/shared/layouts/navbar/entity-title.service.ts index d342f8eb1464..a50aab27ddc4 100644 --- a/src/main/webapp/app/shared/layouts/navbar/entity-title.service.ts +++ b/src/main/webapp/app/shared/layouts/navbar/entity-title.service.ts @@ -1,6 +1,7 @@ import { HttpClient, HttpResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { captureException } from '@sentry/angular'; +import { Exercise } from 'app/entities/exercise.model'; import { EMPTY, Observable, ReplaySubject, Subject } from 'rxjs'; export enum EntityType { @@ -88,6 +89,16 @@ export class EntityTitleService { } } + public setExerciseTitle(exercise: Exercise) { + // we only want to show the exercise group name as exercise name to the students for exam exercises. + // for tutors and more privileged users, we want to show the exercise title + if (exercise.exerciseGroup && !exercise?.isAtLeastTutor) { + this.setTitle(EntityType.EXERCISE, [exercise.id], exercise.exerciseGroup.title); + } else { + this.setTitle(EntityType.EXERCISE, [exercise.id], exercise.title); + } + } + /** * Fetches the title of the given entity from the server. * diff --git a/src/test/javascript/spec/service/entity-title.service.spec.ts b/src/test/javascript/spec/service/entity-title.service.spec.ts index 1e8165930ba7..a0d25c344ae6 100644 --- a/src/test/javascript/spec/service/entity-title.service.spec.ts +++ b/src/test/javascript/spec/service/entity-title.service.spec.ts @@ -1,3 +1,4 @@ +import { Exercise } from 'app/entities/exercise.model'; import { EntityTitleService, EntityType } from 'app/shared/layouts/navbar/entity-title.service'; import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { MockHttpService } from '../helpers/mocks/service/mock-http.service'; @@ -115,4 +116,34 @@ describe('EntityTitleService', () => { service.setTitle(type, ids, title); expect(captureSpy).toHaveBeenCalledOnce(); }); + + it('sets the exercise group title for students during an exam', () => { + const exercise = { id: 1, exerciseGroup: { title: 'Group Title' }, isAtLeastTutor: false } as Exercise; + service.setExerciseTitle(exercise); + + let result: string | undefined = undefined; + service.getTitle(EntityType.EXERCISE, [1]).subscribe((title) => (result = title)); + + expect(result).toBe('Group Title'); + }); + + it('sets the exercise title for tutors and more privileged users', () => { + const exercise = { id: 1, exerciseGroup: { title: 'Group Title' }, isAtLeastTutor: true, title: 'Exercise Title' } as Exercise; + service.setExerciseTitle(exercise); + + let result: string | undefined = undefined; + service.getTitle(EntityType.EXERCISE, [1]).subscribe((title) => (result = title)); + + expect(result).toBe('Exercise Title'); + }); + + it('sets the exercise title for course exercises', () => { + const exercise = { id: 1, isAtLeastTutor: false, title: 'Exercise Title' } as Exercise; + service.setExerciseTitle(exercise); + + let result: string | undefined = undefined; + service.getTitle(EntityType.EXERCISE, [1]).subscribe((title) => (result = title)); + + expect(result).toBe('Exercise Title'); + }); }); diff --git a/src/test/javascript/spec/service/exercise.service.spec.ts b/src/test/javascript/spec/service/exercise.service.spec.ts index 415078a437e1..3213088306c1 100644 --- a/src/test/javascript/spec/service/exercise.service.spec.ts +++ b/src/test/javascript/spec/service/exercise.service.spec.ts @@ -22,7 +22,7 @@ import { SafeHtml } from '@angular/platform-browser'; import { ExerciseCategory } from 'app/entities/exercise-category.model'; import { Observable } from 'rxjs'; import { AccountService } from 'app/core/auth/account.service'; -import { EntityTitleService, EntityType } from 'app/shared/layouts/navbar/entity-title.service'; +import { EntityTitleService } from 'app/shared/layouts/navbar/entity-title.service'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; describe('Exercise Service', () => { @@ -358,7 +358,7 @@ describe('Exercise Service', () => { const profileService = TestBed.inject(ProfileService); const accountServiceSpy = jest.spyOn(accountService, 'setAccessRightsForExerciseAndReferencedCourse'); - const entityTitleServiceSpy = jest.spyOn(entityTitleService, 'setTitle'); + const entityTitleServiceSpy = jest.spyOn(entityTitleService, 'setExerciseTitle'); const profileServiceSpy = jest.spyOn(profileService, 'getProfileInfo'); const category = { @@ -387,7 +387,7 @@ describe('Exercise Service', () => { expect(accountServiceSpy).toHaveBeenCalledWith(expect.objectContaining({ id: exerciseFromServer.id })); expect(entityTitleServiceSpy).toHaveBeenCalledOnce(); - expect(entityTitleServiceSpy).toHaveBeenCalledWith(EntityType.EXERCISE, [exerciseFromServer.id], exerciseFromServer.title); + expect(entityTitleServiceSpy).toHaveBeenCalledWith(exerciseFromServer); expect(profileServiceSpy).not.toHaveBeenCalled(); }); @@ -520,23 +520,4 @@ describe('Exercise Service', () => { method: 'PUT', }); }); - - it('should correctly send the exercise name to the title service', () => { - const entityTitleService = TestBed.inject(EntityTitleService); - const examExerciseForStudent = { id: 1, title: 'exercise', exerciseGroup: { id: 1, title: 'exercise group' } } as Exercise; - const examExerciseForTutor = { ...examExerciseForStudent, isAtLeastTutor: true } as Exercise; - const courseExerciseForStudent = { ...examExerciseForStudent, exerciseGroup: undefined, course: { id: 2, title: 'course' } } as Exercise; - const courseExerciseForTutor = { ...courseExerciseForStudent, isAtLeastTutor: true } as Exercise; - const entityTitleServiceSpy = jest.spyOn(entityTitleService, 'setTitle'); - service.sendExerciseTitleToTitleService(examExerciseForStudent); - expect(entityTitleServiceSpy).toHaveBeenCalledWith(EntityType.EXERCISE, [1], 'exercise group'); - service.sendExerciseTitleToTitleService(examExerciseForTutor); - expect(entityTitleServiceSpy).toHaveBeenCalledWith(EntityType.EXERCISE, [1], 'exercise'); - service.sendExerciseTitleToTitleService(courseExerciseForStudent); - expect(entityTitleServiceSpy).toHaveBeenCalledWith(EntityType.EXERCISE, [1], 'exercise'); - expect(entityTitleServiceSpy).toHaveBeenCalledWith(EntityType.COURSE, [2], 'course'); - service.sendExerciseTitleToTitleService(courseExerciseForTutor); - expect(entityTitleServiceSpy).toHaveBeenCalledWith(EntityType.EXERCISE, [1], 'exercise'); - expect(entityTitleServiceSpy).toHaveBeenCalledWith(EntityType.COURSE, [2], 'course'); - }); }); From 9b517bcc57c48520d533ad788f610b91fcb64c22 Mon Sep 17 00:00:00 2001 From: Simon Entholzer <33342534+SimonEntholzer@users.noreply.github.com> Date: Mon, 9 Sep 2024 23:05:31 +0200 Subject: [PATCH 4/5] Exam mode: Fix displaying incorrect task in exam summary exercise (#9294) --- .../programming-exercise-instruction.component.ts | 4 ++-- src/test/javascript/spec/helpers/sample/problemStatement.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts b/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts index f1f669c7fb5c..b0bd830496a9 100644 --- a/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts @@ -348,14 +348,14 @@ export class ProgrammingExerciseInstructionComponent implements OnChanges, OnDes // Insert anchor divs into the text so that injectable elements can be inserted into them. // Without class="d-flex" the injected components height would be 0. // Added zero-width space as content so the div actually consumes a line to prevent a
    display bug in Safari - acc.replace(new RegExp(escapeStringForUseInRegex(task), 'g'), `
    `), + acc.replace(new RegExp(escapeStringForUseInRegex(task), 'g'), `
    `), problemStatementHtml, ); } private injectTasksIntoDocument = () => { this.tasks.forEach(({ id, taskName, testIds }) => { - const taskHtmlContainers = document.getElementsByClassName(`pe-task-${id}`); + const taskHtmlContainers = document.getElementsByClassName(`pe-${this.exercise.id}-task-${id}`); for (let i = 0; i < taskHtmlContainers.length; i++) { const taskHtmlContainer = taskHtmlContainers[i]; diff --git a/src/test/javascript/spec/helpers/sample/problemStatement.json b/src/test/javascript/spec/helpers/sample/problemStatement.json index 71494b6f9608..125a7c25d5c9 100644 --- a/src/test/javascript/spec/helpers/sample/problemStatement.json +++ b/src/test/javascript/spec/helpers/sample/problemStatement.json @@ -7,8 +7,8 @@ "problemStatementBothFailedRendered": "
      \n
    1. Implement Bubble Sort: artemisApp.editor.testStatusLabels.noResult
      \nImplement the method performSort(List<Date>) in the class BubbleSort. Make sure to follow the Bubble Sort algorithm exactly.
    2. \n
    3. Implement Merge Sort: artemisApp.editor.testStatusLabels.noResult
      \nImplement the method performSort(List<Date>) in the class MergeSort. Make sure to follow the Merge Sort algorithm exactly.
    4. \n
    \n", "problemStatementBothFailedHtml": "
      \n
    1. Implement Bubble Sort: artemisApp.editor.testStatusLabels.testFailing
      \nImplement the method performSort(List<Date>) in the class BubbleSort. Make sure to follow the Bubble Sort algorithm exactly.
    2. \n
    3. Implement Merge Sort: artemisApp.editor.testStatusLabels.testPassing
      \nImplement the method performSort(List<Date>) in the class MergeSort. Make sure to follow the Merge Sort algorithm exactly.
    4. \n
    \n", "problemStatementBubbleSortFailsRendered": "
      \n
    1. Implement Bubble Sort: artemisApp.editor.testStatusLabels.noResult
      \nImplement the method performSort(List<Date>) in the class BubbleSort. Make sure to follow the Bubble Sort algorithm exactly.
    2. \n
    3. Implement Merge Sort: artemisApp.editor.testStatusLabels.noResult
      \nImplement the method performSort(List<Date>) in the class MergeSort. Make sure to follow the Merge Sort algorithm exactly.
    4. \n
    \n", - "problemStatementBubbleSortNotExecutedHtml": "
      \n
    1. Implement Bubble SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":0}]
      \nImplement the method performSort(List<Date>) in the class BubbleSort. Make sure to follow the Bubble Sort algorithm exactly.
    2. \n
    3. Implement Merge SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":1}]
      \nImplement the method performSort(List<Date>) in the class MergeSort. Make sure to follow the Merge Sort algorithm exactly.
    4. \n
    ", + "problemStatementBubbleSortNotExecutedHtml": "
      \n
    1. Implement Bubble SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":0}]
      \nImplement the method performSort(List<Date>) in the class BubbleSort. Make sure to follow the Bubble Sort algorithm exactly.
    2. \n
    3. Implement Merge SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":1}]
      \nImplement the method performSort(List<Date>) in the class MergeSort. Make sure to follow the Merge Sort algorithm exactly.
    4. \n
    ", "problemStatementEmptySecondTask": "1. [task][Bubble Sort](1) \n Implement the method. \n 2. [task][Merge Sort]() \n Implement the method.", - "problemStatementEmptySecondTaskNotExecutedHtml": "
      \n
    1. Bubble SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":1}]
      \nImplement the method.
    2. \n
    3. Merge SortartemisApp.editor.testStatusLabels.noTests
      \nImplement the method.
    4. \n
    ", + "problemStatementEmptySecondTaskNotExecutedHtml": "
      \n
    1. Bubble SortartemisApp.editor.testStatusLabels.totalTestsPassing: [{\"totalTests\":1,\"passedTests\":1}]
      \nImplement the method.
    2. \n
    3. Merge SortartemisApp.editor.testStatusLabels.noTests
      \nImplement the method.
    4. \n
    ", "problemStatementPlantUMLWithTest": "@startuml\nclass Policy {\n1)>+configure()\n2)>+testWithParenthesis()}\n@enduml" } From 8b80d61ed3e52837b1d2a051fdcf3dee8e3b30ae Mon Sep 17 00:00:00 2001 From: Yannik Schmidt Date: Mon, 9 Sep 2024 23:06:10 +0200 Subject: [PATCH 5/5] Programming exercises: Fix translation issue for participation modes in exam management (#9293) --- ...programming-exercise-group-cell.component.html | 15 ++++++++++----- .../programming-exercise-group-cell.component.ts | 4 +++- .../manage/programming-exercise.component.html | 14 ++++++++------ .../manage/programming-exercise.component.ts | 4 +++- ...gramming-exercise-group-cell.component.spec.ts | 12 +++++++----- 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.html b/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.html index e59af00e81b9..5de7c8465f1a 100644 --- a/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.html +++ b/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.html @@ -84,14 +84,19 @@ @if (displayEditorModus) {
    - : {{ programmingExercise.allowOfflineIde || false }} + +
    - : {{ programmingExercise.allowOnlineEditor || false }} -
    -
    - : {{ programmingExercise.allowOnlineIde || false }} + +
    + @if (onlineIdeEnabled) { +
    + + +
    + }
    } } diff --git a/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.ts b/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.ts index a2b6d64ca6e0..a171fc11d30e 100644 --- a/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.ts +++ b/src/main/webapp/app/exam/manage/exercise-groups/programming-exercise-cell/programming-exercise-group-cell.component.ts @@ -9,7 +9,7 @@ import { ProgrammingExerciseInstructorRepositoryType, ProgrammingExerciseService import { downloadZipFileFromResponse } from 'app/shared/util/download.util'; import { AlertService } from 'app/core/util/alert.service'; import { faDownload } from '@fortawesome/free-solid-svg-icons'; -import { PROFILE_LOCALVC } from 'app/app.constants'; +import { PROFILE_LOCALVC, PROFILE_THEIA } from 'app/app.constants'; @Component({ selector: 'jhi-programming-exercise-group-cell', @@ -22,6 +22,7 @@ export class ProgrammingExerciseGroupCellComponent implements OnInit { programmingExercise: ProgrammingExercise; localVCEnabled = false; + onlineIdeEnabled = false; @Input() displayShortName = false; @@ -48,6 +49,7 @@ export class ProgrammingExerciseGroupCellComponent implements OnInit { ngOnInit(): void { this.profileService.getProfileInfo().subscribe((profileInfo) => { this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); + this.onlineIdeEnabled = profileInfo.activeProfiles.includes(PROFILE_THEIA); if (this.programmingExercise.projectKey) { if (this.programmingExercise.solutionParticipation?.buildPlanId) { this.programmingExercise.solutionParticipation!.buildPlanUrl = createBuildPlanUrl( diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html index 098e76ae96b9..de82f5d4dac1 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.html @@ -139,17 +139,19 @@ }
    - : +
    - : +
    -
    - : - -
    + @if (onlineIdeEnabled) { +
    + + +
    + } @if (course.presentationScore) { {{ programmingExercise.presentationScoreEnabled }} diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts index e40533f526cc..bcc83f36839e 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise.component.ts @@ -39,7 +39,7 @@ import { } from '@fortawesome/free-solid-svg-icons'; import { CourseExerciseService } from 'app/exercises/shared/course-exercises/course-exercise.service'; import { downloadZipFileFromResponse } from 'app/shared/util/download.util'; -import { PROFILE_LOCALCI, PROFILE_LOCALVC } from 'app/app.constants'; +import { PROFILE_LOCALCI, PROFILE_LOCALVC, PROFILE_THEIA } from 'app/app.constants'; @Component({ selector: 'jhi-programming-exercise', @@ -55,6 +55,7 @@ export class ProgrammingExerciseComponent extends ExerciseComponent implements O // Used to make the repository links download the repositories instead of linking to GitLab. localVCEnabled = false; localCIEnabled = false; + onlineIdeEnabled = false; // extension points, see shared/extension-point @ContentChild('overrideRepositoryAndBuildPlan') overrideRepositoryAndBuildPlan: TemplateRef; @@ -111,6 +112,7 @@ export class ProgrammingExerciseComponent extends ExerciseComponent implements O this.buildPlanLinkTemplate = profileInfo.buildPlanURLTemplate; this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); this.localCIEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALCI); + this.onlineIdeEnabled = profileInfo.activeProfiles.includes(PROFILE_THEIA); }); // reconnect exercise with course this.programmingExercises.forEach((exercise) => { diff --git a/src/test/javascript/spec/component/exam/manage/exercise-groups/programming-exercise-group-cell.component.spec.ts b/src/test/javascript/spec/component/exam/manage/exercise-groups/programming-exercise-group-cell.component.spec.ts index 499338bffb52..4fa017b56649 100644 --- a/src/test/javascript/spec/component/exam/manage/exercise-groups/programming-exercise-group-cell.component.spec.ts +++ b/src/test/javascript/spec/component/exam/manage/exercise-groups/programming-exercise-group-cell.component.spec.ts @@ -11,6 +11,8 @@ import { of } from 'rxjs'; import { ProgrammingExerciseService } from 'app/exercises/programming/manage/services/programming-exercise.service'; import { AlertService } from 'app/core/util/alert.service'; import { MockAlertService } from '../../../../helpers/mocks/service/mock-alert.service'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { PROFILE_THEIA } from 'app/app.constants'; describe('Programming Exercise Group Cell Component', () => { let comp: ProgrammingExerciseGroupCellComponent; @@ -43,12 +45,12 @@ describe('Programming Exercise Group Cell Component', () => { // @ts-ignore of({ buildPlanURLTemplate: 'https://example.com/{buildPlanId}/{projectKey}', - activeProfiles: [], + activeProfiles: [PROFILE_THEIA], }), }; TestBed.configureTestingModule({ - imports: [ArtemisTestModule], + imports: [ArtemisTestModule, ArtemisSharedCommonModule], declarations: [ProgrammingExerciseGroupCellComponent, TranslatePipeMock], providers: [ { provide: ProfileService, useValue: mockedProfileService }, @@ -94,15 +96,15 @@ describe('Programming Exercise Group Cell Component', () => { const div0 = fixture.debugElement.query(By.css('div > div > div:first-child')); expect(div0).not.toBeNull(); - expect(div0.nativeElement.textContent?.trim()).toBe(': true'); + expect(div0.nativeElement.textContent?.trim()).toBe('artemisApp.programmingExercise.offlineIdeartemisApp.exercise.yes'); const div1 = fixture.debugElement.query(By.css('div > div > div:nth-child(2)')); expect(div1).not.toBeNull(); - expect(div1.nativeElement.textContent?.trim()).toBe(': true'); + expect(div1.nativeElement.textContent?.trim()).toBe('artemisApp.programmingExercise.onlineEditorartemisApp.exercise.yes'); const div2 = fixture.debugElement.query(By.css('div > div > div:nth-child(3)')); expect(div2).not.toBeNull(); - expect(div2.nativeElement.textContent?.trim()).toBe(': false'); + expect(div2.nativeElement.textContent?.trim()).toBe('artemisApp.programmingExercise.onlineIdeartemisApp.exercise.no'); }); it('should download the repository', () => {