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

Development: Prevent exception in Iris for inactive exercises #10099

Merged
merged 8 commits into from
Jan 4, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,8 @@ public Result createNewManualResult(Result result, boolean ratedResult) {
return savedResult;
}

public Result createNewRatedManualResult(Result result) {
return createNewManualResult(result, true);
public void createNewRatedManualResult(Result result) {
createNewManualResult(result, true);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import de.tum.cit.aet.artemis.communication.service.WebsocketMessagingService;
import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService;
import de.tum.cit.aet.artemis.exam.service.ExamDateService;
import de.tum.cit.aet.artemis.exercise.domain.Exercise;
import de.tum.cit.aet.artemis.exercise.domain.Team;
import de.tum.cit.aet.artemis.exercise.domain.participation.Participation;
import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation;
Expand Down Expand Up @@ -73,7 +72,7 @@ public void broadcastNewResult(Participation participation, Result result) {
}

private void broadcastNewResultToParticipants(StudentParticipation studentParticipation, Result result) {
final Exercise exercise = studentParticipation.getExercise();
final var exercise = studentParticipation.getExercise();
boolean isWorkingPeriodOver;
if (exercise.isExamExercise()) {
isWorkingPeriodOver = examDateService.isIndividualExerciseWorkingPeriodOver(exercise.getExam(), studentParticipation);
Expand Down
4 changes: 4 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 @@ -534,6 +534,10 @@ public void setIrisAcceptedTimestamp(@Nullable ZonedDateTime irisAccepted) {
this.irisAccepted = irisAccepted;
}

public boolean hasAcceptedIris() {
return irisAccepted != null;
}

/**
* Checks if the user has accepted the Iris privacy policy.
* If not, an {@link AccessForbiddenException} is thrown.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ public interface IrisExerciseChatSessionRepository extends ArtemisJpaRepository<
* @return A list of chat sessions sorted by creation date in descending order.
*/
@Query("""

SELECT s
FROM IrisExerciseChatSession s
WHERE s.exercise.id = :exerciseId
Expand Down Expand Up @@ -68,11 +67,9 @@ public interface IrisExerciseChatSessionRepository extends ArtemisJpaRepository<
*/
default List<IrisExerciseChatSession> findLatestByExerciseIdAndUserIdWithMessages(Long exerciseId, Long userId, Pageable pageable) {
List<Long> ids = findSessionsByExerciseIdAndUserId(exerciseId, userId, pageable).stream().map(DomainObject::getId).toList();

if (ids.isEmpty()) {
return Collections.emptyList();
}

return findSessionsWithMessagesByIdIn(ids);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package de.tum.cit.aet.artemis.iris.repository;

import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_IRIS;

import org.springframework.context.annotation.Profile;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository;
import de.tum.cit.aet.artemis.iris.domain.settings.IrisExerciseSettings;

@Repository
@Profile(PROFILE_IRIS)
public interface IrisExerciseSettingsRepository extends ArtemisJpaRepository<IrisExerciseSettings, Long> {

@Query("""
SELECT EXISTS(s)
FROM IrisExerciseSettings s
WHERE s.exercise.id = :exerciseId
AND s.irisTextExerciseChatSettings.enabled = TRUE
""")
boolean isExerciseChatEnabled(@Param("exerciseId") long exerciseId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import de.tum.cit.aet.artemis.iris.domain.session.IrisChatSession;
Expand Down Expand Up @@ -41,17 +42,18 @@ public PyrisEventService(IrisCourseChatSessionService irisCourseChatSessionServi
*
* @see PyrisEvent
*/
@Async
public void trigger(PyrisEvent<? extends AbstractIrisChatSessionService<? extends IrisChatSession>, ?> event) {
log.debug("Starting to process event of type: {}", event.getClass().getSimpleName());
try {
switch (event) {
case CompetencyJolSetEvent competencyJolSetEvent -> {
log.info("Processing CompetencyJolSetEvent: {}", competencyJolSetEvent);
log.debug("Processing CompetencyJolSetEvent: {}", competencyJolSetEvent);
competencyJolSetEvent.handleEvent(irisCourseChatSessionService);
log.debug("Successfully processed CompetencyJolSetEvent");
}
case NewResultEvent newResultEvent -> {
log.info("Processing NewResultEvent: {}", newResultEvent);
log.debug("Processing NewResultEvent: {}", newResultEvent);
newResultEvent.handleEvent(irisExerciseChatSessionService);
log.debug("Successfully processed NewResultEvent");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_LTI;

import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation;
Expand All @@ -22,6 +23,7 @@ public LtiNewResultService(Lti13Service lti13Service) {
*
* @param participation The exercise participation for which a new build result is available
*/
@Async
public void onNewResult(StudentParticipation participation) {
if (!participation.getExercise().getCourseViaExerciseGroupOrCourseMember().isOnlineCourse()) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import de.tum.cit.aet.artemis.exercise.dto.SubmissionDTO;
import de.tum.cit.aet.artemis.exercise.repository.ParticipationRepository;
import de.tum.cit.aet.artemis.exercise.repository.TeamRepository;
import de.tum.cit.aet.artemis.iris.repository.IrisExerciseSettingsRepository;
import de.tum.cit.aet.artemis.iris.service.pyris.PyrisEventService;
import de.tum.cit.aet.artemis.iris.service.pyris.event.NewResultEvent;
import de.tum.cit.aet.artemis.lti.service.LtiNewResultService;
Expand Down Expand Up @@ -57,16 +58,20 @@ public class ProgrammingMessagingService {

private final Optional<PyrisEventService> pyrisEventService;

private final Optional<IrisExerciseSettingsRepository> irisExerciseSettingsRepository;

private final ParticipationRepository participationRepository;

public ProgrammingMessagingService(GroupNotificationService groupNotificationService, WebsocketMessagingService websocketMessagingService,
ResultWebsocketService resultWebsocketService, Optional<LtiNewResultService> ltiNewResultService, TeamRepository teamRepository,
Optional<PyrisEventService> pyrisEventService, ParticipationRepository participationRepository) {
Optional<PyrisEventService> pyrisEventService, Optional<IrisExerciseSettingsRepository> irisExerciseSettingsRepository,
ParticipationRepository participationRepository) {
this.groupNotificationService = groupNotificationService;
this.websocketMessagingService = websocketMessagingService;
this.resultWebsocketService = resultWebsocketService;
this.ltiNewResultService = ltiNewResultService;
this.teamRepository = teamRepository;
this.irisExerciseSettingsRepository = irisExerciseSettingsRepository;
this.participationRepository = participationRepository;
this.pyrisEventService = pyrisEventService;
}
Expand Down Expand Up @@ -188,13 +193,16 @@ public void notifyUserAboutNewResult(Result result, ProgrammingExerciseParticipa
if (participation instanceof ProgrammingExerciseStudentParticipation studentParticipation) {
// do not try to report results for template or solution participations
ltiNewResultService.ifPresent(newResultService -> newResultService.onNewResult(studentParticipation));
// Inform Iris about the submission status
// Inform Iris about the submission status (when certain conditions are met)
krusche marked this conversation as resolved.
Show resolved Hide resolved
notifyIrisAboutSubmissionStatus(result, studentParticipation);
}
}

/**
* Notify Iris about the submission status for the given result and student participation.
* Only if the user has accepted Iris, the exercise is not an exam exercise, and the exercise chat is enabled in the exercise settings
krusche marked this conversation as resolved.
Show resolved Hide resolved
* NOTE: we check those settings early to prevent unnecessary database queries and exceptions later on in most cases. More sophisticated checks are done in the Iris service.
* <p>
* If the submission was successful, Iris will be informed about the successful submission.
* If the submission failed, Iris will be informed about the submission failure.
* Iris will only be informed about the submission status if the participant is a user.
Expand All @@ -203,14 +211,18 @@ public void notifyUserAboutNewResult(Result result, ProgrammingExerciseParticipa
* @param studentParticipation the student participation for which Iris should be informed about the submission status
*/
private void notifyIrisAboutSubmissionStatus(Result result, ProgrammingExerciseStudentParticipation studentParticipation) {
if (studentParticipation.getParticipant() instanceof User) {
if (studentParticipation.getParticipant() instanceof User user) {
pyrisEventService.ifPresent(eventService -> {
// Inform event service about the new result
try {
eventService.trigger(new NewResultEvent(result));
}
catch (Exception e) {
log.error("Could not trigger service for result {}", result.getId(), e);
final var exercise = studentParticipation.getExercise();
if (user.hasAcceptedIris() && !exercise.isExamExercise() && irisExerciseSettingsRepository.get().isExerciseChatEnabled(exercise.getId())) {
krusche marked this conversation as resolved.
Show resolved Hide resolved
// Inform event service about the new result
try {
// This is done asynchronously to prevent blocking the current thread
eventService.trigger(new NewResultEvent(result));
}
catch (Exception e) {
log.error("Could not trigger service for result {}", result.getId(), e);
}
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import de.tum.cit.aet.artemis.atlas.domain.competency.CompetencyJol;
import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.core.exception.AccessForbiddenAlertException;
import de.tum.cit.aet.artemis.core.user.util.UserUtilService;
import de.tum.cit.aet.artemis.exercise.domain.SubmissionType;
import de.tum.cit.aet.artemis.exercise.participation.util.ParticipationFactory;
Expand All @@ -40,13 +39,7 @@
import de.tum.cit.aet.artemis.iris.domain.settings.event.IrisEventType;
import de.tum.cit.aet.artemis.iris.repository.IrisSettingsRepository;
import de.tum.cit.aet.artemis.iris.service.pyris.PyrisEventProcessingException;
import de.tum.cit.aet.artemis.iris.service.pyris.PyrisEventService;
import de.tum.cit.aet.artemis.iris.service.pyris.PyrisJobService;
import de.tum.cit.aet.artemis.iris.service.pyris.PyrisStatusUpdateService;
import de.tum.cit.aet.artemis.iris.service.pyris.UnsupportedPyrisEventException;
import de.tum.cit.aet.artemis.iris.service.pyris.event.NewResultEvent;
import de.tum.cit.aet.artemis.iris.service.pyris.event.PyrisEvent;
import de.tum.cit.aet.artemis.iris.service.session.IrisExerciseChatSessionService;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExercise;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission;
Expand All @@ -59,12 +52,6 @@ class PyrisEventSystemIntegrationTest extends AbstractIrisIntegrationTest {

private static final String TEST_PREFIX = "pyriseventsystemintegration";

@Autowired
protected PyrisStatusUpdateService pyrisStatusUpdateService;

@Autowired
protected PyrisJobService pyrisJobService;

@Autowired
protected IrisSettingsRepository irisSettingsRepository;

Expand All @@ -74,9 +61,6 @@ class PyrisEventSystemIntegrationTest extends AbstractIrisIntegrationTest {
@Autowired
private SubmissionTestRepository submissionRepository;

@Autowired
private PyrisEventService pyrisEventService;

@Autowired
private ParticipationUtilService participationUtilService;

Expand All @@ -103,6 +87,13 @@ class PyrisEventSystemIntegrationTest extends AbstractIrisIntegrationTest {
void initTestCase() throws GitAPIException, IOException, URISyntaxException {
userUtilService.addUsers(TEST_PREFIX, 2, 0, 0, 1);

var student1 = userUtilService.getUserByLogin(TEST_PREFIX + "student1");
student1.setIrisAcceptedTimestamp(ZonedDateTime.now().minusDays(1));
userTestRepository.save(student1);
var student2 = userUtilService.getUserByLogin(TEST_PREFIX + "student2");
student2.setIrisAcceptedTimestamp(ZonedDateTime.now().minusDays(1));
userTestRepository.save(student2);

course = programmingExerciseUtilService.addCourseWithOneProgrammingExercise();
competency = competencyUtilService.createCompetency(course);
exercise = exerciseUtilService.getFirstExerciseWithType(course, ProgrammingExercise.class);
Expand Down Expand Up @@ -194,7 +185,9 @@ void testShouldFireProgressStalledEvent() {
});

pyrisEventService.trigger(new NewResultEvent(result));
verify(irisExerciseChatSessionService, times(1)).onNewResult(eq(result));
// Wrap the following code into await() to ensure that the pipeline is executed before the test finishes.

await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> verify(irisExerciseChatSessionService, times(1)).onNewResult(eq(result)));

await().atMost(2, TimeUnit.SECONDS).until(() -> pipelineDone.get());

Expand All @@ -214,12 +207,13 @@ void testShouldFireBuildFailedEvent() {
});

pyrisEventService.trigger(new NewResultEvent(result));
verify(irisExerciseChatSessionService, times(1)).onBuildFailure(eq(result));

await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> verify(irisExerciseChatSessionService, times(1)).onBuildFailure(eq(result)));

await().atMost(2, TimeUnit.SECONDS).until(() -> pipelineDone.get());

verify(pyrisPipelineService, times(1)).executeExerciseChatPipeline(eq("default"), eq(Optional.ofNullable((ProgrammingSubmission) result.getSubmission())), eq(exercise),
eq(irisSession), eq(Optional.of("build_failed")));
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> verify(pyrisPipelineService, times(1)).executeExerciseChatPipeline(eq("default"),
eq(Optional.ofNullable((ProgrammingSubmission) result.getSubmission())), eq(exercise), eq(irisSession), eq(Optional.of("build_failed"))));
}

@Test
Expand All @@ -237,20 +231,6 @@ void testShouldFireJolEvent() {

verify(irisCourseChatSessionService, times(1)).onJudgementOfLearningSet(any(CompetencyJol.class));
verify(pyrisPipelineService, times(1)).executeCourseChatPipeline(eq("default"), eq(irisSession), any(CompetencyJol.class));

}

@Test
@WithMockUser(username = TEST_PREFIX + "student1", roles = "USER")
void testShouldThrowUnsupportedEventException() {
assertThatExceptionOfType(UnsupportedPyrisEventException.class).isThrownBy(() -> pyrisEventService.trigger(new PyrisEvent<IrisExerciseChatSessionService, Object>() {

@Override
public void handleEvent(IrisExerciseChatSessionService service) {
// Do nothing
}
})).withMessageStartingWith("Unsupported event");

}

@Test
Expand All @@ -264,7 +244,7 @@ void testShouldNotFireProgressStalledEventWithEventDisabled() {
createSubmissionWithScore(studentParticipation, 40);
createSubmissionWithScore(studentParticipation, 40);
var result = createSubmissionWithScore(studentParticipation, 40);
assertThatExceptionOfType(AccessForbiddenAlertException.class).isThrownBy(() -> pyrisEventService.trigger(new NewResultEvent(result)));
verify(pyrisEventService, times(0)).trigger(new NewResultEvent(result));
krusche marked this conversation as resolved.
Show resolved Hide resolved
}

@Test
Expand All @@ -278,7 +258,8 @@ void testShouldNotFireBuildFailedEventWhenEventSettingDisabled() {
irisExerciseChatSessionService.createChatSessionForProgrammingExercise(exercise, userUtilService.getUserByLogin(TEST_PREFIX + "student1"));
// Create a failing submission for the student.
var result = createFailingSubmission(studentParticipation);
assertThatExceptionOfType(AccessForbiddenAlertException.class).isThrownBy(() -> pyrisEventService.trigger(new NewResultEvent(result)));
// very that the event is not fired
verify(pyrisEventService, times(0)).trigger(new NewResultEvent(result));
}

@Test
Expand All @@ -301,8 +282,10 @@ void testShouldShouldNotFireProgressStalledEventWithExistingSuccessfulSubmission
pyrisEventService.trigger(new NewResultEvent(result));
await().atMost(2, TimeUnit.SECONDS);

verify(irisExerciseChatSessionService, times(2)).onNewResult(any(Result.class));
verify(pyrisPipelineService, times(0)).executeExerciseChatPipeline(any(), any(), any(), any(), any());
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
verify(irisExerciseChatSessionService, times(2)).onNewResult(any(Result.class));
verify(pyrisPipelineService, times(0)).executeExerciseChatPipeline(any(), any(), any(), any(), any());
});
}

@Test
Expand All @@ -315,7 +298,7 @@ void testShouldNotFireProgressStalledEventWithLessThanThreeSubmissions() {

pyrisEventService.trigger(new NewResultEvent(result));

verify(irisExerciseChatSessionService, times(1)).onNewResult(any(Result.class));
verify(irisExerciseChatSessionService, times(0)).onNewResult(any(Result.class));
verify(pyrisPipelineService, times(0)).executeExerciseChatPipeline(any(), any(), any(), any(), any());
}

Expand All @@ -330,7 +313,7 @@ void testShouldNotFireProgressStalledEventWithIncreasingScores() {

pyrisEventService.trigger(new NewResultEvent(result));

verify(irisExerciseChatSessionService, times(1)).onNewResult(any(Result.class));
verify(irisExerciseChatSessionService, times(0)).onNewResult(any(Result.class));
verify(pyrisPipelineService, times(0)).executeExerciseChatPipeline(any(), any(), any(), any(), any());
}

Expand Down
Loading
Loading