Search in sources :

Example 1 with QuizBatch

use of de.tum.in.www1.artemis.domain.quiz.QuizBatch in project ArTEMiS by ls1intum.

the class QuizScheduleService method processCachedQuizSubmissions.

/**
 * // @formatter:off
 * 1. Check cached submissions for new submissions with “isSubmitted() == true”
 *      a. Process each Submission (set submissionType to “SubmissionType.MANUAL”) and create Participation and Result and save them to Database (DB WRITE)
 *      b. Remove processed Submissions from SubmissionHashMap and write Participation with Result into ParticipationHashMap and write Result into ResultHashMap
 * 2. If Quiz has ended:
 *      a. Process all cached Submissions that belong to this quiz i. set “isSubmitted” to “true” and submissionType to “SubmissionType.TIMEOUT”
 *          ii. Create Participation and Result and save to Database (DB WRITE)
 *          iii. Remove processed Submissions from cache and write the Participations with Result and the Results into the cache
 *      b. Send out cached Participations (including QuizExercise and Result) from to each participant and remove them from the cache (WEBSOCKET SEND)
 * 3. Update Statistics with Results from ResultHashMap (DB READ and DB WRITE) and remove from cache
 * 4. Send out new Statistics to instructors (WEBSOCKET SEND)
 */
