Search in sources :

Example 26 with QuizExercise

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

the class QuizSubmissionService method submitForPractice.

/**
 * Submit the given submission for practice
 *
 * @param quizSubmission the submission to submit
 * @param quizExercise   the exercise to submit in
 * @param participation  the participation where the result should be saved
 * @return the result entity
 */
public Result submitForPractice(QuizSubmission quizSubmission, QuizExercise quizExercise, Participation participation) {
    // update submission properties
    quizSubmission.setSubmitted(true);
    quizSubmission.setType(SubmissionType.MANUAL);
    quizSubmission.setSubmissionDate(ZonedDateTime.now());
    // calculate scores
    quizSubmission.calculateAndUpdateScores(quizExercise);
    // save parent submission object
    quizSubmission = quizSubmissionRepository.save(quizSubmission);
    // create result
    Result result = new Result().participation(participation);
    result.setRated(false);
    result.setAssessmentType(AssessmentType.AUTOMATIC);
    result.setCompletionDate(ZonedDateTime.now());
    // save result
    result = resultRepository.save(result);
    // setup result - submission relation
    result.setSubmission(quizSubmission);
    // calculate score and update result accordingly
    result.evaluateQuizSubmission();
    quizSubmission.addResult(result);
    quizSubmission.setParticipation(participation);
    // save submission to set result index column
    quizSubmissionRepository.save(quizSubmission);
    // save result to store score
    resultRepository.save(result);
    // result.participation.exercise.quizQuestions turn into proxy objects after saving, so we need to set it again to prevent problems later on
    result.setParticipation(participation);
    // add result to statistics
    quizScheduleService.addResultForStatisticUpdate(quizExercise.getId(), result);
    log.debug("submit practice quiz finished: {}", quizSubmission);
    return result;
}
Also used : Result(de.tum.in.www1.artemis.domain.Result)

Example 27 with QuizExercise

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

the class QuizSubmissionService method saveSubmissionForExamMode.

/**
 * Updates a submission for the exam mode
 *
 * @param quizExercise      the quiz exercise for which the submission for the exam mode should be done
 * @param quizSubmission    the quiz submission includes the submitted answers by the student
 * @param user              the student who wants to submit the quiz during the exam
 * @return                  the updated quiz submission after it has been saved to the database
 */
public QuizSubmission saveSubmissionForExamMode(QuizExercise quizExercise, QuizSubmission quizSubmission, String user) {
    // update submission properties
    quizSubmission.setSubmitted(true);
    quizSubmission.setType(SubmissionType.MANUAL);
    quizSubmission.setSubmissionDate(ZonedDateTime.now());
    Optional<StudentParticipation> optionalParticipation = participationService.findOneByExerciseAndStudentLoginAnyState(quizExercise, user);
    if (optionalParticipation.isEmpty()) {
        log.warn("The participation for quiz exercise {}, quiz submission {} and user {} was not found", quizExercise.getId(), quizSubmission.getId(), user);
        // TODO: think of better way to handle failure
        throw new EntityNotFoundException("Participation for quiz exercise " + quizExercise.getId() + " and quiz submission " + quizSubmission.getId() + " for user " + user + " was not found!");
    }
    StudentParticipation studentParticipation = optionalParticipation.get();
    quizSubmission.setParticipation(studentParticipation);
    // remove result from submission (in the unlikely case it is passed here), so that students cannot inject a result
    quizSubmission.setResults(new ArrayList<>());
    quizSubmissionRepository.save(quizSubmission);
    // versioning of submission
    try {
        submissionVersionService.saveVersionForIndividual(quizSubmission, user);
    } catch (Exception ex) {
        log.error("Quiz submission version could not be saved", ex);
    }
    log.debug("submit exam quiz finished: {}", quizSubmission);
    return quizSubmission;
}
Also used : EntityNotFoundException(de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException) StudentParticipation(de.tum.in.www1.artemis.domain.participation.StudentParticipation) QuizSubmissionException(de.tum.in.www1.artemis.exception.QuizSubmissionException) EntityNotFoundException(de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException)

Example 28 with QuizExercise

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

the class QuizSubmissionService method saveSubmissionForLiveMode.

