From 07c3d448398895412bd2c0a7d617ff6275ace0c7 Mon Sep 17 00:00:00 2001 From: Kaan Cayli <38523756+kaancayli@users.noreply.github.com> Date: Tue, 14 Jan 2025 00:30:00 +0100 Subject: [PATCH 1/2] feat: Initial commit --- .../tum/cit/aet/artemis/core/domain/User.java | 32 +++++++++++++++++++ .../tum/cit/aet/artemis/core/dto/UserDTO.java | 17 ++++++++-- .../core/repository/UserRepository.java | 10 ++++++ .../core/service/user/UserService.java | 25 +++++++++++++++ .../aet/artemis/core/web/UserResource.java | 23 +++++++++++++ .../repository/SubmissionRepository.java | 9 ------ .../dto/IrisProactiveEventDisableDTO.java | 11 +++++++ .../IrisProactiveEventDisableDuration.java | 5 +++ .../aet/artemis/iris/dto/IrisStatusDTO.java | 6 +++- .../IrisExerciseChatSessionService.java | 19 ++++++----- .../aet/artemis/iris/web/IrisResource.java | 3 +- .../ProgrammingSubmissionRepository.java | 20 ++++++++++++ .../changelog/20250113223500_changelog.xml | 12 +++++++ .../resources/config/liquibase/master.xml | 1 + .../app/entities/iris/iris-health.model.ts | 1 + src/main/webapp/app/iris/iris-chat.service.ts | 4 +++ .../webapp/app/iris/iris-status.service.ts | 5 +++ 17 files changed, 180 insertions(+), 23 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisProactiveEventDisableDTO.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisProactiveEventDisableDuration.java create mode 100644 src/main/resources/config/liquibase/changelog/20250113223500_changelog.xml diff --git a/src/main/java/de/tum/cit/aet/artemis/core/domain/User.java b/src/main/java/de/tum/cit/aet/artemis/core/domain/User.java index cebc42aafde9..cf812d6c2123 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/domain/User.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/domain/User.java @@ -3,6 +3,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.USERNAME_MAX_LENGTH; import static de.tum.cit.aet.artemis.core.config.Constants.USERNAME_MIN_LENGTH; +import java.time.Duration; import java.time.Instant; import java.time.ZonedDateTime; import java.util.HashSet; @@ -211,6 +212,10 @@ public class User extends AbstractAuditingEntity implements Participant { @Column(name = "iris_accepted") private ZonedDateTime irisAccepted = null; + @Nullable + @Column(name = "iris_proactive_events_disabled") + private Instant irisProactiveEventsDisabled = null; + public User() { } @@ -538,6 +543,33 @@ public boolean hasAcceptedIris() { return irisAccepted != null; } + @Nullable + public Instant getIrisProactiveEventsDisabledTimestamp() { + return irisProactiveEventsDisabled; + } + + public void setIrisProactiveEventsDisabledTimestamp(@Nullable Instant irisProactiveEventsDisabled) { + this.irisProactiveEventsDisabled = irisProactiveEventsDisabled; + } + + /** + * Checks if the user has disabled Iris proactive events. + * + * @return true if the timestamp is set and in the future + */ + public boolean hasDisabledIrisProactiveEvents() { + return irisProactiveEventsDisabled != null && irisProactiveEventsDisabled.isAfter(Instant.now()); + } + + /** + * Disables Iris proactive events for the user for the given duration. + * + * @param duration the duration for which the proactive events should be disabled + */ + public void disableIrisProactiveEventsFor(Duration duration) { + irisProactiveEventsDisabled = Instant.now().plus(duration); + } + /** * Checks if the user has accepted the Iris privacy policy. * If not, an {@link AccessForbiddenException} is thrown. diff --git a/src/main/java/de/tum/cit/aet/artemis/core/dto/UserDTO.java b/src/main/java/de/tum/cit/aet/artemis/core/dto/UserDTO.java index 1f5ea1e653b8..164d60aba3ed 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/dto/UserDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/dto/UserDTO.java @@ -78,6 +78,8 @@ public class UserDTO extends AuditingEntityDTO { private ZonedDateTime irisAccepted; + private Instant irisProactiveEventsDisabled; + public UserDTO() { // Empty constructor needed for Jackson. } @@ -85,12 +87,14 @@ public UserDTO() { public UserDTO(User user) { this(user.getId(), user.getLogin(), user.getName(), user.getFirstName(), user.getLastName(), user.getEmail(), user.getVisibleRegistrationNumber(), user.getActivated(), user.getImageUrl(), user.getLangKey(), user.isInternal(), user.getCreatedBy(), user.getCreatedDate(), user.getLastModifiedBy(), user.getLastModifiedDate(), - user.getLastNotificationRead(), user.getAuthorities(), user.getGroups(), user.getGuidedTourSettings(), user.getOrganizations(), user.getIrisAcceptedTimestamp()); + user.getLastNotificationRead(), user.getAuthorities(), user.getGroups(), user.getGuidedTourSettings(), user.getOrganizations(), user.getIrisAcceptedTimestamp(), + user.getIrisProactiveEventsDisabledTimestamp()); } public UserDTO(Long id, String login, String name, String firstName, String lastName, String email, String visibleRegistrationNumber, boolean activated, String imageUrl, String langKey, boolean isInternal, String createdBy, Instant createdDate, String lastModifiedBy, Instant lastModifiedDate, ZonedDateTime lastNotificationRead, - Set authorities, Set groups, Set guidedTourSettings, Set organizations, ZonedDateTime irisAccepted) { + Set authorities, Set groups, Set guidedTourSettings, Set organizations, ZonedDateTime irisAccepted, + Instant irisProactiveEventsDisabled) { this.id = id; this.login = login; @@ -115,6 +119,7 @@ public UserDTO(Long id, String login, String name, String firstName, String last this.guidedTourSettings = guidedTourSettings; this.organizations = organizations; this.irisAccepted = irisAccepted; + this.irisProactiveEventsDisabled = irisProactiveEventsDisabled; } public Long getId() { @@ -281,4 +286,12 @@ public ZonedDateTime getIrisAccepted() { public void setIrisAccepted(ZonedDateTime irisAccepted) { this.irisAccepted = irisAccepted; } + + public Instant getIrisProactiveEventsDisabled() { + return irisProactiveEventsDisabled; + } + + public void setIrisProactiveEventsDisabled(Instant irisProactiveEventsDisabled) { + this.irisProactiveEventsDisabled = irisProactiveEventsDisabled; + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java index 6eb94330acff..a64185671b78 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java @@ -11,6 +11,7 @@ import static de.tum.cit.aet.artemis.core.repository.UserSpecs.notSoftDeleted; import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.LOAD; +import java.time.Instant; import java.time.ZonedDateTime; import java.util.Collection; import java.util.Collections; @@ -744,6 +745,15 @@ void updateUserVcsAccessToken(@Param("userId") long userId, @Param("vcsAccessTok """) void updateIrisAcceptedToDate(@Param("userId") long userId, @Param("acceptDatetime") ZonedDateTime acceptDatetime); + @Modifying + @Transactional // ok because of modifying query + @Query(""" + UPDATE User user + SET user.irisProactiveEventsDisabled = :disabledUntil + WHERE user.id = :userId + """) + void updateIrisProactiveEventsDisabled(@Param("userId") long userId, @Param("disabledUntil") Instant disabledUntil); + @Query(""" SELECT DISTINCT user FROM User user diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java index 4ac3f22aaa17..46d0134a802a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java @@ -14,6 +14,7 @@ import static org.apache.commons.lang3.StringUtils.lowerCase; import java.net.URI; +import java.time.Duration; import java.time.Instant; import java.time.ZonedDateTime; import java.util.ArrayList; @@ -63,6 +64,7 @@ import de.tum.cit.aet.artemis.core.service.ldap.LdapUserDto; import de.tum.cit.aet.artemis.core.service.ldap.LdapUserService; import de.tum.cit.aet.artemis.core.service.messaging.InstanceMessageSendService; +import de.tum.cit.aet.artemis.iris.dto.IrisProactiveEventDisableDuration; import de.tum.cit.aet.artemis.programming.domain.ParticipationVCSAccessToken; import de.tum.cit.aet.artemis.programming.service.ParticipationVcsAccessTokenService; import de.tum.cit.aet.artemis.programming.service.ci.CIUserManagementService; @@ -816,6 +818,29 @@ public void updateUserLanguageKey(Long userId, String languageKey) { userRepository.updateUserLanguageKey(userId, languageKey); } + /** + * Update the user's iris proactive events disabled status + * + * @param userId the id of the user + * @param duration type of duration for which the events are disabled + * @param endTime the end time of the custom duration + */ + public void updateIrisProactiveEventsDisabled(Long userId, IrisProactiveEventDisableDuration duration, Instant endTime) { + var currentTimestamp = Instant.now(); + switch (duration) { + case THIRTY_MINUTES -> userRepository.updateIrisProactiveEventsDisabled(userId, currentTimestamp.plus(Duration.ofMinutes(30))); + case ONE_HOUR -> userRepository.updateIrisProactiveEventsDisabled(userId, currentTimestamp.plus(Duration.ofHours(1))); + case ONE_DAY -> userRepository.updateIrisProactiveEventsDisabled(userId, currentTimestamp.plus(Duration.ofDays(1))); + case FOREVER -> { + // Indefinite delay - using year 9999 as a practical "infinity" + var forever = Instant.parse("9999-12-31T23:59:59.999999999Z"); + userRepository.updateIrisProactiveEventsDisabled(userId, forever); + } + case CUSTOM -> userRepository.updateIrisProactiveEventsDisabled(userId, endTime); + default -> throw new IllegalArgumentException("Invalid duration: " + duration); + } + } + /** * This method first tries to find and then to add each user of the given list to the course * diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/UserResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/UserResource.java index 60612c7ec6b2..7fc9dd6c32d3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/UserResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/UserResource.java @@ -31,6 +31,8 @@ import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.service.user.UserCreationService; import de.tum.cit.aet.artemis.core.service.user.UserService; +import de.tum.cit.aet.artemis.iris.dto.IrisProactiveEventDisableDTO; +import de.tum.cit.aet.artemis.iris.dto.IrisProactiveEventDisableDuration; import de.tum.cit.aet.artemis.lti.service.LtiService; import tech.jhipster.web.util.PaginationUtil; @@ -101,6 +103,7 @@ public ResponseEntity> searchAllUsers(@RequestParam("loginOrName") user.setCreatedBy(null); user.setCreatedDate(null); user.setIrisAccepted(null); + user.setIrisProactiveEventsDisabled(null); }); HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); @@ -169,4 +172,24 @@ public ResponseEntity setIrisAcceptedToTimestamp() { userRepository.updateIrisAcceptedToDate(user.getId(), ZonedDateTime.now()); return ResponseEntity.ok().build(); } + + @PutMapping("users/disable-iris-proactive-events") + @EnforceAtLeastStudent + public ResponseEntity setIrisProactiveEventsDisabledToTimestamp(@RequestBody IrisProactiveEventDisableDTO disableDTO) { + User user = userRepository.getUser(); + + var duration = disableDTO.duration(); + var timestamp = disableDTO.endTime(); + + if (duration == IrisProactiveEventDisableDuration.CUSTOM && timestamp == null) { + return ResponseEntity.badRequest().build(); + } + try { + userService.updateIrisProactiveEventsDisabled(user.getId(), duration, timestamp); + return ResponseEntity.ok().build(); + } + catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().build(); + } + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/repository/SubmissionRepository.java b/src/main/java/de/tum/cit/aet/artemis/exercise/repository/SubmissionRepository.java index 26925f2ccbf4..16cac0f54cef 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/repository/SubmissionRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/repository/SubmissionRepository.java @@ -80,15 +80,6 @@ public interface SubmissionRepository extends ArtemisJpaRepository findAllWithResultsAndAssessorByParticipationId(Long participationId); - /** - * Get all submissions of a participation and eagerly load results ordered by submission date in ascending order - * - * @param participationId the id of the participation - * @return a list of the participation's submissions - */ - @EntityGraph(type = LOAD, attributePaths = { "results" }) - List findAllWithResultsByParticipationIdOrderBySubmissionDateAsc(Long participationId); - /** * Get all submissions with their results by the submission ids * diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisProactiveEventDisableDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisProactiveEventDisableDTO.java new file mode 100644 index 000000000000..7f6dd3ddace8 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisProactiveEventDisableDTO.java @@ -0,0 +1,11 @@ +package de.tum.cit.aet.artemis.iris.dto; + +import java.time.Instant; + +import jakarta.annotation.Nullable; + +/** + * DTO for disabling proactive events. + */ +public record IrisProactiveEventDisableDTO(IrisProactiveEventDisableDuration duration, @Nullable Instant endTime) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisProactiveEventDisableDuration.java b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisProactiveEventDisableDuration.java new file mode 100644 index 000000000000..bc9eb690656d --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisProactiveEventDisableDuration.java @@ -0,0 +1,5 @@ +package de.tum.cit.aet.artemis.iris.dto; + +public enum IrisProactiveEventDisableDuration { + THIRTY_MINUTES, ONE_HOUR, ONE_DAY, FOREVER, CUSTOM +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisStatusDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisStatusDTO.java index 056e166d67cf..52f94c3f69dd 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisStatusDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisStatusDTO.java @@ -1,9 +1,13 @@ package de.tum.cit.aet.artemis.iris.dto; +import java.time.Instant; + +import jakarta.annotation.Nullable; + import com.fasterxml.jackson.annotation.JsonInclude; import de.tum.cit.aet.artemis.iris.service.IrisRateLimitService; @JsonInclude(JsonInclude.Include.NON_EMPTY) -public record IrisStatusDTO(boolean active, IrisRateLimitService.IrisRateLimitInformation rateLimitInfo) { +public record IrisStatusDTO(boolean active, IrisRateLimitService.IrisRateLimitInformation rateLimitInfo, @Nullable Instant proactiveEventsDisabledUntil) { } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java index 91b7c643258f..db2b0a54bec5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/service/session/IrisExerciseChatSessionService.java @@ -74,8 +74,6 @@ public class IrisExerciseChatSessionService extends AbstractIrisChatSessionServi private final IrisExerciseChatSessionRepository irisExerciseChatSessionRepository; - private final SubmissionRepository submissionRepository; - public IrisExerciseChatSessionService(IrisMessageService irisMessageService, LLMTokenUsageService llmTokenUsageService, IrisSettingsService irisSettingsService, IrisChatWebsocketService irisChatWebsocketService, AuthorizationCheckService authCheckService, IrisSessionRepository irisSessionRepository, ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, ProgrammingSubmissionRepository programmingSubmissionRepository, @@ -92,7 +90,6 @@ public IrisExerciseChatSessionService(IrisMessageService irisMessageService, LLM this.pyrisPipelineService = pyrisPipelineService; this.programmingExerciseRepository = programmingExerciseRepository; this.irisExerciseChatSessionRepository = irisExerciseChatSessionRepository; - this.submissionRepository = submissionRepository; } /** @@ -213,20 +210,22 @@ public void onNewResult(Result result) { var exercise = validateExercise(participation.getExercise()); - var recentSubmissions = submissionRepository.findAllWithResultsByParticipationIdOrderBySubmissionDateAsc(studentParticipation.getId()); + var recentSubmissions = programmingSubmissionRepository + .findSubmissionsWithResultsByParticipationIdAndBuildFailedIsFalseAndTypeIsNotIllegalOrderBySubmissionDateAsc(studentParticipation.getId()); double successThreshold = 100.0; // TODO: Retrieve configuration from Iris settings // Check if the user has already successfully submitted before - var successfulSubmission = recentSubmissions.stream() - .anyMatch(submission -> submission.getLatestResult() != null && submission.getLatestResult().getScore() == successThreshold); - if (!successfulSubmission && recentSubmissions.size() >= 3) { + var successfulSubmission = recentSubmissions.stream().anyMatch(submission -> Objects.requireNonNull(submission.getLatestResult()).getScore() == successThreshold); + + // Check after every third non-build-failed submission + if (!successfulSubmission && recentSubmissions.size() >= 3 && recentSubmissions.size() % 3 == 0) { var listOfScores = recentSubmissions.stream().map(Submission::getLatestResult).filter(Objects::nonNull).map(Result::getScore).toList(); // Check if the student needs intervention based on their recent score trajectory var needsIntervention = needsIntervention(listOfScores); if (needsIntervention) { - log.info("Scores in the last 3 submissions did not improve for user {}", studentParticipation.getParticipant().getName()); + log.debug("Scores in the last 3 submissions did not improve for user {}", studentParticipation.getParticipant().getName()); var participant = ((ProgrammingExerciseStudentParticipation) participation).getParticipant(); if (participant instanceof User user) { var session = getCurrentSessionOrCreateIfNotExistsInternal(exercise, user, false); @@ -238,9 +237,9 @@ public void onNewResult(Result result) { } } else { - log.info("Submission was not successful for user {}", studentParticipation.getParticipant().getName()); + log.debug("Submission was not successful for user {}", studentParticipation.getParticipant().getName()); if (successfulSubmission) { - log.info("User {} has already successfully submitted before, so we do not inform Iris about the submission failure", + log.debug("User {} has already successfully submitted before, so we do not inform Iris about the submission failure", studentParticipation.getParticipant().getName()); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisResource.java b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisResource.java index 37e2983e39e0..1f6ec6c394af 100644 --- a/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/iris/web/IrisResource.java @@ -69,8 +69,9 @@ public ResponseEntity getStatus() { var user = userRepository.getUser(); var health = pyrisHealthIndicator.health(true); var rateLimitInfo = irisRateLimitService.getRateLimitInformation(user); + var irisProactiveEventsDisabled = user.getIrisProactiveEventsDisabledTimestamp(); - return ResponseEntity.ok(new IrisStatusDTO(health.getStatus() == Status.UP, rateLimitInfo)); + return ResponseEntity.ok(new IrisStatusDTO(health.getStatus() == Status.UP, rateLimitInfo, irisProactiveEventsDisabled)); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingSubmissionRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingSubmissionRepository.java index 90d514ca4b68..56e224672927 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingSubmissionRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/ProgrammingSubmissionRepository.java @@ -94,6 +94,26 @@ default Optional findFirstByParticipationIdWithResultsOrd @EntityGraph(type = LOAD, attributePaths = { "results" }) List findSubmissionsWithResultsByIdIn(List ids); + /** + * Find all programming submissions with results for a student participation where the build failed is false and the type is not illegal ordered by submission date in ascending + * order + * + * @return List of ProgrammingSubmission + */ + @Query(""" + SELECT s + FROM ProgrammingSubmission s + LEFT JOIN FETCH s.results r + WHERE s.participation.id = :participationId + AND s.buildFailed = FALSE + AND (s.type <> de.tum.cit.aet.artemis.exercise.domain.SubmissionType.ILLEGAL + OR s.type IS NULL) + AND r IS NOT NULL + ORDER BY s.submissionDate ASC + """) + List findSubmissionsWithResultsByParticipationIdAndBuildFailedIsFalseAndTypeIsNotIllegalOrderBySubmissionDateAsc( + @Param("participationId") long participationId); + /** * Provide a list of graded submissions. To be graded a submission must: * - be of type 'INSTRUCTOR' or 'TEST' diff --git a/src/main/resources/config/liquibase/changelog/20250113223500_changelog.xml b/src/main/resources/config/liquibase/changelog/20250113223500_changelog.xml new file mode 100644 index 000000000000..9d9290e88bb6 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20250113223500_changelog.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index c25b722955e3..2e96f95a5751 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -43,6 +43,7 @@ + diff --git a/src/main/webapp/app/entities/iris/iris-health.model.ts b/src/main/webapp/app/entities/iris/iris-health.model.ts index 9663d20bf22c..1f9f30031a7e 100644 --- a/src/main/webapp/app/entities/iris/iris-health.model.ts +++ b/src/main/webapp/app/entities/iris/iris-health.model.ts @@ -3,4 +3,5 @@ import { IrisRateLimitInformation } from 'app/entities/iris/iris-ratelimit-info. export class IrisStatusDTO { active: boolean; rateLimitInfo: IrisRateLimitInformation; + proactiveEventsDisabledUntil: Date | null; } diff --git a/src/main/webapp/app/iris/iris-chat.service.ts b/src/main/webapp/app/iris/iris-chat.service.ts index c8e243f0f6c3..60f9e60ed8f3 100644 --- a/src/main/webapp/app/iris/iris-chat.service.ts +++ b/src/main/webapp/app/iris/iris-chat.service.ts @@ -36,6 +36,8 @@ export class IrisChatService implements OnDestroy { rateLimitInfo?: IrisRateLimitInformation; rateLimitSubscription: Subscription; + proactiveEventsDisabledUntil: Date | null = null; + proactiveEventsDisabledUntilSubscription: Subscription; private sessionCreationIdentifier?: string; @@ -57,10 +59,12 @@ export class IrisChatService implements OnDestroy { private accountService: AccountService, ) { this.rateLimitSubscription = this.status.currentRatelimitInfo().subscribe((info) => (this.rateLimitInfo = info)); + this.proactiveEventsDisabledUntilSubscription = this.status.proactiveEventsDisabledUntil.subscribe((date) => (this.proactiveEventsDisabledUntil = date)); } ngOnDestroy(): void { this.rateLimitSubscription.unsubscribe(); + this.proactiveEventsDisabledUntilSubscription.unsubscribe(); } protected start() { diff --git a/src/main/webapp/app/iris/iris-status.service.ts b/src/main/webapp/app/iris/iris-status.service.ts index 25316415f785..f1438485f293 100644 --- a/src/main/webapp/app/iris/iris-status.service.ts +++ b/src/main/webapp/app/iris/iris-status.service.ts @@ -22,6 +22,7 @@ export class IrisStatusService implements OnDestroy { activeSubject = new BehaviorSubject(this.active); currentRatelimitInfoSubject = new BehaviorSubject(new IrisRateLimitInformation(0, 0, 0)); + proactiveEventsDisabledUntil = new BehaviorSubject(null); /** * Creates an instance of IrisHeartbeatService. @@ -75,6 +76,10 @@ export class IrisStatusService implements OnDestroy { if (response.body) { this.active = Boolean(response.body.active); + if (response.body.proactiveEventsDisabledUntil) { + this.proactiveEventsDisabledUntil.next(new Date(response.body.proactiveEventsDisabledUntil)); + } + if (response.body.rateLimitInfo) { this.currentRatelimitInfoSubject.next(response.body.rateLimitInfo); } From 69209072a61a8ed3870d99d45de1a31b9663ff15 Mon Sep 17 00:00:00 2001 From: Kaan Cayli <38523756+kaancayli@users.noreply.github.com> Date: Mon, 20 Jan 2025 23:33:38 +0100 Subject: [PATCH 2/2] chore: Rename dtos and start with FE part --- .../tum/cit/aet/artemis/core/domain/User.java | 10 ----- .../core/service/user/UserService.java | 25 ++++++------ .../aet/artemis/core/web/UserResource.java | 16 ++++++-- .../dto/IrisDisableProactiveEventsDTO.java | 11 ++++++ ...IrisDisableProactiveEventsResponseDTO.java | 9 +++++ .../dto/IrisProactiveEventDisableDTO.java | 11 ------ src/main/webapp/app/core/user/user.model.ts | 3 ++ src/main/webapp/app/core/user/user.service.ts | 8 ++++ ...iris-disable-proactive-events-dto.model.ts | 18 +++++++++ .../app/entities/iris/iris-errors.model.ts | 2 + .../settings/iris-personal-settings.model.ts | 5 +++ src/main/webapp/app/iris/iris-chat.service.ts | 38 +++++++++++++++++-- 12 files changed, 114 insertions(+), 42 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisDisableProactiveEventsDTO.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisDisableProactiveEventsResponseDTO.java delete mode 100644 src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisProactiveEventDisableDTO.java create mode 100644 src/main/webapp/app/entities/iris/iris-disable-proactive-events-dto.model.ts create mode 100644 src/main/webapp/app/entities/iris/settings/iris-personal-settings.model.ts diff --git a/src/main/java/de/tum/cit/aet/artemis/core/domain/User.java b/src/main/java/de/tum/cit/aet/artemis/core/domain/User.java index cf812d6c2123..9a7032944c4b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/domain/User.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/domain/User.java @@ -3,7 +3,6 @@ import static de.tum.cit.aet.artemis.core.config.Constants.USERNAME_MAX_LENGTH; import static de.tum.cit.aet.artemis.core.config.Constants.USERNAME_MIN_LENGTH; -import java.time.Duration; import java.time.Instant; import java.time.ZonedDateTime; import java.util.HashSet; @@ -561,15 +560,6 @@ public boolean hasDisabledIrisProactiveEvents() { return irisProactiveEventsDisabled != null && irisProactiveEventsDisabled.isAfter(Instant.now()); } - /** - * Disables Iris proactive events for the user for the given duration. - * - * @param duration the duration for which the proactive events should be disabled - */ - public void disableIrisProactiveEventsFor(Duration duration) { - irisProactiveEventsDisabled = Instant.now().plus(duration); - } - /** * Checks if the user has accepted the Iris privacy policy. * If not, an {@link AccessForbiddenException} is thrown. diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java index 46d0134a802a..2691ca0edb3e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java @@ -824,21 +824,20 @@ public void updateUserLanguageKey(Long userId, String languageKey) { * @param userId the id of the user * @param duration type of duration for which the events are disabled * @param endTime the end time of the custom duration + * @return the updated timestamp of the end time */ - public void updateIrisProactiveEventsDisabled(Long userId, IrisProactiveEventDisableDuration duration, Instant endTime) { + public Instant updateIrisProactiveEventsDisabled(Long userId, IrisProactiveEventDisableDuration duration, Instant endTime) { var currentTimestamp = Instant.now(); - switch (duration) { - case THIRTY_MINUTES -> userRepository.updateIrisProactiveEventsDisabled(userId, currentTimestamp.plus(Duration.ofMinutes(30))); - case ONE_HOUR -> userRepository.updateIrisProactiveEventsDisabled(userId, currentTimestamp.plus(Duration.ofHours(1))); - case ONE_DAY -> userRepository.updateIrisProactiveEventsDisabled(userId, currentTimestamp.plus(Duration.ofDays(1))); - case FOREVER -> { - // Indefinite delay - using year 9999 as a practical "infinity" - var forever = Instant.parse("9999-12-31T23:59:59.999999999Z"); - userRepository.updateIrisProactiveEventsDisabled(userId, forever); - } - case CUSTOM -> userRepository.updateIrisProactiveEventsDisabled(userId, endTime); - default -> throw new IllegalArgumentException("Invalid duration: " + duration); - } + Instant updatedTimestamp = switch (duration) { + case THIRTY_MINUTES -> currentTimestamp.plus(Duration.ofMinutes(30)); + case ONE_HOUR -> currentTimestamp.plus(Duration.ofHours(1)); + case ONE_DAY -> currentTimestamp.plus(Duration.ofDays(1)); + case FOREVER -> // Indefinite delay - using year 9999 as a practical "infinity" + Instant.parse("9999-12-31T23:59:59.999999999Z"); + case CUSTOM -> endTime; + }; + userRepository.updateIrisProactiveEventsDisabled(userId, updatedTimestamp); + return updatedTimestamp; } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/UserResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/UserResource.java index 7fc9dd6c32d3..067dd89235ae 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/UserResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/UserResource.java @@ -2,6 +2,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; +import java.time.Instant; import java.time.ZonedDateTime; import java.util.List; import java.util.Optional; @@ -31,7 +32,8 @@ import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.service.user.UserCreationService; import de.tum.cit.aet.artemis.core.service.user.UserService; -import de.tum.cit.aet.artemis.iris.dto.IrisProactiveEventDisableDTO; +import de.tum.cit.aet.artemis.iris.dto.IrisDisableProactiveEventsDTO; +import de.tum.cit.aet.artemis.iris.dto.IrisDisableProactiveEventsResponseDTO; import de.tum.cit.aet.artemis.iris.dto.IrisProactiveEventDisableDuration; import de.tum.cit.aet.artemis.lti.service.LtiService; import tech.jhipster.web.util.PaginationUtil; @@ -175,18 +177,24 @@ public ResponseEntity setIrisAcceptedToTimestamp() { @PutMapping("users/disable-iris-proactive-events") @EnforceAtLeastStudent - public ResponseEntity setIrisProactiveEventsDisabledToTimestamp(@RequestBody IrisProactiveEventDisableDTO disableDTO) { + public ResponseEntity setIrisProactiveEventsDisabledToTimestamp(@RequestBody IrisDisableProactiveEventsDTO disableDTO) { User user = userRepository.getUser(); var duration = disableDTO.duration(); var timestamp = disableDTO.endTime(); + if (duration == null) { + return ResponseEntity.badRequest().build(); + } if (duration == IrisProactiveEventDisableDuration.CUSTOM && timestamp == null) { return ResponseEntity.badRequest().build(); } try { - userService.updateIrisProactiveEventsDisabled(user.getId(), duration, timestamp); - return ResponseEntity.ok().build(); + Instant endTime = timestamp != null ? timestamp.toInstant() : null; + var updatedTimestamp = userService.updateIrisProactiveEventsDisabled(user.getId(), duration, endTime); + // Return the updated timestamp to the client + return ResponseEntity.ok().body(new IrisDisableProactiveEventsResponseDTO(updatedTimestamp)); + } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().build(); diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisDisableProactiveEventsDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisDisableProactiveEventsDTO.java new file mode 100644 index 000000000000..a39d9f8b62ef --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisDisableProactiveEventsDTO.java @@ -0,0 +1,11 @@ +package de.tum.cit.aet.artemis.iris.dto; + +import java.time.ZonedDateTime; + +import jakarta.annotation.Nullable; + +/** + * DTO for disabling proactive events. + */ +public record IrisDisableProactiveEventsDTO(IrisProactiveEventDisableDuration duration, @Nullable ZonedDateTime endTime) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisDisableProactiveEventsResponseDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisDisableProactiveEventsResponseDTO.java new file mode 100644 index 000000000000..85f74096850f --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisDisableProactiveEventsResponseDTO.java @@ -0,0 +1,9 @@ +package de.tum.cit.aet.artemis.iris.dto; + +import java.time.Instant; + +/** + * DTO for the response of disabling proactive events. + */ +public record IrisDisableProactiveEventsResponseDTO(Instant disabledUntil) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisProactiveEventDisableDTO.java b/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisProactiveEventDisableDTO.java deleted file mode 100644 index 7f6dd3ddace8..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/iris/dto/IrisProactiveEventDisableDTO.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.tum.cit.aet.artemis.iris.dto; - -import java.time.Instant; - -import jakarta.annotation.Nullable; - -/** - * DTO for disabling proactive events. - */ -public record IrisProactiveEventDisableDTO(IrisProactiveEventDisableDuration duration, @Nullable Instant endTime) { -} diff --git a/src/main/webapp/app/core/user/user.model.ts b/src/main/webapp/app/core/user/user.model.ts index 52fa28f56ab9..6f697176b4d6 100644 --- a/src/main/webapp/app/core/user/user.model.ts +++ b/src/main/webapp/app/core/user/user.model.ts @@ -16,6 +16,7 @@ export class User extends Account { public vcsAccessToken?: string; public vcsAccessTokenExpiryDate?: string; public irisAccepted?: dayjs.Dayjs; + public irisProactiveEventsDisabled?: dayjs.Dayjs; constructor( id?: number, @@ -37,6 +38,7 @@ export class User extends Account { vcsAccessToken?: string, vcsAccessTokenExpiryDate?: string, irisAccepted?: dayjs.Dayjs, + irisProactiveEventsDisabled?: dayjs.Dayjs, ) { super(activated, authorities, email, firstName, langKey, lastName, login, imageUrl); this.id = id; @@ -50,6 +52,7 @@ export class User extends Account { this.vcsAccessToken = vcsAccessToken; this.vcsAccessTokenExpiryDate = vcsAccessTokenExpiryDate; this.irisAccepted = irisAccepted; + this.irisProactiveEventsDisabled = irisProactiveEventsDisabled; } } /** diff --git a/src/main/webapp/app/core/user/user.service.ts b/src/main/webapp/app/core/user/user.service.ts index 7544c8525fe0..4385f6a49473 100644 --- a/src/main/webapp/app/core/user/user.service.ts +++ b/src/main/webapp/app/core/user/user.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { User } from 'app/core/user/user.model'; +import { IrisDisableProactiveEventsDTO, IrisDisableProactiveEventsResponseDTO } from 'app/entities/iris/iris-disable-proactive-events-dto.model'; @Injectable({ providedIn: 'root' }) export class UserService { @@ -47,4 +48,11 @@ export class UserService { acceptIris(): Observable> { return this.http.put>(`${this.resourceUrl}/accept-iris`, { observe: 'response' }); } + + /** + * Disable proactive Iris events. + */ + disableProactiveEvents(disableProactiveEventsDTO: IrisDisableProactiveEventsDTO): Observable> { + return this.http.put(`${this.resourceUrl}/disable-iris-proactive-events`, disableProactiveEventsDTO, { observe: 'response' }); + } } diff --git a/src/main/webapp/app/entities/iris/iris-disable-proactive-events-dto.model.ts b/src/main/webapp/app/entities/iris/iris-disable-proactive-events-dto.model.ts new file mode 100644 index 000000000000..3d7e6d1e0431 --- /dev/null +++ b/src/main/webapp/app/entities/iris/iris-disable-proactive-events-dto.model.ts @@ -0,0 +1,18 @@ +import dayjs from 'dayjs'; + +export enum IrisProactiveEventDisableDuration { + THIRTY_MINUTES, + ONE_HOUR, + ONE_DAY, + FOREVER, + CUSTOM, +} + +export class IrisDisableProactiveEventsDTO { + duration: IrisProactiveEventDisableDuration | null; + endTime: dayjs.Dayjs | null; +} + +export class IrisDisableProactiveEventsResponseDTO { + disabledUntil: number | null; +} diff --git a/src/main/webapp/app/entities/iris/iris-errors.model.ts b/src/main/webapp/app/entities/iris/iris-errors.model.ts index fc15951e105d..7cc7c203403b 100644 --- a/src/main/webapp/app/entities/iris/iris-errors.model.ts +++ b/src/main/webapp/app/entities/iris/iris-errors.model.ts @@ -17,6 +17,7 @@ export enum IrisErrorMessageKey { TECHNICAL_ERROR_RESPONSE = 'artemisApp.exerciseChatbot.errors.technicalError', IRIS_NOT_AVAILABLE = 'artemisApp.exerciseChatbot.errors.irisNotAvailable', RATE_LIMIT_EXCEEDED = 'artemisApp.exerciseChatbot.errors.rateLimitExceeded', + DISABLE_PROACTIVE_EVENTS_FAILED = 'artemisApp.exerciseChatbot.errors.disableProactiveEventsFailed', } export interface IrisErrorType { @@ -44,6 +45,7 @@ const IrisErrors: IrisErrorType[] = [ { key: IrisErrorMessageKey.TECHNICAL_ERROR_RESPONSE, fatal: true }, { key: IrisErrorMessageKey.IRIS_NOT_AVAILABLE, fatal: true }, { key: IrisErrorMessageKey.RATE_LIMIT_EXCEEDED, fatal: true }, + { key: IrisErrorMessageKey.DISABLE_PROACTIVE_EVENTS_FAILED, fatal: false }, ]; export const errorMessages: Readonly<{ [key in IrisErrorMessageKey]: IrisErrorType }> = Object.freeze( diff --git a/src/main/webapp/app/entities/iris/settings/iris-personal-settings.model.ts b/src/main/webapp/app/entities/iris/settings/iris-personal-settings.model.ts new file mode 100644 index 000000000000..8120d5f4ebbb --- /dev/null +++ b/src/main/webapp/app/entities/iris/settings/iris-personal-settings.model.ts @@ -0,0 +1,5 @@ +import { IrisDisableProactiveEventsDTO } from 'app/entities/iris/iris-disable-proactive-events-dto.model'; + +export class IrisPersonalSettings { + proactivitySettings: IrisDisableProactiveEventsDTO; +} diff --git a/src/main/webapp/app/iris/iris-chat.service.ts b/src/main/webapp/app/iris/iris-chat.service.ts index 60f9e60ed8f3..6973475be9ab 100644 --- a/src/main/webapp/app/iris/iris-chat.service.ts +++ b/src/main/webapp/app/iris/iris-chat.service.ts @@ -1,8 +1,8 @@ -import { Injectable, OnDestroy } from '@angular/core'; +import { DestroyRef, Injectable, OnDestroy, inject } from '@angular/core'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { IrisAssistantMessage, IrisMessage, IrisSender, IrisUserMessage } from 'app/entities/iris/iris-message.model'; import { IrisErrorMessageKey } from 'app/entities/iris/iris-errors.model'; -import { BehaviorSubject, Observable, Subscription, catchError, map, of, tap, throwError } from 'rxjs'; +import { BehaviorSubject, EMPTY, Observable, Subscription, catchError, map, of, tap, throwError } from 'rxjs'; import { IrisChatHttpService } from 'app/iris/iris-chat-http.service'; import { IrisExerciseChatSession } from 'app/entities/iris/iris-exercise-chat-session.model'; import { IrisStageDTO } from 'app/entities/iris/iris-stage-dto.model'; @@ -14,6 +14,8 @@ import { IrisRateLimitInformation } from 'app/entities/iris/iris-ratelimit-info. import { IrisSession } from 'app/entities/iris/iris-session.model'; import { UserService } from 'app/core/user/user.service'; import { AccountService } from 'app/core/auth/account.service'; +import { IrisDisableProactiveEventsDTO } from 'app/entities/iris/iris-disable-proactive-events-dto.model'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export enum ChatServiceMode { TEXT_EXERCISE = 'text-exercise-chat', @@ -43,6 +45,8 @@ export class IrisChatService implements OnDestroy { hasJustAcceptedIris = false; + private destroyRef = inject(DestroyRef); + /** * Creates an instance of IrisChatService. * @param http The IrisChatHttpService for HTTP operations related to sessions. @@ -170,6 +174,32 @@ export class IrisChatService implements OnDestroy { }); } + /** + * Disables proactive events for the user. + * @param dto The DTO containing the information about the duration of the disablement. + */ + public disableProactiveEvents(dto: IrisDisableProactiveEventsDTO): void { + this.userService + .disableProactiveEvents(dto) + .pipe( + takeUntilDestroyed(this.destroyRef), + map((response) => { + if (!response.body) { + throw new Error(IrisErrorMessageKey.DISABLE_PROACTIVE_EVENTS_FAILED); + } + return response.body; + }), + tap((response) => { + this.status.proactiveEventsDisabledUntil.next(new Date(response.disabledUntil!)); + }), + catchError(() => { + this.error.next(IrisErrorMessageKey.DISABLE_PROACTIVE_EVENTS_FAILED); + return EMPTY; + }), + ) + .subscribe(); + } + private replaceMessage(message: IrisMessage): boolean { const messages = [...this.messages.getValue()]; const index = messages.findIndex((m) => m.id === message.id); @@ -197,7 +227,7 @@ export class IrisChatService implements OnDestroy { /** * Parses the latest suggestions string and updates the suggestions subject. - * @param s: The latest suggestions string + * @param s The JSON string containing the suggestions. * @private */ private parseLatestSuggestions(s?: string) { @@ -291,7 +321,7 @@ export class IrisChatService implements OnDestroy { ); } - switchTo(mode: ChatServiceMode, id?: number): void { + public switchTo(mode: ChatServiceMode, id?: number): void { const newIdentifier = mode && id ? mode + '/' + id : undefined; const isDifferent = this.sessionCreationIdentifier !== newIdentifier; this.sessionCreationIdentifier = newIdentifier;