public void processCachedQuizSubmissions() {
    log.debug("Process cached quiz submissions");
    // global try-catch for error logging
    try {
        for (QuizExerciseCache cachedQuiz : quizCache.getAllQuizExerciseCaches()) {
            // this way near cache is used (values will deserialize new objects)
            Long quizExerciseId = cachedQuiz.getExerciseId();
            // Get fresh QuizExercise from DB
            QuizExercise quizExercise = quizExerciseRepository.findOne(quizExerciseId);
            // check if quiz has been deleted
            if (quizExercise == null) {
                log.debug("Remove quiz {} from resultHashMap", quizExerciseId);
                quizCache.removeAndClear(quizExerciseId);
                continue;
            }
            // Update cached exercise object (use the expensive operation upfront)
            quizExercise = quizExerciseRepository.findByIdWithQuestionsAndStatisticsElseThrow(quizExerciseId);
            Map<Long, QuizBatch> batchCache = quizExercise.getQuizBatches().stream().collect(Collectors.toUnmodifiableMap(QuizBatch::getId, batch -> batch));
            // this is required to ensure that students cannot gain extra attempts this way
            for (var batch : cachedQuiz.getBatches().entrySet()) {
                if (batchCache.get(batch.getValue()).isEnded()) {
                    cachedQuiz.getSubmissions().putIfAbsent(batch.getKey(), new QuizSubmission());
                }
            }
            // (Boolean wrapper is safe to auto-unbox here)
            boolean hasEnded = quizExercise.isQuizEnded();
            // Note that those might not be true later on due to concurrency and a distributed system,
            // do not rely on that for actions upon the whole set, such as clear()
            boolean hasNewSubmissions = !cachedQuiz.getSubmissions().isEmpty();
            boolean hasNewParticipations = !cachedQuiz.getParticipations().isEmpty();
            boolean hasNewResults = !cachedQuiz.getResults().isEmpty();
            // Skip quizzes with no cached changes
            if (!hasNewSubmissions && !hasNewParticipations && !hasNewResults) {
                // Remove quiz if it has ended
                if (hasEnded) {
                    removeCachedQuiz(cachedQuiz);
                }
                continue;
            }
            // Save cached Submissions (this will also generate results and participations and place them in the cache)
            long start = System.nanoTime();
            if (hasNewSubmissions) {
                // Create Participations and Results if the submission was submitted or if the quiz has ended and save them to Database (DB Write)
                Map<String, QuizSubmission> submissions = cachedQuiz.getSubmissions();
                Map<String, Long> batches = cachedQuiz.getBatches();
                // This call will remove the processed Submission map entries itself
                int numberOfSubmittedSubmissions = saveQuizSubmissionWithParticipationAndResultToDatabase(quizExercise, submissions, batches, batchCache);
                // .. and likely generate new participations and results
                if (numberOfSubmittedSubmissions > 0) {
                    // .. so we set the boolean variables here again if some were submitted
                    hasNewParticipations = true;
                    hasNewResults = true;
                    log.info("Saved {} submissions to database in {} in quiz {}", numberOfSubmittedSubmissions, formatDurationFrom(start), quizExercise.getTitle());
                }
            }
            // Send out Participations from ParticipationHashMap to each user if the quiz has ended
            start = System.nanoTime();
            if (hasNewParticipations && hasEnded) {
                // Send the participation with containing result and quiz back to the users via websocket and remove the participation from the ParticipationHashMap
                Collection<Entry<String, StudentParticipation>> finishedParticipations = cachedQuiz.getParticipations().entrySet();
                // TODO maybe find a better way to optimize the performance (use an executor service with e.g. X parallel threads)
                finishedParticipations.parallelStream().forEach(entry -> {
                    StudentParticipation participation = entry.getValue();
                    if (participation.getParticipant() == null || participation.getParticipantIdentifier() == null) {
                        log.error("Participation is missing student (or student is missing username): {}", participation);
                    } else {
                        sendQuizResultToUser(quizExerciseId, participation);
                        cachedQuiz.getParticipations().remove(entry.getKey());
                    }
                });
                if (!finishedParticipations.isEmpty()) {
                    log.info("Sent out {} participations in {} for quiz {}", finishedParticipations.size(), formatDurationFrom(start), quizExercise.getTitle());
                }
            }
            // Update Statistics with Results (DB Read and DB Write) and remove the results from the cache
            start = System.nanoTime();
            if (hasNewResults) {
                // Fetch a new quiz exercise here including deeper attribute paths (this is relatively expensive, so we only do that if necessary)
                try {
                    // Get a Set because QuizStatisticService needs one (currently)
                    Set<Result> newResultsForQuiz = Set.copyOf(cachedQuiz.getResults().values());
                    // Update the statistics
                    quizStatisticService.updateStatistics(newResultsForQuiz, quizExercise);
                    log.info("Updated statistics with {} new results in {} for quiz {}", newResultsForQuiz.size(), formatDurationFrom(start), quizExercise.getTitle());
                    // Remove only processed results
                    for (Result result : newResultsForQuiz) {
                        cachedQuiz.getResults().remove(result.getId());
                    }
                } catch (Exception e) {
                    log.error("Exception in StatisticService.updateStatistics(): {}", e.getMessage(), e);
                }
            }
        }
    } catch (Exception e) {
        log.error("Exception in Quiz Schedule: {}", e.getMessage(), e);
    }
}
Also used : QuizStatisticService(de.tum.in.www1.artemis.service.QuizStatisticService) java.util(java.util) SecurityUtils(de.tum.in.www1.artemis.security.SecurityUtils) de.tum.in.www1.artemis.repository(de.tum.in.www1.artemis.repository) ApplicationReadyEvent(org.springframework.boot.context.event.ApplicationReadyEvent) ZonedDateTime(java.time.ZonedDateTime) LoggerFactory(org.slf4j.LoggerFactory) QuizBatch(de.tum.in.www1.artemis.domain.quiz.QuizBatch) QuizMessagingService(de.tum.in.www1.artemis.service.QuizMessagingService) TimeLogUtil.formatDurationFrom(de.tum.in.www1.artemis.service.util.TimeLogUtil.formatDurationFrom) Service(org.springframework.stereotype.Service) Duration(java.time.Duration) SubmittedAnswer(de.tum.in.www1.artemis.domain.quiz.SubmittedAnswer) Nullable(javax.annotation.Nullable) Config(com.hazelcast.config.Config) HazelcastInstance(com.hazelcast.core.HazelcastInstance) Logger(org.slf4j.Logger) QuizExercise(de.tum.in.www1.artemis.domain.quiz.QuizExercise) ConstraintViolationException(org.hibernate.exception.ConstraintViolationException) com.hazelcast.scheduledexecutor(com.hazelcast.scheduledexecutor) IAtomicReference(com.hazelcast.cp.IAtomicReference) QuizSubmission(de.tum.in.www1.artemis.domain.quiz.QuizSubmission) EventListener(org.springframework.context.event.EventListener) SubmissionType(de.tum.in.www1.artemis.domain.enumeration.SubmissionType) NotNull(javax.validation.constraints.NotNull) AssessmentType(de.tum.in.www1.artemis.domain.enumeration.AssessmentType) InitializationState(de.tum.in.www1.artemis.domain.enumeration.InitializationState) Collectors(java.util.stream.Collectors) Constants(de.tum.in.www1.artemis.config.Constants) TimeUnit(java.util.concurrent.TimeUnit) Result(de.tum.in.www1.artemis.domain.Result) User(de.tum.in.www1.artemis.domain.User) QuizMode(de.tum.in.www1.artemis.domain.enumeration.QuizMode) Entry(java.util.Map.Entry) StudentParticipation(de.tum.in.www1.artemis.domain.participation.StudentParticipation) SimpMessageSendingOperations(org.springframework.messaging.simp.SimpMessageSendingOperations) StudentParticipation(de.tum.in.www1.artemis.domain.participation.StudentParticipation) ConstraintViolationException(org.hibernate.exception.ConstraintViolationException) QuizExercise(de.tum.in.www1.artemis.domain.quiz.QuizExercise) Result(de.tum.in.www1.artemis.domain.Result) QuizSubmission(de.tum.in.www1.artemis.domain.quiz.QuizSubmission) Entry(java.util.Map.Entry) QuizBatch(de.tum.in.www1.artemis.domain.quiz.QuizBatch)

