Skip to content

Commit

Permalink
Programming exercises: Add online IDE settings (#8965)
Browse files Browse the repository at this point in the history
  • Loading branch information
iyannsch authored and kaancayli committed Sep 5, 2024
1 parent 10ef0e4 commit 3850102
Show file tree
Hide file tree
Showing 36 changed files with 823 additions and 111 deletions.
3 changes: 2 additions & 1 deletion src/main/java/de/tum/in/www1/artemis/ArtemisApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ProgrammingLanguage, Map<String, String>> images;

public void setImages(final Map<ProgrammingLanguage, Map<String, String>> images) {
this.images = images;
}

/**
* Get the images for all languages
*
* @return a map of language -> [flavor/name -> image-link]
*/
public Map<ProgrammingLanguage, Map<String, String>> 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<String, String> getImagesForLanguage(ProgrammingLanguage language) {
return images.get(language);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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");
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -151,6 +154,8 @@ public class ProgrammingExerciseResource {

private final Optional<AthenaModuleService> athenaModuleService;

private final Environment environment;

public ProgrammingExerciseResource(ProgrammingExerciseRepository programmingExerciseRepository, ProgrammingExerciseTestCaseRepository programmingExerciseTestCaseRepository,
UserRepository userRepository, AuthorizationCheckService authCheckService, CourseService courseService,
Optional<ContinuousIntegrationService> continuousIntegrationService, Optional<VersionControlService> versionControlService, ExerciseService exerciseService,
Expand All @@ -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> athenaModuleService) {
BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository, ChannelRepository channelRepository, Optional<AthenaModuleService> athenaModuleService,
Environment environment) {
this.programmingExerciseTaskService = programmingExerciseTaskService;
this.programmingExerciseRepository = programmingExerciseRepository;
this.programmingExerciseTestCaseRepository = programmingExerciseTestCaseRepository;
Expand All @@ -184,6 +190,7 @@ public ProgrammingExerciseResource(ProgrammingExerciseRepository programmingExer
this.buildLogStatisticsEntryRepository = buildLogStatisticsEntryRepository;
this.channelRepository = channelRepository;
this.athenaModuleService = athenaModuleService;
this.environment = environment;
}

/**
Expand Down Expand Up @@ -303,9 +310,26 @@ public ResponseEntity<ProgrammingExercise> 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(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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=<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<Map<String, String>> getImagesForLanguage(@RequestParam("language") ProgrammingLanguage language) {
return ResponseEntity.ok(this.theiaConfiguration.getImagesForLanguage(language));
}

}
11 changes: 10 additions & 1 deletion src/main/resources/config/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

13 changes: 12 additions & 1 deletion src/main/resources/config/application-theia.yml
Original file line number Diff line number Diff line change
@@ -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"
3 changes: 3 additions & 0 deletions src/main/webapp/app/entities/programming-exercise.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,13 @@
@if (displayEditorModus) {
<div>
<div class="d-flex justify-content-between">
{{ 'artemisApp.programmingExercise.offlineIde' | artemisTranslate }}
: {{ programmingExercise.allowOfflineIde || false }}
<span jhiTranslate="'artemisApp.programmingExercise.offlineIde'">: {{ programmingExercise.allowOfflineIde || false }}</span>
</div>
<div class="d-flex justify-content-between">
{{ 'artemisApp.programmingExercise.onlineEditor' | artemisTranslate }}
: {{ programmingExercise.allowOnlineEditor || false }}
<span jhiTranslate="'artemisApp.programmingExercise.onlineEditor'">: {{ programmingExercise.allowOnlineEditor || false }}</span>
</div>
<div class="d-flex justify-content-between">
<span jhiTranslate="'artemisApp.programmingExercise.onlineIde'">: {{ programmingExercise.allowOnlineIde || false }}</span>
</div>
</div>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
},
],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,16 @@
}
<td class="d-md-table-cell">
<div class="d-flex justify-content-between">
{{ 'artemisApp.programmingExercise.offlineIde' | artemisTranslate }}:
{{ programmingExercise.allowOfflineIde ? ('artemisApp.exercise.yes' | artemisTranslate) : ('artemisApp.exercise.no' | artemisTranslate) }}
<span [jhiTranslate]="'artemisApp.programmingExercise.offlineIde'"></span>:
<span [jhiTranslate]="programmingExercise.allowOfflineIde ? 'artemisApp.exercise.yes' : 'artemisApp.exercise.no'"></span>
</div>
<div class="d-flex justify-content-between">
{{ 'artemisApp.programmingExercise.onlineEditor' | artemisTranslate }}:
{{ programmingExercise.allowOnlineEditor ? ('artemisApp.exercise.yes' | artemisTranslate) : ('artemisApp.exercise.no' | artemisTranslate) }}
<span [jhiTranslate]="'artemisApp.programmingExercise.onlineEditor'"></span>:
<span [jhiTranslate]="programmingExercise.allowOnlineEditor ? 'artemisApp.exercise.yes' : 'artemisApp.exercise.no'"></span>
</div>
<div class="d-flex justify-content-between">
<span [jhiTranslate]="'artemisApp.programmingExercise.onlineIde'"></span>:
<span [jhiTranslate]="programmingExercise.allowOnlineIde ? 'artemisApp.exercise.yes' : 'artemisApp.exercise.no'"></span>
</div>
</td>
@if (course.presentationScore !== 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type ProgrammingExerciseCreationConfig = {
hasUnsavedChanges: boolean;
rerenderSubject: Observable<void>;
validIdeSelection: () => boolean | undefined;
validOnlineIdeSelection: () => boolean | undefined;
inProductionEnvironment: boolean;
recreateBuildPlans: boolean;
onRecreateBuildPlanOrUpdateTemplateChange: () => void;
Expand Down
Loading

0 comments on commit 3850102

Please sign in to comment.