Skip to content
2 changes: 1 addition & 1 deletion .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:

# TODO these values should become zero in the future
max_large_classes=9
max_complex_beans=10
max_complex_beans=9

echo "=========================================="
echo "CODE QUALITY ANALYSIS SUMMARY"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package de.tum.cit.aet.artemis.exercise.service;

import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import jakarta.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Profile;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.stereotype.Service;

import de.tum.cit.aet.artemis.assessment.domain.Result;
import de.tum.cit.aet.artemis.assessment.repository.ResultRepository;
import de.tum.cit.aet.artemis.core.config.Constants;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation;
import de.tum.cit.aet.artemis.exercise.repository.SubmissionRepository;
import de.tum.cit.aet.artemis.quiz.domain.QuizBatch;
import de.tum.cit.aet.artemis.quiz.domain.QuizExercise;
import de.tum.cit.aet.artemis.quiz.domain.QuizSubmission;
import de.tum.cit.aet.artemis.quiz.dto.participation.StudentQuizParticipationWithQuestionsDTO;
import de.tum.cit.aet.artemis.quiz.dto.participation.StudentQuizParticipationWithSolutionsDTO;
import de.tum.cit.aet.artemis.quiz.dto.participation.StudentQuizParticipationWithoutQuestionsDTO;
import de.tum.cit.aet.artemis.quiz.repository.QuizExerciseRepository;
import de.tum.cit.aet.artemis.quiz.repository.SubmittedAnswerRepository;
import de.tum.cit.aet.artemis.quiz.service.QuizBatchService;
import de.tum.cit.aet.artemis.quiz.service.QuizSubmissionService;