Example 2 with QuizBatch

use of de.tum.in.www1.artemis.domain.quiz.QuizBatch in project Artemis by ls1intum.

the class QuizScheduleService method processCachedQuizSubmissions.

/**
 * // @formatter:off
 * 1. Check cached submissions for new submissions with “isSubmitted() == true”
 *      a. Process each Submission (set submissionType to “SubmissionType.MANUAL”) and create Participation and Result and save them to Database (DB WRITE)
 *      b. Remove processed Submissions from SubmissionHashMap and write Participation with Result into ParticipationHashMap and write Result into ResultHashMap
 * 2. If Quiz has ended:
 *      a. Process all cached Submissions that belong to this quiz i. set “isSubmitted” to “true” and submissionType to “SubmissionType.TIMEOUT”
 *          ii. Create Participation and Result and save to Database (DB WRITE)
 *          iii. Remove processed Submissions from cache and write the Participations with Result and the Results into the cache
 *      b. Send out cached Participations (including QuizExercise and Result) from to each participant and remove them from the cache (WEBSOCKET SEND)
 * 3. Update Statistics with Results from ResultHashMap (DB READ and DB WRITE) and remove from cache
 * 4. Send out new Statistics to instructors (WEBSOCKET SEND)
 */
public void processCachedQuizSubmissions() {
    log.debug("Process cached quiz submissions");
    // global try-catch for error logging
    try {
        for (Cache cache : quizCache.getAllCaches()) {
            QuizExerciseCache cachedQuiz = (QuizExerciseCache) cache;
            // this way near cache is used (values will deserialize new objects)
            Long quizExerciseId = cachedQuiz.getExerciseId();
            // Get fresh QuizExercise from DB
            QuizExercise quizExercise = quizExerciseRepository.findOne(quizExerciseId);
            // check if quiz has been deleted
            if (quizExercise == null) {
                log.debug("Remove quiz {} from resultHashMap", quizExerciseId);
                quizCache.removeAndClear(quizExerciseId);
                continue;
            }
            // Update cached exercise object (use the expensive operation upfront)
            quizExercise = quizExerciseRepository.findByIdWithQuestionsAndStatisticsElseThrow(quizExerciseId);
            Map<Long, QuizBatch> batchCache = quizExercise.getQuizBatches().stream().collect(Collectors.toUnmodifiableMap(QuizBatch::getId, batch -> batch));
            // this is required to ensure that students cannot gain extra attempts this way
            for (var batch : cachedQuiz.getBatches().entrySet()) {
                if (batchCache.get(batch.getValue()).isEnded()) {
                    cachedQuiz.getSubmissions().putIfAbsent(batch.getKey(), new QuizSubmission());
                }
            }
            // (Boolean wrapper is safe to auto-unbox here)
            boolean hasEnded = quizExercise.isQuizEnded();
            // Note that those might not be true later on due to concurrency and a distributed system,
            // do not rely on that for actions upon the whole set, such as clear()
            boolean hasNewSubmissions = !cachedQuiz.getSubmissions().isEmpty();
            boolean hasNewParticipations = !cachedQuiz.getParticipations().isEmpty();
            boolean hasNewResults = !cachedQuiz.getResults().isEmpty();
            // Skip quizzes with no cached changes
            if (!hasNewSubmissions && !hasNewParticipations && !hasNewResults) {
                // Remove quiz if it has ended
                if (hasEnded) {
                    removeCachedQuiz(cachedQuiz);
                }
                continue;
            }
            // Save cached Submissions (this will also generate results and participations and place them in the cache)
            long start = System.nanoTime();
            if (hasNewSubmissions) {
                // Create Participations and Results if the submission was submitted or if the quiz has ended and save them to Database (DB Write)
                Map<String, QuizSubmission> submissions = cachedQuiz.getSubmissions();
                Map<String, Long> batches = cachedQuiz.getBatches();
                // This call will remove the processed Submission map entries itself
                int numberOfSubmittedSubmissions = saveQuizSubmissionWithParticipationAndResultToDatabase(quizExercise, submissions, batches, batchCache);
                // .. and likely generate new participations and results
                if (numberOfSubmittedSubmissions > 0) {
                    // .. so we set the boolean variables here again if some were submitted
                    hasNewParticipations = true;
                    hasNewResults = true;
                    log.info("Saved {} submissions to database in {} in quiz {}", numberOfSubmittedSubmissions, formatDurationFrom(start), quizExercise.getTitle());
                }
            }
            // Send out Participations from ParticipationHashMap to each user if the quiz has ended
            start = System.nanoTime();
            if (hasNewParticipations && hasEnded) {
                // Send the participation with containing result and quiz back to the users via websocket and remove the participation from the ParticipationHashMap
                Collection<Entry<String, StudentParticipation>> finishedParticipations = cachedQuiz.getParticipations().entrySet();
                // TODO maybe find a better way to optimize the performance (use an executor service with e.g. X parallel threads)
                finishedParticipations.parallelStream().forEach(entry -> {
                    StudentParticipation participation = entry.getValue();
                    if (participation.getParticipant() == null || participation.getParticipantIdentifier() == null) {
                        log.error("Participation is missing student (or student is missing username): {}", participation);
                    } else {
                        sendQuizResultToUser(quizExerciseId, participation);
                        cachedQuiz.getParticipations().remove(entry.getKey());
                    }
                });
                if (!finishedParticipations.isEmpty()) {
                    log.info("Sent out {} participations in {} for quiz {}", finishedParticipations.size(), formatDurationFrom(start), quizExercise.getTitle());
                }
            }
            // Update Statistics with Results (DB Read and DB Write) and remove the results from the cache
            start = System.nanoTime();
            if (hasNewResults) {
                // Fetch a new quiz exercise here including deeper attribute paths (this is relatively expensive, so we only do that if necessary)
                try {
                    // Get a Set because QuizStatisticService needs one (currently)
                    Set<Result> newResultsForQuiz = Set.copyOf(cachedQuiz.getResults().values());
                    // Update the statistics
                    quizStatisticService.updateStatistics(newResultsForQuiz, quizExercise);
                    log.info("Updated statistics with {} new results in {} for quiz {}", newResultsForQuiz.size(), formatDurationFrom(start), quizExercise.getTitle());
                    // Remove only processed results
                    for (Result result : newResultsForQuiz) {
                        cachedQuiz.getResults().remove(result.getId());
                    }
                } catch (Exception e) {
                    log.error("Exception in StatisticService.updateStatistics(): {}", e.getMessage(), e);
                }
            }
        }
    } catch (Exception e) {
        log.error("Exception in Quiz Schedule: {}", e.getMessage(), e);
    }
}
Also used : QuizStatisticService(de.tum.in.www1.artemis.service.QuizStatisticService) java.util(java.util) SecurityUtils(de.tum.in.www1.artemis.security.SecurityUtils) de.tum.in.www1.artemis.repository(de.tum.in.www1.artemis.repository) ApplicationReadyEvent(org.springframework.boot.context.event.ApplicationReadyEvent) ZonedDateTime(java.time.ZonedDateTime) LoggerFactory(org.slf4j.LoggerFactory) QuizBatch(de.tum.in.www1.artemis.domain.quiz.QuizBatch) QuizMessagingService(de.tum.in.www1.artemis.service.QuizMessagingService) TimeLogUtil.formatDurationFrom(de.tum.in.www1.artemis.service.util.TimeLogUtil.formatDurationFrom) Service(org.springframework.stereotype.Service) Duration(java.time.Duration) SubmittedAnswer(de.tum.in.www1.artemis.domain.quiz.SubmittedAnswer) Cache(de.tum.in.www1.artemis.service.scheduled.cache.Cache) Nullable(javax.annotation.Nullable) Config(com.hazelcast.config.Config) HazelcastInstance(com.hazelcast.core.HazelcastInstance) Logger(org.slf4j.Logger) QuizExercise(de.tum.in.www1.artemis.domain.quiz.QuizExercise) ConstraintViolationException(org.hibernate.exception.ConstraintViolationException) com.hazelcast.scheduledexecutor(com.hazelcast.scheduledexecutor) IAtomicReference(com.hazelcast.cp.IAtomicReference) QuizSubmission(de.tum.in.www1.artemis.domain.quiz.QuizSubmission) EventListener(org.springframework.context.event.EventListener) SubmissionType(de.tum.in.www1.artemis.domain.enumeration.SubmissionType) NotNull(javax.validation.constraints.NotNull) AssessmentType(de.tum.in.www1.artemis.domain.enumeration.AssessmentType) InitializationState(de.tum.in.www1.artemis.domain.enumeration.InitializationState) Collectors(java.util.stream.Collectors) Constants(de.tum.in.www1.artemis.config.Constants) TimeUnit(java.util.concurrent.TimeUnit) Result(de.tum.in.www1.artemis.domain.Result) User(de.tum.in.www1.artemis.domain.User) QuizMode(de.tum.in.www1.artemis.domain.enumeration.QuizMode) Entry(java.util.Map.Entry) StudentParticipation(de.tum.in.www1.artemis.domain.participation.StudentParticipation) SimpMessageSendingOperations(org.springframework.messaging.simp.SimpMessageSendingOperations) StudentParticipation(de.tum.in.www1.artemis.domain.participation.StudentParticipation) ConstraintViolationException(org.hibernate.exception.ConstraintViolationException) QuizExercise(de.tum.in.www1.artemis.domain.quiz.QuizExercise) Result(de.tum.in.www1.artemis.domain.Result) QuizSubmission(de.tum.in.www1.artemis.domain.quiz.QuizSubmission) Entry(java.util.Map.Entry) QuizBatch(de.tum.in.www1.artemis.domain.quiz.QuizBatch) Cache(de.tum.in.www1.artemis.service.scheduled.cache.Cache)

