Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Iris: Make proactive events optional for students #10144

Draft
wants to merge 3 commits into
base: bugfix/iris/event-service-exception
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/main/java/de/tum/cit/aet/artemis/core/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ public class User extends AbstractAuditingEntity implements Participant {
@JoinColumn(name = "learner_profile_id")
private LearnerProfile learnerProfile;

@Nullable
@Column(name = "iris_proactive_events_disabled")
private Instant irisProactiveEventsDisabled = null;

public User() {
}

Expand Down Expand Up @@ -545,6 +549,24 @@ 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());
}

/**
* Checks if the user has accepted the Iris privacy policy.
* If not, an {@link AccessForbiddenException} is thrown.
Expand Down
17 changes: 15 additions & 2 deletions src/main/java/de/tum/cit/aet/artemis/core/dto/UserDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,19 +78,23 @@ public class UserDTO extends AuditingEntityDTO {

private ZonedDateTime irisAccepted;

private Instant irisProactiveEventsDisabled;

public UserDTO() {
// Empty constructor needed for Jackson.
}

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<Authority> authorities, Set<String> groups, Set<GuidedTourSetting> guidedTourSettings, Set<Organization> organizations, ZonedDateTime irisAccepted) {
Set<Authority> authorities, Set<String> groups, Set<GuidedTourSetting> guidedTourSettings, Set<Organization> organizations, ZonedDateTime irisAccepted,
Instant irisProactiveEventsDisabled) {

this.id = id;
this.login = login;
Expand All @@ -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() {
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -747,6 +748,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -64,6 +65,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;
Expand Down Expand Up @@ -822,6 +824,28 @@ 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
* @return the updated timestamp of the end time
*/
public Instant updateIrisProactiveEventsDisabled(Long userId, IrisProactiveEventDisableDuration duration, Instant endTime) {
var currentTimestamp = Instant.now();
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;
}

/**
* This method first tries to find and then to add each user of the given list to the course
*
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/de/tum/cit/aet/artemis/core/web/UserResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -31,6 +32,9 @@
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.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;

Expand Down Expand Up @@ -101,6 +105,7 @@ public ResponseEntity<List<UserDTO>> 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);
Expand Down Expand Up @@ -169,4 +174,30 @@ public ResponseEntity<Void> setIrisAcceptedToTimestamp() {
userRepository.updateIrisAcceptedToDate(user.getId(), ZonedDateTime.now());
return ResponseEntity.ok().build();
}

@PutMapping("users/disable-iris-proactive-events")
@EnforceAtLeastStudent
public ResponseEntity<IrisDisableProactiveEventsResponseDTO> 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 {
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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,6 @@ public interface SubmissionRepository extends ArtemisJpaRepository<Submission, L
@EntityGraph(type = LOAD, attributePaths = { "results", "results.assessor" })
List<Submission> 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<Submission> findAllWithResultsByParticipationIdOrderBySubmissionDateAsc(Long participationId);

/**
* Get all submissions with their results by the submission ids
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
}
Original file line number Diff line number Diff line change
@@ -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) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package de.tum.cit.aet.artemis.iris.dto;

public enum IrisProactiveEventDisableDuration {
THIRTY_MINUTES, ONE_HOUR, ONE_DAY, FOREVER, CUSTOM
}
Original file line number Diff line number Diff line change
@@ -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) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -92,7 +90,6 @@ public IrisExerciseChatSessionService(IrisMessageService irisMessageService, LLM
this.pyrisPipelineService = pyrisPipelineService;
this.programmingExerciseRepository = programmingExerciseRepository;
this.irisExerciseChatSessionRepository = irisExerciseChatSessionRepository;
this.submissionRepository = submissionRepository;
}

/**
Expand Down Expand Up @@ -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);
Expand All @@ -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());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ public ResponseEntity<IrisStatusDTO> 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));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,26 @@ default Optional<ProgrammingSubmission> findFirstByParticipationIdWithResultsOrd
@EntityGraph(type = LOAD, attributePaths = { "results" })
List<ProgrammingSubmission> findSubmissionsWithResultsByIdIn(List<Long> 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<ProgrammingSubmission> findSubmissionsWithResultsByParticipationIdAndBuildFailedIsFalseAndTypeIsNotIllegalOrderBySubmissionDateAsc(
@Param("participationId") long participationId);

/**
* Provide a list of graded submissions. To be graded a submission must:
* - be of type 'INSTRUCTOR' or 'TEST'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>

<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="20250113223500" author="kaancayli">
<!-- Add new column to iris_sub_settings -->
<addColumn tableName="jhi_user">
<column name="iris_proactive_events_disabled" type="datetime(3)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
1 change: 1 addition & 0 deletions src/main/resources/config/liquibase/master.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<include file="classpath:config/liquibase/changelog/20241125000900_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20241107130000_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20241217150008_changelog.xml" relativeToChangelogFile="false"/>
<include file="classpath:config/liquibase/changelog/20250113223500_changelog.xml" relativeToChangelogFile="false"/>

<!-- NOTE: please use the format "YYYYMMDDhhmmss_changelog.xml", i.e. year month day hour minutes seconds and not something else! -->
<!-- we should also stay in a chronological order! -->
Expand Down
Loading
Loading