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 be230de89c1b..ce8e9c22bdec 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 @@ -26,7 +26,6 @@ import de.tum.in.www1.artemis.domain.enumeration.Language; import de.tum.in.www1.artemis.domain.enumeration.ProgrammingLanguage; import de.tum.in.www1.artemis.domain.exam.Exam; -import de.tum.in.www1.artemis.domain.iris.settings.IrisSettings; import de.tum.in.www1.artemis.domain.metis.Post; import de.tum.in.www1.artemis.domain.tutorialgroups.TutorialGroup; import de.tum.in.www1.artemis.domain.tutorialgroups.TutorialGroupsConfiguration; @@ -255,10 +254,6 @@ public class Course extends DomainObject { @JsonIgnoreProperties("course") private TutorialGroupsConfiguration tutorialGroupsConfiguration; - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) - @JoinColumn(name = "iris_settings_id") - private IrisSettings irisSettings; - // NOTE: Helpers variable names must be different from Getter name, so that Jackson ignores the @Transient annotation, but Hibernate still respects it @Transient private Long numberOfInstructorsTransient; @@ -1030,12 +1025,4 @@ public String getCourseInformationSharingMessagingCodeOfConduct() { public void setCourseInformationSharingMessagingCodeOfConduct(String courseInformationSharingMessagingCodeOfConduct) { this.courseInformationSharingMessagingCodeOfConduct = courseInformationSharingMessagingCodeOfConduct; } - - public IrisSettings getIrisSettings() { - return irisSettings; - } - - public void setIrisSettings(IrisSettings irisSettings) { - this.irisSettings = irisSettings; - } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java b/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java index 7ea3081a6399..9d206159de6e 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/ProgrammingExercise.java @@ -24,7 +24,6 @@ import de.tum.in.www1.artemis.domain.enumeration.*; import de.tum.in.www1.artemis.domain.hestia.ExerciseHint; import de.tum.in.www1.artemis.domain.hestia.ProgrammingExerciseTask; -import de.tum.in.www1.artemis.domain.iris.settings.IrisSettings; import de.tum.in.www1.artemis.domain.participation.Participation; import de.tum.in.www1.artemis.domain.participation.SolutionProgrammingExerciseParticipation; import de.tum.in.www1.artemis.domain.participation.StudentParticipation; @@ -147,10 +146,6 @@ public String getType() { @Column(name = "release_tests_with_example_solution", table = "programming_exercise_details") private boolean releaseTestsWithExampleSolution; - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) - @JoinColumn(name = "iris_settings_id", table = "programming_exercise_details") - private IrisSettings irisSettings; - @Column(name = "build_plan_configuration", table = "programming_exercise_details", columnDefinition = "longtext") private String buildPlanConfiguration; @@ -862,10 +857,6 @@ public void generateAndSetBuildPlanAccessSecret() { buildPlanAccessSecret = UUID.randomUUID().toString(); } - public IrisSettings getIrisSettings() { - return irisSettings; - } - /** * {@inheritDoc} */ @@ -876,10 +867,6 @@ public void disconnectRelatedEntities() { super.disconnectRelatedEntities(); } - public void setIrisSettings(IrisSettings irisSettings) { - this.irisSettings = irisSettings; - } - /** * Returns the JSON encoded custom build plan configuration * diff --git a/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisChatSubSettings.java b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisChatSubSettings.java new file mode 100644 index 000000000000..777e65cad7ce --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisChatSubSettings.java @@ -0,0 +1,58 @@ +package de.tum.in.www1.artemis.domain.iris.settings; + +import javax.annotation.Nullable; +import javax.persistence.*; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.artemis.domain.iris.IrisTemplate; + +/** + * An {@link IrisSubSettings} implementation for chat settings. + * Chat settings notably provide settings for the rate limit. + * Chat settings provide a single {@link IrisTemplate} for the chat messages. + */ +@Entity +@DiscriminatorValue("CHAT") +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class IrisChatSubSettings extends IrisSubSettings { + + @Nullable + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + private IrisTemplate template; + + @Nullable + @Column(name = "rate_limit") + private Integer rateLimit; + + @Nullable + @Column(name = "rate_limit_timeframe_hours") + private Integer rateLimitTimeframeHours; + + @Nullable + public IrisTemplate getTemplate() { + return template; + } + + public void setTemplate(@Nullable IrisTemplate template) { + this.template = template; + } + + @Nullable + public Integer getRateLimit() { + return rateLimit; + } + + public void setRateLimit(@Nullable Integer rateLimit) { + this.rateLimit = rateLimit; + } + + @Nullable + public Integer getRateLimitTimeframeHours() { + return rateLimitTimeframeHours; + } + + public void setRateLimitTimeframeHours(@Nullable Integer rateLimitTimeframeHours) { + this.rateLimitTimeframeHours = rateLimitTimeframeHours; + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisCodeEditorSubSettings.java b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisCodeEditorSubSettings.java new file mode 100644 index 000000000000..fe17cb0e8537 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisCodeEditorSubSettings.java @@ -0,0 +1,89 @@ +package de.tum.in.www1.artemis.domain.iris.settings; + +import javax.annotation.Nullable; +import javax.persistence.*; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.artemis.domain.iris.IrisTemplate; + +/** + * An {@link IrisSubSettings} implementation for code editor settings. + * Code editor settings notably provide multiple {@link IrisTemplate}s for the different steps in the code generation. + */ +@Entity +@DiscriminatorValue("CODE_EDITOR") +@SecondaryTable(name = "iris_code_editor_sub_settings") +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class IrisCodeEditorSubSettings extends IrisSubSettings { + + @Nullable + @JoinColumn(table = "iris_code_editor_sub_settings") + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + private IrisTemplate chatTemplate; + + @Nullable + @JoinColumn(table = "iris_code_editor_sub_settings") + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + private IrisTemplate problemStatementGenerationTemplate; + + @Nullable + @JoinColumn(table = "iris_code_editor_sub_settings") + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + private IrisTemplate templateRepoGenerationTemplate; + + @Nullable + @JoinColumn(table = "iris_code_editor_sub_settings") + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + private IrisTemplate solutionRepoGenerationTemplate; + + @Nullable + @JoinColumn(table = "iris_code_editor_sub_settings") + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + private IrisTemplate testRepoGenerationTemplate; + + @Nullable + public IrisTemplate getChatTemplate() { + return chatTemplate; + } + + public void setChatTemplate(@Nullable IrisTemplate chatTemplate) { + this.chatTemplate = chatTemplate; + } + + @Nullable + public IrisTemplate getProblemStatementGenerationTemplate() { + return problemStatementGenerationTemplate; + } + + public void setProblemStatementGenerationTemplate(@Nullable IrisTemplate problemStatementGenerationTemplate) { + this.problemStatementGenerationTemplate = problemStatementGenerationTemplate; + } + + @Nullable + public IrisTemplate getTemplateRepoGenerationTemplate() { + return templateRepoGenerationTemplate; + } + + public void setTemplateRepoGenerationTemplate(@Nullable IrisTemplate templateRepoGenerationTemplate) { + this.templateRepoGenerationTemplate = templateRepoGenerationTemplate; + } + + @Nullable + public IrisTemplate getSolutionRepoGenerationTemplate() { + return solutionRepoGenerationTemplate; + } + + public void setSolutionRepoGenerationTemplate(@Nullable IrisTemplate solutionRepoGenerationTemplate) { + this.solutionRepoGenerationTemplate = solutionRepoGenerationTemplate; + } + + @Nullable + public IrisTemplate getTestRepoGenerationTemplate() { + return testRepoGenerationTemplate; + } + + public void setTestRepoGenerationTemplate(@Nullable IrisTemplate testRepoGenerationTemplate) { + this.testRepoGenerationTemplate = testRepoGenerationTemplate; + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisCourseSettings.java b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisCourseSettings.java new file mode 100644 index 000000000000..4266bece8c29 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisCourseSettings.java @@ -0,0 +1,72 @@ +package de.tum.in.www1.artemis.domain.iris.settings; + +import javax.persistence.*; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.artemis.domain.Course; + +/** + * An {@link IrisSettings} implementation for course specific settings. + * Course settings are used to override global settings and allows all sub setting types. + */ +@Entity +@DiscriminatorValue("COURSE") +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class IrisCourseSettings extends IrisSettings { + + @OneToOne(optional = false) + @JoinColumn(name = "course_id", unique = true, nullable = false) + private Course course; + + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + @JoinColumn(name = "iris_chat_settings_id") + private IrisChatSubSettings irisChatSettings; + + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + @JoinColumn(name = "iris_hestia_settings_id") + private IrisHestiaSubSettings irisHestiaSettings; + + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + @JoinColumn(name = "iris_code_editor_settings_id") + private IrisCodeEditorSubSettings irisCodeEditorSettings; + + @Override + public boolean isValid() { + return course != null; + } + + public Course getCourse() { + return course; + } + + public void setCourse(Course course) { + this.course = course; + } + + public IrisChatSubSettings getIrisChatSettings() { + return irisChatSettings; + } + + public void setIrisChatSettings(IrisChatSubSettings irisChatSettings) { + this.irisChatSettings = irisChatSettings; + } + + public IrisHestiaSubSettings getIrisHestiaSettings() { + return irisHestiaSettings; + } + + public void setIrisHestiaSettings(IrisHestiaSubSettings irisHestiaSettings) { + this.irisHestiaSettings = irisHestiaSettings; + } + + @Override + public IrisCodeEditorSubSettings getIrisCodeEditorSettings() { + return irisCodeEditorSettings; + } + + @Override + public void setIrisCodeEditorSettings(IrisCodeEditorSubSettings irisCodeEditorSettings) { + this.irisCodeEditorSettings = irisCodeEditorSettings; + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisExerciseSettings.java b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisExerciseSettings.java new file mode 100644 index 000000000000..b5d0a0fb218d --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisExerciseSettings.java @@ -0,0 +1,66 @@ +package de.tum.in.www1.artemis.domain.iris.settings; + +import javax.persistence.*; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.artemis.domain.Exercise; + +/** + * An {@link IrisSettings} implementation for exercise specific settings. + * Exercise settings are used to override course settings and currently only allow setting the {@link IrisChatSubSettings}. + */ +@Entity +@DiscriminatorValue("EXERCISE") +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class IrisExerciseSettings extends IrisSettings { + + @OneToOne(optional = false) + @JoinColumn(name = "exercise_id", unique = true, nullable = false) + private Exercise exercise; + + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + @JoinColumn(name = "iris_chat_settings_id") + private IrisChatSubSettings irisChatSettings; + + @Override + public boolean isValid() { + return exercise != null; + } + + public Exercise getExercise() { + return exercise; + } + + public void setExercise(Exercise exercise) { + this.exercise = exercise; + } + + public IrisChatSubSettings getIrisChatSettings() { + return irisChatSettings; + } + + public void setIrisChatSettings(IrisChatSubSettings irisChatSettings) { + this.irisChatSettings = irisChatSettings; + } + + @Override + public IrisHestiaSubSettings getIrisHestiaSettings() { + return null; + } + + @Override + public void setIrisHestiaSettings(IrisHestiaSubSettings irisHestiaSettings) { + + } + + @Override + public IrisCodeEditorSubSettings getIrisCodeEditorSettings() { + return null; + } + + @Override + public void setIrisCodeEditorSettings(IrisCodeEditorSubSettings irisCodeEditorSettings) { + + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisGlobalSettings.java b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisGlobalSettings.java new file mode 100644 index 000000000000..66995a7410b9 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisGlobalSettings.java @@ -0,0 +1,109 @@ +package de.tum.in.www1.artemis.domain.iris.settings; + +import javax.persistence.*; + +import org.hibernate.Hibernate; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * An {@link IrisSettings} implementation for global settings. + * Global settings provide default values for all of Artemis for all sub setting types. + * It also includes functionality to automatically update the sub settings in the future. + */ +@Entity +@DiscriminatorValue("GLOBAL") +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class IrisGlobalSettings extends IrisSettings { + + @Column(name = "current_version") + private int currentVersion; + + @Column(name = "enable_auto_update_chat") + private boolean enableAutoUpdateChat; + + @Column(name = "enable_auto_update_hestia") + private boolean enableAutoUpdateHestia; + + @Column(name = "enable_auto_update_code_editor") + private boolean enableAutoUpdateCodeEditor; + + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, optional = false) + @JoinColumn(name = "iris_chat_settings_id") + private IrisChatSubSettings irisChatSettings; + + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, optional = false) + @JoinColumn(name = "iris_hestia_settings_id") + private IrisHestiaSubSettings irisHestiaSettings; + + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, optional = false) + @JoinColumn(name = "iris_code_editor_settings_id") + private IrisCodeEditorSubSettings irisCodeEditorSettings; + + @Override + public boolean isValid() { + var chatSettingsValid = !Hibernate.isInitialized(irisChatSettings) || irisChatSettings == null + || (irisChatSettings.getTemplate() != null && irisChatSettings.getTemplate().getContent() != null && !irisChatSettings.getTemplate().getContent().isEmpty()); + var hestiaSettingsValid = !Hibernate.isInitialized(irisHestiaSettings) || irisHestiaSettings == null + || (irisHestiaSettings.getTemplate() != null && irisHestiaSettings.getTemplate().getContent() != null && !irisHestiaSettings.getTemplate().getContent().isEmpty()); + return chatSettingsValid && hestiaSettingsValid; + } + + public int getCurrentVersion() { + return currentVersion; + } + + public void setCurrentVersion(int currentVersion) { + this.currentVersion = currentVersion; + } + + public boolean isEnableAutoUpdateChat() { + return enableAutoUpdateChat; + } + + public void setEnableAutoUpdateChat(boolean enableAutoUpdateChat) { + this.enableAutoUpdateChat = enableAutoUpdateChat; + } + + public boolean isEnableAutoUpdateHestia() { + return enableAutoUpdateHestia; + } + + public void setEnableAutoUpdateHestia(boolean enableAutoUpdateHestia) { + this.enableAutoUpdateHestia = enableAutoUpdateHestia; + } + + public boolean isEnableAutoUpdateCodeEditor() { + return enableAutoUpdateCodeEditor; + } + + public void setEnableAutoUpdateCodeEditor(boolean enableAutoUpdateCodeEditor) { + this.enableAutoUpdateCodeEditor = enableAutoUpdateCodeEditor; + } + + public IrisChatSubSettings getIrisChatSettings() { + return irisChatSettings; + } + + public void setIrisChatSettings(IrisChatSubSettings irisChatSettings) { + this.irisChatSettings = irisChatSettings; + } + + public IrisHestiaSubSettings getIrisHestiaSettings() { + return irisHestiaSettings; + } + + public void setIrisHestiaSettings(IrisHestiaSubSettings irisHestiaSettings) { + this.irisHestiaSettings = irisHestiaSettings; + } + + @Override + public IrisCodeEditorSubSettings getIrisCodeEditorSettings() { + return irisCodeEditorSettings; + } + + @Override + public void setIrisCodeEditorSettings(IrisCodeEditorSubSettings irisCodeEditorSettings) { + this.irisCodeEditorSettings = irisCodeEditorSettings; + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisHestiaSubSettings.java b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisHestiaSubSettings.java new file mode 100644 index 000000000000..4657d3796c79 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisHestiaSubSettings.java @@ -0,0 +1,31 @@ +package de.tum.in.www1.artemis.domain.iris.settings; + +import javax.annotation.Nullable; +import javax.persistence.*; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import de.tum.in.www1.artemis.domain.iris.IrisTemplate; + +/** + * An {@link IrisSubSettings} implementation for the Hestia integration settings. + * Hestia settings provide a single {@link IrisTemplate} for the hestia code hint generation requests. + */ +@Entity +@DiscriminatorValue("HESTIA") +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class IrisHestiaSubSettings extends IrisSubSettings { + + @Nullable + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + private IrisTemplate template; + + @Nullable + public IrisTemplate getTemplate() { + return template; + } + + public void setTemplate(@Nullable IrisTemplate template) { + this.template = template; + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisModelListConverter.java b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisModelListConverter.java new file mode 100644 index 000000000000..1bf7e6242cd6 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisModelListConverter.java @@ -0,0 +1,30 @@ +package de.tum.in.www1.artemis.domain.iris.settings; + +import java.util.Comparator; +import java.util.Set; +import java.util.TreeSet; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +@Converter +public class IrisModelListConverter implements AttributeConverter, String> { + + @Override + public String convertToDatabaseColumn(Set type) { + if (type == null || type.isEmpty()) { + return null; + } + + return String.join(",", type); + } + + @Override + public Set convertToEntityAttribute(String value) { + var treeSet = new TreeSet(Comparator.naturalOrder()); + if (value != null) { + treeSet.addAll(Set.of(value.split(","))); + } + return treeSet; + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisSettings.java b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisSettings.java index cf540eacb07b..5ca32f9b18c5 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisSettings.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisSettings.java @@ -4,56 +4,44 @@ import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; -import org.hibernate.annotations.ColumnDefault; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import de.tum.in.www1.artemis.domain.DomainObject; /** - * An IrisSettings object represents the settings for Iris for a part of Artemis. - * These settings can be either global, course or exercise specific. - * {@link de.tum.in.www1.artemis.service.iris.IrisSettingsService} for more details how IrisSettings are used. + * IrisSettings is an abstract super class for the specific settings types. + * Settings bundle {@link IrisSubSettings} together. + * {@link IrisGlobalSettings} are used to specify settings on a global level. + * {@link IrisCourseSettings} are used to specify settings on a course level. + * {@link IrisExerciseSettings} are used to specify settings on an exercise level. + *