Example 3 with QuizBatch

use of de.tum.in.www1.artemis.domain.quiz.QuizBatch in project Artemis by ls1intum.

the class QuizScheduleService method saveQuizSubmissionWithParticipationAndResultToDatabase.

/**
 * check if the user submitted the submission or if the quiz has ended: if true: -> Create Participation and Result and save to Database (DB Write) Remove processed Submissions
 * from SubmissionHashMap and write Participations with Result into ParticipationHashMap and Results into ResultHashMap
 *
 * @param quizExercise      the quiz which should be checked
 * @param userSubmissionMap a Map with all submissions for the given quizExercise mapped by the username
 * @param userBatchMap      a Map of the username to quiz batch id for the given quizExercise
 * @param batchCache        a Map of all the batches for the given quizExercise
 * @return                  the number of processed submissions (submit or timeout)
 */
private int saveQuizSubmissionWithParticipationAndResultToDatabase(@NotNull QuizExercise quizExercise, Map<String, QuizSubmission> userSubmissionMap, Map<String, Long> userBatchMap, Map<Long, QuizBatch> batchCache) {
    int count = 0;
    for (String username : userSubmissionMap.keySet()) {
        try {
            // first case: the user submitted the quizSubmission
            QuizSubmission quizSubmission = userSubmissionMap.get(username);
            QuizBatch quizBatch = batchCache.get(userBatchMap.getOrDefault(username, 0L));
            if (quizSubmission.isSubmitted()) {
                if (quizSubmission.getType() == null) {
                    quizSubmission.setType(SubmissionType.MANUAL);
                }
            } else // second case: the quiz or batch has ended
            if (quizExercise.isQuizEnded() || quizBatch != null && quizBatch.isEnded()) {
                quizSubmission.setSubmitted(true);
                quizSubmission.setType(SubmissionType.TIMEOUT);
                quizSubmission.setSubmissionDate(ZonedDateTime.now());
            } else {
                // the quiz is running and the submission was not yet submitted.
                continue;
            }
            if (quizBatch != null) {
                // record which batch the submission belongs to
                quizSubmission.setQuizBatch(quizBatch.getId());
            }
            count++;
            // Create Participation and Result and save to Database (DB Write)
            // Remove processed Submissions from SubmissionHashMap and write Participations with Result into ParticipationHashMap and Results into ResultHashMap
            StudentParticipation participation = new StudentParticipation();
            // TODO: when this is set earlier for the individual quiz start of a student, we don't need to set this here anymore
            participation.setInitializationDate(quizSubmission.getSubmissionDate());
            Optional<User> user = userRepository.findOneByLogin(username);
            user.ifPresent(participation::setParticipant);
            // add the quizExercise to the participation
            participation.setExercise(quizExercise);
            participation.setInitializationState(InitializationState.FINISHED);
            // create new result
            Result result = new Result().participation(participation);
            result.setRated(true);
            result.setAssessmentType(AssessmentType.AUTOMATIC);
            result.setCompletionDate(quizSubmission.getSubmissionDate());
            result.setSubmission(quizSubmission);
            // calculate scores and update result and submission accordingly
            quizSubmission.calculateAndUpdateScores(quizExercise);
            result.evaluateQuizSubmission();
            // add result to participation
            participation.addResult(result);
            // add submission to participation
            participation.setSubmissions(Set.of(quizSubmission));
            // NOTE: we save (1) participation and (2) submission (in this particular order) here individually so that one exception (e.g. duplicated key) cannot
            // destroy multiple student answers
            participation = studentParticipationRepository.save(participation);
            quizSubmission.addResult(result);
            quizSubmission.setParticipation(participation);
            // this automatically saves the results due to CascadeType.ALL
            quizSubmission = quizSubmissionRepository.save(quizSubmission);
            log.info("Successfully saved submission in quiz " + quizExercise.getTitle() + " for user " + username);
            // reconnect entities after save
            participation.setSubmissions(Set.of(quizSubmission));
            participation.setResults(Set.of(result));
            result.setSubmission(quizSubmission);
            result.setParticipation(participation);
            // no point in keeping the participation around for non-synchronized modes where the due date may only be in a week
            if (quizExercise.getQuizMode() == QuizMode.SYNCHRONIZED) {
                // add the participation to the participationHashMap for the send out at the end of the quiz
                addParticipation(quizExercise.getId(), participation);
            }
            // remove the submission only after the participation has been added to the participation hashmap to avoid duplicated key exceptions for multiple participations for
            // the same user
            userSubmissionMap.remove(username);
            // clean up the batch association
            userBatchMap.remove(username);
            // add the result of the participation resultHashMap for the statistic-Update
            addResultForStatisticUpdate(quizExercise.getId(), result);
        } catch (ConstraintViolationException constraintViolationException) {
            log.error("ConstraintViolationException in saveQuizSubmissionWithParticipationAndResultToDatabase() for user {} in quiz {}: {}", username, quizExercise.getId(), constraintViolationException.getMessage(), constraintViolationException);
            // We got a ConstraintViolationException -> The "User-Quiz" pair is already saved in the database, but for some reason was not removed from the maps
            // We remove it from the maps now to prevent this error from occurring again
            // We do NOT add it to the participation map, as this should have been done already earlier (when the entry was added to the database)
            userSubmissionMap.remove(username);
            // clean up the batch association
            userBatchMap.remove(username);
        } catch (Exception e) {
            log.error("Exception in saveQuizSubmissionWithParticipationAndResultToDatabase() for user {} in quiz {}: {}", username, quizExercise.getId(), e.getMessage(), e);
        }
    }
    return count;
}
Also used : QuizSubmission(de.tum.in.www1.artemis.domain.quiz.QuizSubmission) User(de.tum.in.www1.artemis.domain.User) ConstraintViolationException(org.hibernate.exception.ConstraintViolationException) QuizBatch(de.tum.in.www1.artemis.domain.quiz.QuizBatch) StudentParticipation(de.tum.in.www1.artemis.domain.participation.StudentParticipation) ConstraintViolationException(org.hibernate.exception.ConstraintViolationException) Result(de.tum.in.www1.artemis.domain.Result)

