From 814c2e6d952849b35dc5838a10317f2dd8ac11f3 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Wed, 13 Dec 2023 23:34:45 +0100 Subject: [PATCH 01/31] Allow module selection per exercise, make necessary adaptions, add migration, fix tests --- .../de/tum/in/www1/artemis/domain/Course.java | 11 ++ .../tum/in/www1/artemis/domain/Exercise.java | 17 ++- .../repository/ExerciseRepository.java | 25 ++-- .../artemis/service/SubmissionService.java | 2 +- .../service/TextExerciseImportService.java | 2 +- .../athena/AthenaFeedbackSendingService.java | 10 +- .../AthenaFeedbackSuggestionsService.java | 11 +- .../athena/AthenaModuleService.java | 126 ++++++++++++++++++ .../athena/AthenaModuleUrlHelper.java | 42 ------ .../athena/AthenaRepositoryExportService.java | 2 +- .../AthenaSubmissionSelectionService.java | 10 +- .../AthenaSubmissionSendingService.java | 10 +- .../ProgrammingExerciseImportService.java | 3 +- .../scheduled/AthenaScheduleService.java | 2 +- .../www1/artemis/web/rest/AthenaResource.java | 46 ++++++- .../www1/artemis/web/rest/CourseResource.java | 19 ++- .../rest/ProgrammingAssessmentResource.java | 2 +- .../web/rest/ProgrammingExerciseResource.java | 13 +- .../web/rest/TextAssessmentResource.java | 2 +- .../web/rest/TextExerciseResource.java | 20 ++- .../resources/config/application-artemis.yml | 4 +- .../changelog/20231212191800_changelog.xml | 20 +++ .../resources/config/liquibase/master.xml | 1 + src/main/webapp/app/app.constants.ts | 2 + .../webapp/app/assessment/athena.service.ts | 22 ++- .../manage/course-update.component.html | 18 +++ .../course/manage/course-update.component.ts | 10 +- .../detail/course-detail.component.html | 8 ++ .../manage/detail/course-detail.component.ts | 4 +- src/main/webapp/app/entities/course.model.ts | 2 + .../webapp/app/entities/exercise.model.ts | 2 +- ...programming-exercise-detail.component.html | 6 +- ...gramming-exercise-lifecycle.component.html | 20 +-- ...rogramming-exercise-lifecycle.component.ts | 2 +- .../programming-exercise-lifecycle.module.ts | 3 +- ...feedback-suggestion-options.component.html | 28 ++++ ...e-feedback-suggestion-options.component.ts | 63 +++++++++ ...cise-feedback-suggestion-options.module.ts | 11 ++ .../text-exercise-detail.component.html | 6 +- .../text-exercise-update.component.html | 15 +-- .../text-exercise/text-exercise.module.ts | 2 + src/main/webapp/i18n/de/course.json | 4 + src/main/webapp/i18n/en/course.json | 4 + .../connector/AthenaRequestMockProvider.java | 4 + .../AthenaResourceIntegrationTest.java | 19 ++- .../AthenaFeedbackSendingServiceTest.java | 14 +- .../AthenaFeedbackSuggestionsServiceTest.java | 4 + .../AthenaRepositoryExportServiceTest.java | 5 +- .../AthenaSubmissionSelectionServiceTest.java | 10 +- .../AthenaSubmissionSendingServiceTest.java | 12 +- .../text/TextAssessmentIntegrationTest.java | 3 +- .../course/course-update.component.spec.ts | 2 + ...mming-exercise-lifecycle.component.spec.ts | 7 +- .../spec/service/athena.service.spec.ts | 6 +- .../resources/config/application-artemis.yml | 4 +- 55 files changed, 552 insertions(+), 170 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java delete mode 100644 src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleUrlHelper.java create mode 100644 src/main/resources/config/liquibase/changelog/20231212191800_changelog.xml create mode 100644 src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html create mode 100644 src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts create mode 100644 src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.module.ts diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Course.java b/src/main/java/de/tum/in/www1/artemis/domain/Course.java index ce8e9c22bdec..11005b35e99c 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Course.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Course.java @@ -199,6 +199,9 @@ public class Course extends DomainObject { @JsonView(QuizView.Before.class) private Integer accuracyOfScores = 1; // default value + @Column(name = "restricted_athena_modules_access", nullable = false) + private boolean restrictedAthenaModulesAccess = false; // default is false + /** * Note: Currently just used in the scope of the tutorial groups feature */ @@ -803,6 +806,14 @@ public void setAccuracyOfScores(Integer accuracyOfScores) { this.accuracyOfScores = accuracyOfScores; } + public boolean getRestrictedAthenaModulesAccess() { + return restrictedAthenaModulesAccess; + } + + public void setRestrictedAthenaModulesAccess(boolean restrictedAthenaModulesAccess) { + this.restrictedAthenaModulesAccess = restrictedAthenaModulesAccess; + } + public Set getTutorialGroups() { return tutorialGroups; } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java b/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java index 72145c1c0f67..30b266d5a4b8 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java @@ -101,8 +101,9 @@ public abstract class Exercise extends BaseExercise implements LearningObject { @Column(name = "second_correction_enabled") private Boolean secondCorrectionEnabled = false; - @Column(name = "feedback_suggestions_enabled") // enables Athena - private Boolean feedbackSuggestionsEnabled = false; + // TODO Athena: adapt to new exercise model: instead of using a boolean, we just use the module name (enabled) or null + @Column(name = "feedback_suggestion_module") // Athena module name (Athena enabled) or null + private String feedbackSuggestionModule; @ManyToOne @JsonView(QuizView.Before.class) @@ -783,12 +784,16 @@ public void setSecondCorrectionEnabled(boolean secondCorrectionEnabled) { this.secondCorrectionEnabled = secondCorrectionEnabled; } - public boolean getFeedbackSuggestionsEnabled() { - return Boolean.TRUE.equals(feedbackSuggestionsEnabled); + public String getFeedbackSuggestionModule() { + return feedbackSuggestionModule; } - public void setFeedbackSuggestionsEnabled(boolean feedbackSuggestionsEnabled) { - this.feedbackSuggestionsEnabled = feedbackSuggestionsEnabled; + public void setFeedbackSuggestionModule(String feedbackSuggestionModule) { + this.feedbackSuggestionModule = feedbackSuggestionModule; + } + + public boolean isFeedbackSuggestionsEnabled() { + return feedbackSuggestionModule != null; } public List getGradingCriteria() { diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java index 673124f85379..82f806300ec6 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java @@ -9,11 +9,10 @@ import javax.validation.constraints.NotNull; import org.springframework.cache.annotation.Cacheable; -import org.springframework.data.jpa.repository.EntityGraph; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.*; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import de.tum.in.www1.artemis.domain.Exercise; import de.tum.in.www1.artemis.domain.metrics.ExerciseTypeMetricsEntry; @@ -529,13 +528,12 @@ default boolean toggleSecondCorrection(Exercise exercise) { Set getAllExercisesUserParticipatedInWithEagerParticipationsSubmissionsResultsFeedbacksTestCasesByUserId(long userId); /** - * Finds all exercises filtered by feedback suggestions and due date. + * Finds all exercises filtered by feedback suggestion modules not null and due date. * - * @param feedbackSuggestionsEnabled - filter by feedback suggestions enabled - * @param dueDate - filter by due date + * @param dueDate - filter by due date * @return Set of Exercises */ - Set findByFeedbackSuggestionsEnabledAndDueDateIsAfter(boolean feedbackSuggestionsEnabled, ZonedDateTime dueDate); + Set findByFeedbackSuggestionModuleNotNullAndDueDateIsAfter(ZonedDateTime dueDate); /** * Find all exercises feedback suggestions (Athena) and with *Due Date* in the future. @@ -543,9 +541,20 @@ default boolean toggleSecondCorrection(Exercise exercise) { * @return Set of Exercises */ default Set findAllFeedbackSuggestionsEnabledExercisesWithFutureDueDate() { - return findByFeedbackSuggestionsEnabledAndDueDateIsAfter(true, ZonedDateTime.now()); + return findByFeedbackSuggestionModuleNotNullAndDueDateIsAfter(ZonedDateTime.now()); } + @Transactional // ok because of modifying query + @Modifying + @Query(""" + UPDATE Exercise e + SET e.feedbackSuggestionModule = NULL + WHERE e.course.id = :courseId + AND e.feedbackSuggestionModule IN :restrictedFeedbackSuggestionModule + """) + void revokeAccessToRestrictedFeedbackSuggestionModulesByCourseId(@Param("courseId") Long courseId, + @Param("restrictedFeedbackSuggestionModule") Set restrictedFeedbackSuggestionModule); + /** * For an explanation, see {@link de.tum.in.www1.artemis.web.rest.ExamResource#getAllExercisesWithPotentialPlagiarismForExam(long,long)} * diff --git a/src/main/java/de/tum/in/www1/artemis/service/SubmissionService.java b/src/main/java/de/tum/in/www1/artemis/service/SubmissionService.java index 35e5b2e38ae1..2d5d2aa1c75a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/SubmissionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/SubmissionService.java @@ -236,7 +236,7 @@ public Optional getNextAssessableSubmission(Exercise exercise, boole */ public Optional getAthenaSubmissionToAssess(Exercise exercise, boolean skipAssessmentQueue, boolean examMode, int correctionRound, Function> findSubmissionById) { - if (exercise.getFeedbackSuggestionsEnabled() && athenaSubmissionSelectionService.isPresent() && !skipAssessmentQueue && correctionRound == 0) { + if (exercise.isFeedbackSuggestionsEnabled() && athenaSubmissionSelectionService.isPresent() && !skipAssessmentQueue && correctionRound == 0) { var assessableSubmissions = getAssessableSubmissions(exercise, examMode, correctionRound); var athenaSubmissionId = athenaSubmissionSelectionService.get().getProposedSubmissionId(exercise, assessableSubmissions.stream().map(Submission::getId).toList()); if (athenaSubmissionId.isPresent()) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/TextExerciseImportService.java b/src/main/java/de/tum/in/www1/artemis/service/TextExerciseImportService.java index 71cfb927b9ab..f1b468d5c2cd 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/TextExerciseImportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/TextExerciseImportService.java @@ -57,7 +57,7 @@ public TextExercise importTextExercise(final TextExercise templateExercise, Text TextExercise newExercise = copyTextExerciseBasis(importedExercise, gradingInstructionCopyTracker); if (newExercise.isExamExercise()) { // Disable feedback suggestions on exam exercises (currently not supported) - newExercise.setFeedbackSuggestionsEnabled(false); + newExercise.setFeedbackSuggestionModule(null); } TextExercise newTextExercise = textExerciseRepository.save(newExercise); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSendingService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSendingService.java index 651c180cee51..40aabdd166e5 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSendingService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSendingService.java @@ -28,17 +28,17 @@ public class AthenaFeedbackSendingService { private final AthenaConnector connector; - private final AthenaModuleUrlHelper athenaModuleUrlHelper; + private final AthenaModuleService athenaModuleService; private final AthenaDTOConverter athenaDTOConverter; /** * Creates a new service to send feedback to the Athena service */ - public AthenaFeedbackSendingService(@Qualifier("athenaRestTemplate") RestTemplate athenaRestTemplate, AthenaModuleUrlHelper athenaModuleUrlHelper, + public AthenaFeedbackSendingService(@Qualifier("athenaRestTemplate") RestTemplate athenaRestTemplate, AthenaModuleService athenaModuleService, AthenaDTOConverter athenaDTOConverter) { connector = new AthenaConnector<>(athenaRestTemplate, ResponseDTO.class); - this.athenaModuleUrlHelper = athenaModuleUrlHelper; + this.athenaModuleService = athenaModuleService; this.athenaDTOConverter = athenaDTOConverter; } @@ -72,7 +72,7 @@ public void sendFeedback(Exercise exercise, Submission submission, List feedbacks, int maxRetries) { - if (!exercise.getFeedbackSuggestionsEnabled()) { + if (!exercise.isFeedbackSuggestionsEnabled()) { throw new IllegalArgumentException("The exercise does not have feedback suggestions enabled."); } @@ -89,7 +89,7 @@ public void sendFeedback(Exercise exercise, Submission submission, List athenaDTOConverter.ofFeedback(exercise, submission.getId(), feedback)).toList()); - ResponseDTO response = connector.invokeWithRetry(athenaModuleUrlHelper.getAthenaModuleUrl(exercise.getExerciseType()) + "/feedbacks", request, maxRetries); + ResponseDTO response = connector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/feedbacks", request, maxRetries); log.info("Athena responded to feedback: {}", response.data); } catch (NetworkingException networkingException) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSuggestionsService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSuggestionsService.java index a2d6d39ed7bc..4a67266d85c8 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSuggestionsService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSuggestionsService.java @@ -29,19 +29,19 @@ public class AthenaFeedbackSuggestionsService { private final AthenaConnector programmingAthenaConnector; - private final AthenaModuleUrlHelper athenaModuleUrlHelper; + private final AthenaModuleService athenaModuleService; private final AthenaDTOConverter athenaDTOConverter; /** * Creates a new AthenaFeedbackSuggestionsService to receive feedback suggestions from the Athena service. */ - public AthenaFeedbackSuggestionsService(@Qualifier("athenaRestTemplate") RestTemplate athenaRestTemplate, AthenaModuleUrlHelper athenaModuleUrlHelper, + public AthenaFeedbackSuggestionsService(@Qualifier("athenaRestTemplate") RestTemplate athenaRestTemplate, AthenaModuleService athenaModuleService, AthenaDTOConverter athenaDTOConverter) { textAthenaConnector = new AthenaConnector<>(athenaRestTemplate, ResponseDTOText.class); programmingAthenaConnector = new AthenaConnector<>(athenaRestTemplate, ResponseDTOProgramming.class); this.athenaDTOConverter = athenaDTOConverter; - this.athenaModuleUrlHelper = athenaModuleUrlHelper; + this.athenaModuleService = athenaModuleService; } private record RequestDTO(ExerciseDTO exercise, SubmissionDTO submission) { @@ -70,7 +70,7 @@ public List getTextFeedbackSuggestions(TextExercise exercise, T } final RequestDTO request = new RequestDTO(athenaDTOConverter.ofExercise(exercise), athenaDTOConverter.ofSubmission(exercise.getId(), submission)); - ResponseDTOText response = textAthenaConnector.invokeWithRetry(athenaModuleUrlHelper.getAthenaModuleUrl(exercise.getExerciseType()) + "/feedback_suggestions", request, 0); + ResponseDTOText response = textAthenaConnector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/feedback_suggestions", request, 0); log.info("Athena responded to feedback suggestions request: {}", response.data); return response.data.stream().toList(); } @@ -86,8 +86,7 @@ public List getProgrammingFeedbackSuggestions(Programmin log.debug("Start Athena Feedback Suggestions Service for Exercise '{}' (#{}).", exercise.getTitle(), exercise.getId()); final RequestDTO request = new RequestDTO(athenaDTOConverter.ofExercise(exercise), athenaDTOConverter.ofSubmission(exercise.getId(), submission)); - ResponseDTOProgramming response = programmingAthenaConnector.invokeWithRetry(athenaModuleUrlHelper.getAthenaModuleUrl(exercise.getExerciseType()) + "/feedback_suggestions", - request, 0); + ResponseDTOProgramming response = programmingAthenaConnector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/feedback_suggestions", request, 0); log.info("Athena responded to feedback suggestions request: {}", response.data); return response.data.stream().toList(); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java new file mode 100644 index 000000000000..e4086c1d3cbc --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java @@ -0,0 +1,126 @@ +package de.tum.in.www1.artemis.service.connectors.athena; + +import java.util.HashSet; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.Exercise; +import de.tum.in.www1.artemis.repository.ExerciseRepository; +import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; + +/** + * Service to get the URL for an Athena module, depending on the type of exercise. + */ +@Service +@Profile("athena") +public class AthenaModuleService { + + @Value("${artemis.athena.url}") + private String athenaUrl; + + // If value is present, split the provided modules by comma; default to empty list + @Value("#{'${artemis.athena.restricted-modules:}'}") + private List restrictedModules; + + private final Logger log = LoggerFactory.getLogger(AthenaModuleService.class); + + private final RestTemplate shortTimeoutRestTemplate; + + private final ObjectMapper objectMapper; + + private final ExerciseRepository exerciseRepository; + + public AthenaModuleService(@Qualifier("shortTimeoutAthenaRestTemplate") RestTemplate shortTimeoutRestTemplate, MappingJackson2HttpMessageConverter springMvcJacksonConverter, + ExerciseRepository exerciseRepository) { + this.shortTimeoutRestTemplate = shortTimeoutRestTemplate; + this.objectMapper = springMvcJacksonConverter.getObjectMapper(); + this.exerciseRepository = exerciseRepository; + } + + private record AthenaModuleDTO(String name, String type) { + } + + private List getAthenaModules() { + try { + var response = shortTimeoutRestTemplate.getForEntity(athenaUrl + "/modules", JsonNode.class); + // if (!response.getStatusCode().is2xxSuccessful() || !response.hasBody()) { + // throw new IrisConnectorException("Could not fetch modules"); + // } + // todo error handling + AthenaModuleDTO[] modules = objectMapper.treeToValue(response.getBody(), AthenaModuleDTO[].class); + return List.of(modules); + } + catch (HttpStatusCodeException | JsonProcessingException e) { + log.error("Failed to fetch modules from Athena", e); + // todo error handling + } + return List.of(); + } + + public List getAthenaProgrammingModulesForCourse(Course course) { + List availableProgrammingModules = getAthenaModules().stream().filter(module -> "programming".equals(module.type)).map(module -> module.name).toList(); + if (!course.getRestrictedAthenaModulesAccess()) { + // filter out restricted modules + availableProgrammingModules = availableProgrammingModules.stream().filter(moduleName -> !restrictedModules.contains(moduleName)).toList(); + } + return availableProgrammingModules; + } + + public List getAthenaTextModulesForCourse(Course course) { + List availableProgrammingModules = getAthenaModules().stream().filter(module -> "text".equals(module.type)).map(module -> module.name).toList(); + if (!course.getRestrictedAthenaModulesAccess()) { + // filter out restricted modules + availableProgrammingModules = availableProgrammingModules.stream().filter(moduleName -> !restrictedModules.contains(moduleName)).toList(); + } + return availableProgrammingModules; + } + + /** + * Get the URL for an Athena module, depending on the type of exercise. + * + * @param exerciseType The type of exercise + * @return The URL prefix to access the Athena module. Example: "http://athena.example.com/modules/text/module_text_cofee" + */ + public String getAthenaModuleUrl(Exercise exercise) { + // TODO Athena: Use the specified module in the exercise instead of the config specified one + switch (exercise.getExerciseType()) { + case TEXT -> { + return athenaUrl + "/modules/text/" + exercise.getFeedbackSuggestionModule(); + } + case PROGRAMMING -> { + return athenaUrl + "/modules/programming/" + exercise.getFeedbackSuggestionModule(); + } + default -> throw new IllegalArgumentException("Exercise type not supported: " + exercise.getExerciseType()); + } + } + + public void checkHasAccessToAthenaModule(Exercise exercise, Course course, String entityName) throws BadRequestAlertException { + if (!course.getRestrictedAthenaModulesAccess() && restrictedModules.contains(exercise.getFeedbackSuggestionModule())) { + // Course does not have access to the restricted Athena modules + throw new BadRequestAlertException("The exercise has no access to the selected Athena module", entityName, "noAccessToAthenaModule"); + } + } + + public void revokeAccessToRestrictedFeedbackSuggestionModules(Course course) { + exerciseRepository.revokeAccessToRestrictedFeedbackSuggestionModulesByCourseId(course.getId(), new HashSet<>(restrictedModules)); + } + + public List getRestrictedModules() { + // TODO Athena: Just for testing, remove afterwards + return restrictedModules; + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleUrlHelper.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleUrlHelper.java deleted file mode 100644 index 36f5e65c184b..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleUrlHelper.java +++ /dev/null @@ -1,42 +0,0 @@ -package de.tum.in.www1.artemis.service.connectors.athena; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Service; - -import de.tum.in.www1.artemis.domain.enumeration.ExerciseType; - -/** - * Service to get the URL for an Athena module, depending on the type of exercise. - */ -@Service -@Profile("athena") -public class AthenaModuleUrlHelper { - - @Value("${artemis.athena.url}") - private String athenaUrl; - - @Value("${artemis.athena.modules.text:module_text_cofee}") - private String textModuleName; - - @Value("${artemis.athena.modules.programming:module_programming_themisml}") - private String programmingModuleName; - - /** - * Get the URL for an Athena module, depending on the type of exercise. - * - * @param exerciseType The type of exercise - * @return The URL prefix to access the Athena module. Example: "http://athena.example.com/modules/text/module_text_cofee" - */ - public String getAthenaModuleUrl(ExerciseType exerciseType) { - switch (exerciseType) { - case TEXT -> { - return athenaUrl + "/modules/text/" + textModuleName; - } - case PROGRAMMING -> { - return athenaUrl + "/modules/programming/" + programmingModuleName; - } - default -> throw new IllegalArgumentException("Exercise type not supported: " + exerciseType); - } - } -} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportService.java index 65eaba6ca262..3ea4376c2dc9 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportService.java @@ -63,7 +63,7 @@ public AthenaRepositoryExportService(ProgrammingExerciseRepository programmingEx * @throws AccessForbiddenException if the feedback suggestions are not enabled for the given exercise */ private void checkFeedbackSuggestionsEnabledElseThrow(Exercise exercise) { - if (!exercise.getFeedbackSuggestionsEnabled()) { + if (!exercise.isFeedbackSuggestionsEnabled()) { log.error("Feedback suggestions are not enabled for exercise {}", exercise.getId()); throw new ServiceUnavailableException("Feedback suggestions are not enabled for exercise"); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionService.java index e5526347a1c2..4a390e32f808 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionService.java @@ -31,7 +31,7 @@ public class AthenaSubmissionSelectionService { private final AthenaConnector connector; - private final AthenaModuleUrlHelper athenaModuleUrlHelper; + private final AthenaModuleService athenaModuleService; private final AthenaDTOConverter athenaDTOConverter; @@ -48,9 +48,9 @@ private record ResponseDTO(@JsonProperty("data") long submissionId // submission * Responses should be fast, and it's not too bad if it fails. Therefore, we use a very short timeout for requests. */ public AthenaSubmissionSelectionService(@Qualifier("veryShortTimeoutAthenaRestTemplate") RestTemplate veryShortTimeoutAthenaRestTemplate, - AthenaModuleUrlHelper athenaModuleUrlHelper, AthenaDTOConverter athenaDTOConverter) { + AthenaModuleService athenaModuleService, AthenaDTOConverter athenaDTOConverter) { connector = new AthenaConnector<>(veryShortTimeoutAthenaRestTemplate, ResponseDTO.class); - this.athenaModuleUrlHelper = athenaModuleUrlHelper; + this.athenaModuleService = athenaModuleService; this.athenaDTOConverter = athenaDTOConverter; } @@ -64,7 +64,7 @@ public AthenaSubmissionSelectionService(@Qualifier("veryShortTimeoutAthenaRestTe * @throws IllegalArgumentException if exercise isn't automatically assessable */ public Optional getProposedSubmissionId(Exercise exercise, List submissionIds) { - if (!exercise.getFeedbackSuggestionsEnabled()) { + if (!exercise.isFeedbackSuggestionsEnabled()) { throw new IllegalArgumentException("The Exercise does not have feedback suggestions enabled."); } if (submissionIds.isEmpty()) { @@ -78,7 +78,7 @@ public Optional getProposedSubmissionId(Exercise exercise, List subm try { final RequestDTO request = new RequestDTO(athenaDTOConverter.ofExercise(exercise), submissionIds); // allow no retries because this should be fast and it's not too bad if it fails - ResponseDTO response = connector.invokeWithRetry(athenaModuleUrlHelper.getAthenaModuleUrl(exercise.getExerciseType()) + "/select_submission", request, 0); + ResponseDTO response = connector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/select_submission", request, 0); log.info("Athena to calculate next proposes submissions responded: {}", response.submissionId); if (response.submissionId == -1) { return Optional.empty(); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSendingService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSendingService.java index 74b73ad28307..43e2306c4c32 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSendingService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSendingService.java @@ -37,7 +37,7 @@ public class AthenaSubmissionSendingService { private final AthenaConnector connector; - private final AthenaModuleUrlHelper athenaModuleUrlHelper; + private final AthenaModuleService athenaModuleService; private final AthenaDTOConverter athenaDTOConverter; @@ -45,10 +45,10 @@ public class AthenaSubmissionSendingService { * Creates a new AthenaSubmissionSendingService. */ public AthenaSubmissionSendingService(@Qualifier("athenaRestTemplate") RestTemplate athenaRestTemplate, SubmissionRepository submissionRepository, - AthenaModuleUrlHelper athenaModuleUrlHelper, AthenaDTOConverter athenaDTOConverter) { + AthenaModuleService athenaModuleService, AthenaDTOConverter athenaDTOConverter) { this.submissionRepository = submissionRepository; connector = new AthenaConnector<>(athenaRestTemplate, ResponseDTO.class); - this.athenaModuleUrlHelper = athenaModuleUrlHelper; + this.athenaModuleService = athenaModuleService; this.athenaDTOConverter = athenaDTOConverter; } @@ -74,7 +74,7 @@ public void sendSubmissions(Exercise exercise) { * @param maxRetries number of retries before the request will be canceled */ public void sendSubmissions(Exercise exercise, int maxRetries) { - if (!exercise.getFeedbackSuggestionsEnabled()) { + if (!exercise.isFeedbackSuggestionsEnabled()) { throw new IllegalArgumentException("The Exercise does not have feedback suggestions enabled."); } @@ -119,7 +119,7 @@ public void sendSubmissions(Exercise exercise, Set submissions, int try { final RequestDTO request = new RequestDTO(athenaDTOConverter.ofExercise(exercise), filteredSubmissions.stream().map((submission) -> athenaDTOConverter.ofSubmission(exercise.getId(), submission)).toList()); - ResponseDTO response = connector.invokeWithRetry(athenaModuleUrlHelper.getAthenaModuleUrl(exercise.getExerciseType()) + "/submissions", request, maxRetries); + ResponseDTO response = connector.invokeWithRetry(athenaModuleService.getAthenaModuleUrl(exercise) + "/submissions", request, maxRetries); log.info("Athena (calculating automatic feedback) responded: {}", response.data); } catch (NetworkingException error) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java index 14b42786a60d..5b7436180892 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java @@ -280,7 +280,8 @@ public ProgrammingExercise importProgrammingExercise(ProgrammingExercise origina if (newExercise.isExamExercise()) { // Disable feedback suggestions on exam exercises (currently not supported) - newExercise.setFeedbackSuggestionsEnabled(false); + // TODO Athena: Check that only allowed athena modules are used + newExercise.setFeedbackSuggestionModule(null); } final var importedProgrammingExercise = programmingExerciseImportBasicService.importProgrammingExerciseBasis(originalProgrammingExercise, newExercise); diff --git a/src/main/java/de/tum/in/www1/artemis/service/scheduled/AthenaScheduleService.java b/src/main/java/de/tum/in/www1/artemis/service/scheduled/AthenaScheduleService.java index 84f05a171f29..271a89b03053 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/scheduled/AthenaScheduleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/scheduled/AthenaScheduleService.java @@ -65,7 +65,7 @@ public void scheduleRunningExercisesOnStartup() { * @param exercise exercise to schedule Athena for */ public void scheduleExerciseForAthenaIfRequired(Exercise exercise) { - if (!exercise.getFeedbackSuggestionsEnabled()) { + if (!exercise.isFeedbackSuggestionsEnabled()) { cancelScheduledAthena(exercise.getId()); return; } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java index 7ccd9f7a3a5a..4f509aef4474 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java @@ -13,17 +13,15 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import de.tum.in.www1.artemis.domain.Exercise; -import de.tum.in.www1.artemis.domain.Submission; +import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.RepositoryType; import de.tum.in.www1.artemis.exception.NetworkingException; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.security.Role; -import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastTutor; -import de.tum.in.www1.artemis.security.annotations.EnforceNothing; -import de.tum.in.www1.artemis.security.annotations.ManualConfig; +import de.tum.in.www1.artemis.security.annotations.*; import de.tum.in.www1.artemis.service.AuthorizationCheckService; import de.tum.in.www1.artemis.service.connectors.athena.AthenaFeedbackSuggestionsService; +import de.tum.in.www1.artemis.service.connectors.athena.AthenaModuleService; import de.tum.in.www1.artemis.service.connectors.athena.AthenaRepositoryExportService; import de.tum.in.www1.artemis.service.dto.athena.ProgrammingFeedbackDTO; import de.tum.in.www1.artemis.service.dto.athena.TextFeedbackDTO; @@ -43,6 +41,8 @@ public class AthenaResource { @Value("${artemis.athena.secret}") private String athenaSecret; + private final CourseRepository courseRepository; + private final TextExerciseRepository textExerciseRepository; private final TextSubmissionRepository textSubmissionRepository; @@ -57,13 +57,16 @@ public class AthenaResource { private final AthenaRepositoryExportService athenaRepositoryExportService; + private final AthenaModuleService athenaModuleService; + /** * The AthenaResource provides an endpoint for the client to fetch feedback suggestions from Athena. */ - public AthenaResource(TextExerciseRepository textExerciseRepository, TextSubmissionRepository textSubmissionRepository, + public AthenaResource(CourseRepository courseRepository, TextExerciseRepository textExerciseRepository, TextSubmissionRepository textSubmissionRepository, ProgrammingExerciseRepository programmingExerciseRepository, ProgrammingSubmissionRepository programmingSubmissionRepository, AuthorizationCheckService authCheckService, AthenaFeedbackSuggestionsService athenaFeedbackSuggestionsService, - AthenaRepositoryExportService athenaRepositoryExportService) { + AthenaRepositoryExportService athenaRepositoryExportService, AthenaModuleService athenaModuleService) { + this.courseRepository = courseRepository; this.textExerciseRepository = textExerciseRepository; this.textSubmissionRepository = textSubmissionRepository; this.programmingExerciseRepository = programmingExerciseRepository; @@ -71,6 +74,7 @@ public AthenaResource(TextExerciseRepository textExerciseRepository, TextSubmiss this.authCheckService = authCheckService; this.athenaFeedbackSuggestionsService = athenaFeedbackSuggestionsService; this.athenaRepositoryExportService = athenaRepositoryExportService; + this.athenaModuleService = athenaModuleService; } @FunctionalInterface @@ -129,6 +133,34 @@ public ResponseEntity> getProgrammingFeedbackSugges athenaFeedbackSuggestionsService::getProgrammingFeedbackSuggestions); } + @GetMapping("athena/programming-exercises/{courseId}/available-modules") + @EnforceAtLeastEditor + public ResponseEntity> getAvailableModulesForProgrammingExercises(@PathVariable long courseId) { + Course course = courseRepository.findByIdElseThrow(courseId); + log.debug("REST request to get available Athena modules for programming exercises in Course {}", course.getTitle()); + // todo error handling + var modules = athenaModuleService.getAthenaProgrammingModulesForCourse(course); + return ResponseEntity.ok(modules); + } + + @GetMapping("athena/text-exercises/{courseId}/available-modules") + @EnforceAtLeastEditor + public ResponseEntity> getAvailableModulesForTextExercises(@PathVariable long courseId) { + Course course = courseRepository.findByIdElseThrow(courseId); + log.debug("REST request to get available Athena modules for text exercises in Course {}", course.getTitle()); + // todo error handling + var modules = athenaModuleService.getAthenaTextModulesForCourse(course); + return ResponseEntity.ok(modules); + } + + @GetMapping("public/athena/restricted-modules") + @EnforceNothing + @ManualConfig + public ResponseEntity> getRestrictedModules() { + // TODO Athena: Just for testing, remove afterwards + return ResponseEntity.ok(athenaModuleService.getRestrictedModules()); + } + /** * Check if the given auth header is valid for Athena, otherwise throw an exception. * diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java index 50d4ff884caf..6e50725e622a 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java @@ -43,6 +43,7 @@ import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastTutor; import de.tum.in.www1.artemis.service.*; +import de.tum.in.www1.artemis.service.connectors.athena.AthenaModuleService; import de.tum.in.www1.artemis.service.connectors.ci.CIUserManagementService; import de.tum.in.www1.artemis.service.connectors.vcs.VcsUserManagementService; import de.tum.in.www1.artemis.service.dto.StudentDTO; @@ -110,6 +111,8 @@ public class CourseResource { private final ConductAgreementService conductAgreementService; + private final Optional athenaModuleService; + @Value("${artemis.course-archives-path}") private String courseArchivesDirPath; @@ -121,7 +124,7 @@ public CourseResource(UserRepository userRepository, CourseService courseService AssessmentDashboardService assessmentDashboardService, ExerciseRepository exerciseRepository, Optional optionalCiUserManagementService, FileService fileService, TutorialGroupsConfigurationService tutorialGroupsConfigurationService, GradingScaleService gradingScaleService, CourseScoreCalculationService courseScoreCalculationService, GradingScaleRepository gradingScaleRepository, LearningPathService learningPathService, - ConductAgreementService conductAgreementService) { + ConductAgreementService conductAgreementService, Optional athenaModuleService) { this.courseService = courseService; this.courseRepository = courseRepository; this.exerciseService = exerciseService; @@ -142,6 +145,7 @@ public CourseResource(UserRepository userRepository, CourseService courseService this.gradingScaleRepository = gradingScaleRepository; this.learningPathService = learningPathService; this.conductAgreementService = conductAgreementService; + this.athenaModuleService = athenaModuleService; } /** @@ -166,6 +170,8 @@ public ResponseEntity updateCourse(@PathVariable Long courseId, @Request var timeZoneChanged = (existingCourse.getTimeZone() != null && courseUpdate.getTimeZone() != null && !existingCourse.getTimeZone().equals(courseUpdate.getTimeZone())); + var athenaModuleAccessChanged = existingCourse.getRestrictedAthenaModulesAccess() != courseUpdate.getRestrictedAthenaModulesAccess(); + if (!Objects.equals(existingCourse.getShortName(), courseUpdate.getShortName())) { throw new BadRequestAlertException("The course short name cannot be changed", Course.ENTITY_NAME, "shortNameCannotChange", true); } @@ -197,6 +203,11 @@ public ResponseEntity updateCourse(@PathVariable Long courseId, @Request if (!changedGroupNames.isEmpty()) { throw new BadRequestAlertException("You are not allowed to change the group names of a course", Course.ENTITY_NAME, "groupNamesCannotChange", true); } + // instructors are not allowed to change the access to restricted Athena modules + if (athenaModuleAccessChanged) { + throw new BadRequestAlertException("You are not allowed to change the access to restricted Athena modules of a course", Course.ENTITY_NAME, + "restrictedAthenaModulesAccessCannotChange", true); + } } if (courseUpdate.getPresentationScore() != null && courseUpdate.getPresentationScore() != 0) { @@ -252,6 +263,12 @@ public ResponseEntity updateCourse(@PathVariable Long courseId, @Request learningPathService.generateLearningPaths(courseWithCompetencies); } + // if access to restricted athena modules got disabled for the course, we need to set all exercises that use restricted modules to null + if (athenaModuleAccessChanged && !courseUpdate.getRestrictedAthenaModulesAccess()) { + // todo athena: revoke access for all course exercises that use restricted modules + athenaModuleService.ifPresent(ams -> ams.revokeAccessToRestrictedFeedbackSuggestionModules(result)); + } + // Based on the old instructors, editors and TAs, we can update all exercises in the course in the VCS (if necessary) // We need the old instructors, editors and TAs, so that the VCS user management service can determine which // users no longer have TA, editor or instructor rights in the related exercise repositories. diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingAssessmentResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingAssessmentResource.java index b073b827a208..d284362dee77 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingAssessmentResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingAssessmentResource.java @@ -250,7 +250,7 @@ public ResponseEntity deleteAssessment(@PathVariable Long participationId, * Send feedback to Athena (if enabled for both the Artemis instance and the exercise). */ private void sendFeedbackToAthena(final ProgrammingExercise exercise, final ProgrammingSubmission programmingSubmission, final List feedbacks) { - if (athenaFeedbackSendingService.isPresent() && exercise.getFeedbackSuggestionsEnabled()) { + if (athenaFeedbackSendingService.isPresent() && exercise.isFeedbackSuggestionsEnabled()) { athenaFeedbackSendingService.get().sendFeedback(exercise, programmingSubmission, feedbacks); } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java index 9c9a51002ac5..f2613c210cee 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java @@ -33,6 +33,7 @@ import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastTutor; import de.tum.in.www1.artemis.service.*; import de.tum.in.www1.artemis.service.connectors.GitService; +import de.tum.in.www1.artemis.service.connectors.athena.AthenaModuleService; import de.tum.in.www1.artemis.service.connectors.ci.ContinuousIntegrationService; import de.tum.in.www1.artemis.service.connectors.vcs.VersionControlService; import de.tum.in.www1.artemis.service.feature.Feature; @@ -113,6 +114,8 @@ public class ProgrammingExerciseResource { private final InstanceMessageSendService instanceMessageSendService; + private final Optional athenaModuleService; + public ProgrammingExerciseResource(ProgrammingExerciseRepository programmingExerciseRepository, ProgrammingExerciseTestCaseRepository programmingExerciseTestCaseRepository, UserRepository userRepository, AuthorizationCheckService authCheckService, CourseService courseService, Optional continuousIntegrationService, Optional versionControlService, ExerciseService exerciseService, @@ -122,7 +125,8 @@ public ProgrammingExerciseResource(ProgrammingExerciseRepository programmingExer GradingCriterionRepository gradingCriterionRepository, CourseRepository courseRepository, GitService gitService, AuxiliaryRepositoryService auxiliaryRepositoryService, SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository, TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository, ProfileService profileService, - BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository, ChannelRepository channelRepository, InstanceMessageSendService instanceMessageSendService) { + BuildLogStatisticsEntryRepository buildLogStatisticsEntryRepository, ChannelRepository channelRepository, InstanceMessageSendService instanceMessageSendService, + Optional athenaModuleService) { this.programmingExerciseTaskService = programmingExerciseTaskService; this.profileService = profileService; this.programmingExerciseRepository = programmingExerciseRepository; @@ -147,6 +151,7 @@ public ProgrammingExerciseResource(ProgrammingExerciseRepository programmingExer this.buildLogStatisticsEntryRepository = buildLogStatisticsEntryRepository; this.channelRepository = channelRepository; this.instanceMessageSendService = instanceMessageSendService; + this.athenaModuleService = athenaModuleService; } /** @@ -201,6 +206,9 @@ public ResponseEntity createProgrammingExercise(@RequestBod authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, null); programmingExerciseService.validateNewProgrammingExerciseSettings(programmingExercise, course); + // TODO Athena: Check that only allowed athena modules are used + athenaModuleService.ifPresent(ams -> ams.checkHasAccessToAthenaModule(programmingExercise, course, ENTITY_NAME)); + try { // Setup all repositories etc ProgrammingExercise newProgrammingExercise = programmingExerciseService.createProgrammingExercise(programmingExercise, false); @@ -272,6 +280,9 @@ public ResponseEntity updateProgrammingExercise(@RequestBod // Forbid conversion between normal course exercise and exam exercise exerciseService.checkForConversionBetweenExamAndCourseExercise(updatedProgrammingExercise, programmingExerciseBeforeUpdate, ENTITY_NAME); + // Check that only allowed Athena modules are used + athenaModuleService.ifPresent(ams -> ams.checkHasAccessToAthenaModule(updatedProgrammingExercise, course, ENTITY_NAME)); + // Ignore changes to the default branch updatedProgrammingExercise.setBranch(programmingExerciseBeforeUpdate.getBranch()); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/TextAssessmentResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/TextAssessmentResource.java index b80032035d62..f6af1cc4447f 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/TextAssessmentResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/TextAssessmentResource.java @@ -489,7 +489,7 @@ private void saveTextBlocks(final Set textBlocks, final TextSubmissio * Send feedback to Athena (if enabled for both the Artemis instance and the exercise). */ private void sendFeedbackToAthena(final TextExercise exercise, final TextSubmission textSubmission, final List feedbacks) { - if (athenaFeedbackSendingService.isPresent() && exercise.getFeedbackSuggestionsEnabled()) { + if (athenaFeedbackSendingService.isPresent() && exercise.isFeedbackSuggestionsEnabled()) { athenaFeedbackSendingService.get().sendFeedback(exercise, textSubmission, feedbacks); } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java index fac2c717f077..8fab23c0ce6a 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java @@ -31,6 +31,7 @@ import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.security.annotations.*; import de.tum.in.www1.artemis.service.*; +import de.tum.in.www1.artemis.service.connectors.athena.AthenaModuleService; import de.tum.in.www1.artemis.service.export.TextSubmissionExportService; import de.tum.in.www1.artemis.service.feature.Feature; import de.tum.in.www1.artemis.service.feature.FeatureToggle; @@ -111,6 +112,8 @@ public class TextExerciseResource { private final ChannelRepository channelRepository; + private final Optional athenaModuleService; + public TextExerciseResource(TextExerciseRepository textExerciseRepository, TextExerciseService textExerciseService, FeedbackRepository feedbackRepository, ExerciseDeletionService exerciseDeletionService, PlagiarismResultRepository plagiarismResultRepository, UserRepository userRepository, AuthorizationCheckService authCheckService, CourseService courseService, StudentParticipationRepository studentParticipationRepository, @@ -118,7 +121,7 @@ public TextExerciseResource(TextExerciseRepository textExerciseRepository, TextE TextSubmissionExportService textSubmissionExportService, ExampleSubmissionRepository exampleSubmissionRepository, ExerciseService exerciseService, GradingCriterionRepository gradingCriterionRepository, TextBlockRepository textBlockRepository, GroupNotificationScheduleService groupNotificationScheduleService, InstanceMessageSendService instanceMessageSendService, PlagiarismDetectionService plagiarismDetectionService, CourseRepository courseRepository, - ChannelService channelService, ChannelRepository channelRepository) { + ChannelService channelService, ChannelRepository channelRepository, Optional athenaModuleService) { this.feedbackRepository = feedbackRepository; this.exerciseDeletionService = exerciseDeletionService; this.plagiarismResultRepository = plagiarismResultRepository; @@ -142,6 +145,7 @@ public TextExerciseResource(TextExerciseRepository textExerciseRepository, TextE this.courseRepository = courseRepository; this.channelService = channelService; this.channelRepository = channelRepository; + this.athenaModuleService = athenaModuleService; } /** @@ -173,6 +177,9 @@ public ResponseEntity createTextExercise(@RequestBody TextExercise // Check that the user is authorized to create the exercise authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, null); + // TODO Athena: Check that only allowed athena modules are used + athenaModuleService.ifPresent(ams -> ams.checkHasAccessToAthenaModule(textExercise, course, ENTITY_NAME)); + TextExercise result = textExerciseRepository.save(textExercise); channelService.createExerciseChannel(result, Optional.ofNullable(textExercise.getChannelName())); @@ -219,6 +226,9 @@ public ResponseEntity updateTextExercise(@RequestBody TextExercise // Forbid conversion between normal course exercise and exam exercise exerciseService.checkForConversionBetweenExamAndCourseExercise(textExercise, textExerciseBeforeUpdate, ENTITY_NAME); + // TODO Athena: Check that only allowed athena modules are used + athenaModuleService.ifPresent(ams -> ams.checkHasAccessToAthenaModule(textExercise, textExerciseBeforeUpdate.getCourseViaExerciseGroupOrCourseMember(), ENTITY_NAME)); + channelService.updateExerciseChannel(textExerciseBeforeUpdate, textExercise); TextExercise updatedTextExercise = textExerciseRepository.save(textExercise); @@ -447,6 +457,14 @@ public ResponseEntity importExercise(@PathVariable long sourceExer // validates general settings: points, dates importedExercise.validateGeneralSettings(); + // TODO Athena: Check that only allowed athena modules are used, if not we disable feedback suggestions for the imported exercise + try { + athenaModuleService.ifPresent(ams -> ams.checkHasAccessToAthenaModule(importedExercise, importedExercise.getCourseViaExerciseGroupOrCourseMember(), ENTITY_NAME)); + } + catch (BadRequestAlertException e) { + importedExercise.setFeedbackSuggestionModule(null); + } + final var newTextExercise = textExerciseImportService.importTextExercise(originalTextExercise, importedExercise); textExerciseRepository.save(newTextExercise); return ResponseEntity.created(new URI("/api/text-exercises/" + newTextExercise.getId())).body(newTextExercise); diff --git a/src/main/resources/config/application-artemis.yml b/src/main/resources/config/application-artemis.yml index 07c444320bbe..f507fdb35ad1 100644 --- a/src/main/resources/config/application-artemis.yml +++ b/src/main/resources/config/application-artemis.yml @@ -93,9 +93,7 @@ artemis: athena: url: http://localhost:5000 secret: abcdef12345 - modules: - text: module_text_cofee - programming: module_programming_themisml + restricted-modules: module_text_llm,module_programming_llm apollon: conversion-service-url: http://localhost:8080 diff --git a/src/main/resources/config/liquibase/changelog/20231212191800_changelog.xml b/src/main/resources/config/liquibase/changelog/20231212191800_changelog.xml new file mode 100644 index 000000000000..9431d137cc11 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20231212191800_changelog.xml @@ -0,0 +1,20 @@ + + + + Add new column to the course table to persist if a course has access to the restricted Athena modules. + + + + + + + + Add new column to the exercise table to persist the athena module that should be used for feedback suggestions and remove the feedback_suggestions_enabled column of type boolean, which is not needed anymore. + + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 99142f54d90e..05ed3d6711bd 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -74,6 +74,7 @@ + diff --git a/src/main/webapp/app/app.constants.ts b/src/main/webapp/app/app.constants.ts index c27e1de79947..6015573ca926 100644 --- a/src/main/webapp/app/app.constants.ts +++ b/src/main/webapp/app/app.constants.ts @@ -26,3 +26,5 @@ export const PROFILE_LOCALCI = 'localci'; export const PROFILE_AEOLUS = 'aeolus'; export const PROFILE_LTI = 'lti'; + +export const PROFILE_ATHENA = 'athena'; diff --git a/src/main/webapp/app/assessment/athena.service.ts b/src/main/webapp/app/assessment/athena.service.ts index 11c7ff211fd2..d18f82a0ec93 100644 --- a/src/main/webapp/app/assessment/athena.service.ts +++ b/src/main/webapp/app/assessment/athena.service.ts @@ -8,6 +8,7 @@ import { FEEDBACK_SUGGESTION_ACCEPTED_IDENTIFIER, FEEDBACK_SUGGESTION_IDENTIFIER import { TextBlock } from 'app/entities/text-block.model'; import { TextBlockRef } from 'app/entities/text-block-ref.model'; import { TextSubmission } from 'app/entities/text-submission.model'; +import { PROFILE_ATHENA } from 'app/app.constants'; @Injectable({ providedIn: 'root' }) export class AthenaService { @@ -19,19 +20,32 @@ export class AthenaService { ) {} public isEnabled(): Observable { - return this.profileService.getProfileInfo().pipe(switchMap((profileInfo) => of(profileInfo.activeProfiles.includes('athena')))); + return this.profileService.getProfileInfo().pipe(switchMap((profileInfo) => of(profileInfo.activeProfiles.includes(PROFILE_ATHENA)))); + } + + // TODO Athena: Add methods to get available modules + public getAvailableModules(courseId: number, exercise: Exercise): Observable { + return this.isEnabled().pipe( + switchMap((isAthenaEnabled) => { + if (!isAthenaEnabled) { + return of([] as string[]); + } + return this.http + .get(`${this.resourceUrl}/${exercise.type}-exercises/${courseId}/available-modules`, { observe: 'response' }) + .pipe(switchMap((res: HttpResponse) => of(res.body!))); + }), + ); } /** - * Get feedback suggestions for the given submission from Athena - for programming exercises - * Currently, this is separate for programming and text exercises (will be changed) + * Get feedback suggestions for the given submission from Athena * * @param exercise * @param submissionId the id of the submission * @return observable that emits the feedback suggestions */ private getFeedbackSuggestions(exercise: Exercise, submissionId: number): Observable { - if (!exercise.feedbackSuggestionsEnabled) { + if (!exercise.feedbackSuggestionModule) { return of([]); } return this.isEnabled().pipe( diff --git a/src/main/webapp/app/course/manage/course-update.component.html b/src/main/webapp/app/course/manage/course-update.component.html index 1ec223d212b4..85ac7e8b43bb 100644 --- a/src/main/webapp/app/course/manage/course-update.component.html +++ b/src/main/webapp/app/course/manage/course-update.component.html @@ -361,6 +361,24 @@
+
+ + + +
Course Details: + +
+
Access to Restricted Athena Modules Enabled
+
+ {{ 'global.generic.yes' | artemisTranslate }} + {{ 'global.generic.no' | artemisTranslate }} +
+
Iris Chat
diff --git a/src/main/webapp/app/course/manage/detail/course-detail.component.ts b/src/main/webapp/app/course/manage/detail/course-detail.component.ts index 522ff025c1a5..40b21635a760 100644 --- a/src/main/webapp/app/course/manage/detail/course-detail.component.ts +++ b/src/main/webapp/app/course/manage/detail/course-detail.component.ts @@ -1,7 +1,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; -import { PROFILE_LTI } from 'app/app.constants'; +import { PROFILE_ATHENA, PROFILE_LTI } from 'app/app.constants'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { Subscription } from 'rxjs'; import { Course } from 'app/entities/course.model'; @@ -50,6 +50,7 @@ export class CourseDetailComponent implements OnInit, OnDestroy { irisHestiaEnabled = false; irisCodeEditorEnabled = false; ltiEnabled = false; + isAthenaEnabled = false; isAdmin = false; @@ -83,6 +84,7 @@ export class CourseDetailComponent implements OnInit, OnDestroy { ngOnInit() { this.profileService.getProfileInfo().subscribe((profileInfo) => { this.ltiEnabled = profileInfo.activeProfiles.includes(PROFILE_LTI); + this.isAthenaEnabled = profileInfo.activeProfiles.includes(PROFILE_ATHENA); this.irisEnabled = profileInfo.activeProfiles.includes('iris'); if (this.irisEnabled) { this.irisSettingsService.getGlobalSettings().subscribe((settings) => { diff --git a/src/main/webapp/app/entities/course.model.ts b/src/main/webapp/app/entities/course.model.ts index 27f5dfd1c985..183fc710a143 100644 --- a/src/main/webapp/app/entities/course.model.ts +++ b/src/main/webapp/app/entities/course.model.ts @@ -88,6 +88,7 @@ export class Course implements BaseEntity { public maxRequestMoreFeedbackTimeDays?: number; public maxPoints?: number; public accuracyOfScores?: number; + public restrictedAthenaModulesAccess?: boolean; public tutorialGroupsConfiguration?: TutorialGroupsConfiguration; // Note: Currently just used in the scope of the tutorial groups feature public timeZone?: string; @@ -138,6 +139,7 @@ export class Course implements BaseEntity { this.requestMoreFeedbackEnabled = true; // default value this.maxRequestMoreFeedbackTimeDays = 7; // default value this.accuracyOfScores = 1; // default value + this.restrictedAthenaModulesAccess = false; // default value this.courseInformationSharingConfiguration = CourseInformationSharingConfiguration.COMMUNICATION_AND_MESSAGING; // default value } diff --git a/src/main/webapp/app/entities/exercise.model.ts b/src/main/webapp/app/entities/exercise.model.ts index 51cdcaf0a763..e13a54e20e6a 100644 --- a/src/main/webapp/app/entities/exercise.model.ts +++ b/src/main/webapp/app/entities/exercise.model.ts @@ -125,7 +125,7 @@ export abstract class Exercise implements BaseEntity { // helper attributes public secondCorrectionEnabled = false; - public feedbackSuggestionsEnabled? = false; + public feedbackSuggestionModule?: string; public isAtLeastTutor?: boolean; public isAtLeastEditor?: boolean; public isAtLeastInstructor?: boolean; diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html index c42e7fa55022..5ec0ba04d026 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html @@ -252,7 +252,11 @@

Exercise Details

Enable feedback suggestions from Athena
- {{ (programmingExercise.feedbackSuggestionsEnabled ? 'global.generic.yes' : 'global.generic.no') | artemisTranslate }} + {{ + !!programmingExercise.feedbackSuggestionModule + ? ('global.generic.yes' | artemisTranslate) + ' - ' + programmingExercise.feedbackSuggestionModule + : ('global.generic.no' | artemisTranslate) + }}
Automatic Submission Run After Due Date
diff --git a/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.html b/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.html index 1b094b62c4fa..aaf508b0ce93 100644 --- a/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.html +++ b/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.html @@ -180,23 +180,5 @@
>
- -
- - - -
+
diff --git a/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.ts b/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.ts index e23ae1d82755..c78bcd6b3ed9 100644 --- a/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.ts @@ -77,7 +77,7 @@ export class ProgrammingExerciseLifecycleComponent implements OnInit, OnChanges this.exercise.assessmentType = AssessmentType.AUTOMATIC; this.exercise.assessmentDueDate = undefined; this.exercise.allowComplaintsForAutomaticAssessments = false; - this.exercise.feedbackSuggestionsEnabled = false; + this.exercise.feedbackSuggestionModule = undefined; } else { this.exercise.assessmentType = AssessmentType.SEMI_AUTOMATIC; this.exercise.allowComplaintsForAutomaticAssessments = false; diff --git a/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.module.ts b/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.module.ts index 5029c65a331b..3a6a81c38b82 100644 --- a/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.module.ts +++ b/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.module.ts @@ -4,9 +4,10 @@ import { ArtemisSharedComponentModule } from 'app/shared/components/shared-compo import { ProgrammingExerciseTestScheduleDatePickerComponent } from 'app/exercises/programming/shared/lifecycle/programming-exercise-test-schedule-date-picker.component'; import { ArtemisSharedModule } from 'app/shared/shared.module'; import { OwlDateTimeModule } from '@danielmoncada/angular-datetime-picker'; +import { ExerciseFeedbackSuggestionOptionsModule } from 'app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.module'; @NgModule({ - imports: [ArtemisSharedComponentModule, OwlDateTimeModule, ArtemisSharedModule], + imports: [ArtemisSharedComponentModule, OwlDateTimeModule, ArtemisSharedModule, ExerciseFeedbackSuggestionOptionsModule], declarations: [ProgrammingExerciseLifecycleComponent, ProgrammingExerciseTestScheduleDatePickerComponent], exports: [ProgrammingExerciseLifecycleComponent], }) diff --git a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html new file mode 100644 index 000000000000..c2bb9832fc52 --- /dev/null +++ b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html @@ -0,0 +1,28 @@ +
+
+ + + +
+
+ + +
+
+ + diff --git a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts new file mode 100644 index 000000000000..05c8361d7970 --- /dev/null +++ b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts @@ -0,0 +1,63 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { AssessmentType } from 'app/entities/assessment-type.model'; +import { Exercise, ExerciseType } from 'app/entities/exercise.model'; +import { Observable } from 'rxjs'; +import { AthenaService } from 'app/assessment/athena.service'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + selector: 'jhi-exercise-feedback-suggestion-options', + templateUrl: './exercise-feedback-suggestion-options.component.html', +}) +export class ExerciseFeedbackSuggestionOptionsComponent implements OnInit { + @Input() exercise: Exercise; + @Input() readOnly?: boolean; + + readonly assessmentType: AssessmentType; + + isAthenaEnabled$: Observable | undefined; + modulesAvailable: boolean; + availableAthenaModules: string[]; + + constructor( + private athenaService: AthenaService, + private activatedRoute: ActivatedRoute, + ) {} + + ngOnInit(): void { + const courseId = Number(this.activatedRoute.snapshot.paramMap.get('courseId')); + this.athenaService.getAvailableModules(courseId, this.exercise).subscribe((modules) => { + this.availableAthenaModules = modules; + this.modulesAvailable = modules.length > 0; + }); + this.isAthenaEnabled$ = this.athenaService.isEnabled(); + } + + checkboxDisabled() { + if (this.exercise.type == ExerciseType.PROGRAMMING) { + return this.exercise.assessmentType == AssessmentType.AUTOMATIC || this.readOnly; + } + return false; + } + + getCheckboxLabelStyle() { + if (this.exercise.type == ExerciseType.PROGRAMMING && this.exercise.assessmentType == AssessmentType.AUTOMATIC) { + return { color: 'grey' }; + } + return {}; + } + + toggleFeedbackSuggestions(event: any) { + if (event.target.checked) { + this.exercise.feedbackSuggestionModule = this.availableAthenaModules.first(); + } else { + this.exercise.feedbackSuggestionModule = undefined; + } + } + + buttonClick() { + console.log(this.exercise.feedbackSuggestionModule); + } + + protected readonly AssessmentType = AssessmentType; +} diff --git a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.module.ts b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.module.ts new file mode 100644 index 000000000000..4e2acac9776b --- /dev/null +++ b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { ArtemisSharedCommonModule } from 'app/shared/shared-common.module'; +import { ExerciseFeedbackSuggestionOptionsComponent } from 'app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component'; +import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; + +@NgModule({ + declarations: [ExerciseFeedbackSuggestionOptionsComponent], + imports: [ArtemisSharedCommonModule, ArtemisSharedComponentModule], + exports: [ExerciseFeedbackSuggestionOptionsComponent], +}) +export class ExerciseFeedbackSuggestionOptionsModule {} diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.html b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.html index 26589566a8b1..600456210280 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.html +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.html @@ -35,7 +35,11 @@

Exercise Details

Enable feedback suggestions from Athena
- {{ (textExercise.feedbackSuggestionsEnabled ? 'global.generic.yes' : 'global.generic.no') | artemisTranslate }} + {{ + !!textExercise.feedbackSuggestionModule + ? ('global.generic.yes' | artemisTranslate) + ' - ' + textExercise.feedbackSuggestionModule + : ('global.generic.no' | artemisTranslate) + }}
Example Solution
diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html index 8067bb405083..80dbdfa616d4 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html @@ -134,20 +134,7 @@

-
- - - -
- +
diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.module.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.module.ts index 4f616c400d0c..77ca0281fc21 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.module.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise.module.ts @@ -27,6 +27,7 @@ import { ExerciseCategoriesModule } from 'app/shared/exercise-categories/exercis import { ExerciseTitleChannelNameModule } from 'app/exercises/shared/exercise-title-channel-name/exercise-title-channel-name.module'; import { ExerciseUpdateNotificationModule } from 'app/exercises/shared/exercise-update-notification/exercise-update-notification.module'; import { ExerciseUpdatePlagiarismModule } from 'app/exercises/shared/plagiarism/exercise-update-plagiarism/exercise-update-plagiarism.module'; +import { ExerciseFeedbackSuggestionOptionsModule } from 'app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.module'; const ENTITY_STATES = [...textExerciseRoute]; @@ -54,6 +55,7 @@ const ENTITY_STATES = [...textExerciseRoute]; ExerciseTitleChannelNameModule, ExerciseUpdateNotificationModule, ExerciseUpdatePlagiarismModule, + ExerciseFeedbackSuggestionOptionsModule, ], declarations: [TextExerciseComponent, TextExerciseDetailComponent, TextExerciseUpdateComponent, TextExerciseRowButtonsComponent], exports: [TextExerciseComponent], diff --git a/src/main/webapp/i18n/de/course.json b/src/main/webapp/i18n/de/course.json index 736e7f87c5f0..308d3794a2f0 100644 --- a/src/main/webapp/i18n/de/course.json +++ b/src/main/webapp/i18n/de/course.json @@ -82,6 +82,10 @@ "label": "Lernpfade aktiviert", "tooltip": "Ermöglicht es aus Kompetenzen und deren Lerninhalten einen individuellen Lernpfad für Studierende zu generieren." }, + "restrictedAthenaModulesAccess": { + "label": "Zugriff auf eingeschränkte Athena-Module aktiviert", + "tooltip": "Wenn diese Option ausgewählt ist, können die Übungen des Kurses auch geschützte Athena-Module für die Feedbackerstellung verwenden." + }, "onlineCourse": { "title": "Onlinekurs", "description": "Aktiviere diese Checkbox für externe Onlinekurse (z.B. auf edX/Moodle), damit Studierende diesen Artemis-Kurs (über die LTI Schnittstelle) nutzen können. Dies ermöglicht auch eine automatische Kontoerstellung, sofern dies vom Artemis Systemadministrator eingestellt ist. Du kannst dies auf der Seite LTI-Konfiguration weiter konfigurieren." diff --git a/src/main/webapp/i18n/en/course.json b/src/main/webapp/i18n/en/course.json index 7454a6b08cfa..dcd9d582cba3 100644 --- a/src/main/webapp/i18n/en/course.json +++ b/src/main/webapp/i18n/en/course.json @@ -82,6 +82,10 @@ "label": "Learning Paths Enabled", "tooltip": "Enables generation of individualized learning paths for students consisting of competencies and their linked learning materials." }, + "restrictedAthenaModulesAccess": { + "label": "Access to Restricted Athena Modules Enabled", + "tooltip": "If this option is selected, the course's exercises can also use restricted Athena modules for feedback generation." + }, "onlineCourse": { "title": "Online Course", "description": "Activate this checkbox for external online courses (e.g. on edX/Moodle) so that students can use this Artemis course (via the LTI interface). This also allows automatic account creation if enabled by the Artemis system administrator. You can further configure this in the LTI Configuration page." diff --git a/src/test/java/de/tum/in/www1/artemis/connector/AthenaRequestMockProvider.java b/src/test/java/de/tum/in/www1/artemis/connector/AthenaRequestMockProvider.java index 2ac32499d48d..2e88f46846a8 100644 --- a/src/test/java/de/tum/in/www1/artemis/connector/AthenaRequestMockProvider.java +++ b/src/test/java/de/tum/in/www1/artemis/connector/AthenaRequestMockProvider.java @@ -46,6 +46,10 @@ public class AthenaRequestMockProvider { private AutoCloseable closeable; + public static final String ATHENA_MODULE_TEXT_TEST = "module_text_test"; + + public static final String ATHENA_MODULE_PROGRAMMING_TEST = "module_programming_test"; + public AthenaRequestMockProvider(@Qualifier("athenaRestTemplate") RestTemplate restTemplate, @Qualifier("shortTimeoutAthenaRestTemplate") RestTemplate shortTimeoutRestTemplate, @Qualifier("veryShortTimeoutAthenaRestTemplate") RestTemplate veryShortTimeoutRestTemplate) { this.restTemplate = restTemplate; diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/AthenaResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/AthenaResourceIntegrationTest.java index 8e532a6ae1e3..1d6c016f39c3 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/AthenaResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/AthenaResourceIntegrationTest.java @@ -1,5 +1,7 @@ package de.tum.in.www1.artemis.exercise; +import static de.tum.in.www1.artemis.connector.AthenaRequestMockProvider.ATHENA_MODULE_PROGRAMMING_TEST; +import static de.tum.in.www1.artemis.connector.AthenaRequestMockProvider.ATHENA_MODULE_TEXT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.time.ZonedDateTime; @@ -61,6 +63,9 @@ class AthenaResourceIntegrationTest extends AbstractAthenaTest { @Autowired private ProgrammingExerciseRepository programmingExerciseRepository; + @Autowired + private TextExerciseRepository textExerciseRepository; + @Autowired private FeedbackRepository feedbackRepository; @@ -105,6 +110,10 @@ protected void initTestCase() { @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void testGetFeedbackSuggestionsSuccessText() throws Exception { + // Enable Athena for the exercise + textExercise.setFeedbackSuggestionModule(ATHENA_MODULE_TEXT_TEST); + textExerciseRepository.save(textExercise); + athenaRequestMockProvider.mockGetFeedbackSuggestionsAndExpect("text"); List response = request.getList("/api/athena/text-exercises/" + textExercise.getId() + "/submissions/" + textSubmission.getId() + "/feedback-suggestions", HttpStatus.OK, Feedback.class); @@ -114,6 +123,10 @@ void testGetFeedbackSuggestionsSuccessText() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void testGetFeedbackSuggestionsSuccessProgramming() throws Exception { + // Enable Athena for the exercise + programmingExercise.setFeedbackSuggestionModule(ATHENA_MODULE_PROGRAMMING_TEST); + programmingExerciseRepository.save(programmingExercise); + athenaRequestMockProvider.mockGetFeedbackSuggestionsAndExpect("programming"); List response = request.getList( "/api/athena/programming-exercises/" + programmingExercise.getId() + "/submissions/" + programmingSubmission.getId() + "/feedback-suggestions", HttpStatus.OK, @@ -175,7 +188,7 @@ void testGetFeedbackSuggestionsAthenaEnabled() throws Exception { feedbackRepository.save(feedback); // Enable Athena for the exercise - programmingExercise.setFeedbackSuggestionsEnabled(true); + programmingExercise.setFeedbackSuggestionModule(ATHENA_MODULE_PROGRAMMING_TEST); programmingExerciseRepository.save(programmingExercise); Result response = request.putWithResponseBody("/api/participations/" + participation.getId() + "/manual-results?submit=true", result, Result.class, HttpStatus.OK); @@ -188,7 +201,7 @@ void testGetFeedbackSuggestionsAthenaEnabled() throws Exception { @ValueSource(strings = { "repository/template", "repository/solution", "repository/tests" }) void testRepositoryExportEndpoint(String urlSuffix) throws Exception { // Enable Athena for the exercise - programmingExercise.setFeedbackSuggestionsEnabled(true); + programmingExercise.setFeedbackSuggestionModule(ATHENA_MODULE_PROGRAMMING_TEST); programmingExerciseRepository.save(programmingExercise); // Add Git repo for export @@ -223,7 +236,7 @@ void testRepositoryExportEndpointsFailWithWrongAuthentication(String urlSuffix) authHeaders.add("Authorization", athenaSecret + "-wrong"); // Enable Athena for the exercise - programmingExercise.setFeedbackSuggestionsEnabled(true); + programmingExercise.setFeedbackSuggestionModule(ATHENA_MODULE_PROGRAMMING_TEST); programmingExerciseRepository.save(programmingExercise); // Expect status 403 because the Authorization header is wrong diff --git a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSendingServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSendingServiceTest.java index 4a6aac4de9df..9302beffc342 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSendingServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSendingServiceTest.java @@ -1,5 +1,7 @@ package de.tum.in.www1.artemis.service.connectors.athena; +import static de.tum.in.www1.artemis.connector.AthenaRequestMockProvider.ATHENA_MODULE_PROGRAMMING_TEST; +import static de.tum.in.www1.artemis.connector.AthenaRequestMockProvider.ATHENA_MODULE_TEXT_TEST; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.when; import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath; @@ -27,7 +29,7 @@ class AthenaFeedbackSendingServiceTest extends AbstractAthenaTest { @Autowired - private AthenaModuleUrlHelper athenaModuleUrlHelper; + private AthenaModuleService athenaModuleService; @Mock private TextBlockRepository textBlockRepository; @@ -62,13 +64,13 @@ class AthenaFeedbackSendingServiceTest extends AbstractAthenaTest { @BeforeEach void setUp() { - athenaFeedbackSendingService = new AthenaFeedbackSendingService(athenaRequestMockProvider.getRestTemplate(), athenaModuleUrlHelper, + athenaFeedbackSendingService = new AthenaFeedbackSendingService(athenaRequestMockProvider.getRestTemplate(), athenaModuleService, new AthenaDTOConverter(textBlockRepository, textExerciseRepository, programmingExerciseRepository)); athenaRequestMockProvider.enableMockingOfRequests(); textExercise = textExerciseUtilService.createSampleTextExercise(null); - textExercise.setFeedbackSuggestionsEnabled(true); + textExercise.setFeedbackSuggestionModule(ATHENA_MODULE_TEXT_TEST); when(textExerciseRepository.findByIdWithGradingCriteriaElseThrow(textExercise.getId())).thenReturn(textExercise); textSubmission = new TextSubmission(2L).text("Test - This is what the feedback references - Submission"); @@ -86,7 +88,7 @@ void setUp() { result.setParticipation(participation); programmingExercise = programmingExerciseUtilService.createSampleProgrammingExercise(); - programmingExercise.setFeedbackSuggestionsEnabled(true); + programmingExercise.setFeedbackSuggestionModule(ATHENA_MODULE_PROGRAMMING_TEST); when(programmingExerciseRepository.findByIdWithGradingCriteriaElseThrow(programmingExercise.getId())).thenReturn(programmingExercise); programmingSubmission = new ProgrammingSubmission(); @@ -184,9 +186,9 @@ void testEmptyFeedbackNotSending() { @Test void testSendFeedbackWithFeedbackSuggestionsDisabled() { - textExercise.setFeedbackSuggestionsEnabled(false); + textExercise.setFeedbackSuggestionModule(null); assertThatThrownBy(() -> athenaFeedbackSendingService.sendFeedback(textExercise, textSubmission, List.of(textFeedback))).isInstanceOf(IllegalArgumentException.class); - programmingExercise.setFeedbackSuggestionsEnabled(false); + programmingExercise.setFeedbackSuggestionModule(null); assertThatThrownBy(() -> athenaFeedbackSendingService.sendFeedback(programmingExercise, programmingSubmission, List.of(programmingFeedback))) .isInstanceOf(IllegalArgumentException.class); } diff --git a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSuggestionsServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSuggestionsServiceTest.java index 51dea71ae4f5..a6c5c2605829 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSuggestionsServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSuggestionsServiceTest.java @@ -1,5 +1,7 @@ package de.tum.in.www1.artemis.service.connectors.athena; +import static de.tum.in.www1.artemis.connector.AthenaRequestMockProvider.ATHENA_MODULE_PROGRAMMING_TEST; +import static de.tum.in.www1.artemis.connector.AthenaRequestMockProvider.ATHENA_MODULE_TEXT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath; @@ -47,10 +49,12 @@ void setUp() { athenaRequestMockProvider.enableMockingOfRequests(); textExercise = textExerciseUtilService.createSampleTextExercise(null); + textExercise.setFeedbackSuggestionModule(ATHENA_MODULE_TEXT_TEST); textSubmission = new TextSubmission(2L).text("This is a text submission"); textSubmission.setParticipation(new StudentParticipation().exercise(textExercise)); programmingExercise = programmingExerciseUtilService.createSampleProgrammingExercise(); + programmingExercise.setFeedbackSuggestionModule(ATHENA_MODULE_PROGRAMMING_TEST); programmingSubmission = new ProgrammingSubmission(); programmingSubmission.setId(3L); programmingSubmission.setParticipation(new StudentParticipation().exercise(programmingExercise)); diff --git a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportServiceTest.java index c6e0bcf70550..20be1f0dfde3 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportServiceTest.java @@ -1,5 +1,6 @@ package de.tum.in.www1.artemis.service.connectors.athena; +import static de.tum.in.www1.artemis.connector.AthenaRequestMockProvider.ATHENA_MODULE_PROGRAMMING_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -61,7 +62,7 @@ void initTestCase() throws Exception { void shouldExportRepository() throws Exception { Course course = programmingExerciseUtilService.addCourseWithOneProgrammingExercise(); var programmingExercise = programmingExerciseRepository.findByCourseIdWithLatestResultForTemplateSolutionParticipations(course.getId()).stream().iterator().next(); - programmingExercise.setFeedbackSuggestionsEnabled(true); + programmingExercise.setFeedbackSuggestionModule(ATHENA_MODULE_PROGRAMMING_TEST); programmingExerciseUtilService.addTemplateParticipationForProgrammingExercise(programmingExercise); programmingExerciseUtilService.addSolutionParticipationForProgrammingExercise(programmingExercise); var programmingExerciseWithId = programmingExerciseRepository.save(programmingExercise); @@ -85,7 +86,7 @@ void shouldExportRepository() throws Exception { @Test void shouldThrowServiceUnavailableWhenFeedbackSuggestionsNotEnabled() { var programmingExercise = new ProgrammingExercise(); - programmingExercise.setFeedbackSuggestionsEnabled(false); + programmingExercise.setFeedbackSuggestionModule(null); var programmingExerciseWithId = programmingExerciseRepository.save(programmingExercise); assertThatExceptionOfType(ServiceUnavailableException.class) diff --git a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionServiceTest.java index f33abbd3775d..3358e1859eaa 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionServiceTest.java @@ -1,5 +1,7 @@ package de.tum.in.www1.artemis.service.connectors.athena; +import static de.tum.in.www1.artemis.connector.AthenaRequestMockProvider.ATHENA_MODULE_PROGRAMMING_TEST; +import static de.tum.in.www1.artemis.connector.AthenaRequestMockProvider.ATHENA_MODULE_TEXT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -55,14 +57,14 @@ void setUp() { athenaRequestMockProvider.enableMockingOfRequests(); textExercise = textExerciseUtilService.createSampleTextExercise(null); - textExercise.setFeedbackSuggestionsEnabled(true); + textExercise.setFeedbackSuggestionModule(ATHENA_MODULE_TEXT_TEST); textExercise.setGradingCriteria(List.of(new GradingCriterion())); textExerciseRepository.save(textExercise); textSubmission1 = new TextSubmission(1L); textSubmission2 = new TextSubmission(2L); programmingExercise = programmingExerciseUtilService.createSampleProgrammingExercise(); - programmingExercise.setFeedbackSuggestionsEnabled(true); + programmingExercise.setFeedbackSuggestionModule(ATHENA_MODULE_PROGRAMMING_TEST); programmingExercise.setGradingCriteria(List.of(new GradingCriterion())); programmingExerciseRepository.save(programmingExercise); programmingSubmission1 = new ProgrammingSubmission(); @@ -147,7 +149,7 @@ void testSubmissionSelectionFromTwoProgramming() { @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void testTextSubmissionSelectionWithFeedbackSuggestionsDisabled() { - textExercise.setFeedbackSuggestionsEnabled(false); + textExercise.setFeedbackSuggestionModule(null); assertThatThrownBy(() -> athenaSubmissionSelectionService.getProposedSubmissionId(textExercise, List.of(textSubmission1.getId()))) .isInstanceOf(IllegalArgumentException.class); } @@ -155,7 +157,7 @@ void testTextSubmissionSelectionWithFeedbackSuggestionsDisabled() { @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void testProgrammingSubmissionSelectionWithFeedbackSuggestionsDisabled() { - programmingExercise.setFeedbackSuggestionsEnabled(false); + programmingExercise.setFeedbackSuggestionModule(null); assertThatThrownBy(() -> athenaSubmissionSelectionService.getProposedSubmissionId(programmingExercise, List.of(programmingSubmission1.getId()))) .isInstanceOf(IllegalArgumentException.class); } diff --git a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSendingServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSendingServiceTest.java index 994e5de618ff..e34aa1fd121d 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSendingServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSendingServiceTest.java @@ -1,5 +1,7 @@ package de.tum.in.www1.artemis.service.connectors.athena; +import static de.tum.in.www1.artemis.connector.AthenaRequestMockProvider.ATHENA_MODULE_PROGRAMMING_TEST; +import static de.tum.in.www1.artemis.connector.AthenaRequestMockProvider.ATHENA_MODULE_TEXT_TEST; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath; @@ -34,7 +36,7 @@ class AthenaSubmissionSendingServiceTest extends AbstractAthenaTest { private SubmissionRepository submissionRepository; @Autowired - private AthenaModuleUrlHelper athenaModuleUrlHelper; + private AthenaModuleService athenaModuleService; @Autowired private AthenaDTOConverter athenaDTOConverter; @@ -63,14 +65,14 @@ void setUp() { // we need to have one student per participation, otherwise the database constraints cannot be fulfilled userUtilService.addUsers(TEST_PREFIX, MAX_NUMBER_OF_TOTAL_PARTICIPATIONS, 0, 0, 0); - athenaSubmissionSendingService = new AthenaSubmissionSendingService(athenaRequestMockProvider.getRestTemplate(), submissionRepository, athenaModuleUrlHelper, + athenaSubmissionSendingService = new AthenaSubmissionSendingService(athenaRequestMockProvider.getRestTemplate(), submissionRepository, athenaModuleService, athenaDTOConverter); textExercise = textExerciseUtilService.createSampleTextExercise(null); - textExercise.setFeedbackSuggestionsEnabled(true); + textExercise.setFeedbackSuggestionModule(ATHENA_MODULE_TEXT_TEST); programmingExercise = programmingExerciseUtilService.createSampleProgrammingExercise(); - programmingExercise.setFeedbackSuggestionsEnabled(true); + programmingExercise.setFeedbackSuggestionModule(ATHENA_MODULE_PROGRAMMING_TEST); } @AfterEach @@ -164,7 +166,7 @@ void testSendMultipleSubmissionBatches() { @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void testSendSubmissionsWithFeedbackSuggestionsDisabledText() { - textExercise.setFeedbackSuggestionsEnabled(false); + textExercise.setFeedbackSuggestionModule(null); assertThatThrownBy(() -> athenaSubmissionSendingService.sendSubmissions(textExercise)).isInstanceOf(IllegalArgumentException.class); } } diff --git a/src/test/java/de/tum/in/www1/artemis/text/TextAssessmentIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/text/TextAssessmentIntegrationTest.java index 738d174d7714..52565dc074de 100644 --- a/src/test/java/de/tum/in/www1/artemis/text/TextAssessmentIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/text/TextAssessmentIntegrationTest.java @@ -1,5 +1,6 @@ package de.tum.in.www1.artemis.text; +import static de.tum.in.www1.artemis.connector.AthenaRequestMockProvider.ATHENA_MODULE_TEXT_TEST; import static java.time.ZonedDateTime.now; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; @@ -939,7 +940,7 @@ private void overrideAssessment(String student, String originalAssessor, HttpSta @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void testTextBlocksAreConsistentWhenOpeningSameAssessmentTwiceWithAthenaEnabled() throws Exception { - textExercise.setFeedbackSuggestionsEnabled(true); + textExercise.setFeedbackSuggestionModule(ATHENA_MODULE_TEXT_TEST); textExerciseRepository.save(textExercise); TextSubmission textSubmission = ParticipationFactory.generateTextSubmission("This is Part 1, and this is Part 2. There is also Part 3.", Language.ENGLISH, true); textExerciseUtilService.saveTextSubmission(textExercise, textSubmission, TEST_PREFIX + "student1"); diff --git a/src/test/javascript/spec/component/course/course-update.component.spec.ts b/src/test/javascript/spec/component/course/course-update.component.spec.ts index e87efda1da67..b69e7d29594a 100644 --- a/src/test/javascript/spec/component/course/course-update.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-update.component.spec.ts @@ -209,6 +209,7 @@ describe('Course Management Update Component', () => { id: new FormControl(entity.id), onlineCourse: new FormControl(entity.onlineCourse), registrationEnabled: new FormControl(entity.enrollmentEnabled), + restrictedAthenaModulesAccess: new FormControl(entity.restrictedAthenaModulesAccess), presentationScore: new FormControl(entity.presentationScore), maxComplaints: new FormControl(entity.maxComplaints), accuracyOfScores: new FormControl(entity.accuracyOfScores), @@ -242,6 +243,7 @@ describe('Course Management Update Component', () => { comp.courseForm = new FormGroup({ onlineCourse: new FormControl(entity.onlineCourse), registrationEnabled: new FormControl(entity.enrollmentEnabled), + restrictedAthenaModulesAccess: new FormControl(entity.restrictedAthenaModulesAccess), presentationScore: new FormControl(entity.presentationScore), maxComplaints: new FormControl(entity.maxComplaints), accuracyOfScores: new FormControl(entity.accuracyOfScores), diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-lifecycle.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-lifecycle.component.spec.ts index cc7bc26c4a99..f6f4fe1f5715 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-lifecycle.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-lifecycle.component.spec.ts @@ -13,6 +13,7 @@ import { SimpleChange } from '@angular/core'; import { IncludedInOverallScore } from 'app/entities/exercise.model'; import { expectElementToBeDisabled, expectElementToBeEnabled } from '../../helpers/utils/general.utils'; import { Course } from 'app/entities/course.model'; +import { ExerciseFeedbackSuggestionOptionsComponent } from 'app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component'; describe('ProgrammingExerciseLifecycleComponent', () => { let comp: ProgrammingExerciseLifecycleComponent; @@ -31,6 +32,7 @@ describe('ProgrammingExerciseLifecycleComponent', () => { ProgrammingExerciseLifecycleComponent, MockComponent(ProgrammingExerciseTestScheduleDatePickerComponent), MockComponent(HelpIconComponent), + MockComponent(ExerciseFeedbackSuggestionOptionsComponent), MockDirective(NgModel), TranslatePipeMock, ], @@ -155,10 +157,10 @@ describe('ProgrammingExerciseLifecycleComponent', () => { it('should disable feedback suggestions when changing the assessment type to automatic', () => { comp.exercise = exercise; comp.exercise.assessmentType = AssessmentType.SEMI_AUTOMATIC; - comp.exercise.feedbackSuggestionsEnabled = true; + comp.exercise.feedbackSuggestionModule = 'programming_module'; comp.toggleAssessmentType(); // toggle to AUTOMATIC - expect(comp.exercise.feedbackSuggestionsEnabled).toBeFalse(); + expect(comp.exercise.feedbackSuggestionModule).toBeUndefined(); }); it('should change publication of tests for programming exercise with published solution', () => { @@ -312,4 +314,5 @@ describe('ProgrammingExerciseLifecycleComponent', () => { const checkbox: HTMLInputElement = fixture.debugElement.nativeElement.querySelector('#releaseTestsWithExampleSolution'); expectElementToBeDisabled(checkbox); }); + // TODO Athena: Add test }); diff --git a/src/test/javascript/spec/service/athena.service.spec.ts b/src/test/javascript/spec/service/athena.service.spec.ts index faf1ccde9319..3d1f35ffd6a6 100644 --- a/src/test/javascript/spec/service/athena.service.spec.ts +++ b/src/test/javascript/spec/service/athena.service.spec.ts @@ -33,13 +33,13 @@ describe('AthenaService', () => { const textExercise = { id: 1, type: 'text', - feedbackSuggestionsEnabled: true, + feedbackSuggestionModule: 'text_module', gradingCriteria, } as Exercise; const programmingExercise = { id: 2, type: 'programming', - feedbackSuggestionsEnabled: true, + feedbackSuggestionModule: 'programming_module', gradingCriteria, } as Exercise; beforeEach(() => { @@ -102,7 +102,7 @@ describe('AthenaService', () => { const mockProfileInfo = { activeProfiles: ['athena'] } as ProfileInfo; jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(of(mockProfileInfo)); - const exerciseWithoutFeedbackSuggestions = { ...textExercise, feedbackSuggestionsEnabled: false } as Exercise; + const exerciseWithoutFeedbackSuggestions = { ...textExercise, feedbackSuggestionModule: undefined } as Exercise; athenaService.getTextFeedbackSuggestions(exerciseWithoutFeedbackSuggestions, { id: 2, text: '' } as TextSubmission).subscribe((suggestions: TextBlockRef[]) => { response = suggestions; diff --git a/src/test/resources/config/application-artemis.yml b/src/test/resources/config/application-artemis.yml index e4cb80f5e20a..eb8e097392d6 100644 --- a/src/test/resources/config/application-artemis.yml +++ b/src/test/resources/config/application-artemis.yml @@ -56,9 +56,7 @@ artemis: athena: url: http://localhost:5000 secret: abcdef12345 - modules: - text: module_text_test - programming: module_programming_test + restricted-modules: module_text_llm,module_programming_llm apollon: conversion-service-url: http://localhost:8080 plagiarism-checks: From a18661c058cdd9c45afc969b9851d723e2a9ecdc Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Mon, 18 Dec 2023 16:46:58 +0100 Subject: [PATCH 02/31] Add more tests to increase coverage --- .../athena/AthenaModuleService.java | 22 ++-- .../www1/artemis/web/rest/AthenaResource.java | 27 ++++- ...feedback-suggestion-options.component.html | 12 +- ...e-feedback-suggestion-options.component.ts | 12 +- .../connector/AthenaRequestMockProvider.java | 42 +++++++ .../AthenaResourceIntegrationTest.java | 82 +++++++++++++- ...edback-suggestion-option.component.spec.ts | 103 ++++++++++++++++++ .../resources/config/application-artemis.yml | 2 +- 8 files changed, 269 insertions(+), 33 deletions(-) create mode 100644 src/test/javascript/spec/component/exercises/shared/feedback/feedback-suggestion-option.component.spec.ts diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java index e4086c1d3cbc..f65c415df294 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java @@ -10,7 +10,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.stereotype.Service; -import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import com.fasterxml.jackson.core.JsonProcessingException; @@ -19,6 +19,7 @@ import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.Exercise; +import de.tum.in.www1.artemis.exception.NetworkingException; import de.tum.in.www1.artemis.repository.ExerciseRepository; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; @@ -54,24 +55,22 @@ public AthenaModuleService(@Qualifier("shortTimeoutAthenaRestTemplate") RestTemp private record AthenaModuleDTO(String name, String type) { } - private List getAthenaModules() { + private List getAthenaModules() throws NetworkingException { try { var response = shortTimeoutRestTemplate.getForEntity(athenaUrl + "/modules", JsonNode.class); - // if (!response.getStatusCode().is2xxSuccessful() || !response.hasBody()) { - // throw new IrisConnectorException("Could not fetch modules"); - // } - // todo error handling + if (!response.getStatusCode().is2xxSuccessful() || !response.hasBody()) { + throw new NetworkingException("Could not fetch Athena modules"); + } AthenaModuleDTO[] modules = objectMapper.treeToValue(response.getBody(), AthenaModuleDTO[].class); return List.of(modules); } - catch (HttpStatusCodeException | JsonProcessingException e) { + catch (RestClientException | JsonProcessingException e) { log.error("Failed to fetch modules from Athena", e); - // todo error handling + throw new NetworkingException("Failed to fetch modules from Athena", e); } - return List.of(); } - public List getAthenaProgrammingModulesForCourse(Course course) { + public List getAthenaProgrammingModulesForCourse(Course course) throws NetworkingException { List availableProgrammingModules = getAthenaModules().stream().filter(module -> "programming".equals(module.type)).map(module -> module.name).toList(); if (!course.getRestrictedAthenaModulesAccess()) { // filter out restricted modules @@ -80,7 +79,7 @@ public List getAthenaProgrammingModulesForCourse(Course course) { return availableProgrammingModules; } - public List getAthenaTextModulesForCourse(Course course) { + public List getAthenaTextModulesForCourse(Course course) throws NetworkingException { List availableProgrammingModules = getAthenaModules().stream().filter(module -> "text".equals(module.type)).map(module -> module.name).toList(); if (!course.getRestrictedAthenaModulesAccess()) { // filter out restricted modules @@ -96,7 +95,6 @@ public List getAthenaTextModulesForCourse(Course course) { * @return The URL prefix to access the Athena module. Example: "http://athena.example.com/modules/text/module_text_cofee" */ public String getAthenaModuleUrl(Exercise exercise) { - // TODO Athena: Use the specified module in the exercise instead of the config specified one switch (exercise.getExerciseType()) { case TEXT -> { return athenaUrl + "/modules/text/" + exercise.getFeedbackSuggestionModule(); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java index 4f509aef4474..525628409e18 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java @@ -26,6 +26,7 @@ import de.tum.in.www1.artemis.service.dto.athena.ProgrammingFeedbackDTO; import de.tum.in.www1.artemis.service.dto.athena.TextFeedbackDTO; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; +import de.tum.in.www1.artemis.web.rest.errors.InternalServerErrorException; import de.tum.in.www1.artemis.web.rest.util.ResponseUtil; /** @@ -138,9 +139,17 @@ public ResponseEntity> getProgrammingFeedbackSugges public ResponseEntity> getAvailableModulesForProgrammingExercises(@PathVariable long courseId) { Course course = courseRepository.findByIdElseThrow(courseId); log.debug("REST request to get available Athena modules for programming exercises in Course {}", course.getTitle()); - // todo error handling - var modules = athenaModuleService.getAthenaProgrammingModulesForCourse(course); - return ResponseEntity.ok(modules); + + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, null); + + try { + var modules = athenaModuleService.getAthenaProgrammingModulesForCourse(course); + return ResponseEntity.ok(modules); + } + catch (NetworkingException e) { + throw new InternalServerErrorException("Could not fetch available Athena modules for programming exercises"); + } + } @GetMapping("athena/text-exercises/{courseId}/available-modules") @@ -148,9 +157,17 @@ public ResponseEntity> getAvailableModulesForProgrammingExercises(@ public ResponseEntity> getAvailableModulesForTextExercises(@PathVariable long courseId) { Course course = courseRepository.findByIdElseThrow(courseId); log.debug("REST request to get available Athena modules for text exercises in Course {}", course.getTitle()); + + authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, null); // todo error handling - var modules = athenaModuleService.getAthenaTextModulesForCourse(course); - return ResponseEntity.ok(modules); + try { + var modules = athenaModuleService.getAthenaTextModulesForCourse(course); + return ResponseEntity.ok(modules); + } + catch (NetworkingException e) { + throw new InternalServerErrorException("Could not fetch available Athena modules for programming exercises"); + } + } @GetMapping("public/athena/restricted-modules") diff --git a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html index c2bb9832fc52..8758af669b3e 100644 --- a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html +++ b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html @@ -5,7 +5,7 @@ class="form-check-input" name="feedbackSuggestionsEnabledCheck" [checked]="!!exercise.feedbackSuggestionModule" - [disabled]="checkboxDisabled()" + [disabled]="inputControlsDisabled()" id="feedbackSuggestionsEnabledCheck" (change)="toggleFeedbackSuggestions($event)" /> @@ -19,10 +19,14 @@
-
- - diff --git a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts index 05c8361d7970..38a10ecdb24b 100644 --- a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts +++ b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts @@ -11,7 +11,9 @@ import { ActivatedRoute } from '@angular/router'; }) export class ExerciseFeedbackSuggestionOptionsComponent implements OnInit { @Input() exercise: Exercise; - @Input() readOnly?: boolean; + @Input() readOnly: boolean = false; + + protected readonly AssessmentType = AssessmentType; readonly assessmentType: AssessmentType; @@ -33,7 +35,7 @@ export class ExerciseFeedbackSuggestionOptionsComponent implements OnInit { this.isAthenaEnabled$ = this.athenaService.isEnabled(); } - checkboxDisabled() { + inputControlsDisabled() { if (this.exercise.type == ExerciseType.PROGRAMMING) { return this.exercise.assessmentType == AssessmentType.AUTOMATIC || this.readOnly; } @@ -54,10 +56,4 @@ export class ExerciseFeedbackSuggestionOptionsComponent implements OnInit { this.exercise.feedbackSuggestionModule = undefined; } } - - buttonClick() { - console.log(this.exercise.feedbackSuggestionModule); - } - - protected readonly AssessmentType = AssessmentType; } diff --git a/src/test/java/de/tum/in/www1/artemis/connector/AthenaRequestMockProvider.java b/src/test/java/de/tum/in/www1/artemis/connector/AthenaRequestMockProvider.java index 2e88f46846a8..77011cbfc271 100644 --- a/src/test/java/de/tum/in/www1/artemis/connector/AthenaRequestMockProvider.java +++ b/src/test/java/de/tum/in/www1/artemis/connector/AthenaRequestMockProvider.java @@ -20,6 +20,7 @@ import org.springframework.web.client.RestTemplate; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; @Component @@ -48,8 +49,12 @@ public class AthenaRequestMockProvider { public static final String ATHENA_MODULE_TEXT_TEST = "module_text_test"; + public static final String ATHENA_RESTRICTED_MODULE_TEXT_TEST = "module_text_test_restricted"; + public static final String ATHENA_MODULE_PROGRAMMING_TEST = "module_programming_test"; + public static final String ATHENA_RESTRICTED_MODULE_PROGRAMMING_TEST = "module_programming_test_restricted"; + public AthenaRequestMockProvider(@Qualifier("athenaRestTemplate") RestTemplate restTemplate, @Qualifier("shortTimeoutAthenaRestTemplate") RestTemplate shortTimeoutRestTemplate, @Qualifier("veryShortTimeoutAthenaRestTemplate") RestTemplate veryShortTimeoutRestTemplate) { this.restTemplate = restTemplate; @@ -199,6 +204,43 @@ else if (moduleType.equals("programming")) { responseActions.andRespond(withSuccess(node.toString(), MediaType.APPLICATION_JSON)); } + public void mockGetAvailableModulesSuccessEmptyModulesList() { + // Response: [] + final ArrayNode array = mapper.createArrayNode(); + + final ResponseActions responseActions = mockServerShortTimeout.expect(ExpectedCount.once(), requestTo(athenaUrl + "/modules")).andExpect(method(HttpMethod.GET)); + responseActions.andRespond(withSuccess().body(array.toString()).contentType(MediaType.APPLICATION_JSON)); + } + + public void mockGetAvailableModulesSuccess() { + // Response: + // [{"name":"module_example","url":"http://module-example-service:5001","type":"programming","supports_evaluation":true},{"name":"module_programming_llm","url":"http://module-programming-llm-service:5002","type":"programming","supports_evaluation":false},{"name":"module_text_llm","url":"http://module-text-llm-service:5003","type":"text","supports_evaluation":true},{"name":"module_text_cofee","url":"http://module-text-cofee-service:5004","type":"text","supports_evaluation":false},{"name":"module_programming_themisml","url":"http://module-programming-themisml-service:5005","type":"programming","supports_evaluation":false}] + final ArrayNode array = mapper.createArrayNode(); + + array.add(createModule(ATHENA_MODULE_TEXT_TEST, "http://module-text-test-service:5001", "text", true)); + array.add(createModule(ATHENA_MODULE_PROGRAMMING_TEST, "http://module-programming-test-service:5002", "programming", false)); + array.add(createModule(ATHENA_RESTRICTED_MODULE_TEXT_TEST, "http://module-restricted-text-service:5004", "text", false)); + array.add(createModule(ATHENA_RESTRICTED_MODULE_PROGRAMMING_TEST, "http://module-restricted-programming-test-service:5003", "programming", true)); + + final ResponseActions responseActions = mockServerShortTimeout.expect(ExpectedCount.once(), requestTo(athenaUrl + "/modules")).andExpect(method(HttpMethod.GET)); + responseActions.andRespond(withSuccess().body(array.toString()).contentType(MediaType.APPLICATION_JSON)); + } + + public void mockGetAvailableModulesFailure() { + final ResponseActions responseActions = mockServerShortTimeout.expect(ExpectedCount.once(), requestTo(athenaUrl + "/modules")).andExpect(method(HttpMethod.GET)); + responseActions.andRespond(withException(new SocketTimeoutException())); + } + + private ObjectNode createModule(String name, String url, String type, boolean supportsEvaluation) { + // creates {"name":"module_example","url":"http://module-example-service:5001","type":"programming","supports_evaluation":true} + ObjectNode moduleNode = mapper.createObjectNode(); + moduleNode.put("name", name); + moduleNode.put("url", url); + moduleNode.put("type", type); + moduleNode.put("supports_evaluation", supportsEvaluation); + return moduleNode; + } + /** * Mocks ths /health API endpoint from Athena used to check if the service is up and running * diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/AthenaResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/AthenaResourceIntegrationTest.java index 1d6c016f39c3..0ed0dc1ac559 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/AthenaResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/AthenaResourceIntegrationTest.java @@ -1,7 +1,6 @@ package de.tum.in.www1.artemis.exercise; -import static de.tum.in.www1.artemis.connector.AthenaRequestMockProvider.ATHENA_MODULE_PROGRAMMING_TEST; -import static de.tum.in.www1.artemis.connector.AthenaRequestMockProvider.ATHENA_MODULE_TEXT_TEST; +import static de.tum.in.www1.artemis.connector.AthenaRequestMockProvider.*; import static org.assertj.core.api.Assertions.assertThat; import java.time.ZonedDateTime; @@ -69,6 +68,9 @@ class AthenaResourceIntegrationTest extends AbstractAthenaTest { @Autowired private FeedbackRepository feedbackRepository; + @Autowired + private CourseRepository courseRepository; + private TextExercise textExercise; private TextSubmission textSubmission; @@ -80,7 +82,7 @@ class AthenaResourceIntegrationTest extends AbstractAthenaTest { @BeforeEach protected void initTestCase() { super.initTestCase(); - userUtilService.addUsers(TEST_PREFIX, 1, 1, 0, 1); + userUtilService.addUsers(TEST_PREFIX, 1, 1, 1, 0); var textCourse = textExerciseUtilService.addCourseWithOneReleasedTextExercise(); textExercise = exerciseUtilService.findTextExerciseWithTitle(textCourse.getExercises(), "Text"); @@ -107,6 +109,80 @@ protected void initTestCase() { programmingSubmissionRepository.save(programmingSubmission); } + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void testGetAvailableProgrammingModulesSuccess_EmptyModules() throws Exception { + var course = textExercise.getCourseViaExerciseGroupOrCourseMember(); + + athenaRequestMockProvider.mockGetAvailableModulesSuccessEmptyModulesList(); + List response = request.getList("/api/athena/programming-exercises/" + course.getId() + "/available-modules", HttpStatus.OK, String.class); + assertThat(response).isEmpty(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void testGetAvailableProgrammingModulesSuccess() throws Exception { + var course = textExercise.getCourseViaExerciseGroupOrCourseMember(); + + athenaRequestMockProvider.mockGetAvailableModulesSuccess(); + List response = request.getList("/api/athena/programming-exercises/" + course.getId() + "/available-modules", HttpStatus.OK, String.class); + assertThat(response).contains(ATHENA_MODULE_PROGRAMMING_TEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void testGetAvailableTextModulesSuccess_RestrictedModuleAccess() throws Exception { + // give the course access to the restricted Athena modules + var course = textExercise.getCourseViaExerciseGroupOrCourseMember(); + course.setRestrictedAthenaModulesAccess(true); + courseRepository.save(course); + + athenaRequestMockProvider.mockGetAvailableModulesSuccess(); + List response = request.getList("/api/athena/text-exercises/" + course.getId() + "/available-modules", HttpStatus.OK, String.class); + assertThat(response).containsExactlyInAnyOrder(ATHENA_MODULE_TEXT_TEST, ATHENA_RESTRICTED_MODULE_TEXT_TEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void testGetAvailableProgrammingModulesSuccess_RestrictedModuleAccess() throws Exception { + // give the course access to the restricted Athena modules + var course = textExercise.getCourseViaExerciseGroupOrCourseMember(); + course.setRestrictedAthenaModulesAccess(true); + courseRepository.save(course); + + athenaRequestMockProvider.mockGetAvailableModulesSuccess(); + List response = request.getList("/api/athena/programming-exercises/" + course.getId() + "/available-modules", HttpStatus.OK, String.class); + assertThat(response).containsExactlyInAnyOrder(ATHENA_MODULE_PROGRAMMING_TEST, ATHENA_RESTRICTED_MODULE_PROGRAMMING_TEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void testGetAvailableTextModulesSuccess() throws Exception { + var course = textExercise.getCourseViaExerciseGroupOrCourseMember(); + + athenaRequestMockProvider.mockGetAvailableModulesSuccess(); + List response = request.getList("/api/athena/text-exercises/" + course.getId() + "/available-modules", HttpStatus.OK, String.class); + assertThat(response).contains(ATHENA_MODULE_TEXT_TEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") + void testGetAvailableProgrammingModulesAccessForbidden() throws Exception { + var course = textExercise.getCourseViaExerciseGroupOrCourseMember(); + + athenaRequestMockProvider.mockGetAvailableModulesSuccess(); + request.getList("/api/athena/programming-exercises/" + course.getId() + "/available-modules", HttpStatus.FORBIDDEN, String.class); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") + void testGetAvailableTextModulesAccessForbidden() throws Exception { + var course = textExercise.getCourseViaExerciseGroupOrCourseMember(); + + athenaRequestMockProvider.mockGetAvailableModulesSuccess(); + request.getList("/api/athena/text-exercises/" + course.getId() + "/available-modules", HttpStatus.FORBIDDEN, String.class); + } + @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void testGetFeedbackSuggestionsSuccessText() throws Exception { diff --git a/src/test/javascript/spec/component/exercises/shared/feedback/feedback-suggestion-option.component.spec.ts b/src/test/javascript/spec/component/exercises/shared/feedback/feedback-suggestion-option.component.spec.ts new file mode 100644 index 000000000000..08267185760d --- /dev/null +++ b/src/test/javascript/spec/component/exercises/shared/feedback/feedback-suggestion-option.component.spec.ts @@ -0,0 +1,103 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of } from 'rxjs'; +import { MockProvider } from 'ng-mocks'; +import { Exercise, ExerciseType } from 'app/entities/exercise.model'; +import { AssessmentType } from 'app/entities/assessment-type.model'; +import { AthenaService } from 'app/assessment/athena.service'; +import { ExerciseFeedbackSuggestionOptionsComponent } from 'app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component'; + +describe('ExerciseFeedbackSuggestionOptionsComponent', () => { + let component: ExerciseFeedbackSuggestionOptionsComponent; + let fixture: ComponentFixture; + let athenaService: AthenaService; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ExerciseFeedbackSuggestionOptionsComponent], + providers: [ + MockProvider(AthenaService, { + isEnabled: () => of(true), + }), + { provide: ActivatedRoute, useValue: { snapshot: { paramMap: { get: () => '1' } } } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ExerciseFeedbackSuggestionOptionsComponent); + component = fixture.componentInstance; + athenaService = TestBed.inject(AthenaService); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize with available modules', async () => { + const modules = ['Module1', 'Module2']; + jest.spyOn(athenaService, 'getAvailableModules').mockReturnValue(of(modules)); + + await component.ngOnInit(); + + expect(component.availableAthenaModules).toEqual(modules); + expect(component.modulesAvailable).toBeTruthy(); + }); + + it('should set isAthenaEnabled$ with the result from athenaService', async () => { + jest.spyOn(athenaService, 'getAvailableModules').mockReturnValue(of()); + jest.spyOn(athenaService, 'isEnabled').mockReturnValue(of(true)); + + await component.ngOnInit(); + + expect(component.isAthenaEnabled$).toBeDefined(); + component.isAthenaEnabled$.subscribe((result) => { + expect(result).toBeTrue(); + }); + }); + + it('should disable input controls for programming exercises with automatic assessment type or read-only', () => { + component.exercise = { type: ExerciseType.PROGRAMMING, assessmentType: AssessmentType.AUTOMATIC } as Exercise; + + let result = component.inputControlsDisabled(); + expect(result).toBeTruthy(); + + component.exercise.assessmentType = AssessmentType.MANUAL; + result = component.inputControlsDisabled(); + expect(result).toBeFalsy(); + + component.readOnly = true; + result = component.inputControlsDisabled(); + expect(result).toBeTruthy(); + }); + + it('should return grey color for checkbox label style for automatic programming exercises', () => { + component.exercise = { type: ExerciseType.PROGRAMMING, assessmentType: AssessmentType.AUTOMATIC } as Exercise; + + const style = component.getCheckboxLabelStyle(); + + expect(style).toEqual({ color: 'grey' }); + }); + + it('should return an empty object for checkbox label style for non-automatic programming exercises', () => { + component.exercise = { type: ExerciseType.PROGRAMMING, assessmentType: AssessmentType.MANUAL } as Exercise; + + const style = component.getCheckboxLabelStyle(); + + expect(style).toEqual({}); + }); + + it('should toggle feedback suggestions and set the module for programming exercises', () => { + const modules = ['Module1', 'Module2']; + component.availableAthenaModules = modules; + component.exercise = { type: ExerciseType.PROGRAMMING } as Exercise; + + const event = { target: { checked: true } }; + component.toggleFeedbackSuggestions(event); + + expect(component.exercise.feedbackSuggestionModule).toBe('Module1'); + + event.target.checked = false; + component.toggleFeedbackSuggestions(event); + + expect(component.exercise.feedbackSuggestionModule).toBeUndefined(); + }); +}); diff --git a/src/test/resources/config/application-artemis.yml b/src/test/resources/config/application-artemis.yml index eb8e097392d6..16063bf4e1f1 100644 --- a/src/test/resources/config/application-artemis.yml +++ b/src/test/resources/config/application-artemis.yml @@ -56,7 +56,7 @@ artemis: athena: url: http://localhost:5000 secret: abcdef12345 - restricted-modules: module_text_llm,module_programming_llm + restricted-modules: module_text_test_restricted,module_programming_test_restricted apollon: conversion-service-url: http://localhost:8080 plagiarism-checks: From b4501bdd16d7ac759635684d4f90c5cda63f3218 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Mon, 18 Dec 2023 17:04:59 +0100 Subject: [PATCH 03/31] Fix client-test build --- .../exercise-feedback-suggestion-options.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts index 38a10ecdb24b..123c820e4180 100644 --- a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts +++ b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts @@ -17,7 +17,7 @@ export class ExerciseFeedbackSuggestionOptionsComponent implements OnInit { readonly assessmentType: AssessmentType; - isAthenaEnabled$: Observable | undefined; + isAthenaEnabled$: Observable; modulesAvailable: boolean; availableAthenaModules: string[]; From e3fa983c49be886fb9edd1ae9e73c26bd86f9acb Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Mon, 18 Dec 2023 18:59:28 +0100 Subject: [PATCH 04/31] Add more client-test for athena-service.ts --- .../spec/service/athena.service.spec.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/test/javascript/spec/service/athena.service.spec.ts b/src/test/javascript/spec/service/athena.service.spec.ts index 3d1f35ffd6a6..56989a221b1d 100644 --- a/src/test/javascript/spec/service/athena.service.spec.ts +++ b/src/test/javascript/spec/service/athena.service.spec.ts @@ -127,4 +127,51 @@ describe('AthenaService', () => { expect(response).toEqual([]); })); + + it('should return no modules when athena is disabled on the server', fakeAsync(() => { + let response: string[] | null = null; + + const mockProfileInfo = { activeProfiles: ['something'] } as ProfileInfo; + jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(of(mockProfileInfo)); + + athenaService.getAvailableModules(1).subscribe((modules: string[]) => { + response = modules; + }); + + tick(); + + expect(response).toEqual([]); + })); + + it('should get available modules when athena is enabled', fakeAsync(() => { + const textModules = ['module_text_1', 'module_text_2']; + const programmingModules = ['module_programming_1', 'module_programming_2']; + let textResponse: string[] | null = null; + let programmingResponse: string[] | null = null; + + const mockProfileInfo = { activeProfiles: ['athena'] } as ProfileInfo; + jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(of(mockProfileInfo)); + + athenaService.getAvailableModules(1, textExercise).subscribe((modules: string[]) => { + textResponse = modules; + }); + const requestWrapperText = httpTestingController.expectOne({ url: 'api/athena/text-exercises/1/available-modules' }); + requestWrapperText.flush(textModules); + + tick(); + + athenaService.getAvailableModules(1, programmingExercise).subscribe((modules: string[]) => { + programmingResponse = modules; + }); + const requestWrapperProgramming = httpTestingController.expectOne({ url: 'api/athena/programming-exercises/1/available-modules' }); + requestWrapperProgramming.flush(programmingModules); + + tick(); + + expect(requestWrapperText.request.method).toBe('GET'); + expect(textResponse!).toEqual(textModules); + + expect(requestWrapperProgramming.request.method).toBe('GET'); + expect(programmingResponse!).toEqual(programmingModules); + })); }); From ba9e6e4d706ea13e66ea899691891e2aaf323202 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Mon, 18 Dec 2023 22:26:32 +0100 Subject: [PATCH 05/31] Fix client-test build --- src/test/javascript/spec/service/athena.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/javascript/spec/service/athena.service.spec.ts b/src/test/javascript/spec/service/athena.service.spec.ts index 56989a221b1d..332fe98a4463 100644 --- a/src/test/javascript/spec/service/athena.service.spec.ts +++ b/src/test/javascript/spec/service/athena.service.spec.ts @@ -134,7 +134,7 @@ describe('AthenaService', () => { const mockProfileInfo = { activeProfiles: ['something'] } as ProfileInfo; jest.spyOn(profileService, 'getProfileInfo').mockReturnValue(of(mockProfileInfo)); - athenaService.getAvailableModules(1).subscribe((modules: string[]) => { + athenaService.getAvailableModules(1, textExercise).subscribe((modules: string[]) => { response = modules; }); From 6f7f9ac756e2f6bc492f8fa081a12395307537fe Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Tue, 19 Dec 2023 13:31:35 +0100 Subject: [PATCH 06/31] Rename REST endpoint for available athena modules --- .../connectors/athena/AthenaModuleService.java | 9 ++------- .../tum/in/www1/artemis/web/rest/AthenaResource.java | 12 ++---------- src/main/webapp/app/assessment/athena.service.ts | 2 +- .../javascript/spec/service/athena.service.spec.ts | 4 ++-- 4 files changed, 7 insertions(+), 20 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java index f65c415df294..eed2bd48a254 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java @@ -91,8 +91,8 @@ public List getAthenaTextModulesForCourse(Course course) throws Networki /** * Get the URL for an Athena module, depending on the type of exercise. * - * @param exerciseType The type of exercise - * @return The URL prefix to access the Athena module. Example: "http://athena.example.com/modules/text/module_text_cofee" + * @param exercise The exercise for which the URL to Athena should be returned + * @return The URL prefix to access the Athena module. Example: */ public String getAthenaModuleUrl(Exercise exercise) { switch (exercise.getExerciseType()) { @@ -116,9 +116,4 @@ public void checkHasAccessToAthenaModule(Exercise exercise, Course course, Strin public void revokeAccessToRestrictedFeedbackSuggestionModules(Course course) { exerciseRepository.revokeAccessToRestrictedFeedbackSuggestionModulesByCourseId(course.getId(), new HashSet<>(restrictedModules)); } - - public List getRestrictedModules() { - // TODO Athena: Just for testing, remove afterwards - return restrictedModules; - } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java index 525628409e18..b3b61605813f 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java @@ -134,7 +134,7 @@ public ResponseEntity> getProgrammingFeedbackSugges athenaFeedbackSuggestionsService::getProgrammingFeedbackSuggestions); } - @GetMapping("athena/programming-exercises/{courseId}/available-modules") + @GetMapping("athena/courses/{courseId}/programming-exercises/available-modules") @EnforceAtLeastEditor public ResponseEntity> getAvailableModulesForProgrammingExercises(@PathVariable long courseId) { Course course = courseRepository.findByIdElseThrow(courseId); @@ -152,7 +152,7 @@ public ResponseEntity> getAvailableModulesForProgrammingExercises(@ } - @GetMapping("athena/text-exercises/{courseId}/available-modules") + @GetMapping("athena/courses/{courseId}/text-exercises/available-modules") @EnforceAtLeastEditor public ResponseEntity> getAvailableModulesForTextExercises(@PathVariable long courseId) { Course course = courseRepository.findByIdElseThrow(courseId); @@ -170,14 +170,6 @@ public ResponseEntity> getAvailableModulesForTextExercises(@PathVar } - @GetMapping("public/athena/restricted-modules") - @EnforceNothing - @ManualConfig - public ResponseEntity> getRestrictedModules() { - // TODO Athena: Just for testing, remove afterwards - return ResponseEntity.ok(athenaModuleService.getRestrictedModules()); - } - /** * Check if the given auth header is valid for Athena, otherwise throw an exception. * diff --git a/src/main/webapp/app/assessment/athena.service.ts b/src/main/webapp/app/assessment/athena.service.ts index d18f82a0ec93..aabe395e9472 100644 --- a/src/main/webapp/app/assessment/athena.service.ts +++ b/src/main/webapp/app/assessment/athena.service.ts @@ -31,7 +31,7 @@ export class AthenaService { return of([] as string[]); } return this.http - .get(`${this.resourceUrl}/${exercise.type}-exercises/${courseId}/available-modules`, { observe: 'response' }) + .get(`${this.resourceUrl}/courses/${courseId}/${exercise.type}-exercises/available-modules`, { observe: 'response' }) .pipe(switchMap((res: HttpResponse) => of(res.body!))); }), ); diff --git a/src/test/javascript/spec/service/athena.service.spec.ts b/src/test/javascript/spec/service/athena.service.spec.ts index 332fe98a4463..730d81dab60f 100644 --- a/src/test/javascript/spec/service/athena.service.spec.ts +++ b/src/test/javascript/spec/service/athena.service.spec.ts @@ -155,7 +155,7 @@ describe('AthenaService', () => { athenaService.getAvailableModules(1, textExercise).subscribe((modules: string[]) => { textResponse = modules; }); - const requestWrapperText = httpTestingController.expectOne({ url: 'api/athena/text-exercises/1/available-modules' }); + const requestWrapperText = httpTestingController.expectOne({ url: 'api/athena/courses/1/text-exercises/available-modules' }); requestWrapperText.flush(textModules); tick(); @@ -163,7 +163,7 @@ describe('AthenaService', () => { athenaService.getAvailableModules(1, programmingExercise).subscribe((modules: string[]) => { programmingResponse = modules; }); - const requestWrapperProgramming = httpTestingController.expectOne({ url: 'api/athena/programming-exercises/1/available-modules' }); + const requestWrapperProgramming = httpTestingController.expectOne({ url: 'api/athena/courses/1/programming-exercises/available-modules' }); requestWrapperProgramming.flush(programmingModules); tick(); From 08d8f5923963068b91bc2b86d2e8103cdbeb8c05 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Tue, 19 Dec 2023 16:34:24 +0100 Subject: [PATCH 07/31] Add AthenaExerciseIntegrationTest --- .../repository/ExerciseRepository.java | 2 +- .../athena/AthenaModuleService.java | 3 +- .../www1/artemis/web/rest/CourseResource.java | 1 - .../web/rest/ProgrammingExerciseResource.java | 6 +- .../web/rest/TextExerciseResource.java | 11 +- .../AthenaExerciseIntegrationTest.java | 196 ++++++++++++++++++ 6 files changed, 209 insertions(+), 10 deletions(-) create mode 100644 src/test/java/de/tum/in/www1/artemis/exercise/AthenaExerciseIntegrationTest.java diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java index 82f806300ec6..c166b9e8b45f 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java @@ -553,7 +553,7 @@ default Set findAllFeedbackSuggestionsEnabledExercisesWithFutureDueDat AND e.feedbackSuggestionModule IN :restrictedFeedbackSuggestionModule """) void revokeAccessToRestrictedFeedbackSuggestionModulesByCourseId(@Param("courseId") Long courseId, - @Param("restrictedFeedbackSuggestionModule") Set restrictedFeedbackSuggestionModule); + @Param("restrictedFeedbackSuggestionModule") Collection restrictedFeedbackSuggestionModule); /** * For an explanation, see {@link de.tum.in.www1.artemis.web.rest.ExamResource#getAllExercisesWithPotentialPlagiarismForExam(long,long)} diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java index eed2bd48a254..8abee75b3392 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java @@ -1,6 +1,5 @@ package de.tum.in.www1.artemis.service.connectors.athena; -import java.util.HashSet; import java.util.List; import org.slf4j.Logger; @@ -114,6 +113,6 @@ public void checkHasAccessToAthenaModule(Exercise exercise, Course course, Strin } public void revokeAccessToRestrictedFeedbackSuggestionModules(Course course) { - exerciseRepository.revokeAccessToRestrictedFeedbackSuggestionModulesByCourseId(course.getId(), new HashSet<>(restrictedModules)); + exerciseRepository.revokeAccessToRestrictedFeedbackSuggestionModulesByCourseId(course.getId(), restrictedModules); } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java index 6e50725e622a..153c56d0c2dc 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/CourseResource.java @@ -265,7 +265,6 @@ public ResponseEntity updateCourse(@PathVariable Long courseId, @Request // if access to restricted athena modules got disabled for the course, we need to set all exercises that use restricted modules to null if (athenaModuleAccessChanged && !courseUpdate.getRestrictedAthenaModulesAccess()) { - // todo athena: revoke access for all course exercises that use restricted modules athenaModuleService.ifPresent(ams -> ams.revokeAccessToRestrictedFeedbackSuggestionModules(result)); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java index f2613c210cee..6014f368a0fc 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java @@ -207,7 +207,8 @@ public ResponseEntity createProgrammingExercise(@RequestBod programmingExerciseService.validateNewProgrammingExerciseSettings(programmingExercise, course); // TODO Athena: Check that only allowed athena modules are used - athenaModuleService.ifPresent(ams -> ams.checkHasAccessToAthenaModule(programmingExercise, course, ENTITY_NAME)); + athenaModuleService.ifPresentOrElse(ams -> ams.checkHasAccessToAthenaModule(programmingExercise, course, ENTITY_NAME), + () -> programmingExercise.setFeedbackSuggestionModule(null)); try { // Setup all repositories etc @@ -281,7 +282,8 @@ public ResponseEntity updateProgrammingExercise(@RequestBod exerciseService.checkForConversionBetweenExamAndCourseExercise(updatedProgrammingExercise, programmingExerciseBeforeUpdate, ENTITY_NAME); // Check that only allowed Athena modules are used - athenaModuleService.ifPresent(ams -> ams.checkHasAccessToAthenaModule(updatedProgrammingExercise, course, ENTITY_NAME)); + athenaModuleService.ifPresentOrElse(ams -> ams.checkHasAccessToAthenaModule(updatedProgrammingExercise, course, ENTITY_NAME), + () -> updatedProgrammingExercise.setFeedbackSuggestionModule(null)); // Ignore changes to the default branch updatedProgrammingExercise.setBranch(programmingExerciseBeforeUpdate.getBranch()); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java index 8fab23c0ce6a..e4e1f0e8930c 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java @@ -178,7 +178,7 @@ public ResponseEntity createTextExercise(@RequestBody TextExercise authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, null); // TODO Athena: Check that only allowed athena modules are used - athenaModuleService.ifPresent(ams -> ams.checkHasAccessToAthenaModule(textExercise, course, ENTITY_NAME)); + athenaModuleService.ifPresentOrElse(ams -> ams.checkHasAccessToAthenaModule(textExercise, course, ENTITY_NAME), () -> textExercise.setFeedbackSuggestionModule(null)); TextExercise result = textExerciseRepository.save(textExercise); @@ -227,7 +227,8 @@ public ResponseEntity updateTextExercise(@RequestBody TextExercise exerciseService.checkForConversionBetweenExamAndCourseExercise(textExercise, textExerciseBeforeUpdate, ENTITY_NAME); // TODO Athena: Check that only allowed athena modules are used - athenaModuleService.ifPresent(ams -> ams.checkHasAccessToAthenaModule(textExercise, textExerciseBeforeUpdate.getCourseViaExerciseGroupOrCourseMember(), ENTITY_NAME)); + Course course = courseService.retrieveCourseOverExerciseGroupOrCourseId(textExerciseBeforeUpdate); + athenaModuleService.ifPresentOrElse(ams -> ams.checkHasAccessToAthenaModule(textExercise, course, ENTITY_NAME), () -> textExercise.setFeedbackSuggestionModule(null)); channelService.updateExerciseChannel(textExerciseBeforeUpdate, textExercise); @@ -457,9 +458,11 @@ public ResponseEntity importExercise(@PathVariable long sourceExer // validates general settings: points, dates importedExercise.validateGeneralSettings(); - // TODO Athena: Check that only allowed athena modules are used, if not we disable feedback suggestions for the imported exercise + // Athena: Check that only allowed athena modules are used, if not we catch the exception and disable feedback suggestions for the imported exercise + // If Athena is disabled and the service is not present, we also disable feedback suggestions try { - athenaModuleService.ifPresent(ams -> ams.checkHasAccessToAthenaModule(importedExercise, importedExercise.getCourseViaExerciseGroupOrCourseMember(), ENTITY_NAME)); + athenaModuleService.ifPresentOrElse(ams -> ams.checkHasAccessToAthenaModule(importedExercise, importedExercise.getCourseViaExerciseGroupOrCourseMember(), ENTITY_NAME), + () -> importedExercise.setFeedbackSuggestionModule(null)); } catch (BadRequestAlertException e) { importedExercise.setFeedbackSuggestionModule(null); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/AthenaExerciseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/AthenaExerciseIntegrationTest.java new file mode 100644 index 000000000000..7d8c15b9aec2 --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/exercise/AthenaExerciseIntegrationTest.java @@ -0,0 +1,196 @@ +package de.tum.in.www1.artemis.exercise; + +import static de.tum.in.www1.artemis.connector.AthenaRequestMockProvider.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MvcResult; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.tum.in.www1.artemis.AbstractAthenaTest; +import de.tum.in.www1.artemis.course.CourseTestService; +import de.tum.in.www1.artemis.course.CourseUtilService; +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.ProgrammingExercise; +import de.tum.in.www1.artemis.domain.TextExercise; +import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; +import de.tum.in.www1.artemis.repository.*; +import de.tum.in.www1.artemis.user.UserUtilService; + +public class AthenaExerciseIntegrationTest extends AbstractAthenaTest { + + private static final String TEST_PREFIX = "athenaexerciseintegration"; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private UserUtilService userUtilService; + + @Autowired + private ProgrammingExerciseUtilService programmingExerciseUtilService; + + @Autowired + private TextExerciseUtilService textExerciseUtilService; + + @Autowired + private ExerciseUtilService exerciseUtilService; + + @Autowired + private CourseUtilService courseUtilService; + + @Autowired + private CourseTestService courseTestService; + + @Autowired + private CourseRepository courseRepository; + + @Autowired + private ProgrammingExerciseRepository programmingExerciseRepository; + + @Autowired + private TextExerciseRepository textExerciseRepository; + + private Course course; + + private TextExercise textExercise; + + private ProgrammingExercise programmingExercise; + + @BeforeEach + protected void initTestCase() { + super.initTestCase(); + + userUtilService.addUsers(TEST_PREFIX, 0, 0, 1, 1); + + course = courseUtilService.addEmptyCourse(); + textExercise = textExerciseUtilService.createSampleTextExercise(course); + programmingExercise = programmingExerciseUtilService.createSampleProgrammingExercise(); + course.addExercises(programmingExercise); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void createTextExercise_useRestrictedAthenaModule_success() throws Exception { + course.setRestrictedAthenaModulesAccess(true); + courseRepository.save(course); + + textExercise.setId(null); + textExercise.setFeedbackSuggestionModule(ATHENA_RESTRICTED_MODULE_TEXT_TEST); + + request.postWithResponseBody("/api/text-exercises/", textExercise, TextExercise.class, HttpStatus.CREATED); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void createTextExercise_useRestrictedAthenaModule_badRequest() throws Exception { + textExercise.setId(null); + textExercise.setFeedbackSuggestionModule(ATHENA_RESTRICTED_MODULE_TEXT_TEST); + + request.postWithResponseBody("/api/text-exercises/", textExercise, TextExercise.class, HttpStatus.BAD_REQUEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void updateTextExercise_useRestrictedAthenaModule_success() throws Exception { + course.setRestrictedAthenaModulesAccess(true); + courseRepository.save(course); + + textExercise.setFeedbackSuggestionModule(ATHENA_RESTRICTED_MODULE_TEXT_TEST); + + request.putWithResponseBody("/api/text-exercises/", textExercise, TextExercise.class, HttpStatus.OK); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void updateTextExercise_useRestrictedAthenaModule_badRequest() throws Exception { + textExercise.setFeedbackSuggestionModule(ATHENA_RESTRICTED_MODULE_TEXT_TEST); + + request.putWithResponseBody("/api/text-exercises/", textExercise, TextExercise.class, HttpStatus.BAD_REQUEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void createProgrammingExercise_useRestrictedAthenaModule_badRequest() throws Exception { + programmingExercise.setId(null); + programmingExercise.setFeedbackSuggestionModule(ATHENA_RESTRICTED_MODULE_PROGRAMMING_TEST); + + request.postWithResponseBody("/api/programming-exercises/setup/", programmingExercise, ProgrammingExercise.class, HttpStatus.BAD_REQUEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void updateProgrammingExercise_useRestrictedAthenaModule_badRequest() throws Exception { + programmingExercise.setFeedbackSuggestionModule(ATHENA_RESTRICTED_MODULE_PROGRAMMING_TEST); + + request.postWithResponseBody("/api/programming-exercises/setup/", programmingExercise, ProgrammingExercise.class, HttpStatus.BAD_REQUEST); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void updateCourse_revokeRestrictedAthenaModuleAccess_badRequest() throws Exception { + course.setRestrictedAthenaModulesAccess(true); + courseRepository.save(course); + + course.setRestrictedAthenaModulesAccess(false); + + request.getMvc().perform(courseTestService.buildUpdateCourse(course.getId(), course)).andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser(username = "admin", roles = "ADMIN") + void updateCourse_revokeRestrictedAthenaModuleAccess_success() throws Exception { + course.setRestrictedAthenaModulesAccess(true); + courseRepository.save(course); + + // Set allowed modules for the default exercises + textExercise.setFeedbackSuggestionModule(ATHENA_MODULE_TEXT_TEST); + programmingExercise.setFeedbackSuggestionModule(ATHENA_MODULE_PROGRAMMING_TEST); + + // Create two new exercises + TextExercise textExerciseRestrictedModule = textExerciseUtilService.createSampleTextExercise(course); + ProgrammingExercise programmingExerciseRestrictedModule = programmingExerciseUtilService.createSampleProgrammingExercise(); + course.addExercises(programmingExerciseRestrictedModule); + // Set restricted modules for two new exercises + textExerciseRestrictedModule.setFeedbackSuggestionModule(ATHENA_RESTRICTED_MODULE_TEXT_TEST); + programmingExerciseRestrictedModule.setFeedbackSuggestionModule(ATHENA_RESTRICTED_MODULE_PROGRAMMING_TEST); + + // Save all exercise changes + textExerciseRepository.saveAll(List.of(textExercise, textExerciseRestrictedModule)); + programmingExerciseRepository.saveAll(List.of(programmingExercise, programmingExerciseRestrictedModule)); + + // Revoke access to restricted Athena modules for the course + course.setRestrictedAthenaModulesAccess(false); + MvcResult result = request.getMvc().perform(courseTestService.buildUpdateCourse(course.getId(), course)).andExpect(status().isOk()).andReturn(); + Course updatedCourse = objectMapper.readValue(result.getResponse().getContentAsString(), Course.class); + + assertThat(updatedCourse.getRestrictedAthenaModulesAccess()).as("restricted Athena modules access was correctly updated for the course") + .isEqualTo(course.getRestrictedAthenaModulesAccess()); + + ProgrammingExercise updatedProgrammingExercise = request.get("/api/programming-exercises/" + programmingExercise.getId(), HttpStatus.OK, ProgrammingExercise.class); + ProgrammingExercise updatedProgrammingExerciseRestrictedModule = request.get("/api/programming-exercises/" + programmingExerciseRestrictedModule.getId(), HttpStatus.OK, + ProgrammingExercise.class); + TextExercise updatedTextExercise = request.get("/api/text-exercises/" + textExercise.getId(), HttpStatus.OK, TextExercise.class); + TextExercise updatedTextExerciseRestrictedModule = request.get("/api/text-exercises/" + textExerciseRestrictedModule.getId(), HttpStatus.OK, TextExercise.class); + + // Check that the default exercises still have their module set + assertThat(updatedProgrammingExercise.getFeedbackSuggestionModule()).as("Athena module for the programming exercise was unchanged") + .isEqualTo(programmingExercise.getFeedbackSuggestionModule()); + assertThat(updatedTextExercise.getFeedbackSuggestionModule()).as("Athena module for the text exercise was unchanged").isEqualTo(textExercise.getFeedbackSuggestionModule()); + + // Check that the two additional exercises do not have a restricted module set + assertThat(updatedProgrammingExerciseRestrictedModule.getFeedbackSuggestionModule()) + .as("access to restricted Athena module for the programming exercise was revoked successfully").isNull(); + assertThat(updatedTextExerciseRestrictedModule.getFeedbackSuggestionModule()).as("access to restricted Athena module for the text exercise was revoked successfully") + .isNull(); + } +} From dc01774fa252c8fb4fd12110a04857e575ea792d Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Tue, 19 Dec 2023 19:14:26 +0100 Subject: [PATCH 08/31] Fix failing server-tests --- .../athena/AthenaModuleService.java | 3 ++ ...ogrammingExerciseExportImportResource.java | 16 +++++++- .../AthenaExerciseIntegrationTest.java | 38 +++++++++---------- .../AthenaResourceIntegrationTest.java | 22 +++++------ 4 files changed, 46 insertions(+), 33 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java index 8abee75b3392..715c02125a26 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java @@ -106,6 +106,9 @@ public String getAthenaModuleUrl(Exercise exercise) { } public void checkHasAccessToAthenaModule(Exercise exercise, Course course, String entityName) throws BadRequestAlertException { + if (exercise.isExamExercise()) { + throw new BadRequestAlertException("The exam exercise has no access to Athena", entityName, "examExerciseNoAccessToAthena"); + } if (!course.getRestrictedAthenaModulesAccess() && restrictedModules.contains(exercise.getFeedbackSuggestionModule())) { // Course does not have access to the restricted Athena modules throw new BadRequestAlertException("The exercise has no access to the selected Athena module", entityName, "noAccessToAthenaModule"); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseExportImportResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseExportImportResource.java index ec3b44f36e89..7556b45a12c8 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseExportImportResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseExportImportResource.java @@ -42,6 +42,7 @@ import de.tum.in.www1.artemis.service.ConsistencyCheckService; import de.tum.in.www1.artemis.service.CourseService; import de.tum.in.www1.artemis.service.SubmissionPolicyService; +import de.tum.in.www1.artemis.service.connectors.athena.AthenaModuleService; import de.tum.in.www1.artemis.service.exam.ExamAccessService; import de.tum.in.www1.artemis.service.export.ProgrammingExerciseExportService; import de.tum.in.www1.artemis.service.feature.Feature; @@ -93,12 +94,15 @@ public class ProgrammingExerciseExportImportResource { private final ConsistencyCheckService consistencyCheckService; + private final Optional athenaModuleService; + public ProgrammingExerciseExportImportResource(ProgrammingExerciseRepository programmingExerciseRepository, UserRepository userRepository, AuthorizationCheckService authCheckService, CourseService courseService, ProgrammingExerciseImportService programmingExerciseImportService, ProgrammingExerciseExportService programmingExerciseExportService, Optional programmingLanguageFeatureService, AuxiliaryRepositoryRepository auxiliaryRepositoryRepository, SubmissionPolicyService submissionPolicyService, ProgrammingExerciseTaskRepository programmingExerciseTaskRepository, ExamAccessService examAccessService, CourseRepository courseRepository, - ProgrammingExerciseImportFromFileService programmingExerciseImportFromFileService, ConsistencyCheckService consistencyCheckService) { + ProgrammingExerciseImportFromFileService programmingExerciseImportFromFileService, ConsistencyCheckService consistencyCheckService, + Optional athenaModuleService) { this.programmingExerciseRepository = programmingExerciseRepository; this.userRepository = userRepository; this.courseService = courseService; @@ -113,6 +117,7 @@ public ProgrammingExerciseExportImportResource(ProgrammingExerciseRepository pro this.courseRepository = courseRepository; this.programmingExerciseImportFromFileService = programmingExerciseImportFromFileService; this.consistencyCheckService = consistencyCheckService; + this.athenaModuleService = athenaModuleService; } /** @@ -196,6 +201,15 @@ public ResponseEntity importProgrammingExercise(@PathVariab Course originalCourse = courseService.retrieveCourseOverExerciseGroupOrCourseId(originalProgrammingExercise); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, originalCourse, user); + // Athena: Check that only allowed athena modules are used, if not we catch the exception and disable feedback suggestions for the imported exercise + // If Athena is disabled and the service is not present, we also disable feedback suggestions + try { + athenaModuleService.ifPresentOrElse(ams -> ams.checkHasAccessToAthenaModule(newExercise, course, ENTITY_NAME), () -> newExercise.setFeedbackSuggestionModule(null)); + } + catch (BadRequestAlertException e) { + newExercise.setFeedbackSuggestionModule(null); + } + try { var importedProgrammingExercise = programmingExerciseImportService.importProgrammingExercise(originalProgrammingExercise, newExercise, updateTemplate, recreateBuildPlans); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/AthenaExerciseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/AthenaExerciseIntegrationTest.java index 7d8c15b9aec2..d242a205b6d6 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/AthenaExerciseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/AthenaExerciseIntegrationTest.java @@ -21,12 +21,15 @@ import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.TextExercise; +import de.tum.in.www1.artemis.domain.exam.ExerciseGroup; +import de.tum.in.www1.artemis.exam.ExamUtilService; import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; +import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseFactory; import de.tum.in.www1.artemis.exercise.textexercise.TextExerciseUtilService; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.user.UserUtilService; -public class AthenaExerciseIntegrationTest extends AbstractAthenaTest { +class AthenaExerciseIntegrationTest extends AbstractAthenaTest { private static final String TEST_PREFIX = "athenaexerciseintegration"; @@ -43,10 +46,10 @@ public class AthenaExerciseIntegrationTest extends AbstractAthenaTest { private TextExerciseUtilService textExerciseUtilService; @Autowired - private ExerciseUtilService exerciseUtilService; + private CourseUtilService courseUtilService; @Autowired - private CourseUtilService courseUtilService; + private ExamUtilService examUtilService; @Autowired private CourseTestService courseTestService; @@ -80,7 +83,7 @@ protected void initTestCase() { @Test @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") - void createTextExercise_useRestrictedAthenaModule_success() throws Exception { + void testCreateTextExercise_useRestrictedAthenaModule_success() throws Exception { course.setRestrictedAthenaModulesAccess(true); courseRepository.save(course); @@ -92,7 +95,7 @@ void createTextExercise_useRestrictedAthenaModule_success() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") - void createTextExercise_useRestrictedAthenaModule_badRequest() throws Exception { + void testCreateTextExercise_useRestrictedAthenaModule_badRequest() throws Exception { textExercise.setId(null); textExercise.setFeedbackSuggestionModule(ATHENA_RESTRICTED_MODULE_TEXT_TEST); @@ -101,7 +104,7 @@ void createTextExercise_useRestrictedAthenaModule_badRequest() throws Exception @Test @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") - void updateTextExercise_useRestrictedAthenaModule_success() throws Exception { + void testUpdateTextExercise_useRestrictedAthenaModule_success() throws Exception { course.setRestrictedAthenaModulesAccess(true); courseRepository.save(course); @@ -112,7 +115,7 @@ void updateTextExercise_useRestrictedAthenaModule_success() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") - void updateTextExercise_useRestrictedAthenaModule_badRequest() throws Exception { + void testUpdateTextExercise_useRestrictedAthenaModule_badRequest() throws Exception { textExercise.setFeedbackSuggestionModule(ATHENA_RESTRICTED_MODULE_TEXT_TEST); request.putWithResponseBody("/api/text-exercises/", textExercise, TextExercise.class, HttpStatus.BAD_REQUEST); @@ -120,24 +123,17 @@ void updateTextExercise_useRestrictedAthenaModule_badRequest() throws Exception @Test @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") - void createProgrammingExercise_useRestrictedAthenaModule_badRequest() throws Exception { - programmingExercise.setId(null); - programmingExercise.setFeedbackSuggestionModule(ATHENA_RESTRICTED_MODULE_PROGRAMMING_TEST); - - request.postWithResponseBody("/api/programming-exercises/setup/", programmingExercise, ProgrammingExercise.class, HttpStatus.BAD_REQUEST); - } - - @Test - @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") - void updateProgrammingExercise_useRestrictedAthenaModule_badRequest() throws Exception { - programmingExercise.setFeedbackSuggestionModule(ATHENA_RESTRICTED_MODULE_PROGRAMMING_TEST); + void testCreateExamTextExercise_useAthena_badRequest() throws Exception { + ExerciseGroup group = examUtilService.addExerciseGroupWithExamAndCourse(true); + TextExercise examTextExercise = TextExerciseFactory.generateTextExerciseForExam(group); + examTextExercise.setFeedbackSuggestionModule(ATHENA_RESTRICTED_MODULE_TEXT_TEST); - request.postWithResponseBody("/api/programming-exercises/setup/", programmingExercise, ProgrammingExercise.class, HttpStatus.BAD_REQUEST); + request.postWithResponseBody("/api/text-exercises/", examTextExercise, TextExercise.class, HttpStatus.BAD_REQUEST); } @Test @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void updateCourse_revokeRestrictedAthenaModuleAccess_badRequest() throws Exception { + void testUpdateCourse_revokeRestrictedAthenaModuleAccess_badRequest() throws Exception { course.setRestrictedAthenaModulesAccess(true); courseRepository.save(course); @@ -148,7 +144,7 @@ void updateCourse_revokeRestrictedAthenaModuleAccess_badRequest() throws Excepti @Test @WithMockUser(username = "admin", roles = "ADMIN") - void updateCourse_revokeRestrictedAthenaModuleAccess_success() throws Exception { + void testUpdateCourse_revokeRestrictedAthenaModuleAccess_success() throws Exception { course.setRestrictedAthenaModulesAccess(true); courseRepository.save(course); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/AthenaResourceIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/AthenaResourceIntegrationTest.java index 0ed0dc1ac559..fdc3e33e5439 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/AthenaResourceIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/AthenaResourceIntegrationTest.java @@ -112,20 +112,20 @@ protected void initTestCase() { @Test @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") void testGetAvailableProgrammingModulesSuccess_EmptyModules() throws Exception { - var course = textExercise.getCourseViaExerciseGroupOrCourseMember(); + var course = programmingExercise.getCourseViaExerciseGroupOrCourseMember(); athenaRequestMockProvider.mockGetAvailableModulesSuccessEmptyModulesList(); - List response = request.getList("/api/athena/programming-exercises/" + course.getId() + "/available-modules", HttpStatus.OK, String.class); + List response = request.getList("/api/athena/courses/" + course.getId() + "/programming-exercises/available-modules", HttpStatus.OK, String.class); assertThat(response).isEmpty(); } @Test @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") void testGetAvailableProgrammingModulesSuccess() throws Exception { - var course = textExercise.getCourseViaExerciseGroupOrCourseMember(); + var course = programmingExercise.getCourseViaExerciseGroupOrCourseMember(); athenaRequestMockProvider.mockGetAvailableModulesSuccess(); - List response = request.getList("/api/athena/programming-exercises/" + course.getId() + "/available-modules", HttpStatus.OK, String.class); + List response = request.getList("/api/athena/courses/" + course.getId() + "/programming-exercises/available-modules", HttpStatus.OK, String.class); assertThat(response).contains(ATHENA_MODULE_PROGRAMMING_TEST); } @@ -138,7 +138,7 @@ void testGetAvailableTextModulesSuccess_RestrictedModuleAccess() throws Exceptio courseRepository.save(course); athenaRequestMockProvider.mockGetAvailableModulesSuccess(); - List response = request.getList("/api/athena/text-exercises/" + course.getId() + "/available-modules", HttpStatus.OK, String.class); + List response = request.getList("/api/athena/courses/" + course.getId() + "/text-exercises/available-modules", HttpStatus.OK, String.class); assertThat(response).containsExactlyInAnyOrder(ATHENA_MODULE_TEXT_TEST, ATHENA_RESTRICTED_MODULE_TEXT_TEST); } @@ -146,12 +146,12 @@ void testGetAvailableTextModulesSuccess_RestrictedModuleAccess() throws Exceptio @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") void testGetAvailableProgrammingModulesSuccess_RestrictedModuleAccess() throws Exception { // give the course access to the restricted Athena modules - var course = textExercise.getCourseViaExerciseGroupOrCourseMember(); + var course = programmingExercise.getCourseViaExerciseGroupOrCourseMember(); course.setRestrictedAthenaModulesAccess(true); courseRepository.save(course); athenaRequestMockProvider.mockGetAvailableModulesSuccess(); - List response = request.getList("/api/athena/programming-exercises/" + course.getId() + "/available-modules", HttpStatus.OK, String.class); + List response = request.getList("/api/athena/courses/" + course.getId() + "/programming-exercises/available-modules", HttpStatus.OK, String.class); assertThat(response).containsExactlyInAnyOrder(ATHENA_MODULE_PROGRAMMING_TEST, ATHENA_RESTRICTED_MODULE_PROGRAMMING_TEST); } @@ -161,17 +161,17 @@ void testGetAvailableTextModulesSuccess() throws Exception { var course = textExercise.getCourseViaExerciseGroupOrCourseMember(); athenaRequestMockProvider.mockGetAvailableModulesSuccess(); - List response = request.getList("/api/athena/text-exercises/" + course.getId() + "/available-modules", HttpStatus.OK, String.class); + List response = request.getList("/api/athena/courses/" + course.getId() + "/text-exercises/available-modules", HttpStatus.OK, String.class); assertThat(response).contains(ATHENA_MODULE_TEXT_TEST); } @Test @WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA") void testGetAvailableProgrammingModulesAccessForbidden() throws Exception { - var course = textExercise.getCourseViaExerciseGroupOrCourseMember(); + var course = programmingExercise.getCourseViaExerciseGroupOrCourseMember(); athenaRequestMockProvider.mockGetAvailableModulesSuccess(); - request.getList("/api/athena/programming-exercises/" + course.getId() + "/available-modules", HttpStatus.FORBIDDEN, String.class); + request.getList("/api/athena/courses/" + course.getId() + "/programming-exercises/available-modules", HttpStatus.FORBIDDEN, String.class); } @Test @@ -180,7 +180,7 @@ void testGetAvailableTextModulesAccessForbidden() throws Exception { var course = textExercise.getCourseViaExerciseGroupOrCourseMember(); athenaRequestMockProvider.mockGetAvailableModulesSuccess(); - request.getList("/api/athena/text-exercises/" + course.getId() + "/available-modules", HttpStatus.FORBIDDEN, String.class); + request.getList("/api/athena/courses/" + course.getId() + "/text-exercises/available-modules", HttpStatus.FORBIDDEN, String.class); } @Test From 38fdcab6aaf5f891fb50181ec5b06981f7414ef1 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Tue, 19 Dec 2023 19:43:47 +0100 Subject: [PATCH 09/31] Add JavaDocs comments & improve German label translation --- .../repository/ExerciseRepository.java | 6 ++++ .../athena/AthenaModuleService.java | 33 +++++++++++++++++++ .../www1/artemis/web/rest/AthenaResource.java | 12 +++++++ src/main/webapp/i18n/de/course.json | 2 +- 4 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java index c166b9e8b45f..02bfab3340d7 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/ExerciseRepository.java @@ -544,6 +544,12 @@ default Set findAllFeedbackSuggestionsEnabledExercisesWithFutureDueDat return findByFeedbackSuggestionModuleNotNullAndDueDateIsAfter(ZonedDateTime.now()); } + /** + * Revokes the access by setting all exercises that currently utilize a restricted module to null. + * + * @param courseId The course for which the access should be revoked + * @param restrictedFeedbackSuggestionModule Collection of restricted modules + */ @Transactional // ok because of modifying query @Modifying @Query(""" diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java index 715c02125a26..d7d09ec2fd27 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java @@ -54,6 +54,12 @@ public AthenaModuleService(@Qualifier("shortTimeoutAthenaRestTemplate") RestTemp private record AthenaModuleDTO(String name, String type) { } + /** + * Get all available modules from Athena. + * + * @return A list of all available Athena Modules + * @throws NetworkingException is thrown in case the modules can't be fetched from Athena + */ private List getAthenaModules() throws NetworkingException { try { var response = shortTimeoutRestTemplate.getForEntity(athenaUrl + "/modules", JsonNode.class); @@ -69,6 +75,13 @@ private List getAthenaModules() throws NetworkingException { } } + /** + * Get all available Athena programming modules for a specific course. + * + * @param course The course for which the modules should be retrieved + * @return The list of available Athena text modules for the course + * @throws NetworkingException is thrown in case the modules can't be fetched from Athena + */ public List getAthenaProgrammingModulesForCourse(Course course) throws NetworkingException { List availableProgrammingModules = getAthenaModules().stream().filter(module -> "programming".equals(module.type)).map(module -> module.name).toList(); if (!course.getRestrictedAthenaModulesAccess()) { @@ -78,6 +91,13 @@ public List getAthenaProgrammingModulesForCourse(Course course) throws N return availableProgrammingModules; } + /** + * Get all available Athena text modules for a specific course. + * + * @param course The course for which the modules should be retrieved + * @return The list of available Athena text modules for the course + * @throws NetworkingException is thrown in case the modules can't be fetched from Athena + */ public List getAthenaTextModulesForCourse(Course course) throws NetworkingException { List availableProgrammingModules = getAthenaModules().stream().filter(module -> "text".equals(module.type)).map(module -> module.name).toList(); if (!course.getRestrictedAthenaModulesAccess()) { @@ -105,6 +125,14 @@ public String getAthenaModuleUrl(Exercise exercise) { } } + /** + * Checks if an exercise has access to the provided Athena module. + * + * @param exercise The exercise for which the access should be checked + * @param course The course to which the exercise belongs to. + * @param entityName + * @throws BadRequestAlertException when the exercise has no access to the exercise's provided module. + */ public void checkHasAccessToAthenaModule(Exercise exercise, Course course, String entityName) throws BadRequestAlertException { if (exercise.isExamExercise()) { throw new BadRequestAlertException("The exam exercise has no access to Athena", entityName, "examExerciseNoAccessToAthena"); @@ -115,6 +143,11 @@ public void checkHasAccessToAthenaModule(Exercise exercise, Course course, Strin } } + /** + * Revokes the access to restricted Athena modules for all exercises of a course. + * + * @param course The course for which the access to restricted modules should be revoked + */ public void revokeAccessToRestrictedFeedbackSuggestionModules(Course course) { exerciseRepository.revokeAccessToRestrictedFeedbackSuggestionModulesByCourseId(course.getId(), restrictedModules); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java index b3b61605813f..5ff520fd4d2f 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java @@ -134,6 +134,12 @@ public ResponseEntity> getProgrammingFeedbackSugges athenaFeedbackSuggestionsService::getProgrammingFeedbackSuggestions); } + /** + * GET athena/courses/{courseId}/programming-exercises/available-modules : Get all available Athena modules for a programming exercise in the course + * + * @param courseId the id of the course the programming exercise belongs to + * @return 200 Ok if successful with the modules as body + */ @GetMapping("athena/courses/{courseId}/programming-exercises/available-modules") @EnforceAtLeastEditor public ResponseEntity> getAvailableModulesForProgrammingExercises(@PathVariable long courseId) { @@ -152,6 +158,12 @@ public ResponseEntity> getAvailableModulesForProgrammingExercises(@ } + /** + * GET athena/courses/{courseId}/text-exercises/available-modules : Get all available Athena modules for a text exercise in the course + * + * @param courseId the id of the course the text exercise belongs to + * @return 200 Ok if successful with the modules as body + */ @GetMapping("athena/courses/{courseId}/text-exercises/available-modules") @EnforceAtLeastEditor public ResponseEntity> getAvailableModulesForTextExercises(@PathVariable long courseId) { diff --git a/src/main/webapp/i18n/de/course.json b/src/main/webapp/i18n/de/course.json index 308d3794a2f0..405795c7d0ea 100644 --- a/src/main/webapp/i18n/de/course.json +++ b/src/main/webapp/i18n/de/course.json @@ -83,7 +83,7 @@ "tooltip": "Ermöglicht es aus Kompetenzen und deren Lerninhalten einen individuellen Lernpfad für Studierende zu generieren." }, "restrictedAthenaModulesAccess": { - "label": "Zugriff auf eingeschränkte Athena-Module aktiviert", + "label": "Zugriff auf geschützte Athena-Module aktiviert", "tooltip": "Wenn diese Option ausgewählt ist, können die Übungen des Kurses auch geschützte Athena-Module für die Feedbackerstellung verwenden." }, "onlineCourse": { From a4ac99732786167c8838b0ee9076f8f7ef6107f5 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Tue, 19 Dec 2023 20:54:14 +0100 Subject: [PATCH 10/31] Adapt feedback suggestions options component to new Angular control syntax --- ...feedback-suggestion-options.component.html | 68 ++++++++++--------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html index 8758af669b3e..02e2adaf9995 100644 --- a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html +++ b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html @@ -1,32 +1,38 @@ -
-
- - - +@if ((isAthenaEnabled$ | async) && modulesAvailable) { +
+
+ + + +
+ @if (!!this.exercise.feedbackSuggestionModule) { +
+ + +
+ }
-
- - -
-
+} From 7d2faf4d454ccf0c5b58e9eed4c08684278fc9e6 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Tue, 19 Dec 2023 20:57:26 +0100 Subject: [PATCH 11/31] Fix Athena module access check for exam exercises --- .../artemis/service/connectors/athena/AthenaModuleService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java index d7d09ec2fd27..93be575d20f6 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java @@ -134,7 +134,7 @@ public String getAthenaModuleUrl(Exercise exercise) { * @throws BadRequestAlertException when the exercise has no access to the exercise's provided module. */ public void checkHasAccessToAthenaModule(Exercise exercise, Course course, String entityName) throws BadRequestAlertException { - if (exercise.isExamExercise()) { + if (exercise.isExamExercise() && exercise.getFeedbackSuggestionModule() != null) { throw new BadRequestAlertException("The exam exercise has no access to Athena", entityName, "examExerciseNoAccessToAthena"); } if (!course.getRestrictedAthenaModulesAccess() && restrictedModules.contains(exercise.getFeedbackSuggestionModule())) { From c6211e7d3666fb1a78584e955a428f2d1014f374 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Fri, 22 Dec 2023 16:24:36 +0100 Subject: [PATCH 12/31] Remove duplicate plagiarism control from merge --- .../manage/text-exercise/text-exercise-update.component.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html index eedf8933331f..e7bd22861cb4 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html @@ -155,7 +155,6 @@

- }
From aa034d867bc92e0dbca95003faa2e163d12a04cb Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Fri, 22 Dec 2023 16:28:30 +0100 Subject: [PATCH 13/31] Add comments to mock methods --- .../connector/AthenaRequestMockProvider.java | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/test/java/de/tum/in/www1/artemis/connector/AthenaRequestMockProvider.java b/src/test/java/de/tum/in/www1/artemis/connector/AthenaRequestMockProvider.java index 77011cbfc271..e54a3d76e679 100644 --- a/src/test/java/de/tum/in/www1/artemis/connector/AthenaRequestMockProvider.java +++ b/src/test/java/de/tum/in/www1/artemis/connector/AthenaRequestMockProvider.java @@ -204,6 +204,11 @@ else if (moduleType.equals("programming")) { responseActions.andRespond(withSuccess(node.toString(), MediaType.APPLICATION_JSON)); } + /** + * Mocks the /modules API from Athena used to retrieve all available feedback suggestion modules + * Makes the endpoint return an empty module list. + * + */ public void mockGetAvailableModulesSuccessEmptyModulesList() { // Response: [] final ArrayNode array = mapper.createArrayNode(); @@ -212,6 +217,11 @@ public void mockGetAvailableModulesSuccessEmptyModulesList() { responseActions.andRespond(withSuccess().body(array.toString()).contentType(MediaType.APPLICATION_JSON)); } + /** + * Mocks the /modules API from Athena used to retrieve all available feedback suggestion modules + * Makes the endpoint return four modules (2x text and 2x programming). + * + */ public void mockGetAvailableModulesSuccess() { // Response: // [{"name":"module_example","url":"http://module-example-service:5001","type":"programming","supports_evaluation":true},{"name":"module_programming_llm","url":"http://module-programming-llm-service:5002","type":"programming","supports_evaluation":false},{"name":"module_text_llm","url":"http://module-text-llm-service:5003","type":"text","supports_evaluation":true},{"name":"module_text_cofee","url":"http://module-text-cofee-service:5004","type":"text","supports_evaluation":false},{"name":"module_programming_themisml","url":"http://module-programming-themisml-service:5005","type":"programming","supports_evaluation":false}] @@ -226,11 +236,15 @@ public void mockGetAvailableModulesSuccess() { responseActions.andRespond(withSuccess().body(array.toString()).contentType(MediaType.APPLICATION_JSON)); } - public void mockGetAvailableModulesFailure() { - final ResponseActions responseActions = mockServerShortTimeout.expect(ExpectedCount.once(), requestTo(athenaUrl + "/modules")).andExpect(method(HttpMethod.GET)); - responseActions.andRespond(withException(new SocketTimeoutException())); - } - + /** + * Helper method to create a JSON representation of a feedback module as sent by Athena + * + * @param name The name of the module + * @param url The URL of the module + * @param type The type for which the module can generate feedback suggestions + * @param supportsEvaluation Indicating if the module can support evaluation (not used in Artemis) + * @return JSON representation of the feedback module + */ private ObjectNode createModule(String name, String url, String type, boolean supportsEvaluation) { // creates {"name":"module_example","url":"http://module-example-service:5001","type":"programming","supports_evaluation":true} ObjectNode moduleNode = mapper.createObjectNode(); @@ -242,7 +256,7 @@ private ObjectNode createModule(String name, String url, String type, boolean su } /** - * Mocks ths /health API endpoint from Athena used to check if the service is up and running + * Mocks the /health API endpoint from Athena used to check if the service is up and running * * @param exampleModuleHealthy Example module health status (in addition to the general status) */ From 4a69b0fc411a10576c69e6412b757b59ea53a341 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Thu, 28 Dec 2023 12:53:09 +0100 Subject: [PATCH 14/31] Add check if athena is enabled for the exercise to feedback suggestion endpoint --- .../de/tum/in/www1/artemis/web/rest/AthenaResource.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java index 5ff520fd4d2f..9437621e4b41 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java @@ -96,6 +96,11 @@ private Re final var exercise = exerciseFetcher.apply(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.TEACHING_ASSISTANT, exercise, null); + // Check if feedback suggestions are actually enabled + if (!exercise.isFeedbackSuggestionsEnabled()) { + throw new InternalServerErrorException("Feedback suggestions are not enabled for this exercise"); + } + final var submission = submissionFetcher.apply(submissionId); try { @@ -130,6 +135,7 @@ public ResponseEntity> getTextFeedbackSuggestions(@PathVar @GetMapping("athena/programming-exercises/{exerciseId}/submissions/{submissionId}/feedback-suggestions") @EnforceAtLeastTutor public ResponseEntity> getProgrammingFeedbackSuggestions(@PathVariable long exerciseId, @PathVariable long submissionId) { + Exercise exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); return getFeedbackSuggestions(exerciseId, submissionId, programmingExerciseRepository::findByIdElseThrow, programmingSubmissionRepository::findByIdElseThrow, athenaFeedbackSuggestionsService::getProgrammingFeedbackSuggestions); } From 7f0bb5e9ebc83d3246298bcd4ce0322ac54a92bd Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Fri, 29 Dec 2023 15:07:59 +0100 Subject: [PATCH 15/31] Add check to disallow module change after due date has passed --- .../athena/AthenaModuleService.java | 20 ++++++++++- .../web/rest/ProgrammingExerciseResource.java | 2 ++ .../web/rest/TextExerciseResource.java | 4 ++- ...gramming-exercise-lifecycle.component.html | 2 +- ...e-feedback-suggestion-options.component.ts | 33 ++++++++++++++++--- .../text-exercise-update.component.html | 2 +- .../AthenaExerciseIntegrationTest.java | 12 +++++++ ...edback-suggestion-option.component.spec.ts | 18 ++++++++-- 8 files changed, 81 insertions(+), 12 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java index 93be575d20f6..a16f82f1f6c2 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java @@ -1,6 +1,8 @@ package de.tum.in.www1.artemis.service.connectors.athena; +import java.time.ZonedDateTime; import java.util.List; +import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -130,7 +132,7 @@ public String getAthenaModuleUrl(Exercise exercise) { * * @param exercise The exercise for which the access should be checked * @param course The course to which the exercise belongs to. - * @param entityName + * @param entityName Name of the entity * @throws BadRequestAlertException when the exercise has no access to the exercise's provided module. */ public void checkHasAccessToAthenaModule(Exercise exercise, Course course, String entityName) throws BadRequestAlertException { @@ -143,6 +145,22 @@ public void checkHasAccessToAthenaModule(Exercise exercise, Course course, Strin } } + /** + * Checks if a module change is valid or not. In case it is not allowed it throws an exception. + * Modules cannot be changed after the exercise due date has passed. + * + * @param originalExercise The exercise before the update + * @param updatedExercise The exercise after the update + * @param entityName Name of the entity + * @throws BadRequestAlertException Is thrown in case the module change is not allowed + */ + public void checkValidAthenaModuleChange(Exercise originalExercise, Exercise updatedExercise, String entityName) throws BadRequestAlertException { + if (!Objects.equals(originalExercise.getFeedbackSuggestionModule(), updatedExercise.getFeedbackSuggestionModule()) + && originalExercise.getDueDate().isBefore(ZonedDateTime.now())) { + throw new BadRequestAlertException("Athena module can't be changed after due date has passed", entityName, "athenaModuleChangeAfterDueDate"); + } + } + /** * Revokes the access to restricted Athena modules for all exercises of a course. * diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java index 789dfdcd9320..1eb2185705ba 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java @@ -284,6 +284,8 @@ public ResponseEntity updateProgrammingExercise(@RequestBod // Check that only allowed Athena modules are used athenaModuleService.ifPresentOrElse(ams -> ams.checkHasAccessToAthenaModule(updatedProgrammingExercise, course, ENTITY_NAME), () -> updatedProgrammingExercise.setFeedbackSuggestionModule(null)); + // Changing Athena module after the due date has passed is not allowed + athenaModuleService.ifPresent(ams -> ams.checkValidAthenaModuleChange(programmingExerciseBeforeUpdate, updatedProgrammingExercise, ENTITY_NAME)); // Ignore changes to the default branch updatedProgrammingExercise.setBranch(programmingExerciseBeforeUpdate.getBranch()); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java index e4e1f0e8930c..92714ea98a58 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java @@ -226,9 +226,11 @@ public ResponseEntity updateTextExercise(@RequestBody TextExercise // Forbid conversion between normal course exercise and exam exercise exerciseService.checkForConversionBetweenExamAndCourseExercise(textExercise, textExerciseBeforeUpdate, ENTITY_NAME); - // TODO Athena: Check that only allowed athena modules are used + // Check that only allowed athena modules are used Course course = courseService.retrieveCourseOverExerciseGroupOrCourseId(textExerciseBeforeUpdate); athenaModuleService.ifPresentOrElse(ams -> ams.checkHasAccessToAthenaModule(textExercise, course, ENTITY_NAME), () -> textExercise.setFeedbackSuggestionModule(null)); + // Changing Athena module after the due date has passed is not allowed + athenaModuleService.ifPresent(ams -> ams.checkValidAthenaModuleChange(textExerciseBeforeUpdate, textExercise, ENTITY_NAME)); channelService.updateExerciseChannel(textExerciseBeforeUpdate, textExercise); diff --git a/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.html b/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.html index 9ee15aae9789..d05638ea2335 100644 --- a/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.html +++ b/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.html @@ -190,6 +190,6 @@
@if (!isExamMode) { - + }

diff --git a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts index 123c820e4180..9422da32d6b7 100644 --- a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts +++ b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.ts @@ -1,16 +1,18 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnChanges, OnInit } from '@angular/core'; import { AssessmentType } from 'app/entities/assessment-type.model'; import { Exercise, ExerciseType } from 'app/entities/exercise.model'; import { Observable } from 'rxjs'; import { AthenaService } from 'app/assessment/athena.service'; import { ActivatedRoute } from '@angular/router'; +import dayjs from 'dayjs/esm'; @Component({ selector: 'jhi-exercise-feedback-suggestion-options', templateUrl: './exercise-feedback-suggestion-options.component.html', }) -export class ExerciseFeedbackSuggestionOptionsComponent implements OnInit { +export class ExerciseFeedbackSuggestionOptionsComponent implements OnInit, OnChanges { @Input() exercise: Exercise; + @Input() dueDate?: dayjs.Dayjs; @Input() readOnly: boolean = false; protected readonly AssessmentType = AssessmentType; @@ -20,6 +22,7 @@ export class ExerciseFeedbackSuggestionOptionsComponent implements OnInit { isAthenaEnabled$: Observable; modulesAvailable: boolean; availableAthenaModules: string[]; + initialAthenaModule?: string; constructor( private athenaService: AthenaService, @@ -33,17 +36,33 @@ export class ExerciseFeedbackSuggestionOptionsComponent implements OnInit { this.modulesAvailable = modules.length > 0; }); this.isAthenaEnabled$ = this.athenaService.isEnabled(); + this.initialAthenaModule = this.exercise.feedbackSuggestionModule; } + ngOnChanges(changes: any) { + if (!changes['dueDate'].isFirstChange()) { + if (this.inputControlsDisabled()) { + this.exercise.feedbackSuggestionModule = this.initialAthenaModule; + } + } + } + + /** + * Returns true in case the input controls should be disabled. This is the case for all exercises when the due date has passed. For programming exercises, + * it returns true in case the assessment type is automatic, the exercise is readonly, the due date is undefined or the due date has passed. + */ inputControlsDisabled() { if (this.exercise.type == ExerciseType.PROGRAMMING) { - return this.exercise.assessmentType == AssessmentType.AUTOMATIC || this.readOnly; + return this.exercise.assessmentType == AssessmentType.AUTOMATIC || this.readOnly || this.exercise.dueDate == undefined || this.hasDueDatePassed(); } - return false; + return this.hasDueDatePassed(); } + /** + * Returns the label style for the checkbox to enable feedback suggestions. In case the input controls are disabled, the label text color is set to grey. + */ getCheckboxLabelStyle() { - if (this.exercise.type == ExerciseType.PROGRAMMING && this.exercise.assessmentType == AssessmentType.AUTOMATIC) { + if (this.inputControlsDisabled()) { return { color: 'grey' }; } return {}; @@ -56,4 +75,8 @@ export class ExerciseFeedbackSuggestionOptionsComponent implements OnInit { this.exercise.feedbackSuggestionModule = undefined; } } + + private hasDueDatePassed() { + return dayjs(this.exercise.dueDate).isBefore(dayjs()); + } } diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html index e7bd22861cb4..96e36ae75d79 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html @@ -153,7 +153,7 @@

+ } diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/AthenaExerciseIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/AthenaExerciseIntegrationTest.java index d242a205b6d6..f1b2b85be2bc 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/AthenaExerciseIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/AthenaExerciseIntegrationTest.java @@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.time.ZonedDateTime; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -121,6 +122,17 @@ void testUpdateTextExercise_useRestrictedAthenaModule_badRequest() throws Except request.putWithResponseBody("/api/text-exercises/", textExercise, TextExercise.class, HttpStatus.BAD_REQUEST); } + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void testUpdateTextExercise_afterDueDate_badRequest() throws Exception { + textExercise.setDueDate(ZonedDateTime.now()); + textExerciseRepository.save(textExercise); + + textExercise.setFeedbackSuggestionModule(ATHENA_MODULE_TEXT_TEST); + + request.putWithResponseBody("/api/text-exercises/", textExercise, TextExercise.class, HttpStatus.BAD_REQUEST); + } + @Test @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") void testCreateExamTextExercise_useAthena_badRequest() throws Exception { diff --git a/src/test/javascript/spec/component/exercises/shared/feedback/feedback-suggestion-option.component.spec.ts b/src/test/javascript/spec/component/exercises/shared/feedback/feedback-suggestion-option.component.spec.ts index 08267185760d..79273358dc6d 100644 --- a/src/test/javascript/spec/component/exercises/shared/feedback/feedback-suggestion-option.component.spec.ts +++ b/src/test/javascript/spec/component/exercises/shared/feedback/feedback-suggestion-option.component.spec.ts @@ -6,11 +6,14 @@ import { Exercise, ExerciseType } from 'app/entities/exercise.model'; import { AssessmentType } from 'app/entities/assessment-type.model'; import { AthenaService } from 'app/assessment/athena.service'; import { ExerciseFeedbackSuggestionOptionsComponent } from 'app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component'; +import dayjs from 'dayjs/esm'; describe('ExerciseFeedbackSuggestionOptionsComponent', () => { let component: ExerciseFeedbackSuggestionOptionsComponent; let fixture: ComponentFixture; let athenaService: AthenaService; + const pastDueDate = dayjs().subtract(1, 'hour'); + const futureDueDate = dayjs().add(1, 'hour'); beforeEach(() => { TestBed.configureTestingModule({ @@ -26,6 +29,8 @@ describe('ExerciseFeedbackSuggestionOptionsComponent', () => { fixture = TestBed.createComponent(ExerciseFeedbackSuggestionOptionsComponent); component = fixture.componentInstance; athenaService = TestBed.inject(AthenaService); + + component.exercise = { feedbackSuggestionModule: undefined, dueDate: undefined }; }); it('should create', () => { @@ -55,7 +60,7 @@ describe('ExerciseFeedbackSuggestionOptionsComponent', () => { }); it('should disable input controls for programming exercises with automatic assessment type or read-only', () => { - component.exercise = { type: ExerciseType.PROGRAMMING, assessmentType: AssessmentType.AUTOMATIC } as Exercise; + component.exercise = { type: ExerciseType.PROGRAMMING, assessmentType: AssessmentType.AUTOMATIC, dueDate: futureDueDate } as Exercise; let result = component.inputControlsDisabled(); expect(result).toBeTruthy(); @@ -69,8 +74,15 @@ describe('ExerciseFeedbackSuggestionOptionsComponent', () => { expect(result).toBeTruthy(); }); + it('should disable input controls if due date has passed', () => { + component.exercise = { type: ExerciseType.TEXT, dueDate: pastDueDate } as Exercise; + + const result = component.inputControlsDisabled(); + expect(result).toBeTruthy(); + }); + it('should return grey color for checkbox label style for automatic programming exercises', () => { - component.exercise = { type: ExerciseType.PROGRAMMING, assessmentType: AssessmentType.AUTOMATIC } as Exercise; + component.exercise = { type: ExerciseType.PROGRAMMING, assessmentType: AssessmentType.AUTOMATIC, dueDate: futureDueDate } as Exercise; const style = component.getCheckboxLabelStyle(); @@ -78,7 +90,7 @@ describe('ExerciseFeedbackSuggestionOptionsComponent', () => { }); it('should return an empty object for checkbox label style for non-automatic programming exercises', () => { - component.exercise = { type: ExerciseType.PROGRAMMING, assessmentType: AssessmentType.MANUAL } as Exercise; + component.exercise = { type: ExerciseType.PROGRAMMING, assessmentType: AssessmentType.MANUAL, dueDate: futureDueDate } as Exercise; const style = component.getCheckboxLabelStyle(); From 1ef48c82ced4b5460dfc8057e98d78826d5d8b79 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Fri, 29 Dec 2023 15:57:54 +0100 Subject: [PATCH 16/31] Fix failing test --- .../service/connectors/athena/AthenaModuleService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java index a16f82f1f6c2..b2ba48faa901 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java @@ -155,8 +155,9 @@ public void checkHasAccessToAthenaModule(Exercise exercise, Course course, Strin * @throws BadRequestAlertException Is thrown in case the module change is not allowed */ public void checkValidAthenaModuleChange(Exercise originalExercise, Exercise updatedExercise, String entityName) throws BadRequestAlertException { - if (!Objects.equals(originalExercise.getFeedbackSuggestionModule(), updatedExercise.getFeedbackSuggestionModule()) - && originalExercise.getDueDate().isBefore(ZonedDateTime.now())) { + var dueDate = originalExercise.getDueDate(); + if (!Objects.equals(originalExercise.getFeedbackSuggestionModule(), updatedExercise.getFeedbackSuggestionModule()) && dueDate != null + && dueDate.isBefore(ZonedDateTime.now())) { throw new BadRequestAlertException("Athena module can't be changed after due date has passed", entityName, "athenaModuleChangeAfterDueDate"); } } From 5a097aaa3ed73d76e1ba19b4c049cc08a5fd5af6 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Fri, 29 Dec 2023 16:30:24 +0100 Subject: [PATCH 17/31] Fix client test compilation --- .../feedback/feedback-suggestion-option.component.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/javascript/spec/component/exercises/shared/feedback/feedback-suggestion-option.component.spec.ts b/src/test/javascript/spec/component/exercises/shared/feedback/feedback-suggestion-option.component.spec.ts index 79273358dc6d..2a6e4bf46ef5 100644 --- a/src/test/javascript/spec/component/exercises/shared/feedback/feedback-suggestion-option.component.spec.ts +++ b/src/test/javascript/spec/component/exercises/shared/feedback/feedback-suggestion-option.component.spec.ts @@ -29,8 +29,7 @@ describe('ExerciseFeedbackSuggestionOptionsComponent', () => { fixture = TestBed.createComponent(ExerciseFeedbackSuggestionOptionsComponent); component = fixture.componentInstance; athenaService = TestBed.inject(AthenaService); - - component.exercise = { feedbackSuggestionModule: undefined, dueDate: undefined }; + component.exercise = { feedbackSuggestionModule: undefined, dueDate: undefined } as Exercise; }); it('should create', () => { From 9f4669ac194fbc494aee4fe09d5dc14730298cf9 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Fri, 29 Dec 2023 17:04:06 +0100 Subject: [PATCH 18/31] Fix client test --- .../feedback/feedback-suggestion-option.component.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/javascript/spec/component/exercises/shared/feedback/feedback-suggestion-option.component.spec.ts b/src/test/javascript/spec/component/exercises/shared/feedback/feedback-suggestion-option.component.spec.ts index 2a6e4bf46ef5..ff2cd13ba827 100644 --- a/src/test/javascript/spec/component/exercises/shared/feedback/feedback-suggestion-option.component.spec.ts +++ b/src/test/javascript/spec/component/exercises/shared/feedback/feedback-suggestion-option.component.spec.ts @@ -29,7 +29,6 @@ describe('ExerciseFeedbackSuggestionOptionsComponent', () => { fixture = TestBed.createComponent(ExerciseFeedbackSuggestionOptionsComponent); component = fixture.componentInstance; athenaService = TestBed.inject(AthenaService); - component.exercise = { feedbackSuggestionModule: undefined, dueDate: undefined } as Exercise; }); it('should create', () => { @@ -39,6 +38,7 @@ describe('ExerciseFeedbackSuggestionOptionsComponent', () => { it('should initialize with available modules', async () => { const modules = ['Module1', 'Module2']; jest.spyOn(athenaService, 'getAvailableModules').mockReturnValue(of(modules)); + component.exercise = { type: ExerciseType.TEXT, dueDate: futureDueDate, feedbackSuggestionModule: undefined } as Exercise; await component.ngOnInit(); @@ -49,6 +49,7 @@ describe('ExerciseFeedbackSuggestionOptionsComponent', () => { it('should set isAthenaEnabled$ with the result from athenaService', async () => { jest.spyOn(athenaService, 'getAvailableModules').mockReturnValue(of()); jest.spyOn(athenaService, 'isEnabled').mockReturnValue(of(true)); + component.exercise = { type: ExerciseType.TEXT, dueDate: futureDueDate, feedbackSuggestionModule: undefined } as Exercise; await component.ngOnInit(); From d356565cfb9568ca8214535e1f46f3b7ea70889e Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Fri, 29 Dec 2023 23:37:49 +0100 Subject: [PATCH 19/31] Remove all todos --- src/main/java/de/tum/in/www1/artemis/domain/Exercise.java | 1 - .../programming/ProgrammingExerciseImportService.java | 1 - .../www1/artemis/web/rest/ProgrammingExerciseResource.java | 2 +- .../tum/in/www1/artemis/web/rest/TextExerciseResource.java | 2 +- src/main/webapp/app/assessment/athena.service.ts | 7 ++++++- .../programming-exercise-lifecycle.component.spec.ts | 1 - 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java b/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java index d32d359e8ca8..e02b22d1fc3c 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java @@ -101,7 +101,6 @@ public abstract class Exercise extends BaseExercise implements LearningObject { @Column(name = "second_correction_enabled") private Boolean secondCorrectionEnabled = false; - // TODO Athena: adapt to new exercise model: instead of using a boolean, we just use the module name (enabled) or null @Column(name = "feedback_suggestion_module") // Athena module name (Athena enabled) or null private String feedbackSuggestionModule; diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java index 5b7436180892..2b81e7b49964 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseImportService.java @@ -280,7 +280,6 @@ public ProgrammingExercise importProgrammingExercise(ProgrammingExercise origina if (newExercise.isExamExercise()) { // Disable feedback suggestions on exam exercises (currently not supported) - // TODO Athena: Check that only allowed athena modules are used newExercise.setFeedbackSuggestionModule(null); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java index 1eb2185705ba..ca724a7eb270 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingExerciseResource.java @@ -206,7 +206,7 @@ public ResponseEntity createProgrammingExercise(@RequestBod authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, null); programmingExerciseService.validateNewProgrammingExerciseSettings(programmingExercise, course); - // TODO Athena: Check that only allowed athena modules are used + // Check that only allowed athena modules are used athenaModuleService.ifPresentOrElse(ams -> ams.checkHasAccessToAthenaModule(programmingExercise, course, ENTITY_NAME), () -> programmingExercise.setFeedbackSuggestionModule(null)); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java index 92714ea98a58..f88c7e41cbb5 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/TextExerciseResource.java @@ -177,7 +177,7 @@ public ResponseEntity createTextExercise(@RequestBody TextExercise // Check that the user is authorized to create the exercise authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, null); - // TODO Athena: Check that only allowed athena modules are used + // Check that only allowed athena modules are used athenaModuleService.ifPresentOrElse(ams -> ams.checkHasAccessToAthenaModule(textExercise, course, ENTITY_NAME), () -> textExercise.setFeedbackSuggestionModule(null)); TextExercise result = textExerciseRepository.save(textExercise); diff --git a/src/main/webapp/app/assessment/athena.service.ts b/src/main/webapp/app/assessment/athena.service.ts index aabe395e9472..de0031a3f9c6 100644 --- a/src/main/webapp/app/assessment/athena.service.ts +++ b/src/main/webapp/app/assessment/athena.service.ts @@ -23,7 +23,12 @@ export class AthenaService { return this.profileService.getProfileInfo().pipe(switchMap((profileInfo) => of(profileInfo.activeProfiles.includes(PROFILE_ATHENA)))); } - // TODO Athena: Add methods to get available modules + /** + * Fetches all available modules for a course and exercise. + * + * @param courseId The id of the course for which the feedback suggestion modules should be fetched + * @param exercise The exercise for which the feedback suggestion modules should be fetched + */ public getAvailableModules(courseId: number, exercise: Exercise): Observable { return this.isEnabled().pipe( switchMap((isAthenaEnabled) => { diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-lifecycle.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-lifecycle.component.spec.ts index f6f4fe1f5715..e92c1a25cc49 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-lifecycle.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-lifecycle.component.spec.ts @@ -314,5 +314,4 @@ describe('ProgrammingExerciseLifecycleComponent', () => { const checkbox: HTMLInputElement = fixture.debugElement.nativeElement.querySelector('#releaseTestsWithExampleSolution'); expectElementToBeDisabled(checkbox); }); - // TODO Athena: Add test }); From 4da186fe63c76105b78822e43926be176078891e Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Sat, 30 Dec 2023 00:51:56 +0100 Subject: [PATCH 20/31] Add client test for course update component --- .../course/course-update.component.spec.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/test/javascript/spec/component/course/course-update.component.spec.ts b/src/test/javascript/spec/component/course/course-update.component.spec.ts index b69e7d29594a..1adf30acd7f6 100644 --- a/src/test/javascript/spec/component/course/course-update.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-update.component.spec.ts @@ -482,6 +482,23 @@ describe('Course Management Update Component', () => { }); }); + describe('changeRestrictedAthenaModulesEnabled', () => { + it('should toggle restricted athena modules access', () => { + comp.course = new Course(); + comp.course.restrictedAthenaModulesAccess = true; + comp.courseForm = new FormGroup({ restrictedAthenaModulesAccess: new FormControl(true) }); + + expect(comp.course.restrictedAthenaModulesAccess).toBeTrue(); + expect(comp.courseForm.controls['restrictedAthenaModulesAccess'].value).toBeTruthy(); + comp.changeRestrictedAthenaModulesEnabled(); + expect(comp.course.restrictedAthenaModulesAccess).toBeFalse(); + expect(comp.courseForm.controls['restrictedAthenaModulesAccess'].value).toBeFalsy(); + comp.changeRestrictedAthenaModulesEnabled(); + expect(comp.course.restrictedAthenaModulesAccess).toBeTrue(); + expect(comp.courseForm.controls['restrictedAthenaModulesAccess'].value).toBeTruthy(); + }); + }); + describe('getSemesters', () => { it('should get semesters around current year', () => { const years = dayjs().year() - 2018 + 1; From 8dc67fe866b750ecb38785d471886d7e491da4c7 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Mon, 22 Jan 2024 12:57:23 +0100 Subject: [PATCH 21/31] Integrate review suggestions --- .../course/manage/detail/course-detail.component.html | 3 +-- .../manage/programming-exercise-detail.component.html | 10 +++++----- .../programming-exercise-lifecycle.component.html | 2 +- ...exercise-feedback-suggestion-options.component.html | 4 ++-- .../text-exercise/text-exercise-detail.component.html | 10 +++++----- .../text-exercise/text-exercise-update.component.html | 2 +- 6 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/main/webapp/app/course/manage/detail/course-detail.component.html b/src/main/webapp/app/course/manage/detail/course-detail.component.html index 93841dc344b1..f6e181ccbd75 100644 --- a/src/main/webapp/app/course/manage/detail/course-detail.component.html +++ b/src/main/webapp/app/course/manage/detail/course-detail.component.html @@ -273,8 +273,7 @@

Course Details: @if (this.course.restrictedAthenaModulesAccess) { {{ 'global.generic.yes' | artemisTranslate }} - } - @if (!this.course.restrictedAthenaModulesAccess) { + } @else { {{ 'global.generic.no' | artemisTranslate }} } diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html index 13146832eaba..63bd23bfe5d8 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-detail.component.html @@ -272,11 +272,11 @@

Exercise Details

Enable feedback suggestions from Athena
- {{ - !!programmingExercise.feedbackSuggestionModule - ? ('global.generic.yes' | artemisTranslate) + ' - ' + programmingExercise.feedbackSuggestionModule - : ('global.generic.no' | artemisTranslate) - }} + @if (!!programmingExercise.feedbackSuggestionModule) { + {{ ('global.generic.yes' | artemisTranslate) + ' - ' + programmingExercise.feedbackSuggestionModule }} + } @else { + + }
@if (programmingExercise.dueDate) {
Automatic Submission Run After Due Date
diff --git a/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.html b/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.html index d05638ea2335..f64d835bae48 100644 --- a/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.html +++ b/src/main/webapp/app/exercises/programming/shared/lifecycle/programming-exercise-lifecycle.component.html @@ -190,6 +190,6 @@
@if (!isExamMode) { - + } diff --git a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html index 02e2adaf9995..7b9686c8e905 100644 --- a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html +++ b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html @@ -15,8 +15,8 @@ class="form-control-label" for="feedbackSuggestionsEnabledCheck" jhiTranslate="artemisApp.exercise.feedbackSuggestionsEnabled" - > - + /> + @if (!!this.exercise.feedbackSuggestionModule) {
diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.html b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.html index 74352a012adc..b109991a720b 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.html +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.html @@ -41,11 +41,11 @@

Exercise Details

Enable feedback suggestions from Athena
- {{ - !!textExercise.feedbackSuggestionModule - ? ('global.generic.yes' | artemisTranslate) + ' - ' + textExercise.feedbackSuggestionModule - : ('global.generic.no' | artemisTranslate) - }} + @if (!!textExercise.feedbackSuggestionModule) { + {{ ('global.generic.yes' | artemisTranslate) + ' - ' + textExercise.feedbackSuggestionModule }} + } @else { + + }
Example Solution
diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html index 96e36ae75d79..4c9666b122bd 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-update.component.html @@ -153,7 +153,7 @@

+ } From ea561ea2b415edbeea1815271b6e988c63634817 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Mon, 22 Jan 2024 13:40:58 +0100 Subject: [PATCH 22/31] Add empty tag to silence intellij warning --- .../exercise-feedback-suggestion-options.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html index 7b9686c8e905..cce547bdf2ca 100644 --- a/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html +++ b/src/main/webapp/app/exercises/shared/feedback-suggestion/exercise-feedback-suggestion-options.component.html @@ -15,7 +15,7 @@ class="form-control-label" for="feedbackSuggestionsEnabledCheck" jhiTranslate="artemisApp.exercise.feedbackSuggestionsEnabled" - /> + >

@if (!!this.exercise.feedbackSuggestionModule) { From b9ee338bfb63f0acb769a27b87c4d4b02d0e80dc Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Mon, 22 Jan 2024 16:48:19 +0100 Subject: [PATCH 23/31] Fix merge issues --- .../ProgrammingAssessmentService.java | 2 +- .../rest/ProgrammingAssessmentResource.java | 83 ++----------------- .../text-exercise-detail.component.ts | 2 +- 3 files changed, 7 insertions(+), 80 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingAssessmentService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingAssessmentService.java index 14d3bfa019bd..299eaf23ddf2 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingAssessmentService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingAssessmentService.java @@ -117,7 +117,7 @@ private Result submitManualAssessment(Result newManualResult, ProgrammingSubmiss * Send feedback to Athena (if enabled for both the Artemis instance and the exercise). */ private void sendFeedbackToAthena(final ProgrammingExercise exercise, final ProgrammingSubmission programmingSubmission, final List feedbacks) { - if (athenaFeedbackSendingService.isPresent() && exercise.getFeedbackSuggestionsEnabled()) { + if (athenaFeedbackSendingService.isPresent() && exercise.isFeedbackSuggestionsEnabled()) { athenaFeedbackSendingService.get().sendFeedback(exercise, programmingSubmission, feedbacks); } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingAssessmentResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingAssessmentResource.java index d284362dee77..eb7566163d8d 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingAssessmentResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/ProgrammingAssessmentResource.java @@ -1,9 +1,6 @@ package de.tum.in.www1.artemis.web.rest; -import java.time.ZonedDateTime; import java.util.Comparator; -import java.util.List; -import java.util.Optional; import org.hibernate.Hibernate; import org.slf4j.Logger; @@ -14,23 +11,16 @@ import de.tum.in.www1.artemis.domain.*; import de.tum.in.www1.artemis.domain.enumeration.FeedbackType; -import de.tum.in.www1.artemis.domain.participation.ProgrammingExerciseStudentParticipation; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; import de.tum.in.www1.artemis.repository.*; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastTutor; import de.tum.in.www1.artemis.service.AuthorizationCheckService; -import de.tum.in.www1.artemis.service.ExerciseDateService; -import de.tum.in.www1.artemis.service.connectors.athena.AthenaFeedbackSendingService; -import de.tum.in.www1.artemis.service.connectors.lti.LtiNewResultService; import de.tum.in.www1.artemis.service.exam.ExamService; -import de.tum.in.www1.artemis.service.notifications.SingleUserNotificationService; import de.tum.in.www1.artemis.service.programming.ProgrammingAssessmentService; -import de.tum.in.www1.artemis.service.programming.ProgrammingExerciseParticipationService; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; -import de.tum.in.www1.artemis.web.websocket.ResultWebsocketService; /** * REST controller for managing ProgrammingAssessment. @@ -39,35 +29,23 @@ @RequestMapping("/api") public class ProgrammingAssessmentResource extends AssessmentResource { - private final Logger log = LoggerFactory.getLogger(ProgrammingAssessmentResource.class); - private static final String ENTITY_NAME = "programmingAssessment"; + private static final Logger log = LoggerFactory.getLogger(ProgrammingAssessmentResource.class); + private final ProgrammingAssessmentService programmingAssessmentService; private final ProgrammingSubmissionRepository programmingSubmissionRepository; - private final Optional ltiNewResultService; - private final StudentParticipationRepository studentParticipationRepository; - private final ProgrammingExerciseParticipationService programmingExerciseParticipationService; - - private final Optional athenaFeedbackSendingService; - public ProgrammingAssessmentResource(AuthorizationCheckService authCheckService, UserRepository userRepository, ProgrammingAssessmentService programmingAssessmentService, ProgrammingSubmissionRepository programmingSubmissionRepository, ExerciseRepository exerciseRepository, ResultRepository resultRepository, ExamService examService, - ResultWebsocketService resultWebsocketService, Optional ltiNewResultService, StudentParticipationRepository studentParticipationRepository, - ExampleSubmissionRepository exampleSubmissionRepository, SubmissionRepository submissionRepository, SingleUserNotificationService singleUserNotificationService, - ProgrammingExerciseParticipationService programmingExerciseParticipationService, Optional athenaFeedbackSendingService) { - super(authCheckService, userRepository, exerciseRepository, programmingAssessmentService, resultRepository, examService, resultWebsocketService, - exampleSubmissionRepository, submissionRepository, singleUserNotificationService); + StudentParticipationRepository studentParticipationRepository, ExampleSubmissionRepository exampleSubmissionRepository, SubmissionRepository submissionRepository) { + super(authCheckService, userRepository, exerciseRepository, programmingAssessmentService, resultRepository, examService, exampleSubmissionRepository, submissionRepository); this.programmingAssessmentService = programmingAssessmentService; this.programmingSubmissionRepository = programmingSubmissionRepository; - this.ltiNewResultService = ltiNewResultService; this.studentParticipationRepository = studentParticipationRepository; - this.programmingExerciseParticipationService = programmingExerciseParticipationService; - this.athenaFeedbackSendingService = athenaFeedbackSendingService; } /** @@ -181,53 +159,11 @@ public ResponseEntity saveProgrammingAssessment(@PathVariable Long parti throw new BadRequestAlertException("In case feedback is present, a feedback must contain points.", ENTITY_NAME, "feedbackCreditsNull"); } - // TODO: move this logic into a service - - // make sure that the submission cannot be manipulated on the client side - var submission = (ProgrammingSubmission) existingManualResult.getSubmission(); - newManualResult.setSubmission(submission); - newManualResult.setHasComplaint(existingManualResult.getHasComplaint().isPresent() && existingManualResult.getHasComplaint().get()); - newManualResult = programmingAssessmentService.saveManualAssessment(newManualResult, user); - - if (submission.getParticipation() == null) { - newManualResult.setParticipation(submission.getParticipation()); - } - Result savedResult = resultRepository.save(newManualResult); - savedResult.setSubmission(submission); - - // Re-load result to fetch the test cases - newManualResult = resultRepository.findByIdWithEagerSubmissionAndFeedbackAndTestCasesElseThrow(newManualResult.getId()); - - if (submit) { - newManualResult = resultRepository.submitManualAssessment(newManualResult); - - if (submission.getParticipation() instanceof StudentParticipation studentParticipation && studentParticipation.getStudent().isPresent()) { - singleUserNotificationService.checkNotificationForAssessmentExerciseSubmission(programmingExercise, studentParticipation.getStudent().get(), newManualResult); - } - sendFeedbackToAthena(programmingExercise, submission, newManualResult.getFeedbacks()); - } + newManualResult = programmingAssessmentService.saveAndSubmitManualAssessment(participation, newManualResult, existingManualResult, user, submit); // remove information about the student for tutors to ensure double-blind assessment if (!isAtLeastInstructor) { newManualResult.getParticipation().filterSensitiveInformation(); } - // Note: we always need to report the result over LTI, otherwise it might never become visible in the external system - if (ltiNewResultService.isPresent()) { - ltiNewResultService.get().onNewResult((StudentParticipation) newManualResult.getParticipation()); - } - if (submit && ExerciseDateService.isAfterAssessmentDueDate(programmingExercise)) { - resultWebsocketService.broadcastNewResult(newManualResult.getParticipation(), newManualResult); - } - - var isManualFeedbackRequest = programmingExercise.getAllowManualFeedbackRequests() && participation.getIndividualDueDate() != null - && participation.getIndividualDueDate().isBefore(ZonedDateTime.now()); - var isBeforeDueDate = programmingExercise.getDueDate() != null && programmingExercise.getDueDate().isAfter(ZonedDateTime.now()); - if (isManualFeedbackRequest && isBeforeDueDate) { - participation.setIndividualDueDate(null); - studentParticipationRepository.save(participation); - newManualResult.setParticipation(participation); - - programmingExerciseParticipationService.unlockStudentRepositoryAndParticipation((ProgrammingExerciseStudentParticipation) participation); - } return ResponseEntity.ok(newManualResult); } @@ -246,15 +182,6 @@ public ResponseEntity deleteAssessment(@PathVariable Long participationId, return super.deleteAssessment(participationId, submissionId, resultId); } - /** - * Send feedback to Athena (if enabled for both the Artemis instance and the exercise). - */ - private void sendFeedbackToAthena(final ProgrammingExercise exercise, final ProgrammingSubmission programmingSubmission, final List feedbacks) { - if (athenaFeedbackSendingService.isPresent() && exercise.isFeedbackSuggestionsEnabled()) { - athenaFeedbackSendingService.get().sendFeedback(exercise, programmingSubmission, feedbacks); - } - } - @Override String getEntityName() { return ENTITY_NAME; diff --git a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.ts b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.ts index ff5b43589bae..d132b7d2a177 100644 --- a/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.ts +++ b/src/main/webapp/app/exercises/text/manage/text-exercise/text-exercise-detail.component.ts @@ -105,7 +105,7 @@ export class TextExerciseDetailComponent implements OnInit, OnDestroy { headline: 'artemisApp.exercise.sections.grading', details: [ ...defaultGradingDetails, - { type: DetailType.Boolean, title: 'artemisApp.exercise.feedbackSuggestionsEnabled', data: { boolean: exercise.feedbackSuggestionsEnabled } }, + { type: DetailType.Boolean, title: 'artemisApp.exercise.feedbackSuggestionsEnabled', data: { boolean: !!exercise.feedbackSuggestionModule } }, ...gradingInstructionsCriteriaDetails, ].filter(Boolean), }, From 8a4447885913dc386db691ca60cede2c6492f8a2 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Mon, 22 Jan 2024 17:13:46 +0100 Subject: [PATCH 24/31] Fix logger usage in AthenaModuleService --- .../artemis/service/connectors/athena/AthenaModuleService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java index b2ba48faa901..72dcd2cd58bf 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java @@ -38,7 +38,7 @@ public class AthenaModuleService { @Value("#{'${artemis.athena.restricted-modules:}'}") private List restrictedModules; - private final Logger log = LoggerFactory.getLogger(AthenaModuleService.class); + private static final Logger log = LoggerFactory.getLogger(AthenaModuleService.class); private final RestTemplate shortTimeoutRestTemplate; From 0da87ec51986a0dba7d754efefdfcec939517b71 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Wed, 24 Jan 2024 13:30:55 +0100 Subject: [PATCH 25/31] Show Athena status on prog exercise details page --- .../programming/manage/programming-exercise-detail.component.ts | 1 + 1 file changed, 1 insertion(+) 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 bce1c6cdbcb3..5f56e2b12972 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 @@ -509,6 +509,7 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { title: 'artemisApp.programmingExercise.timeline.releaseTestsWithExampleSolution', data: { boolean: exercise.releaseTestsWithExampleSolution }, }, + { type: DetailType.Boolean, title: 'artemisApp.exercise.feedbackSuggestionsEnabled', data: { boolean: !!exercise.feedbackSuggestionModule } }, { type: DetailType.Markdown, title: 'artemisApp.exercise.assessmentInstructions', data: { innerHtml: this.formattedGradingInstructions } }, exercise.gradingCriteria && { type: DetailType.GradingCriteria, From 57ba9db9c270abbc8ad1d9488c87d83c0996a777 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Tue, 13 Feb 2024 15:20:36 +0100 Subject: [PATCH 26/31] Implement review suggestions --- src/main/java/de/tum/in/www1/artemis/domain/Exercise.java | 2 +- .../de/tum/in/www1/artemis/service/SubmissionService.java | 2 +- .../connectors/athena/AthenaFeedbackSendingService.java | 2 +- .../connectors/athena/AthenaRepositoryExportService.java | 2 +- .../athena/AthenaSubmissionSelectionService.java | 2 +- .../connectors/athena/AthenaSubmissionSendingService.java | 2 +- .../service/programming/ProgrammingAssessmentService.java | 2 +- .../artemis/service/scheduled/AthenaScheduleService.java | 2 +- .../de/tum/in/www1/artemis/web/rest/AthenaResource.java | 8 ++++---- .../in/www1/artemis/web/rest/TextAssessmentResource.java | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java b/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java index 63509eb97971..336f97992fd7 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/Exercise.java @@ -790,7 +790,7 @@ public void setFeedbackSuggestionModule(String feedbackSuggestionModule) { this.feedbackSuggestionModule = feedbackSuggestionModule; } - public boolean isFeedbackSuggestionsEnabled() { + public boolean areFeedbackSuggestionsEnabled() { return feedbackSuggestionModule != null; } diff --git a/src/main/java/de/tum/in/www1/artemis/service/SubmissionService.java b/src/main/java/de/tum/in/www1/artemis/service/SubmissionService.java index e23c1e2b7256..92adfe16cdaf 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/SubmissionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/SubmissionService.java @@ -235,7 +235,7 @@ public Optional getNextAssessableSubmission(Exercise exercise, boole */ public Optional getAthenaSubmissionToAssess(Exercise exercise, boolean skipAssessmentQueue, boolean examMode, int correctionRound, Function> findSubmissionById) { - if (exercise.isFeedbackSuggestionsEnabled() && athenaSubmissionSelectionService.isPresent() && !skipAssessmentQueue && correctionRound == 0) { + if (exercise.areFeedbackSuggestionsEnabled() && athenaSubmissionSelectionService.isPresent() && !skipAssessmentQueue && correctionRound == 0) { var assessableSubmissions = getAssessableSubmissions(exercise, examMode, correctionRound); var athenaSubmissionId = athenaSubmissionSelectionService.get().getProposedSubmissionId(exercise, assessableSubmissions.stream().map(Submission::getId).toList()); if (athenaSubmissionId.isPresent()) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSendingService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSendingService.java index 48b179aa3bf0..570169360cf4 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSendingService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaFeedbackSendingService.java @@ -72,7 +72,7 @@ public void sendFeedback(Exercise exercise, Submission submission, List feedbacks, int maxRetries) { - if (!exercise.isFeedbackSuggestionsEnabled()) { + if (!exercise.areFeedbackSuggestionsEnabled()) { throw new IllegalArgumentException("The exercise does not have feedback suggestions enabled."); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportService.java index 0d515e899d70..ed88fa87d67a 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaRepositoryExportService.java @@ -63,7 +63,7 @@ public AthenaRepositoryExportService(ProgrammingExerciseRepository programmingEx * @throws AccessForbiddenException if the feedback suggestions are not enabled for the given exercise */ private void checkFeedbackSuggestionsEnabledElseThrow(Exercise exercise) { - if (!exercise.isFeedbackSuggestionsEnabled()) { + if (!exercise.areFeedbackSuggestionsEnabled()) { log.error("Feedback suggestions are not enabled for exercise {}", exercise.getId()); throw new ServiceUnavailableException("Feedback suggestions are not enabled for exercise"); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionService.java index 5bfb8bf68f2c..6afb8c05140b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionService.java @@ -64,7 +64,7 @@ public AthenaSubmissionSelectionService(@Qualifier("veryShortTimeoutAthenaRestTe * @throws IllegalArgumentException if exercise isn't automatically assessable */ public Optional getProposedSubmissionId(Exercise exercise, List submissionIds) { - if (!exercise.isFeedbackSuggestionsEnabled()) { + if (!exercise.areFeedbackSuggestionsEnabled()) { throw new IllegalArgumentException("The Exercise does not have feedback suggestions enabled."); } if (submissionIds.isEmpty()) { diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSendingService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSendingService.java index 4bc875341703..d1ed6fd2f4c7 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSendingService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSendingService.java @@ -74,7 +74,7 @@ public void sendSubmissions(Exercise exercise) { * @param maxRetries number of retries before the request will be canceled */ public void sendSubmissions(Exercise exercise, int maxRetries) { - if (!exercise.isFeedbackSuggestionsEnabled()) { + if (!exercise.areFeedbackSuggestionsEnabled()) { throw new IllegalArgumentException("The Exercise does not have feedback suggestions enabled."); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingAssessmentService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingAssessmentService.java index 299eaf23ddf2..702a3164ad01 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingAssessmentService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingAssessmentService.java @@ -117,7 +117,7 @@ private Result submitManualAssessment(Result newManualResult, ProgrammingSubmiss * Send feedback to Athena (if enabled for both the Artemis instance and the exercise). */ private void sendFeedbackToAthena(final ProgrammingExercise exercise, final ProgrammingSubmission programmingSubmission, final List feedbacks) { - if (athenaFeedbackSendingService.isPresent() && exercise.isFeedbackSuggestionsEnabled()) { + if (athenaFeedbackSendingService.isPresent() && exercise.areFeedbackSuggestionsEnabled()) { athenaFeedbackSendingService.get().sendFeedback(exercise, programmingSubmission, feedbacks); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/scheduled/AthenaScheduleService.java b/src/main/java/de/tum/in/www1/artemis/service/scheduled/AthenaScheduleService.java index 56a8e398d376..04f8fc086b62 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/scheduled/AthenaScheduleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/scheduled/AthenaScheduleService.java @@ -65,7 +65,7 @@ public void scheduleRunningExercisesOnStartup() { * @param exercise exercise to schedule Athena for */ public void scheduleExerciseForAthenaIfRequired(Exercise exercise) { - if (!exercise.isFeedbackSuggestionsEnabled()) { + if (!exercise.areFeedbackSuggestionsEnabled()) { cancelScheduledAthena(exercise.getId()); return; } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java index 07b307e3f6ef..017a05bdd908 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/AthenaResource.java @@ -97,7 +97,7 @@ private Re authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.TEACHING_ASSISTANT, exercise, null); // Check if feedback suggestions are actually enabled - if (!exercise.isFeedbackSuggestionsEnabled()) { + if (!exercise.areFeedbackSuggestionsEnabled()) { throw new InternalServerErrorException("Feedback suggestions are not enabled for this exercise"); } @@ -155,7 +155,7 @@ public ResponseEntity> getAvailableModulesForProgrammingExercises(@ authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, null); try { - var modules = athenaModuleService.getAthenaProgrammingModulesForCourse(course); + List modules = athenaModuleService.getAthenaProgrammingModulesForCourse(course); return ResponseEntity.ok(modules); } catch (NetworkingException e) { @@ -177,9 +177,9 @@ public ResponseEntity> getAvailableModulesForTextExercises(@PathVar log.debug("REST request to get available Athena modules for text exercises in Course {}", course.getTitle()); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, null); - // todo error handling + try { - var modules = athenaModuleService.getAthenaTextModulesForCourse(course); + List modules = athenaModuleService.getAthenaTextModulesForCourse(course); return ResponseEntity.ok(modules); } catch (NetworkingException e) { diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/TextAssessmentResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/TextAssessmentResource.java index 3a55f69fdc95..f9f4f69d3ef5 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/TextAssessmentResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/TextAssessmentResource.java @@ -483,7 +483,7 @@ private void saveTextBlocks(final Set textBlocks, final TextSubmissio * Send feedback to Athena (if enabled for both the Artemis instance and the exercise). */ private void sendFeedbackToAthena(final TextExercise exercise, final TextSubmission textSubmission, final List feedbacks) { - if (athenaFeedbackSendingService.isPresent() && exercise.isFeedbackSuggestionsEnabled()) { + if (athenaFeedbackSendingService.isPresent() && exercise.areFeedbackSuggestionsEnabled()) { athenaFeedbackSendingService.get().sendFeedback(exercise, textSubmission, feedbacks); } } From 4e9d6411d632e7364613495c58aa4a6b163ce678 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Tue, 13 Feb 2024 15:29:54 +0100 Subject: [PATCH 27/31] Fix failing test compilation introduced in merge conflict resolution --- .../athena/AthenaSubmissionSelectionServiceTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionServiceTest.java index eed270a003db..23dbb26f9af4 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaSubmissionSelectionServiceTest.java @@ -1,5 +1,7 @@ package de.tum.in.www1.artemis.service.connectors.athena; +import static de.tum.in.www1.artemis.connector.AthenaRequestMockProvider.ATHENA_MODULE_PROGRAMMING_TEST; +import static de.tum.in.www1.artemis.connector.AthenaRequestMockProvider.ATHENA_MODULE_TEXT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -56,14 +58,14 @@ void setUp() { athenaRequestMockProvider.enableMockingOfRequests(); textExercise = textExerciseUtilService.createSampleTextExercise(null); - textExercise.setFeedbackSuggestionsEnabled(true); + textExercise.setFeedbackSuggestionModule(ATHENA_MODULE_TEXT_TEST); textExercise.setGradingCriteria(Set.of(new GradingCriterion())); textExerciseRepository.save(textExercise); textSubmission1 = new TextSubmission(1L); textSubmission2 = new TextSubmission(2L); programmingExercise = programmingExerciseUtilService.createSampleProgrammingExercise(); - programmingExercise.setFeedbackSuggestionsEnabled(true); + programmingExercise.setFeedbackSuggestionModule(ATHENA_MODULE_PROGRAMMING_TEST); programmingExercise.setGradingCriteria(Set.of(new GradingCriterion())); programmingExerciseRepository.save(programmingExercise); programmingSubmission1 = new ProgrammingSubmission(); From a1fe637d414961b866eb2de6b49386e605c36255 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Tue, 13 Feb 2024 17:13:54 +0100 Subject: [PATCH 28/31] Fix client issue if profileInfo is not loaded yet --- .../webapp/app/course/manage/detail/course-detail.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/app/course/manage/detail/course-detail.component.ts b/src/main/webapp/app/course/manage/detail/course-detail.component.ts index 2afebc978fbc..8f1c0b6af29a 100644 --- a/src/main/webapp/app/course/manage/detail/course-detail.component.ts +++ b/src/main/webapp/app/course/manage/detail/course-detail.component.ts @@ -92,7 +92,7 @@ export class CourseDetailComponent implements OnInit, OnDestroy { this.tutorialEnabled = await firstValueFrom(this.featureToggleService.getFeatureToggleActive(FeatureToggle.TutorialGroups)); const profileInfo = await firstValueFrom(this.profileService.getProfileInfo()); this.ltiEnabled = profileInfo?.activeProfiles.includes(PROFILE_LTI); - this.isAthenaEnabled = profileInfo.activeProfiles.includes(PROFILE_ATHENA); + this.isAthenaEnabled = profileInfo?.activeProfiles.includes(PROFILE_ATHENA); this.irisEnabled = profileInfo?.activeProfiles.includes(PROFILE_IRIS); if (this.irisEnabled) { const irisSettings = await firstValueFrom(this.irisSettingsService.getGlobalSettings()); From 238d54c833151a83204cb180f7ada4a6d7f673ba Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Sat, 17 Feb 2024 14:42:15 +0100 Subject: [PATCH 29/31] revert prettier changes --- src/test/cypress/tsconfig.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/cypress/tsconfig.json b/src/test/cypress/tsconfig.json index d5b190b2fa10..65dece962d13 100644 --- a/src/test/cypress/tsconfig.json +++ b/src/test/cypress/tsconfig.json @@ -6,6 +6,6 @@ "ignoreDeprecations": "5.0", "target": "es2019", "sourceMap": false, - "types": ["cypress", "@4tw/cypress-drag-drop", "node", "cypress-wait-until", "cypress-file-upload"], - }, + "types": ["cypress", "@4tw/cypress-drag-drop", "node", "cypress-wait-until", "cypress-file-upload"] + } } From f03b7f7efb6a1ae35792d9883c6b8ab6ae8e51f2 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Fri, 23 Feb 2024 11:23:52 +0100 Subject: [PATCH 30/31] Fix wrong variable name in getAthenaTextModulesForCourse --- .../service/connectors/athena/AthenaModuleService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java index 72dcd2cd58bf..29a0f152de7e 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/athena/AthenaModuleService.java @@ -101,12 +101,12 @@ public List getAthenaProgrammingModulesForCourse(Course course) throws N * @throws NetworkingException is thrown in case the modules can't be fetched from Athena */ public List getAthenaTextModulesForCourse(Course course) throws NetworkingException { - List availableProgrammingModules = getAthenaModules().stream().filter(module -> "text".equals(module.type)).map(module -> module.name).toList(); + List availableTextModules = getAthenaModules().stream().filter(module -> "text".equals(module.type)).map(module -> module.name).toList(); if (!course.getRestrictedAthenaModulesAccess()) { // filter out restricted modules - availableProgrammingModules = availableProgrammingModules.stream().filter(moduleName -> !restrictedModules.contains(moduleName)).toList(); + availableTextModules = availableTextModules.stream().filter(moduleName -> !restrictedModules.contains(moduleName)).toList(); } - return availableProgrammingModules; + return availableTextModules; } /** From 9b847bd869bc55d21f6797d043c7d02723c50962 Mon Sep 17 00:00:00 2001 From: Maximilian Soelch Date: Sat, 2 Mar 2024 20:19:15 +0100 Subject: [PATCH 31/31] Remove old client test case getSemesters() introduced while resolving merge conflict --- .../component/course/course-update.component.spec.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/test/javascript/spec/component/course/course-update.component.spec.ts b/src/test/javascript/spec/component/course/course-update.component.spec.ts index 8efcd67bda76..1e7db7e62905 100644 --- a/src/test/javascript/spec/component/course/course-update.component.spec.ts +++ b/src/test/javascript/spec/component/course/course-update.component.spec.ts @@ -499,18 +499,6 @@ describe('Course Management Update Component', () => { }); }); - describe('getSemesters', () => { - it('should get semesters around current year', () => { - const years = dayjs().year() - 2018 + 1; - const semesters = comp.getSemesters(); - expect(semesters.last()).toBe(''); - for (let i = 0; i <= years; i++) { - expect(semesters[2 * i]).toBe('WS' + (18 + years - i) + '/' + (19 + years - i)); - expect(semesters[2 * i + 1]).toBe('SS' + (18 + years - i)); - } - }); - }); - describe('isValidDate', () => { it('should handle valid dates', () => { comp.course = new Course();