use of de.tum.in.www1.artemis.domain.participation.Participation in project ArTEMiS by ls1intum.
the class AssessmentResource method saveAssessment.
/**
* Save or submit manual assessment depending on the submit flag.
*
* @param submission the submission containing the assessment
* @param feedbackList list of feedbacks
* @param submit if true the assessment is submitted, else only saved
* @param resultId resultId of the result we save the feedbackList to, null of no results exists yet
* @return result after saving/submitting modeling assessment
*/
ResponseEntity<Result> saveAssessment(Submission submission, boolean submit, List<Feedback> feedbackList, Long resultId) {
User user = userRepository.getUserWithGroupsAndAuthorities();
StudentParticipation studentParticipation = (StudentParticipation) submission.getParticipation();
long exerciseId = studentParticipation.getExercise().getId();
Exercise exercise = exerciseRepository.findByIdElseThrow(exerciseId);
checkAuthorization(exercise, user);
final var isAtLeastInstructor = authCheckService.isAtLeastInstructorForExercise(exercise, user);
if (!assessmentService.isAllowedToCreateOrOverrideResult(submission.getLatestResult(), exercise, studentParticipation, user, isAtLeastInstructor)) {
log.debug("The user {} is not allowed to override the assessment for the submission {}", user.getLogin(), submission.getId());
throw new AccessForbiddenException("The user is not allowed to override the assessment");
}
Result result = assessmentService.saveManualAssessment(submission, feedbackList, resultId);
if (submit) {
result = assessmentService.submitManualAssessment(result.getId(), exercise, submission.getSubmissionDate());
Optional<User> optionalStudent = ((StudentParticipation) submission.getParticipation()).getStudent();
if (optionalStudent.isPresent()) {
singleUserNotificationService.checkNotificationForAssessmentExerciseSubmission(exercise, optionalStudent.get(), result);
}
}
var participation = result.getParticipation();
// remove information about the student for tutors to ensure double-blind assessment
if (!isAtLeastInstructor) {
participation.filterSensitiveInformation();
}
if (submit && (participation.getExercise().getAssessmentDueDate() == null || participation.getExercise().getAssessmentDueDate().isBefore(ZonedDateTime.now()))) {
messagingService.broadcastNewResult(result.getParticipation(), result);
}
return ResponseEntity.ok(result);
}
use of de.tum.in.www1.artemis.domain.participation.Participation in project ArTEMiS by ls1intum.
the class TextPlagiarismDetectionService method checkPlagiarism.
/**
* Download all submissions of the exercise, run JPlag, and return the result
*
* @param textExercise to detect plagiarism for
* @param similarityThreshold ignore comparisons whose similarity is below this threshold (%)
* @param minimumScore consider only submissions whose score is greater or equal to this value
* @param minimumSize consider only submissions whose size is greater or equal to this value
* @return a zip file that can be returned to the client
* @throws ExitException is thrown if JPlag exits unexpectedly
*/
public TextPlagiarismResult checkPlagiarism(TextExercise textExercise, float similarityThreshold, int minimumScore, int minimumSize) throws ExitException {
long start = System.nanoTime();
String topic = plagiarismWebsocketService.getTextExercisePlagiarismCheckTopic(textExercise.getId());
// TODO: why do we have such a strange folder name?
final var submissionsFolderName = "./tmp/submissions";
final var submissionFolderFile = new File(submissionsFolderName);
submissionFolderFile.mkdirs();
final List<TextSubmission> textSubmissions = textSubmissionsForComparison(textExercise, minimumScore, minimumSize);
final var submissionsSize = textSubmissions.size();
log.info("Save text submissions for JPlag text comparison with {} submissions", submissionsSize);
if (textSubmissions.size() < 2) {
log.info("Insufficient amount of submissions for plagiarism detection. Return empty result.");
TextPlagiarismResult textPlagiarismResult = new TextPlagiarismResult();
textPlagiarismResult.setExercise(textExercise);
textPlagiarismResult.setSimilarityDistribution(new int[0]);
return textPlagiarismResult;
}
AtomicInteger processedSubmissionCount = new AtomicInteger(1);
textSubmissions.forEach(submission -> {
var progressMessage = "Getting submission: " + processedSubmissionCount + "/" + textSubmissions.size();
plagiarismWebsocketService.notifyInstructorAboutPlagiarismState(topic, PlagiarismCheckState.RUNNING, List.of(progressMessage));
submission.setResults(new ArrayList<>());
StudentParticipation participation = (StudentParticipation) submission.getParticipation();
participation.setExercise(null);
participation.setSubmissions(null);
String participantIdentifier = participation.getParticipantIdentifier();
if (participantIdentifier == null) {
participantIdentifier = "unknown";
}
try {
textSubmissionExportService.saveSubmissionToFile(submission, participantIdentifier, submissionsFolderName);
} catch (IOException e) {
log.error(e.getMessage());
}
processedSubmissionCount.getAndIncrement();
});
log.info("Saving text submissions done");
JPlagOptions options = new JPlagOptions(submissionsFolderName, LanguageOption.TEXT);
options.setMinimumTokenMatch(minimumSize);
// Important: for large courses with more than 1000 students, we might get more than one million results and 10 million files in the file system due to many 0% results,
// therefore we limit the results to at least 50% or 0.5 similarity, the passed threshold is between 0 and 100%
options.setSimilarityThreshold(similarityThreshold);
log.info("Start JPlag Text comparison");
JPlag jplag = new JPlag(options);
JPlagResult jPlagResult = jplag.run();
log.info("JPlag Text comparison finished with {} comparisons. Will limit the number of comparisons to 500", jPlagResult.getComparisons().size());
log.info("Delete submission folder");
if (submissionFolderFile.exists()) {
FileSystemUtils.deleteRecursively(submissionFolderFile);
}
TextPlagiarismResult textPlagiarismResult = new TextPlagiarismResult();
textPlagiarismResult.convertJPlagResult(jPlagResult);
textPlagiarismResult.setExercise(textExercise);
log.info("JPlag text comparison for {} submissions done in {}", submissionsSize, TimeLogUtil.formatDurationFrom(start));
plagiarismWebsocketService.notifyInstructorAboutPlagiarismState(topic, PlagiarismCheckState.COMPLETED, List.of());
return textPlagiarismResult;
}
use of de.tum.in.www1.artemis.domain.participation.Participation in project ArTEMiS by ls1intum.
the class QuizScheduleService method removeUnnecessaryObjectsBeforeSendingToClient.
private void removeUnnecessaryObjectsBeforeSendingToClient(StudentParticipation participation) {
if (participation.getExercise() != null) {
var quizExercise = (QuizExercise) participation.getExercise();
// we do not need the course and lectures
quizExercise.setCourse(null);
// students should not see statistics
// TODO: this would be useful, but leads to problems when the quiz schedule service wants to access the statistics again later on
// quizExercise.setQuizPointStatistic(null);
// quizExercise.getQuizQuestions().forEach(quizQuestion -> quizQuestion.setQuizQuestionStatistic(null));
}
// submissions are part of results, so we do not need them twice
participation.setSubmissions(null);
participation.setParticipant(null);
if (participation.getResults() != null && !participation.getResults().isEmpty()) {
QuizSubmission quizSubmission = (QuizSubmission) participation.getResults().iterator().next().getSubmission();
if (quizSubmission != null && quizSubmission.getSubmittedAnswers() != null) {
for (SubmittedAnswer submittedAnswer : quizSubmission.getSubmittedAnswers()) {
if (submittedAnswer.getQuizQuestion() != null) {
// we do not need all information of the questions again, they are already stored in the exercise
var question = submittedAnswer.getQuizQuestion();
submittedAnswer.setQuizQuestion(question.copyQuestionId());
}
}
}
}
}
use of de.tum.in.www1.artemis.domain.participation.Participation 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);
}
}
use of de.tum.in.www1.artemis.domain.participation.Participation in project ArTEMiS by ls1intum.
the class AutomaticProgrammingExerciseCleanupService method deleteBuildPlans.
private void deleteBuildPlans(Set<ProgrammingExerciseStudentParticipation> participationsWithBuildPlanToDelete) {
// Limit to 5000 deletions per night
List<ProgrammingExerciseStudentParticipation> actualParticipationsToClean = participationsWithBuildPlanToDelete.stream().limit(5000).toList();
List<String> buildPlanIds = actualParticipationsToClean.stream().map(ProgrammingExerciseStudentParticipation::getBuildPlanId).toList();
log.info("Build plans to cleanup: {}", buildPlanIds);
int index = 0;
for (ProgrammingExerciseStudentParticipation participation : actualParticipationsToClean) {
if (index > 0 && index % externalSystemRequestBatchSize == 0) {
try {
log.info("Sleep for {}s during cleanupBuildPlansOnContinuousIntegrationServer", externalSystemRequestBatchWaitingTime / 1000);
Thread.sleep(externalSystemRequestBatchWaitingTime);
} catch (InterruptedException ex) {
log.error("Exception encountered when pausing before cleaning up build plans", ex);
}
}
try {
participationService.cleanupBuildPlan(participation);
} catch (Exception ex) {
log.error("Could not cleanup build plan in participation " + participation.getId(), ex);
}
index++;
}
log.info("{} build plans have been cleaned", actualParticipationsToClean.size());
}
Aggregations