Example 4 with QuizBatch

use of de.tum.in.www1.artemis.domain.quiz.QuizBatch in project ArTEMiS by ls1intum.

the class QuizScheduleService method saveQuizSubmissionWithParticipationAndResultToDatabase.

/**
 * check if the user submitted the submission or if the quiz has ended: if true: -> Create Participation and Result and save to Database (DB Write) Remove processed Submissions
 * from SubmissionHashMap and write Participations with Result into ParticipationHashMap and Results into ResultHashMap
 *
 * @param quizExercise      the quiz which should be checked
 * @param userSubmissionMap a Map with all submissions for the given quizExercise mapped by the username
 * @param userBatchMap      a Map of the username to quiz batch id for the given quizExercise
 * @param batchCache        a Map of all the batches for the given quizExercise
 * @return                  the number of processed submissions (submit or timeout)
 */
private int saveQuizSubmissionWithParticipationAndResultToDatabase(@NotNull QuizExercise quizExercise, Map<String, QuizSubmission> userSubmissionMap, Map<String, Long> userBatchMap, Map<Long, QuizBatch> batchCache) {
    int count = 0;
    for (String username : userSubmissionMap.keySet()) {
        try {
            // first case: the user submitted the quizSubmission
            QuizSubmission quizSubmission = userSubmissionMap.get(username);
            QuizBatch quizBatch = batchCache.get(userBatchMap.getOrDefault(username, 0L));
            if (quizSubmission.isSubmitted()) {
                if (quizSubmission.getType() == null) {
                    quizSubmission.setType(SubmissionType.MANUAL);
                }
            } else // second case: the quiz or batch has ended
            if (quizExercise.isQuizEnded() || quizBatch != null && quizBatch.isEnded()) {
                quizSubmission.setSubmitted(true);
                quizSubmission.setType(SubmissionType.TIMEOUT);
                quizSubmission.setSubmissionDate(ZonedDateTime.now());
            } else {
                // the quiz is running and the submission was not yet submitted.
                continue;
            }
            if (quizBatch != null) {
                // record which batch the submission belongs to
                quizSubmission.setQuizBatch(quizBatch.getId());
            }
            count++;
            // Create Participation and Result and save to Database (DB Write)
            // Remove processed Submissions from SubmissionHashMap and write Participations with Result into ParticipationHashMap and Results into ResultHashMap
            StudentParticipation participation = new StudentParticipation();
            // TODO: when this is set earlier for the individual quiz start of a student, we don't need to set this here anymore
            participation.setInitializationDate(quizSubmission.getSubmissionDate());
            Optional<User> user = userRepository.findOneByLogin(username);
            user.ifPresent(participation::setParticipant);
            // add the quizExercise to the participation
            participation.setExercise(quizExercise);
            participation.setInitializationState(InitializationState.FINISHED);
            // create new result
            Result result = new Result().participation(participation);
            result.setRated(true);
            result.setAssessmentType(AssessmentType.AUTOMATIC);
            result.setCompletionDate(quizSubmission.getSubmissionDate());
            result.setSubmission(quizSubmission);
            // calculate scores and update result and submission accordingly
            quizSubmission.calculateAndUpdateScores(quizExercise);
            result.evaluateQuizSubmission();
            // add result to participation
            participation.addResult(result);
            // add submission to participation
            participation.setSubmissions(Set.of(quizSubmission));
            // NOTE: we save (1) participation and (2) submission (in this particular order) here individually so that one exception (e.g. duplicated key) cannot
            // destroy multiple student answers
            participation = studentParticipationRepository.save(participation);
            quizSubmission.addResult(result);
            quizSubmission.setParticipation(participation);
            // this automatically saves the results due to CascadeType.ALL
            quizSubmission = quizSubmissionRepository.save(quizSubmission);
            log.info("Successfully saved submission in quiz " + quizExercise.getTitle() + " for user " + username);
            // reconnect entities after save
            participation.setSubmissions(Set.of(quizSubmission));
            participation.setResults(Set.of(result));
            result.setSubmission(quizSubmission);
            result.setParticipation(participation);
            // no point in keeping the participation around for non-synchronized modes where the due date may only be in a week
            if (quizExercise.getQuizMode() == QuizMode.SYNCHRONIZED) {
                // add the participation to the participationHashMap for the send out at the end of the quiz
                addParticipation(quizExercise.getId(), participation);
            }
            // remove the submission only after the participation has been added to the participation hashmap to avoid duplicated key exceptions for multiple participations for
            // the same user
            userSubmissionMap.remove(username);
            // clean up the batch association
            userBatchMap.remove(username);
            // add the result of the participation resultHashMap for the statistic-Update
            addResultForStatisticUpdate(quizExercise.getId(), result);
        } catch (ConstraintViolationException constraintViolationException) {
            log.error("ConstraintViolationException in saveQuizSubmissionWithParticipationAndResultToDatabase() for user {} in quiz {}: {}", username, quizExercise.getId(), constraintViolationException.getMessage(), constraintViolationException);
            // We got a ConstraintViolationException -> The "User-Quiz" pair is already saved in the database, but for some reason was not removed from the maps
            // We remove it from the maps now to prevent this error from occurring again
            // We do NOT add it to the participation map, as this should have been done already earlier (when the entry was added to the database)
            userSubmissionMap.remove(username);
            // clean up the batch association
            userBatchMap.remove(username);
        } catch (Exception e) {
            log.error("Exception in saveQuizSubmissionWithParticipationAndResultToDatabase() for user {} in quiz {}: {}", username, quizExercise.getId(), e.getMessage(), e);
        }
    }
    return count;
}
Also used : QuizSubmission(de.tum.in.www1.artemis.domain.quiz.QuizSubmission) User(de.tum.in.www1.artemis.domain.User) ConstraintViolationException(org.hibernate.exception.ConstraintViolationException) QuizBatch(de.tum.in.www1.artemis.domain.quiz.QuizBatch) StudentParticipation(de.tum.in.www1.artemis.domain.participation.StudentParticipation) ConstraintViolationException(org.hibernate.exception.ConstraintViolationException) Result(de.tum.in.www1.artemis.domain.Result)