/**
 * Saves a quiz submission into the hash maps for live quizzes. Submitted quizzes are marked to be saved into the database in the QuizScheduleService
 *
 * @param exerciseId the exerciseID to the corresponding QuizExercise
 * @param quizSubmission the submission which should be saved
 * @param userLogin the login of the user who has initiated the request
 * @param submitted whether the user has pressed the submit button or not
 *
 * @return the updated quiz submission object
 * @throws QuizSubmissionException handles errors, e.g. when the live quiz has already ended, or when the quiz was already submitted before
 */
public QuizSubmission saveSubmissionForLiveMode(Long exerciseId, QuizSubmission quizSubmission, String userLogin, boolean submitted) throws QuizSubmissionException {
    // TODO: what happens if a user executes this call twice in the same moment (using 2 threads)
    String logText = submitted ? "submit quiz in live mode:" : "save quiz in live mode:";
    long start = System.nanoTime();
    checkSubmissionForLiveModeOrThrow(exerciseId, userLogin, logText, start);
    // recreate pointers back to submission in each submitted answer
    for (SubmittedAnswer submittedAnswer : quizSubmission.getSubmittedAnswers()) {
        submittedAnswer.setSubmission(quizSubmission);
    }
    // set submission date
    quizSubmission.setSubmissionDate(ZonedDateTime.now());
    // save submission to HashMap
    quizScheduleService.updateSubmission(exerciseId, userLogin, quizSubmission);
    log.info("{} Saved quiz submission for user {} in quiz {} after {} µs ", logText, userLogin, exerciseId, (System.nanoTime() - start) / 1000);
    return quizSubmission;
}
Also used : SubmittedAnswer(de.tum.in.www1.artemis.domain.quiz.SubmittedAnswer)

Example 29 with QuizExercise

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

the class QuizSubmissionService method checkSubmissionForLiveModeOrThrow.

/**
 * Check that the user is allowed to currently submit to the specified exercise and throws an exception if not
 */
