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;
}
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;
}
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;
}
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
}
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);
}
}
Aggregations