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: Introduce exam module API #10262

Open
wants to merge 26 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
df8bc10
add common ApiNotPresentException
ole-ve Feb 4, 2025
00aa592
add ExamAccessApi and use for module-external communication
ole-ve Feb 4, 2025
40a5742
add ExamDateApi and use for module-external communication
ole-ve Feb 4, 2025
1e0bd03
add ExamLiveEventsApi and use for module-external communication
ole-ve Feb 4, 2025
420d25d
add ExamDeletionApi and use for module-external communication
ole-ve Feb 4, 2025
4e5a1a4
add ExamSubmissionApi and use for module-external communication
ole-ve Feb 4, 2025
548775c
add ExamApi and use for module-external communication
ole-ve Feb 4, 2025
88f04e7
migrate remaining APIs
ole-ve Feb 4, 2025
134c45f
add arch tests to validate api architecture for exam module
ole-ve Feb 4, 2025
0b151d9
lazily wire ExamService in ExamApi
ole-ve Feb 4, 2025
8f8e36d
replace ExamApi with ExamRepositoryApi when only facading to ExamRepo…
ole-ve Feb 7, 2025
5c64881
remove comment
ole-ve Feb 7, 2025
c7361c1
move access to exam result check from AuthorizationCheckService to Ex…
ole-ve Feb 7, 2025
b7b6986
align documentation
ole-ve Feb 7, 2025
b3a7918
throw AccessForbiddenException when exercise is exam exercise, but th…
ole-ve Feb 7, 2025
b634ea9
improve JavaDoc
ole-ve Feb 7, 2025
fd896b5
enforce exercise is part of an exam before in checkIfAllowedToGetExam…
ole-ve Feb 7, 2025
aca0fd3
fix condition in ExamAccessService#checkIfAllowedToGetExamResult
ole-ve Feb 8, 2025
1878c93
use modelingExercise instead of participation for checking exam auth …
ole-ve Feb 8, 2025
9b1dd76
add tests for ExamAccessService#checkIfAllowedToGetExamResult
ole-ve Feb 8, 2025
fbdf893
fix exam auth condition in TextExerciseResource
ole-ve Feb 8, 2025
163a5d6
replace StudentParticipationRepository with respective test repository
ole-ve Feb 8, 2025
356d9c1
Merge branch 'develop' into chore/add-exam-api
ole-ve Feb 10, 2025
08d25cc
merge with origin/develop
ole-ve Feb 16, 2025
85aa983
merge aftermath
ole-ve Feb 16, 2025
a4543fa
explicitly type unpacked optional api instead of using var-identifier
ole-ve Feb 16, 2025
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 @@ -21,11 +21,12 @@
import de.tum.cit.aet.artemis.assessment.web.ResultWebsocketService;
import de.tum.cit.aet.artemis.communication.service.notifications.SingleUserNotificationService;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.core.exception.ApiNotPresentException;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException;
import de.tum.cit.aet.artemis.core.repository.UserRepository;
import de.tum.cit.aet.artemis.exam.api.ExamDateApi;
import de.tum.cit.aet.artemis.exam.domain.Exam;
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.Submission;
import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation;
Expand Down Expand Up @@ -53,7 +54,7 @@ public class AssessmentService {

protected final ResultService resultService;

private final ExamDateService examDateService;
private final Optional<ExamDateApi> examDateApi;

protected final SubmissionRepository submissionRepository;

Expand All @@ -69,7 +70,7 @@ public class AssessmentService {

public AssessmentService(ComplaintResponseService complaintResponseService, ComplaintRepository complaintRepository, FeedbackRepository feedbackRepository,
ResultRepository resultRepository, StudentParticipationRepository studentParticipationRepository, ResultService resultService, SubmissionService submissionService,
SubmissionRepository submissionRepository, ExamDateService examDateService, UserRepository userRepository, Optional<LtiNewResultService> ltiNewResultService,
SubmissionRepository submissionRepository, Optional<ExamDateApi> examDateApi, UserRepository userRepository, Optional<LtiNewResultService> ltiNewResultService,
SingleUserNotificationService singleUserNotificationService, ResultWebsocketService resultWebsocketService) {
this.complaintResponseService = complaintResponseService;
this.complaintRepository = complaintRepository;
Expand All @@ -79,7 +80,7 @@ public AssessmentService(ComplaintResponseService complaintResponseService, Comp
this.resultService = resultService;
this.submissionService = submissionService;
this.submissionRepository = submissionRepository;
this.examDateService = examDateService;
this.examDateApi = examDateApi;
this.userRepository = userRepository;
this.ltiNewResultService = ltiNewResultService;
this.singleUserNotificationService = singleUserNotificationService;
Expand Down Expand Up @@ -155,7 +156,8 @@ public boolean isAllowedToCreateOrOverrideResult(Result existingResult, Exercise
// Tutors can assess exam exercises only after the last student has finished the exam and before the publishing result date
if (isExamMode && !isAtLeastInstructor) {
final Exam exam = exercise.getExerciseGroup().getExam();
ZonedDateTime latestExamDueDate = examDateService.getLatestIndividualExamEndDate(exam.getId());
var api = examDateApi.orElseThrow(() -> new ApiNotPresentException(ExamDateApi.class, PROFILE_CORE));
ZonedDateTime latestExamDueDate = api.getLatestIndividualExamEndDate(exam.getId());
if (latestExamDueDate.isAfter(ZonedDateTime.now()) || (exam.getPublishResultsDate() != null && exam.getPublishResultsDate().isBefore(ZonedDateTime.now()))) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@
import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.core.domain.DomainObject;
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.core.exception.ApiNotPresentException;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
import de.tum.cit.aet.artemis.core.repository.UserRepository;
import de.tum.cit.aet.artemis.exam.api.ExamRepositoryApi;
import de.tum.cit.aet.artemis.exam.domain.Exam;
import de.tum.cit.aet.artemis.exam.repository.ExamRepository;
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.Participant;
Expand All @@ -55,16 +56,16 @@ public class ComplaintService {

private final UserRepository userRepository;

private final ExamRepository examRepository;
private final Optional<ExamRepositoryApi> examRepositoryApi;

private final TeamRepository teamRepository;

public ComplaintService(ComplaintRepository complaintRepository, ComplaintResponseRepository complaintResponseRepository, ResultRepository resultRepository,
ExamRepository examRepository, UserRepository userRepository, TeamRepository teamRepository) {
Optional<ExamRepositoryApi> examRepositoryApi, UserRepository userRepository, TeamRepository teamRepository) {
this.complaintRepository = complaintRepository;
this.complaintResponseRepository = complaintResponseRepository;
this.resultRepository = resultRepository;
this.examRepository = examRepository;
this.examRepositoryApi = examRepositoryApi;
this.userRepository = userRepository;
this.teamRepository = teamRepository;
}
Expand Down Expand Up @@ -99,7 +100,8 @@ public Complaint createComplaint(ComplaintRequestDTO complaintRequest, Optional<

// checking if it is allowed to create a complaint
if (examId.isPresent()) {
final Exam exam = examRepository.findByIdElseThrow(examId.get());
var api = examRepositoryApi.orElseThrow(() -> new ApiNotPresentException(ExamRepositoryApi.class, PROFILE_CORE));
final Exam exam = api.findByIdElseThrow(examId.get());
final Set<User> instructors = userRepository.getInstructors(exam.getCourse());
boolean examTestRun = instructors.stream().anyMatch(instructor -> instructor.getLogin().equals(principal.getName()));
if (!examTestRun && !isTimeOfComplaintValid(exam)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,14 @@
import de.tum.cit.aet.artemis.core.domain.User;
import de.tum.cit.aet.artemis.core.dto.SearchResultPageDTO;
import de.tum.cit.aet.artemis.core.dto.SortingOrder;
import de.tum.cit.aet.artemis.core.exception.ApiNotPresentException;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
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.service.AuthorizationCheckService;
import de.tum.cit.aet.artemis.core.util.PageUtil;
import de.tum.cit.aet.artemis.exam.api.StudentExamApi;
import de.tum.cit.aet.artemis.exam.domain.Exam;
import de.tum.cit.aet.artemis.exam.repository.StudentExamRepository;
import de.tum.cit.aet.artemis.exercise.domain.Exercise;
import de.tum.cit.aet.artemis.exercise.domain.Submission;
import de.tum.cit.aet.artemis.exercise.domain.participation.Participation;
Expand Down Expand Up @@ -112,7 +113,7 @@ public class ResultService {

private final ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository;

private final StudentExamRepository studentExamRepository;
private final Optional<StudentExamApi> studentExamApi;

private final LongFeedbackTextRepository longFeedbackTextRepository;

Expand All @@ -136,7 +137,7 @@ public ResultService(UserRepository userRepository, ResultRepository resultRepos
ParticipantScoreRepository participantScoreRepository, AuthorizationCheckService authCheckService, ExerciseDateService exerciseDateService,
TemplateProgrammingExerciseParticipationRepository templateProgrammingExerciseParticipationRepository,
SolutionProgrammingExerciseParticipationRepository solutionProgrammingExerciseParticipationRepository,
ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, StudentExamRepository studentExamRepository,
ProgrammingExerciseStudentParticipationRepository programmingExerciseStudentParticipationRepository, Optional<StudentExamApi> studentExamApi,
BuildJobRepository buildJobRepository, BuildLogEntryService buildLogEntryService, StudentParticipationRepository studentParticipationRepository,
ProgrammingExerciseTaskService programmingExerciseTaskService, ProgrammingExerciseRepository programmingExerciseRepository) {
this.userRepository = userRepository;
Expand All @@ -154,7 +155,7 @@ public ResultService(UserRepository userRepository, ResultRepository resultRepos
this.templateProgrammingExerciseParticipationRepository = templateProgrammingExerciseParticipationRepository;
this.solutionProgrammingExerciseParticipationRepository = solutionProgrammingExerciseParticipationRepository;
this.programmingExerciseStudentParticipationRepository = programmingExerciseStudentParticipationRepository;
this.studentExamRepository = studentExamRepository;
this.studentExamApi = studentExamApi;
this.buildJobRepository = buildJobRepository;
this.buildLogEntryService = buildLogEntryService;
this.studentParticipationRepository = studentParticipationRepository;
Expand Down Expand Up @@ -356,11 +357,12 @@ private void filterSensitiveFeedbackInCourseExercise(Participation participation
}

private void filterSensitiveFeedbacksInExamExercise(Participation participation, Collection<Result> results, Exercise exercise) {
var api = studentExamApi.orElseThrow(() -> new ApiNotPresentException(StudentExamApi.class, PROFILE_CORE));
Exam exam = exercise.getExerciseGroup().getExam();
boolean shouldResultsBePublished = exam.resultsPublished();
if (!shouldResultsBePublished && exam.isTestExam() && participation instanceof StudentParticipation studentParticipation) {
var participant = studentParticipation.getParticipant();
var studentExamOptional = studentExamRepository.findByExamIdAndUserId(exam.getId(), participant.getId());
var studentExamOptional = api.findByExamIdAndUserId(exam.getId(), participant.getId());
if (studentExamOptional.isPresent()) {
shouldResultsBePublished = studentExamOptional.get().areResultsPublishedYet();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Objects;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -30,6 +31,7 @@
import de.tum.cit.aet.artemis.assessment.service.BonusService;
import de.tum.cit.aet.artemis.assessment.service.CourseScoreCalculationService;
import de.tum.cit.aet.artemis.core.domain.Course;
import de.tum.cit.aet.artemis.core.exception.ApiNotPresentException;
import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException;
import de.tum.cit.aet.artemis.core.exception.ConflictException;
import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException;
Expand All @@ -41,7 +43,7 @@
import de.tum.cit.aet.artemis.core.security.annotations.ManualConfig;
import de.tum.cit.aet.artemis.core.service.AuthorizationCheckService;
import de.tum.cit.aet.artemis.core.util.HeaderUtil;
import de.tum.cit.aet.artemis.exam.service.ExamAccessService;
import de.tum.cit.aet.artemis.exam.api.ExamAccessApi;

/**
* REST controller for managing bonus
Expand All @@ -66,19 +68,19 @@ public class BonusResource {

private final AuthorizationCheckService authCheckService;

private final ExamAccessService examAccessService;
private final Optional<ExamAccessApi> examAccessApi;

private final CourseScoreCalculationService courseScoreCalculationService;

private final CourseRepository courseRepository;

public BonusResource(BonusService bonusService, BonusRepository bonusRepository, GradingScaleRepository gradingScaleRepository, AuthorizationCheckService authCheckService,
ExamAccessService examAccessService, CourseScoreCalculationService courseScoreCalculationService, CourseRepository courseRepository) {
Optional<ExamAccessApi> examAccessApi, CourseScoreCalculationService courseScoreCalculationService, CourseRepository courseRepository) {
this.bonusService = bonusService;
this.bonusRepository = bonusRepository;
this.gradingScaleRepository = gradingScaleRepository;
this.authCheckService = authCheckService;
this.examAccessService = examAccessService;
this.examAccessApi = examAccessApi;
this.courseScoreCalculationService = courseScoreCalculationService;
this.courseRepository = courseRepository;
}
Expand All @@ -96,7 +98,8 @@ public BonusResource(BonusService bonusService, BonusRepository bonusRepository,
@EnforceAtLeastStudent
public ResponseEntity<Bonus> getBonusForExam(@PathVariable Long courseId, @PathVariable Long examId, @RequestParam(required = false) boolean includeSourceGradeSteps) {
log.debug("REST request to get bonus for exam: {}", examId);
examAccessService.checkCourseAndExamAccessForStudentElseThrow(courseId, examId);
var api = examAccessApi.orElseThrow(() -> new ApiNotPresentException(ExamAccessApi.class, PROFILE_CORE));
api.checkCourseAndExamAccessForStudentElseThrow(courseId, examId);

var bonus = bonusRepository.findAllByBonusToExamId(examId).stream().findAny().orElseThrow(() -> new EntityNotFoundException("BonusToGradingScale exam", examId));
bonus.setBonusStrategy(bonus.getBonusToGradingScale().getBonusStrategy());
Expand Down Expand Up @@ -139,7 +142,8 @@ public ResponseEntity<BonusExampleDTO> calculateGradeWithBonus(@PathVariable Lon
@RequestParam Double calculationSign, @RequestParam Double bonusToPoints, @RequestParam Long sourceGradingScaleId, @RequestParam Double sourcePoints) {

// TODO: Add auth and validation and authorize to USER role. Currently enabled only to ADMINs for testing.
examAccessService.checkCourseAndExamAccessForInstructorElseThrow(courseId, examId);
var api = examAccessApi.orElseThrow(() -> new ApiNotPresentException(ExamAccessApi.class, PROFILE_CORE));
api.checkCourseAndExamAccessForInstructorElseThrow(courseId, examId);

var bonusToGradingScale = gradingScaleRepository.findWithEagerBonusFromByExamId(examId).orElseThrow();
var sourceGradingScale = gradingScaleRepository.findById(sourceGradingScaleId).orElseThrow();
Expand Down Expand Up @@ -168,7 +172,8 @@ public ResponseEntity<Bonus> createBonusForExam(@PathVariable Long courseId, @Pa
throw new BadRequestAlertException("A new bonus cannot already have an ID", ENTITY_NAME, "idexists");
}

examAccessService.checkCourseAndExamAccessForInstructorElseThrow(courseId, examId);
var api = examAccessApi.orElseThrow(() -> new ApiNotPresentException(ExamAccessApi.class, PROFILE_CORE));
api.checkCourseAndExamAccessForInstructorElseThrow(courseId, examId);

GradingScale sourceGradingScaleFromDb = gradingScaleRepository.findById(bonus.getSourceGradingScale().getId()).orElseThrow();
bonus.setSourceGradingScale(sourceGradingScaleFromDb);
Expand Down Expand Up @@ -234,7 +239,8 @@ public ResponseEntity<Bonus> updateBonus(@PathVariable Long courseId, @PathVaria
throw new ConflictException("The updatedBonus id in the body and path do not match", ENTITY_NAME, "bonusIdMismatch");
}

examAccessService.checkCourseAndExamAccessForInstructorElseThrow(courseId, examId);
var api = examAccessApi.orElseThrow(() -> new ApiNotPresentException(ExamAccessApi.class, PROFILE_CORE));
api.checkCourseAndExamAccessForInstructorElseThrow(courseId, examId);

Bonus oldBonus = bonusRepository.findByIdElseThrow(updatedBonus.getId());
checkBonusAppliesToExam(oldBonus, examId);
Expand Down Expand Up @@ -283,7 +289,8 @@ private void checkBonusAppliesToExam(Bonus bonus, Long examId) {
@EnforceAtLeastInstructor
public ResponseEntity<Void> deleteBonus(@PathVariable Long courseId, @PathVariable Long examId, @PathVariable Long bonusId) {
log.debug("REST request to delete the bonus: {}", bonusId);
examAccessService.checkCourseAndExamAccessForInstructorElseThrow(courseId, examId);
var api = examAccessApi.orElseThrow(() -> new ApiNotPresentException(ExamAccessApi.class, PROFILE_CORE));
api.checkCourseAndExamAccessForInstructorElseThrow(courseId, examId);
Bonus bonus = bonusRepository.findByIdElseThrow(bonusId);
checkBonusAppliesToExam(bonus, examId);

Expand Down
Loading
Loading