+ * Also see {@link de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService} for more information. */ @Entity @Table(name = "iris_settings") +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +@DiscriminatorColumn(name = "discriminator", discriminatorType = DiscriminatorType.STRING) @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ @JsonSubTypes.Type(value = IrisGlobalSettings.class, name = "global"), @JsonSubTypes.Type(value = IrisCourseSettings.class, name = "course"), + @JsonSubTypes.Type(value = IrisExerciseSettings.class, name = "exercise") }) @JsonInclude(JsonInclude.Include.NON_EMPTY) -public class IrisSettings extends DomainObject { +public abstract class IrisSettings extends DomainObject { - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) - @JoinColumn(name = "iris_chat_settings_id") - private IrisSubSettings irisChatSettings; + public abstract IrisChatSubSettings getIrisChatSettings(); - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) - @JoinColumn(name = "iris_hestia_settings_id") - private IrisSubSettings irisHestiaSettings; + public abstract void setIrisChatSettings(IrisChatSubSettings irisChatSettings); - @Column(name = "is_global") - @ColumnDefault("false") - private boolean isGlobal = false; + public abstract IrisHestiaSubSettings getIrisHestiaSettings(); - public IrisSubSettings getIrisChatSettings() { - return irisChatSettings; - } + public abstract void setIrisHestiaSettings(IrisHestiaSubSettings irisHestiaSettings); - public void setIrisChatSettings(IrisSubSettings irisChatSettings) { - this.irisChatSettings = irisChatSettings; - } + public abstract IrisCodeEditorSubSettings getIrisCodeEditorSettings(); - public IrisSubSettings getIrisHestiaSettings() { - return irisHestiaSettings; - } + public abstract void setIrisCodeEditorSettings(IrisCodeEditorSubSettings irisCodeEditorSettings); - public void setIrisHestiaSettings(IrisSubSettings irisHestiaSettings) { - this.irisHestiaSettings = irisHestiaSettings; - } - - public boolean isGlobal() { - return isGlobal; - } - - public void setGlobal(boolean global) { - isGlobal = global; - } + public abstract boolean isValid(); } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisSettingsType.java b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisSettingsType.java new file mode 100644 index 000000000000..90ec73a0bf87 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisSettingsType.java @@ -0,0 +1,5 @@ +package de.tum.in.www1.artemis.domain.iris.settings; + +public enum IrisSettingsType { + GLOBAL, COURSE, EXERCISE +} diff --git a/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisSubSettings.java b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisSubSettings.java index d3ad1282b64a..e9cc586a444d 100644 --- a/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisSubSettings.java +++ b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisSubSettings.java @@ -1,5 +1,8 @@ package de.tum.in.www1.artemis.domain.iris.settings; +import java.util.Set; +import java.util.TreeSet; + import javax.annotation.Nullable; import javax.persistence.*; @@ -7,39 +10,42 @@ import org.hibernate.annotations.CacheConcurrencyStrategy; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import de.tum.in.www1.artemis.domain.DomainObject; -import de.tum.in.www1.artemis.domain.iris.IrisTemplate; /** - * An IrisSubSettings object represents the settings for a specific feature of Iris. - * {@link IrisSettings} is the parent of this class. + * IrisSubSettings is an abstract super class for the specific sub settings types. + * Sub Settings are settings for a specific feature of Iris. + * {@link IrisChatSubSettings} are used to specify settings for the chat feature. + * {@link IrisHestiaSubSettings} are used to specify settings for the Hestia integration. + * {@link IrisCodeEditorSubSettings} are used to specify settings for the code editor feature. + *

+ * Also see {@link de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService} for more information. */ @Entity @Table(name = "iris_sub_settings") +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +@DiscriminatorColumn(name = "discriminator", discriminatorType = DiscriminatorType.STRING) @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ @JsonSubTypes.Type(value = IrisChatSubSettings.class, name = "chat"), @JsonSubTypes.Type(value = IrisHestiaSubSettings.class, name = "hestia"), + @JsonSubTypes.Type(value = IrisCodeEditorSubSettings.class, name = "code-editor") }) @JsonInclude(JsonInclude.Include.NON_EMPTY) -public class IrisSubSettings extends DomainObject { +public abstract class IrisSubSettings extends DomainObject { @Column(name = "enabled") private boolean enabled = false; - @Nullable - @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) - private IrisTemplate template; + @Column(name = "allowed_models") + @Convert(converter = IrisModelListConverter.class) + private Set allowedModels = new TreeSet<>(); @Nullable - @Column(name = "preferredModel") + @Column(name = "preferred_model") private String preferredModel; - @Nullable - @Column(name = "rateLimit") - private Integer rateLimit; - - @Nullable - @Column(name = "rateLimitTimeframeHours") - private Integer rateLimitTimeframeHours; - public boolean isEnabled() { return enabled; } @@ -48,13 +54,12 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } - @Nullable - public IrisTemplate getTemplate() { - return template; + public Set getAllowedModels() { + return allowedModels; } - public void setTemplate(@Nullable IrisTemplate template) { - this.template = template; + public void setAllowedModels(Set allowedModels) { + this.allowedModels = allowedModels; } @Nullable @@ -65,22 +70,4 @@ public String getPreferredModel() { public void setPreferredModel(@Nullable String preferredModel) { this.preferredModel = preferredModel; } - - @Nullable - public Integer getRateLimit() { - return rateLimit; - } - - public void setRateLimit(@Nullable Integer rateLimit) { - this.rateLimit = rateLimit; - } - - @Nullable - public Integer getRateLimitTimeframeHours() { - return rateLimitTimeframeHours; - } - - public void setRateLimitTimeframeHours(@Nullable Integer rateLimitTimeframeHours) { - this.rateLimitTimeframeHours = rateLimitTimeframeHours; - } } diff --git a/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisSubSettingsType.java b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisSubSettingsType.java new file mode 100644 index 000000000000..9a33ab0867d0 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/domain/iris/settings/IrisSubSettingsType.java @@ -0,0 +1,5 @@ +package de.tum.in.www1.artemis.domain.iris.settings; + +public enum IrisSubSettingsType { + CHAT, HESTIA, CODE_EDITOR +} diff --git a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java index 19fbfdec3774..6fa372833546 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/CourseRepository.java @@ -143,7 +143,7 @@ SELECT CASE WHEN (count(c) > 0) THEN true ELSE false END @EntityGraph(type = LOAD, attributePaths = { "lectures", "lectures.lectureUnits" }) Optional findWithEagerLecturesAndLectureUnitsById(long courseId); - @EntityGraph(type = LOAD, attributePaths = { "organizations", "competencies", "prerequisites", "tutorialGroupsConfiguration", "onlineCourseConfiguration", "irisSettings" }) + @EntityGraph(type = LOAD, attributePaths = { "organizations", "competencies", "prerequisites", "tutorialGroupsConfiguration", "onlineCourseConfiguration" }) Optional findForUpdateById(long courseId); @EntityGraph(type = LOAD, attributePaths = { "exercises", "lectures", "lectures.lectureUnits", "competencies", "prerequisites" }) diff --git a/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisSettingsRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisSettingsRepository.java index c8397107e52e..fc402041bc34 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisSettingsRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/iris/IrisSettingsRepository.java @@ -1,13 +1,14 @@ package de.tum.in.www1.artemis.repository.iris; +import java.util.Comparator; import java.util.Optional; import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import de.tum.in.www1.artemis.domain.iris.settings.IrisSettings; +import de.tum.in.www1.artemis.domain.iris.settings.*; +import de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException; /** * Spring Data repository for the IrisSettings entity. @@ -16,19 +17,36 @@ public interface IrisSettingsRepository extends JpaRepository findAllGlobalSettings(); + Set findAllGlobalSettings(); + + default IrisGlobalSettings findGlobalSettingsElseThrow() { + return findAllGlobalSettings().stream().max(Comparator.comparingLong(IrisGlobalSettings::getId)).orElseThrow(() -> new EntityNotFoundException("Iris Global Settings")); + } @Query(""" SELECT irisSettings - FROM IrisSettings irisSettings + FROM IrisCourseSettings irisSettings LEFT JOIN FETCH irisSettings.irisChatSettings ics LEFT JOIN FETCH irisSettings.irisHestiaSettings ihs - LEFT JOIN ProgrammingExercise pe ON pe.irisSettings.id = irisSettings.id - WHERE pe.id = :programmingExerciseId + LEFT JOIN FETCH irisSettings.irisCodeEditorSettings ices + WHERE irisSettings.course.id = :courseId + """) + Optional findCourseSettings(Long courseId); + + @Query(""" + SELECT irisSettings + FROM IrisExerciseSettings irisSettings + LEFT JOIN FETCH irisSettings.irisChatSettings ics + WHERE irisSettings.exercise.id = :exerciseId """) - Optional findByProgrammingExerciseId(@Param("programmingExerciseId") Long programmingExerciseId); + Optional findExerciseSettings(Long exerciseId); + + default IrisSettings findByIdElseThrow(long existingSettingsId) { + return findById(existingSettingsId).orElseThrow(() -> new EntityNotFoundException("Iris Settings", existingSettingsId)); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/CourseService.java b/src/main/java/de/tum/in/www1/artemis/service/CourseService.java index 83afb219f85a..d08b534043fe 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/CourseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/CourseService.java @@ -56,6 +56,7 @@ import de.tum.in.www1.artemis.service.dto.StudentDTO; import de.tum.in.www1.artemis.service.exam.ExamDeletionService; import de.tum.in.www1.artemis.service.export.CourseExamExportService; +import de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService; import de.tum.in.www1.artemis.service.learningpath.LearningPathService; import de.tum.in.www1.artemis.service.notifications.GroupNotificationService; import de.tum.in.www1.artemis.service.tutorialgroups.TutorialGroupService; @@ -154,6 +155,8 @@ public class CourseService { private final LearningPathService learningPathService; + private final Optional irisSettingsService; + public CourseService(Environment env, ArtemisAuthenticationProvider artemisAuthenticationProvider, CourseRepository courseRepository, ExerciseService exerciseService, ExerciseDeletionService exerciseDeletionService, AuthorizationCheckService authCheckService, UserRepository userRepository, LectureService lectureService, GroupNotificationRepository groupNotificationRepository, ExerciseGroupRepository exerciseGroupRepository, AuditEventRepository auditEventRepository, @@ -164,7 +167,8 @@ public CourseService(Environment env, ArtemisAuthenticationProvider artemisAuthe ComplaintResponseRepository complaintResponseRepository, SubmissionRepository submissionRepository, ProgrammingExerciseRepository programmingExerciseRepository, ExerciseRepository exerciseRepository, ParticipantScoreRepository participantScoreRepository, PresentationPointsCalculationService presentationPointsCalculationService, TutorialGroupRepository tutorialGroupRepository, TutorialGroupService tutorialGroupService, TutorialGroupsConfigurationRepository tutorialGroupsConfigurationRepository, - PlagiarismCaseRepository plagiarismCaseRepository, ConversationRepository conversationRepository, LearningPathService learningPathService) { + PlagiarismCaseRepository plagiarismCaseRepository, ConversationRepository conversationRepository, LearningPathService learningPathService, + Optional irisSettingsService) { this.env = env; this.artemisAuthenticationProvider = artemisAuthenticationProvider; this.courseRepository = courseRepository; @@ -203,6 +207,7 @@ public CourseService(Environment env, ArtemisAuthenticationProvider artemisAuthe this.plagiarismCaseRepository = plagiarismCaseRepository; this.conversationRepository = conversationRepository; this.learningPathService = learningPathService; + this.irisSettingsService = irisSettingsService; } /** @@ -373,6 +378,7 @@ public void delete(Course course) { deleteExamsOfCourse(course); deleteGradingScaleOfCourse(course); deleteTutorialGroupsOfCourse(course); + irisSettingsService.ifPresent(iss -> iss.deleteSettingsFor(course)); courseRepository.deleteById(course.getId()); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedChatSubSettingsDTO.java b/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedChatSubSettingsDTO.java new file mode 100644 index 000000000000..3539b3f6a643 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedChatSubSettingsDTO.java @@ -0,0 +1,68 @@ +package de.tum.in.www1.artemis.service.dto.iris; + +import java.util.Set; + +import de.tum.in.www1.artemis.domain.iris.IrisTemplate; + +public class IrisCombinedChatSubSettingsDTO { + + private boolean enabled; + + private Integer rateLimit; + + private Integer rateLimitTimeframeHours; + + private Set allowedModels; + + private String preferredModel; + + private IrisTemplate template; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Integer getRateLimit() { + return rateLimit; + } + + public void setRateLimit(Integer rateLimit) { + this.rateLimit = rateLimit; + } + + public Integer getRateLimitTimeframeHours() { + return rateLimitTimeframeHours; + } + + public void setRateLimitTimeframeHours(Integer rateLimitTimeframeHours) { + this.rateLimitTimeframeHours = rateLimitTimeframeHours; + } + + public Set getAllowedModels() { + return allowedModels; + } + + public void setAllowedModels(Set allowedModels) { + this.allowedModels = allowedModels; + } + + public String getPreferredModel() { + return preferredModel; + } + + public void setPreferredModel(String preferredModel) { + this.preferredModel = preferredModel; + } + + public IrisTemplate getTemplate() { + return template; + } + + public void setTemplate(IrisTemplate template) { + this.template = template; + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedCodeEditorSubSettingsDTO.java b/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedCodeEditorSubSettingsDTO.java new file mode 100644 index 000000000000..951612105414 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedCodeEditorSubSettingsDTO.java @@ -0,0 +1,88 @@ +package de.tum.in.www1.artemis.service.dto.iris; + +import java.util.Set; + +import de.tum.in.www1.artemis.domain.iris.IrisTemplate; + +public class IrisCombinedCodeEditorSubSettingsDTO { + + private boolean enabled; + + private Set allowedModels; + + private String preferredModel; + + private IrisTemplate chatTemplate; + + private IrisTemplate problemStatementGenerationTemplate; + + private IrisTemplate templateRepoGenerationTemplate; + + private IrisTemplate solutionRepoGenerationTemplate; + + private IrisTemplate testRepoGenerationTemplate; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Set getAllowedModels() { + return allowedModels; + } + + public void setAllowedModels(Set allowedModels) { + this.allowedModels = allowedModels; + } + + public String getPreferredModel() { + return preferredModel; + } + + public void setPreferredModel(String preferredModel) { + this.preferredModel = preferredModel; + } + + public IrisTemplate getChatTemplate() { + return chatTemplate; + } + + public void setChatTemplate(IrisTemplate chatTemplate) { + this.chatTemplate = chatTemplate; + } + + public IrisTemplate getProblemStatementGenerationTemplate() { + return problemStatementGenerationTemplate; + } + + public void setProblemStatementGenerationTemplate(IrisTemplate problemStatementGenerationTemplate) { + this.problemStatementGenerationTemplate = problemStatementGenerationTemplate; + } + + public IrisTemplate getTemplateRepoGenerationTemplate() { + return templateRepoGenerationTemplate; + } + + public void setTemplateRepoGenerationTemplate(IrisTemplate templateRepoGenerationTemplate) { + this.templateRepoGenerationTemplate = templateRepoGenerationTemplate; + } + + public IrisTemplate getSolutionRepoGenerationTemplate() { + return solutionRepoGenerationTemplate; + } + + public void setSolutionRepoGenerationTemplate(IrisTemplate solutionRepoGenerationTemplate) { + this.solutionRepoGenerationTemplate = solutionRepoGenerationTemplate; + } + + public IrisTemplate getTestRepoGenerationTemplate() { + return testRepoGenerationTemplate; + } + + public void setTestRepoGenerationTemplate(IrisTemplate testRepoGenerationTemplate) { + this.testRepoGenerationTemplate = testRepoGenerationTemplate; + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedHestiaSubSettingsDTO.java b/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedHestiaSubSettingsDTO.java new file mode 100644 index 000000000000..5a72e542eb63 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedHestiaSubSettingsDTO.java @@ -0,0 +1,48 @@ +package de.tum.in.www1.artemis.service.dto.iris; + +import java.util.Set; + +import de.tum.in.www1.artemis.domain.iris.IrisTemplate; + +public class IrisCombinedHestiaSubSettingsDTO { + + private boolean enabled; + + private Set allowedModels; + + private String preferredModel; + + private IrisTemplate template; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Set getAllowedModels() { + return allowedModels; + } + + public void setAllowedModels(Set allowedModels) { + this.allowedModels = allowedModels; + } + + public String getPreferredModel() { + return preferredModel; + } + + public void setPreferredModel(String preferredModel) { + this.preferredModel = preferredModel; + } + + public IrisTemplate getTemplate() { + return template; + } + + public void setTemplate(IrisTemplate template) { + this.template = template; + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedSettingsDTO.java b/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedSettingsDTO.java new file mode 100644 index 000000000000..121db800b540 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/dto/iris/IrisCombinedSettingsDTO.java @@ -0,0 +1,5 @@ +package de.tum.in.www1.artemis.service.dto.iris; + +public record IrisCombinedSettingsDTO(IrisCombinedChatSubSettingsDTO irisChatSettings, IrisCombinedHestiaSubSettingsDTO irisHestiaSettings, + IrisCombinedCodeEditorSubSettingsDTO irisCodeEditorSettings) { +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/IrisConstants.java b/src/main/java/de/tum/in/www1/artemis/service/iris/IrisConstants.java index f5626032bb11..75e26ec45aba 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/IrisConstants.java +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/IrisConstants.java @@ -5,9 +5,9 @@ */ public final class IrisConstants { - private IrisConstants() { - // Utility class for constants - } + // The current version of the global settings defaults + // Increment this if you change the default settings + public static final int GLOBAL_SETTINGS_VERSION = 1; // The default guidance template for the chat feature public static final String DEFAULT_CHAT_TEMPLATE = """ @@ -112,4 +112,24 @@ private IrisConstants() { public static final String DEFAULT_HESTIA_TEMPLATE = """ TODO: Will be added in a future PR """; + + // The default guidance templates for the code editor feature + public static final String DEFAULT_CODE_EDITOR_CHAT_TEMPLATE = """ + """; + + public static final String DEFAULT_CODE_EDITOR_PROBLEM_STATEMENT_GENERATION_TEMPLATE = """ + """; + + public static final String DEFAULT_CODE_EDITOR_TEMPLATE_REPO_GENERATION_TEMPLATE = """ + """; + + public static final String DEFAULT_CODE_EDITOR_SOLUTION_REPO_GENERATION_TEMPLATE = """ + """; + + public static final String DEFAULT_CODE_EDITOR_TEST_REPO_GENERATION_TEMPLATE = """ + """; + + private IrisConstants() { + // Utility class for constants + } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/IrisRateLimitService.java b/src/main/java/de/tum/in/www1/artemis/service/iris/IrisRateLimitService.java index 02265c7261f9..33339ad7696b 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/IrisRateLimitService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/IrisRateLimitService.java @@ -9,6 +9,7 @@ import de.tum.in.www1.artemis.domain.User; import de.tum.in.www1.artemis.repository.iris.IrisMessageRepository; import de.tum.in.www1.artemis.service.iris.exception.IrisRateLimitExceededException; +import de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService; /** * Service for the rate limit of the iris chatbot. diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/IrisSettingsService.java b/src/main/java/de/tum/in/www1/artemis/service/iris/IrisSettingsService.java deleted file mode 100644 index 11f5215fd7d2..000000000000 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/IrisSettingsService.java +++ /dev/null @@ -1,437 +0,0 @@ -package de.tum.in.www1.artemis.service.iris; - -import java.util.Comparator; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -import javax.ws.rs.ForbiddenException; - -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.ApplicationContext; -import org.springframework.context.event.EventListener; -import org.springframework.core.env.Profiles; -import org.springframework.stereotype.Service; - -import de.tum.in.www1.artemis.domain.Course; -import de.tum.in.www1.artemis.domain.ProgrammingExercise; -import de.tum.in.www1.artemis.domain.iris.IrisTemplate; -import de.tum.in.www1.artemis.domain.iris.settings.IrisSettings; -import de.tum.in.www1.artemis.domain.iris.settings.IrisSubSettings; -import de.tum.in.www1.artemis.repository.CourseRepository; -import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; -import de.tum.in.www1.artemis.repository.iris.IrisSettingsRepository; -import de.tum.in.www1.artemis.service.AuthorizationCheckService; -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.ConflictException; - -/** - * Service for managing {@link IrisSettings}. - * It is used to manage and combine global, course and exercise specific settings. - */ -@Service -public class IrisSettingsService { - - private final CourseRepository courseRepository; - - private final ApplicationContext applicationContext; - - private final IrisSettingsRepository irisSettingsRepository; - - private final ProgrammingExerciseRepository programmingExerciseRepository; - - private final AuthorizationCheckService authCheckService; - - public IrisSettingsService(CourseRepository courseRepository, ApplicationContext applicationContext, IrisSettingsRepository irisSettingsRepository, - ProgrammingExerciseRepository programmingExerciseRepository, AuthorizationCheckService authCheckService) { - this.courseRepository = courseRepository; - this.applicationContext = applicationContext; - this.irisSettingsRepository = irisSettingsRepository; - this.programmingExerciseRepository = programmingExerciseRepository; - this.authCheckService = authCheckService; - } - - /** - * Hooks into the {@link ApplicationReadyEvent} and creates the global IrisSettings object on startup. - * - * @param event Specifies when this method gets called and provides the event with all application data - */ - @EventListener - public void execute(ApplicationReadyEvent event) throws Exception { - var settingsOptional = irisSettingsRepository.findAllGlobalSettings(); - if (settingsOptional.size() == 1) { - return; - } - else if (settingsOptional.size() > 1) { - var maxIdSettings = settingsOptional.stream().max(Comparator.comparingLong(IrisSettings::getId)).orElseThrow(); - settingsOptional.stream().filter(settings -> !Objects.equals(settings.getId(), maxIdSettings.getId())).forEach(irisSettingsRepository::delete); - return; - } - - if (event.getApplicationContext().getEnvironment().acceptsProfiles(Profiles.of("iris"))) { - var settings = createDefaultIrisSettings(true); - settings.setGlobal(true); - settings.getIrisChatSettings().setEnabled(true); - settings.getIrisChatSettings().setTemplate(new IrisTemplate(IrisConstants.DEFAULT_CHAT_TEMPLATE)); - settings.getIrisHestiaSettings().setEnabled(true); - settings.getIrisHestiaSettings().setTemplate(new IrisTemplate(IrisConstants.DEFAULT_HESTIA_TEMPLATE)); - saveIrisSettings(settings); - } - } - - /** - * Check if the Iris Hestia feature is enabled for a programming exercise, else throw a {@link ForbiddenException}. - * See {@link #isIrisHestiaSessionEnabled(ProgrammingExercise)} - * - * @param programmingExercise the programming exercise for which to check the settings - */ - public void checkIsIrisHestiaSessionEnabledElseThrow(ProgrammingExercise programmingExercise) { - if (!isIrisHestiaSessionEnabled(programmingExercise)) { - throw new AccessForbiddenException("Iris Hestia feature is not enabled for programming exercise " + programmingExercise.getId()); - } - } - - /** - * Check if the Iris chat feature is enabled for a programming exercise, else throw a {@link ForbiddenException}. - * See {@link #isIrisChatSessionEnabled(ProgrammingExercise)} - * - * @param programmingExercise the programming exercise for which to check the settings - */ - public void checkIsIrisChatSessionEnabledElseThrow(ProgrammingExercise programmingExercise) { - if (!isIrisChatSessionEnabled(programmingExercise)) { - throw new AccessForbiddenException("Iris Chat feature is not enabled for programming exercise " + programmingExercise.getId()); - } - } - - /** - * Check if the Iris Hestia feature is enabled for a programming exercise. - * This is the case if they are enabled in the global and course settings and not disabled in the exercise settings. - * - * @param programmingExercise the programming exercise for which to check the settings - * @return true if the Iris Hestia feature is enabled, false otherwise - */ - public boolean isIrisHestiaSessionEnabled(ProgrammingExercise programmingExercise) { - if (programmingExercise == null || programmingExercise.getIrisSettings() == null) { - return false; - } - var settings = getCombinedIrisSettings(programmingExercise, true); - return settings.getIrisHestiaSettings().isEnabled(); - } - - /** - * Check if the Iris chat feature is enabled for a programming exercise. - * This is the case if they are enabled in the global, course, and exercise settings. - * - * @param programmingExercise the programming exercise for which to check the settings - * @return true if the Iris chat feature is enabled, false otherwise - */ - public boolean isIrisChatSessionEnabled(ProgrammingExercise programmingExercise) { - if (programmingExercise == null || programmingExercise.getIrisSettings() == null) { - return false; - } - var settings = getCombinedIrisSettings(programmingExercise, true); - return settings.getIrisChatSettings().isEnabled(); - } - - /** - * Get the global Iris settings. - * - * @return the global Iris settings - */ - public IrisSettings getGlobalSettings() { - return irisSettingsRepository.findAllGlobalSettings().stream().max(Comparator.comparingLong(IrisSettings::getId)).orElseThrow(); - } - - /** - * Get the Iris settings for a course. If no settings exist, a default settings object is created. - * {@link IrisSettingsService#addDefaultIrisSettingsTo(Course)} for more details about the default settings. - * - * @param course the course for which to get the settings - * @return the IrisSettings - */ - public IrisSettings getIrisSettingsOrDefault(Course course) { - if (course.getIrisSettings() == null || course.getIrisSettings().getId() == null) { - return createDefaultIrisSettings(true); - } - return irisSettingsRepository.findById(course.getIrisSettings().getId()).orElse(createDefaultIrisSettings(true)); - } - - /** - * Get the Iris settings for a programming exercise. If no settings exist, a default settings object is created. - * {@link IrisSettingsService#addDefaultIrisSettingsTo(Course)} for more details about the default settings. - * - * @param exercise the programming exercise for which to get the settings - * @return the IrisSettings - */ - public IrisSettings getIrisSettingsOrDefault(ProgrammingExercise exercise) { - if (exercise.getIrisSettings() == null || exercise.getIrisSettings().getId() == null) { - return createDefaultIrisSettings(false); - } - return irisSettingsRepository.findById(exercise.getIrisSettings().getId()).orElse(createDefaultIrisSettings(true)); - } - - /** - * Get the combined Iris settings for a course. Combines the global and course settings together. - * The course settings override the global settings, except for the enabled flag, which is combined. - * - * @param course the course for which to get the settings - * @param reduced if true only the enabled flag is combined, otherwise all settings are combined - * @return the combined IrisSettings - */ - public IrisSettings getCombinedIrisSettings(Course course, boolean reduced) { - var globalSettings = getGlobalSettings(); - var courseSettings = getIrisSettingsOrDefault(course); - - var combinedSettings = new IrisSettings(); - combinedSettings.setIrisChatSettings(combineSubSettings(globalSettings.getIrisChatSettings(), courseSettings.getIrisChatSettings(), false, reduced)); - combinedSettings.setIrisHestiaSettings(combineSubSettings(globalSettings.getIrisHestiaSettings(), courseSettings.getIrisHestiaSettings(), false, reduced)); - return combinedSettings; - } - - /** - * Get the combined Iris settings for a programming exercise. Combines the course and exercise settings together. - * The exercise settings override the course settings, but depending on the sub settings type, the combining strategy is different. - * ChatSettings: exercise settings are mandatory for the chat feature to be enabled - * HestiaSettings: exercise settings are optional for the hestia feature to be enabled - * - * @param programmingExercise the programming exercise for which to get the settings - * @param reduced if true only the enabled flag is combined, otherwise all settings are combined - * @return the combined IrisSettings - */ - public IrisSettings getCombinedIrisSettings(ProgrammingExercise programmingExercise, boolean reduced) { - var courseSettings = getCombinedIrisSettings(programmingExercise.getCourseViaExerciseGroupOrCourseMember(), reduced); - var exerciseSettings = getIrisSettingsOrDefault(programmingExercise); - - var combinedSettings = new IrisSettings(); - combinedSettings.setIrisChatSettings(combineSubSettings(courseSettings.getIrisChatSettings(), exerciseSettings.getIrisChatSettings(), false, reduced)); - combinedSettings.setIrisHestiaSettings(combineSubSettings(courseSettings.getIrisHestiaSettings(), exerciseSettings.getIrisHestiaSettings(), true, reduced)); - return combinedSettings; - } - - /** - * Combines the course and exercise sub-settings together. - * The exercise settings override the course settings, but depending on the exerciseSettingsAreOptional parameter, - * the combining strategy is different. If exerciseSettingsAreOptional is true, the course settings are used in full - * if the exercise settings are null. Otherwise the exercise settings have to be present. - * - * @param subSettings1 The course settings - * @param subSettings2 The exercise settings - * @param secondSubSettingsAreOptional Whether the exercise settings are optional or not - * @param reduced Whether only the enabled flag should be combined or all settings - * @return The combined sub-settings - */ - private IrisSubSettings combineSubSettings(IrisSubSettings subSettings1, IrisSubSettings subSettings2, boolean secondSubSettingsAreOptional, boolean reduced) { - if (secondSubSettingsAreOptional && subSettings2 == null) { - return subSettings1; - } - - var combinedSettings = new IrisSubSettings(); - - var enabled = subSettings2 != null && subSettings2.isEnabled() && subSettings1 != null && subSettings1.isEnabled() - && applicationContext.getEnvironment().acceptsProfiles(Profiles.of("iris")); - combinedSettings.setEnabled(enabled); - - if (!reduced) { - String preferredModel = null; - if (subSettings2 != null && subSettings2.getPreferredModel() != null) { - preferredModel = subSettings2.getPreferredModel(); - } - else if (subSettings1 != null && subSettings1.getPreferredModel() != null) { - preferredModel = subSettings1.getPreferredModel(); - } - combinedSettings.setPreferredModel(preferredModel); - - IrisTemplate template; - if (subSettings2 != null && subSettings2.getTemplate() != null) { - template = subSettings2.getTemplate(); - } - else if (subSettings1 != null) { - template = subSettings1.getTemplate(); - } - else { - template = null; - } - combinedSettings.setTemplate(template); - } - - return combinedSettings; - } - - /** - * Adds the default Iris settings to a course if they are not present yet. - * - * @param course The course to add the default Iris settings to - * @return The course with the default Iris settings - */ - public Course addDefaultIrisSettingsTo(Course course) { - if (course.getIrisSettings() != null) { - return course; - } - course.setIrisSettings(createDefaultIrisSettings(true)); - return courseRepository.save(course); - } - - /** - * Adds the default Iris settings to a programming exercise if they are not present yet. - * - * @param programmingExercise The programming exercise to add the default Iris settings to - * @return The programming exercise with the default Iris settings - */ - public ProgrammingExercise addDefaultIrisSettingsTo(ProgrammingExercise programmingExercise) { - if (programmingExercise.getIrisSettings() != null) { - return programmingExercise; - } - programmingExercise.setIrisSettings(createDefaultIrisSettings(false)); - return programmingExerciseRepository.save(programmingExercise); - } - - private IrisSettings createDefaultIrisSettings(boolean withOptionalSettings) { - var irisSettings = new IrisSettings(); - irisSettings.setIrisChatSettings(createDefaultIrisSubSettings()); - irisSettings.setIrisHestiaSettings(withOptionalSettings ? createDefaultIrisSubSettings() : null); - return irisSettings; - } - - private IrisSubSettings createDefaultIrisSubSettings() { - var subSettings = new IrisSubSettings(); - subSettings.setEnabled(false); - subSettings.setPreferredModel(null); - subSettings.setTemplate(null); - return subSettings; - } - - /** - * Save the Iris settings. Should always be used over directly calling the repository. - * Ensures that there is only one global Iris settings object. - * - * @param settings The Iris settings to save - * @return The saved Iris settings - */ - public IrisSettings saveIrisSettings(IrisSettings settings) { - if (settings.isGlobal()) { - var allGlobalSettings = irisSettingsRepository.findAllGlobalSettings(); - if (!allGlobalSettings.isEmpty() && !allGlobalSettings.stream().map(IrisSettings::getId).toList().equals(List.of(settings.getId()))) { - throw new IllegalStateException("There can only be one global Iris settings object."); - } - } - return irisSettingsRepository.save(settings); - } - - /** - * Save the global Iris settings. - * - * @param settings The Iris settings to save - * @return The saved Iris settings - */ - public IrisSettings saveGlobalIrisSettings(IrisSettings settings) { - if (!settings.isGlobal()) { - throw new BadRequestAlertException("The settings must be global", "IrisSettings", "notGlobal"); - } - var globalSettings = getGlobalSettings(); - globalSettings.setIrisChatSettings(copyIrisSubSettings(globalSettings.getIrisChatSettings(), settings.getIrisChatSettings())); - globalSettings.setIrisHestiaSettings(copyIrisSubSettings(globalSettings.getIrisHestiaSettings(), settings.getIrisHestiaSettings())); - return irisSettingsRepository.save(globalSettings); - } - - /** - * Save the Iris settings for a course. - * - * @param course The course for which to save the settings - * @param settings The Iris settings to save - * @return The saved Iris settings - */ - public IrisSettings saveIrisSettings(Course course, IrisSettings settings) { - var existingSettingsOptional = getIrisSettings(course); - if (existingSettingsOptional.isPresent()) { - var existingSettings = existingSettingsOptional.get(); - existingSettings.setIrisChatSettings(copyIrisSubSettings(existingSettings.getIrisChatSettings(), settings.getIrisChatSettings())); - existingSettings.setIrisHestiaSettings(copyIrisSubSettings(existingSettings.getIrisHestiaSettings(), settings.getIrisHestiaSettings())); - return saveIrisSettings(existingSettings); - } - else { - settings.setId(null); - course.setIrisSettings(saveIrisSettings(settings)); - var updatedCourse = courseRepository.save(course); - return updatedCourse.getIrisSettings(); - } - } - - /** - * Save the Iris settings for a programming exercise. - * - * @param exercise the programming exercise for which to save the settings - * @param settings the Iris settings to save - * @return the saved Iris settings - */ - public IrisSettings saveIrisSettings(ProgrammingExercise exercise, IrisSettings settings) { - if (exercise.isExamExercise()) { - throw new ConflictException("Iris is not supported for exam exercises", "Iris", "irisExamExercise"); - } - var existingSettingsOptional = getIrisSettings(exercise); - if (existingSettingsOptional.isPresent()) { - var existingSettings = existingSettingsOptional.get(); - existingSettings.setIrisChatSettings(copyIrisSubSettings(existingSettings.getIrisChatSettings(), settings.getIrisChatSettings())); - existingSettings.setIrisHestiaSettings(copyIrisSubSettings(existingSettings.getIrisHestiaSettings(), settings.getIrisHestiaSettings())); - return saveIrisSettings(existingSettings); - } - else { - settings.setId(null); - exercise.setIrisSettings(saveIrisSettings(settings)); - var updatedExercise = programmingExerciseRepository.save(exercise); - return updatedExercise.getIrisSettings(); - } - } - - /** - * Update the Iris settings for a programming exercise. - * - * @param programmingExerciseBeforeUpdate the programming exercise before the update - * @param updatedProgrammingExercise the programming exercise after the update - */ - public void updateIrisSettings(ProgrammingExercise programmingExerciseBeforeUpdate, ProgrammingExercise updatedProgrammingExercise) { - irisSettingsRepository.findByProgrammingExerciseId(programmingExerciseBeforeUpdate.getId()).ifPresent(updatedProgrammingExercise::setIrisSettings); - } - - private IrisSubSettings copyIrisSubSettings(IrisSubSettings target, IrisSubSettings source) { - if (target == null || source == null) { - return source; - } - if (authCheckService.isAdmin()) { - target.setEnabled(source.isEnabled()); - target.setPreferredModel(source.getPreferredModel()); - target.setRateLimit(source.getRateLimit()); - target.setRateLimitTimeframeHours(source.getRateLimitTimeframeHours()); - } - if (!Objects.equals(source.getTemplate(), target.getTemplate())) { - target.setTemplate(source.getTemplate()); - } - return target; - } - - /** - * Get the Iris settings for a course. If no settings exist, an empty optional is returned. - * - * @param course the course for which to get the settings - * @return the IrisSettings - */ - private Optional getIrisSettings(Course course) { - if (course.getIrisSettings() == null) { - return Optional.empty(); - } - return irisSettingsRepository.findById(course.getIrisSettings().getId()); - } - - /** - * Get the Iris settings for a course. If no settings exist, an empty optional is returned. - * - * @param exercise the course for which to get the settings - * @return the IrisSettings - */ - private Optional getIrisSettings(ProgrammingExercise exercise) { - if (exercise.getIrisSettings() == null) { - return Optional.empty(); - } - return irisSettingsRepository.findById(exercise.getIrisSettings().getId()); - } -} diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisChatSessionService.java b/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisChatSessionService.java index 32e73233c803..2cdca1818d10 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisChatSessionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisChatSessionService.java @@ -18,6 +18,7 @@ import de.tum.in.www1.artemis.domain.iris.IrisMessageSender; import de.tum.in.www1.artemis.domain.iris.session.IrisChatSession; import de.tum.in.www1.artemis.domain.iris.session.IrisSession; +import de.tum.in.www1.artemis.domain.iris.settings.IrisSubSettingsType; import de.tum.in.www1.artemis.repository.ProgrammingExerciseStudentParticipationRepository; import de.tum.in.www1.artemis.repository.ProgrammingSubmissionRepository; import de.tum.in.www1.artemis.repository.TemplateProgrammingExerciseParticipationRepository; @@ -28,9 +29,9 @@ import de.tum.in.www1.artemis.service.connectors.GitService; import de.tum.in.www1.artemis.service.connectors.iris.IrisConnectorService; import de.tum.in.www1.artemis.service.iris.IrisMessageService; -import de.tum.in.www1.artemis.service.iris.IrisSettingsService; import de.tum.in.www1.artemis.service.iris.IrisWebsocketService; import de.tum.in.www1.artemis.service.iris.exception.IrisNoResponseException; +import de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; import de.tum.in.www1.artemis.web.rest.errors.ConflictException; import de.tum.in.www1.artemis.web.rest.errors.InternalServerErrorException; @@ -108,7 +109,7 @@ public void checkHasAccessToIrisSession(IrisSession session, User user) { @Override public void checkIsIrisActivated(IrisSession session) { var chatSession = castToSessionType(session, IrisChatSession.class); - irisSettingsService.checkIsIrisChatSessionEnabledElseThrow(chatSession.getExercise()); + irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.CHAT, chatSession.getExercise()); } /** @@ -149,8 +150,8 @@ public void requestAndHandleResponse(IrisSession session) { parameters.put("session", fullSession); addDiffAndTemplatesForStudentAndExerciseIfPossible(chatSession.getUser(), exercise, parameters); - var irisSettings = irisSettingsService.getCombinedIrisSettings(exercise, false); - irisConnectorService.sendRequest(irisSettings.getIrisChatSettings().getTemplate(), irisSettings.getIrisChatSettings().getPreferredModel(), parameters) + var irisSettings = irisSettingsService.getCombinedIrisSettingsFor(exercise, false); + irisConnectorService.sendRequest(irisSettings.irisChatSettings().getTemplate(), irisSettings.irisChatSettings().getPreferredModel(), parameters) .handleAsync((irisMessage, throwable) -> { if (throwable != null) { log.error("Error while getting response from Iris model", throwable); diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisHestiaSessionService.java b/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisHestiaSessionService.java index 5aeea216407b..9e70973d2ef1 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisHestiaSessionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/session/IrisHestiaSessionService.java @@ -19,12 +19,13 @@ import de.tum.in.www1.artemis.domain.iris.*; import de.tum.in.www1.artemis.domain.iris.session.IrisHestiaSession; import de.tum.in.www1.artemis.domain.iris.session.IrisSession; +import de.tum.in.www1.artemis.domain.iris.settings.IrisSubSettingsType; import de.tum.in.www1.artemis.repository.iris.IrisSessionRepository; import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.service.AuthorizationCheckService; import de.tum.in.www1.artemis.service.connectors.iris.IrisConnectorService; import de.tum.in.www1.artemis.service.iris.IrisMessageService; -import de.tum.in.www1.artemis.service.iris.IrisSettingsService; +import de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService; import de.tum.in.www1.artemis.web.rest.errors.InternalServerErrorException; /** @@ -87,14 +88,14 @@ public CodeHint generateDescription(CodeHint codeHint) { irisMessageService.saveMessage(userMessage, irisSession, IrisMessageSender.USER); irisSession = (IrisHestiaSession) irisSessionRepository.findByIdWithMessagesAndContents(irisSession.getId()); Map parameters = Map.of("codeHint", irisSession.getCodeHint()); - var irisSettings = irisSettingsService.getCombinedIrisSettings(irisSession.getCodeHint().getExercise(), false); + var irisSettings = irisSettingsService.getCombinedIrisSettingsFor(irisSession.getCodeHint().getExercise(), false); try { - var irisMessage1 = irisConnectorService - .sendRequest(irisSettings.getIrisHestiaSettings().getTemplate(), irisSettings.getIrisHestiaSettings().getPreferredModel(), parameters).get(); + var irisMessage1 = irisConnectorService.sendRequest(irisSettings.irisHestiaSettings().getTemplate(), irisSettings.irisHestiaSettings().getPreferredModel(), parameters) + .get(); irisMessageService.saveMessage(irisMessage1.message(), irisSession, IrisMessageSender.LLM); irisSession = (IrisHestiaSession) irisSessionRepository.findByIdWithMessagesAndContents(irisSession.getId()); - var irisMessage2 = irisConnectorService - .sendRequest(irisSettings.getIrisHestiaSettings().getTemplate(), irisSettings.getIrisHestiaSettings().getPreferredModel(), parameters).get(); + var irisMessage2 = irisConnectorService.sendRequest(irisSettings.irisHestiaSettings().getTemplate(), irisSettings.irisHestiaSettings().getPreferredModel(), parameters) + .get(); irisMessageService.saveMessage(irisMessage2.message(), irisSession, IrisMessageSender.LLM); codeHint.setContent(irisMessage1.message().getContent().stream().map(IrisMessageContent::getTextContent).collect(Collectors.joining("\n"))); @@ -179,6 +180,6 @@ public void checkHasAccessToIrisSession(IrisSession irisSession, User user) { @Override public void checkIsIrisActivated(IrisSession session) { var irisHestiaSession = castToSessionType(session, IrisHestiaSession.class); - irisSettingsService.checkIsIrisHestiaSessionEnabledElseThrow(irisHestiaSession.getCodeHint().getExercise()); + irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.HESTIA, irisHestiaSession.getCodeHint().getExercise()); } } diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/settings/IrisSettingsService.java b/src/main/java/de/tum/in/www1/artemis/service/iris/settings/IrisSettingsService.java new file mode 100644 index 000000000000..4c3b9d3c1632 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/settings/IrisSettingsService.java @@ -0,0 +1,411 @@ +package de.tum.in.www1.artemis.service.iris.settings; + +import static de.tum.in.www1.artemis.domain.iris.settings.IrisSettingsType.*; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Objects; + +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; + +import de.tum.in.www1.artemis.domain.Course; +import de.tum.in.www1.artemis.domain.Exercise; +import de.tum.in.www1.artemis.domain.iris.IrisTemplate; +import de.tum.in.www1.artemis.domain.iris.settings.*; +import de.tum.in.www1.artemis.repository.iris.IrisSettingsRepository; +import de.tum.in.www1.artemis.service.dto.iris.IrisCombinedSettingsDTO; +import de.tum.in.www1.artemis.service.iris.IrisConstants; +import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenAlertException; +import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException; +import de.tum.in.www1.artemis.web.rest.errors.ConflictException; + +/** + * Service for managing {@link IrisSettings}. + * This service is responsible for CRUD operations on {@link IrisSettings}. + * It also provides methods for combining multiple {@link IrisSettings} and checking if a certain Iris feature is + * enabled for an exercise. + * See {@link IrisSubSettingsService} for more information on the handling of {@link IrisSubSettings}. + */ +@Service +@Profile("iris") +public class IrisSettingsService { + + private final IrisSettingsRepository irisSettingsRepository; + + private final IrisSubSettingsService irisSubSettingsService; + + public IrisSettingsService(IrisSettingsRepository irisSettingsRepository, IrisSubSettingsService irisSubSettingsService) { + this.irisSettingsRepository = irisSettingsRepository; + this.irisSubSettingsService = irisSubSettingsService; + } + + /** + * Hooks into the {@link ApplicationReadyEvent} and creates or updates the global IrisSettings object on startup. + * + * @param event Unused event param used to specify when the method should be executed + */ + @Profile("scheduling") + @EventListener + public void execute(ApplicationReadyEvent event) throws Exception { + var allGlobalSettings = irisSettingsRepository.findAllGlobalSettings(); + if (allGlobalSettings.isEmpty()) { + createInitialGlobalSettings(); + return; + } + if (allGlobalSettings.size() > 1) { + var maxIdSettings = allGlobalSettings.stream().max(Comparator.comparingLong(IrisSettings::getId)).orElseThrow(); + allGlobalSettings.stream().filter(settings -> !Objects.equals(settings.getId(), maxIdSettings.getId())).forEach(irisSettingsRepository::delete); + autoUpdateGlobalSettings(maxIdSettings); + } + else { + autoUpdateGlobalSettings(allGlobalSettings.stream().findFirst().get()); + } + } + + /** + * Creates the initial global IrisSettings object. + */ + private void createInitialGlobalSettings() { + var settings = new IrisGlobalSettings(); + settings.setCurrentVersion(IrisConstants.GLOBAL_SETTINGS_VERSION); + + var chatSettings = new IrisChatSubSettings(); + chatSettings.setEnabled(false); + chatSettings.setTemplate(new IrisTemplate(IrisConstants.DEFAULT_CHAT_TEMPLATE)); + settings.setIrisChatSettings(chatSettings); + + var hestiaSettings = new IrisHestiaSubSettings(); + hestiaSettings.setEnabled(false); + hestiaSettings.setTemplate(new IrisTemplate(IrisConstants.DEFAULT_HESTIA_TEMPLATE)); + settings.setIrisHestiaSettings(hestiaSettings); + + updateIrisCodeEditorSettings(settings); + + irisSettingsRepository.save(settings); + } + + /** + * Auto updates the global IrisSettings object if the current version is outdated. + * + * @param settings The global IrisSettings object to update + */ + private void autoUpdateGlobalSettings(IrisGlobalSettings settings) { + if (settings.getCurrentVersion() < IrisConstants.GLOBAL_SETTINGS_VERSION) { + if (settings.isEnableAutoUpdateChat() || settings.getIrisChatSettings() == null) { + settings.getIrisChatSettings().setTemplate(new IrisTemplate(IrisConstants.DEFAULT_CHAT_TEMPLATE)); + } + if (settings.isEnableAutoUpdateHestia() || settings.getIrisHestiaSettings() == null) { + settings.getIrisHestiaSettings().setTemplate(new IrisTemplate(IrisConstants.DEFAULT_HESTIA_TEMPLATE)); + } + if (settings.isEnableAutoUpdateCodeEditor() || settings.getIrisCodeEditorSettings() == null) { + updateIrisCodeEditorSettings(settings); + } + settings.setCurrentVersion(IrisConstants.GLOBAL_SETTINGS_VERSION); + saveIrisSettings(settings); + } + } + + private static void updateIrisCodeEditorSettings(IrisGlobalSettings settings) { + var irisCodeEditorSettings = settings.getIrisCodeEditorSettings(); + if (irisCodeEditorSettings == null) { + irisCodeEditorSettings = new IrisCodeEditorSubSettings(); + irisCodeEditorSettings.setEnabled(false); + } + irisCodeEditorSettings.setChatTemplate(new IrisTemplate(IrisConstants.DEFAULT_CODE_EDITOR_CHAT_TEMPLATE)); + irisCodeEditorSettings.setProblemStatementGenerationTemplate(new IrisTemplate(IrisConstants.DEFAULT_CODE_EDITOR_PROBLEM_STATEMENT_GENERATION_TEMPLATE)); + irisCodeEditorSettings.setTemplateRepoGenerationTemplate(new IrisTemplate(IrisConstants.DEFAULT_CODE_EDITOR_TEMPLATE_REPO_GENERATION_TEMPLATE)); + irisCodeEditorSettings.setSolutionRepoGenerationTemplate(new IrisTemplate(IrisConstants.DEFAULT_CODE_EDITOR_SOLUTION_REPO_GENERATION_TEMPLATE)); + irisCodeEditorSettings.setTestRepoGenerationTemplate(new IrisTemplate(IrisConstants.DEFAULT_CODE_EDITOR_TEST_REPO_GENERATION_TEMPLATE)); + settings.setIrisCodeEditorSettings(irisCodeEditorSettings); + } + + public IrisGlobalSettings getGlobalSettings() { + return irisSettingsRepository.findGlobalSettingsElseThrow(); + } + + /** + * Save the Iris settings. Should always be used over directly calling the repository. + * Automatically decides whether to save a new Iris settings object or update an existing one. + * + * @param The subtype of the IrisSettings object + * @param settings The Iris settings to save + * @return The saved Iris settings + */ + public T saveIrisSettings(T settings) { + if (settings.getId() == null) { + return saveNewIrisSettings(settings); + } + else { + return updateIrisSettings(settings.getId(), settings); + } + } + + /** + * Save a new IrisSettings object. Should always be used over directly calling the repository. + * Ensures that the settings are valid and that no settings for the given object already exist. + * + * @param The subtype of the IrisSettings object + * @param settings The IrisSettings to save + * @return The saved IrisSettings + */ + private T saveNewIrisSettings(T settings) { + if (settings instanceof IrisGlobalSettings) { + throw new BadRequestAlertException("You can not create new global settings", "IrisSettings", "notGlobal"); + } + if (!settings.isValid()) { + throw new BadRequestAlertException("New Iris settings are not valid", "IrisSettings", "notValid"); + } + if (settings instanceof IrisCourseSettings courseSettings && irisSettingsRepository.findCourseSettings(courseSettings.getCourse().getId()).isPresent()) { + throw new ConflictException("Iris settings for this course already exist", "IrisSettings", "alreadyExists"); + } + if (settings instanceof IrisExerciseSettings exerciseSettings && irisSettingsRepository.findExerciseSettings(exerciseSettings.getExercise().getId()).isPresent()) { + throw new ConflictException("Iris settings for this exercise already exist", "IrisSettings", "alreadyExists"); + } + return irisSettingsRepository.save(settings); + } + + /** + * Update an existing IrisSettings object. Should always be used over directly calling the repository. + * Ensures that the settings are valid and that the existing settings ID matches the update ID. + * Then updates the existing settings according to the type of the settings object. + * + * @param The subtype of the IrisSettings object + * @param existingSettingsId The ID of the existing IrisSettings object + * @param settingsUpdate The Iris settings object to update + * @return The updated IrisSettings + */ + @SuppressWarnings("unchecked") + private T updateIrisSettings(long existingSettingsId, T settingsUpdate) { + if (!Objects.equals(existingSettingsId, settingsUpdate.getId())) { + throw new ConflictException("Existing Iris settings ID does not match update ID", "IrisSettings", "idMismatch"); + } + if (!settingsUpdate.isValid()) { + throw new BadRequestAlertException("Updated Iris settings are not valid", "IrisSettings", "notValid"); + } + + var existingSettings = irisSettingsRepository.findByIdElseThrow(existingSettingsId); + + if (existingSettings instanceof IrisGlobalSettings globalSettings && settingsUpdate instanceof IrisGlobalSettings globalSettingsUpdate) { + return (T) updateGlobalSettings(globalSettings, globalSettingsUpdate); + } + else if (existingSettings instanceof IrisCourseSettings courseSettings && settingsUpdate instanceof IrisCourseSettings courseSettingsUpdate) { + return (T) updateCourseSettings(courseSettings, courseSettingsUpdate); + } + else if (existingSettings instanceof IrisExerciseSettings exerciseSettings && settingsUpdate instanceof IrisExerciseSettings exerciseSettingsUpdate) { + return (T) updateExerciseSettings(exerciseSettings, exerciseSettingsUpdate); + } + else { + throw new BadRequestAlertException("Unknown Iris settings type", "IrisSettings", "unknownType"); + } + } + + /** + * Helper method to update global Iris settings. + * + * @param existingSettings The existing global Iris settings + * @param settingsUpdate The global Iris settings to update + * @return The updated global Iris settings + */ + private IrisGlobalSettings updateGlobalSettings(IrisGlobalSettings existingSettings, IrisGlobalSettings settingsUpdate) { + existingSettings.setCurrentVersion(settingsUpdate.getCurrentVersion()); + existingSettings.setEnableAutoUpdateChat(settingsUpdate.isEnableAutoUpdateChat()); + existingSettings.setEnableAutoUpdateHestia(settingsUpdate.isEnableAutoUpdateHestia()); + existingSettings.setEnableAutoUpdateCodeEditor(settingsUpdate.isEnableAutoUpdateCodeEditor()); + existingSettings.setIrisChatSettings(irisSubSettingsService.update(existingSettings.getIrisChatSettings(), settingsUpdate.getIrisChatSettings(), null, GLOBAL)); + existingSettings.setIrisHestiaSettings(irisSubSettingsService.update(existingSettings.getIrisHestiaSettings(), settingsUpdate.getIrisHestiaSettings(), null, GLOBAL)); + existingSettings + .setIrisCodeEditorSettings(irisSubSettingsService.update(existingSettings.getIrisCodeEditorSettings(), settingsUpdate.getIrisCodeEditorSettings(), null, GLOBAL)); + return irisSettingsRepository.save(existingSettings); + } + + /** + * Helper method to update course Iris settings. + * + * @param existingSettings The existing course Iris settings + * @param settingsUpdate The course Iris settings to update + * @return The updated course Iris settings + */ + private IrisCourseSettings updateCourseSettings(IrisCourseSettings existingSettings, IrisCourseSettings settingsUpdate) { + var parentSettings = getCombinedIrisGlobalSettings(); + existingSettings.setIrisChatSettings( + irisSubSettingsService.update(existingSettings.getIrisChatSettings(), settingsUpdate.getIrisChatSettings(), parentSettings.irisChatSettings(), COURSE)); + existingSettings.setIrisHestiaSettings( + irisSubSettingsService.update(existingSettings.getIrisHestiaSettings(), settingsUpdate.getIrisHestiaSettings(), parentSettings.irisHestiaSettings(), COURSE)); + existingSettings.setIrisCodeEditorSettings(irisSubSettingsService.update(existingSettings.getIrisCodeEditorSettings(), settingsUpdate.getIrisCodeEditorSettings(), + parentSettings.irisCodeEditorSettings(), COURSE)); + return irisSettingsRepository.save(existingSettings); + } + + /** + * Helper method to update exercise Iris settings. + * + * @param existingSettings The existing exercise Iris settings + * @param settingsUpdate The exercise Iris settings to update + * @return The updated exercise Iris settings + */ + private IrisExerciseSettings updateExerciseSettings(IrisExerciseSettings existingSettings, IrisExerciseSettings settingsUpdate) { + var parentSettings = getCombinedIrisSettingsFor(existingSettings.getExercise().getCourseViaExerciseGroupOrCourseMember(), false); + existingSettings.setIrisChatSettings( + irisSubSettingsService.update(existingSettings.getIrisChatSettings(), settingsUpdate.getIrisChatSettings(), parentSettings.irisChatSettings(), EXERCISE)); + return irisSettingsRepository.save(existingSettings); + } + + /** + * Checks whether an Iris feature is enabled for an exercise. + * + * @param type The Iris feature to check + * @param exercise The exercise to check + * @return Whether the Iris feature is enabled for the exercise + */ + public boolean isEnabledFor(IrisSubSettingsType type, Exercise exercise) { + var settings = getCombinedIrisSettingsFor(exercise, true); + return switch (type) { + case CHAT -> settings.irisChatSettings().isEnabled(); + case HESTIA -> settings.irisHestiaSettings().isEnabled(); + case CODE_EDITOR -> false; // FIXME: Implement this in another PR + }; + } + + /** + * Checks whether an Iris feature is enabled for an exercise. + * Throws an exception if the feature is disabled. + * + * @param type The Iris feature to check + * @param exercise The exercise to check + */ + public void isEnabledForElseThrow(IrisSubSettingsType type, Exercise exercise) { + if (!isEnabledFor(type, exercise)) { + throw new AccessForbiddenAlertException("The Iris " + type.name() + " feature is disabled for this exercise.", "Iris", + "iris." + type.name().toLowerCase() + "Disabled"); + } + } + + /** + * Get the global Iris settings as an {@link IrisCombinedSettingsDTO}. + * + * @return The (combined) global Iris settings + */ + public IrisCombinedSettingsDTO getCombinedIrisGlobalSettings() { + var settingsList = new ArrayList(); + settingsList.add(getGlobalSettings()); + + return new IrisCombinedSettingsDTO(irisSubSettingsService.combineChatSettings(settingsList, false), irisSubSettingsService.combineHestiaSettings(settingsList, false), + irisSubSettingsService.combineCodeEditorSettings(settingsList, false)); + } + + /** + * Get the combined Iris settings for a course as an {@link IrisCombinedSettingsDTO}. + * Combines the global Iris settings with the course Iris settings. + * If minimal is true, only certain attributes are returned. The minimal version can safely be passed to the students. + * See also {@link IrisSubSettingsService} for how the combining works in detail + * + * @param course The course to get the Iris settings for + * @param minimal Whether to return the minimal version of the settings + * @return The combined Iris settings for the course + */ + public IrisCombinedSettingsDTO getCombinedIrisSettingsFor(Course course, boolean minimal) { + var settingsList = new ArrayList(); + settingsList.add(getGlobalSettings()); + settingsList.add(irisSettingsRepository.findCourseSettings(course.getId()).orElse(null)); + + return new IrisCombinedSettingsDTO(irisSubSettingsService.combineChatSettings(settingsList, minimal), irisSubSettingsService.combineHestiaSettings(settingsList, minimal), + irisSubSettingsService.combineCodeEditorSettings(settingsList, minimal)); + } + + /** + * Get the combined Iris settings for an exercise as an {@link IrisCombinedSettingsDTO}. + * Combines the global Iris settings with the course Iris settings and the exercise Iris settings. + * If minimal is true, only certain attributes are returned. The minimal version can safely be passed to the students. + * See also {@link IrisSubSettingsService} for how the combining works in detail + * + * @param exercise The exercise to get the Iris settings for + * @param minimal Whether to return the minimal version of the settings + * @return The combined Iris settings for the exercise + */ + public IrisCombinedSettingsDTO getCombinedIrisSettingsFor(Exercise exercise, boolean minimal) { + var settingsList = new ArrayList(); + settingsList.add(getGlobalSettings()); + settingsList.add(irisSettingsRepository.findCourseSettings(exercise.getCourseViaExerciseGroupOrCourseMember().getId()).orElse(null)); + settingsList.add(irisSettingsRepository.findExerciseSettings(exercise.getId()).orElse(null)); + + return new IrisCombinedSettingsDTO(irisSubSettingsService.combineChatSettings(settingsList, minimal), irisSubSettingsService.combineHestiaSettings(settingsList, minimal), + irisSubSettingsService.combineCodeEditorSettings(settingsList, minimal)); + } + + /** + * Get the default Iris settings for a course. + * The default settings are used if no Iris settings for the course exist. + * + * @param course The course to get the default Iris settings for + * @return The default Iris settings for the course + */ + public IrisCourseSettings getDefaultSettingsFor(Course course) { + var settings = new IrisCourseSettings(); + settings.setCourse(course); + settings.setIrisChatSettings(new IrisChatSubSettings()); + settings.setIrisHestiaSettings(new IrisHestiaSubSettings()); + settings.setIrisCodeEditorSettings(new IrisCodeEditorSubSettings()); + return settings; + } + + /** + * Get the default Iris settings for an exercise. + * The default settings are used if no Iris settings for the exercise exist. + * + * @param exercise The exercise to get the default Iris settings for + * @return The default Iris settings for the exercise + */ + public IrisExerciseSettings getDefaultSettingsFor(Exercise exercise) { + var settings = new IrisExerciseSettings(); + settings.setExercise(exercise); + settings.setIrisChatSettings(new IrisChatSubSettings()); + return settings; + } + + /** + * Get the raw (uncombined) Iris settings for a course. + * If no Iris settings for the course exist, the default settings are returned. + * + * @param course The course to get the Iris settings for + * @return The raw Iris settings for the course + */ + public IrisCourseSettings getRawIrisSettingsFor(Course course) { + return irisSettingsRepository.findCourseSettings(course.getId()).orElse(getDefaultSettingsFor(course)); + } + + /** + * Get the raw (uncombined) Iris settings for an exercise. + * If no Iris settings for the exercise exist, the default settings are returned. + * + * @param exercise The exercise to get the Iris settings for + * @return The raw Iris settings for the exercise + */ + public IrisExerciseSettings getRawIrisSettingsFor(Exercise exercise) { + return irisSettingsRepository.findExerciseSettings(exercise.getId()).orElse(getDefaultSettingsFor(exercise)); + } + + /** + * Delete the Iris settings for a course. + * If no Iris settings for the course exist, nothing happens. + * + * @param course The course to delete the Iris settings for + */ + public void deleteSettingsFor(Course course) { + var irisCourseSettingsOptional = irisSettingsRepository.findCourseSettings(course.getId()); + irisCourseSettingsOptional.ifPresent(irisSettingsRepository::delete); + } + + /** + * Delete the Iris settings for an exercise. + * If no Iris settings for the exercise exist, nothing happens. + * + * @param exercise The course to delete the Iris settings for + */ + public void deleteSettingsFor(Exercise exercise) { + var irisExerciseSettingsOptional = irisSettingsRepository.findExerciseSettings(exercise.getId()); + irisExerciseSettingsOptional.ifPresent(irisSettingsRepository::delete); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/iris/settings/IrisSubSettingsService.java b/src/main/java/de/tum/in/www1/artemis/service/iris/settings/IrisSubSettingsService.java new file mode 100644 index 000000000000..2641c8eb34f6 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/iris/settings/IrisSubSettingsService.java @@ -0,0 +1,330 @@ +package de.tum.in.www1.artemis.service.iris.settings; + +import java.util.*; +import java.util.function.Function; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.in.www1.artemis.domain.iris.IrisTemplate; +import de.tum.in.www1.artemis.domain.iris.settings.*; +import de.tum.in.www1.artemis.service.AuthorizationCheckService; +import de.tum.in.www1.artemis.service.dto.iris.IrisCombinedChatSubSettingsDTO; +import de.tum.in.www1.artemis.service.dto.iris.IrisCombinedCodeEditorSubSettingsDTO; +import de.tum.in.www1.artemis.service.dto.iris.IrisCombinedHestiaSubSettingsDTO; + +/** + * Service for handling {@link IrisSubSettings} objects. + * This server provides methods to update and combine sub settings objects. + * See {@link IrisSettingsService} for more information about handling {@link IrisSettings}. + */ +@Service +@Profile("iris") +public class IrisSubSettingsService { + + private final AuthorizationCheckService authCheckService; + + public IrisSubSettingsService(AuthorizationCheckService authCheckService) { + this.authCheckService = authCheckService; + } + + /** + * Updates a chat sub settings object. + * If the new settings are null, the current settings will be deleted (except if the parent settings are null == if the settings are global). + * Special notes: + * - If the user is not an admin the rate limit will not be updated. + * - If the user is not an admin the allowed models will not be updated. + * - If the user is not an admin the preferred model will only be updated if it is included in the allowed models. + * + * @param currentSettings Current chat sub settings. + * @param newSettings Updated chat sub settings. + * @param parentSettings Parent chat sub settings. + * @param settingsType Type of the settings the sub settings belong to. + * @return Updated chat sub settings. + */ + public IrisChatSubSettings update(IrisChatSubSettings currentSettings, IrisChatSubSettings newSettings, IrisCombinedChatSubSettingsDTO parentSettings, + IrisSettingsType settingsType) { + if (newSettings == null) { + if (parentSettings == null) { + throw new IllegalArgumentException("Cannot delete the chat settings"); + } + return null; + } + if (currentSettings == null) { + currentSettings = new IrisChatSubSettings(); + } + if (settingsType == IrisSettingsType.EXERCISE || authCheckService.isAdmin()) { + currentSettings.setEnabled(newSettings.isEnabled()); + } + if (authCheckService.isAdmin()) { + currentSettings.setRateLimit(newSettings.getRateLimit()); + currentSettings.setRateLimitTimeframeHours(newSettings.getRateLimitTimeframeHours()); + } + currentSettings.setAllowedModels(selectAllowedModels(currentSettings.getAllowedModels(), newSettings.getAllowedModels())); + currentSettings.setPreferredModel(validatePreferredModel(currentSettings.getPreferredModel(), newSettings.getPreferredModel(), currentSettings.getAllowedModels(), + parentSettings != null ? parentSettings.getAllowedModels() : null)); + currentSettings.setTemplate(newSettings.getTemplate()); + return currentSettings; + } + + /** + * Updates a Hestia sub settings object. + * If the new settings are null, the current settings will be deleted (except if the parent settings are null == if the settings are global). + * Special notes: + * - If the user is not an admin the allowed models will not be updated. + * - If the user is not an admin the preferred model will only be updated if it is included in the allowed models. + * + * @param currentSettings Current Hestia sub settings. + * @param newSettings Updated Hestia sub settings. + * @param parentSettings Parent Hestia sub settings. + * @param settingsType Type of the settings the sub settings belong to. + * @return Updated Hestia sub settings. + */ + public IrisHestiaSubSettings update(IrisHestiaSubSettings currentSettings, IrisHestiaSubSettings newSettings, IrisCombinedHestiaSubSettingsDTO parentSettings, + IrisSettingsType settingsType) { + if (newSettings == null) { + if (parentSettings == null) { + throw new IllegalArgumentException("Cannot delete the Hestia settings"); + } + return null; + } + if (currentSettings == null) { + currentSettings = new IrisHestiaSubSettings(); + } + if (settingsType == IrisSettingsType.EXERCISE || authCheckService.isAdmin()) { + currentSettings.setEnabled(newSettings.isEnabled()); + } + currentSettings.setAllowedModels(selectAllowedModels(currentSettings.getAllowedModels(), newSettings.getAllowedModels())); + currentSettings.setPreferredModel(validatePreferredModel(currentSettings.getPreferredModel(), newSettings.getPreferredModel(), currentSettings.getAllowedModels(), + parentSettings != null ? parentSettings.getAllowedModels() : null)); + currentSettings.setTemplate(newSettings.getTemplate()); + return currentSettings; + } + + /** + * Updates a Code Editor sub settings object. + * If the new settings are null, the current settings will be deleted (except if the parent settings are null == if the settings are global). + * Special notes: + * - If the user is not an admin the allowed models will not be updated. + * - If the user is not an admin the preferred model will only be updated if it is included in the allowed models. + * + * @param currentSettings Current Code Editor sub settings. + * @param newSettings Updated Code Editor sub settings. + * @param parentSettings Parent Code Editor sub settings. + * @param settingsType Type of the settings the sub settings belong to. + * @return Updated Code Editor sub settings. + */ + public IrisCodeEditorSubSettings update(IrisCodeEditorSubSettings currentSettings, IrisCodeEditorSubSettings newSettings, IrisCombinedCodeEditorSubSettingsDTO parentSettings, + IrisSettingsType settingsType) { + if (newSettings == null) { + if (parentSettings == null) { + throw new IllegalArgumentException("Cannot delete the Code Editor settings"); + } + return null; + } + if (currentSettings == null) { + currentSettings = new IrisCodeEditorSubSettings(); + } + if (settingsType == IrisSettingsType.EXERCISE || authCheckService.isAdmin()) { + currentSettings.setEnabled(newSettings.isEnabled()); + } + currentSettings.setAllowedModels(selectAllowedModels(currentSettings.getAllowedModels(), newSettings.getAllowedModels())); + currentSettings.setPreferredModel(validatePreferredModel(currentSettings.getPreferredModel(), newSettings.getPreferredModel(), currentSettings.getAllowedModels(), + parentSettings != null ? parentSettings.getAllowedModels() : null)); + currentSettings.setChatTemplate(newSettings.getChatTemplate()); + currentSettings.setProblemStatementGenerationTemplate(newSettings.getProblemStatementGenerationTemplate()); + currentSettings.setTemplateRepoGenerationTemplate(newSettings.getTemplateRepoGenerationTemplate()); + currentSettings.setSolutionRepoGenerationTemplate(newSettings.getSolutionRepoGenerationTemplate()); + currentSettings.setTestRepoGenerationTemplate(newSettings.getTestRepoGenerationTemplate()); + return currentSettings; + } + + /** + * Filters the allowed models of a sub settings object. + * If the user is an admin, all models are allowed. + * Otherwise, only models that are allowed by the parent settings or the current settings are allowed. + * + * @param allowedModels The allowed models of the current settings. + * @param updatedAllowedModels The allowed models of the updated settings. + * @return The filtered allowed models. + */ + private Set selectAllowedModels(Set allowedModels, Set updatedAllowedModels) { + return authCheckService.isAdmin() ? updatedAllowedModels : allowedModels; + } + + /** + * Validates the preferred model of a sub settings object. + * If the user is an admin, all models are allowed. + * Otherwise, only models that are allowed by the current settings are allowed. + * + * @param preferredModel The preferred model of the current settings. + * @param newPreferredModel The preferred model of the updated settings. + * @param allowedModels The allowed models of the current settings. + * @param parentAllowedModels The allowed models of the parent settings. + * @return The validated preferred model. + */ + private String validatePreferredModel(String preferredModel, String newPreferredModel, Set allowedModels, Set parentAllowedModels) { + if (newPreferredModel == null || newPreferredModel.isBlank()) { + return null; + } + else if (authCheckService.isAdmin()) { + return newPreferredModel; + } + else if (allowedModels != null && !allowedModels.isEmpty() && allowedModels.contains(newPreferredModel)) { + return newPreferredModel; + } + else if ((allowedModels == null || allowedModels.isEmpty()) && parentAllowedModels != null && parentAllowedModels.contains(newPreferredModel)) { + return newPreferredModel; + } + else { + return preferredModel; + } + } + + /** + * Combines the chat settings of multiple {@link IrisSettings} objects. + * If minimal is true, the returned object will only contain the enabled and rateLimit fields. + * The minimal version can safely be sent to students. + * + * @param settingsList List of {@link IrisSettings} objects to combine. + * @param minimal Whether to return a minimal version of the combined settings. + * @return Combined chat settings. + */ + public IrisCombinedChatSubSettingsDTO combineChatSettings(ArrayList settingsList, boolean minimal) { + var combinedChatSettings = new IrisCombinedChatSubSettingsDTO(); + combinedChatSettings.setEnabled(getCombinedEnabled(settingsList, IrisSettings::getIrisChatSettings)); + combinedChatSettings.setRateLimit(getCombinedRateLimit(settingsList)); + if (!minimal) { + combinedChatSettings.setAllowedModels(getCombinedAllowedModels(settingsList, IrisSettings::getIrisChatSettings)); + combinedChatSettings.setPreferredModel(getCombinedPreferredModel(settingsList, IrisSettings::getIrisChatSettings)); + combinedChatSettings.setTemplate(getCombinedTemplate(settingsList, IrisSettings::getIrisChatSettings, IrisChatSubSettings::getTemplate)); + } + return combinedChatSettings; + } + + /** + * Combines the Hestia settings of multiple {@link IrisSettings} objects. + * If minimal is true, the returned object will only contain the enabled field. + * The minimal version can safely be sent to students. + * + * @param settingsList List of {@link IrisSettings} objects to combine. + * @param minimal Whether to return a minimal version of the combined settings. + * @return Combined Hestia settings. + */ + public IrisCombinedHestiaSubSettingsDTO combineHestiaSettings(ArrayList settingsList, boolean minimal) { + var actualSettingsList = settingsList.stream().filter(settings -> !(settings instanceof IrisExerciseSettings)).toList(); + var combinedHestiaSettings = new IrisCombinedHestiaSubSettingsDTO(); + combinedHestiaSettings.setEnabled(getCombinedEnabled(actualSettingsList, IrisSettings::getIrisHestiaSettings)); + if (!minimal) { + combinedHestiaSettings.setAllowedModels(getCombinedAllowedModels(actualSettingsList, IrisSettings::getIrisHestiaSettings)); + combinedHestiaSettings.setPreferredModel(getCombinedPreferredModel(actualSettingsList, IrisSettings::getIrisHestiaSettings)); + combinedHestiaSettings.setTemplate(getCombinedTemplate(actualSettingsList, IrisSettings::getIrisHestiaSettings, IrisHestiaSubSettings::getTemplate)); + } + return combinedHestiaSettings; + } + + /** + * Combines the Code Editor settings of multiple {@link IrisSettings} objects. + * If minimal is true, the returned object will only contain the enabled field. + * The minimal version can safely be sent to students. + * + * @param settingsList List of {@link IrisSettings} objects to combine. + * @param minimal Whether to return a minimal version of the combined settings. + * @return Combined Code Editor settings. + */ + public IrisCombinedCodeEditorSubSettingsDTO combineCodeEditorSettings(ArrayList settingsList, boolean minimal) { + var actualSettingsList = settingsList.stream().filter(settings -> !(settings instanceof IrisExerciseSettings)).toList(); + var combinedCodeEditorSettings = new IrisCombinedCodeEditorSubSettingsDTO(); + combinedCodeEditorSettings.setEnabled(getCombinedEnabled(actualSettingsList, IrisSettings::getIrisHestiaSettings)); + if (!minimal) { + combinedCodeEditorSettings.setAllowedModels(getCombinedAllowedModels(actualSettingsList, IrisSettings::getIrisHestiaSettings)); + combinedCodeEditorSettings.setPreferredModel(getCombinedPreferredModel(actualSettingsList, IrisSettings::getIrisHestiaSettings)); + + combinedCodeEditorSettings + .setChatTemplate(getCombinedTemplate(actualSettingsList, IrisSettings::getIrisCodeEditorSettings, IrisCodeEditorSubSettings::getChatTemplate)); + combinedCodeEditorSettings.setProblemStatementGenerationTemplate( + getCombinedTemplate(actualSettingsList, IrisSettings::getIrisCodeEditorSettings, IrisCodeEditorSubSettings::getProblemStatementGenerationTemplate)); + combinedCodeEditorSettings.setTemplateRepoGenerationTemplate( + getCombinedTemplate(actualSettingsList, IrisSettings::getIrisCodeEditorSettings, IrisCodeEditorSubSettings::getTemplateRepoGenerationTemplate)); + combinedCodeEditorSettings.setSolutionRepoGenerationTemplate( + getCombinedTemplate(actualSettingsList, IrisSettings::getIrisCodeEditorSettings, IrisCodeEditorSubSettings::getSolutionRepoGenerationTemplate)); + combinedCodeEditorSettings.setTestRepoGenerationTemplate( + getCombinedTemplate(actualSettingsList, IrisSettings::getIrisCodeEditorSettings, IrisCodeEditorSubSettings::getTestRepoGenerationTemplate)); + } + return combinedCodeEditorSettings; + } + + /** + * Combines the enabled field of multiple {@link IrisSettings} objects. + * Simply &&s all enabled fields together. + * + * @param settingsList List of {@link IrisSettings} objects to combine. + * @param subSettingsFunction Function to get the sub settings from an IrisSettings object. + * @return Combined enabled field. + */ + private boolean getCombinedEnabled(List settingsList, Function subSettingsFunction) { + for (var irisSettings : settingsList) { + if (irisSettings == null) { + return false; + } + var settings = subSettingsFunction.apply(irisSettings); + if (settings == null || !settings.isEnabled()) { + return false; + } + } + return true; + } + + /** + * Combines the rateLimit field of multiple {@link IrisSettings} objects. + * Simply takes the minimum rateLimit. + * + * @param settingsList List of {@link IrisSettings} objects to combine. + * @return Combined rateLimit field. + */ + private Integer getCombinedRateLimit(List settingsList) { + return settingsList.stream().filter(Objects::nonNull).map(IrisSettings::getIrisChatSettings).filter(Objects::nonNull).map(IrisChatSubSettings::getRateLimit) + .filter(rateLimit -> rateLimit != null && rateLimit >= 0).min(Comparator.comparingInt(Integer::intValue)).orElse(null); + } + + /** + * Combines the allowedModels field of multiple {@link IrisSettings} objects. + * Simply takes the last allowedModels. + * + * @param settingsList List of {@link IrisSettings} objects to combine. + * @param subSettingsFunction Function to get the sub settings from an IrisSettings object. + * @return Combined allowedModels field. + */ + private Set getCombinedAllowedModels(List settingsList, Function subSettingsFunction) { + return settingsList.stream().filter(Objects::nonNull).map(subSettingsFunction).filter(Objects::nonNull).map(IrisSubSettings::getAllowedModels).filter(Objects::nonNull) + .filter(models -> !models.isEmpty()).reduce((first, second) -> second).orElse(new TreeSet<>()); + } + + /** + * Combines the preferredModel field of multiple {@link IrisSettings} objects. + * Simply takes the last preferredModel. + * TODO + * + * @param settingsList List of {@link IrisSettings} objects to combine. + * @param subSettingsFunction Function to get the sub settings from an IrisSettings object. + * @return Combined preferredModel field. + */ + private String getCombinedPreferredModel(List settingsList, Function subSettingsFunction) { + return settingsList.stream().filter(Objects::nonNull).map(subSettingsFunction).filter(Objects::nonNull).map(IrisSubSettings::getPreferredModel) + .filter(model -> model != null && !model.isBlank()).reduce((first, second) -> second).orElse(null); + } + + /** + * Combines the template field of multiple {@link IrisSettings} objects. + * Simply takes the last template. + * + * @param settingsList List of {@link IrisSettings} objects to combine. + * @param templateFunction Function to get the template from the sub settings from an IrisSettings object. + * @return Combined template field. + */ + private IrisTemplate getCombinedTemplate(List settingsList, Function subSettingsFunction, + Function templateFunction) { + return settingsList.stream().filter(Objects::nonNull).map(subSettingsFunction).filter(Objects::nonNull).map(templateFunction) + .filter(template -> template != null && template.getContent() != null && !template.getContent().isBlank()).reduce((first, second) -> second).orElse(null); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java index 80445f0e36ed..9da4f181ffe5 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/programming/ProgrammingExerciseService.java @@ -44,7 +44,7 @@ import de.tum.in.www1.artemis.service.connectors.ci.ContinuousIntegrationTriggerService; import de.tum.in.www1.artemis.service.connectors.vcs.VersionControlService; import de.tum.in.www1.artemis.service.hestia.ProgrammingExerciseTaskService; -import de.tum.in.www1.artemis.service.iris.IrisSettingsService; +import de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService; import de.tum.in.www1.artemis.service.messaging.InstanceMessageSendService; import de.tum.in.www1.artemis.service.metis.conversation.ChannelService; import de.tum.in.www1.artemis.service.notifications.GroupNotificationScheduleService; @@ -131,7 +131,7 @@ public class ProgrammingExerciseService { private final ProgrammingSubmissionService programmingSubmissionService; - private final IrisSettingsService irisSettingsService; + private final Optional irisSettingsService; public ProgrammingExerciseService(ProgrammingExerciseRepository programmingExerciseRepository, GitService gitService, Optional versionControlService, Optional continuousIntegrationService, Optional continuousIntegrationTriggerService, @@ -144,7 +144,7 @@ public ProgrammingExerciseService(ProgrammingExerciseRepository programmingExerc ProgrammingExerciseGitDiffReportRepository programmingExerciseGitDiffReportRepository, ExerciseSpecificationService exerciseSpecificationService, ProgrammingExerciseRepositoryService programmingExerciseRepositoryService, AuxiliaryRepositoryService auxiliaryRepositoryService, SubmissionPolicyService submissionPolicyService, Optional programmingLanguageFeatureService, ChannelService channelService, - ProgrammingSubmissionService programmingSubmissionService, IrisSettingsService irisSettingsService) { + ProgrammingSubmissionService programmingSubmissionService, Optional irisSettingsService) { this.programmingExerciseRepository = programmingExerciseRepository; this.gitService = gitService; this.versionControlService = versionControlService; @@ -450,7 +450,6 @@ public ProgrammingExercise updateProgrammingExercise(ProgrammingExercise program connectAuxiliaryRepositoriesToExercise(updatedProgrammingExercise); channelService.updateExerciseChannel(programmingExerciseBeforeUpdate, updatedProgrammingExercise); - irisSettingsService.updateIrisSettings(programmingExerciseBeforeUpdate, updatedProgrammingExercise); String problemStatementWithTestNames = updatedProgrammingExercise.getProblemStatement(); programmingExerciseTaskService.replaceTestNamesWithIds(updatedProgrammingExercise); @@ -628,7 +627,7 @@ private boolean saveAndPushStructuralOracle(User user, Repository testRepository public void delete(Long programmingExerciseId, boolean deleteBaseReposBuildPlans) { // Note: This method does not accept a programming exercise to solve issues with nested Transactions. // It would be good to refactor the delete calls and move the validity checks down from the resources to the service methods (e.g. EntityNotFound). - var programmingExercise = programmingExerciseRepository.findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesById(programmingExerciseId) + final var programmingExercise = programmingExerciseRepository.findWithTemplateAndSolutionParticipationTeamAssignmentConfigCategoriesById(programmingExerciseId) .orElseThrow(() -> new EntityNotFoundException("Programming Exercise", programmingExerciseId)); // The delete operation cancels scheduled tasks (like locking/unlocking repositories) @@ -644,6 +643,8 @@ public void delete(Long programmingExerciseId, boolean deleteBaseReposBuildPlans programmingExerciseGitDiffReportRepository.deleteByProgrammingExerciseId(programmingExerciseId); + irisSettingsService.ifPresent(iss -> iss.deleteSettingsFor(programmingExercise)); + SolutionProgrammingExerciseParticipation solutionProgrammingExerciseParticipation = programmingExercise.getSolutionParticipation(); TemplateProgrammingExerciseParticipation templateProgrammingExerciseParticipation = programmingExercise.getTemplateParticipation(); if (solutionProgrammingExerciseParticipation != null) { @@ -654,8 +655,8 @@ public void delete(Long programmingExerciseId, boolean deleteBaseReposBuildPlans } // Note: we fetch the programming exercise again here with student participations to avoid Hibernate issues during the delete operation below - programmingExercise = programmingExerciseRepository.findByIdWithStudentParticipationsAndLegalSubmissionsElseThrow(programmingExerciseId); - log.debug("Delete programming exercises with student participations: {}", programmingExercise.getStudentParticipations()); + var programmingExerciseWithStudentParticipations = programmingExerciseRepository.findByIdWithStudentParticipationsAndLegalSubmissionsElseThrow(programmingExerciseId); + log.debug("Delete programming exercises with student participations: {}", programmingExerciseWithStudentParticipations.getStudentParticipations()); // This will also delete the template & solution participation: we explicitly use deleteById to avoid potential Hibernate issues during deletion programmingExerciseRepository.deleteById(programmingExerciseId); } 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 679dd22a400a..0656da5c957e 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 @@ -215,7 +215,6 @@ public ResponseEntity updateCourse(@PathVariable Long courseId, @Request courseUpdate.setPrerequisites(existingCourse.getPrerequisites()); courseUpdate.setTutorialGroupsConfiguration(existingCourse.getTutorialGroupsConfiguration()); courseUpdate.setOnlineCourseConfiguration(existingCourse.getOnlineCourseConfiguration()); - courseUpdate.setIrisSettings(existingCourse.getIrisSettings()); courseUpdate.validateEnrollmentConfirmationMessage(); courseUpdate.validateComplaintsAndRequestMoreFeedbackConfig(); diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/admin/iris/AdminIrisSettingsResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/admin/iris/AdminIrisSettingsResource.java index 19048d1ef407..dacf5c65b3fb 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/admin/iris/AdminIrisSettingsResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/admin/iris/AdminIrisSettingsResource.java @@ -1,5 +1,6 @@ package de.tum.in.www1.artemis.web.rest.admin.iris; +import org.springframework.context.annotation.Profile; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -8,11 +9,12 @@ import de.tum.in.www1.artemis.domain.iris.settings.IrisSettings; import de.tum.in.www1.artemis.security.annotations.EnforceAdmin; -import de.tum.in.www1.artemis.service.iris.IrisSettingsService; +import de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService; /** * REST controller for managing {@link IrisSettings}. */ +@Profile("iris") @RestController @RequestMapping("api/admin/") public class AdminIrisSettingsResource { @@ -32,7 +34,7 @@ public AdminIrisSettingsResource(IrisSettingsService irisSettingsService) { @PutMapping("iris/global-iris-settings") @EnforceAdmin public ResponseEntity updateGlobalSettings(@RequestBody IrisSettings settings) { - var updatedSettings = irisSettingsService.saveGlobalIrisSettings(settings); + var updatedSettings = irisSettingsService.saveIrisSettings(settings); return ResponseEntity.ok(updatedSettings); } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/hestia/CodeHintResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/hestia/CodeHintResource.java index d7c248195cfc..db69ff5120ef 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/hestia/CodeHintResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/hestia/CodeHintResource.java @@ -1,16 +1,16 @@ package de.tum.in.www1.artemis.web.rest.hestia; -import java.util.List; -import java.util.Objects; -import java.util.Set; +import java.util.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.hestia.CodeHint; +import de.tum.in.www1.artemis.domain.iris.settings.IrisSubSettingsType; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.hestia.CodeHintRepository; import de.tum.in.www1.artemis.repository.hestia.ProgrammingExerciseSolutionEntryRepository; @@ -18,7 +18,7 @@ import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastEditor; import de.tum.in.www1.artemis.service.AuthorizationCheckService; import de.tum.in.www1.artemis.service.hestia.CodeHintService; -import de.tum.in.www1.artemis.service.iris.IrisSettingsService; +import de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService; import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException; import de.tum.in.www1.artemis.web.rest.errors.ConflictException; @@ -41,11 +41,11 @@ public class CodeHintResource { private final CodeHintService codeHintService; - private final IrisSettingsService irisSettingsService; + private final Optional irisSettingsService; public CodeHintResource(AuthorizationCheckService authCheckService, ProgrammingExerciseRepository programmingExerciseRepository, ProgrammingExerciseSolutionEntryRepository solutionEntryRepository, CodeHintRepository codeHintRepository, CodeHintService codeHintService, - IrisSettingsService irisSettingsService) { + Optional irisSettingsService) { this.programmingExerciseRepository = programmingExerciseRepository; this.authCheckService = authCheckService; this.solutionEntryRepository = solutionEntryRepository; @@ -102,6 +102,7 @@ public ResponseEntity> generateCodeHintsForExercise(@PathVariable * @param codeHintId The id of the code hint * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the updated code hint */ + @Profile("iris") @PostMapping("programming-exercises/{exerciseId}/code-hints/{codeHintId}/generate-description") @EnforceAtLeastEditor public ResponseEntity generateDescriptionForCodeHint(@PathVariable Long exerciseId, @PathVariable Long codeHintId) { @@ -109,7 +110,7 @@ public ResponseEntity generateDescriptionForCodeHint(@PathVariable Lon ProgrammingExercise exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, exercise, null); - irisSettingsService.checkIsIrisHestiaSessionEnabledElseThrow(exercise); + irisSettingsService.orElseThrow().isEnabledForElseThrow(IrisSubSettingsType.HESTIA, exercise); // Hints for exam exercises are not supported at the moment if (exercise.isExamExercise()) { diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisSessionResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisSessionResource.java index 0d35f8481fd0..0f4b420aaa7f 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisSessionResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisSessionResource.java @@ -11,6 +11,7 @@ import de.tum.in.www1.artemis.domain.ProgrammingExercise; import de.tum.in.www1.artemis.domain.iris.session.IrisSession; +import de.tum.in.www1.artemis.domain.iris.settings.IrisSubSettingsType; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.repository.iris.IrisChatSessionRepository; @@ -21,7 +22,7 @@ import de.tum.in.www1.artemis.service.connectors.iris.dto.IrisStatusDTO; import de.tum.in.www1.artemis.service.iris.IrisRateLimitService; import de.tum.in.www1.artemis.service.iris.IrisSessionService; -import de.tum.in.www1.artemis.service.iris.IrisSettingsService; +import de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService; /** * REST controller for managing {@link IrisSession}. @@ -70,7 +71,7 @@ public IrisSessionResource(ProgrammingExerciseRepository programmingExerciseRepo @EnforceAtLeastStudent public ResponseEntity getCurrentSession(@PathVariable Long exerciseId) { ProgrammingExercise exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); - irisSettingsService.checkIsIrisChatSessionEnabledElseThrow(exercise); + irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.CHAT, exercise); var user = userRepository.getUserWithGroupsAndAuthorities(); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.STUDENT, exercise, user); @@ -89,7 +90,7 @@ public ResponseEntity getCurrentSession(@PathVariable Long exercise @EnforceAtLeastStudent public ResponseEntity> getAllSessions(@PathVariable Long exerciseId) { ProgrammingExercise exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); - irisSettingsService.checkIsIrisChatSessionEnabledElseThrow(exercise); + irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.CHAT, exercise); var user = userRepository.getUserWithGroupsAndAuthorities(); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.STUDENT, exercise, user); @@ -110,7 +111,7 @@ public ResponseEntity> getAllSessions(@PathVariable Long exerc @EnforceAtLeastStudent public ResponseEntity createSessionForProgrammingExercise(@PathVariable Long exerciseId) throws URISyntaxException { ProgrammingExercise exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); - irisSettingsService.checkIsIrisChatSessionEnabledElseThrow(exercise); + irisSettingsService.isEnabledForElseThrow(IrisSubSettingsType.CHAT, exercise); var user = userRepository.getUserWithGroupsAndAuthorities(); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.STUDENT, exercise, user); @@ -134,12 +135,12 @@ public ResponseEntity isIrisActive(@PathVariable Long sessionId) var user = userRepository.getUser(); irisSessionService.checkHasAccessToIrisSession(session, user); irisSessionService.checkIsIrisActivated(session); - var settings = irisSettingsService.getCombinedIrisSettings(session.getExercise(), false); + var settings = irisSettingsService.getCombinedIrisSettingsFor(session.getExercise(), false); var health = irisHealthIndicator.health(); IrisStatusDTO[] modelStatuses = (IrisStatusDTO[]) health.getDetails().get("modelStatuses"); var specificModelStatus = false; if (modelStatuses != null) { - specificModelStatus = Arrays.stream(modelStatuses).filter(x -> x.model().equals(settings.getIrisChatSettings().getPreferredModel())) + specificModelStatus = Arrays.stream(modelStatuses).filter(x -> x.model().equals(settings.irisChatSettings().getPreferredModel())) .anyMatch(x -> x.status() == IrisStatusDTO.ModelStatus.UP); } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisSettingsResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisSettingsResource.java index f5692f388a3c..ff3460c2df41 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisSettingsResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/iris/IrisSettingsResource.java @@ -1,8 +1,11 @@ package de.tum.in.www1.artemis.web.rest.iris; +import org.springframework.context.annotation.Profile; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import de.tum.in.www1.artemis.domain.iris.settings.IrisCourseSettings; +import de.tum.in.www1.artemis.domain.iris.settings.IrisExerciseSettings; import de.tum.in.www1.artemis.domain.iris.settings.IrisSettings; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; @@ -10,11 +13,13 @@ import de.tum.in.www1.artemis.security.Role; import de.tum.in.www1.artemis.security.annotations.*; import de.tum.in.www1.artemis.service.AuthorizationCheckService; -import de.tum.in.www1.artemis.service.iris.IrisSettingsService; +import de.tum.in.www1.artemis.service.dto.iris.IrisCombinedSettingsDTO; +import de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService; /** * REST controller for managing {@link IrisSettings}. */ +@Profile("iris") @RestController @RequestMapping("api/") public class IrisSettingsResource { @@ -61,7 +66,7 @@ public ResponseEntity getGlobalSettings() { public ResponseEntity getRawCourseSettings(@PathVariable Long courseId) { var course = courseRepository.findByIdElseThrow(courseId); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, null); - var irisSettings = irisSettingsService.getIrisSettingsOrDefault(course); + var irisSettings = irisSettingsService.getRawIrisSettingsFor(course); return ResponseEntity.ok(irisSettings); } @@ -78,7 +83,7 @@ public ResponseEntity getRawProgrammingExerciseSettings(@PathVaria var user = userRepository.getUserWithGroupsAndAuthorities(); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.STUDENT, exercise, user); - var combinedIrisSettings = irisSettingsService.getIrisSettingsOrDefault(exercise); + var combinedIrisSettings = irisSettingsService.getRawIrisSettingsFor(exercise); return ResponseEntity.ok(combinedIrisSettings); } @@ -90,14 +95,14 @@ public ResponseEntity getRawProgrammingExerciseSettings(@PathVaria */ @GetMapping("courses/{courseId}/iris-settings") @EnforceAtLeastStudent - public ResponseEntity getCourseSettings(@PathVariable Long courseId) { + public ResponseEntity getCourseSettings(@PathVariable Long courseId) { var course = courseRepository.findByIdElseThrow(courseId); var user = userRepository.getUserWithGroupsAndAuthorities(); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.STUDENT, course, user); // Editors can see the full settings, students only the reduced settings var getReduced = !authCheckService.isAtLeastEditorInCourse(course, user); - var irisSettings = irisSettingsService.getCombinedIrisSettings(course, getReduced); + var irisSettings = irisSettingsService.getCombinedIrisSettingsFor(course, getReduced); return ResponseEntity.ok(irisSettings); } @@ -109,14 +114,14 @@ public ResponseEntity getCourseSettings(@PathVariable Long courseI */ @GetMapping("programming-exercises/{exerciseId}/iris-settings") @EnforceAtLeastStudent - public ResponseEntity getProgrammingExerciseSettings(@PathVariable Long exerciseId) { + public ResponseEntity getProgrammingExerciseSettings(@PathVariable Long exerciseId) { var exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); var user = userRepository.getUserWithGroupsAndAuthorities(); authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.STUDENT, exercise, user); // Editors can see the full settings, students only the reduced settings var getReduced = !authCheckService.isAtLeastEditorForExercise(exercise, user); - var combinedIrisSettings = irisSettingsService.getCombinedIrisSettings(exercise, getReduced); + var combinedIrisSettings = irisSettingsService.getCombinedIrisSettingsFor(exercise, getReduced); return ResponseEntity.ok(combinedIrisSettings); } @@ -129,10 +134,11 @@ public ResponseEntity getProgrammingExerciseSettings(@PathVariable */ @PutMapping("courses/{courseId}/raw-iris-settings") @EnforceAtLeastEditor - public ResponseEntity updateCourseSettings(@PathVariable Long courseId, @RequestBody IrisSettings settings) { + public ResponseEntity updateCourseSettings(@PathVariable Long courseId, @RequestBody IrisCourseSettings settings) { var course = courseRepository.findByIdElseThrow(courseId); authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.EDITOR, course, null); - var updatedSettings = irisSettingsService.saveIrisSettings(course, settings); + settings.setCourse(course); + var updatedSettings = irisSettingsService.saveIrisSettings(settings); return ResponseEntity.ok(updatedSettings); } @@ -145,12 +151,13 @@ public ResponseEntity updateCourseSettings(@PathVariable Long cour * found. */ @PutMapping("programming-exercises/{exerciseId}/raw-iris-settings") - @EnforceAtLeastEditor - public ResponseEntity updateProgrammingExerciseSettings(@PathVariable Long exerciseId, @RequestBody IrisSettings settings) { + @EnforceAtLeastInstructor + public ResponseEntity updateProgrammingExerciseSettings(@PathVariable Long exerciseId, @RequestBody IrisExerciseSettings settings) { var exercise = programmingExerciseRepository.findByIdElseThrow(exerciseId); var user = userRepository.getUserWithGroupsAndAuthorities(); - authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.EDITOR, exercise, user); - var updatedSettings = irisSettingsService.saveIrisSettings(exercise, settings); + authCheckService.checkHasAtLeastRoleForExerciseElseThrow(Role.INSTRUCTOR, exercise, user); + settings.setExercise(exercise); + var updatedSettings = irisSettingsService.saveIrisSettings(settings); return ResponseEntity.ok(updatedSettings); } } diff --git a/src/main/resources/config/liquibase/changelog/20231019191919_changelog.xml b/src/main/resources/config/liquibase/changelog/20231019191919_changelog.xml new file mode 100644 index 000000000000..5eae63266554 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20231019191919_changelog.xml @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + UPDATE iris_settings SET discriminator = 'GLOBAL' WHERE is_global = TRUE; + UPDATE iris_settings SET discriminator = 'COURSE' WHERE id IN (SELECT iris_settings_id FROM course WHERE iris_settings_id IS NOT NULL); + UPDATE iris_settings SET discriminator = 'EXERCISE' WHERE id IN (SELECT iris_settings_id FROM programming_exercise_details WHERE iris_settings_id IS NOT NULL); + + UPDATE iris_sub_settings SET discriminator = 'CHAT' WHERE id IN (SELECT iris_chat_settings_id FROM iris_settings WHERE iris_chat_settings_id IS NOT NULL); + UPDATE iris_sub_settings SET discriminator = 'HESTIA' WHERE id IN (SELECT iris_hestia_settings_id FROM iris_settings WHERE iris_hestia_settings_id IS NOT NULL); + + UPDATE iris_settings SET current_version = 0 WHERE discriminator = 'GLOBAL'; + UPDATE iris_settings SET enable_auto_update_chat = FALSE WHERE discriminator = 'GLOBAL'; + UPDATE iris_settings SET enable_auto_update_hestia = FALSE WHERE discriminator = 'GLOBAL'; + UPDATE iris_settings SET enable_auto_update_code_editor = FALSE WHERE discriminator = 'GLOBAL'; + + + + + + + + + + + + UPDATE iris_settings + JOIN ( + SELECT id, iris_settings_id FROM course WHERE iris_settings_id IS NOT NULL + ) AS course_data + ON iris_settings.id = course_data.iris_settings_id + SET iris_settings.course_id = course_data.id; + + + UPDATE iris_settings + JOIN ( + SELECT id, iris_settings_id FROM programming_exercise_details WHERE iris_settings_id IS NOT NULL + ) AS exercise_data + ON iris_settings.id = exercise_data.iris_settings_id + SET iris_settings.exercise_id = exercise_data.id; + + + + + + + + + + + + UPDATE iris_settings SET course_id = course_data.id FROM (SELECT id, iris_settings_id FROM course WHERE iris_settings_id IS NOT NULL) AS course_data WHERE iris_settings.id = course_data.iris_settings_id; + + + UPDATE iris_settings SET exercise_id = exercise_data.id FROM (SELECT id, iris_settings_id FROM programming_exercise_details WHERE iris_settings_id IS NOT NULL) AS exercise_data WHERE iris_settings.id = exercise_data.iris_settings_id; + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index fe06f7a8d481..34065e21143b 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -65,6 +65,7 @@ + diff --git a/src/main/webapp/app/admin/admin.route.ts b/src/main/webapp/app/admin/admin.route.ts index 59bdee6d265e..6979fa553b82 100644 --- a/src/main/webapp/app/admin/admin.route.ts +++ b/src/main/webapp/app/admin/admin.route.ts @@ -89,7 +89,10 @@ export const adminState: Routes = [ }, { path: 'iris', - loadChildren: () => import('../iris/settings/iris-settings-update-routing.module').then((module) => module.IrisSettingsUpdateRoutingModule), + loadChildren: () => + import('../iris/settings/iris-global-settings-update/iris-global-settings-update-routing.module').then( + (module) => module.IrisGlobalSettingsUpdateRoutingModule, + ), }, ...organizationMgmtRoute, ...userManagementRoute, diff --git a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html index d2dc5c84eba0..1108cb31914e 100644 --- a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html +++ b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.html @@ -22,10 +22,15 @@ Communication - + - Iris - + Iris diff --git a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.ts b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.ts index 56042dc2ba2b..bc97997a9998 100644 --- a/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.ts +++ b/src/main/webapp/app/course/manage/course-management-tab-bar/course-management-tab-bar.component.ts @@ -27,7 +27,6 @@ import { } from '@fortawesome/free-solid-svg-icons'; import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service'; import { CourseAdminService } from 'app/course/manage/course-admin.service'; -import { IrisCourseSettingsUpdateComponent } from 'app/iris/settings/iris-course-settings-update/iris-course-settings-update.component'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; @@ -175,12 +174,4 @@ export class CourseManagementTabBarComponent implements OnInit, OnDestroy { const courseManagementRegex = /course-management\/[0-9]+(\/edit)?$/; return courseManagementRegex.test(this.router.url); } - - /** - * Shows the iris settings in a modal. - */ - showIrisSettings(): void { - const modalRef = this.modalService.open(IrisCourseSettingsUpdateComponent, { size: 'xl' }); - modalRef.componentInstance.courseId = this.course!.id; - } } diff --git a/src/main/webapp/app/course/manage/course-management.module.ts b/src/main/webapp/app/course/manage/course-management.module.ts index 08a645469e4c..4a03b5230da3 100644 --- a/src/main/webapp/app/course/manage/course-management.module.ts +++ b/src/main/webapp/app/course/manage/course-management.module.ts @@ -64,6 +64,7 @@ import { ExerciseCategoriesModule } from 'app/shared/exercise-categories/exercis import { CourseManagementTabBarComponent } from 'app/course/manage/course-management-tab-bar/course-management-tab-bar.component'; import { ArtemisExerciseCreateButtonsModule } from 'app/exercises/shared/manage/exercise-create-buttons.module'; import { ArtemisLearningPathManagementModule } from 'app/course/learning-paths/learning-path-management/learning-path-management.module'; +import { IrisModule } from 'app/iris/iris.module'; @NgModule({ imports: [ @@ -115,6 +116,7 @@ import { ArtemisLearningPathManagementModule } from 'app/course/learning-paths/l NgbNavModule, ArtemisExerciseCreateButtonsModule, ArtemisLearningPathManagementModule, + IrisModule, ], declarations: [ CourseManagementComponent, diff --git a/src/main/webapp/app/course/manage/course-management.route.ts b/src/main/webapp/app/course/manage/course-management.route.ts index fbe905de2c85..d4a93ed99925 100644 --- a/src/main/webapp/app/course/manage/course-management.route.ts +++ b/src/main/webapp/app/course/manage/course-management.route.ts @@ -68,6 +68,11 @@ export const courseManagementState: Routes = [ canActivate: [UserRouteAccessService], loadChildren: () => import('app/grading-system/grading-system.module').then((m) => m.GradingSystemModule), }, + { + path: ':courseId/iris-settings', + loadChildren: () => + import('app/iris/settings/iris-course-settings-update/iris-course-settings-update-routing.module').then((m) => m.IrisCourseSettingsUpdateRoutingModule), + }, { path: ':courseId/tutorial-groups', resolve: { 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 469a639f026d..2b3f5c0d3fe0 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 @@ -238,6 +238,26 @@

Course Details: +
+
Iris Chat
+
+ +
+
+ +
+
Iris Hestia
+
+ +
+
+ +
+
Iris CodeEditor
+
+ +
+
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 aadf18cc563c..522ff025c1a5 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 @@ -13,6 +13,9 @@ import { EventManager } from 'app/core/util/event-manager.service'; import { faChartBar, faClipboard, faEye, faFlag, faListAlt, faTable, faTimes, faWrench } from '@fortawesome/free-solid-svg-icons'; import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service'; import { OrganizationManagementService } from 'app/admin/organization-management/organization-management.service'; +import { IrisSubSettingsType } from 'app/entities/iris/settings/iris-sub-settings.model'; +import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.service'; +import { AccountService } from 'app/core/auth/account.service'; export enum DoughnutChartType { ASSESSMENT = 'ASSESSMENT', @@ -32,6 +35,9 @@ export enum DoughnutChartType { export class CourseDetailComponent implements OnInit, OnDestroy { readonly DoughnutChartType = DoughnutChartType; readonly FeatureToggle = FeatureToggle; + readonly CHAT = IrisSubSettingsType.CHAT; + readonly HESTIA = IrisSubSettingsType.HESTIA; + readonly CODE_EDITOR = IrisSubSettingsType.CODE_EDITOR; courseDTO: CourseManagementDetailViewDto; activeStudents?: number[]; @@ -39,9 +45,14 @@ export class CourseDetailComponent implements OnInit, OnDestroy { messagingEnabled: boolean; communicationEnabled: boolean; - + irisEnabled = false; + irisChatEnabled = false; + irisHestiaEnabled = false; + irisCodeEditorEnabled = false; ltiEnabled = false; + isAdmin = false; + private eventSubscriber: Subscription; paramSub: Subscription; @@ -62,6 +73,8 @@ export class CourseDetailComponent implements OnInit, OnDestroy { private route: ActivatedRoute, private alertService: AlertService, private profileService: ProfileService, + private accountService: AccountService, + private irisSettingsService: IrisSettingsService, ) {} /** @@ -70,6 +83,14 @@ export class CourseDetailComponent implements OnInit, OnDestroy { ngOnInit() { this.profileService.getProfileInfo().subscribe((profileInfo) => { this.ltiEnabled = profileInfo.activeProfiles.includes(PROFILE_LTI); + this.irisEnabled = profileInfo.activeProfiles.includes('iris'); + if (this.irisEnabled) { + this.irisSettingsService.getGlobalSettings().subscribe((settings) => { + this.irisChatEnabled = settings?.irisChatSettings?.enabled ?? false; + this.irisHestiaEnabled = settings?.irisHestiaSettings?.enabled ?? false; + this.irisCodeEditorEnabled = settings?.irisCodeEditorSettings?.enabled ?? false; + }); + } }); this.route.data.subscribe(({ course }) => { if (course) { @@ -77,6 +98,7 @@ export class CourseDetailComponent implements OnInit, OnDestroy { this.messagingEnabled = !!this.course.courseInformationSharingConfiguration?.includes('MESSAGING'); this.communicationEnabled = !!this.course.courseInformationSharingConfiguration?.includes('COMMUNICATION'); } + this.isAdmin = this.accountService.isAdmin(); }); // There is no course 0 -> will fetch no course if route does not provide different courseId let courseId = 0; diff --git a/src/main/webapp/app/entities/iris/settings/iris-settings.model.ts b/src/main/webapp/app/entities/iris/settings/iris-settings.model.ts index 409c24ac93ac..6cd5d74b1ecd 100644 --- a/src/main/webapp/app/entities/iris/settings/iris-settings.model.ts +++ b/src/main/webapp/app/entities/iris/settings/iris-settings.model.ts @@ -1,9 +1,44 @@ import { BaseEntity } from 'app/shared/model/base-entity'; -import { IrisSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; +import { IrisChatSubSettings, IrisCodeEditorSubSettings, IrisHestiaSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; -export class IrisSettings implements BaseEntity { +export enum IrisSettingsType { + GLOBAL = 'global', + COURSE = 'course', + EXERCISE = 'exercise', +} + +export abstract class IrisSettings implements BaseEntity { + id?: number; + type: IrisSettingsType; + irisChatSettings?: IrisChatSubSettings; + irisHestiaSettings?: IrisHestiaSubSettings; + irisCodeEditorSettings?: IrisCodeEditorSubSettings; +} + +export class IrisGlobalSettings implements IrisSettings { + id?: number; + type = IrisSettingsType.GLOBAL; + currentVersion?: number; + enableAutoUpdateChat?: boolean; + enableAutoUpdateHestia?: boolean; + enableAutoUpdateCodeEditor?: boolean; + irisChatSettings?: IrisChatSubSettings; + irisHestiaSettings?: IrisHestiaSubSettings; + irisCodeEditorSettings?: IrisCodeEditorSubSettings; +} + +export class IrisCourseSettings implements IrisSettings { + id?: number; + type = IrisSettingsType.COURSE; + courseId?: number; + irisChatSettings?: IrisChatSubSettings; + irisHestiaSettings?: IrisHestiaSubSettings; + irisCodeEditorSettings?: IrisCodeEditorSubSettings; +} + +export class IrisExerciseSettings implements IrisSettings { id?: number; - irisChatSettings?: IrisSubSettings; - irisHestiaSettings?: IrisSubSettings; - global = false; + type = IrisSettingsType.EXERCISE; + exerciseId?: number; + irisChatSettings?: IrisChatSubSettings; } diff --git a/src/main/webapp/app/entities/iris/settings/iris-sub-settings.model.ts b/src/main/webapp/app/entities/iris/settings/iris-sub-settings.model.ts index aa2981af0411..5000003d4952 100644 --- a/src/main/webapp/app/entities/iris/settings/iris-sub-settings.model.ts +++ b/src/main/webapp/app/entities/iris/settings/iris-sub-settings.model.ts @@ -1,11 +1,37 @@ import { BaseEntity } from 'app/shared/model/base-entity'; import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; -export class IrisSubSettings implements BaseEntity { +export enum IrisSubSettingsType { + CHAT = 'chat', + HESTIA = 'hestia', + CODE_EDITOR = 'code-editor', +} + +export abstract class IrisSubSettings implements BaseEntity { id?: number; + type: IrisSubSettingsType; enabled = false; - template?: IrisTemplate; + allowedModels?: string[]; preferredModel?: string; +} + +export class IrisChatSubSettings extends IrisSubSettings { + type = IrisSubSettingsType.CHAT; + template?: IrisTemplate; rateLimit?: number; rateLimitTimeframeHours?: number; } + +export class IrisHestiaSubSettings extends IrisSubSettings { + type = IrisSubSettingsType.HESTIA; + template?: IrisTemplate; +} + +export class IrisCodeEditorSubSettings extends IrisSubSettings { + type = IrisSubSettingsType.CODE_EDITOR; + chatTemplate?: IrisTemplate; + problemStatementGenerationTemplate?: IrisTemplate; + templateRepoGenerationTemplate?: IrisTemplate; + solutionRepoGenerationTemplate?: IrisTemplate; + testRepoGenerationTemplate?: IrisTemplate; +} 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 32b0006e955b..04ce4af2f563 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 @@ -59,6 +59,17 @@

Programming Grading + + + + Iris +

Show Submissions + +
+
Iris Chat
+
+ +
+
+
Lines added/removed between template and solution
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 85bd2996ca76..23169a615632 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 @@ -54,7 +54,8 @@ import { DocumentationType } from 'app/shared/components/documentation-button/do import { ConsistencyCheckService } from 'app/shared/consistency-check/consistency-check.service'; import { hasEditableBuildPlan } from 'app/shared/layouts/profiles/profile-info.model'; import { PROFILE_LOCALVC } from 'app/app.constants'; -import { IrisProgrammingExerciseSettingsUpdateComponent } from 'app/iris/settings/iris-programming-exercise-settings-update/iris-programming-exercise-settings-update.component'; +import { IrisSubSettingsType } from 'app/entities/iris/settings/iris-sub-settings.model'; +import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.service'; @Component({ selector: 'jhi-programming-exercise-detail', @@ -72,6 +73,7 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { readonly ButtonSize = ButtonSize; readonly AssessmentType = AssessmentType; readonly documentationType: DocumentationType = 'Programming'; + readonly CHAT = IrisSubSettingsType.CHAT; programmingExercise: ProgrammingExercise; isExamExercise: boolean; @@ -88,6 +90,7 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { // Also used to hide the buttons to lock and unlock all repositories as that does not do anything in the local VCS. localVCEnabled = false; irisEnabled = false; + irisChatEnabled = false; isAdmin = false; addedLineCount: number; @@ -138,6 +141,7 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { private router: Router, private programmingLanguageFeatureService: ProgrammingLanguageFeatureService, private consistencyCheckService: ConsistencyCheckService, + private irisSettingsService: IrisSettingsService, ) {} ngOnInit() { @@ -190,7 +194,12 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { this.supportsAuxiliaryRepositories = this.programmingLanguageFeatureService.getProgrammingLanguageFeature(programmingExercise.programmingLanguage).auxiliaryRepositoriesSupported ?? false; this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); - this.irisEnabled = profileInfo.activeProfiles.includes('iris') && !this.programmingExercise.exerciseGroup; + this.irisEnabled = profileInfo.activeProfiles.includes('iris'); + if (this.irisEnabled) { + this.irisSettingsService.getCombinedCourseSettings(this.courseId).subscribe((settings) => { + this.irisChatEnabled = settings?.irisChatSettings?.enabled ?? false; + }); + } } }); @@ -418,14 +427,6 @@ export class ProgrammingExerciseDetailComponent implements OnInit, OnDestroy { modalRef.componentInstance.report = this.programmingExercise.gitDiffReport; } - /** - * Shows the iris settings in a modal. - */ - showIrisSettings(): void { - const modalRef = this.modalService.open(IrisProgrammingExerciseSettingsUpdateComponent, { size: 'xl' }); - modalRef.componentInstance.programmingExerciseId = this.programmingExercise.id; - } - createStructuralSolutionEntries() { this.programmingExerciseService.createStructuralSolutionEntries(this.programmingExercise.id!).subscribe({ next: () => { diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-management-routing.module.ts b/src/main/webapp/app/exercises/programming/manage/programming-exercise-management-routing.module.ts index a32417226794..62d07eaebe91 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-management-routing.module.ts +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-management-routing.module.ts @@ -138,6 +138,11 @@ export const routes: Routes = [ }, canActivate: [UserRouteAccessService], }, + { + path: ':courseId/programming-exercises/:exerciseId/iris-settings', + loadChildren: () => + import('app/iris/settings/iris-exercise-settings-update/iris-exercise-settings-update-routing.module').then((m) => m.IrisExerciseSettingsUpdateRoutingModule), + }, { path: ':courseId/programming-exercises/:exerciseId/edit-build-plan', component: BuildPlanEditorComponent, diff --git a/src/main/webapp/app/exercises/programming/manage/programming-exercise-management.module.ts b/src/main/webapp/app/exercises/programming/manage/programming-exercise-management.module.ts index 9d774a5e1f3d..396ad0155ec0 100644 --- a/src/main/webapp/app/exercises/programming/manage/programming-exercise-management.module.ts +++ b/src/main/webapp/app/exercises/programming/manage/programming-exercise-management.module.ts @@ -28,6 +28,7 @@ import { ArtemisCodeHintGenerationOverviewModule } from 'app/exercises/programmi import { BuildPlanEditorComponent } from 'app/exercises/programming/manage/build-plan-editor.component'; import { AceEditorModule } from 'app/shared/markdown-editor/ace-editor/ace-editor.module'; import { ArtemisCodeEditorModule } from 'app/exercises/programming/shared/code-editor/code-editor.module'; +import { IrisModule } from 'app/iris/iris.module'; @NgModule({ imports: [ @@ -54,6 +55,7 @@ import { ArtemisCodeEditorModule } from 'app/exercises/programming/shared/code-e ArtemisCodeHintGenerationOverviewModule, AceEditorModule, ArtemisCodeEditorModule, + IrisModule, ], declarations: [ ProgrammingExerciseDetailComponent, diff --git a/src/main/webapp/app/iris/iris.module.ts b/src/main/webapp/app/iris/iris.module.ts index 4bb13a252b62..4bc954df4004 100644 --- a/src/main/webapp/app/iris/iris.module.ts +++ b/src/main/webapp/app/iris/iris.module.ts @@ -12,10 +12,15 @@ import { RouterModule } from '@angular/router'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { IrisSettingsUpdateComponent } from './settings/iris-settings-update/iris-settings-update.component'; import { IrisGlobalSettingsUpdateComponent } from './settings/iris-global-settings-update/iris-global-settings-update.component'; -import { IrisSubSettingsUpdateComponent } from './settings/iris-settings-update/iris-sub-settings-update/iris-sub-settings-update.component'; +import { IrisCommonSubSettingsUpdateComponent } from './settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component'; import { IrisCourseSettingsUpdateComponent } from 'app/iris/settings/iris-course-settings-update/iris-course-settings-update.component'; -import { IrisProgrammingExerciseSettingsUpdateComponent } from 'app/iris/settings/iris-programming-exercise-settings-update/iris-programming-exercise-settings-update.component'; +import { IrisExerciseSettingsUpdateComponent } from 'app/iris/settings/iris-exercise-settings-update/iris-exercise-settings-update.component'; import { IrisLogoComponent } from './iris-logo/iris-logo.component'; +import { IrisChatSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component'; +import { IrisHestiaSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component'; +import { IrisGlobalAutoupdateSettingsUpdateComponent } from './settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component'; +import { IrisCodeEditorSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-code-editor-sub-settings-update/iris-code-editor-sub-settings-update.component'; +import { IrisEnabledComponent } from 'app/iris/settings/shared/iris-enabled.component'; @NgModule({ declarations: [ @@ -25,12 +30,17 @@ import { IrisLogoComponent } from './iris-logo/iris-logo.component'; IrisSettingsUpdateComponent, IrisGlobalSettingsUpdateComponent, IrisCourseSettingsUpdateComponent, - IrisProgrammingExerciseSettingsUpdateComponent, - IrisSubSettingsUpdateComponent, + IrisExerciseSettingsUpdateComponent, + IrisCommonSubSettingsUpdateComponent, IrisLogoComponent, + IrisChatSubSettingsUpdateComponent, + IrisHestiaSubSettingsUpdateComponent, + IrisGlobalAutoupdateSettingsUpdateComponent, + IrisCodeEditorSubSettingsUpdateComponent, + IrisEnabledComponent, ], imports: [CommonModule, MatDialogModule, FormsModule, FontAwesomeModule, ArtemisSharedModule, ArtemisMarkdownModule, ArtemisSharedComponentModule, RouterModule], providers: [], - exports: [ExerciseChatbotComponent], + exports: [ExerciseChatbotComponent, IrisEnabledComponent], }) export class IrisModule {} diff --git a/src/main/webapp/app/iris/settings/iris-course-settings-update/iris-course-settings-update-routing.module.ts b/src/main/webapp/app/iris/settings/iris-course-settings-update/iris-course-settings-update-routing.module.ts new file mode 100644 index 000000000000..c85d0946f91a --- /dev/null +++ b/src/main/webapp/app/iris/settings/iris-course-settings-update/iris-course-settings-update-routing.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { Authority } from 'app/shared/constants/authority.constants'; +import { IrisModule } from 'app/iris/iris.module'; +import { IrisCourseSettingsUpdateComponent } from 'app/iris/settings/iris-course-settings-update/iris-course-settings-update.component'; +import { PendingChangesGuard } from 'app/shared/guard/pending-changes.guard'; +import { UserRouteAccessService } from 'app/core/auth/user-route-access-service'; + +const routes: Routes = [ + { + path: '', + component: IrisCourseSettingsUpdateComponent, + data: { + authorities: [Authority.INSTRUCTOR, Authority.ADMIN], + pageTitle: 'artemisApp.iris.settings.title.course', + }, + canActivate: [UserRouteAccessService], + canDeactivate: [PendingChangesGuard], + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes), IrisModule], + exports: [RouterModule], +}) +export class IrisCourseSettingsUpdateRoutingModule {} diff --git a/src/main/webapp/app/iris/settings/iris-course-settings-update/iris-course-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-course-settings-update/iris-course-settings-update.component.html index 0a0f3c96cda4..05a957df50b5 100644 --- a/src/main/webapp/app/iris/settings/iris-course-settings-update/iris-course-settings-update.component.html +++ b/src/main/webapp/app/iris/settings/iris-course-settings-update/iris-course-settings-update.component.html @@ -1,4 +1,4 @@

Course Iris Settings

- +
diff --git a/src/main/webapp/app/iris/settings/iris-course-settings-update/iris-course-settings-update.component.ts b/src/main/webapp/app/iris/settings/iris-course-settings-update/iris-course-settings-update.component.ts index 6a38055b6253..35669b658a41 100644 --- a/src/main/webapp/app/iris/settings/iris-course-settings-update/iris-course-settings-update.component.ts +++ b/src/main/webapp/app/iris/settings/iris-course-settings-update/iris-course-settings-update.component.ts @@ -1,13 +1,35 @@ -import { Component, Input } from '@angular/core'; -import { IrisSettingsType } from 'app/iris/settings/iris-settings-update/iris-settings-update.component'; +import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { IrisSettingsType } from 'app/entities/iris/settings/iris-settings.model'; +import { ComponentCanDeactivate } from 'app/shared/guard/can-deactivate.model'; +import { IrisSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-settings-update.component'; @Component({ selector: 'jhi-iris-course-settings-update', templateUrl: './iris-course-settings-update.component.html', }) -export class IrisCourseSettingsUpdateComponent { +export class IrisCourseSettingsUpdateComponent implements OnInit, ComponentCanDeactivate { + @ViewChild(IrisSettingsUpdateComponent) + settingsUpdateComponent?: IrisSettingsUpdateComponent; + @Input() courseId?: number; COURSE = IrisSettingsType.COURSE; + + constructor(private route: ActivatedRoute) {} + + ngOnInit(): void { + this.route.parent?.params.subscribe((params) => { + this.courseId = Number(params['courseId']); + }); + } + + canDeactivate(): boolean { + return this.settingsUpdateComponent?.canDeactivate() ?? true; + } + + get canDeactivateWarning(): string | undefined { + return this.settingsUpdateComponent?.canDeactivateWarning; + } } diff --git a/src/main/webapp/app/iris/settings/iris-exercise-settings-update/iris-exercise-settings-update-routing.module.ts b/src/main/webapp/app/iris/settings/iris-exercise-settings-update/iris-exercise-settings-update-routing.module.ts new file mode 100644 index 000000000000..4b7253190157 --- /dev/null +++ b/src/main/webapp/app/iris/settings/iris-exercise-settings-update/iris-exercise-settings-update-routing.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { Authority } from 'app/shared/constants/authority.constants'; +import { IrisModule } from 'app/iris/iris.module'; +import { PendingChangesGuard } from 'app/shared/guard/pending-changes.guard'; +import { UserRouteAccessService } from 'app/core/auth/user-route-access-service'; +import { IrisExerciseSettingsUpdateComponent } from 'app/iris/settings/iris-exercise-settings-update/iris-exercise-settings-update.component'; + +const routes: Routes = [ + { + path: '', + component: IrisExerciseSettingsUpdateComponent, + data: { + authorities: [Authority.INSTRUCTOR, Authority.ADMIN], + pageTitle: 'artemisApp.iris.settings.title.exercise', + }, + canActivate: [UserRouteAccessService], + canDeactivate: [PendingChangesGuard], + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes), IrisModule], + exports: [RouterModule], +}) +export class IrisExerciseSettingsUpdateRoutingModule {} diff --git a/src/main/webapp/app/iris/settings/iris-exercise-settings-update/iris-exercise-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-exercise-settings-update/iris-exercise-settings-update.component.html new file mode 100644 index 000000000000..f163c2a1402f --- /dev/null +++ b/src/main/webapp/app/iris/settings/iris-exercise-settings-update/iris-exercise-settings-update.component.html @@ -0,0 +1,4 @@ +
+

Exercise Iris Settings

+ +
diff --git a/src/main/webapp/app/iris/settings/iris-exercise-settings-update/iris-exercise-settings-update.component.ts b/src/main/webapp/app/iris/settings/iris-exercise-settings-update/iris-exercise-settings-update.component.ts new file mode 100644 index 000000000000..762be1ff1626 --- /dev/null +++ b/src/main/webapp/app/iris/settings/iris-exercise-settings-update/iris-exercise-settings-update.component.ts @@ -0,0 +1,38 @@ +import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { IrisSettingsType } from 'app/entities/iris/settings/iris-settings.model'; +import { ComponentCanDeactivate } from 'app/shared/guard/can-deactivate.model'; +import { IrisSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-settings-update.component'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + selector: 'jhi-iris-exercise-settings-update', + templateUrl: './iris-exercise-settings-update.component.html', +}) +export class IrisExerciseSettingsUpdateComponent implements OnInit, ComponentCanDeactivate { + @ViewChild(IrisSettingsUpdateComponent) + settingsUpdateComponent?: IrisSettingsUpdateComponent; + + @Input() + public courseId?: number; + @Input() + public exerciseId?: number; + + EXERCISE = IrisSettingsType.EXERCISE; + + constructor(private route: ActivatedRoute) {} + + ngOnInit(): void { + this.route.parent?.params.subscribe((params) => { + this.courseId = Number(params['courseId']); + this.exerciseId = Number(params['exerciseId']); + }); + } + + canDeactivate(): boolean { + return this.settingsUpdateComponent?.canDeactivate() ?? true; + } + + get canDeactivateWarning(): string | undefined { + return this.settingsUpdateComponent?.canDeactivateWarning; + } +} diff --git a/src/main/webapp/app/iris/settings/iris-settings-update-routing.module.ts b/src/main/webapp/app/iris/settings/iris-global-settings-update/iris-global-settings-update-routing.module.ts similarity index 69% rename from src/main/webapp/app/iris/settings/iris-settings-update-routing.module.ts rename to src/main/webapp/app/iris/settings/iris-global-settings-update/iris-global-settings-update-routing.module.ts index 055591044ae2..33d86b041aee 100644 --- a/src/main/webapp/app/iris/settings/iris-settings-update-routing.module.ts +++ b/src/main/webapp/app/iris/settings/iris-global-settings-update/iris-global-settings-update-routing.module.ts @@ -3,6 +3,8 @@ import { RouterModule, Routes } from '@angular/router'; import { Authority } from 'app/shared/constants/authority.constants'; import { IrisGlobalSettingsUpdateComponent } from 'app/iris/settings/iris-global-settings-update/iris-global-settings-update.component'; import { IrisModule } from 'app/iris/iris.module'; +import { PendingChangesGuard } from 'app/shared/guard/pending-changes.guard'; +import { UserRouteAccessService } from 'app/core/auth/user-route-access-service'; const routes: Routes = [ { @@ -12,6 +14,8 @@ const routes: Routes = [ authorities: [Authority.ADMIN], pageTitle: 'artemisApp.iris.settings.title.global', }, + canActivate: [UserRouteAccessService], + canDeactivate: [PendingChangesGuard], }, ]; @@ -19,4 +23,4 @@ const routes: Routes = [ imports: [RouterModule.forChild(routes), IrisModule], exports: [RouterModule], }) -export class IrisSettingsUpdateRoutingModule {} +export class IrisGlobalSettingsUpdateRoutingModule {} diff --git a/src/main/webapp/app/iris/settings/iris-global-settings-update/iris-global-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-global-settings-update/iris-global-settings-update.component.html index 738f8c050ff0..552daf664eaa 100644 --- a/src/main/webapp/app/iris/settings/iris-global-settings-update/iris-global-settings-update.component.html +++ b/src/main/webapp/app/iris/settings/iris-global-settings-update/iris-global-settings-update.component.html @@ -1,4 +1,4 @@

Global Iris Settings

- +
diff --git a/src/main/webapp/app/iris/settings/iris-global-settings-update/iris-global-settings-update.component.ts b/src/main/webapp/app/iris/settings/iris-global-settings-update/iris-global-settings-update.component.ts index 5f6d1091a6e7..42d4bfe549ca 100644 --- a/src/main/webapp/app/iris/settings/iris-global-settings-update/iris-global-settings-update.component.ts +++ b/src/main/webapp/app/iris/settings/iris-global-settings-update/iris-global-settings-update.component.ts @@ -1,10 +1,23 @@ -import { Component } from '@angular/core'; -import { IrisSettingsType } from 'app/iris/settings/iris-settings-update/iris-settings-update.component'; +import { Component, ViewChild } from '@angular/core'; +import { IrisSettingsType } from 'app/entities/iris/settings/iris-settings.model'; +import { ComponentCanDeactivate } from 'app/shared/guard/can-deactivate.model'; +import { IrisSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-settings-update.component'; @Component({ selector: 'jhi-iris-global-settings-update', templateUrl: './iris-global-settings-update.component.html', }) -export class IrisGlobalSettingsUpdateComponent { +export class IrisGlobalSettingsUpdateComponent implements ComponentCanDeactivate { + @ViewChild(IrisSettingsUpdateComponent) + settingsUpdateComponent?: IrisSettingsUpdateComponent; + GLOBAL = IrisSettingsType.GLOBAL; + + canDeactivate(): boolean { + return this.settingsUpdateComponent?.canDeactivate() ?? true; + } + + get canDeactivateWarning(): string | undefined { + return this.settingsUpdateComponent?.canDeactivateWarning; + } } diff --git a/src/main/webapp/app/iris/settings/iris-programming-exercise-settings-update/iris-programming-exercise-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-programming-exercise-settings-update/iris-programming-exercise-settings-update.component.html deleted file mode 100644 index e1c03e8ab918..000000000000 --- a/src/main/webapp/app/iris/settings/iris-programming-exercise-settings-update/iris-programming-exercise-settings-update.component.html +++ /dev/null @@ -1,4 +0,0 @@ -
-

Programming Exercise Iris Settings

- -
diff --git a/src/main/webapp/app/iris/settings/iris-programming-exercise-settings-update/iris-programming-exercise-settings-update.component.ts b/src/main/webapp/app/iris/settings/iris-programming-exercise-settings-update/iris-programming-exercise-settings-update.component.ts deleted file mode 100644 index 7d0b4daf08f1..000000000000 --- a/src/main/webapp/app/iris/settings/iris-programming-exercise-settings-update/iris-programming-exercise-settings-update.component.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { IrisSettingsType } from 'app/iris/settings/iris-settings-update/iris-settings-update.component'; - -@Component({ - selector: 'jhi-iris-programming-exercise-settings-update', - templateUrl: './iris-programming-exercise-settings-update.component.html', -}) -export class IrisProgrammingExerciseSettingsUpdateComponent { - @Input() - public programmingExerciseId?: number; - - PROGRAMMING_EXERCISE = IrisSettingsType.PROGRAMMING_EXERCISE; -} diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component.html new file mode 100644 index 000000000000..ca6429c85227 --- /dev/null +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component.html @@ -0,0 +1,28 @@ +
+ + + +
+ +
+ + + +
+ +
+

Template

+
+ + +
+ +
diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component.ts b/src/main/webapp/app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component.ts new file mode 100644 index 000000000000..df6192fa55e3 --- /dev/null +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component.ts @@ -0,0 +1,59 @@ +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; +import { IrisChatSubSettings, IrisSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; + +@Component({ + selector: 'jhi-iris-chat-sub-settings-update', + templateUrl: './iris-chat-sub-settings-update.component.html', +}) +export class IrisChatSubSettingsUpdateComponent implements OnInit, OnChanges { + @Input() + subSettings?: IrisChatSubSettings; + + @Input() + parentSubSettings?: IrisChatSubSettings; + + @Input() + rateLimitSettable = false; + + @Output() + onChanges = new EventEmitter(); + + previousTemplate?: IrisTemplate; + + isAdmin: boolean; + + templateContent: string; + + ngOnInit(): void { + this.templateContent = this.subSettings?.template?.content ?? this.parentSubSettings?.template?.content ?? ''; + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.subSettings || changes.parentSubSettings) { + this.templateContent = this.subSettings?.template?.content ?? this.parentSubSettings?.template?.content ?? ''; + } + } + + onInheritTemplateChanged() { + if (this.subSettings?.template) { + this.previousTemplate = this.subSettings?.template; + this.subSettings.template = undefined; + this.templateContent = this.parentSubSettings?.template?.content ?? ''; + } else { + const irisTemplate = new IrisTemplate(); + irisTemplate.content = ''; + this.subSettings!.template = this.previousTemplate ?? irisTemplate; + } + } + + onTemplateChanged() { + if (this.subSettings?.template) { + this.subSettings.template.content = this.templateContent; + } else { + const irisTemplate = new IrisTemplate(); + irisTemplate.content = this.templateContent; + this.subSettings!.template = irisTemplate; + } + } +} diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-code-editor-sub-settings-update/iris-code-editor-sub-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-settings-update/iris-code-editor-sub-settings-update/iris-code-editor-sub-settings-update.component.html new file mode 100644 index 000000000000..eaadb2bab643 --- /dev/null +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-code-editor-sub-settings-update/iris-code-editor-sub-settings-update.component.html @@ -0,0 +1,63 @@ +
+

Templates

+
+ + +
+ +
+

Chat Template

+ +
+
+

Problem Statement Generation Template

+ +
+
+

Template Repo Generation Template

+ +
+
+

Solution Repo Generation Template

+ +
+
+

Test Repo Generation Template

+ +
+
diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-code-editor-sub-settings-update/iris-code-editor-sub-settings-update.component.ts b/src/main/webapp/app/iris/settings/iris-settings-update/iris-code-editor-sub-settings-update/iris-code-editor-sub-settings-update.component.ts new file mode 100644 index 000000000000..d35119e710a5 --- /dev/null +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-code-editor-sub-settings-update/iris-code-editor-sub-settings-update.component.ts @@ -0,0 +1,137 @@ +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; +import { IrisCodeEditorSubSettings, IrisSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; + +@Component({ + selector: 'jhi-iris-code-editor-sub-settings-update', + templateUrl: './iris-code-editor-sub-settings-update.component.html', +}) +export class IrisCodeEditorSubSettingsUpdateComponent implements OnInit, OnChanges { + @Input() + subSettings?: IrisCodeEditorSubSettings; + + @Input() + parentSubSettings?: IrisCodeEditorSubSettings; + + @Output() + onChanges = new EventEmitter(); + + previousChatTemplate?: IrisTemplate; + previousProblemStatementGenerationTemplate?: IrisTemplate; + previousTemplateRepoGenerationTemplate?: IrisTemplate; + previousSolutionRepoGenerationTemplate?: IrisTemplate; + previousTestRepoGenerationTemplate?: IrisTemplate; + + chatTemplateContent: string; + problemStatementGenerationTemplateContent: string; + templateRepoGenerationTemplateContent: string; + solutionRepoGenerationTemplateContent: string; + testRepoGenerationTemplateContent: string; + + ngOnInit(): void { + this.resetTemplates(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.subSettings || changes.parentSubSettings) { + this.resetTemplates(); + } + } + + private resetTemplates() { + this.chatTemplateContent = this.subSettings?.chatTemplate?.content ?? this.parentSubSettings?.chatTemplate?.content ?? ''; + this.problemStatementGenerationTemplateContent = + this.subSettings?.problemStatementGenerationTemplate?.content ?? this.parentSubSettings?.problemStatementGenerationTemplate?.content ?? ''; + this.templateRepoGenerationTemplateContent = + this.subSettings?.templateRepoGenerationTemplate?.content ?? this.parentSubSettings?.templateRepoGenerationTemplate?.content ?? ''; + this.solutionRepoGenerationTemplateContent = + this.subSettings?.solutionRepoGenerationTemplate?.content ?? this.parentSubSettings?.solutionRepoGenerationTemplate?.content ?? ''; + this.testRepoGenerationTemplateContent = this.subSettings?.testRepoGenerationTemplate?.content ?? this.parentSubSettings?.testRepoGenerationTemplate?.content ?? ''; + } + + onInheritTemplateChanged() { + if (this.subSettings?.chatTemplate) { + this.previousChatTemplate = this.subSettings?.chatTemplate; + this.subSettings.chatTemplate = undefined; + this.chatTemplateContent = this.parentSubSettings?.chatTemplate?.content ?? ''; + } else { + const irisTemplate = new IrisTemplate(); + irisTemplate.content = ''; + this.subSettings!.chatTemplate = this.previousChatTemplate ?? irisTemplate; + } + if (this.subSettings?.problemStatementGenerationTemplate) { + this.previousProblemStatementGenerationTemplate = this.subSettings?.problemStatementGenerationTemplate; + this.subSettings.problemStatementGenerationTemplate = undefined; + this.problemStatementGenerationTemplateContent = this.parentSubSettings?.problemStatementGenerationTemplate?.content ?? ''; + } else { + const irisTemplate = new IrisTemplate(); + irisTemplate.content = ''; + this.subSettings!.problemStatementGenerationTemplate = this.previousProblemStatementGenerationTemplate ?? irisTemplate; + } + if (this.subSettings?.templateRepoGenerationTemplate) { + this.previousTemplateRepoGenerationTemplate = this.subSettings?.templateRepoGenerationTemplate; + this.subSettings.templateRepoGenerationTemplate = undefined; + this.templateRepoGenerationTemplateContent = this.parentSubSettings?.templateRepoGenerationTemplate?.content ?? ''; + } else { + const irisTemplate = new IrisTemplate(); + irisTemplate.content = ''; + this.subSettings!.templateRepoGenerationTemplate = this.previousTemplateRepoGenerationTemplate ?? irisTemplate; + } + if (this.subSettings?.solutionRepoGenerationTemplate) { + this.previousSolutionRepoGenerationTemplate = this.subSettings?.solutionRepoGenerationTemplate; + this.subSettings.solutionRepoGenerationTemplate = undefined; + this.solutionRepoGenerationTemplateContent = this.parentSubSettings?.solutionRepoGenerationTemplate?.content ?? ''; + } else { + const irisTemplate = new IrisTemplate(); + irisTemplate.content = ''; + this.subSettings!.solutionRepoGenerationTemplate = this.previousSolutionRepoGenerationTemplate ?? irisTemplate; + } + if (this.subSettings?.testRepoGenerationTemplate) { + this.previousTestRepoGenerationTemplate = this.subSettings?.testRepoGenerationTemplate; + this.subSettings.testRepoGenerationTemplate = undefined; + this.testRepoGenerationTemplateContent = this.parentSubSettings?.testRepoGenerationTemplate?.content ?? ''; + } else { + const irisTemplate = new IrisTemplate(); + irisTemplate.content = ''; + this.subSettings!.testRepoGenerationTemplate = this.previousTestRepoGenerationTemplate ?? irisTemplate; + } + } + + onTemplateChanged() { + if (this.subSettings?.chatTemplate) { + this.subSettings.chatTemplate.content = this.chatTemplateContent; + } else { + const irisTemplate = new IrisTemplate(); + irisTemplate.content = this.chatTemplateContent; + this.subSettings!.chatTemplate = irisTemplate; + } + if (this.subSettings?.problemStatementGenerationTemplate) { + this.subSettings.problemStatementGenerationTemplate.content = this.problemStatementGenerationTemplateContent; + } else { + const irisTemplate = new IrisTemplate(); + irisTemplate.content = this.problemStatementGenerationTemplateContent; + this.subSettings!.problemStatementGenerationTemplate = irisTemplate; + } + if (this.subSettings?.templateRepoGenerationTemplate) { + this.subSettings.templateRepoGenerationTemplate.content = this.templateRepoGenerationTemplateContent; + } else { + const irisTemplate = new IrisTemplate(); + irisTemplate.content = this.templateRepoGenerationTemplateContent; + this.subSettings!.templateRepoGenerationTemplate = irisTemplate; + } + if (this.subSettings?.solutionRepoGenerationTemplate) { + this.subSettings.solutionRepoGenerationTemplate.content = this.solutionRepoGenerationTemplateContent; + } else { + const irisTemplate = new IrisTemplate(); + irisTemplate.content = this.solutionRepoGenerationTemplateContent; + this.subSettings!.solutionRepoGenerationTemplate = irisTemplate; + } + if (this.subSettings?.testRepoGenerationTemplate) { + this.subSettings.testRepoGenerationTemplate.content = this.testRepoGenerationTemplateContent; + } else { + const irisTemplate = new IrisTemplate(); + irisTemplate.content = this.testRepoGenerationTemplateContent; + this.subSettings!.testRepoGenerationTemplate = irisTemplate; + } + } +} diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component.html new file mode 100644 index 000000000000..b8288beb95fa --- /dev/null +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component.html @@ -0,0 +1,64 @@ +
+ + +
+ +

Models

+Allowed Models: +
+ + +
+
+
+ + +
+
+ +
Preferred Model:
+
+
+ +
+ + +
+
+ {{ getPreferredModelNameParent() }} +
diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component.ts b/src/main/webapp/app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component.ts new file mode 100644 index 000000000000..fc382c8f8cc0 --- /dev/null +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component.ts @@ -0,0 +1,101 @@ +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +import { IrisSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; +import { IrisModel } from 'app/entities/iris/settings/iris-model'; +import { AccountService } from 'app/core/auth/account.service'; +import { ButtonType } from 'app/shared/components/button.component'; +import { faTrash } from '@fortawesome/free-solid-svg-icons'; +import { IrisSettingsType } from 'app/entities/iris/settings/iris-settings.model'; + +@Component({ + selector: 'jhi-iris-common-sub-settings-update', + templateUrl: './iris-common-sub-settings-update.component.html', +}) +export class IrisCommonSubSettingsUpdateComponent implements OnInit, OnChanges { + @Input() + subSettings?: IrisSubSettings; + + @Input() + parentSubSettings?: IrisSubSettings; + + @Input() + allIrisModels: IrisModel[]; + + @Input() + settingsType: IrisSettingsType; + + @Output() + onChanges = new EventEmitter(); + + isAdmin: boolean; + + inheritAllowedModels: boolean; + + allowedIrisModels: IrisModel[]; + + enabled: boolean; + + // Settings types + EXERCISE = IrisSettingsType.EXERCISE; + // Button types + WARNING = ButtonType.WARNING; + // Icons + faTrash = faTrash; + + constructor(accountService: AccountService) { + this.isAdmin = accountService.isAdmin(); + } + + ngOnInit() { + this.enabled = this.subSettings?.enabled ?? false; + this.allowedIrisModels = this.getAvailableModels(); + this.inheritAllowedModels = !!(!this.subSettings?.allowedModels && this.parentSubSettings); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.allIrisModels) { + this.allowedIrisModels = this.getAvailableModels(); + } + if (changes.subSettings) { + this.enabled = this.subSettings?.enabled ?? false; + } + } + + getAvailableModels(): IrisModel[] { + return this.allIrisModels.filter((model) => (this.subSettings?.allowedModels ?? this.parentSubSettings?.allowedModels ?? []).includes(model.id)); + } + + getPreferredModelName(): string | undefined { + return this.allIrisModels.find((model) => model.id === this.subSettings?.preferredModel)?.name ?? this.subSettings?.preferredModel; + } + + getPreferredModelNameParent(): string | undefined { + return this.allIrisModels.find((model) => model.id === this.parentSubSettings?.preferredModel)?.name ?? this.parentSubSettings?.preferredModel; + } + + onAllowedIrisModelsSelectionChange(model: IrisModel) { + this.inheritAllowedModels = false; + if (this.allowedIrisModels.includes(model)) { + this.allowedIrisModels = this.allowedIrisModels.filter((m) => m !== model); + } else { + this.allowedIrisModels.push(model); + } + this.subSettings!.allowedModels = this.allowedIrisModels.map((model) => model.id); + } + + setModel(model: IrisModel | undefined) { + this.subSettings!.preferredModel = model?.id; + } + + onEnabledChange() { + this.subSettings!.enabled = this.enabled; + } + + onInheritAllowedModelsChange() { + if (this.inheritAllowedModels) { + this.subSettings!.allowedModels = undefined; + this.allowedIrisModels = this.getAvailableModels(); + } else { + this.subSettings!.allowedModels = this.allowedIrisModels.map((model) => model.id); + } + } +} diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component.html new file mode 100644 index 000000000000..2decab80c5d4 --- /dev/null +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component.html @@ -0,0 +1,14 @@ +
+
+ + +
+
+ + +
+
+ + +
+
diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component.ts b/src/main/webapp/app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component.ts new file mode 100644 index 000000000000..404132633566 --- /dev/null +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component.ts @@ -0,0 +1,15 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { IrisGlobalSettings } from 'app/entities/iris/settings/iris-settings.model'; +import { IrisSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; + +@Component({ + selector: 'jhi-iris-global-autoupdate-settings-update', + templateUrl: './iris-global-autoupdate-settings-update.component.html', +}) +export class IrisGlobalAutoupdateSettingsUpdateComponent { + @Input() + irisSettings?: IrisGlobalSettings; + + @Output() + onChanges = new EventEmitter(); +} diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component.html new file mode 100644 index 000000000000..b3ff6ad8c627 --- /dev/null +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component.html @@ -0,0 +1,8 @@ +
+

Template

+
+ + +
+ +
diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component.ts b/src/main/webapp/app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component.ts new file mode 100644 index 000000000000..ea7e98eac424 --- /dev/null +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component.ts @@ -0,0 +1,56 @@ +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; +import { IrisHestiaSubSettings, IrisSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; + +@Component({ + selector: 'jhi-iris-hestia-sub-settings-update', + templateUrl: './iris-hestia-sub-settings-update.component.html', +}) +export class IrisHestiaSubSettingsUpdateComponent implements OnInit, OnChanges { + @Input() + subSettings: IrisHestiaSubSettings; + + @Input() + parentSubSettings?: IrisHestiaSubSettings; + + @Output() + onChanges = new EventEmitter(); + + previousTemplate?: IrisTemplate; + + isAdmin: boolean; + + templateContent: string; + + ngOnInit(): void { + this.templateContent = this.subSettings.template?.content ?? this.parentSubSettings?.template?.content ?? ''; + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.subSettings || changes.parentSubSettings) { + this.templateContent = this.subSettings?.template?.content ?? this.parentSubSettings?.template?.content ?? ''; + } + } + + onInheritTemplateChanged() { + if (this.subSettings.template) { + this.previousTemplate = this.subSettings.template; + this.subSettings.template = undefined; + this.templateContent = this.parentSubSettings?.template?.content ?? ''; + } else { + const irisTemplate = new IrisTemplate(); + irisTemplate.content = ''; + this.subSettings.template = this.previousTemplate ?? irisTemplate; + } + } + + onTemplateChanged() { + if (this.subSettings.template) { + this.subSettings.template.content = this.templateContent; + } else { + const irisTemplate = new IrisTemplate(); + irisTemplate.content = this.templateContent; + this.subSettings.template = irisTemplate; + } + } +} diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.html index c019b93e5ab9..c9f0bcd5ddc0 100644 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.html +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.html @@ -1,32 +1,71 @@
- +
+
+
+

Auto-Update Settings

+ +
+

Chat Settings

- + +
-
-

Hestia Settings

-
- - +
+
+

Hestia Settings

+ +
-
- + + +
+
+

Code Editor Settings

+ +
diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.ts b/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.ts index 115e054e830a..288b89afeb2a 100644 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.ts +++ b/src/main/webapp/app/iris/settings/iris-settings-update/iris-settings-update.component.ts @@ -1,47 +1,48 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { IrisSettings } from 'app/entities/iris/settings/iris-settings.model'; +import { Component, DoCheck, Input, OnInit } from '@angular/core'; +import { IrisSettings, IrisSettingsType } from 'app/entities/iris/settings/iris-settings.model'; import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.service'; import { HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { AlertService } from 'app/core/util/alert.service'; import { ButtonType } from 'app/shared/components/button.component'; import { faRotate, faSave } from '@fortawesome/free-solid-svg-icons'; -import { IrisSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; import { IrisModel } from 'app/entities/iris/settings/iris-model'; - -export enum IrisSettingsType { - GLOBAL = 'GLOBAL', - COURSE = 'COURSE', - PROGRAMMING_EXERCISE = 'PROGRAMMING_EXERCISE', -} +import { ComponentCanDeactivate } from 'app/shared/guard/can-deactivate.model'; +import { cloneDeep, isEqual } from 'lodash-es'; @Component({ selector: 'jhi-iris-settings-update', templateUrl: './iris-settings-update.component.html', }) -export class IrisSettingsUpdateComponent implements OnInit { +export class IrisSettingsUpdateComponent implements OnInit, DoCheck, ComponentCanDeactivate { @Input() - public settingType: IrisSettingsType; + public settingsType: IrisSettingsType; @Input() public courseId?: number; @Input() - public programmingExerciseId?: number; + public exerciseId?: number; public irisSettings?: IrisSettings; - public irisModels?: IrisModel[]; + public parentIrisSettings?: IrisSettings; + public allIrisModels?: IrisModel[]; + + originalIrisSettings?: IrisSettings; - // Loading bools + // Status bools isLoading = false; isSaving = false; + isDirty = false; // Button types PRIMARY = ButtonType.PRIMARY; + WARNING = ButtonType.WARNING; SUCCESS = ButtonType.SUCCESS; // Icons faSave = faSave; faRotate = faRotate; // Settings types GLOBAL = IrisSettingsType.GLOBAL; - PROGRAMMING_EXERCISE = IrisSettingsType.PROGRAMMING_EXERCISE; + COURSE = IrisSettingsType.COURSE; + EXERCISE = IrisSettingsType.EXERCISE; constructor( private irisSettingsService: IrisSettingsService, @@ -52,9 +53,21 @@ export class IrisSettingsUpdateComponent implements OnInit { this.loadIrisSettings(); } + ngDoCheck(): void { + if (!isEqual(this.irisSettings, this.originalIrisSettings)) { + this.isDirty = true; + } + } + + canDeactivateWarning?: string; + + canDeactivate(): boolean { + return !this.isDirty; + } + loadIrisModels(): void { this.irisSettingsService.getIrisModels().subscribe((models) => { - this.irisModels = models; + this.allIrisModels = models; this.isLoading = false; }); } @@ -67,6 +80,14 @@ export class IrisSettingsUpdateComponent implements OnInit { this.alertService.error('artemisApp.iris.settings.error.noSettings'); } this.irisSettings = settings; + this.originalIrisSettings = cloneDeep(settings); + this.isDirty = false; + }); + this.loadParentIrisSettingsObservable().subscribe((settings) => { + if (!settings) { + this.alertService.error('artemisApp.iris.settings.error.noParentSettings'); + } + this.parentIrisSettings = settings; }); } @@ -75,7 +96,9 @@ export class IrisSettingsUpdateComponent implements OnInit { this.saveIrisSettingsObservable().subscribe( (response) => { this.isSaving = false; + this.isDirty = false; this.irisSettings = response.body ?? undefined; + this.originalIrisSettings = cloneDeep(this.irisSettings); this.alertService.success('artemisApp.iris.settings.success'); }, () => { @@ -85,35 +108,37 @@ export class IrisSettingsUpdateComponent implements OnInit { ); } + loadParentIrisSettingsObservable(): Observable { + switch (this.settingsType) { + case IrisSettingsType.GLOBAL: + // Global settings have no parent + return new Observable(); + case IrisSettingsType.COURSE: + return this.irisSettingsService.getGlobalSettings(); + case IrisSettingsType.EXERCISE: + return this.irisSettingsService.getCombinedCourseSettings(this.courseId!); + } + } + loadIrisSettingsObservable(): Observable { - switch (this.settingType) { + switch (this.settingsType) { case IrisSettingsType.GLOBAL: return this.irisSettingsService.getGlobalSettings(); case IrisSettingsType.COURSE: return this.irisSettingsService.getUncombinedCourseSettings(this.courseId!); - case IrisSettingsType.PROGRAMMING_EXERCISE: - return this.irisSettingsService.getUncombinedProgrammingExerciseSettings(this.programmingExerciseId!); + case IrisSettingsType.EXERCISE: + return this.irisSettingsService.getUncombinedProgrammingExerciseSettings(this.exerciseId!); } } saveIrisSettingsObservable(): Observable> { - switch (this.settingType) { + switch (this.settingsType) { case IrisSettingsType.GLOBAL: return this.irisSettingsService.setGlobalSettings(this.irisSettings!); case IrisSettingsType.COURSE: return this.irisSettingsService.setCourseSettings(this.courseId!, this.irisSettings!); - case IrisSettingsType.PROGRAMMING_EXERCISE: - return this.irisSettingsService.setProgrammingExerciseSettings(this.programmingExerciseId!, this.irisSettings!); - } - } - - onInheritHestiaSettingsChanged() { - if (this.irisSettings?.irisHestiaSettings) { - this.irisSettings!.irisHestiaSettings = undefined; - } else { - const irisSubSettings = new IrisSubSettings(); - irisSubSettings.enabled = true; - this.irisSettings!.irisHestiaSettings = irisSubSettings; + case IrisSettingsType.EXERCISE: + return this.irisSettingsService.setProgrammingExerciseSettings(this.exerciseId!, this.irisSettings!); } } } diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-sub-settings-update/iris-sub-settings-update.component.html b/src/main/webapp/app/iris/settings/iris-settings-update/iris-sub-settings-update/iris-sub-settings-update.component.html deleted file mode 100644 index ca9a800ee168..000000000000 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-sub-settings-update/iris-sub-settings-update.component.html +++ /dev/null @@ -1,47 +0,0 @@ -
- - -
-Preferred Model: -
- -
- - -
-
- -
- - - -
- -
- - - -
- -
- -
- - -
- -
diff --git a/src/main/webapp/app/iris/settings/iris-settings-update/iris-sub-settings-update/iris-sub-settings-update.component.ts b/src/main/webapp/app/iris/settings/iris-settings-update/iris-sub-settings-update/iris-sub-settings-update.component.ts deleted file mode 100644 index dde988881ba9..000000000000 --- a/src/main/webapp/app/iris/settings/iris-settings-update/iris-sub-settings-update/iris-sub-settings-update.component.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { IrisSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; -import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; -import { IrisModel } from 'app/entities/iris/settings/iris-model'; -import { AccountService } from 'app/core/auth/account.service'; - -@Component({ - selector: 'jhi-iris-sub-settings-update', - templateUrl: './iris-sub-settings-update.component.html', -}) -export class IrisSubSettingsUpdateComponent { - @Input() - subSettings: IrisSubSettings; - - @Input() - models: IrisModel[]; - - @Input() - modelOptional = false; - - @Input() - templateOptional = false; - - @Input() - rateLimitSettable = false; - - previousTemplate?: IrisTemplate; - - isAdmin: boolean; - - constructor(accountService: AccountService) { - this.isAdmin = accountService.isAdmin(); - } - - onInheritTemplateChanged() { - if (this.subSettings.template) { - this.previousTemplate = this.subSettings.template; - this.subSettings.template = undefined; - } else { - const irisTemplate = new IrisTemplate(); - irisTemplate.content = ''; - this.subSettings.template = this.previousTemplate ?? irisTemplate; - } - } - - getSelectedModelName(): string { - return this.models.find((model) => model.id === this.subSettings.preferredModel)?.name ?? this.subSettings.preferredModel ?? 'None'; - } - - setModel(model: IrisModel | undefined) { - this.subSettings.preferredModel = model?.id; - } -} diff --git a/src/main/webapp/app/iris/settings/shared/iris-enabled.component.html b/src/main/webapp/app/iris/settings/shared/iris-enabled.component.html new file mode 100644 index 000000000000..17d26de15f89 --- /dev/null +++ b/src/main/webapp/app/iris/settings/shared/iris-enabled.component.html @@ -0,0 +1,18 @@ +
+
+ {{ 'artemisApp.iris.settings.subSettings.enabled.on' | artemisTranslate }} +
+
+ {{ 'artemisApp.iris.settings.subSettings.enabled.off' | artemisTranslate }} +
+
diff --git a/src/main/webapp/app/iris/settings/shared/iris-enabled.component.ts b/src/main/webapp/app/iris/settings/shared/iris-enabled.component.ts new file mode 100644 index 000000000000..26e480f31198 --- /dev/null +++ b/src/main/webapp/app/iris/settings/shared/iris-enabled.component.ts @@ -0,0 +1,68 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { IrisSubSettings, IrisSubSettingsType } from 'app/entities/iris/settings/iris-sub-settings.model'; +import { Exercise } from 'app/entities/exercise.model'; +import { IrisSettings } from 'app/entities/iris/settings/iris-settings.model'; +import { Course } from 'app/entities/course.model'; +import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.service'; + +@Component({ + selector: 'jhi-iris-enabled', + templateUrl: './iris-enabled.component.html', +}) +export class IrisEnabledComponent implements OnInit { + @Input() exercise?: Exercise; + @Input() course?: Course; + @Input() irisSubSettingsType: IrisSubSettingsType; + @Input() disabled? = false; + + irisSettings?: IrisSettings; + irisSubSettings?: IrisSubSettings; + + constructor(private irisSettingsService: IrisSettingsService) {} + + ngOnInit(): void { + console.log(this.exercise, this.course, this.irisSubSettingsType); + if (this.exercise) { + this.irisSettingsService.getUncombinedProgrammingExerciseSettings(this.exercise.id!).subscribe((settings) => { + this.irisSettings = settings; + this.setSubSettings(); + }); + } else if (this.course) { + this.irisSettingsService.getUncombinedCourseSettings(this.course.id!).subscribe((settings) => { + this.irisSettings = settings; + this.setSubSettings(); + }); + } + } + + setEnabled(enabled: boolean) { + if (!this.disabled && this.irisSubSettings) { + this.irisSubSettings.enabled = enabled; + if (this.exercise) { + this.irisSettingsService.setProgrammingExerciseSettings(this.exercise.id!, this.irisSettings!).subscribe((response) => { + this.irisSettings = response.body ?? this.irisSettings; + this.setSubSettings(); + }); + } else if (this.course) { + this.irisSettingsService.setCourseSettings(this.course.id!, this.irisSettings!).subscribe((response) => { + this.irisSettings = response.body ?? this.irisSettings; + this.setSubSettings(); + }); + } + } + } + + private setSubSettings() { + switch (this.irisSubSettingsType) { + case IrisSubSettingsType.CHAT: + this.irisSubSettings = this.irisSettings?.irisChatSettings; + break; + case IrisSubSettingsType.HESTIA: + this.irisSubSettings = this.irisSettings?.irisHestiaSettings; + break; + case IrisSubSettingsType.CODE_EDITOR: + this.irisSubSettings = this.irisSettings?.irisCodeEditorSettings; + break; + } + } +} diff --git a/src/main/webapp/app/iris/settings/shared/iris-settings.service.ts b/src/main/webapp/app/iris/settings/shared/iris-settings.service.ts index 40c775b2e5f5..475540bad156 100644 --- a/src/main/webapp/app/iris/settings/shared/iris-settings.service.ts +++ b/src/main/webapp/app/iris/settings/shared/iris-settings.service.ts @@ -2,11 +2,9 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { IrisSettings } from 'app/entities/iris/settings/iris-settings.model'; +import { IrisCourseSettings, IrisExerciseSettings, IrisGlobalSettings } from 'app/entities/iris/settings/iris-settings.model'; import { IrisModel } from 'app/entities/iris/settings/iris-model'; -type EntityResponseType = HttpResponse; - /** * Service for calling the Iris settings endpoints on the server */ @@ -19,58 +17,58 @@ export class IrisSettingsService { /** * Get the global Iris settings */ - getGlobalSettings(): Observable { + getGlobalSettings(): Observable { return this.http - .get(`${this.resourceUrl}/iris/global-iris-settings`, { observe: 'response' }) - .pipe(map((res: HttpResponse) => res.body ?? undefined)); + .get(`${this.resourceUrl}/iris/global-iris-settings`, { observe: 'response' }) + .pipe(map((res: HttpResponse) => res.body ?? undefined)); } /** * Get the uncombined Iris settings for a course * @param courseId the id of the course */ - getUncombinedCourseSettings(courseId: number): Observable { + getUncombinedCourseSettings(courseId: number): Observable { return this.http - .get(`${this.resourceUrl}/courses/${courseId}/raw-iris-settings`, { observe: 'response' }) - .pipe(map((res: HttpResponse) => res.body ?? undefined)); + .get(`${this.resourceUrl}/courses/${courseId}/raw-iris-settings`, { observe: 'response' }) + .pipe(map((res: HttpResponse) => res.body ?? undefined)); } /** * Get the combined Iris settings for a course * @param courseId the id of the course */ - getCombinedCourseSettings(courseId: number): Observable { + getCombinedCourseSettings(courseId: number): Observable { return this.http - .get(`${this.resourceUrl}/courses/${courseId}/iris-settings`, { observe: 'response' }) - .pipe(map((res: HttpResponse) => res.body ?? undefined)); + .get(`${this.resourceUrl}/courses/${courseId}/iris-settings`, { observe: 'response' }) + .pipe(map((res: HttpResponse) => res.body ?? undefined)); } /** * Get the uncombined Iris settings for a programming exercise * @param exerciseId the id of the programming exercise */ - getUncombinedProgrammingExerciseSettings(exerciseId: number): Observable { + getUncombinedProgrammingExerciseSettings(exerciseId: number): Observable { return this.http - .get(`${this.resourceUrl}/programming-exercises/${exerciseId}/raw-iris-settings`, { observe: 'response' }) - .pipe(map((res: HttpResponse) => res.body ?? undefined)); + .get(`${this.resourceUrl}/programming-exercises/${exerciseId}/raw-iris-settings`, { observe: 'response' }) + .pipe(map((res: HttpResponse) => res.body ?? undefined)); } /** * Get the combined Iris settings for a programming exercise * @param exerciseId the id of the programming exercise */ - getCombinedProgrammingExerciseSettings(exerciseId: number): Observable { + getCombinedProgrammingExerciseSettings(exerciseId: number): Observable { return this.http - .get(`${this.resourceUrl}/programming-exercises/${exerciseId}/iris-settings`, { observe: 'response' }) - .pipe(map((res: HttpResponse) => res.body ?? undefined)); + .get(`${this.resourceUrl}/programming-exercises/${exerciseId}/iris-settings`, { observe: 'response' }) + .pipe(map((res: HttpResponse) => res.body ?? undefined)); } /** * Update the global Iris settings * @param settings the settings to set */ - setGlobalSettings(settings: IrisSettings): Observable { - return this.http.put(`${this.resourceUrl}/admin/iris/global-iris-settings`, settings, { observe: 'response' }); + setGlobalSettings(settings: IrisGlobalSettings): Observable> { + return this.http.put(`${this.resourceUrl}/admin/iris/global-iris-settings`, settings, { observe: 'response' }); } /** @@ -78,8 +76,8 @@ export class IrisSettingsService { * @param courseId the id of the course * @param settings the settings to set */ - setCourseSettings(courseId: number, settings: IrisSettings): Observable { - return this.http.put(`${this.resourceUrl}/courses/${courseId}/raw-iris-settings`, settings, { observe: 'response' }); + setCourseSettings(courseId: number, settings: IrisCourseSettings): Observable> { + return this.http.put(`${this.resourceUrl}/courses/${courseId}/raw-iris-settings`, settings, { observe: 'response' }); } /** @@ -87,8 +85,8 @@ export class IrisSettingsService { * @param exerciseId the id of the programming exercise * @param settings the settings to set */ - setProgrammingExerciseSettings(exerciseId: number, settings: IrisSettings): Observable { - return this.http.put(`${this.resourceUrl}/programming-exercises/${exerciseId}/raw-iris-settings`, settings, { observe: 'response' }); + setProgrammingExerciseSettings(exerciseId: number, settings: IrisExerciseSettings): Observable> { + return this.http.put(`${this.resourceUrl}/programming-exercises/${exerciseId}/raw-iris-settings`, settings, { observe: 'response' }); } /** diff --git a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts index 5a8446607dd6..b0c848d9cfb6 100644 --- a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts +++ b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts @@ -335,6 +335,7 @@ export class NavbarComponent implements OnInit, OnDestroy { suspicious_behavior: 'artemisApp.examManagement.suspiciousBehavior.title', suspicious_sessions: 'artemisApp.examManagement.suspiciousBehavior.suspiciousSessions.title', exam_timeline: 'artemisApp.examTimeline.breadcrumb', + iris_settings: 'artemisApp.iris.settings.title.breadcrumb', }; studentPathBreadcrumbTranslations = { diff --git a/src/main/webapp/i18n/de/iris.json b/src/main/webapp/i18n/de/iris.json index 19c996e4af7c..c6d4524e917c 100644 --- a/src/main/webapp/i18n/de/iris.json +++ b/src/main/webapp/i18n/de/iris.json @@ -22,22 +22,56 @@ "subSettings": { "chatSettings": "Chat Einstellungen", "hestiaSettings": "Hestia Einstellungen", - "inheritChatSettings": "Vererbe Chat Einstellungen", - "inheritHestiaSettings": "Vererbe Hestia Einstellungen", + "codeEditorSettings": "Code Editor Einstellungen", "enabled-disabled": "Aktiviert/Deaktiviert", - "preferredModel": "Präferiertes Modell", - "inheritModel": "Vererbe Modell", + "models": { + "title": "Modelle", + "allowedModels": { + "title": "Erlaubte Modelle", + "inheritSwitch": "Erbe erlaubte Modelle" + }, + "preferredModel": { + "title": "Präferiertes Modell", + "inherit": "Erben" + } + }, "rateLimit": "Rate Limit", "rateLimitTooltip": "Die maximale Anzahl an Antworten, die ein Benutzer vom LLM in einem bestimmten Zeitraum erhalten kann.", + "rateLimitTimeframeHours": "Rate Limit Zeitrahmen (Stunden)", + "rateLimitTimeframeHoursTooltip": "Der Zeitraum, in welchem das Rate Limit angewendet wird, in Stunden.", "template": { "title": "Template", - "inherit": "Inherit Template" + "inherit": "Template erben" + }, + "codeEditor": { + "templates": "Templates", + "templatesInheritSwitch": "Templates erben", + "chatTemplate": "Chat Template", + "problemStatementTemplate": "Problem Statement Generations Template", + "templateRepoTemplate": "Vorlagen Repository Generations Template", + "solutionRepoTemplate": "Lösungs Repository Generations Template", + "testRepoTemplate": "Test Repository Generations Template" + }, + "enabled": { + "on": "Aktiviert", + "off": "Deaktiviert", + "chat": "Iris Chat", + "codeEditor": "Programmieraufgaben Erstellungs Chat", + "hestia": "Hestia Integration" } }, "title": { + "breadcrumb": "Iris Einstellungen", "global": "Globale Iris Einstellungen", "course": "Kurs Iris Einstellungen", "programmingExercise": "Programmieraufgabe Iris Einstellungen" + }, + "autoUpdate": { + "title": "Auto Update Einstellungen", + "tooltip": "Wenn aktiviert, werden die spezifischen globalen Iris Einstellungen automatisch aktualisiert, wenn eine neue Version von Artemis neue Iris Einstellungen bereitstellt.", + "chatLabel": "Auto Update der Chat Einstellungen", + "hestiaLabel": "Auto Update der Hestia Einstellungen", + "codeEditorLabel": "Auto Update der Code Editor Einstellungen" } }, "error": { diff --git a/src/main/webapp/i18n/en/iris.json b/src/main/webapp/i18n/en/iris.json index 9b7f9cdced0c..29e18750e4e2 100644 --- a/src/main/webapp/i18n/en/iris.json +++ b/src/main/webapp/i18n/en/iris.json @@ -22,11 +22,19 @@ "subSettings": { "chatSettings": "Chat Settings", "hestiaSettings": "Hestia Settings", - "inheritChatSettings": "Inherit Chat Settings", - "inheritHestiaSettings": "Inherit Hestia Settings", + "codeEditorSettings": "Code Editor Settings", "enabled-disabled": "Enabled/Disabled", - "preferredModel": "Preferred Model", - "inheritModel": "Inherit Model", + "models": { + "title": "Models", + "allowedModels": { + "title": "Allowed Models", + "inheritSwitch": "Inherit Allowed Models" + }, + "preferredModel": { + "title": "Preferred Model", + "inherit": "Inherit" + } + }, "rateLimit": "Rate Limit", "rateLimitTooltip": "The maximum number of answers a user can receive from the LLM in a given time period.", "rateLimitTimeframeHours": "Rate Limit Timeframe (Hours)", @@ -34,12 +42,36 @@ "template": { "title": "Template", "inherit": "Inherit Template" + }, + "codeEditor": { + "templates": "Templates", + "templatesInheritSwitch": "Inherit Templates", + "chatTemplate": "Chat Template", + "problemStatementTemplate": "Problem Statement Generation Template", + "templateRepoTemplate": "Template Repository Generation Template", + "solutionRepoTemplate": "Solution Repository Generation Template", + "testRepoTemplate": "Test Repository Generation Template" + }, + "enabled": { + "on": "Enabled", + "off": "Disabled", + "chat": "Iris Chat", + "codeEditor": "Programming Exercise Creation Chat", + "hestia": "Hestia Integration" } }, "title": { + "breadcrumb": "Iris Settings", "global": "Global Iris Settings", "course": "Course Iris Settings", - "programmingExercise": "Programming Exercise Iris Settings" + "exercise": "Exercise Iris Settings" + }, + "autoUpdate": { + "title": "Auto Update Settings", + "tooltip": "If enabled, the specific global Iris settings will be automatically updated when a new release of Artemis provides new Iris settings.", + "chatLabel": "Auto Update Chat Settings", + "hestiaLabel": "Auto Update Hestia Settings", + "codeEditorLabel": "Auto Update Code Editor Settings" } }, "error": { diff --git a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java index 0b1122192691..97ff27924d3f 100644 --- a/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java +++ b/src/test/java/de/tum/in/www1/artemis/course/CourseTestService.java @@ -88,7 +88,6 @@ import de.tum.in.www1.artemis.service.dto.UserPublicInfoDTO; import de.tum.in.www1.artemis.service.export.CourseExamExportService; import de.tum.in.www1.artemis.service.export.DataExportUtil; -import de.tum.in.www1.artemis.service.iris.IrisSettingsService; import de.tum.in.www1.artemis.service.notifications.GroupNotificationService; import de.tum.in.www1.artemis.service.scheduled.ParticipantScoreScheduleService; import de.tum.in.www1.artemis.team.TeamUtilService; @@ -231,9 +230,6 @@ public class CourseTestService { @Autowired private ParticipantScoreScheduleService participantScoreScheduleService; - @Autowired - private IrisSettingsService irisSettingsService; - @Autowired private QuizExerciseUtilService quizExerciseUtilService; @@ -627,23 +623,6 @@ public void testEditCourseShouldPreserveAssociations() throws Exception { assertThat(updatedCourse.getPrerequisites()).containsExactlyElementsOf(prerequisites); } - // Test - public void testEditCourseShouldPreserveIrisSettings() throws Exception { - Course course = courseUtilService.createCourseWithOrganizations(); - course = courseRepo.save(course); - - var courseWithSettings = courseRepo.findByIdElseThrow(course.getId()); - courseWithSettings = irisSettingsService.addDefaultIrisSettingsTo(courseWithSettings); - courseWithSettings.getIrisSettings().getIrisChatSettings().setEnabled(true); - courseWithSettings.getIrisSettings().getIrisChatSettings().setPreferredModel(null); - courseRepo.save(courseWithSettings); - - request.getMvc().perform(buildUpdateCourse(course.getId(), course)).andExpect(status().isOk()); - - Course updatedCourse = courseRepo.findByIdForUpdateElseThrow(course.getId()); - assertThat(updatedCourse.getIrisSettings()).isEqualTo(courseWithSettings.getIrisSettings()); - } - // Test public void testUpdateCourseGroups() throws Exception { Course course = programmingExerciseUtilService.addCourseWithOneProgrammingExercise(); diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java index 94e289d0a201..b50ac0b2f696 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseBitbucketBambooJiraIntegrationTest.java @@ -172,12 +172,6 @@ void testEditCourseShouldPreserveAssociations() throws Exception { courseTestService.testEditCourseShouldPreserveAssociations(); } - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testEditCourseShouldPreserveIrisSettings() throws Exception { - courseTestService.testEditCourseShouldPreserveIrisSettings(); - } - @Test @WithMockUser(username = "admin", roles = "ADMIN") void testUpdateCourseGroups() throws Exception { diff --git a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseGitlabJenkinsIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseGitlabJenkinsIntegrationTest.java index 8520826dc12a..603181228f25 100644 --- a/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseGitlabJenkinsIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exercise/programmingexercise/CourseGitlabJenkinsIntegrationTest.java @@ -171,12 +171,6 @@ void testEditCourseShouldPreserveAssociations() throws Exception { courseTestService.testEditCourseShouldPreserveAssociations(); } - @Test - @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") - void testEditCourseShouldPreserveIrisSettings() throws Exception { - courseTestService.testEditCourseShouldPreserveIrisSettings(); - } - @Test @WithMockUser(username = "admin", roles = "ADMIN") void testUpdateCourseGroups() throws Exception { diff --git a/src/test/java/de/tum/in/www1/artemis/iris/AbstractIrisIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/iris/AbstractIrisIntegrationTest.java index 92bfc7737093..b1b67f04d2d6 100644 --- a/src/test/java/de/tum/in/www1/artemis/iris/AbstractIrisIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/iris/AbstractIrisIntegrationTest.java @@ -25,9 +25,10 @@ import de.tum.in.www1.artemis.exercise.programmingexercise.ProgrammingExerciseUtilService; import de.tum.in.www1.artemis.repository.CourseRepository; import de.tum.in.www1.artemis.repository.ProgrammingExerciseRepository; +import de.tum.in.www1.artemis.repository.iris.IrisSettingsRepository; import de.tum.in.www1.artemis.repository.iris.IrisTemplateRepository; -import de.tum.in.www1.artemis.service.iris.IrisSettingsService; import de.tum.in.www1.artemis.service.iris.IrisWebsocketService; +import de.tum.in.www1.artemis.service.iris.settings.IrisSettingsService; import de.tum.in.www1.artemis.user.UserUtilService; public abstract class AbstractIrisIntegrationTest extends AbstractSpringIntegrationBambooBitbucketJiraTest { @@ -54,6 +55,9 @@ public abstract class AbstractIrisIntegrationTest extends AbstractSpringIntegrat @Autowired protected ExerciseUtilService exerciseUtilService; + @Autowired + private IrisSettingsRepository irisSettingsRepository; + @Autowired protected ProgrammingExerciseUtilService programmingExerciseUtilService; @@ -75,26 +79,33 @@ protected void activateIrisGlobally() { globalSettings.getIrisChatSettings().setPreferredModel(null); globalSettings.getIrisHestiaSettings().setEnabled(true); globalSettings.getIrisHestiaSettings().setPreferredModel(null); - irisSettingsService.saveGlobalIrisSettings(globalSettings); + irisSettingsRepository.save(globalSettings); } protected void activateIrisFor(Course course) { - var courseWithSettings = irisSettingsService.addDefaultIrisSettingsTo(course); - courseWithSettings.getIrisSettings().getIrisChatSettings().setEnabled(true); - courseWithSettings.getIrisSettings().getIrisChatSettings().setTemplate(createDummyTemplate()); - courseWithSettings.getIrisSettings().getIrisChatSettings().setPreferredModel(null); - courseWithSettings.getIrisSettings().getIrisHestiaSettings().setEnabled(true); - courseWithSettings.getIrisSettings().getIrisHestiaSettings().setTemplate(createDummyTemplate()); - courseWithSettings.getIrisSettings().getIrisHestiaSettings().setPreferredModel(null); - courseRepository.save(courseWithSettings); + var courseSettings = irisSettingsService.getDefaultSettingsFor(course); + courseSettings.getIrisChatSettings().setEnabled(true); + courseSettings.getIrisChatSettings().setTemplate(createDummyTemplate()); + courseSettings.getIrisChatSettings().setPreferredModel(null); + courseSettings.getIrisHestiaSettings().setEnabled(true); + courseSettings.getIrisHestiaSettings().setTemplate(createDummyTemplate()); + courseSettings.getIrisHestiaSettings().setPreferredModel(null); + courseSettings.getIrisCodeEditorSettings().setEnabled(true); + courseSettings.getIrisCodeEditorSettings().setChatTemplate(createDummyTemplate()); + courseSettings.getIrisCodeEditorSettings().setProblemStatementGenerationTemplate(createDummyTemplate()); + courseSettings.getIrisCodeEditorSettings().setTemplateRepoGenerationTemplate(null); + courseSettings.getIrisCodeEditorSettings().setSolutionRepoGenerationTemplate(null); + courseSettings.getIrisCodeEditorSettings().setTestRepoGenerationTemplate(null); + courseSettings.getIrisCodeEditorSettings().setPreferredModel(null); + irisSettingsRepository.save(courseSettings); } protected void activateIrisFor(ProgrammingExercise exercise) { - var exerciseWithSettings = irisSettingsService.addDefaultIrisSettingsTo(exercise); - exerciseWithSettings.getIrisSettings().getIrisChatSettings().setEnabled(true); - exerciseWithSettings.getIrisSettings().getIrisChatSettings().setTemplate(createDummyTemplate()); - exerciseWithSettings.getIrisSettings().getIrisChatSettings().setPreferredModel(null); - programmingExerciseRepository.save(exerciseWithSettings); + var exerciseSettings = irisSettingsService.getDefaultSettingsFor(exercise); + exerciseSettings.getIrisChatSettings().setEnabled(true); + exerciseSettings.getIrisChatSettings().setTemplate(createDummyTemplate()); + exerciseSettings.getIrisChatSettings().setPreferredModel(null); + irisSettingsRepository.save(exerciseSettings); } protected IrisTemplate createDummyTemplate() { diff --git a/src/test/java/de/tum/in/www1/artemis/iris/IrisHestiaIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/iris/IrisHestiaIntegrationTest.java index bc72a7aa3e31..01c3ada4b1d6 100644 --- a/src/test/java/de/tum/in/www1/artemis/iris/IrisHestiaIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/iris/IrisHestiaIntegrationTest.java @@ -30,6 +30,7 @@ void initTestCase() { final Course course = programmingExerciseUtilService.addCourseWithOneProgrammingExerciseAndTestCases(); exercise = exerciseUtilService.getFirstExerciseWithType(course, ProgrammingExercise.class); + activateIrisGlobally(); activateIrisFor(course); activateIrisFor(exercise); } diff --git a/src/test/java/de/tum/in/www1/artemis/iris/IrisMessageIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/iris/IrisMessageIntegrationTest.java index 3f26ddb6170e..a857f9192aca 100644 --- a/src/test/java/de/tum/in/www1/artemis/iris/IrisMessageIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/iris/IrisMessageIntegrationTest.java @@ -27,7 +27,6 @@ import de.tum.in.www1.artemis.repository.iris.IrisMessageRepository; import de.tum.in.www1.artemis.repository.iris.IrisSessionRepository; import de.tum.in.www1.artemis.service.iris.IrisMessageService; -import de.tum.in.www1.artemis.service.iris.IrisRateLimitService; import de.tum.in.www1.artemis.service.iris.IrisSessionService; import de.tum.in.www1.artemis.util.IrisUtilTestService; import de.tum.in.www1.artemis.util.LocalRepository; @@ -54,9 +53,6 @@ class IrisMessageIntegrationTest extends AbstractIrisIntegrationTest { @Autowired private ParticipationUtilService participationUtilService; - @Autowired - private IrisRateLimitService irisRateLimitService; - private ProgrammingExercise exercise; private LocalRepository repository; @@ -67,6 +63,7 @@ void initTestCase() { final Course course = programmingExerciseUtilService.addCourseWithOneProgrammingExerciseAndTestCases(); exercise = exerciseUtilService.getFirstExerciseWithType(course, ProgrammingExercise.class); + activateIrisGlobally(); activateIrisFor(course); activateIrisFor(exercise); repository = new LocalRepository("main"); @@ -296,7 +293,7 @@ void sendMessageRateLimitReached() throws Exception { var globalSettings = irisSettingsService.getGlobalSettings(); globalSettings.getIrisChatSettings().setRateLimit(1); globalSettings.getIrisChatSettings().setRateLimitTimeframeHours(10); - irisSettingsService.saveGlobalIrisSettings(globalSettings); + irisSettingsService.saveIrisSettings(globalSettings); request.postWithResponseBody("/api/iris/sessions/" + irisSession.getId() + "/messages", messageToSend1, IrisMessage.class, HttpStatus.CREATED); await().until(() -> irisSessionRepository.findByIdWithMessagesElseThrow(irisSession.getId()).getMessages().size() == 2); @@ -311,14 +308,13 @@ void sendMessageRateLimitReached() throws Exception { // Reset to not interfere with other tests globalSettings.getIrisChatSettings().setRateLimit(null); globalSettings.getIrisChatSettings().setRateLimitTimeframeHours(null); - irisSettingsService.saveGlobalIrisSettings(globalSettings); + irisSettingsService.saveIrisSettings(globalSettings); } private void setupExercise() throws Exception { var savedExercise = irisUtilTestService.setupTemplate(exercise, repository); var exerciseParticipation = participationUtilService.addStudentParticipationForProgrammingExercise(savedExercise, TEST_PREFIX + "student1"); irisUtilTestService.setupStudentParticipation(exerciseParticipation, repository); - activateIrisFor(savedExercise); } private IrisMessage createDefaultMockMessage(IrisSession irisSession) { diff --git a/src/test/java/de/tum/in/www1/artemis/iris/IrisSessionActivationIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/iris/IrisSessionActivationIntegrationTest.java index fb5ed418b12f..589110fa5453 100644 --- a/src/test/java/de/tum/in/www1/artemis/iris/IrisSessionActivationIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/iris/IrisSessionActivationIntegrationTest.java @@ -37,6 +37,7 @@ void initTestCase() { final Course course = programmingExerciseUtilService.addCourseWithOneProgrammingExerciseAndTestCases(); exercise = exerciseUtilService.getFirstExerciseWithType(course, ProgrammingExercise.class); + activateIrisGlobally(); activateIrisFor(course); } diff --git a/src/test/java/de/tum/in/www1/artemis/iris/IrisSessionIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/iris/IrisSessionIntegrationTest.java index a84fd55b6d17..d7fc6f98d995 100644 --- a/src/test/java/de/tum/in/www1/artemis/iris/IrisSessionIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/iris/IrisSessionIntegrationTest.java @@ -77,16 +77,16 @@ void isActive() throws Exception { var previousPreferredModel = settings.getIrisChatSettings().getPreferredModel(); settings.getIrisChatSettings().setPreferredModel("TEST_MODEL_UP"); - irisSettingsService.saveGlobalIrisSettings(settings); + irisSettingsService.saveIrisSettings(settings); assertThat(request.get("/api/iris/sessions/" + irisSession.getId() + "/active", HttpStatus.OK, IrisHealthDTO.class).active()).isTrue(); settings.getIrisChatSettings().setPreferredModel("TEST_MODEL_DOWN"); - irisSettingsService.saveGlobalIrisSettings(settings); + irisSettingsService.saveIrisSettings(settings); assertThat(request.get("/api/iris/sessions/" + irisSession.getId() + "/active", HttpStatus.OK, IrisHealthDTO.class).active()).isFalse(); settings.getIrisChatSettings().setPreferredModel("TEST_MODEL_NA"); - irisSettingsService.saveGlobalIrisSettings(settings); + irisSettingsService.saveIrisSettings(settings); assertThat(request.get("/api/iris/sessions/" + irisSession.getId() + "/active", HttpStatus.OK, IrisHealthDTO.class).active()).isFalse(); settings.getIrisChatSettings().setPreferredModel(previousPreferredModel); - irisSettingsService.saveGlobalIrisSettings(settings); + irisSettingsService.saveIrisSettings(settings); } } diff --git a/src/test/java/de/tum/in/www1/artemis/iris/settings/IrisSettingsIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/iris/settings/IrisSettingsIntegrationTest.java index 39f292072d55..84d725911885 100644 --- a/src/test/java/de/tum/in/www1/artemis/iris/settings/IrisSettingsIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/iris/settings/IrisSettingsIntegrationTest.java @@ -2,6 +2,9 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.HashSet; +import java.util.TreeSet; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -10,11 +13,11 @@ import de.tum.in.www1.artemis.domain.Course; import de.tum.in.www1.artemis.domain.ProgrammingExercise; -import de.tum.in.www1.artemis.domain.iris.settings.IrisSettings; -import de.tum.in.www1.artemis.domain.iris.settings.IrisSubSettings; +import de.tum.in.www1.artemis.domain.iris.settings.*; import de.tum.in.www1.artemis.iris.AbstractIrisIntegrationTest; import de.tum.in.www1.artemis.repository.iris.IrisSettingsRepository; import de.tum.in.www1.artemis.repository.iris.IrisSubSettingsRepository; +import de.tum.in.www1.artemis.service.dto.iris.IrisCombinedSettingsDTO; class IrisSettingsIntegrationTest extends AbstractIrisIntegrationTest { @@ -43,12 +46,12 @@ void initTestCase() { void getMissingSettingsForCourse() throws Exception { activateIrisGlobally(); var loadedSettings1 = request.get("/api/courses/" + course.getId() + "/raw-iris-settings", HttpStatus.OK, IrisSettings.class); - var loadedSettings2 = request.get("/api/courses/" + course.getId() + "/iris-settings", HttpStatus.OK, IrisSettings.class); + var loadedSettings2 = request.get("/api/courses/" + course.getId() + "/iris-settings", HttpStatus.OK, IrisCombinedSettingsDTO.class); - assertThat(loadedSettings2).isNotNull().usingRecursiveComparison().ignoringFields("id", "irisChatSettings.id", "irisHestiaSettings.id") - .isEqualTo(irisSettingsService.getCombinedIrisSettings(course, false)); - assertThat(loadedSettings1).isNotNull().usingRecursiveComparison().ignoringFields("id", "irisChatSettings.id", "irisHestiaSettings.id") - .isEqualTo(irisSettingsService.addDefaultIrisSettingsTo(course).getIrisSettings()); + assertThat(loadedSettings2).isNotNull().usingRecursiveComparison().ignoringFieldsOfTypes(HashSet.class, TreeSet.class) + .isEqualTo(irisSettingsService.getCombinedIrisSettingsFor(course, false)); + assertThat(loadedSettings1).isNotNull().usingRecursiveComparison().ignoringFields("id", "course", "irisChatSettings.id", "irisHestiaSettings.id") + .isEqualTo(irisSettingsService.getDefaultSettingsFor(course)); } @Test @@ -59,10 +62,12 @@ void getCourseSettings() throws Exception { course = courseRepository.findByIdElseThrow(course.getId()); var loadedSettings1 = request.get("/api/courses/" + course.getId() + "/raw-iris-settings", HttpStatus.OK, IrisSettings.class); - var loadedSettings2 = request.get("/api/courses/" + course.getId() + "/iris-settings", HttpStatus.OK, IrisSettings.class); + var loadedSettings2 = request.get("/api/courses/" + course.getId() + "/iris-settings", HttpStatus.OK, IrisCombinedSettingsDTO.class); - assertThat(loadedSettings1).isNotNull().usingRecursiveComparison().ignoringFields("id", "irisChatSettings.id", "irisHestiaSettings.id").isEqualTo(loadedSettings2); - assertThat(loadedSettings1).isNotNull().usingRecursiveComparison().isEqualTo(irisSettingsRepository.findById(course.getIrisSettings().getId()).orElseThrow()); + assertThat(loadedSettings1).isNotNull().usingRecursiveComparison() + .ignoringFields("id", "course", "irisChatSettings.id", "irisHestiaSettings.id", "irisCodeEditorSettings.id").isEqualTo(loadedSettings2); + assertThat(loadedSettings1).isNotNull().usingRecursiveComparison().ignoringFields("course") + .isEqualTo(irisSettingsRepository.findCourseSettings(course.getId()).orElseThrow()); } @Test @@ -73,9 +78,9 @@ void getCourseSettingsAsUser() throws Exception { course = courseRepository.findByIdElseThrow(course.getId()); request.get("/api/courses/" + course.getId() + "/raw-iris-settings", HttpStatus.FORBIDDEN, IrisSettings.class); - var loadedSettings = request.get("/api/courses/" + course.getId() + "/iris-settings", HttpStatus.OK, IrisSettings.class); + var loadedSettings = request.get("/api/courses/" + course.getId() + "/iris-settings", HttpStatus.OK, IrisCombinedSettingsDTO.class); - assertThat(loadedSettings).isNotNull().usingRecursiveComparison().ignoringFields("id").isEqualTo(irisSettingsService.getCombinedIrisSettings(course, true)); + assertThat(loadedSettings).isNotNull().usingRecursiveComparison().ignoringFields("id").isEqualTo(irisSettingsService.getCombinedIrisSettingsFor(course, true)); } @Test @@ -117,8 +122,8 @@ void updateCourseSettings2() throws Exception { var updatedSettings = request.putWithResponseBody("/api/courses/" + course.getId() + "/raw-iris-settings", loadedSettings1, IrisSettings.class, HttpStatus.OK); var loadedSettings2 = request.get("/api/courses/" + course.getId() + "/raw-iris-settings", HttpStatus.OK, IrisSettings.class); - assertThat(updatedSettings).isNotNull().usingRecursiveComparison().isEqualTo(loadedSettings1); - assertThat(updatedSettings).isNotNull().usingRecursiveComparison().isEqualTo(loadedSettings2); + assertThat(updatedSettings).isNotNull().usingRecursiveComparison().ignoringFields("course").isEqualTo(loadedSettings1); + assertThat(updatedSettings).isNotNull().usingRecursiveComparison().ignoringFields("course").isEqualTo(loadedSettings2); // Original subsettings should not exist anymore assertThat(irisSubSettingsRepository.findById(chatSubSettingsId)).isEmpty(); assertThat(irisSubSettingsRepository.findById(hestiaSubSettingsId)).isEmpty(); @@ -130,23 +135,24 @@ void updateCourseSettings3() throws Exception { activateIrisGlobally(); course = courseRepository.findByIdElseThrow(course.getId()); - course.setIrisSettings(new IrisSettings()); - course.getIrisSettings().setIrisChatSettings(new IrisSubSettings()); - course.getIrisSettings().getIrisChatSettings().setEnabled(true); - course.getIrisSettings().getIrisChatSettings().setTemplate(createDummyTemplate()); - course.getIrisSettings().getIrisChatSettings().setPreferredModel(null); - course.getIrisSettings().setIrisHestiaSettings(new IrisSubSettings()); - course.getIrisSettings().getIrisHestiaSettings().setEnabled(true); - course.getIrisSettings().getIrisHestiaSettings().setTemplate(createDummyTemplate()); - course.getIrisSettings().getIrisHestiaSettings().setPreferredModel(null); - - var updatedSettings = request.putWithResponseBody("/api/courses/" + course.getId() + "/raw-iris-settings", course.getIrisSettings(), IrisSettings.class, HttpStatus.OK); + var courseSettings = new IrisCourseSettings(); + courseSettings.setCourse(course); + courseSettings.setIrisChatSettings(new IrisChatSubSettings()); + courseSettings.getIrisChatSettings().setEnabled(true); + courseSettings.getIrisChatSettings().setTemplate(createDummyTemplate()); + courseSettings.getIrisChatSettings().setPreferredModel(null); + courseSettings.setIrisHestiaSettings(new IrisHestiaSubSettings()); + courseSettings.getIrisHestiaSettings().setEnabled(true); + courseSettings.getIrisHestiaSettings().setTemplate(createDummyTemplate()); + courseSettings.getIrisHestiaSettings().setPreferredModel(null); + + var updatedSettings = request.putWithResponseBody("/api/courses/" + course.getId() + "/raw-iris-settings", courseSettings, IrisSettings.class, HttpStatus.OK); var loadedSettings1 = request.get("/api/courses/" + course.getId() + "/raw-iris-settings", HttpStatus.OK, IrisSettings.class); - assertThat(updatedSettings).isNotNull().isEqualTo(loadedSettings1); + assertThat(updatedSettings).usingRecursiveComparison().ignoringFields("course").isEqualTo(loadedSettings1); assertThat(loadedSettings1).usingRecursiveComparison() - .ignoringFields("id", "irisChatSettings.id", "irisHestiaSettings.id", "irisChatSettings.template.id", "irisHestiaSettings.template.id") - .isEqualTo(course.getIrisSettings()); + .ignoringFields("id", "course", "irisChatSettings.id", "irisHestiaSettings.id", "irisChatSettings.template.id", "irisHestiaSettings.template.id") + .isEqualTo(courseSettings); } @Test @@ -155,14 +161,14 @@ void getMissingSettingsForProgrammingExercise() throws Exception { activateIrisGlobally(); activateIrisFor(course); var loadedSettings1 = request.get("/api/programming-exercises/" + programmingExercise.getId() + "/raw-iris-settings", HttpStatus.OK, IrisSettings.class); - var loadedSettings2 = request.get("/api/programming-exercises/" + programmingExercise.getId() + "/iris-settings", HttpStatus.OK, IrisSettings.class); + var loadedSettings2 = request.get("/api/programming-exercises/" + programmingExercise.getId() + "/iris-settings", HttpStatus.OK, IrisCombinedSettingsDTO.class); programmingExercise = programmingExerciseRepository.findByIdElseThrow(programmingExercise.getId()); assertThat(loadedSettings2).isNotNull().usingRecursiveComparison().ignoringFields("id", "irisChatSettings.id", "irisHestiaSettings.id") - .isEqualTo(irisSettingsService.getCombinedIrisSettings(programmingExercise, false)); - assertThat(loadedSettings1).isNotNull().usingRecursiveComparison().ignoringFields("id", "irisChatSettings.id", "irisHestiaSettings.id") - .isEqualTo(irisSettingsService.addDefaultIrisSettingsTo(programmingExercise).getIrisSettings()); + .ignoringFieldsOfTypes(HashSet.class, TreeSet.class).isEqualTo(irisSettingsService.getCombinedIrisSettingsFor(programmingExercise, false)); + assertThat(loadedSettings1).isNotNull().usingRecursiveComparison().ignoringFields("id", "exercise", "irisChatSettings.id", "irisHestiaSettings.id") + .isEqualTo(irisSettingsService.getDefaultSettingsFor(programmingExercise)); } @Test @@ -174,11 +180,13 @@ void getProgrammingExerciseSettings() throws Exception { programmingExercise = programmingExerciseRepository.findByIdElseThrow(programmingExercise.getId()); var loadedSettings1 = request.get("/api/programming-exercises/" + programmingExercise.getId() + "/raw-iris-settings", HttpStatus.OK, IrisSettings.class); - var loadedSettings2 = request.get("/api/programming-exercises/" + programmingExercise.getId() + "/iris-settings", HttpStatus.OK, IrisSettings.class); + var loadedSettings2 = request.get("/api/programming-exercises/" + programmingExercise.getId() + "/iris-settings", HttpStatus.OK, IrisCombinedSettingsDTO.class); - assertThat(loadedSettings1).isNotNull().usingRecursiveComparison().ignoringFields("id", "irisChatSettings.id", "irisHestiaSettings").isEqualTo(loadedSettings2); + assertThat(loadedSettings1).isNotNull().usingRecursiveComparison() + .ignoringFields("id", "exercise", "irisChatSettings.id", "irisHestiaSettings.id", "irisCodeEditorSettings.id").isEqualTo(loadedSettings2); assertThat(loadedSettings1.getIrisHestiaSettings()).isNull(); - assertThat(loadedSettings1).isNotNull().usingRecursiveComparison().isEqualTo(irisSettingsRepository.findById(programmingExercise.getIrisSettings().getId()).orElseThrow()); + assertThat(loadedSettings1).isNotNull().usingRecursiveComparison().ignoringFields("exercise") + .isEqualTo(irisSettingsRepository.findExerciseSettings(programmingExercise.getId()).orElseThrow()); } @Test @@ -190,9 +198,9 @@ void getProgrammingExerciseSettingsAsUser() throws Exception { programmingExercise = programmingExerciseRepository.findByIdElseThrow(programmingExercise.getId()); request.get("/api/programming-exercises/" + programmingExercise.getId() + "/raw-iris-settings", HttpStatus.FORBIDDEN, IrisSettings.class); - var loadedSettings = request.get("/api/programming-exercises/" + programmingExercise.getId() + "/iris-settings", HttpStatus.OK, IrisSettings.class); + var loadedSettings = request.get("/api/programming-exercises/" + programmingExercise.getId() + "/iris-settings", HttpStatus.OK, IrisCombinedSettingsDTO.class); - assertThat(loadedSettings).isNotNull().usingRecursiveComparison().ignoringFields("id").isEqualTo(irisSettingsService.getCombinedIrisSettings(programmingExercise, true)); + assertThat(loadedSettings).isNotNull().usingRecursiveComparison().ignoringFields("id").isEqualTo(irisSettingsService.getCombinedIrisSettingsFor(programmingExercise, true)); } @Test @@ -235,8 +243,8 @@ void updateProgrammingExerciseSettings2() throws Exception { HttpStatus.OK); var loadedSettings2 = request.get("/api/programming-exercises/" + programmingExercise.getId() + "/raw-iris-settings", HttpStatus.OK, IrisSettings.class); - assertThat(updatedSettings).isNotNull().usingRecursiveComparison().isEqualTo(loadedSettings1); - assertThat(updatedSettings).isNotNull().usingRecursiveComparison().isEqualTo(loadedSettings2); + assertThat(updatedSettings).isNotNull().usingRecursiveComparison().ignoringFields("exercise").isEqualTo(loadedSettings1); + assertThat(updatedSettings).isNotNull().usingRecursiveComparison().ignoringFields("exercise").isEqualTo(loadedSettings2); // Original subsettings should not exist anymore assertThat(irisSubSettingsRepository.findById(chatSubSettingsId)).isEmpty(); } @@ -248,23 +256,20 @@ void updateProgrammingExerciseSettings3() throws Exception { activateIrisFor(course); programmingExercise = programmingExerciseRepository.findByIdElseThrow(programmingExercise.getId()); - programmingExercise.setIrisSettings(new IrisSettings()); - programmingExercise.getIrisSettings().setIrisChatSettings(new IrisSubSettings()); - programmingExercise.getIrisSettings().getIrisChatSettings().setEnabled(true); - programmingExercise.getIrisSettings().getIrisChatSettings().setTemplate(createDummyTemplate()); - programmingExercise.getIrisSettings().getIrisChatSettings().setPreferredModel(null); - programmingExercise.getIrisSettings().setIrisHestiaSettings(new IrisSubSettings()); - programmingExercise.getIrisSettings().getIrisHestiaSettings().setEnabled(true); - programmingExercise.getIrisSettings().getIrisHestiaSettings().setTemplate(createDummyTemplate()); - programmingExercise.getIrisSettings().getIrisHestiaSettings().setPreferredModel(null); - - var updatedSettings = request.putWithResponseBody("/api/programming-exercises/" + programmingExercise.getId() + "/raw-iris-settings", programmingExercise.getIrisSettings(), - IrisSettings.class, HttpStatus.OK); + var exerciseSettings = new IrisExerciseSettings(); + exerciseSettings.setExercise(programmingExercise); + exerciseSettings.setIrisChatSettings(new IrisChatSubSettings()); + exerciseSettings.getIrisChatSettings().setEnabled(true); + exerciseSettings.getIrisChatSettings().setTemplate(createDummyTemplate()); + exerciseSettings.getIrisChatSettings().setPreferredModel(null); + + var updatedSettings = request.putWithResponseBody("/api/programming-exercises/" + programmingExercise.getId() + "/raw-iris-settings", exerciseSettings, IrisSettings.class, + HttpStatus.OK); var loadedSettings1 = request.get("/api/programming-exercises/" + programmingExercise.getId() + "/raw-iris-settings", HttpStatus.OK, IrisSettings.class); assertThat(updatedSettings).isNotNull().isEqualTo(loadedSettings1); assertThat(loadedSettings1).usingRecursiveComparison() - .ignoringFields("id", "irisChatSettings.id", "irisHestiaSettings.id", "irisChatSettings.template.id", "irisHestiaSettings.template.id") - .isEqualTo(programmingExercise.getIrisSettings()); + .ignoringFields("id", "exercise", "irisChatSettings.id", "irisHestiaSettings.id", "irisChatSettings.template.id", "irisHestiaSettings.template.id") + .isEqualTo(exerciseSettings); } } diff --git a/src/test/javascript/spec/component/iris/settings/iris-sub-settings-update.component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-chat-sub-settings-update.component.spec.ts similarity index 55% rename from src/test/javascript/spec/component/iris/settings/iris-sub-settings-update.component.spec.ts rename to src/test/javascript/spec/component/iris/settings/iris-chat-sub-settings-update.component.spec.ts index bab069444247..e738583f2b4f 100644 --- a/src/test/javascript/spec/component/iris/settings/iris-sub-settings-update.component.spec.ts +++ b/src/test/javascript/spec/component/iris/settings/iris-chat-sub-settings-update.component.spec.ts @@ -2,50 +2,34 @@ import { ArtemisTestModule } from '../../../test.module'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; -import { IrisSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; -import { IrisSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-sub-settings-update/iris-sub-settings-update.component'; -import { IrisModel } from 'app/entities/iris/settings/iris-model'; +import { IrisChatSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { MockDirective } from 'ng-mocks'; +import { IrisChatSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component'; +import { SimpleChange, SimpleChanges } from '@angular/core'; function baseSettings() { const mockTemplate = new IrisTemplate(); mockTemplate.id = 1; mockTemplate.content = 'Hello World'; - const irisSubSettings = new IrisSubSettings(); + const irisSubSettings = new IrisChatSubSettings(); irisSubSettings.id = 2; irisSubSettings.template = mockTemplate; irisSubSettings.enabled = true; return irisSubSettings; } -function models() { - return [ - { - id: '1', - name: 'Model 1', - description: 'Model 1 Description', - }, - { - id: '2', - name: 'Model 2', - description: 'Model 2 Description', - }, - ] as IrisModel[]; -} - -describe('IrisSubSettingsUpdateComponent Component', () => { - let comp: IrisSubSettingsUpdateComponent; - let fixture: ComponentFixture; +describe('IrisChatSubSettingsUpdateComponent Component', () => { + let comp: IrisChatSubSettingsUpdateComponent; + let fixture: ComponentFixture; beforeEach(() => { TestBed.configureTestingModule({ imports: [ArtemisTestModule, FormsModule, MockDirective(NgbTooltip)], - declarations: [IrisSubSettingsUpdateComponent], + declarations: [IrisChatSubSettingsUpdateComponent], }).compileComponents(); - fixture = TestBed.createComponent(IrisSubSettingsUpdateComponent); + fixture = TestBed.createComponent(IrisChatSubSettingsUpdateComponent); comp = fixture.componentInstance; - comp.models = models(); }); afterEach(() => { @@ -54,45 +38,35 @@ describe('IrisSubSettingsUpdateComponent Component', () => { it('template is not optional', () => { comp.subSettings = baseSettings(); - comp.templateOptional = false; fixture.detectChanges(); expect(fixture.debugElement.nativeElement.querySelector('#inheritTemplate')).toBeFalsy(); + expect(fixture.debugElement.nativeElement.querySelector('#template-editor')).toBeTruthy(); }); - it('template is optional and defined', () => { + it('template is optional', () => { comp.subSettings = baseSettings(); - comp.templateOptional = true; + comp.parentSubSettings = baseSettings(); fixture.detectChanges(); expect(fixture.debugElement.nativeElement.querySelector('#inheritTemplate')).toBeTruthy(); expect(fixture.debugElement.nativeElement.querySelector('#template-editor')).toBeTruthy(); }); - it('template is optional and undefined', () => { - const subSettings = baseSettings(); - subSettings.template = undefined; - comp.subSettings = subSettings; - comp.templateOptional = true; - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement.querySelector('#inheritTemplate')).toBeTruthy(); - expect(fixture.debugElement.nativeElement.querySelector('#template-editor')).toBeFalsy(); - }); - it('template is optional and changes from defined to undefined', () => { comp.subSettings = baseSettings(); - comp.templateOptional = true; + comp.parentSubSettings = baseSettings(); fixture.detectChanges(); comp.onInheritTemplateChanged(); fixture.detectChanges(); expect(comp.subSettings.template).toBeUndefined(); expect(fixture.debugElement.nativeElement.querySelector('#inheritTemplate')).toBeTruthy(); - expect(fixture.debugElement.nativeElement.querySelector('#template-editor')).toBeFalsy(); + expect(fixture.debugElement.nativeElement.querySelector('#template-editor')).toBeTruthy(); }); it('template is optional and changes from undefined to defined', () => { const subSettings = baseSettings(); subSettings.template = undefined; comp.subSettings = subSettings; - comp.templateOptional = true; + comp.parentSubSettings = baseSettings(); fixture.detectChanges(); comp.onInheritTemplateChanged(); fixture.detectChanges(); @@ -100,4 +74,38 @@ describe('IrisSubSettingsUpdateComponent Component', () => { expect(fixture.debugElement.nativeElement.querySelector('#inheritTemplate')).toBeTruthy(); expect(fixture.debugElement.nativeElement.querySelector('#template-editor')).toBeTruthy(); }); + + it('template changes', () => { + comp.subSettings = baseSettings(); + fixture.detectChanges(); + comp.templateContent = 'Hello World 2'; + comp.onTemplateChanged(); + + expect(comp.subSettings.template?.content).toBe('Hello World 2'); + }); + + it('template created', () => { + comp.subSettings = baseSettings(); + comp.subSettings.template = undefined; + fixture.detectChanges(); + comp.templateContent = 'Hello World 2'; + comp.onTemplateChanged(); + + expect(comp.subSettings.template!.content).toBe('Hello World 2'); + }); + + it('sub settings changes', () => { + comp.subSettings = baseSettings(); + fixture.detectChanges(); + const newSubSettings = baseSettings(); + newSubSettings.template!.content = 'Hello World 2'; + + const changes: SimpleChanges = { + subSettings: new SimpleChange(comp.subSettings, newSubSettings, false), + }; + comp.subSettings = newSubSettings; + comp.ngOnChanges(changes); + + expect(comp.templateContent).toBe('Hello World 2'); + }); }); diff --git a/src/test/javascript/spec/component/iris/settings/iris-code-editor-sub-settings-update.component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-code-editor-sub-settings-update.component.spec.ts new file mode 100644 index 000000000000..ec9f684e64ce --- /dev/null +++ b/src/test/javascript/spec/component/iris/settings/iris-code-editor-sub-settings-update.component.spec.ts @@ -0,0 +1,159 @@ +import { ArtemisTestModule } from '../../../test.module'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; +import { IrisCodeEditorSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; +import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; +import { MockDirective } from 'ng-mocks'; +import { SimpleChange, SimpleChanges } from '@angular/core'; +import { IrisCodeEditorSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-code-editor-sub-settings-update/iris-code-editor-sub-settings-update.component'; + +function mockTemplate(id: number) { + const mockTemplate = new IrisTemplate(); + mockTemplate.id = id; + mockTemplate.content = 'Hello World'; + return mockTemplate; +} + +function baseSettings() { + const irisSubSettings = new IrisCodeEditorSubSettings(); + irisSubSettings.id = 2; + irisSubSettings.chatTemplate = mockTemplate(1); + irisSubSettings.problemStatementGenerationTemplate = mockTemplate(2); + irisSubSettings.templateRepoGenerationTemplate = mockTemplate(3); + irisSubSettings.solutionRepoGenerationTemplate = mockTemplate(4); + irisSubSettings.testRepoGenerationTemplate = mockTemplate(5); + irisSubSettings.enabled = true; + return irisSubSettings; +} + +describe('IrisCodeEditorSubSettingsUpdateComponent Component', () => { + let comp: IrisCodeEditorSubSettingsUpdateComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, FormsModule, MockDirective(NgbTooltip)], + declarations: [IrisCodeEditorSubSettingsUpdateComponent], + }).compileComponents(); + fixture = TestBed.createComponent(IrisCodeEditorSubSettingsUpdateComponent); + comp = fixture.componentInstance; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('template is not optional', () => { + comp.subSettings = baseSettings(); + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.querySelector('#inheritTemplate')).toBeFalsy(); + expect(fixture.debugElement.nativeElement.querySelector('#chat-template-editor')).toBeTruthy(); + expect(fixture.debugElement.nativeElement.querySelector('#problem-statement-template-editor')).toBeTruthy(); + expect(fixture.debugElement.nativeElement.querySelector('#template-repo-template-editor')).toBeTruthy(); + expect(fixture.debugElement.nativeElement.querySelector('#solution-repo-template-editor')).toBeTruthy(); + expect(fixture.debugElement.nativeElement.querySelector('#test-repo-template-editor')).toBeTruthy(); + }); + + it('template is optional', () => { + comp.subSettings = baseSettings(); + comp.parentSubSettings = baseSettings(); + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.querySelector('#inheritTemplate')).toBeTruthy(); + expect(fixture.debugElement.nativeElement.querySelector('#chat-template-editor')).toBeTruthy(); + expect(fixture.debugElement.nativeElement.querySelector('#problem-statement-template-editor')).toBeTruthy(); + expect(fixture.debugElement.nativeElement.querySelector('#template-repo-template-editor')).toBeTruthy(); + expect(fixture.debugElement.nativeElement.querySelector('#solution-repo-template-editor')).toBeTruthy(); + expect(fixture.debugElement.nativeElement.querySelector('#test-repo-template-editor')).toBeTruthy(); + }); + + it('template is optional and changes from defined to undefined', () => { + comp.subSettings = baseSettings(); + comp.parentSubSettings = baseSettings(); + fixture.detectChanges(); + comp.onInheritTemplateChanged(); + fixture.detectChanges(); + expect(comp.subSettings.chatTemplate).toBeUndefined(); + expect(comp.subSettings.problemStatementGenerationTemplate).toBeUndefined(); + expect(comp.subSettings.templateRepoGenerationTemplate).toBeUndefined(); + expect(comp.subSettings.solutionRepoGenerationTemplate).toBeUndefined(); + expect(comp.subSettings.testRepoGenerationTemplate).toBeUndefined(); + }); + + it('template is optional and changes from undefined to defined', () => { + const subSettings = baseSettings(); + subSettings.chatTemplate = undefined; + subSettings.problemStatementGenerationTemplate = undefined; + subSettings.templateRepoGenerationTemplate = undefined; + subSettings.solutionRepoGenerationTemplate = undefined; + subSettings.testRepoGenerationTemplate = undefined; + comp.subSettings = subSettings; + comp.parentSubSettings = baseSettings(); + fixture.detectChanges(); + comp.onInheritTemplateChanged(); + fixture.detectChanges(); + expect(comp.subSettings.chatTemplate).toBeDefined(); + expect(comp.subSettings.problemStatementGenerationTemplate).toBeDefined(); + expect(comp.subSettings.templateRepoGenerationTemplate).toBeDefined(); + expect(comp.subSettings.solutionRepoGenerationTemplate).toBeDefined(); + expect(comp.subSettings.testRepoGenerationTemplate).toBeDefined(); + }); + + it('template changes', () => { + comp.subSettings = baseSettings(); + fixture.detectChanges(); + comp.chatTemplateContent = 'Hello World 2'; + comp.problemStatementGenerationTemplateContent = 'Hello World 3'; + comp.templateRepoGenerationTemplateContent = 'Hello World 4'; + comp.solutionRepoGenerationTemplateContent = 'Hello World 5'; + comp.testRepoGenerationTemplateContent = 'Hello World 6'; + comp.onTemplateChanged(); + + expect(comp.subSettings.chatTemplate?.content).toBe('Hello World 2'); + expect(comp.subSettings.problemStatementGenerationTemplate?.content).toBe('Hello World 3'); + expect(comp.subSettings.templateRepoGenerationTemplate?.content).toBe('Hello World 4'); + expect(comp.subSettings.solutionRepoGenerationTemplate?.content).toBe('Hello World 5'); + expect(comp.subSettings.testRepoGenerationTemplate?.content).toBe('Hello World 6'); + }); + + it('template created', () => { + comp.subSettings = baseSettings(); + comp.subSettings.chatTemplate = undefined; + fixture.detectChanges(); + comp.chatTemplateContent = 'Hello World 2'; + comp.problemStatementGenerationTemplateContent = 'Hello World 3'; + comp.templateRepoGenerationTemplateContent = 'Hello World 4'; + comp.solutionRepoGenerationTemplateContent = 'Hello World 5'; + comp.testRepoGenerationTemplateContent = 'Hello World 6'; + comp.onTemplateChanged(); + + expect(comp.subSettings.chatTemplate!.content).toBe('Hello World 2'); + expect(comp.subSettings.problemStatementGenerationTemplate!.content).toBe('Hello World 3'); + expect(comp.subSettings.templateRepoGenerationTemplate!.content).toBe('Hello World 4'); + expect(comp.subSettings.solutionRepoGenerationTemplate!.content).toBe('Hello World 5'); + expect(comp.subSettings.testRepoGenerationTemplate!.content).toBe('Hello World 6'); + }); + + it('sub settings changes', () => { + comp.subSettings = baseSettings(); + fixture.detectChanges(); + const newSubSettings = baseSettings(); + newSubSettings.chatTemplate!.content = 'Hello World 2'; + newSubSettings.problemStatementGenerationTemplate!.content = 'Hello World 3'; + newSubSettings.templateRepoGenerationTemplate!.content = 'Hello World 4'; + newSubSettings.solutionRepoGenerationTemplate!.content = 'Hello World 5'; + newSubSettings.testRepoGenerationTemplate!.content = 'Hello World 6'; + + const changes: SimpleChanges = { + subSettings: new SimpleChange(comp.subSettings, newSubSettings, false), + }; + comp.subSettings = newSubSettings; + comp.ngOnChanges(changes); + + expect(comp.chatTemplateContent).toBe('Hello World 2'); + expect(comp.problemStatementGenerationTemplateContent).toBe('Hello World 3'); + expect(comp.templateRepoGenerationTemplateContent).toBe('Hello World 4'); + expect(comp.solutionRepoGenerationTemplateContent).toBe('Hello World 5'); + expect(comp.testRepoGenerationTemplateContent).toBe('Hello World 6'); + }); +}); diff --git a/src/test/javascript/spec/component/iris/settings/iris-common-sub-settings-update.component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-common-sub-settings-update.component.spec.ts new file mode 100644 index 000000000000..ebb97b0be791 --- /dev/null +++ b/src/test/javascript/spec/component/iris/settings/iris-common-sub-settings-update.component.spec.ts @@ -0,0 +1,153 @@ +import { ArtemisTestModule } from '../../../test.module'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; +import { IrisChatSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; +import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; +import { MockDirective, MockPipe } from 'ng-mocks'; +import { SimpleChange, SimpleChanges } from '@angular/core'; +import { IrisCommonSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component'; +import { mockModels } from './mock-settings'; +import { IrisSettingsType } from 'app/entities/iris/settings/iris-settings.model'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; + +function baseSettings() { + const mockTemplate = new IrisTemplate(); + mockTemplate.id = 1; + mockTemplate.content = 'Hello World'; + const irisSubSettings = new IrisChatSubSettings(); + irisSubSettings.id = 2; + irisSubSettings.enabled = true; + const allowedModels = mockModels(); + allowedModels.pop(); + irisSubSettings.allowedModels = allowedModels.map((model) => model.id!); + irisSubSettings.preferredModel = allowedModels[0].id!; + return irisSubSettings; +} + +describe('IrisCommonSubSettingsUpdateComponent Component', () => { + let comp: IrisCommonSubSettingsUpdateComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, FormsModule, MockDirective(NgbTooltip), MockPipe(ArtemisTranslatePipe)], + declarations: [IrisCommonSubSettingsUpdateComponent], + }).compileComponents(); + fixture = TestBed.createComponent(IrisCommonSubSettingsUpdateComponent); + comp = fixture.componentInstance; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('child setup works', () => { + comp.subSettings = baseSettings(); + comp.parentSubSettings = baseSettings(); + comp.allIrisModels = mockModels(); + comp.settingsType = IrisSettingsType.EXERCISE; + fixture.detectChanges(); + + expect(comp.enabled).toBeTrue(); + expect(comp.inheritAllowedModels).toBeFalse(); + expect(comp.allowedIrisModels).toEqual([mockModels()[0]]); + }); + + it('parent setup works', () => { + const subSettings = baseSettings(); + subSettings.allowedModels = undefined; + subSettings.preferredModel = undefined; + comp.subSettings = subSettings; + comp.parentSubSettings = baseSettings(); + comp.allIrisModels = mockModels(); + comp.settingsType = IrisSettingsType.EXERCISE; + fixture.detectChanges(); + + expect(comp.enabled).toBeTrue(); + expect(comp.inheritAllowedModels).toBeTrue(); + expect(comp.allowedIrisModels).toEqual([mockModels()[0]]); + }); + + it('change allowed model', () => { + const allIrisModels = mockModels(); + comp.subSettings = baseSettings(); + comp.parentSubSettings = baseSettings(); + comp.allIrisModels = allIrisModels; + comp.settingsType = IrisSettingsType.EXERCISE; + fixture.detectChanges(); + + comp.onAllowedIrisModelsSelectionChange(allIrisModels[1]); + expect(comp.allowedIrisModels).toEqual([allIrisModels[0], allIrisModels[1]]); + comp.onAllowedIrisModelsSelectionChange(allIrisModels[0]); + expect(comp.allowedIrisModels).toEqual([allIrisModels[1]]); + }); + + it('change preferred model', () => { + comp.subSettings = baseSettings(); + comp.parentSubSettings = baseSettings(); + comp.allIrisModels = mockModels(); + comp.settingsType = IrisSettingsType.EXERCISE; + fixture.detectChanges(); + + comp.setModel(mockModels()[1]); + expect(comp.subSettings!.preferredModel).toBe(mockModels()[1].id); + }); + + it('change enabled', () => { + comp.subSettings = baseSettings(); + comp.parentSubSettings = baseSettings(); + comp.allIrisModels = mockModels(); + comp.settingsType = IrisSettingsType.EXERCISE; + fixture.detectChanges(); + + comp.enabled = false; + comp.onEnabledChange(); + expect(comp.subSettings!.enabled).toBeFalse(); + + comp.enabled = true; + comp.onEnabledChange(); + expect(comp.subSettings!.enabled).toBeTrue(); + }); + + it('change inherit allowed models', () => { + comp.subSettings = baseSettings(); + comp.parentSubSettings = baseSettings(); + comp.allIrisModels = mockModels(); + comp.settingsType = IrisSettingsType.EXERCISE; + fixture.detectChanges(); + + comp.inheritAllowedModels = true; + comp.onInheritAllowedModelsChange(); + expect(comp.subSettings!.allowedModels).toBeUndefined(); + expect(comp.allowedIrisModels).toEqual(comp.getAvailableModels()); + + comp.inheritAllowedModels = false; + comp.onInheritAllowedModelsChange(); + expect(comp.subSettings!.allowedModels).toEqual(comp.allowedIrisModels.map((model) => model.id)); + }); + + it('ngOnChanges works', () => { + comp.subSettings = baseSettings(); + comp.parentSubSettings = baseSettings(); + comp.allIrisModels = mockModels(); + comp.settingsType = IrisSettingsType.EXERCISE; + fixture.detectChanges(); + + const newSubSettings = baseSettings(); + newSubSettings.enabled = false; + const newModels = mockModels(); + newModels.pop(); + + const changes: SimpleChanges = { + subSettings: new SimpleChange(comp.subSettings, newSubSettings, false), + allIrisModels: new SimpleChange(comp.allIrisModels, newModels, false), + }; + comp.subSettings = newSubSettings; + comp.allIrisModels = mockModels(); + comp.ngOnChanges(changes); + + expect(comp.enabled).toBeFalse(); + expect(comp.allowedIrisModels).toEqual(newModels); + }); +}); diff --git a/src/test/javascript/spec/component/iris/settings/iris-course-settings-update.component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-course-settings-update.component.spec.ts new file mode 100644 index 000000000000..19add1e5a5cc --- /dev/null +++ b/src/test/javascript/spec/component/iris/settings/iris-course-settings-update.component.spec.ts @@ -0,0 +1,103 @@ +import { ArtemisTestModule } from '../../../test.module'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { IrisSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-settings-update.component'; +import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.service'; +import { MockComponent, MockDirective, MockProvider } from 'ng-mocks'; +import { BehaviorSubject, of } from 'rxjs'; +import { ButtonComponent } from 'app/shared/components/button.component'; +import { IrisChatSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component'; +import { IrisHestiaSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component'; +import { IrisCommonSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component'; +import { IrisGlobalAutoupdateSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component'; +import { mockModels, mockSettings } from './mock-settings'; +import { ActivatedRoute, Params } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgModel } from '@angular/forms'; +import { IrisCourseSettingsUpdateComponent } from 'app/iris/settings/iris-course-settings-update/iris-course-settings-update.component'; +import { By } from '@angular/platform-browser'; +import { IrisSettings } from 'app/entities/iris/settings/iris-settings.model'; +import { HttpResponse } from '@angular/common/http'; + +describe('IrisCourseSettingsUpdateComponent Component', () => { + let comp: IrisCourseSettingsUpdateComponent; + let fixture: ComponentFixture; + let irisSettingsService: IrisSettingsService; + const routeParamsSubject = new BehaviorSubject({ courseId: 1 }); + const route = { parent: { params: routeParamsSubject.asObservable() } } as ActivatedRoute; + let paramsSpy: jest.SpyInstance; + let getSettingsSpy: jest.SpyInstance; + let getModelsSpy: jest.SpyInstance; + let getParentSettingsSpy: jest.SpyInstance; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, RouterTestingModule], + declarations: [ + IrisCourseSettingsUpdateComponent, + IrisSettingsUpdateComponent, + MockComponent(IrisCommonSubSettingsUpdateComponent), + MockComponent(IrisChatSubSettingsUpdateComponent), + MockComponent(IrisHestiaSubSettingsUpdateComponent), + MockComponent(IrisGlobalAutoupdateSettingsUpdateComponent), + MockComponent(ButtonComponent), + MockDirective(NgModel), + ], + providers: [MockProvider(IrisSettingsService), { provide: ActivatedRoute, useValue: route }], + }) + .compileComponents() + .then(() => { + irisSettingsService = TestBed.inject(IrisSettingsService); + + // Setup + routeParamsSubject.next({ courseId: 1 }); + paramsSpy = jest.spyOn(route.parent!.params, 'subscribe'); + + const irisSettings = mockSettings(); + getSettingsSpy = jest.spyOn(irisSettingsService, 'getUncombinedCourseSettings').mockReturnValue(of(irisSettings)); + getModelsSpy = jest.spyOn(irisSettingsService, 'getIrisModels').mockReturnValue(of(mockModels())); + getParentSettingsSpy = jest.spyOn(irisSettingsService, 'getGlobalSettings').mockReturnValue(of(irisSettings)); + }); + fixture = TestBed.createComponent(IrisCourseSettingsUpdateComponent); + comp = fixture.componentInstance; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('Setup works correctly', () => { + fixture.detectChanges(); + expect(paramsSpy).toHaveBeenCalledOnce(); + expect(comp.courseId).toBe(1); + expect(comp.settingsUpdateComponent).toBeTruthy(); + expect(getSettingsSpy).toHaveBeenCalledWith(1); + expect(getModelsSpy).toHaveBeenCalledOnce(); + expect(getParentSettingsSpy).toHaveBeenCalledOnce(); + + expect(fixture.debugElement.query(By.directive(IrisGlobalAutoupdateSettingsUpdateComponent))).toBeFalsy(); + expect(fixture.debugElement.queryAll(By.directive(IrisCommonSubSettingsUpdateComponent))).toHaveLength(2); + expect(fixture.debugElement.query(By.directive(IrisChatSubSettingsUpdateComponent))).toBeTruthy(); + expect(fixture.debugElement.query(By.directive(IrisHestiaSubSettingsUpdateComponent))).toBeTruthy(); + }); + + it('Can deactivate correctly', () => { + fixture.detectChanges(); + expect(comp.canDeactivate()).toBeTrue(); + comp.settingsUpdateComponent!.isDirty = true; + expect(comp.canDeactivate()).toBeFalse(); + comp.settingsUpdateComponent!.canDeactivateWarning = 'Warning'; + expect(comp.canDeactivateWarning).toBe('Warning'); + }); + + it('Saves settings correctly', () => { + fixture.detectChanges(); + const irisSettings = mockSettings(); + irisSettings.id = undefined; + const irisSettingsSaved = mockSettings(); + const setSettingsSpy = jest.spyOn(irisSettingsService, 'setCourseSettings').mockReturnValue(of(new HttpResponse({ body: irisSettingsSaved }))); + comp.settingsUpdateComponent!.irisSettings = irisSettings; + comp.settingsUpdateComponent!.saveIrisSettings(); + expect(setSettingsSpy).toHaveBeenCalledWith(1, irisSettings); + expect(comp.settingsUpdateComponent!.irisSettings).toEqual(irisSettingsSaved); + }); +}); diff --git a/src/test/javascript/spec/component/iris/settings/iris-exercise-settings-update.component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-exercise-settings-update.component.spec.ts new file mode 100644 index 000000000000..0de12c7c83e0 --- /dev/null +++ b/src/test/javascript/spec/component/iris/settings/iris-exercise-settings-update.component.spec.ts @@ -0,0 +1,104 @@ +import { ArtemisTestModule } from '../../../test.module'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { IrisSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-settings-update.component'; +import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.service'; +import { MockComponent, MockDirective, MockProvider } from 'ng-mocks'; +import { BehaviorSubject, of } from 'rxjs'; +import { ButtonComponent } from 'app/shared/components/button.component'; +import { IrisChatSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component'; +import { IrisHestiaSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component'; +import { IrisCommonSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component'; +import { IrisGlobalAutoupdateSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component'; +import { mockModels, mockSettings } from './mock-settings'; +import { IrisExerciseSettingsUpdateComponent } from 'app/iris/settings/iris-exercise-settings-update/iris-exercise-settings-update.component'; +import { ActivatedRoute, Params } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgModel } from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { IrisSettings } from 'app/entities/iris/settings/iris-settings.model'; +import { HttpResponse } from '@angular/common/http'; + +describe('IrisExerciseSettingsUpdateComponent Component', () => { + let comp: IrisExerciseSettingsUpdateComponent; + let fixture: ComponentFixture; + let irisSettingsService: IrisSettingsService; + const routeParamsSubject = new BehaviorSubject({ courseId: 1, exerciseId: 1 }); + const route = { parent: { params: routeParamsSubject.asObservable() } } as ActivatedRoute; + let paramsSpy: jest.SpyInstance; + let getSettingsSpy: jest.SpyInstance; + let getModelsSpy: jest.SpyInstance; + let getParentSettingsSpy: jest.SpyInstance; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, RouterTestingModule], + declarations: [ + IrisExerciseSettingsUpdateComponent, + IrisSettingsUpdateComponent, + MockComponent(IrisCommonSubSettingsUpdateComponent), + MockComponent(IrisChatSubSettingsUpdateComponent), + MockComponent(IrisHestiaSubSettingsUpdateComponent), + MockComponent(IrisGlobalAutoupdateSettingsUpdateComponent), + MockComponent(ButtonComponent), + MockDirective(NgModel), + ], + providers: [MockProvider(IrisSettingsService), { provide: ActivatedRoute, useValue: route }], + }) + .compileComponents() + .then(() => { + irisSettingsService = TestBed.inject(IrisSettingsService); + + // Setup + routeParamsSubject.next({ courseId: 1, exerciseId: 2 }); + paramsSpy = jest.spyOn(route.parent!.params, 'subscribe'); + + const irisSettings = mockSettings(); + getSettingsSpy = jest.spyOn(irisSettingsService, 'getUncombinedProgrammingExerciseSettings').mockReturnValue(of(irisSettings)); + getModelsSpy = jest.spyOn(irisSettingsService, 'getIrisModels').mockReturnValue(of(mockModels())); + getParentSettingsSpy = jest.spyOn(irisSettingsService, 'getCombinedCourseSettings').mockReturnValue(of(irisSettings)); + }); + fixture = TestBed.createComponent(IrisExerciseSettingsUpdateComponent); + comp = fixture.componentInstance; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('Setup works correctly', () => { + fixture.detectChanges(); + expect(paramsSpy).toHaveBeenCalledOnce(); + expect(comp.courseId).toBe(1); + expect(comp.exerciseId).toBe(2); + expect(comp.settingsUpdateComponent).toBeTruthy(); + expect(getSettingsSpy).toHaveBeenCalledWith(2); + expect(getModelsSpy).toHaveBeenCalledOnce(); + expect(getParentSettingsSpy).toHaveBeenCalledWith(1); + + expect(fixture.debugElement.query(By.directive(IrisGlobalAutoupdateSettingsUpdateComponent))).toBeFalsy(); + expect(fixture.debugElement.queryAll(By.directive(IrisCommonSubSettingsUpdateComponent))).toHaveLength(1); + expect(fixture.debugElement.query(By.directive(IrisChatSubSettingsUpdateComponent))).toBeTruthy(); + expect(fixture.debugElement.query(By.directive(IrisHestiaSubSettingsUpdateComponent))).toBeFalsy(); + }); + + it('Can deactivate correctly', () => { + fixture.detectChanges(); + expect(comp.canDeactivate()).toBeTrue(); + comp.settingsUpdateComponent!.isDirty = true; + expect(comp.canDeactivate()).toBeFalse(); + comp.settingsUpdateComponent!.canDeactivateWarning = 'Warning'; + expect(comp.canDeactivateWarning).toBe('Warning'); + }); + + it('Saves settings correctly', () => { + fixture.detectChanges(); + const irisSettings = mockSettings(); + irisSettings.id = undefined; + const irisSettingsSaved = mockSettings(); + const setSettingsSpy = jest.spyOn(irisSettingsService, 'setProgrammingExerciseSettings').mockReturnValue(of(new HttpResponse({ body: irisSettingsSaved }))); + comp.settingsUpdateComponent!.irisSettings = irisSettings; + comp.settingsUpdateComponent!.saveIrisSettings(); + expect(setSettingsSpy).toHaveBeenCalledWith(2, irisSettings); + expect(comp.settingsUpdateComponent!.irisSettings).toEqual(irisSettingsSaved); + }); +}); diff --git a/src/test/javascript/spec/component/iris/settings/iris-global-settings-update.component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-global-settings-update.component.spec.ts new file mode 100644 index 000000000000..6cb5e4bd0c94 --- /dev/null +++ b/src/test/javascript/spec/component/iris/settings/iris-global-settings-update.component.spec.ts @@ -0,0 +1,90 @@ +import { ArtemisTestModule } from '../../../test.module'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { IrisSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-settings-update.component'; +import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.service'; +import { MockComponent, MockDirective, MockProvider } from 'ng-mocks'; +import { of } from 'rxjs'; +import { ButtonComponent } from 'app/shared/components/button.component'; +import { IrisChatSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-chat-sub-settings-update/iris-chat-sub-settings-update.component'; +import { IrisHestiaSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component'; +import { IrisCommonSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-common-sub-settings-update/iris-common-sub-settings-update.component'; +import { IrisGlobalAutoupdateSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-global-autoupdate-settings-update/iris-global-autoupdate-settings-update.component'; +import { mockModels, mockSettings } from './mock-settings'; +import { NgModel } from '@angular/forms'; +import { IrisGlobalSettingsUpdateComponent } from 'app/iris/settings/iris-global-settings-update/iris-global-settings-update.component'; +import { By } from '@angular/platform-browser'; +import { IrisSettings } from 'app/entities/iris/settings/iris-settings.model'; +import { HttpResponse } from '@angular/common/http'; + +describe('IrisGlobalSettingsUpdateComponent Component', () => { + let comp: IrisGlobalSettingsUpdateComponent; + let fixture: ComponentFixture; + let irisSettingsService: IrisSettingsService; + let getSettingsSpy: jest.SpyInstance; + let getModelsSpy: jest.SpyInstance; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule], + declarations: [ + IrisGlobalSettingsUpdateComponent, + IrisSettingsUpdateComponent, + MockComponent(IrisCommonSubSettingsUpdateComponent), + MockComponent(IrisChatSubSettingsUpdateComponent), + MockComponent(IrisHestiaSubSettingsUpdateComponent), + MockComponent(IrisGlobalAutoupdateSettingsUpdateComponent), + MockComponent(ButtonComponent), + MockDirective(NgModel), + ], + providers: [MockProvider(IrisSettingsService)], + }) + .compileComponents() + .then(() => { + irisSettingsService = TestBed.inject(IrisSettingsService); + + // Setup + const irisSettings = mockSettings(); + getSettingsSpy = jest.spyOn(irisSettingsService, 'getGlobalSettings').mockReturnValue(of(irisSettings)); + getModelsSpy = jest.spyOn(irisSettingsService, 'getIrisModels').mockReturnValue(of(mockModels())); + }); + fixture = TestBed.createComponent(IrisGlobalSettingsUpdateComponent); + comp = fixture.componentInstance; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('Setup works correctly', () => { + fixture.detectChanges(); + expect(comp.settingsUpdateComponent).toBeTruthy(); + expect(getSettingsSpy).toHaveBeenCalledOnce(); + expect(getModelsSpy).toHaveBeenCalledOnce(); + + expect(fixture.debugElement.query(By.directive(IrisGlobalAutoupdateSettingsUpdateComponent))).toBeTruthy(); + expect(fixture.debugElement.queryAll(By.directive(IrisCommonSubSettingsUpdateComponent))).toHaveLength(2); + expect(fixture.debugElement.query(By.directive(IrisChatSubSettingsUpdateComponent))).toBeTruthy(); + expect(fixture.debugElement.query(By.directive(IrisHestiaSubSettingsUpdateComponent))).toBeTruthy(); + }); + + it('Can deactivate correctly', () => { + fixture.detectChanges(); + expect(comp.canDeactivate()).toBeTrue(); + comp.settingsUpdateComponent!.isDirty = true; + expect(comp.canDeactivate()).toBeFalse(); + comp.settingsUpdateComponent!.canDeactivateWarning = 'Warning'; + expect(comp.canDeactivateWarning).toBe('Warning'); + }); + + it('Saves settings correctly', () => { + fixture.detectChanges(); + const irisSettings = mockSettings(); + irisSettings.id = undefined; + const irisSettingsSaved = mockSettings(); + const setSettingsSpy = jest.spyOn(irisSettingsService, 'setGlobalSettings').mockReturnValue(of(new HttpResponse({ body: irisSettingsSaved }))); + comp.settingsUpdateComponent!.irisSettings = irisSettings; + comp.settingsUpdateComponent!.saveIrisSettings(); + expect(setSettingsSpy).toHaveBeenCalledWith(irisSettings); + expect(comp.settingsUpdateComponent!.irisSettings).toEqual(irisSettingsSaved); + }); +}); diff --git a/src/test/javascript/spec/component/iris/settings/iris-hestia-sub-settings-update.component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-hestia-sub-settings-update.component.spec.ts new file mode 100644 index 000000000000..4941c6c92dbb --- /dev/null +++ b/src/test/javascript/spec/component/iris/settings/iris-hestia-sub-settings-update.component.spec.ts @@ -0,0 +1,111 @@ +import { ArtemisTestModule } from '../../../test.module'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; +import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; +import { MockDirective } from 'ng-mocks'; +import { SimpleChange, SimpleChanges } from '@angular/core'; +import { IrisHestiaSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; +import { IrisHestiaSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-hestia-sub-settings-update/iris-hestia-sub-settings-update.component'; + +function baseSettings() { + const mockTemplate = new IrisTemplate(); + mockTemplate.id = 1; + mockTemplate.content = 'Hello World'; + const irisSubSettings = new IrisHestiaSubSettings(); + irisSubSettings.id = 2; + irisSubSettings.template = mockTemplate; + irisSubSettings.enabled = true; + return irisSubSettings; +} + +describe('IrisHestiaSubSettingsUpdateComponent Component', () => { + let comp: IrisHestiaSubSettingsUpdateComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule, FormsModule, MockDirective(NgbTooltip)], + declarations: [IrisHestiaSubSettingsUpdateComponent], + }).compileComponents(); + fixture = TestBed.createComponent(IrisHestiaSubSettingsUpdateComponent); + comp = fixture.componentInstance; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('template is not optional', () => { + comp.subSettings = baseSettings(); + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.querySelector('#inheritTemplate')).toBeFalsy(); + expect(fixture.debugElement.nativeElement.querySelector('#template-editor')).toBeTruthy(); + }); + + it('template is optional', () => { + comp.subSettings = baseSettings(); + comp.parentSubSettings = baseSettings(); + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.querySelector('#inheritTemplate')).toBeTruthy(); + expect(fixture.debugElement.nativeElement.querySelector('#template-editor')).toBeTruthy(); + }); + + it('template is optional and changes from defined to undefined', () => { + comp.subSettings = baseSettings(); + comp.parentSubSettings = baseSettings(); + fixture.detectChanges(); + comp.onInheritTemplateChanged(); + fixture.detectChanges(); + expect(comp.subSettings.template).toBeUndefined(); + expect(fixture.debugElement.nativeElement.querySelector('#inheritTemplate')).toBeTruthy(); + expect(fixture.debugElement.nativeElement.querySelector('#template-editor')).toBeTruthy(); + }); + + it('template is optional and changes from undefined to defined', () => { + const subSettings = baseSettings(); + subSettings.template = undefined; + comp.subSettings = subSettings; + comp.parentSubSettings = baseSettings(); + fixture.detectChanges(); + comp.onInheritTemplateChanged(); + fixture.detectChanges(); + expect(comp.subSettings.template).toBeDefined(); + expect(fixture.debugElement.nativeElement.querySelector('#inheritTemplate')).toBeTruthy(); + expect(fixture.debugElement.nativeElement.querySelector('#template-editor')).toBeTruthy(); + }); + + it('template changes', () => { + comp.subSettings = baseSettings(); + fixture.detectChanges(); + comp.templateContent = 'Hello World 2'; + comp.onTemplateChanged(); + + expect(comp.subSettings.template?.content).toBe('Hello World 2'); + }); + + it('template created', () => { + comp.subSettings = baseSettings(); + comp.subSettings.template = undefined; + fixture.detectChanges(); + comp.templateContent = 'Hello World 2'; + comp.onTemplateChanged(); + + expect(comp.subSettings.template!.content).toBe('Hello World 2'); + }); + + it('sub settings changes', () => { + comp.subSettings = baseSettings(); + fixture.detectChanges(); + const newSubSettings = baseSettings(); + newSubSettings.template!.content = 'Hello World 2'; + + const changes: SimpleChanges = { + subSettings: new SimpleChange(comp.subSettings, newSubSettings, false), + }; + comp.subSettings = newSubSettings; + comp.ngOnChanges(changes); + + expect(comp.templateContent).toBe('Hello World 2'); + }); +}); diff --git a/src/test/javascript/spec/component/iris/settings/iris-settings-update.component.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-settings-update.component.spec.ts deleted file mode 100644 index 0bed904d03eb..000000000000 --- a/src/test/javascript/spec/component/iris/settings/iris-settings-update.component.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { ArtemisTestModule } from '../../../test.module'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; -import { IrisSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; -import { IrisSettings } from 'app/entities/iris/settings/iris-settings.model'; -import { IrisSettingsType, IrisSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-settings-update.component'; -import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.service'; -import { MockComponent, MockProvider } from 'ng-mocks'; -import { of } from 'rxjs'; -import { IrisSubSettingsUpdateComponent } from 'app/iris/settings/iris-settings-update/iris-sub-settings-update/iris-sub-settings-update.component'; -import { ButtonComponent } from 'app/shared/components/button.component'; -import { HttpResponse } from '@angular/common/http'; -import { IrisModel } from 'app/entities/iris/settings/iris-model'; - -function baseSettings() { - const mockTemplate = new IrisTemplate(); - mockTemplate.id = 1; - mockTemplate.content = 'Hello World'; - const mockChatSettings = new IrisSubSettings(); - mockChatSettings.id = 1; - mockChatSettings.template = mockTemplate; - mockChatSettings.enabled = true; - const mockHestiaSettings = new IrisSubSettings(); - mockHestiaSettings.id = 2; - mockHestiaSettings.template = mockTemplate; - mockHestiaSettings.enabled = true; - const irisSettings = new IrisSettings(); - irisSettings.id = 1; - irisSettings.irisChatSettings = mockChatSettings; - irisSettings.irisHestiaSettings = mockHestiaSettings; - return irisSettings; -} - -function models() { - return [ - { - id: '1', - name: 'Model 1', - description: 'Model 1 Description', - }, - { - id: '2', - name: 'Model 2', - description: 'Model 2 Description', - }, - ] as IrisModel[]; -} - -describe('IrisSettingsUpdateComponent Component', () => { - let comp: IrisSettingsUpdateComponent; - let fixture: ComponentFixture; - let irisSettingsService: IrisSettingsService; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ArtemisTestModule], - declarations: [IrisSettingsUpdateComponent, MockComponent(IrisSubSettingsUpdateComponent), MockComponent(ButtonComponent)], - providers: [MockProvider(IrisSettingsService)], - }) - .compileComponents() - .then(() => { - irisSettingsService = TestBed.inject(IrisSettingsService); - }); - fixture = TestBed.createComponent(IrisSettingsUpdateComponent); - comp = fixture.componentInstance; - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('Loads global settings correctly', () => { - const irisSettings = baseSettings(); - const getSettingsSpy = jest.spyOn(irisSettingsService, 'getGlobalSettings').mockReturnValue(of(irisSettings)); - const getModelsSpy = jest.spyOn(irisSettingsService, 'getIrisModels').mockReturnValue(of(models())); - comp.settingType = IrisSettingsType.GLOBAL; - fixture.detectChanges(); - expect(getSettingsSpy).toHaveBeenCalledOnce(); - expect(getModelsSpy).toHaveBeenCalledOnce(); - expect(comp.irisSettings).toEqual(irisSettings); - expect(fixture.debugElement.nativeElement.querySelector('#inheritHestia')).toBeFalsy(); - }); - - it('Loads course settings correctly', () => { - const irisSettings = baseSettings(); - const getSettingsSpy = jest.spyOn(irisSettingsService, 'getUncombinedCourseSettings').mockReturnValue(of(irisSettings)); - const getModelsSpy = jest.spyOn(irisSettingsService, 'getIrisModels').mockReturnValue(of(models())); - comp.settingType = IrisSettingsType.COURSE; - comp.courseId = 1; - fixture.detectChanges(); - expect(getSettingsSpy).toHaveBeenCalledWith(1); - expect(getModelsSpy).toHaveBeenCalledOnce(); - expect(comp.irisSettings).toEqual(irisSettings); - expect(fixture.debugElement.nativeElement.querySelector('#inheritHestia')).toBeFalsy(); - }); - - it('Loads programming exercise settings correctly', () => { - const irisSettings = baseSettings(); - const getSettingsSpy = jest.spyOn(irisSettingsService, 'getUncombinedProgrammingExerciseSettings').mockReturnValue(of(irisSettings)); - const getModelsSpy = jest.spyOn(irisSettingsService, 'getIrisModels').mockReturnValue(of(models())); - comp.settingType = IrisSettingsType.PROGRAMMING_EXERCISE; - comp.programmingExerciseId = 1; - fixture.detectChanges(); - expect(getSettingsSpy).toHaveBeenCalledWith(1); - expect(getModelsSpy).toHaveBeenCalledOnce(); - expect(comp.irisSettings).toEqual(irisSettings); - expect(fixture.debugElement.nativeElement.querySelector('#inheritHestia')).toBeTruthy(); - }); - - it('Saves global settings correctly', () => { - const irisSettings = baseSettings(); - irisSettings.id = undefined; - const irisSettingsSaved = baseSettings(); - const setSettingsSpy = jest.spyOn(irisSettingsService, 'setGlobalSettings').mockReturnValue(of(new HttpResponse({ body: irisSettingsSaved }))); - comp.settingType = IrisSettingsType.GLOBAL; - comp.irisSettings = irisSettings; - comp.saveIrisSettings(); - expect(setSettingsSpy).toHaveBeenCalledWith(irisSettings); - expect(comp.irisSettings).toEqual(irisSettingsSaved); - }); - - it('Saves course settings correctly', () => { - const irisSettings = baseSettings(); - irisSettings.id = undefined; - const irisSettingsSaved = baseSettings(); - const setSettingsSpy = jest.spyOn(irisSettingsService, 'setCourseSettings').mockReturnValue(of(new HttpResponse({ body: irisSettingsSaved }))); - comp.settingType = IrisSettingsType.COURSE; - comp.courseId = 1; - comp.irisSettings = irisSettings; - comp.saveIrisSettings(); - expect(setSettingsSpy).toHaveBeenCalledWith(1, irisSettings); - expect(comp.irisSettings).toEqual(irisSettingsSaved); - }); - - it('Saves programming exercise settings correctly', () => { - const irisSettings = baseSettings(); - irisSettings.id = undefined; - const irisSettingsSaved = baseSettings(); - const setSettingsSpy = jest.spyOn(irisSettingsService, 'setProgrammingExerciseSettings').mockReturnValue(of(new HttpResponse({ body: irisSettingsSaved }))); - comp.settingType = IrisSettingsType.PROGRAMMING_EXERCISE; - comp.programmingExerciseId = 1; - comp.irisSettings = irisSettings; - comp.saveIrisSettings(); - expect(setSettingsSpy).toHaveBeenCalledWith(1, irisSettings); - expect(comp.irisSettings).toEqual(irisSettingsSaved); - }); -}); diff --git a/src/test/javascript/spec/component/iris/settings/iris-settings.service.spec.ts b/src/test/javascript/spec/component/iris/settings/iris-settings.service.spec.ts index e067fde8f433..4a181b852a84 100644 --- a/src/test/javascript/spec/component/iris/settings/iris-settings.service.spec.ts +++ b/src/test/javascript/spec/component/iris/settings/iris-settings.service.spec.ts @@ -2,28 +2,7 @@ import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { take } from 'rxjs/operators'; import { IrisSettingsService } from 'app/iris/settings/shared/iris-settings.service'; -import { IrisSettings } from 'app/entities/iris/settings/iris-settings.model'; -import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; -import { IrisSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; - -function mockSettings() { - const mockTemplate = new IrisTemplate(); - mockTemplate.id = 1; - mockTemplate.content = 'Hello World'; - const mockChatSettings = new IrisSubSettings(); - mockChatSettings.id = 1; - mockChatSettings.template = mockTemplate; - mockChatSettings.enabled = true; - const mockHestiaSettings = new IrisSubSettings(); - mockHestiaSettings.id = 2; - mockHestiaSettings.template = mockTemplate; - mockHestiaSettings.enabled = true; - const irisSettings = new IrisSettings(); - irisSettings.id = 1; - irisSettings.irisChatSettings = mockChatSettings; - irisSettings.irisHestiaSettings = mockHestiaSettings; - return irisSettings; -} +import { mockSettings } from './mock-settings'; describe('Iris Settings Service', () => { let service: IrisSettingsService; diff --git a/src/test/javascript/spec/component/iris/settings/mock-settings.ts b/src/test/javascript/spec/component/iris/settings/mock-settings.ts new file mode 100644 index 000000000000..c25844a503d8 --- /dev/null +++ b/src/test/javascript/spec/component/iris/settings/mock-settings.ts @@ -0,0 +1,42 @@ +import { IrisModel } from 'app/entities/iris/settings/iris-model'; +import { IrisTemplate } from 'app/entities/iris/settings/iris-template'; +import { IrisChatSubSettings, IrisCodeEditorSubSettings, IrisHestiaSubSettings } from 'app/entities/iris/settings/iris-sub-settings.model'; +import { IrisGlobalSettings } from 'app/entities/iris/settings/iris-settings.model'; + +export function mockSettings() { + const mockTemplate = new IrisTemplate(); + mockTemplate.id = 1; + mockTemplate.content = 'Hello World'; + const mockChatSettings = new IrisChatSubSettings(); + mockChatSettings.id = 1; + mockChatSettings.template = mockTemplate; + mockChatSettings.enabled = true; + const mockHestiaSettings = new IrisHestiaSubSettings(); + mockHestiaSettings.id = 2; + mockHestiaSettings.template = mockTemplate; + mockHestiaSettings.enabled = true; + const mockCodeEditorSettings = new IrisCodeEditorSubSettings(); + mockCodeEditorSettings.id = 2; + mockCodeEditorSettings.enabled = false; + const irisSettings = new IrisGlobalSettings(); + irisSettings.id = 1; + irisSettings.irisChatSettings = mockChatSettings; + irisSettings.irisHestiaSettings = mockHestiaSettings; + irisSettings.irisCodeEditorSettings = mockCodeEditorSettings; + return irisSettings; +} + +export function mockModels() { + return [ + { + id: '1', + name: 'Model 1', + description: 'Model 1 Description', + }, + { + id: '2', + name: 'Model 2', + description: 'Model 2 Description', + }, + ] as IrisModel[]; +}