Example 5 with QuizBatch

use of de.tum.in.www1.artemis.domain.quiz.QuizBatch in project ArTEMiS by ls1intum.

the class QuizScheduleService method executeQuizStartNowTask.

/**
 * Internal method to start and send the {@link QuizExercise} to the clients when called
 */
void executeQuizStartNowTask(Long quizExerciseId) {
    quizCache.performCacheWriteIfPresent(quizExerciseId, quizExerciseCache -> {
        quizExerciseCache.getQuizStart().clear();
        log.debug("Removed quiz {} start tasks", quizExerciseId);
        return quizExerciseCache;
    });
    log.debug("Sending quiz {} start", quizExerciseId);
    QuizExercise quizExercise = quizExerciseRepository.findByIdWithQuestionsAndStatisticsElseThrow(quizExerciseId);
    updateQuizExercise(quizExercise);
    if (quizExercise.getQuizMode() != QuizMode.SYNCHRONIZED) {
        throw new IllegalStateException();
    }
    // TODO: quiz cleanup: We create a batch that has just started here because we can't access QuizBatchService here because of dependencies
    var quizBatch = new QuizBatch();
    quizBatch.setQuizExercise(quizExercise);
    quizBatch.setStartTime(ZonedDateTime.now());
    quizExercise.setQuizBatches(Set.of(quizBatch));
    quizMessagingService.sendQuizExerciseToSubscribedClients(quizExercise, quizBatch, "start-now");
}
Also used : QuizBatch(de.tum.in.www1.artemis.domain.quiz.QuizBatch) QuizExercise(de.tum.in.www1.artemis.domain.quiz.QuizExercise)