/**
* Service Implementation for managing the participation of students in quiz exercises.
*/
@Profile(Constants.PROFILE_CORE)
@Lazy
@Service
public class QuizParticipationService {

private static final Logger log = LoggerFactory.getLogger(QuizParticipationService.class);

private final QuizBatchService quizBatchService;

private final ParticipationService participationService;

private final QuizSubmissionService quizSubmissionService;

private final ResultRepository resultRepository;

private final SubmittedAnswerRepository submittedAnswerRepository;

private final SubmissionRepository submissionRepository;

private final QuizExerciseRepository quizExerciseRepository;

public QuizParticipationService(QuizBatchService quizBatchService, ParticipationService participationService, QuizSubmissionService quizSubmissionService,
ResultRepository resultRepository, SubmittedAnswerRepository submittedAnswerRepository, SubmissionRepository submissionRepository,
QuizExerciseRepository quizExerciseRepository) {
this.quizBatchService = quizBatchService;
this.participationService = participationService;
this.quizSubmissionService = quizSubmissionService;
this.resultRepository = resultRepository;
this.submittedAnswerRepository = submittedAnswerRepository;
this.submissionRepository = submissionRepository;
this.quizExerciseRepository = quizExerciseRepository;
}

/**
* Handles the request of a student to participate in a quiz exercise.
*
* @param quizExercise the quiz exercise the user wants to participate in
* @param user the user that wants to participate
* @return a DTO containing the participation and possibly the quiz questions
*/
@Nullable
// TODO: use a proper DTO (or interface here for the return type and avoid MappingJacksonValue)
public MappingJacksonValue participationForQuizExercise(QuizExercise quizExercise, User user) {
// 1st case the quiz has already ended
if (quizExercise.isQuizEnded()) {
return handleQuizEnded(quizExercise, user);
}
quizExercise.setQuizBatches(null); // not available here
var quizBatch = quizBatchService.getQuizBatchForStudentByLogin(quizExercise, user.getLogin());

if (quizBatch.isPresent() && quizBatch.get().isStarted()) {
return handleQuizBatchStarted(quizExercise, user, quizBatch);
}
else {
return handleQuizNotStarted(quizExercise, user, quizBatch);
}

}

private MappingJacksonValue handleQuizNotStarted(QuizExercise quizExercise, User user, Optional<QuizBatch> quizBatch) {
// Quiz hasn't started yet => no Result, only quizExercise without questions
quizExercise.filterSensitiveInformation();
quizExercise.setQuizBatches(quizBatch.stream().collect(Collectors.toSet()));
if (quizExercise.getAllowedNumberOfAttempts() != null) {
var attempts = submissionRepository.countByExerciseIdAndStudentLogin(quizExercise.getId(), user.getLogin());
quizExercise.setRemainingNumberOfAttempts(quizExercise.getAllowedNumberOfAttempts() - attempts);
}
StudentParticipation participation = new StudentParticipation().exercise(quizExercise);
return new MappingJacksonValue(participation);
}

private MappingJacksonValue handleQuizBatchStarted(QuizExercise quizExercise, User user, Optional<QuizBatch> quizBatch) {
// Quiz is active => construct Participation from
// filtered quizExercise and submission from HashMap
quizExercise = quizExerciseRepository.findByIdWithQuestionsElseThrow(quizExercise.getId());
quizExercise.setQuizBatches(quizBatch.stream().collect(Collectors.toSet()));
quizExercise.filterForStudentsDuringQuiz();
StudentParticipation participation = participationForQuizWithSubmissionAndResult(quizExercise, user.getLogin(), quizBatch.get());

// TODO: Duplicate
Object responseDTO = null;
if (participation != null) {
var submissions = submissionRepository.findAllWithResultsByParticipationIdOrderBySubmissionDateAsc(participation.getId());
participation.setSubmissions(new HashSet<>(submissions));
if (quizExercise.isQuizEnded()) {
responseDTO = StudentQuizParticipationWithSolutionsDTO.of(participation);
}
else if (quizBatch.get().isStarted()) {
responseDTO = StudentQuizParticipationWithQuestionsDTO.of(participation);
}
else {
responseDTO = StudentQuizParticipationWithoutQuestionsDTO.of(participation);
}
}

return responseDTO != null ? new MappingJacksonValue(responseDTO) : null;
}

private MappingJacksonValue handleQuizEnded(QuizExercise quizExercise, User user) {
// quiz has ended => get participation from database and add full quizExercise
quizExercise = quizExerciseRepository.findByIdWithQuestionsElseThrow(quizExercise.getId());
StudentParticipation participation = participationForQuizWithSubmissionAndResult(quizExercise, user.getLogin(), null);
if (participation == null) {
return null;
}

return new MappingJacksonValue(participation);
}

/**
* Get a participation for the given quiz and username.
* If the quiz hasn't ended, participation is constructed from cached submission.
* If the quiz has ended, we first look in the database for the participation and construct one if none was found
*
* @param quizExercise the quiz exercise to attach to the participation
* @param username the username of the user that the participation belongs to
* @param quizBatch the quiz batch of quiz exercise which user participated in
* @return the found or created participation with a result
*/
private StudentParticipation participationForQuizWithSubmissionAndResult(QuizExercise quizExercise, String username, QuizBatch quizBatch) {
// try getting participation from database
Optional<StudentParticipation> optionalParticipation = participationService.findOneByExerciseAndStudentLoginAnyState(quizExercise, username);

if (quizExercise.isQuizEnded() || quizSubmissionService.hasUserSubmitted(quizBatch, username)) {

if (optionalParticipation.isEmpty()) {
log.error("Participation in quiz {} not found for user {}", quizExercise.getTitle(), username);
// TODO properly handle this case
return null;
}
StudentParticipation participation = optionalParticipation.get();
// add exercise
participation.setExercise(quizExercise);

// add the appropriate submission and result
Result result = resultRepository.findFirstByParticipationIdAndRatedWithSubmissionOrderByCompletionDateDesc(participation.getId(), true).orElse(null);
if (result != null) {
// find the submitted answers (they are NOT loaded eagerly anymore)
var quizSubmission = (QuizSubmission) result.getSubmission();
quizSubmission.setResults(List.of(result));
var submittedAnswers = submittedAnswerRepository.findBySubmission(quizSubmission);
quizSubmission.setSubmittedAnswers(submittedAnswers);
participation.setSubmissions(Set.of(quizSubmission));
}
return participation;
}

return optionalParticipation.orElse(null);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package de.tum.cit.aet.artemis.exercise.web;

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

import java.security.Principal;

import jakarta.validation.constraints.NotNull;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.boot.actuate.audit.AuditEventRepository;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import de.tum.cit.aet.artemis.core.config.Constants;
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.AccessForbiddenException;
import de.tum.cit.aet.artemis.core.repository.UserRepository;
import de.tum.cit.aet.artemis.core.security.Role;
import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastInstructor;
import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService;
import de.tum.cit.aet.artemis.core.service.feature.Feature;
import de.tum.cit.aet.artemis.core.service.feature.FeatureToggle;
import de.tum.cit.aet.artemis.core.service.feature.FeatureToggleService;
import de.tum.cit.aet.artemis.core.util.HeaderUtil;
import de.tum.cit.aet.artemis.exercise.domain.participation.Participation;
import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation;
import de.tum.cit.aet.artemis.exercise.repository.StudentParticipationRepository;
import de.tum.cit.aet.artemis.exercise.service.ParticipationDeletionService;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseParticipation;
import de.tum.cit.aet.artemis.programming.domain.ProgrammingExerciseStudentParticipation;

/**
* REST controller for deleting a participation and cleaning up a build plan for a participation.
*/
@Profile(PROFILE_CORE)
@Lazy
@RestController
@RequestMapping("api/exercise/")
public class ParticipationDeletionResource {

private static final Logger log = LoggerFactory.getLogger(ParticipationDeletionResource.class);

@Value("${jhipster.clientApp.name}")
private String applicationName;

private final ParticipationDeletionService participationDeletionService;

private final AuthorizationCheckService authCheckService;

private final FeatureToggleService featureToggleService;

private final UserRepository userRepository;

private final AuditEventRepository auditEventRepository;

private final StudentParticipationRepository studentParticipationRepository;

public ParticipationDeletionResource(ParticipationDeletionService participationDeletionService, AuthorizationCheckService authCheckService, UserRepository userRepository,
StudentParticipationRepository studentParticipationRepository, AuditEventRepository auditEventRepository, FeatureToggleService featureToggleService) {
this.participationDeletionService = participationDeletionService;
this.authCheckService = authCheckService;
this.userRepository = userRepository;
this.auditEventRepository = auditEventRepository;
this.featureToggleService = featureToggleService;
this.studentParticipationRepository = studentParticipationRepository;
}

/**
* DELETE /participations/:participationId : delete the "participationId" participation. This only works for student participations - other participations should not be deleted
* here!
*
* @param participationId the participationId of the participation to delete
* @return the ResponseEntity with status 200 (OK)
*/
@DeleteMapping("participations/{participationId}")
@EnforceAtLeastInstructor
public ResponseEntity<Void> deleteParticipation(@PathVariable Long participationId) {
StudentParticipation participation = studentParticipationRepository.findByIdElseThrow(participationId);
if (participation instanceof ProgrammingExerciseParticipation && !featureToggleService.isFeatureEnabled(Feature.ProgrammingExercises)) {
throw new AccessForbiddenException("Programming Exercise Feature is disabled.");
}
User user = userRepository.getUserWithGroupsAndAuthorities();
checkAccessPermissionAtLeastInstructor(participation, user);
return deleteParticipation(participation, user);
}

/**
* delete the participation, potentially including build plan and repository and log the event in the database audit
*
* @param participation the participation to be deleted
* @param user the currently logged-in user who initiated the delete operation
* @return the response to the client
*/
@NotNull
private ResponseEntity<Void> deleteParticipation(StudentParticipation participation, User user) {
String name = participation.getParticipantName();
var logMessage = "Delete Participation " + participation.getId() + " of exercise " + participation.getExercise().getTitle() + " for " + name + " by " + user.getLogin();
var auditEvent = new AuditEvent(user.getLogin(), Constants.DELETE_PARTICIPATION, logMessage);
auditEventRepository.add(auditEvent);
log.info(logMessage);
participationDeletionService.delete(participation.getId(), true);
return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, "participation", name)).build();
}

/**
* DELETE /participations/:participationId/cleanup-build-plan : remove the build plan of the ProgrammingExerciseStudentParticipation of the "participationId".
* This only works for programming exercises.
*
* @param participationId the participationId of the ProgrammingExerciseStudentParticipation for which the build plan should be removed
* @param principal The identity of the user accessing this resource
* @return the ResponseEntity with status 200 (OK)
*/
@PutMapping("participations/{participationId}/cleanup-build-plan")
@EnforceAtLeastInstructor
@FeatureToggle(Feature.ProgrammingExercises)
public ResponseEntity<Participation> cleanupBuildPlan(@PathVariable Long participationId, Principal principal) {
ProgrammingExerciseStudentParticipation participation = (ProgrammingExerciseStudentParticipation) studentParticipationRepository.findByIdElseThrow(participationId);
User user = userRepository.getUserWithGroupsAndAuthorities();
checkAccessPermissionAtLeastInstructor(participation, user);
log.info("Clean up participation with build plan {} by {}", participation.getBuildPlanId(), principal.getName());
participationDeletionService.cleanupBuildPlan(participation);
return ResponseEntity.ok().body(participation);
}

private void checkAccessPermissionAtLeastInstructor(StudentParticipation participation, User user) {
Course course = findCourseFromParticipation(participation);
authCheckService.checkHasAtLeastRoleInCourseElseThrow(Role.INSTRUCTOR, course, user);
}

private Course findCourseFromParticipation(StudentParticipation participation) {
if (participation.getExercise() != null && participation.getExercise().getCourseViaExerciseGroupOrCourseMember() != null) {
return participation.getExercise().getCourseViaExerciseGroupOrCourseMember();
}

return studentParticipationRepository.findByIdElseThrow(participation.getId()).getExercise().getCourseViaExerciseGroupOrCourseMember();
}

}
Loading
Loading