use of de.tum.in.www1.artemis.domain.exam.StudentExam in project ArTEMiS by ls1intum.
the class ExamIntegrationTest method testGetStatsForExamAssessmentDashboard.
public void testGetStatsForExamAssessmentDashboard(int numberOfCorrectionRounds) throws Exception {
User examTutor1 = userRepo.findOneByLogin("tutor1").get();
User examTutor2 = userRepo.findOneByLogin("tutor2").get();
var examVisibleDate = ZonedDateTime.now().minusMinutes(5);
var examStartDate = ZonedDateTime.now().plusMinutes(5);
var examEndDate = ZonedDateTime.now().plusMinutes(20);
Course course = database.addEmptyCourse();
Exam exam = database.addExam(course, examVisibleDate, examStartDate, examEndDate);
exam.setNumberOfCorrectionRoundsInExam(numberOfCorrectionRounds);
exam = examRepository.save(exam);
exam = database.addExerciseGroupsAndExercisesToExam(exam, false);
var stats = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/stats-for-exam-assessment-dashboard", HttpStatus.OK, StatsForDashboardDTO.class);
assertThat(stats.getNumberOfSubmissions()).isInstanceOf(DueDateStat.class);
assertThat(stats.getTutorLeaderboardEntries()).isInstanceOf(List.class);
assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()).isInstanceOf(DueDateStat[].class);
assertThat(stats.getNumberOfAssessmentLocks()).isZero();
assertThat(stats.getNumberOfSubmissions().inTime()).isZero();
assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[0].inTime()).isZero();
assertThat(stats.getTotalNumberOfAssessmentLocks()).isZero();
var lockedSubmissions = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/lockedSubmissions", HttpStatus.OK, List.class);
assertThat(lockedSubmissions).isEmpty();
// register users. Instructors are ignored from scores as they are exclusive for test run exercises
Set<User> registeredStudents = users.stream().filter(user -> !user.getLogin().contains("instructor") && !user.getLogin().contains("admin")).collect(Collectors.toSet());
exam.setRegisteredUsers(registeredStudents);
exam.setNumberOfExercisesInExam(exam.getExerciseGroups().size());
exam.setRandomizeExerciseOrder(false);
exam = examRepository.save(exam);
exam = examRepository.findWithRegisteredUsersAndExerciseGroupsAndExercisesById(exam.getId()).get();
// generate individual student exams
List<StudentExam> studentExams = request.postListWithResponseBody("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/generate-student-exams", Optional.empty(), StudentExam.class, HttpStatus.OK);
Integer noGeneratedParticipations = request.postWithResponseBody("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/student-exams/start-exercises", Optional.empty(), Integer.class, HttpStatus.OK);
// set start and submitted date as results are created below
studentExams.forEach(studentExam -> {
studentExam.setStarted(true);
studentExam.setStartedDate(ZonedDateTime.now().minusMinutes(2));
studentExam.setSubmitted(true);
studentExam.setSubmissionDate(ZonedDateTime.now().minusMinutes(1));
});
studentExamRepository.saveAll(studentExams);
// Fetch the created participations and assign them to the exercises
int participationCounter = 0;
List<Exercise> exercisesInExam = exam.getExerciseGroups().stream().map(ExerciseGroup::getExercises).flatMap(Collection::stream).collect(Collectors.toList());
for (var exercise : exercisesInExam) {
List<StudentParticipation> participations = studentParticipationRepository.findByExerciseIdWithEagerLegalSubmissionsResult(exercise.getId());
exercise.setStudentParticipations(new HashSet<>(participations));
participationCounter += exercise.getStudentParticipations().size();
}
assertEquals(participationCounter, noGeneratedParticipations);
// Assign submissions to the participations
for (var exercise : exercisesInExam) {
for (var participation : exercise.getStudentParticipations()) {
Submission submission;
assertThat(participation.getSubmissions()).hasSize(1);
submission = participation.getSubmissions().iterator().next();
submission.submitted(true);
submission.setSubmissionDate(ZonedDateTime.now().minusMinutes(6));
submissionRepository.save(submission);
}
}
// check the stats again - check the count of submitted submissions
stats = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/stats-for-exam-assessment-dashboard", HttpStatus.OK, StatsForDashboardDTO.class);
assertThat(stats.getNumberOfAssessmentLocks()).isZero();
// 75 = (15 users * 5 exercises); quiz submissions are not counted
assertThat(stats.getNumberOfSubmissions().inTime()).isEqualTo(75L);
assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[0].inTime()).isZero();
assertThat(stats.getNumberOfComplaints()).isZero();
assertThat(stats.getTotalNumberOfAssessmentLocks()).isZero();
// Score used for all exercise results
Double resultScore = 75.0;
// Lock all submissions
for (var exercise : exercisesInExam) {
for (var participation : exercise.getStudentParticipations()) {
Submission submission;
assertThat(participation.getSubmissions()).hasSize(1);
submission = participation.getSubmissions().iterator().next();
// Create results
var result = new Result().score(resultScore).resultString("Good");
if (exercise instanceof QuizExercise) {
result.completionDate(ZonedDateTime.now().minusMinutes(4));
result.setRated(true);
}
result.setAssessmentType(AssessmentType.SEMI_AUTOMATIC);
result.setParticipation(participation);
result.setAssessor(examTutor1);
result = resultRepository.save(result);
result.setSubmission(submission);
submission.addResult(result);
submissionRepository.save(submission);
}
}
// check the stats again
database.changeUser("tutor1");
stats = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/stats-for-exam-assessment-dashboard", HttpStatus.OK, StatsForDashboardDTO.class);
assertThat(stats.getNumberOfAssessmentLocks()).isEqualTo(75L);
// 75 = (15 users * 5 exercises); quiz submissions are not counted
assertThat(stats.getNumberOfSubmissions().inTime()).isEqualTo(75L);
// the 15 quiz submissions are already assessed
assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[0].inTime()).isEqualTo(15L);
assertThat(stats.getNumberOfComplaints()).isZero();
assertThat(stats.getTotalNumberOfAssessmentLocks()).isEqualTo(75L);
// test the query needed for assessment information
database.changeUser("tutor2");
exam.getExerciseGroups().forEach(group -> {
var locks = group.getExercises().stream().map(exercise -> resultRepository.countNumberOfLockedAssessmentsByOtherTutorsForExamExerciseForCorrectionRounds(exercise, numberOfCorrectionRounds, examTutor2)[0].inTime()).reduce(Long::sum).get();
if (group.getExercises().stream().anyMatch(exercise -> !(exercise instanceof QuizExercise)))
assertThat(locks).isEqualTo(15L);
});
database.changeUser("instructor1");
lockedSubmissions = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/lockedSubmissions", HttpStatus.OK, List.class);
assertThat(lockedSubmissions).hasSize(75);
// Finish assessment of all submissions
for (var exercise : exercisesInExam) {
for (var participation : exercise.getStudentParticipations()) {
Submission submission;
assertThat(participation.getSubmissions()).hasSize(1);
submission = participation.getSubmissions().iterator().next();
var result = submission.getLatestResult().completionDate(ZonedDateTime.now().minusMinutes(5));
result.setRated(true);
resultRepository.save(result);
}
}
// check the stats again
stats = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/stats-for-exam-assessment-dashboard", HttpStatus.OK, StatsForDashboardDTO.class);
assertThat(stats.getNumberOfAssessmentLocks()).isZero();
// 75 = (15 users * 5 exercises); quiz submissions are not counted
assertThat(stats.getNumberOfSubmissions().inTime()).isEqualTo(75L);
// 75 + the 15 quiz submissions
assertThat(stats.getNumberOfAssessmentsOfCorrectionRounds()[0].inTime()).isEqualTo(90L);
assertThat(stats.getNumberOfComplaints()).isZero();
assertThat(stats.getTotalNumberOfAssessmentLocks()).isZero();
lockedSubmissions = request.get("/api/courses/" + course.getId() + "/exams/" + exam.getId() + "/lockedSubmissions", HttpStatus.OK, List.class);
assertThat(lockedSubmissions).isEmpty();
if (numberOfCorrectionRounds == 2) {
lockAndAssessForSecondCorrection(exam, course, exercisesInExam, numberOfCorrectionRounds);
}
}
use of de.tum.in.www1.artemis.domain.exam.StudentExam in project Artemis by ls1intum.
the class ExerciseDeletionService method delete.
/**
* Delete the exercise by id and all its participations.
*
* @param exerciseId the exercise to be deleted
* @param deleteStudentReposBuildPlans whether the student repos and build plans should be deleted (can be true for programming exercises and should be false for all other exercise types)
* @param deleteBaseReposBuildPlans whether the template and solution repos and build plans should be deleted (can be true for programming exercises and should be false for all other exercise types)
*/
// ok
@Transactional
public void delete(long exerciseId, boolean deleteStudentReposBuildPlans, boolean deleteBaseReposBuildPlans) {
// Delete has a transactional mechanism. Therefore, all lazy objects that are deleted below, should be fetched when needed.
final var exercise = exerciseRepository.findByIdElseThrow(exerciseId);
log.info("Checking if exercise {} is modeling exercise", exercise.getId());
if (exercise instanceof ModelingExercise) {
log.info("Deleting clusters, elements and cancel scheduled operations of exercise {}", exercise.getId());
modelingExerciseService.deleteClustersAndElements((ModelingExercise) exercise);
modelingExerciseService.cancelScheduledOperations(exerciseId);
}
participantScoreRepository.deleteAllByExerciseIdTransactional(exerciseId);
// delete all exercise units linking to the exercise
List<ExerciseUnit> exerciseUnits = this.exerciseUnitRepository.findByIdWithLearningGoalsBidirectional(exerciseId);
for (ExerciseUnit exerciseUnit : exerciseUnits) {
this.lectureUnitService.removeLectureUnit(exerciseUnit);
}
// delete all plagiarism results belonging to this exercise
plagiarismResultRepository.deletePlagiarismResultsByExerciseId(exerciseId);
// delete all participations belonging to this exercise, this will also delete submissions, results, feedback, complaints, etc.
participationService.deleteAllByExerciseId(exercise.getId(), deleteStudentReposBuildPlans, deleteStudentReposBuildPlans);
// clean up the many-to-many relationship to avoid problems when deleting the entities but not the relationship table
// to avoid a ConcurrentModificationException, we need to use a copy of the set
var exampleSubmissions = new HashSet<>(exercise.getExampleSubmissions());
for (ExampleSubmission exampleSubmission : exampleSubmissions) {
exampleSubmissionService.deleteById(exampleSubmission.getId());
}
// make sure tutor participations are deleted before the exercise is deleted
tutorParticipationRepository.deleteAllByAssessedExerciseId(exercise.getId());
if (exercise.isExamExercise()) {
Exam exam = examRepository.findOneWithEagerExercisesGroupsAndStudentExams(exercise.getExerciseGroup().getExam().getId());
for (StudentExam studentExam : exam.getStudentExams()) {
if (studentExam.getExercises().contains(exercise)) {
// remove exercise reference from student exam
List<Exercise> exerciseList = studentExam.getExercises();
exerciseList.remove(exercise);
studentExam.setExercises(exerciseList);
studentExamRepository.save(studentExam);
}
}
}
// Programming exercises have some special stuff that needs to be cleaned up (solution/template participation, build plans, etc.).
if (exercise instanceof ProgrammingExercise) {
// TODO: delete all schedules related to this programming exercise
programmingExerciseService.delete(exercise.getId(), deleteBaseReposBuildPlans);
} else {
// delete text assessment knowledge if exercise is of type TextExercise and if no other exercise uses same knowledge
if (exercise instanceof TextExercise) {
// explicitly load the text exercise as such so that the knowledge is eagerly loaded as well
TextExercise textExercise = textExerciseRepository.findByIdElseThrow(exercise.getId());
if (textExercise.getKnowledge() != null) {
textAssessmentKnowledgeService.deleteKnowledge(textExercise.getKnowledge().getId(), textExercise.getId());
}
} else // delete model assessment knowledge if exercise is of type ModelExercise and if no other exercise uses same knowledge
if (exercise instanceof ModelingExercise) {
// explicitly load the modeling exercise as such so that the knowledge is eagerly loaded as well
ModelingExercise modelingExercise = modelingExerciseRepository.findByIdElseThrow(exercise.getId());
if (modelingExercise.getKnowledge() != null) {
modelAssessmentKnowledgeService.deleteKnowledge(modelingExercise.getKnowledge().getId(), modelingExercise.getId());
}
}
exerciseRepository.delete(exercise);
}
}
use of de.tum.in.www1.artemis.domain.exam.StudentExam in project Artemis by ls1intum.
the class ExamRegistrationService method unregisterAllStudentFromExam.
/**
* Unregisters all students from the exam
*
* @param examId the exam for which a student should be unregistered
* @param deleteParticipationsAndSubmission whether the participations and submissions of the student should be deleted
*/
public void unregisterAllStudentFromExam(Long examId, boolean deleteParticipationsAndSubmission) {
var exam = examRepository.findWithRegisteredUsersById(examId).orElseThrow(() -> new EntityNotFoundException("Exam", examId));
// remove all registered students
List<Long> userIds = new ArrayList<>();
exam.getRegisteredUsers().forEach(user -> userIds.add(user.getId()));
List<User> registeredStudentsList = userRepository.findAllById(userIds);
registeredStudentsList.forEach(exam::removeRegisteredUser);
examRepository.save(exam);
// remove all students exams
Set<StudentExam> studentExams = studentExamRepository.findAllWithExercisesByExamId(examId);
studentExams.forEach(studentExam -> removeStudentExam(studentExam, deleteParticipationsAndSubmission));
User currentUser = userRepository.getUserWithGroupsAndAuthorities();
AuditEvent auditEvent = new AuditEvent(currentUser.getLogin(), Constants.REMOVE_ALL_USERS_FROM_EXAM, "exam=" + exam.getTitle());
auditEventRepository.add(auditEvent);
log.info("User {} has removed all users from the exam {} with id {}. This also deleted potentially existing student exams with all its participations and submissions.", currentUser.getLogin(), exam.getTitle(), exam.getId());
}
use of de.tum.in.www1.artemis.domain.exam.StudentExam in project Artemis by ls1intum.
the class ExamService method calculateExamScores.
/**
* Puts students, result and exerciseGroups together for ExamScoresDTO
*
* @param examId the id of the exam
* @return return ExamScoresDTO with students, scores and exerciseGroups for exam
*/
public ExamScoresDTO calculateExamScores(Long examId) {
Exam exam = examRepository.findWithExerciseGroupsAndExercisesById(examId).orElseThrow(() -> new EntityNotFoundException("Exam", examId));
// without test run participations
List<StudentParticipation> studentParticipations = studentParticipationRepository.findByExamIdWithSubmissionRelevantResult(examId);
// Adding exam information to DTO
ExamScoresDTO scores = new ExamScoresDTO(exam.getId(), exam.getTitle(), exam.getMaxPoints());
// setting multiplicity of correction rounds
scores.hasSecondCorrectionAndStarted = false;
// Counts how many participants each exercise has
Map<Long, Long> exerciseIdToNumberParticipations = studentParticipations.stream().collect(Collectors.groupingBy(studentParticipation -> studentParticipation.getExercise().getId(), Collectors.counting()));
// Adding exercise group information to DTO
for (ExerciseGroup exerciseGroup : exam.getExerciseGroups()) {
// Find the maximum points for this exercise group
OptionalDouble optionalMaxPointsGroup = exerciseGroup.getExercises().stream().mapToDouble(Exercise::getMaxPoints).max();
Double maxPointsGroup = optionalMaxPointsGroup.orElse(0);
// Counter for exerciseGroup participations. Is calculated by summing up the number of exercise participations
long numberOfExerciseGroupParticipants = 0;
// Add information about exercise groups and exercises
var exerciseGroupDTO = new ExamScoresDTO.ExerciseGroup(exerciseGroup.getId(), exerciseGroup.getTitle(), maxPointsGroup);
for (Exercise exercise : exerciseGroup.getExercises()) {
Long participantsForExercise = exerciseIdToNumberParticipations.get(exercise.getId());
// If no participation exists for an exercise then no entry exists in the map
if (participantsForExercise == null) {
participantsForExercise = 0L;
}
numberOfExerciseGroupParticipants += participantsForExercise;
exerciseGroupDTO.containedExercises.add(new ExamScoresDTO.ExerciseGroup.ExerciseInfo(exercise.getId(), exercise.getTitle(), exercise.getMaxPoints(), participantsForExercise, exercise.getClass().getSimpleName()));
}
exerciseGroupDTO.numberOfParticipants = numberOfExerciseGroupParticipants;
scores.exerciseGroups.add(exerciseGroupDTO);
}
// Adding registered student information to DTO
// fetched without test runs
Set<StudentExam> studentExams = studentExamRepository.findByExamId(examId);
ObjectMapper objectMapper = new ObjectMapper();
for (StudentExam studentExam : studentExams) {
User user = studentExam.getUser();
var studentResult = new ExamScoresDTO.StudentResult(user.getId(), user.getName(), user.getEmail(), user.getLogin(), user.getRegistrationNumber(), studentExam.isSubmitted());
// Adding student results information to DTO
List<StudentParticipation> participationsOfStudent = studentParticipations.stream().filter(studentParticipation -> studentParticipation.getStudent().get().getId().equals(studentResult.userId)).toList();
studentResult.overallPointsAchieved = 0.0;
studentResult.overallPointsAchievedInFirstCorrection = 0.0;
for (StudentParticipation studentParticipation : participationsOfStudent) {
Exercise exercise = studentParticipation.getExercise();
// Relevant Result is already calculated
if (studentParticipation.getResults() != null && !studentParticipation.getResults().isEmpty()) {
Result relevantResult = studentParticipation.getResults().iterator().next();
// Note: It is important that we round on the individual exercise level first and then sum up.
// This is necessary so that the student arrives at the same overall result when doing his own recalculation.
// Let's assume that the student achieved 1.05 points in each of 5 exercises.
// In the client, these are now displayed rounded as 1.1 points.
// If the student adds up the displayed points, he gets a total of 5.5 points.
// In order to get the same total result as the student, we have to round before summing.
double achievedPoints = roundScoreSpecifiedByCourseSettings(relevantResult.getScore() / 100.0 * exercise.getMaxPoints(), exam.getCourse());
// points earned in NOT_INCLUDED exercises do not count towards the students result in the exam
if (!exercise.getIncludedInOverallScore().equals(IncludedInOverallScore.NOT_INCLUDED)) {
studentResult.overallPointsAchieved += achievedPoints;
}
// collect points of first correction, if a second correction exists
if (exam.getNumberOfCorrectionRoundsInExam() == 2 && !exercise.getIncludedInOverallScore().equals(IncludedInOverallScore.NOT_INCLUDED)) {
Optional<Submission> latestSubmission = studentParticipation.findLatestSubmission();
if (latestSubmission.isPresent()) {
Submission submission = latestSubmission.get();
// Check if second correction already started
if (submission.getManualResults().size() > 1) {
if (!scores.hasSecondCorrectionAndStarted) {
scores.hasSecondCorrectionAndStarted = true;
}
Result firstManualResult = submission.getFirstManualResult();
double achievedPointsInFirstCorrection = 0.0;
if (firstManualResult != null) {
Double resultScore = firstManualResult.getScore();
achievedPointsInFirstCorrection = resultScore != null ? roundScoreSpecifiedByCourseSettings(resultScore / 100.0 * exercise.getMaxPoints(), exam.getCourse()) : 0.0;
}
studentResult.overallPointsAchievedInFirstCorrection += achievedPointsInFirstCorrection;
}
}
}
// Check whether the student attempted to solve the exercise
boolean hasNonEmptySubmission = hasNonEmptySubmission(studentParticipation.getSubmissions(), exercise, objectMapper);
studentResult.exerciseGroupIdToExerciseResult.put(exercise.getExerciseGroup().getId(), new ExamScoresDTO.ExerciseResult(exercise.getId(), exercise.getTitle(), exercise.getMaxPoints(), relevantResult.getScore(), achievedPoints, hasNonEmptySubmission));
}
}
if (scores.maxPoints != null) {
studentResult.overallScoreAchieved = (studentResult.overallPointsAchieved / scores.maxPoints) * 100.0;
var overallScoreAchievedInFirstCorrection = (studentResult.overallPointsAchievedInFirstCorrection / scores.maxPoints) * 100.0;
// Sets grading scale related properties for exam scores
Optional<GradingScale> gradingScale = gradingScaleRepository.findByExamId(examId);
if (gradingScale.isPresent()) {
// Calculate current student grade
GradeStep studentGrade = gradingScaleRepository.matchPercentageToGradeStep(studentResult.overallScoreAchieved, gradingScale.get().getId());
GradeStep studentGradeInFirstCorrection = gradingScaleRepository.matchPercentageToGradeStep(overallScoreAchievedInFirstCorrection, gradingScale.get().getId());
studentResult.overallGrade = studentGrade.getGradeName();
studentResult.overallGradeInFirstCorrection = studentGradeInFirstCorrection.getGradeName();
studentResult.hasPassed = studentGrade.getIsPassingGrade();
}
}
scores.studentResults.add(studentResult);
}
// Updating exam information in DTO
double sumOverallPoints = scores.studentResults.stream().mapToDouble(studentResult -> studentResult.overallPointsAchieved).sum();
int numberOfStudentResults = scores.studentResults.size();
if (numberOfStudentResults != 0) {
scores.averagePointsAchieved = sumOverallPoints / numberOfStudentResults;
}
return scores;
}
use of de.tum.in.www1.artemis.domain.exam.StudentExam in project Artemis by ls1intum.
the class ExamResource method generateMissingStudentExams.
/**
* POST /courses/:courseId/exams/:examId/generate-missing-student-exams:
* Generates exams for students, who don't have an individual exam yet.
* They are created randomly based on the exam configuration and the exercise groups.
*
* @param courseId the id of the course
* @param examId the id of the exam
* @return the list of student exams with their corresponding users
*/
@PostMapping(value = "/courses/{courseId}/exams/{examId}/generate-missing-student-exams")
@PreAuthorize("hasRole('INSTRUCTOR')")
public ResponseEntity<List<StudentExam>> generateMissingStudentExams(@PathVariable Long courseId, @PathVariable Long examId) {
log.info("REST request to generate missing student exams for exam {}", examId);
final Exam exam = examRepository.findByIdWithRegisteredUsersExerciseGroupsAndExercisesElseThrow(examId);
examAccessService.checkCourseAndExamAccessForInstructorElseThrow(courseId, examId);
// Validate settings of the exam
examService.validateForStudentExamGeneration(exam);
List<StudentExam> studentExams = studentExamRepository.generateMissingStudentExams(exam);
// we need to break a cycle for the serialization
for (StudentExam studentExam : studentExams) {
studentExam.getExam().setRegisteredUsers(null);
studentExam.getExam().setExerciseGroups(null);
studentExam.getExam().setStudentExams(null);
}
// Reschedule after creation (possible longer working time)
examMonitoringScheduleService.scheduleExamActivitySave(examId);
log.info("Generated {} missing student exams for exam {}", studentExams.size(), examId);
return ResponseEntity.ok().body(studentExams);
}
Aggregations