private void checkSubmissionForLiveModeOrThrow(Long exerciseId, String userLogin, String logText, long start) throws QuizSubmissionException {
    // check if submission is still allowed
    QuizExercise quizExercise = quizScheduleService.getQuizExercise(exerciseId);
    if (quizExercise == null) {
        // Fallback solution
        log.info("Quiz not in QuizScheduleService cache, fetching from DB");
        quizExercise = quizExerciseRepository.findByIdElseThrow(exerciseId);
        quizExercise.setQuizBatches(null);
    }
    log.debug("{}: Received quiz exercise for user {} in quiz {} in {} µs.", logText, userLogin, exerciseId, (System.nanoTime() - start) / 1000);
    if (!quizExercise.isQuizStarted() || quizExercise.isQuizEnded()) {
        throw new QuizSubmissionException("The quiz is not active");
    }
    var cachedSubmission = quizScheduleService.getQuizSubmission(exerciseId, userLogin);
    if (cachedSubmission.isSubmitted()) {
        // the old submission has not yet been processed, so don't allow a new one yet
        throw new QuizSubmissionException("You have already submitted the quiz");
    }
    if (quizExercise.getQuizMode() == QuizMode.SYNCHRONIZED) {
        // the batch exists if the quiz is active, otherwise a new inactive batch is returned
        if (!quizBatchService.getOrCreateSynchronizedQuizBatch(quizExercise).isSubmissionAllowed()) {
            throw new QuizSubmissionException("The quiz is not active");
        }
        // in synchronized mode we cache the participation after we processed the submission, so we can check there if the submission was already processed
        var cachedParticipation = quizScheduleService.getParticipation(exerciseId, userLogin);
        if (cachedParticipation != null && cachedParticipation.getResults().stream().anyMatch(r -> r.getSubmission().isSubmitted())) {
            throw new QuizSubmissionException("You have already submitted the quiz");
        }
    } else {
        // in the other modes the resubmission checks are done at join time and the student-batch association is removed when processing a submission
        var batch = quizBatchService.getQuizBatchForStudentByLogin(quizExercise, userLogin);
        // there is no way of distinguishing these two error cases without an extra db query
        if (batch.isEmpty()) {
            throw new QuizSubmissionException("You did not join or have already submitted the quiz");
        }
        if (!batch.get().isSubmissionAllowed()) {
            throw new QuizSubmissionException("The quiz is not active");
        }
    }
// TODO: additional checks that may be beneficial
// for example it is possible for students that are not members of the course to submit the quiz
// but for performance reasons the checks may have to be done in the quiz submission service where no feedback for the students can be generated
}
Also used : Logger(org.slf4j.Logger) QuizExercise(de.tum.in.www1.artemis.domain.quiz.QuizExercise) QuizSubmission(de.tum.in.www1.artemis.domain.quiz.QuizSubmission) QuizSubmissionException(de.tum.in.www1.artemis.exception.QuizSubmissionException) ZonedDateTime(java.time.ZonedDateTime) Participation(de.tum.in.www1.artemis.domain.participation.Participation) LoggerFactory(org.slf4j.LoggerFactory) SubmissionType(de.tum.in.www1.artemis.domain.enumeration.SubmissionType) AssessmentType(de.tum.in.www1.artemis.domain.enumeration.AssessmentType) ArrayList(java.util.ArrayList) Result(de.tum.in.www1.artemis.domain.Result) EntityNotFoundException(de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException) QuizMode(de.tum.in.www1.artemis.domain.enumeration.QuizMode) Service(org.springframework.stereotype.Service) QuizExerciseRepository(de.tum.in.www1.artemis.repository.QuizExerciseRepository) Optional(java.util.Optional) StudentParticipation(de.tum.in.www1.artemis.domain.participation.StudentParticipation) SubmittedAnswer(de.tum.in.www1.artemis.domain.quiz.SubmittedAnswer) QuizScheduleService(de.tum.in.www1.artemis.service.scheduled.cache.quiz.QuizScheduleService) QuizSubmissionRepository(de.tum.in.www1.artemis.repository.QuizSubmissionRepository) ResultRepository(de.tum.in.www1.artemis.repository.ResultRepository) QuizSubmissionException(de.tum.in.www1.artemis.exception.QuizSubmissionException) QuizExercise(de.tum.in.www1.artemis.domain.quiz.QuizExercise)

Example 30 with QuizExercise

use of de.tum.in.www1.artemis.domain.quiz.QuizExercise 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)

Aggregations

QuizExercise (de.tum.in.www1.artemis.domain.quiz.QuizExercise)46 StudentParticipation (de.tum.in.www1.artemis.domain.participation.StudentParticipation)31 QuizSubmission (de.tum.in.www1.artemis.domain.quiz.QuizSubmission)26 WithMockUser (org.springframework.security.test.context.support.WithMockUser)24 Result (de.tum.in.www1.artemis.domain.Result)23 Test (org.junit.jupiter.api.Test)20 ModelingExercise (de.tum.in.www1.artemis.domain.modeling.ModelingExercise)18 StudentExam (de.tum.in.www1.artemis.domain.exam.StudentExam)16 EntityNotFoundException (de.tum.in.www1.artemis.web.rest.errors.EntityNotFoundException)14 ModelingSubmission (de.tum.in.www1.artemis.domain.modeling.ModelingSubmission)12 SubmittedAnswer (de.tum.in.www1.artemis.domain.quiz.SubmittedAnswer)12 ZonedDateTime (java.time.ZonedDateTime)12 AbstractSpringIntegrationBambooBitbucketJiraTest (de.tum.in.www1.artemis.AbstractSpringIntegrationBambooBitbucketJiraTest)10 User (de.tum.in.www1.artemis.domain.User)10 AssessmentType (de.tum.in.www1.artemis.domain.enumeration.AssessmentType)8 ExerciseGroup (de.tum.in.www1.artemis.domain.exam.ExerciseGroup)8 PreAuthorize (org.springframework.security.access.prepost.PreAuthorize)8 Exam (de.tum.in.www1.artemis.domain.exam.Exam)6 Participation (de.tum.in.www1.artemis.domain.participation.Participation)6 ModelingPlagiarismResult (de.tum.in.www1.artemis.domain.plagiarism.modeling.ModelingPlagiarismResult)6