Aggregations

QuizBatch (de.tum.in.www1.artemis.domain.quiz.QuizBatch)6 Result (de.tum.in.www1.artemis.domain.Result)4 User (de.tum.in.www1.artemis.domain.User)4 StudentParticipation (de.tum.in.www1.artemis.domain.participation.StudentParticipation)4 QuizExercise (de.tum.in.www1.artemis.domain.quiz.QuizExercise)4 QuizSubmission (de.tum.in.www1.artemis.domain.quiz.QuizSubmission)4 ConstraintViolationException (org.hibernate.exception.ConstraintViolationException)4 Config (com.hazelcast.config.Config)2 HazelcastInstance (com.hazelcast.core.HazelcastInstance)2 IAtomicReference (com.hazelcast.cp.IAtomicReference)2 com.hazelcast.scheduledexecutor (com.hazelcast.scheduledexecutor)2 Constants (de.tum.in.www1.artemis.config.Constants)2 AssessmentType (de.tum.in.www1.artemis.domain.enumeration.AssessmentType)2 InitializationState (de.tum.in.www1.artemis.domain.enumeration.InitializationState)2 QuizMode (de.tum.in.www1.artemis.domain.enumeration.QuizMode)2 SubmissionType (de.tum.in.www1.artemis.domain.enumeration.SubmissionType)2 SubmittedAnswer (de.tum.in.www1.artemis.domain.quiz.SubmittedAnswer)2 de.tum.in.www1.artemis.repository (de.tum.in.www1.artemis.repository)2 SecurityUtils (de.tum.in.www1.artemis.security.SecurityUtils)2 QuizMessagingService (de.tum.in.www1.artemis.service.QuizMessagingService)2