Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Iris: Allow to configure exercises based on their categories #9461

Merged
merged 4 commits into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
Expand All @@ -25,6 +26,7 @@
import de.tum.cit.aet.artemis.exam.repository.StudentExamRepository;
import de.tum.cit.aet.artemis.exercise.domain.Exercise;
import de.tum.cit.aet.artemis.exercise.repository.ExerciseRepository;
import de.tum.cit.aet.artemis.iris.service.settings.IrisSettingsService;
import de.tum.cit.aet.artemis.lecture.domain.ExerciseUnit;
import de.tum.cit.aet.artemis.lecture.repository.ExerciseUnitRepository;
import de.tum.cit.aet.artemis.lecture.service.LectureUnitService;
Expand Down Expand Up @@ -78,11 +80,14 @@ public class ExerciseDeletionService {

private final CompetencyProgressService competencyProgressService;

private final Optional<IrisSettingsService> irisSettingsService;

public ExerciseDeletionService(ExerciseRepository exerciseRepository, ExerciseUnitRepository exerciseUnitRepository, ParticipationService participationService,
ProgrammingExerciseService programmingExerciseService, ModelingExerciseService modelingExerciseService, QuizExerciseService quizExerciseService,
TutorParticipationRepository tutorParticipationRepository, ExampleSubmissionService exampleSubmissionService, StudentExamRepository studentExamRepository,
LectureUnitService lectureUnitService, PlagiarismResultRepository plagiarismResultRepository, TextExerciseService textExerciseService,
ChannelRepository channelRepository, ChannelService channelService, CompetencyProgressService competencyProgressService) {
ChannelRepository channelRepository, ChannelService channelService, CompetencyProgressService competencyProgressService,
Optional<IrisSettingsService> irisSettingsService) {
this.exerciseRepository = exerciseRepository;
this.participationService = participationService;
this.programmingExerciseService = programmingExerciseService;
Expand All @@ -98,6 +103,7 @@ public ExerciseDeletionService(ExerciseRepository exerciseRepository, ExerciseUn
this.channelRepository = channelRepository;
this.channelService = channelService;
this.competencyProgressService = competencyProgressService;
this.irisSettingsService = irisSettingsService;
}

/**
Expand Down Expand Up @@ -169,6 +175,10 @@ public void delete(long exerciseId, boolean deleteStudentReposBuildPlans, boolea
lectureUnitService.removeLectureUnit(exerciseUnit);
}

if (irisSettingsService.isPresent()) {
irisSettingsService.get().deleteSettingsFor(exercise);
}

Hialus marked this conversation as resolved.
Show resolved Hide resolved
// delete all plagiarism results belonging to this exercise
plagiarismResultRepository.deletePlagiarismResultsByExerciseId(exerciseId);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package de.tum.cit.aet.artemis.iris.domain.settings;

import java.util.SortedSet;
import java.util.TreeSet;

import jakarta.annotation.Nullable;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;

Expand All @@ -24,6 +28,10 @@ public class IrisChatSubSettings extends IrisSubSettings {
@Column(name = "rate_limit_timeframe_hours")
private Integer rateLimitTimeframeHours;

@Column(name = "enabled_for_categories")
@Convert(converter = IrisListConverter.class)
private SortedSet<String> enabledForCategories = new TreeSet<>();

@Nullable
public Integer getRateLimit() {
return rateLimit;
Expand All @@ -41,4 +49,12 @@ public Integer getRateLimitTimeframeHours() {
public void setRateLimitTimeframeHours(@Nullable Integer rateLimitTimeframeHours) {
this.rateLimitTimeframeHours = rateLimitTimeframeHours;
}

public SortedSet<String> getEnabledForCategories() {
return enabledForCategories;
}
coderabbitai[bot] marked this conversation as resolved.
Show resolved Hide resolved

public void setEnabledForCategories(SortedSet<String> enabledForCategories) {
this.enabledForCategories = enabledForCategories;
}
Hialus marked this conversation as resolved.
Show resolved Hide resolved
coderabbitai[bot] marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package de.tum.cit.aet.artemis.iris.domain.settings;

import java.util.SortedSet;
import java.util.TreeSet;
krusche marked this conversation as resolved.
Show resolved Hide resolved

import jakarta.annotation.Nullable;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;

Expand All @@ -23,6 +27,11 @@ public class IrisTextExerciseChatSubSettings extends IrisSubSettings {
@Column(name = "rate_limit_timeframe_hours")
private Integer rateLimitTimeframeHours;

@Nullable
@Column(name = "enabled_for_categories")
@Convert(converter = IrisListConverter.class)
private SortedSet<String> enabledForCategories = new TreeSet<>();
krusche marked this conversation as resolved.
Show resolved Hide resolved

@Nullable
public Integer getRateLimit() {
return rateLimit;
Expand All @@ -41,4 +50,12 @@ public void setRateLimitTimeframeHours(@Nullable Integer rateLimitTimeframeHours
this.rateLimitTimeframeHours = rateLimitTimeframeHours;
}

@Nullable
public SortedSet<String> getEnabledForCategories() {
return enabledForCategories;
}
krusche marked this conversation as resolved.
Show resolved Hide resolved

public void setEnabledForCategories(@Nullable SortedSet<String> enabledForCategories) {
this.enabledForCategories = enabledForCategories;
}
krusche marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package de.tum.cit.aet.artemis.iris.dto;

import java.util.Set;
import java.util.SortedSet;

import jakarta.annotation.Nullable;

import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record IrisCombinedChatSubSettingsDTO(boolean enabled, Integer rateLimit, Integer rateLimitTimeframeHours, @Nullable Set<String> allowedVariants,
@Nullable String selectedVariant) {
public record IrisCombinedChatSubSettingsDTO(boolean enabled, Integer rateLimit, Integer rateLimitTimeframeHours, @Nullable SortedSet<String> allowedVariants,
@Nullable String selectedVariant, @Nullable SortedSet<String> enabledForCategories) {

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package de.tum.cit.aet.artemis.iris.dto;

import java.util.Set;
import java.util.SortedSet;

import jakarta.annotation.Nullable;

import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record IrisCombinedTextExerciseChatSubSettingsDTO(boolean enabled, Integer rateLimit, Integer rateLimitTimeframeHours, @Nullable Set<String> allowedVariants,
@Nullable String selectedVariant) {
public record IrisCombinedTextExerciseChatSubSettingsDTO(boolean enabled, Integer rateLimit, Integer rateLimitTimeframeHours, @Nullable SortedSet<String> allowedVariants,
@Nullable String selectedVariant, @Nullable SortedSet<String> enabledForCategories) {
coderabbitai[bot] marked this conversation as resolved.
Show resolved Hide resolved

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.Supplier;

Expand All @@ -18,6 +20,9 @@
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.core.exception.AccessForbiddenAlertException;
Expand All @@ -37,6 +42,10 @@
import de.tum.cit.aet.artemis.iris.domain.settings.IrisTextExerciseChatSubSettings;
import de.tum.cit.aet.artemis.iris.dto.IrisCombinedSettingsDTO;
import de.tum.cit.aet.artemis.iris.repository.IrisSettingsRepository;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise;
import de.tum.cit.aet.artemis.programming.repository.ProgrammingExerciseRepository;
import de.tum.cit.aet.artemis.text.domain.TextExercise;
import de.tum.cit.aet.artemis.text.repository.TextExerciseRepository;

/**
* Service for managing {@link IrisSettings}.
Expand All @@ -55,10 +64,20 @@ public class IrisSettingsService {

private final AuthorizationCheckService authCheckService;

public IrisSettingsService(IrisSettingsRepository irisSettingsRepository, IrisSubSettingsService irisSubSettingsService, AuthorizationCheckService authCheckService) {
private final ProgrammingExerciseRepository programmingExerciseRepository;

private final ObjectMapper objectMapper;

private final TextExerciseRepository textExerciseRepository;

public IrisSettingsService(IrisSettingsRepository irisSettingsRepository, IrisSubSettingsService irisSubSettingsService, AuthorizationCheckService authCheckService,
ProgrammingExerciseRepository programmingExerciseRepository, ObjectMapper objectMapper, TextExerciseRepository textExerciseRepository) {
this.irisSettingsRepository = irisSettingsRepository;
this.irisSubSettingsService = irisSubSettingsService;
this.authCheckService = authCheckService;
this.programmingExerciseRepository = programmingExerciseRepository;
this.objectMapper = objectMapper;
this.textExerciseRepository = textExerciseRepository;
coderabbitai[bot] marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down Expand Up @@ -248,6 +267,11 @@ private IrisGlobalSettings updateGlobalSettings(IrisGlobalSettings existingSetti
* @return The updated course Iris settings
*/
private IrisCourseSettings updateCourseSettings(IrisCourseSettings existingSettings, IrisCourseSettings settingsUpdate) {
var oldEnabledForCategoriesExerciseChat = existingSettings.getIrisChatSettings() == null ? new TreeSet<String>()
: existingSettings.getIrisChatSettings().getEnabledForCategories();
var oldEnabledForCategoriesTextExerciseChat = existingSettings.getIrisTextExerciseChatSettings() == null ? new TreeSet<String>()
: existingSettings.getIrisTextExerciseChatSettings().getEnabledForCategories();

coderabbitai[bot] marked this conversation as resolved.
Show resolved Hide resolved
var parentSettings = getCombinedIrisGlobalSettings();
// @formatter:off
existingSettings.setIrisChatSettings(irisSubSettingsService.update(
Expand Down Expand Up @@ -276,9 +300,125 @@ private IrisCourseSettings updateCourseSettings(IrisCourseSettings existingSetti
));
// @formatter:on

// Automatically update the exercise settings when the enabledForCategories is changed
var newEnabledForCategoriesExerciseChat = existingSettings.getIrisChatSettings() == null ? new TreeSet<String>()
: existingSettings.getIrisChatSettings().getEnabledForCategories();
if (!oldEnabledForCategoriesExerciseChat.equals(newEnabledForCategoriesExerciseChat)) {
programmingExerciseRepository.findAllWithCategoriesByCourseId(existingSettings.getCourse().getId())
.forEach(exercise -> setEnabledForExerciseByCategories(exercise, oldEnabledForCategoriesExerciseChat, newEnabledForCategoriesExerciseChat));
Hialus marked this conversation as resolved.
Show resolved Hide resolved
}

var newEnabledForCategoriesTextExerciseChat = existingSettings.getIrisTextExerciseChatSettings() == null ? new TreeSet<String>()
: existingSettings.getIrisTextExerciseChatSettings().getEnabledForCategories();
if (!Objects.equals(oldEnabledForCategoriesTextExerciseChat, newEnabledForCategoriesTextExerciseChat)) {
textExerciseRepository.findAllWithCategoriesByCourseId(existingSettings.getCourse().getId())
.forEach(exercise -> setEnabledForExerciseByCategories(exercise, oldEnabledForCategoriesTextExerciseChat, newEnabledForCategoriesTextExerciseChat));
}

return irisSettingsRepository.save(existingSettings);
}

/**
* Set the enabled status for an exercise based on it's categories.
coderabbitai[bot] marked this conversation as resolved.
Show resolved Hide resolved
* Compares the old and new enabled categories, reads the exercise categories,
* and updates the Iris chat settings accordingly if the new enabled categories match any of the exercise categories.
* This method is used when the enabled categories of the course settings are updated.
*
* @param exercise The exercise to update the enabled status for
* @param oldEnabledForCategories The old enabled categories
* @param newEnabledForCategories The new enabled categories
*/
public void setEnabledForExerciseByCategories(Exercise exercise, SortedSet<String> oldEnabledForCategories, SortedSet<String> newEnabledForCategories) {
var removedCategories = new TreeSet<>(oldEnabledForCategories);
removedCategories.removeAll(newEnabledForCategories);
var categories = getCategoryNames(exercise.getCategories());

if (categories.stream().anyMatch(newEnabledForCategories::contains)) {
setExerciseSettingsEnabled(exercise, true);
}
else if (categories.stream().anyMatch(removedCategories::contains)) {
setExerciseSettingsEnabled(exercise, false);
}
}

/**
* Set the enabled status for an exercise based on its categories.
coderabbitai[bot] marked this conversation as resolved.
Show resolved Hide resolved
* Reads the exercise categories and updates the Iris chat settings accordingly if the enabled categories match any of the exercise categories.
* This method is used when the categories of an exercise are updated.
*
* @param exercise The exercise to update the enabled status for
* @param oldExerciseCategories The old exercise categories
*/
public void setEnabledForExerciseByCategories(Exercise exercise, Set<String> oldExerciseCategories) {
var oldCategories = getCategoryNames(oldExerciseCategories);
var newCategories = getCategoryNames(exercise.getCategories());
if (oldCategories.isEmpty() && newCategories.isEmpty()) {
return;
}

var course = exercise.getCourseViaExerciseGroupOrCourseMember();
var courseSettings = getRawIrisSettingsFor(course);

Set<String> enabledForCategories;
if (exercise instanceof ProgrammingExercise) {
enabledForCategories = courseSettings.getIrisChatSettings().getEnabledForCategories();
}
else if (exercise instanceof TextExercise) {
enabledForCategories = courseSettings.getIrisTextExerciseChatSettings().getEnabledForCategories();
}
else {
return;
}
if (enabledForCategories == null) {
return;
}

if (newCategories.stream().anyMatch(enabledForCategories::contains)) {
setExerciseSettingsEnabled(exercise, true);
}
else if (oldCategories.stream().anyMatch(enabledForCategories::contains)) {
setExerciseSettingsEnabled(exercise, false);
}
}

/**
* Helper method to set the enabled status for an exercise's Iris settings.
* Currently able to handle {@link ProgrammingExercise} and {@link TextExercise} settings.
*
* @param exercise The exercise to update the enabled status for
* @param enabled Whether the Iris settings should be enabled
*/
private void setExerciseSettingsEnabled(Exercise exercise, boolean enabled) {
var exerciseSettings = getRawIrisSettingsFor(exercise);
if (exercise instanceof ProgrammingExercise) {
exerciseSettings.getIrisChatSettings().setEnabled(enabled);
}
else if (exercise instanceof TextExercise) {
exerciseSettings.getIrisTextExerciseChatSettings().setEnabled(enabled);
}
irisSettingsRepository.save(exerciseSettings);
}

/**
* Convert the category JSON strings of an exercise to a set of category names.
*
* @param exerciseCategories The set of category JSON strings
* @return The set of category names
*/
private Set<String> getCategoryNames(Set<String> exerciseCategories) {
var categories = new HashSet<String>();
for (var categoryJson : exerciseCategories) {
try {
var category = objectMapper.readTree(categoryJson);
categories.add(category.get("category").asText());
}
catch (JsonProcessingException e) {
return new HashSet<>();
}
}
return categories;
}
coderabbitai[bot] marked this conversation as resolved.
Show resolved Hide resolved

/**
* Helper method to update exercise Iris settings.
*
Expand Down
Loading
Loading