From 90bf23bb3ff63e41437a34aca2422abc14b29b8a Mon Sep 17 00:00:00 2001 From: Yannik S Date: Mon, 2 Sep 2024 11:08:39 +0200 Subject: [PATCH] Programming exercises: Add online IDE settings (#8965) --- .../de/tum/in/www1/artemis/ArtemisApp.java | 3 +- .../artemis/config/TheiaConfiguration.java | 43 ++++++ .../artemis/domain/ProgrammingExercise.java | 10 +- .../ProgrammingExerciseResource.java | 32 ++++- .../theia/TheiaConfigurationResource.java | 41 ++++++ src/main/resources/config/application-dev.yml | 11 +- .../resources/config/application-theia.yml | 13 +- .../entities/programming-exercise.model.ts | 3 + ...ramming-exercise-group-cell.component.html | 9 +- .../programming-exercise-detail.component.ts | 9 +- .../programming-exercise.component.html | 12 +- .../programming-exercise-creation-config.ts | 1 + .../programming-exercise-update.component.ts | 133 +++++++++++------- .../programming-exercise-update.module.ts | 2 + ...ramming-exercise-difficulty.component.html | 57 +++++++- ...ogramming-exercise-difficulty.component.ts | 16 ++- ...ogramming-exercise-language.component.html | 7 + ...programming-exercise-language.component.ts | 2 + .../programming-exercise-theia.component.html | 21 +++ .../programming-exercise-theia.component.ts | 82 +++++++++++ .../shared/service/theia.service.ts | 25 ++++ ...rcise-details-student-actions.component.ts | 28 ++-- .../webapp/i18n/de/programmingExercise.json | 17 ++- .../webapp/i18n/en/programmingExercise.json | 17 ++- ...tractSpringIntegrationIndependentTest.java | 3 +- .../config/TheiaConfigurationTest.java | 41 ++++++ .../theia/TheiaInfoContributorTest.java | 7 +- ...ming-exercise-group-cell.component.spec.ts | 9 +- ...-details-student-actions.component.spec.ts | 55 +++++++- ...gramming-exercise-update.component.spec.ts | 63 ++++++++- ...ogramming-exercise-creation-config-mock.ts | 3 + ...ming-exercise-difficulty.component.spec.ts | 29 +++- ...amming-exercise-language.component.spec.ts | 29 ++++ ...ogramming-exercise-theia.component.spec.ts | 90 ++++++++++++ .../programming-exercise.service.spec.ts | 2 + .../resources/config/application-theia.yml | 9 ++ 36 files changed, 823 insertions(+), 111 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/config/TheiaConfiguration.java create mode 100644 src/main/java/de/tum/in/www1/artemis/web/rest/theia/TheiaConfigurationResource.java create mode 100644 src/main/webapp/app/exercises/programming/manage/update/update-components/theia/programming-exercise-theia.component.html create mode 100644 src/main/webapp/app/exercises/programming/manage/update/update-components/theia/programming-exercise-theia.component.ts create mode 100644 src/main/webapp/app/exercises/programming/shared/service/theia.service.ts create mode 100644 src/test/java/de/tum/in/www1/artemis/config/TheiaConfigurationTest.java create mode 100644 src/test/javascript/spec/component/programming-exercise/update-components/theia/programming-exercise-theia.component.spec.ts create mode 100644 src/test/resources/config/application-theia.yml diff --git a/src/main/java/de/tum/in/www1/artemis/ArtemisApp.java b/src/main/java/de/tum/in/www1/artemis/ArtemisApp.java index e64bb62fccbb..674012737006 100644 --- a/src/main/java/de/tum/in/www1/artemis/ArtemisApp.java +++ b/src/main/java/de/tum/in/www1/artemis/ArtemisApp.java @@ -19,11 +19,12 @@ import org.springframework.core.env.Environment; import de.tum.in.www1.artemis.config.ProgrammingLanguageConfiguration; +import de.tum.in.www1.artemis.config.TheiaConfiguration; import tech.jhipster.config.DefaultProfileUtil; import tech.jhipster.config.JHipsterConstants; @SpringBootApplication -@EnableConfigurationProperties({ LiquibaseProperties.class, ProgrammingLanguageConfiguration.class }) +@EnableConfigurationProperties({ LiquibaseProperties.class, ProgrammingLanguageConfiguration.class, TheiaConfiguration.class }) public class ArtemisApp { private static final Logger log = LoggerFactory.getLogger(ArtemisApp.class); diff --git a/src/main/java/de/tum/in/www1/artemis/config/TheiaConfiguration.java b/src/main/java/de/tum/in/www1/artemis/config/TheiaConfiguration.java new file mode 100644 index 000000000000..02a50a5201ed --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/config/TheiaConfiguration.java @@ -0,0 +1,43 @@ +package de.tum.in.www1.artemis.config; + +import static de.tum.in.www1.artemis.config.Constants.PROFILE_THEIA; + +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; + +@Profile(PROFILE_THEIA) +@Configuration +@ConfigurationProperties(prefix = "theia") +public class TheiaConfiguration { + + private Map> images; + + public void setImages(final Map> images) { + this.images = images; + } + + /** + * Get the images for all languages + * + * @return a map of language -> [flavor/name -> image-link] + */ + public Map> getImagesForAllLanguages() { + return images; + } + + /** + * Get the images for a specific language + * + * @param language the language for which the images should be retrieved + * @return a map of flavor/name -> image-link + */ + public Map getImagesForLanguage(ProgrammingLanguage language) { + return images.get(language); + } + +} diff --git a/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java b/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java index b44ce90a2e67..ccb797194f8d 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java @@ -732,8 +732,9 @@ public String toString() { public void validateProgrammingSettings() { // Check if a participation mode was selected - if (!Boolean.TRUE.equals(isAllowOnlineEditor()) && !Boolean.TRUE.equals(isAllowOfflineIde())) { - throw new BadRequestAlertException("You need to allow at least one participation mode, the online editor or the offline IDE", "Exercise", "noParticipationModeAllowed"); + if (!Boolean.TRUE.equals(isAllowOnlineEditor()) && !Boolean.TRUE.equals(isAllowOfflineIde()) && !isAllowOnlineIde()) { + throw new BadRequestAlertException("You need to allow at least one participation mode, the online editor, the offline IDE, or the online IDE", "Exercise", + "noParticipationModeAllowed"); } // Check if Xcode has no online code editor enabled @@ -745,6 +746,11 @@ public void validateProgrammingSettings() { if (getProgrammingLanguage() == null) { throw new BadRequestAlertException("No programming language was specified", "Exercise", "programmingLanguageNotSet"); } + + // Check if theia image was selected if the online IDE is enabled + if (isAllowOnlineIde() && buildConfig.getTheiaImage() == null) { + throw new BadRequestAlertException("The Theia image must be selected if the online IDE is enabled", "Exercise", "theiaImageNotSet"); + } } /** diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java index 180d2ca8e077..2620f59f6a5d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/programming/ProgrammingExerciseResource.java @@ -1,6 +1,7 @@ package de.tum.in.www1.artemis.web.rest.programming; import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; +import static de.tum.in.www1.artemis.config.Constants.PROFILE_THEIA; import java.io.IOException; import java.net.URI; @@ -18,6 +19,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; +import org.springframework.core.env.Environment; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -89,6 +91,7 @@ import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; import de.tum.in.www1.artemis.web.rest.util.HeaderUtil; import de.tum.in.www1.artemis.web.websocket.dto.ProgrammingExerciseTestCaseStateDTO; +import io.jsonwebtoken.lang.Arrays; /** * REST controller for managing ProgrammingExercise. @@ -151,6 +154,8 @@ public class ProgrammingExerciseResource { private final Optional athenaModuleService; + private final Environment environment; + public ProgrammingExerciseResource(ProgrammingExerciseRepository programmingExerciseRepository, ProgrammingExerciseTestCaseRepository programmingExerciseTestCaseRepository, UserRepository userRepository, AuthorizationCheckService authCheckService, CourseService courseService, Optional continuousIntegrationService, Optional versionControlService, ExerciseService exerciseService, @@ -160,7 +165,8 @@ public ProgrammingExerciseResource(ProgrammingExerciseRepository programmingExer GradingCriterionRepository gradingCriterionRepository, CourseRepository courseRepository, GitService gitService, AuxiliaryRepositoryService auxiliaryRepositoryService, SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository, TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository, - BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository, ChannelRepository channelRepository, Optional athenaModuleService) { + BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository, ChannelRepository channelRepository, Optional athenaModuleService, + Environment environment) { this.programmingExerciseTaskService = programmingExerciseTaskService; this.programmingExerciseRepository = programmingExerciseRepository; this.programmingExerciseTestCaseRepository = programmingExerciseTestCaseRepository; @@ -184,6 +190,7 @@ public ProgrammingExerciseResource(ProgrammingExerciseRepository programmingExer this.buildLogStatisticsEntryRepository = buildLogStatisticsEntryRepository; this.channelRepository = channelRepository; this.athenaModuleService = athenaModuleService; + this.environment = environment; } /** @@ -303,9 +310,26 @@ public ResponseEntity updateProgrammingExercise(@RequestBod updatedProgrammingExercise.getBuildConfig().isTestwiseCoverageEnabled())) { throw new BadRequestAlertException("Testwise coverage enabled flag must not be changed", ENTITY_NAME, "testwiseCoverageCannotChange"); } - if (!Boolean.TRUE.equals(updatedProgrammingExercise.isAllowOnlineEditor()) && !Boolean.TRUE.equals(updatedProgrammingExercise.isAllowOfflineIde())) { - return ResponseEntity.badRequest().headers(HeaderUtil.createAlert(applicationName, - "You need to allow at least one participation mode, the online editor or the offline IDE", "noParticipationModeAllowed")).body(null); + // Check if theia Profile is enabled + if (Arrays.asList(this.environment.getActiveProfiles()).contains(PROFILE_THEIA)) { + // Require 1 / 3 participation modes to be enabled + if (!Boolean.TRUE.equals(updatedProgrammingExercise.isAllowOnlineEditor()) && !Boolean.TRUE.equals(updatedProgrammingExercise.isAllowOfflineIde()) + && !updatedProgrammingExercise.isAllowOnlineIde()) { + throw new BadRequestAlertException("You need to allow at least one participation mode, the online editor, the offline IDE, or the online IDE", ENTITY_NAME, + "noParticipationModeAllowed"); + } + } + else { + // Require 1 / 2 participation modes to be enabled + if (!Boolean.TRUE.equals(updatedProgrammingExercise.isAllowOnlineEditor()) && !Boolean.TRUE.equals(updatedProgrammingExercise.isAllowOfflineIde())) { + throw new BadRequestAlertException("You need to allow at least one participation mode, the online editor or the offline IDE", ENTITY_NAME, + "noParticipationModeAllowed"); + } + } + + // Verify that a theia image is provided when the online IDE is enabled + if (updatedProgrammingExercise.isAllowOnlineIde() && updatedProgrammingExercise.getBuildConfig().getTheiaImage() == null) { + throw new BadRequestAlertException("You need to provide a Theia image when the online IDE is enabled", ENTITY_NAME, "noTheiaImageProvided"); } // Forbid changing the course the exercise belongs to. if (!Objects.equals(programmingExerciseBeforeUpdate.getCourseViaExerciseGroupOrCourseMember().getId(), diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/theia/TheiaConfigurationResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/theia/TheiaConfigurationResource.java new file mode 100644 index 000000000000..24a6977afa5b --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/theia/TheiaConfigurationResource.java @@ -0,0 +1,41 @@ +package de.tum.in.www1.artemis.web.rest.theia; + +import static de.tum.in.www1.artemis.config.Constants.PROFILE_THEIA; + +import java.util.Map; + +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import de.tum.in.www1.artemis.config.TheiaConfiguration; +import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; +import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; + +@Profile(PROFILE_THEIA) +@RestController +@RequestMapping("api/theia/") +public class TheiaConfigurationResource { + + private final TheiaConfiguration theiaConfiguration; + + public TheiaConfigurationResource(TheiaConfiguration theiaConfiguration) { + this.theiaConfiguration = theiaConfiguration; + } + + /** + * GET /api/theia/images?language=: Get the images for a specific language + * + * @param language the language for which the images should be retrieved + * @return a map of flavor/name -> image-link + */ + @GetMapping("images") + @EnforceAtLeastInstructor + public ResponseEntity> getImagesForLanguage(@RequestParam("language") ProgrammingLanguage language) { + return ResponseEntity.ok(this.theiaConfiguration.getImagesForLanguage(language)); + } + +} diff --git a/src/main/resources/config/application-dev.yml b/src/main/resources/config/application-dev.yml index 242f427c6a8e..ec007df2f264 100644 --- a/src/main/resources/config/application-dev.yml +++ b/src/main/resources/config/application-dev.yml @@ -129,4 +129,13 @@ eureka: # Theia configuration theia: - portal-url: https://theia-test.k8s.ase.cit.tum.de + portal-url: https://theia-test.k8s.ase.cit.tum.de + + images: + java: + Java-17: "ghcr.io/ls1intum/theia/java-17:latest" + Java-Test: "ghcr.io/ls1intum/theia/java-test:latest" + Java-Test2: "ghcr.io/ls1intum/theia/java-test:2" + c: + C: "ghcr.io/ls1intum/theia/c:latest" + diff --git a/src/main/resources/config/application-theia.yml b/src/main/resources/config/application-theia.yml index 2b8e1346d008..95b058f66e20 100644 --- a/src/main/resources/config/application-theia.yml +++ b/src/main/resources/config/application-theia.yml @@ -1,2 +1,13 @@ theia: - portal-url: https://your-theia-instance.com + portal-url: https://your-theia-instance.com + + # Theia IDE images available for the different programming languages + images: + # Upper level key is the language category (must match the language key in the programming-exercise configuration) + java: + # Lower level key can be multiple flavors of the image, e.g. version, tag, or additional dependencies + Java-17: "my-registry/my-image:my-tag" + # Add more flavors here (e.g. Java-11, Java-8, etc.) + # Add more languages here (e.g. c, python, etc.) + c: + C: "my-registry/my-image:my-tag" diff --git a/src/main/webapp/app/entities/programming-exercise.model.ts b/src/main/webapp/app/entities/programming-exercise.model.ts index a2e6a093f9a1..daf1bf77e907 100644 --- a/src/main/webapp/app/entities/programming-exercise.model.ts +++ b/src/main/webapp/app/entities/programming-exercise.model.ts @@ -68,6 +68,7 @@ export class ProgrammingExerciseBuildConfig { public dockerFlags?: string; public windfile?: WindFile; public testwiseCoverageEnabled?: boolean; + public theiaImage?: string; constructor() { this.checkoutSolutionRepository = false; // default value @@ -114,6 +115,7 @@ export class ProgrammingExercise extends Exercise { */ public maxStaticCodeAnalysisPenalty?: number; public allowOfflineIde?: boolean; + public allowOnlineIde?: boolean; public programmingLanguage?: ProgrammingLanguage; public packageName?: string; public showTestNamesToStudents?: boolean; @@ -148,6 +150,7 @@ export class ProgrammingExercise extends Exercise { this.templateParticipation = new TemplateProgrammingExerciseParticipation(); this.solutionParticipation = new SolutionProgrammingExerciseParticipation(); this.allowOnlineEditor = false; // default value + this.allowOnlineIde = false; // default value this.staticCodeAnalysisEnabled = false; // default value this.allowOfflineIde = true; // default value this.programmingLanguage = ProgrammingLanguage.JAVA; // default value 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 7194d5a9f141..e59af00e81b9 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,12 +84,13 @@ @if (displayEditorModus) {
- {{ 'artemisApp.programmingExercise.offlineIde' | artemisTranslate }} - : {{ programmingExercise.allowOfflineIde || false }} + : {{ programmingExercise.allowOfflineIde || false }}
- {{ 'artemisApp.programmingExercise.onlineEditor' | artemisTranslate }} - : {{ programmingExercise.allowOnlineEditor || false }} + : {{ programmingExercise.allowOnlineEditor || false }} +
+
+ : {{ programmingExercise.allowOnlineIde || false }}
} diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts index a2f1e2174227..6576932d20e1 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.ts @@ -332,12 +332,17 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { { type: DetailType.Boolean, title: 'artemisApp.programmingExercise.allowOfflineIde.title', - data: { boolean: exercise.allowOfflineIde }, + data: { boolean: exercise.allowOfflineIde ?? false }, }, { type: DetailType.Boolean, title: 'artemisApp.programmingExercise.allowOnlineEditor.title', - data: { boolean: exercise.allowOnlineEditor }, + data: { boolean: exercise.allowOnlineEditor ?? false }, + }, + { + type: DetailType.Boolean, + title: 'artemisApp.programmingExercise.allowOnlineIde.title', + data: { boolean: exercise.allowOnlineIde ?? false }, }, ], }; 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 89d72f95b7ef..39f4a28bbff0 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,12 +139,16 @@ }
- {{ 'artemisApp.programmingExercise.offlineIde' | artemisTranslate }}: - {{ programmingExercise.allowOfflineIde ? ('artemisApp.exercise.yes' | artemisTranslate) : ('artemisApp.exercise.no' | artemisTranslate) }} + : +
- {{ 'artemisApp.programmingExercise.onlineEditor' | artemisTranslate }}: - {{ programmingExercise.allowOnlineEditor ? ('artemisApp.exercise.yes' | artemisTranslate) : ('artemisApp.exercise.no' | artemisTranslate) }} + : + +
+
+ : +
@if (course.presentationScore !== 0) { diff --git a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-creation-config.ts b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-creation-config.ts index 6f7e4b3332c3..8ab3314ac388 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-creation-config.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-creation-config.ts @@ -44,6 +44,7 @@ export type ProgrammingExerciseCreationConfig = { hasUnsavedChanges: boolean; rerenderSubject: Observable; validIdeSelection: () => boolean | undefined; + validOnlineIdeSelection: () => boolean | undefined; inProductionEnvironment: boolean; recreateBuildPlans: boolean; onRecreateBuildPlanOrUpdateTemplateChange: () => void; diff --git a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts index e48803fc9c81..5fe22b0b6ff9 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.component.ts @@ -29,7 +29,7 @@ import { ModePickerOption } from 'app/exercises/shared/mode-picker/mode-picker.c import { DocumentationType } from 'app/shared/components/documentation-button/documentation-button.component'; import { ProgrammingExerciseCreationConfig } from 'app/exercises/programming/manage/update/programming-exercise-creation-config'; import { loadCourseExerciseCategories } from 'app/exercises/shared/course-exercises/course-utils'; -import { PROFILE_AEOLUS, PROFILE_LOCALCI } from 'app/app.constants'; +import { PROFILE_AEOLUS, PROFILE_LOCALCI, PROFILE_THEIA } from 'app/app.constants'; import { AeolusService } from 'app/exercises/programming/shared/service/aeolus.service'; import { FormSectionStatus } from 'app/forms/form-status-bar/form-status-bar.component'; import { ProgrammingExerciseInformationComponent } from 'app/exercises/programming/manage/update/update-components/programming-exercise-information.component'; @@ -134,6 +134,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest public auxiliaryRepositoriesSupported = false; public auxiliaryRepositoriesValid = true; public customBuildPlansSupported: string = ''; + public theiaEnabled = false; // Additional options for import // This is a wrapper to allow modifications from the other subcomponents @@ -402,50 +403,52 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest }); // If it is an import from this instance, just get the course, otherwise handle the edit and new cases - this.activatedRoute.url - .pipe( - tap((segments) => { - this.isImportFromExistingExercise = segments.some((segment) => segment.path === 'import'); - this.isImportFromFile = segments.some((segment) => segment.path === 'import-from-file'); - }), - switchMap(() => this.activatedRoute.params), - tap((params) => { - if (this.isImportFromFile) { - this.createProgrammingExerciseForImportFromFile(); - } - if (this.isImportFromExistingExercise) { - this.createProgrammingExerciseForImport(params); - } else { - if (params['courseId'] && params['examId'] && params['exerciseGroupId']) { - this.isExamMode = true; - this.exerciseGroupService.find(params['courseId'], params['examId'], params['exerciseGroupId']).subscribe((res) => { - this.programmingExercise.exerciseGroup = res.body!; - if (!params['exerciseId'] && this.programmingExercise.exerciseGroup.exam?.course?.defaultProgrammingLanguage && !this.isImportFromFile) { - this.selectedProgrammingLanguage = this.programmingExercise.exerciseGroup.exam.course.defaultProgrammingLanguage; + if (this.activatedRoute && this.activatedRoute.url) { + this.activatedRoute.url + .pipe( + tap((segments) => { + this.isImportFromExistingExercise = segments.some((segment) => segment.path === 'import'); + this.isImportFromFile = segments.some((segment) => segment.path === 'import-from-file'); + }), + switchMap(() => this.activatedRoute.params), + tap((params) => { + if (this.isImportFromFile) { + this.createProgrammingExerciseForImportFromFile(); + } + if (this.isImportFromExistingExercise) { + this.createProgrammingExerciseForImport(params); + } else { + if (params['courseId'] && params['examId'] && params['exerciseGroupId']) { + this.isExamMode = true; + this.exerciseGroupService.find(params['courseId'], params['examId'], params['exerciseGroupId']).subscribe((res) => { + this.programmingExercise.exerciseGroup = res.body!; + if (!params['exerciseId'] && this.programmingExercise.exerciseGroup.exam?.course?.defaultProgrammingLanguage && !this.isImportFromFile) { + this.selectedProgrammingLanguage = this.programmingExercise.exerciseGroup.exam.course.defaultProgrammingLanguage; + } + }); + // we need the course id to make the request to the server if it's an import from file + if (this.isImportFromFile) { + this.courseId = params['courseId']; + this.loadCourseExerciseCategories(params['courseId']); } - }); - // we need the course id to make the request to the server if it's an import from file - if (this.isImportFromFile) { + } else if (params['courseId']) { this.courseId = params['courseId']; - this.loadCourseExerciseCategories(params['courseId']); + this.isExamMode = false; + this.courseService.find(this.courseId).subscribe((res) => { + this.programmingExercise.course = res.body!; + if (!params['exerciseId'] && this.programmingExercise.course?.defaultProgrammingLanguage && !this.isImportFromFile) { + this.selectedProgrammingLanguage = this.programmingExercise.course.defaultProgrammingLanguage!; + } + this.exerciseCategories = this.programmingExercise.categories || []; + + this.loadCourseExerciseCategories(this.programmingExercise.course!.id!); + }); } - } else if (params['courseId']) { - this.courseId = params['courseId']; - this.isExamMode = false; - this.courseService.find(this.courseId).subscribe((res) => { - this.programmingExercise.course = res.body!; - if (!params['exerciseId'] && this.programmingExercise.course?.defaultProgrammingLanguage && !this.isImportFromFile) { - this.selectedProgrammingLanguage = this.programmingExercise.course.defaultProgrammingLanguage!; - } - this.exerciseCategories = this.programmingExercise.categories || []; - - this.loadCourseExerciseCategories(this.programmingExercise.course!.id!); - }); } - } - }), - ) - .subscribe(); + }), + ) + .subscribe(); + } // If an exercise is created, load our readme template so the problemStatement is not empty this.selectedProgrammingLanguage = this.programmingExercise.programmingLanguage!; @@ -470,6 +473,9 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest if (profileInfo?.activeProfiles.includes(PROFILE_AEOLUS)) { this.customBuildPlansSupported = PROFILE_AEOLUS; } + if (profileInfo?.activeProfiles.includes(PROFILE_THEIA)) { + this.theiaEnabled = true; + } }); this.defineSupportedProgrammingLanguages(); } @@ -500,13 +506,9 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest }, { title: 'artemisApp.programmingExercise.wizardMode.detailedSteps.languageStepTitle', - valid: this.exerciseLanguageComponent?.formValid ?? false, - }, - { - title: 'artemisApp.programmingExercise.wizardMode.detailedSteps.problemStepTitle', - valid: true, - empty: !this.programmingExercise.problemStatement, + valid: (this.exerciseLanguageComponent?.formValid && this.validOnlineIdeSelection()) ?? false, }, + { title: 'artemisApp.programmingExercise.wizardMode.detailedSteps.problemStepTitle', valid: true, empty: !this.programmingExercise.problemStatement }, { title: 'artemisApp.programmingExercise.wizardMode.detailedSteps.gradingStepTitle', valid: Boolean(this.exerciseGradingComponent?.formValid && (this.isExamMode || this.exercisePlagiarismComponent?.formValid)), @@ -676,8 +678,12 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest private subscribeToSaveResponse(result: Observable>) { result.subscribe({ - next: (response: HttpResponse) => this.onSaveSuccess(response.body!), - error: (error: HttpErrorResponse) => this.onSaveError(error), + next: (response: HttpResponse) => { + this.onSaveSuccess(response.body!); + }, + error: (error: HttpErrorResponse) => { + this.onSaveError(error); + }, }); } @@ -821,10 +827,21 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest } /** - * checking if at least one of Online Editor or Offline Ide is selected + * checking if at least one of Online Editor, Offline Ide, or Online Ide is selected */ validIdeSelection = () => { - return this.programmingExercise?.allowOnlineEditor || this.programmingExercise?.allowOfflineIde; + if (this.theiaEnabled) { + return this.programmingExercise?.allowOnlineEditor || this.programmingExercise?.allowOfflineIde || this.programmingExercise?.allowOnlineIde; + } else { + return this.programmingExercise?.allowOnlineEditor || this.programmingExercise?.allowOfflineIde; + } + }; + + /** + * Checking if the online IDE is selected and a valid image is selected + */ + validOnlineIdeSelection = () => { + return !this.programmingExercise?.allowOnlineIde || this.programmingExercise?.buildConfig!.theiaImage !== undefined; }; isEventInsideTextArea(event: Event): boolean { @@ -846,6 +863,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest this.validateExerciseAuxiliaryRepositories(validationErrorReasons); this.validateExercisePackageName(validationErrorReasons); this.validateExerciseIdeSelection(validationErrorReasons); + this.validateExerciseOnlineIdeSelection(validationErrorReasons); this.validateExercisePoints(validationErrorReasons); this.validateExerciseBonusPoints(validationErrorReasons); this.validateExerciseSCAMaxPenalty(validationErrorReasons); @@ -1044,8 +1062,18 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest private validateExerciseIdeSelection(validationErrorReasons: ValidationReason[]): void { if (!this.validIdeSelection()) { + const translateKey = this.theiaEnabled ? 'artemisApp.programmingExercise.allowOnlineEditor.alert' : 'artemisApp.programmingExercise.allowOnlineEditor.alertNoTheia'; + validationErrorReasons.push({ + translateKey: translateKey, + translateValues: {}, + }); + } + } + + private validateExerciseOnlineIdeSelection(validationErrorReasons: ValidationReason[]): void { + if (!this.validOnlineIdeSelection()) { validationErrorReasons.push({ - translateKey: 'artemisApp.programmingExercise.allowOnlineEditor.alert', + translateKey: 'artemisApp.programmingExercise.theiaImage.alert', translateValues: {}, }); } @@ -1110,6 +1138,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest hasUnsavedChanges: this.hasUnsavedChanges, rerenderSubject: this.rerenderSubject.asObservable(), validIdeSelection: this.validIdeSelection, + validOnlineIdeSelection: this.validOnlineIdeSelection, inProductionEnvironment: this.inProductionEnvironment, recreateBuildPlans: this.importOptions.recreateBuildPlans, onRecreateBuildPlanOrUpdateTemplateChange: this.onRecreateBuildPlanOrUpdateTemplateChange, diff --git a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.module.ts b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.module.ts index 8c5fa972cd92..7ac399d6daeb 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.module.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/programming-exercise-update.module.ts @@ -31,6 +31,7 @@ import { ProgrammingExerciseDockerImageComponent } from 'app/exercises/programmi import { FormsModule } from 'app/forms/forms.module'; import { ProgrammingExerciseBuildPlanCheckoutDirectoriesComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-build-plan-checkout-directories.component'; import { ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component'; +import { ProgrammingExerciseTheiaComponent } from 'app/exercises/programming/manage/update/update-components/theia/programming-exercise-theia.component'; import { MonacoEditorModule } from 'app/shared/monaco-editor/monaco-editor.module'; @NgModule({ @@ -57,6 +58,7 @@ import { MonacoEditorModule } from 'app/shared/monaco-editor/monaco-editor.modul ProgrammingExerciseBuildPlanCheckoutDirectoriesComponent, ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent, MonacoEditorModule, + ProgrammingExerciseTheiaComponent, ], declarations: [ ProgrammingExerciseUpdateComponent, diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-difficulty.component.html b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-difficulty.component.html index addc823095a9..8f66c32f99dd 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-difficulty.component.html +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-difficulty.component.html @@ -27,12 +27,21 @@ /> @if (!programmingExerciseCreationConfig.validIdeSelection()) { - + @if (theiaEnabled) { + + } @else { + + } } @@ -49,12 +58,46 @@ (ngModelChange)="triggerValidation.emit()" /> + @if (!programmingExerciseCreationConfig.validIdeSelection()) { + @if (theiaEnabled) { + + } @else { + + } + } + + + } + + @if (!programmingExercise.exerciseGroup && theiaEnabled) { +
+ diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-difficulty.component.ts b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-difficulty.component.ts index 77e30693bff9..5c3f1eb64302 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-difficulty.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-difficulty.component.ts @@ -1,15 +1,17 @@ -import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { ProgrammingExercise, ProjectType } from 'app/entities/programming-exercise.model'; import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import { ProgrammingExerciseCreationConfig } from 'app/exercises/programming/manage/update/programming-exercise-creation-config'; import { TeamConfigFormGroupComponent } from 'app/exercises/shared/team-config-form-group/team-config-form-group.component'; +import { PROFILE_THEIA } from 'app/app.constants'; +import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; @Component({ selector: 'jhi-programming-exercise-difficulty', templateUrl: './programming-exercise-difficulty.component.html', styleUrls: ['../../programming-exercise-form.scss'], }) -export class ProgrammingExerciseDifficultyComponent { +export class ProgrammingExerciseDifficultyComponent implements OnInit { @Input() programmingExercise: ProgrammingExercise; @Input() programmingExerciseCreationConfig: ProgrammingExerciseCreationConfig; @ViewChild(TeamConfigFormGroupComponent) teamConfigComponent: TeamConfigFormGroupComponent; @@ -18,5 +20,15 @@ export class ProgrammingExerciseDifficultyComponent { protected readonly ProjectType = ProjectType; + theiaEnabled: boolean = false; + + constructor(private profileService: ProfileService) {} + + ngOnInit(): void { + this.profileService.getProfileInfo().subscribe((profileInfo) => { + this.theiaEnabled = profileInfo.activeProfiles?.includes(PROFILE_THEIA); + }); + } + faQuestionCircle = faQuestionCircle; } diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.html b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.html index 1e4c258dfd16..e801f1a04d85 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.html +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.html @@ -111,6 +111,13 @@ }
} + + + @if (programmingExercise.allowOnlineIde && programmingExercise.programmingLanguage) { + + + } + @if (programmingExercise.programmingLanguage && programmingExerciseCreationConfig.staticCodeAnalysisAllowed) {
diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.ts b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.ts index 51bb90eb46d8..5bcf49abc770 100644 --- a/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.ts +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/programming-exercise-language.component.ts @@ -7,6 +7,7 @@ import { NgModel } from '@angular/forms'; import { Subject, Subscription } from 'rxjs'; import { ProgrammingExerciseCustomAeolusBuildPlanComponent } from 'app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-aeolus-build-plan.component'; import { ProgrammingExerciseCustomBuildPlanComponent } from 'app/exercises/programming/manage/update/update-components/custom-build-plans/programming-exercise-custom-build-plan.component'; +import { ProgrammingExerciseTheiaComponent } from 'app/exercises/programming/manage/update/update-components/theia/programming-exercise-theia.component'; @Component({ selector: 'jhi-programming-exercise-language', @@ -24,6 +25,7 @@ export class ProgrammingExerciseLanguageComponent implements AfterViewChecked, A @ViewChild('packageName') packageNameField?: NgModel; @ViewChild(ProgrammingExerciseCustomAeolusBuildPlanComponent) programmingExerciseCustomAeolusBuildPlanComponent?: ProgrammingExerciseCustomAeolusBuildPlanComponent; @ViewChild(ProgrammingExerciseCustomBuildPlanComponent) programmingExerciseCustomBuildPlanComponent?: ProgrammingExerciseCustomBuildPlanComponent; + @ViewChild(ProgrammingExerciseTheiaComponent) programmingExerciseTheiaComponent?: ProgrammingExerciseTheiaComponent; formValid: boolean; formValidChanges = new Subject(); diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/theia/programming-exercise-theia.component.html b/src/main/webapp/app/exercises/programming/manage/update/update-components/theia/programming-exercise-theia.component.html new file mode 100644 index 000000000000..96d23fa998e8 --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/theia/programming-exercise-theia.component.html @@ -0,0 +1,21 @@ +
+ +
diff --git a/src/main/webapp/app/exercises/programming/manage/update/update-components/theia/programming-exercise-theia.component.ts b/src/main/webapp/app/exercises/programming/manage/update/update-components/theia/programming-exercise-theia.component.ts new file mode 100644 index 000000000000..98849723625d --- /dev/null +++ b/src/main/webapp/app/exercises/programming/manage/update/update-components/theia/programming-exercise-theia.component.ts @@ -0,0 +1,82 @@ +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { ProgrammingExercise, ProgrammingLanguage } from 'app/entities/programming-exercise.model'; +import { ProgrammingExerciseCreationConfig } from 'app/exercises/programming/manage/update/programming-exercise-creation-config'; +import { TheiaService } from 'app/exercises/programming/shared/service/theia.service'; +import { ArtemisSharedLibsModule } from 'app/shared/shared-libs.module'; + +@Component({ + selector: 'jhi-programming-exercise-theia', + templateUrl: './programming-exercise-theia.component.html', + styleUrls: ['../../../programming-exercise-form.scss'], + standalone: true, + imports: [ArtemisSharedLibsModule], +}) +export class ProgrammingExerciseTheiaComponent implements OnChanges { + @Input() programmingExercise: ProgrammingExercise; + @Input() programmingExerciseCreationConfig: ProgrammingExerciseCreationConfig; + + programmingLanguage?: ProgrammingLanguage; + theiaImages = {}; + + constructor(private theiaService: TheiaService) {} + + ngOnChanges(changes: SimpleChanges) { + if ((changes.programmingExerciseCreationConfig || changes.programmingExercise) && this.shouldReloadTemplate()) { + this.loadTheiaImages(); + } + } + + onTheiaImageChange(theiaImage: string) { + if (this.programmingExercise.buildConfig) { + this.programmingExercise.buildConfig.theiaImage = theiaImage; + } + } + + shouldReloadTemplate(): boolean { + return this.programmingExercise.programmingLanguage !== this.programmingLanguage; + } + + /** + * In case the programming language or project type changes, we need to reset the template and the build plan + * @private + */ + resetImageSelection() { + if (this.programmingExercise.buildConfig) { + this.programmingExercise.buildConfig.theiaImage = undefined; + } + } + + /** + * Loads the predefined template for the selected programming language if there is one available. + * @private + */ + loadTheiaImages() { + if (!this.programmingExercise || !this.programmingExercise.programmingLanguage) { + return; + } + + this.programmingLanguage = this.programmingExercise.programmingLanguage; + + this.theiaService.getTheiaImages(this.programmingLanguage).subscribe({ + next: (images) => { + if (!images) { + // Remove selection if no image is available + this.theiaImages = {}; + this.resetImageSelection(); + return; + } + + this.theiaImages = images; + + // Set the first image as default if none is selected + if (this.programmingExercise && this.programmingExercise.buildConfig && !this.programmingExercise.buildConfig.theiaImage && Object.values(images).length > 0) { + this.programmingExercise.buildConfig.theiaImage = Object.values(images).first() as string; + } + }, + error: () => { + this.theiaImages = {}; + this.resetImageSelection(); + }, + }); + } +} diff --git a/src/main/webapp/app/exercises/programming/shared/service/theia.service.ts b/src/main/webapp/app/exercises/programming/shared/service/theia.service.ts new file mode 100644 index 000000000000..d59673d738ac --- /dev/null +++ b/src/main/webapp/app/exercises/programming/shared/service/theia.service.ts @@ -0,0 +1,25 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { ProgrammingLanguage } from 'app/entities/programming-exercise.model'; + +@Injectable({ providedIn: 'root' }) +export class TheiaService { + private resourceUrl = 'api/theia'; + + constructor(private http: HttpClient) {} + + /** + * Fetches the theia images for the given programming language + * @param {ProgrammingLanguage} language + * @returns the theia images or undefined if no images are available for this language + */ + getTheiaImages(language: ProgrammingLanguage): Observable<{ [key: string]: string } | undefined> { + return this.http.get<{ [key: string]: string }>(`${this.resourceUrl}/images`, { + params: { + language: language, + }, + }); + } +} diff --git a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts index f2e251a81804..87e74ad54d11 100644 --- a/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts +++ b/src/main/webapp/app/overview/exercise-details/exercise-details-student-actions.component.ts @@ -61,14 +61,14 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges theiaPortalURL: string; // Icons - faFolderOpen = faFolderOpen; - faUsers = faUsers; - faEye = faEye; - faPlayCircle = faPlayCircle; - faRedo = faRedo; - faCodeBranch = faCodeBranch; - faDesktop = faDesktop; - faPenSquare = faPenSquare; + readonly faFolderOpen = faFolderOpen; + readonly faUsers = faUsers; + readonly faEye = faEye; + readonly faPlayCircle = faPlayCircle; + readonly faRedo = faRedo; + readonly faCodeBranch = faCodeBranch; + readonly faDesktop = faDesktop; + readonly faPenSquare = faPenSquare; private feedbackSent = false; @@ -109,7 +109,7 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges this.athenaEnabled = profileInfo.activeProfiles?.includes(PROFILE_ATHENA); // The online IDE is only available with correct SpringProfile and if it's enabled for this exercise - if (profileInfo.activeProfiles?.includes(PROFILE_THEIA)) { + if (profileInfo.activeProfiles?.includes(PROFILE_THEIA) && this.programmingExercise) { this.theiaEnabled = true; // Set variables now, sanitize later on @@ -119,6 +119,16 @@ export class ExerciseDetailsStudentActionsComponent implements OnInit, OnChanges if (this.theiaPortalURL === '') { this.theiaEnabled = false; } + + // Verify that the exercise allows the online IDE + if (!this.programmingExercise.allowOnlineIde) { + this.theiaEnabled = false; + } + + // Verify that the exercise has a theia blueprint configured + if (!this.programmingExercise.buildConfig?.theiaImage) { + this.theiaEnabled = false; + } } }); } else if (this.exercise.type === ExerciseType.MODELING) { diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json index ae25116ddc7a..9409c371c61a 100644 --- a/src/main/webapp/i18n/de/programmingExercise.json +++ b/src/main/webapp/i18n/de/programmingExercise.json @@ -118,14 +118,22 @@ "workdir": "Verzeichnis", "allowOnlineEditor": { "title": "Online-Editor erlauben", - "alert": "Es muss mindestens eine Option (Offline-IDE oder Online-Editor) ausgewählt sein" + "alert": "Es muss mindestens eine Option (Offline-IDE, Online-Editor oder Online-IDE) ausgewählt sein", + "alertNoTheia": "Es muss mindestens eine Option (Offline-IDE oder Online-Editor) ausgewählt sein" }, "onlineEditor": "Online", "allowOfflineIde": { "title": "Offline-IDE erlauben", - "alert": "Es muss mindestens eine Option (Offline-IDE oder Online-Editor) ausgewählt sein" + "alert": "Es muss mindestens eine Option (Offline-IDE, Online-Editor oder Online-IDE) ausgewählt sein", + "alertNoTheia": "Es muss mindestens eine Option (Offline-IDE oder Online-Editor) ausgewählt sein" }, "offlineIde": "IDE", + "allowOnlineIde": { + "title": "Online-IDE erlauben", + "alert": "Es muss mindestens eine Option (Offline-IDE, Online-Editor oder Online-IDE) ausgewählt sein.", + "alertNoTheia": "Es muss mindestens eine Option (Offline-IDE oder Online-Editor) ausgewählt sein" + }, + "onlineIde": "Online IDE", "showTestNamesToStudents": "Zeige die Test Namen den Studierenden", "showTestNamesToStudentsTooltip": "Durch Aktivierung dieser Option werden die Namen der automatischen Tests den Studierenden angezeigt. Lasse die Option deaktiviert, um keine visuelle Unterscheidung zwischen manuellem und automatischem Feedback für die Studierenden vorzunehmen.", "participationMode": "Teilnahmemodus", @@ -170,6 +178,11 @@ "projectType": "Projekttyp", "testRepositoryProjectType": "Projekttyp des Test-Repository", "packageName": "Package-Name", + "theiaImage": { + "title": "Konfiguration für Online IDE", + "noImageAvailable": "Die Online IDE ist für diese Programmiersprache noch nicht verfügbar.", + "alert": "Es muss eine gültige Konfiguration für die Online IDE ausgewählt werden." + }, "appName": "App-Name", "templateResult": "Ergebnis der Vorlage", "solutionResult": "Ergebnis der Musterlösung", diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json index e199355c4a82..06f826607ba7 100644 --- a/src/main/webapp/i18n/en/programmingExercise.json +++ b/src/main/webapp/i18n/en/programmingExercise.json @@ -130,14 +130,22 @@ "customizeDockerImage": "You can customize the Docker image. Make sure to provide it in amd64 and arm64 and include all build dependencies to guarantee a short build duration.", "allowOnlineEditor": { "title": "Allow Online Editor", - "alert": "At least one option (Offline IDE or Online Editor) must be selected" + "alert": "At least one option (Offline IDE, Online Editor, or Online IDE) must be selected", + "alertNoTheia": "At least one option (Offline IDE or Online Editor) must be selected" }, "onlineEditor": "Online", "allowOfflineIde": { "title": "Allow Offline IDE", - "alert": "At least one option (Offline IDE or Online Editor) must be selected" + "alert": "At least one option (Offline IDE, Online Editor, or Online IDE) must be selected", + "alertNoTheia": "At least one option (Offline IDE or Online Editor) must be selected" }, "offlineIde": "IDE", + "allowOnlineIde": { + "title": "Allow Online IDE", + "alert": "At least one option (Offline IDE, Online Editor, or Online IDE) must be selected.", + "alertNoTheia": "At least one option (Offline IDE or Online Editor) must be selected" + }, + "onlineIde": "Online IDE", "showTestNamesToStudents": "Show Test Names to Students", "showTestNamesToStudentsTooltip": "Activate this option to show the names of the automated test cases to the students. Leave the option disabled to make no visual distinction between manual and automated feedback for the students.", "participationMode": "Participation Mode", @@ -172,6 +180,11 @@ "projectType": "Project Type", "testRepositoryProjectType": "Test Repository Project Type", "packageName": "Package Name", + "theiaImage": { + "title": "Configuration for Online IDE", + "noImageAvailable": "The Online IDE is not yet available for this programming language.", + "alert": "A valid configuration for the Online IDE must be selected." + }, "appName": "App Name", "templateResult": "Template Result", "solutionResult": "Solution Result", diff --git a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationIndependentTest.java b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationIndependentTest.java index c72bb2a37e0f..d2b41249721c 100644 --- a/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationIndependentTest.java +++ b/src/test/java/de/tum/in/www1/artemis/AbstractSpringIntegrationIndependentTest.java @@ -2,6 +2,7 @@ import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; import static de.tum.in.www1.artemis.config.Constants.PROFILE_SCHEDULING; +import static de.tum.in.www1.artemis.config.Constants.PROFILE_THEIA; import static tech.jhipster.config.JHipsterConstants.SPRING_PROFILE_TEST; import java.util.Set; @@ -34,7 +35,7 @@ */ @ResourceLock("AbstractSpringIntegrationIndependentTest") // NOTE: we use a common set of active profiles to reduce the number of application launches during testing. This significantly saves time and memory! -@ActiveProfiles({ SPRING_PROFILE_TEST, "artemis", PROFILE_SCHEDULING, "athena", "apollon", "lti", "aeolus", PROFILE_CORE }) +@ActiveProfiles({ SPRING_PROFILE_TEST, "artemis", PROFILE_SCHEDULING, "athena", "apollon", "lti", "aeolus", PROFILE_THEIA, PROFILE_CORE }) @TestPropertySource(properties = { "artemis.user-management.use-external=false" }) public abstract class AbstractSpringIntegrationIndependentTest extends AbstractArtemisIntegrationTest { diff --git a/src/test/java/de/tum/in/www1/artemis/config/TheiaConfigurationTest.java b/src/test/java/de/tum/in/www1/artemis/config/TheiaConfigurationTest.java new file mode 100644 index 000000000000..41852515b971 --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/config/TheiaConfigurationTest.java @@ -0,0 +1,41 @@ +package de.tum.in.www1.artemis.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; +import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; + +class TheiaConfigurationTest extends AbstractSpringIntegrationIndependentTest { + + @Autowired + private TheiaConfiguration theiaConfiguration; + + @Test + void testAutowired() { + assertThat(theiaConfiguration).isNotNull(); + } + + @Test + void testAmountOfLanguageImages() { + assertThat(theiaConfiguration.getImagesForAllLanguages()).hasSize(2); + } + + @Test + void testFlavorsForLanguage() { + Map images = theiaConfiguration.getImagesForLanguage(ProgrammingLanguage.valueOf("JAVA")); + assertThat(images).hasSize(2); + assertThat(images).containsKey("Java-17"); + assertThat(images).containsValue("ghcr.io/ls1intum/theia/java-17:latest"); + assertThat(images.get("Java-Non-Existent")).isEqualTo("this-is-not-a-valid-image"); + } + + @Test + void testNonExistentLanguage() { + assertThat(theiaConfiguration.getImagesForLanguage(null)).isNull(); + } +} diff --git a/src/test/java/de/tum/in/www1/artemis/theia/TheiaInfoContributorTest.java b/src/test/java/de/tum/in/www1/artemis/theia/TheiaInfoContributorTest.java index 04c75c487a6f..8b3b6738b219 100644 --- a/src/test/java/de/tum/in/www1/artemis/theia/TheiaInfoContributorTest.java +++ b/src/test/java/de/tum/in/www1/artemis/theia/TheiaInfoContributorTest.java @@ -23,14 +23,9 @@ class TheiaInfoContributorTest { void testContribute() { Info.Builder builder = new Info.Builder(); theiaInfoContributor = new TheiaInfoContributor(); - try { - theiaInfoContributor.contribute(builder); - } - catch (NullPointerException e) { - } + theiaInfoContributor.contribute(builder); Info info = builder.build(); assertThat(info.getDetails().get(Constants.THEIA_PORTAL_URL)).isEqualTo(expectedValue); - } } 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 fe38c8c71ae0..43d15f8f8634 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 @@ -30,6 +30,7 @@ describe('Programming Exercise Group Cell Component', () => { }, allowOfflineIde: true, allowOnlineEditor: true, + allowOnlineIde: false, } as any as ProgrammingExercise; let mockedProfileService: ProfileService; @@ -93,11 +94,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('artemisApp.programmingExercise.offlineIde : true'); + expect(div0.nativeElement.textContent?.trim()).toBe(': true'); const div1 = fixture.debugElement.query(By.css('div > div > div:nth-child(2)')); expect(div1).not.toBeNull(); - expect(div1.nativeElement.textContent?.trim()).toBe('artemisApp.programmingExercise.onlineEditor : true'); + expect(div1.nativeElement.textContent?.trim()).toBe(': true'); + + const div2 = fixture.debugElement.query(By.css('div > div > div:nth-child(3)')); + expect(div2).not.toBeNull(); + expect(div2.nativeElement.textContent?.trim()).toBe(': false'); }); it('should download the repository', () => { diff --git a/src/test/javascript/spec/component/overview/exercise-details/exercise-details-student-actions.component.spec.ts b/src/test/javascript/spec/component/overview/exercise-details/exercise-details-student-actions.component.spec.ts index 9a0a2327c015..1a770b4e5002 100644 --- a/src/test/javascript/spec/component/overview/exercise-details/exercise-details-student-actions.component.spec.ts +++ b/src/test/javascript/spec/component/overview/exercise-details/exercise-details-student-actions.component.spec.ts @@ -55,6 +55,7 @@ describe('ExerciseDetailsStudentActionsComponent', () => { secondCorrectionEnabled: false, studentAssignedTeamIdComputed: false, }; + const teamExerciseWithoutTeamAssigned: Exercise = { ...exercise, mode: ExerciseMode.TEAM, @@ -607,18 +608,58 @@ describe('ExerciseDetailsStudentActionsComponent', () => { it.each([ [ - 'start theia button should be visible when profile is active and url is set', + 'start theia button should be visible when profile is active and theia is configured', { activeProfiles: [PROFILE_THEIA], theiaPortalURL: 'https://theia.test', }, + { + allowOnlineIde: true, + }, + { + theiaImage: 'this-is-a-theia-image', + }, true, ], + [ + 'start theia button should not be visible when profile is active but theia is ill-configured', + { + activeProfiles: [PROFILE_THEIA], + theiaPortalURL: 'https://theia.test', + }, + { + allowOnlineIde: true, + }, + { + theiaImage: undefined, + }, + false, + ], + [ + 'start theia button should not be visible when profile is active but onlineIde is not activated', + { + activeProfiles: [PROFILE_THEIA], + theiaPortalURL: 'https://theia.test', + }, + { + allowOnlineIde: false, + }, + { + theiaImage: 'this-is-an-old-image', + }, + false, + ], [ 'start theia button should not be visible when profile is active but url is not set', { activeProfiles: [PROFILE_THEIA], }, + { + allowOnlineIde: true, + }, + { + theiaImage: 'this-is-a-theia-image', + }, false, ], [ @@ -626,12 +667,20 @@ describe('ExerciseDetailsStudentActionsComponent', () => { { theiaPortalURL: 'https://theia.test', }, + { + allowOnlineIde: true, + }, + { + theiaImage: 'this-is-a-theia-image', + }, false, ], - ])('%s', (description, profileInfo, expectedVisibility) => { + ])('%s', (description, profileInfo, programmingExercise, buildConfig, expectedVisibility) => { getProfileInfoSub = jest.spyOn(profileService, 'getProfileInfo'); getProfileInfoSub.mockReturnValue(of(profileInfo as ProfileInfo)); - comp.exercise = exercise; + + // Expand the programmingExercise by given properties + comp.exercise = { ...exercise, ...programmingExercise, buildConfig: buildConfig } as ProgrammingExercise; fixture.detectChanges(); diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts index c4f40f4b79dc..5441500fe1a3 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-update.component.spec.ts @@ -67,6 +67,9 @@ import { AlertService, AlertType } from 'app/core/util/alert.service'; import { FormStatusBarComponent } from 'app/forms/form-status-bar/form-status-bar.component'; import { FormFooterComponent } from 'app/forms/form-footer/form-footer.component'; import { ProgrammingExerciseRepositoryAndBuildPlanDetailsComponent } from 'app/exercises/programming/shared/build-details/programming-exercise-repository-and-build-plan-details.component'; +import { ProfileInfo } from 'app/shared/layouts/profiles/profile-info.model'; +import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; +import { PROFILE_THEIA } from 'app/app.constants'; describe('ProgrammingExerciseUpdateComponent', () => { const courseId = 1; @@ -80,6 +83,9 @@ describe('ProgrammingExerciseUpdateComponent', () => { let exerciseGroupService: ExerciseGroupService; let programmingExerciseFeatureService: ProgrammingLanguageFeatureService; let alertService: AlertService; + let profileService: ProfileService; + + let getProfileInfoSub: jest.SpyInstance; beforeEach(() => { TestBed.configureTestingModule({ @@ -147,6 +153,12 @@ describe('ProgrammingExerciseUpdateComponent', () => { exerciseGroupService = debugElement.injector.get(ExerciseGroupService); programmingExerciseFeatureService = debugElement.injector.get(ProgrammingLanguageFeatureService); alertService = debugElement.injector.get(AlertService); + profileService = debugElement.injector.get(ProfileService); + + getProfileInfoSub = jest.spyOn(profileService, 'getProfileInfo'); + const newProfileInfo = new ProfileInfo(); + newProfileInfo.activeProfiles = []; + getProfileInfoSub.mockReturnValue(of(newProfileInfo)); }); }); @@ -792,11 +804,56 @@ describe('ProgrammingExerciseUpdateComponent', () => { }); }); - it('find validation errors for invalid ide selection', () => { + it.each([ + [ + 'find validation errors for invalid ide selection', + { + activeProfiles: [PROFILE_THEIA], + }, + { + translateKey: 'artemisApp.programmingExercise.allowOnlineEditor.alert', + translateValues: {}, + }, + ], + [ + 'find validation errors for invalid ide selection without theia profile', + { + activeProfiles: [], + }, + { + translateKey: 'artemisApp.programmingExercise.allowOnlineEditor.alertNoTheia', + translateValues: {}, + }, + ], + ])('%s', (description, profileInfo, expectedException) => { + getProfileInfoSub = jest.spyOn(profileService, 'getProfileInfo'); + + const newProfileInfo = new ProfileInfo(); + newProfileInfo.activeProfiles = profileInfo.activeProfiles; + + getProfileInfoSub.mockReturnValue(of(newProfileInfo)); + + const route = TestBed.inject(ActivatedRoute); + route.params = of({ courseId }); + route.url = of([{ path: 'new' } as UrlSegment]); + route.data = of({ programmingExercise: comp.programmingExercise }); + + jest.spyOn(programmingExerciseFeatureService, 'getProgrammingLanguageFeature').mockReturnValue(getProgrammingLanguageFeature(ProgrammingLanguage.JAVA)); + comp.programmingExercise.allowOnlineEditor = false; comp.programmingExercise.allowOfflineIde = false; + comp.programmingExercise.allowOnlineIde = false; + + fixture.detectChanges(); + + expect(comp.getInvalidReasons()).toContainEqual(expectedException); + }); + + it('find validation errors for invalid online IDE image', () => { + comp.programmingExercise.allowOnlineIde = true; + comp.programmingExercise.buildConfig!.theiaImage = undefined; expect(comp.getInvalidReasons()).toContainEqual({ - translateKey: 'artemisApp.programmingExercise.allowOnlineEditor.alert', + translateKey: 'artemisApp.programmingExercise.theiaImage.alert', translateValues: {}, }); }); @@ -832,6 +889,7 @@ describe('ProgrammingExerciseUpdateComponent', () => { comp.programmingExercise.maxStaticCodeAnalysisPenalty = 60; comp.programmingExercise.allowOfflineIde = true; comp.programmingExercise.allowOnlineEditor = false; + comp.programmingExercise.allowOnlineIde = false; comp.programmingExercise.packageName = 'de.tum.in'; comp.programmingExercise.programmingLanguage = ProgrammingLanguage.JAVA; @@ -1017,6 +1075,7 @@ describe('ProgrammingExerciseUpdateComponent', () => { comp.programmingExercise.allowOfflineIde = false; comp.programmingExercise.allowOnlineEditor = false; + comp.programmingExercise.allowOnlineIde = false; comp.calculateFormStatusSections(); expect(comp.formStatusSections[1].valid).toBeFalse(); comp.programmingExercise.allowOnlineEditor = true; diff --git a/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-creation-config-mock.ts b/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-creation-config-mock.ts index d33537559cbd..7879f1f5c0b5 100644 --- a/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-creation-config-mock.ts +++ b/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-creation-config-mock.ts @@ -69,5 +69,8 @@ export const programmingExerciseCreationConfigMock: ProgrammingExerciseCreationC validIdeSelection(): boolean | undefined { return true; }, + validOnlineIdeSelection(): boolean | undefined { + return true; + }, withDependencies: false, }; diff --git a/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-difficulty.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-difficulty.component.spec.ts index 11d63911cc2c..ce4ba0bc736f 100644 --- a/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-difficulty.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-difficulty.component.spec.ts @@ -1,5 +1,6 @@ -import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MockComponent, MockPipe } from 'ng-mocks'; +import { DebugElement } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { of } from 'rxjs'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; @@ -9,14 +10,21 @@ import { DifficultyPickerComponent } from 'app/exercises/shared/difficulty-picke import { TeamConfigFormGroupComponent } from 'app/exercises/shared/team-config-form-group/team-config-form-group.component'; import { programmingExerciseCreationConfigMock } from './programming-exercise-creation-config-mock'; import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; +import { ProfileInfo } from 'app/shared/layouts/profiles/profile-info.model'; +import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; +import { PROFILE_THEIA } from 'app/app.constants'; +import { ArtemisTestModule } from '../../../test.module'; describe('ProgrammingExerciseDifficultyComponent', () => { let fixture: ComponentFixture; let comp: ProgrammingExerciseDifficultyComponent; + let debugElement: DebugElement; + let profileService: ProfileService; + let getProfileInfoSub: jest.SpyInstance; beforeEach(() => { TestBed.configureTestingModule({ - imports: [], + imports: [ArtemisTestModule], declarations: [ CheckboxControlValueAccessor, DefaultValueAccessor, @@ -42,6 +50,11 @@ describe('ProgrammingExerciseDifficultyComponent', () => { comp = fixture.componentInstance; comp.programmingExercise = new ProgrammingExercise(undefined, undefined); comp.programmingExerciseCreationConfig = programmingExerciseCreationConfigMock; + + debugElement = fixture.debugElement; + profileService = debugElement.injector.get(ProfileService); + getProfileInfoSub = jest.spyOn(profileService, 'getProfileInfo'); + getProfileInfoSub.mockReturnValue(of({ inProduction: false, sshCloneURLTemplate: 'ssh://git@testserver.com:1234/' } as ProfileInfo)); }); }); @@ -49,8 +62,16 @@ describe('ProgrammingExerciseDifficultyComponent', () => { jest.restoreAllMocks(); }); - it('should initialize', fakeAsync(() => { + it('should initialize', () => { fixture.detectChanges(); expect(comp).not.toBeNull(); - })); + }); + + it('should initialize theiaEnabled', () => { + getProfileInfoSub = jest.spyOn(profileService, 'getProfileInfo'); + getProfileInfoSub.mockReturnValue(of({ activeProfiles: [PROFILE_THEIA] } as ProfileInfo)); + + fixture.detectChanges(); + expect(comp.theiaEnabled).toBeTrue(); + }); }); diff --git a/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-language.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-language.component.spec.ts index 399f1969f22d..4d21b3741ce6 100644 --- a/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-language.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/update-components/programming-exercise-language.component.spec.ts @@ -8,16 +8,25 @@ import { RemoveKeysPipe } from 'app/shared/pipes/remove-keys.pipe'; import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; import { ProgrammingExerciseLanguageComponent } from 'app/exercises/programming/manage/update/update-components/programming-exercise-language.component'; import { programmingExerciseCreationConfigMock } from './programming-exercise-creation-config-mock'; +import { ProgrammingExerciseTheiaComponent } from 'app/exercises/programming/manage/update/update-components/theia/programming-exercise-theia.component'; +import { provideHttpClient } from '@angular/common/http'; +import { TheiaService } from 'app/exercises/programming/shared/service/theia.service'; describe('ProgrammingExerciseLanguageComponent', () => { let fixture: ComponentFixture; let comp: ProgrammingExerciseLanguageComponent; + let theiaServiceMock!: { getTheiaImages: jest.Mock }; + beforeEach(() => { + theiaServiceMock = { + getTheiaImages: jest.fn(), + }; TestBed.configureTestingModule({ imports: [], declarations: [ ProgrammingExerciseLanguageComponent, + ProgrammingExerciseTheiaComponent, CheckboxControlValueAccessor, DefaultValueAccessor, SelectControlValueAccessor, @@ -27,10 +36,15 @@ describe('ProgrammingExerciseLanguageComponent', () => { MockPipe(RemoveKeysPipe), ], providers: [ + provideHttpClient(), { provide: ActivatedRoute, useValue: { queryParams: of({}) }, }, + { + provide: TheiaService, + useValue: theiaServiceMock, + }, ], schemas: [], }) @@ -52,4 +66,19 @@ describe('ProgrammingExerciseLanguageComponent', () => { tick(); expect(comp).not.toBeNull(); })); + + it('should not load TheiaComponent when online IDE is not allowed', fakeAsync(() => { + comp.programmingExercise.allowOnlineIde = false; + fixture.detectChanges(); + tick(); + expect(comp.programmingExerciseTheiaComponent).toBeUndefined(); + })); + + it('should load TheiaComponent when online IDE is allowed', fakeAsync(() => { + theiaServiceMock.getTheiaImages.mockReturnValue(of({})); + comp.programmingExercise.allowOnlineIde = true; + fixture.detectChanges(); + tick(); + expect(comp.programmingExerciseTheiaComponent).not.toBeNull(); + })); }); diff --git a/src/test/javascript/spec/component/programming-exercise/update-components/theia/programming-exercise-theia.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/update-components/theia/programming-exercise-theia.component.spec.ts new file mode 100644 index 000000000000..13f4e23cbbcd --- /dev/null +++ b/src/test/javascript/spec/component/programming-exercise/update-components/theia/programming-exercise-theia.component.spec.ts @@ -0,0 +1,90 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { MockPipe } from 'ng-mocks'; +import { ActivatedRoute } from '@angular/router'; +import { of } from 'rxjs'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { RemoveKeysPipe } from 'app/shared/pipes/remove-keys.pipe'; +import { ProgrammingExercise } from 'app/entities/programming-exercise.model'; +import { programmingExerciseCreationConfigMock } from '../programming-exercise-creation-config-mock'; +import { ProgrammingExerciseTheiaComponent } from 'app/exercises/programming/manage/update/update-components/theia/programming-exercise-theia.component'; +import { TheiaService } from 'app/exercises/programming/shared/service/theia.service'; +import { ArtemisSharedLibsModule } from 'app/shared/shared-libs.module'; + +describe('ProgrammingExerciseTheiaComponent', () => { + let fixture: ComponentFixture; + let comp: ProgrammingExerciseTheiaComponent; + + let theiaServiceMock!: { getTheiaImages: jest.Mock }; + + beforeEach(() => { + theiaServiceMock = { + getTheiaImages: jest.fn(), + }; + TestBed.configureTestingModule({ + imports: [ProgrammingExerciseTheiaComponent, ArtemisSharedLibsModule], + declarations: [MockPipe(ArtemisTranslatePipe), MockPipe(RemoveKeysPipe)], + providers: [ + { + provide: ActivatedRoute, + useValue: { queryParams: of({}) }, + }, + { + provide: TheiaService, + useValue: theiaServiceMock, + }, + ], + schemas: [], + }).compileComponents(); + + fixture = TestBed.createComponent(ProgrammingExerciseTheiaComponent); + comp = fixture.componentInstance; + comp.programmingExerciseCreationConfig = programmingExerciseCreationConfigMock; + comp.programmingExercise = new ProgrammingExercise(undefined, undefined); + comp.programmingExercise.allowOnlineIde = true; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should initialize', fakeAsync(() => { + fixture.detectChanges(); + tick(); + expect(comp).not.toBeNull(); + })); + + it('should have no selectedImage when no image is available', fakeAsync(() => { + theiaServiceMock.getTheiaImages.mockReturnValue(of({})); + fixture.detectChanges(); + comp.loadTheiaImages(); + tick(); + expect(comp.programmingExercise.buildConfig?.theiaImage).toBeUndefined(); + })); + + it('should select first image when none was selected', fakeAsync(() => { + theiaServiceMock.getTheiaImages.mockReturnValue( + of({ + 'Java-17': 'test-url', + 'Java-Test': 'test-url-2', + }), + ); + fixture.detectChanges(); + comp.loadTheiaImages(); + tick(); + expect(comp.programmingExercise.buildConfig?.theiaImage).toMatch('test-url'); + })); + + it('should not overwrite selected image when others are loaded', fakeAsync(() => { + comp.programmingExercise.buildConfig!.theiaImage = 'test-url-2'; + theiaServiceMock.getTheiaImages.mockReturnValue( + of({ + 'Java-17': 'test-url', + 'Java-Test': 'test-url-2', + }), + ); + fixture.detectChanges(); + comp.loadTheiaImages(); + tick(); + expect(comp.programmingExercise.buildConfig?.theiaImage).toMatch('test-url-2'); + })); +}); diff --git a/src/test/javascript/spec/service/programming-exercise.service.spec.ts b/src/test/javascript/spec/service/programming-exercise.service.spec.ts index 66ca8a1e0d44..fd2ab5d925e7 100644 --- a/src/test/javascript/spec/service/programming-exercise.service.spec.ts +++ b/src/test/javascript/spec/service/programming-exercise.service.spec.ts @@ -179,6 +179,7 @@ describe('ProgrammingExercise Service', () => { solutionRepositoryUri: 'BBBBBB', templateBuildPlanId: 'BBBBBB', allowOnlineEditor: true, + allowOnlineIde: true, releaseDate: undefined, dueDate: undefined, assessmentDueDate: undefined, @@ -222,6 +223,7 @@ describe('ProgrammingExercise Service', () => { solutionRepositoryUri: 'BBBBBB', templateBuildPlanId: 'BBBBBB', allowOnlineEditor: true, + allowOnlineIde: true, releaseDate: undefined, dueDate: undefined, assessmentDueDate: undefined, diff --git a/src/test/resources/config/application-theia.yml b/src/test/resources/config/application-theia.yml new file mode 100644 index 000000000000..883dea41098f --- /dev/null +++ b/src/test/resources/config/application-theia.yml @@ -0,0 +1,9 @@ +theia: + portal-url: https://your-theia-instance.com + + images: + java: + Java-17: "ghcr.io/ls1intum/theia/java-17:latest" + Java-Non-Existent: "this-is-not-a-valid-image" + c: + C: "ghcr.io/ls1intum/theia/c:latest"