Skip to content

Commit

Permalink
Plagiarism checks: Hide other plagiarism case submission before exerc…
Browse files Browse the repository at this point in the history
…ise due date (#7582)
  • Loading branch information
jakubriegel authored Jan 7, 2024
1 parent 0d2758e commit 798620a
Show file tree
Hide file tree
Showing 15 changed files with 167 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ public void checkAccessRepositoryElseThrow(ProgrammingExerciseParticipation prog

// Error case 1: The user does not have permissions to push into the repository and the user is not notified for a related plagiarism case.
boolean hasPermissions = participationAuthCheckService.canAccessParticipation(programmingParticipation, user);
boolean userWasNotifiedAboutPlagiarismCase = plagiarismService.wasUserNotifiedByInstructor(programmingParticipation.getId(), user.getLogin());
if (!hasPermissions && !userWasNotifiedAboutPlagiarismCase) {
var exerciseDueDate = programmingExercise.isExamExercise() ? programmingExercise.getExerciseGroup().getExam().getEndDate() : programmingExercise.getDueDate();
boolean hasAccessToSubmission = plagiarismService.hasAccessToSubmission(programmingParticipation.getId(), user.getLogin(), exerciseDueDate);
if (!hasPermissions && !hasAccessToSubmission) {
throw new AccessUnauthorizedException();
}

Expand Down Expand Up @@ -106,7 +107,7 @@ else if (programmingExercise.getSubmissionPolicy() instanceof LockRepositoryPoli
// to read the student's repository.
// But the student should still be able to access if they are notified for a related plagiarism case.
if ((isStudent || (isTeachingAssistant && repositoryActionType != RepositoryActionType.READ))
&& !examSubmissionService.isAllowedToSubmitDuringExam(programmingExercise, user, false) && !userWasNotifiedAboutPlagiarismCase) {
&& !examSubmissionService.isAllowedToSubmitDuringExam(programmingExercise, user, false) && !hasAccessToSubmission) {
throw new AccessForbiddenException();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package de.tum.in.www1.artemis.service.plagiarism;

import static java.util.function.Predicate.isEqual;
import static java.util.function.Predicate.not;

import java.time.ZonedDateTime;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;

import org.springframework.stereotype.Service;

Expand Down Expand Up @@ -38,41 +44,56 @@ public PlagiarismService(PlagiarismComparisonRepository plagiarismComparisonRepo
* Anonymize the submission for the student view.
* A student should not see sensitive information but be able to retrieve both answers from both students for the comparison
*
* @param submission the submission to anonymize.
* @param userLogin the user login of the student asking to see his plagiarism comparison.
* @param submission the submission to anonymize.
* @param userLogin the user login of the student asking to see his plagiarism comparison.
* @param exerciseDueDate due date of the exercise.
*/
public void checkAccessAndAnonymizeSubmissionForStudent(Submission submission, String userLogin) {
if (!wasUserNotifiedByInstructor(submission.getId(), userLogin)) {
public void checkAccessAndAnonymizeSubmissionForStudent(Submission submission, String userLogin, ZonedDateTime exerciseDueDate) {
if (!hasAccessToSubmission(submission.getId(), userLogin, exerciseDueDate)) {
throw new AccessForbiddenException("This plagiarism submission is not related to the requesting user or the user has not been notified yet.");
}
submission.setParticipation(null);
submission.setResults(null);
submission.setSubmissionDate(null);
}

/**
* A student should not see both answers from both students for the comparison before the due date
*
* @param submissionId the id of the submission to check.
* @param userLogin the user login of the student asking to see his plagiarism comparison.
* @param exerciseDueDate due date of the exercise.
* @return true is the user has access to the submission
*/
public boolean hasAccessToSubmission(Long submissionId, String userLogin, ZonedDateTime exerciseDueDate) {
var comparisonOptional = plagiarismComparisonRepository.findBySubmissionA_SubmissionIdOrSubmissionB_SubmissionId(submissionId, submissionId);
return comparisonOptional.filter(not(Set::isEmpty)).isPresent()
&& isOwnSubmissionOrIsAfterExerciseDueDate(submissionId, userLogin, comparisonOptional.get(), exerciseDueDate)
&& wasUserNotifiedByInstructor(userLogin, comparisonOptional.get());
}

private boolean isOwnSubmissionOrIsAfterExerciseDueDate(Long submissionId, String userLogin, Set<PlagiarismComparison<?>> comparisons, ZonedDateTime exerciseDueDate) {
var isOwnSubmission = comparisons.stream().flatMap(it -> Stream.of(it.getSubmissionA(), it.getSubmissionB())).filter(Objects::nonNull)
.filter(it -> it.getSubmissionId() == submissionId).findFirst().map(PlagiarismSubmission::getStudentLogin).filter(isEqual(userLogin)).isPresent();
return isOwnSubmission || exerciseDueDate.isBefore(ZonedDateTime.now());
}

/**
* Checks whether the student with the given user login is involved in a plagiarism case which contains the given submissionId and the student is notified by the instructor.
*
* @param submissionId the id of a submissions that will be checked in plagiarism cases
* @param userLogin the user login of the student
* @param userLogin the user login of the student
* @return true if the student with user login owns one of the submissions in a PlagiarismComparison which contains the given submissionId and is notified by the instructor,
* otherwise false
*/
public boolean wasUserNotifiedByInstructor(Long submissionId, String userLogin) {
var comparisonOptional = plagiarismComparisonRepository.findBySubmissionA_SubmissionIdOrSubmissionB_SubmissionId(submissionId, submissionId);

private boolean wasUserNotifiedByInstructor(String userLogin, Set<PlagiarismComparison<?>> comparisons) {
// disallow requests from users who are not notified about this case:
if (comparisonOptional.isPresent()) {
var comparisons = comparisonOptional.get();
return comparisons.stream()
.anyMatch(comparison -> (comparison.getSubmissionA().getPlagiarismCase() != null
&& (comparison.getSubmissionA().getPlagiarismCase().getPost() != null || comparison.getSubmissionA().getPlagiarismCase().getVerdict() != null)
&& (comparison.getSubmissionA().getStudentLogin().equals(userLogin)))
|| (comparison.getSubmissionB().getPlagiarismCase() != null
&& (comparison.getSubmissionB().getPlagiarismCase().getPost() != null || comparison.getSubmissionB().getPlagiarismCase().getVerdict() != null)
&& (comparison.getSubmissionB().getStudentLogin().equals(userLogin))));
}
return false;
return comparisons.stream()
.anyMatch(comparison -> (comparison.getSubmissionA().getPlagiarismCase() != null
&& (comparison.getSubmissionA().getPlagiarismCase().getPost() != null || comparison.getSubmissionA().getPlagiarismCase().getVerdict() != null)
&& (comparison.getSubmissionA().getStudentLogin().equals(userLogin)))
|| (comparison.getSubmissionB().getPlagiarismCase() != null
&& (comparison.getSubmissionB().getPlagiarismCase().getPost() != null || comparison.getSubmissionB().getPlagiarismCase().getVerdict() != null)
&& (comparison.getSubmissionB().getStudentLogin().equals(userLogin))));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ public ResponseEntity<ModelingSubmission> getModelingSubmission(@PathVariable Lo

if (!authCheckService.isAllowedToAssessExercise(modelingExercise, user, resultId)) {
// anonymize and throw exception if not authorized to view submission
plagiarismService.checkAccessAndAnonymizeSubmissionForStudent(modelingSubmission, userRepository.getUser().getLogin());
plagiarismService.checkAccessAndAnonymizeSubmissionForStudent(modelingSubmission, userRepository.getUser().getLogin(), modelingExercise.getDueDate());
return ResponseEntity.ok(modelingSubmission);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ public ResponseEntity<TextSubmission> getTextSubmissionWithResults(@PathVariable

if (!authCheckService.isAtLeastTeachingAssistantForExercise(textSubmission.getParticipation().getExercise())) {
// anonymize and throw exception if not authorized to view submission
plagiarismService.checkAccessAndAnonymizeSubmissionForStudent(textSubmission, userRepository.getUser().getLogin());
plagiarismService.checkAccessAndAnonymizeSubmissionForStudent(textSubmission, userRepository.getUser().getLogin(),
textSubmission.getParticipation().getExercise().getDueDate());
return ResponseEntity.ok(textSubmission);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
[exercise]="plagiarismCase.exercise!"
[sortByStudentLogin]="forStudent ? 'Your submission' : plagiarismCase.student!.login!"
[splitControlSubject]="splitControlSubject"
[forStudent]="forStudent"
></jhi-plagiarism-split-view>
</ng-template>
</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ <h3>{{ 'artemisApp.plagiarism.plagiarismCases.conversation' | artemisTranslate }
<section>
<h3 class="mt-5">{{ 'artemisApp.plagiarism.plagiarismCases.comparisons' | artemisTranslate }}</h3>
<div class="container-fluid">
<jhi-plagiarism-case-review [plagiarismCase]="plagiarismCase" [forStudent]="false"></jhi-plagiarism-case-review>
<jhi-plagiarism-case-review [plagiarismCase]="plagiarismCase" [forStudent]="true"></jhi-plagiarism-case-review>
</div>
</section>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { UMLModel } from '@ls1intum/apollon';
export class ModelingSubmissionViewerComponent implements OnChanges {
@Input() exercise: ModelingExercise;
@Input() plagiarismSubmission: PlagiarismSubmission<ModelingSubmissionElement>;
@Input() hideContent: boolean;

public loading: boolean;
public submissionModel: UMLModel;
Expand All @@ -22,19 +23,20 @@ export class ModelingSubmissionViewerComponent implements OnChanges {

ngOnChanges(changes: SimpleChanges): void {
if (changes.plagiarismSubmission) {
this.loading = true;

const currentPlagiarismSubmission: PlagiarismSubmission<ModelingSubmissionElement> = changes.plagiarismSubmission.currentValue;

this.modelingSubmissionService.getSubmissionWithoutLock(currentPlagiarismSubmission.submissionId).subscribe({
next: (submission: ModelingSubmission) => {
this.loading = false;
this.submissionModel = JSON.parse(submission.model!) as UMLModel;
},
error: () => {
this.loading = false;
},
});
if (!this.hideContent) {
this.loading = true;
this.modelingSubmissionService.getSubmissionWithoutLock(currentPlagiarismSubmission.submissionId).subscribe({
next: (submission: ModelingSubmission) => {
this.loading = false;
this.submissionModel = JSON.parse(submission.model!) as UMLModel;
},
error: () => {
this.loading = false;
},
});
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,29 @@
<div class="plagiarism-split-pane" jhiPane>
@if (plagiarismComparison) {
@if (isModelingExercise) {
<jhi-modeling-submission-viewer [exercise]="exercise" [plagiarismSubmission]="getModelingSubmissionA()"></jhi-modeling-submission-viewer>
<jhi-modeling-submission-viewer [exercise]="exercise" [plagiarismSubmission]="getModelingSubmissionA()" [hideContent]="false"></jhi-modeling-submission-viewer>
}
@if (isProgrammingOrTextExercise) {
<jhi-text-submission-viewer [exercise]="exercise" [matches]="matchesA" [plagiarismSubmission]="getTextSubmissionA()"></jhi-text-submission-viewer>
<jhi-text-submission-viewer [exercise]="exercise" [matches]="matchesA" [plagiarismSubmission]="getTextSubmissionA()" [hideContent]="false" />
}
}
</div>
<div class="plagiarism-split-pane" jhiPane>
@if (plagiarismComparison) {
@if (isModelingExercise) {
<jhi-modeling-submission-viewer [exercise]="exercise" [plagiarismSubmission]="getModelingSubmissionB()"></jhi-modeling-submission-viewer>
<jhi-modeling-submission-viewer
[exercise]="exercise"
[plagiarismSubmission]="getModelingSubmissionB()"
[hideContent]="forStudent && dayjs(exercise.dueDate).isAfter(dayjs())"
/>
}
@if (isProgrammingOrTextExercise) {
<jhi-text-submission-viewer [exercise]="exercise" [matches]="matchesB" [plagiarismSubmission]="getTextSubmissionB()"></jhi-text-submission-viewer>
<jhi-text-submission-viewer
[exercise]="exercise"
[matches]="matchesB"
[plagiarismSubmission]="getTextSubmissionB()"
[hideContent]="forStudent && dayjs(exercise.dueDate).isAfter(dayjs())"
/>
}
}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { PlagiarismSubmission } from 'app/exercises/shared/plagiarism/types/Plag
import { PlagiarismCasesService } from 'app/course/plagiarism-cases/shared/plagiarism-cases.service';
import { HttpResponse } from '@angular/common/http';
import { SimpleMatch } from 'app/exercises/shared/plagiarism/types/PlagiarismMatch';
import dayjs from 'dayjs/esm';

@Directive({ selector: '[jhiPane]' })
export class SplitPaneDirective {
Expand All @@ -25,6 +26,7 @@ export class PlagiarismSplitViewComponent implements AfterViewInit, OnChanges, O
@Input() exercise: Exercise;
@Input() splitControlSubject: Subject<string>;
@Input() sortByStudentLogin: string;
@Input() forStudent: boolean;

@ViewChildren(SplitPaneDirective) panes!: QueryList<SplitPaneDirective>;

Expand All @@ -38,6 +40,8 @@ export class PlagiarismSplitViewComponent implements AfterViewInit, OnChanges, O
public matchesA: Map<string, FromToElement[]>;
public matchesB: Map<string, FromToElement[]>;

readonly dayjs = dayjs;

constructor(private plagiarismCasesService: PlagiarismCasesService) {}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class TextSubmissionViewerComponent implements OnChanges {
@Input() exercise: ProgrammingExercise | TextExercise;
@Input() matches: Map<string, FromToElement[]>;
@Input() plagiarismSubmission: PlagiarismSubmission<TextSubmissionElement>;
@Input() hideContent: boolean;

/**
* Name of the currently selected file.
Expand Down Expand Up @@ -73,12 +74,14 @@ export class TextSubmissionViewerComponent implements OnChanges {
ngOnChanges(changes: SimpleChanges): void {
if (changes.plagiarismSubmission) {
const currentPlagiarismSubmission: PlagiarismSubmission<TextSubmissionElement> = changes.plagiarismSubmission.currentValue;
this.loading = true;
if (!this.hideContent) {
this.loading = true;

if (this.exercise.type === ExerciseType.PROGRAMMING) {
this.loadProgrammingExercise(currentPlagiarismSubmission);
} else {
this.loadTextExercise(currentPlagiarismSubmission);
if (this.exercise.type === ExerciseType.PROGRAMMING) {
this.loadProgrammingExercise(currentPlagiarismSubmission);
} else {
this.loadTextExercise(currentPlagiarismSubmission);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,23 @@ void getModelSubmissionWithResult_notInvolved_notAllowed() throws Exception {
request.get("/api/modeling-submissions/" + submission.getId(), HttpStatus.FORBIDDEN, ModelingSubmission.class);
}

@Test
@WithMockUser(value = TEST_PREFIX + "student1", roles = "USER")
void getModelSubmissionWithResult_notOwner_beforeDueDate_notAllowed() throws Exception {
var submission = ParticipationFactory.generateModelingSubmission(validModel, true);
submission = modelingExerciseUtilService.addModelingSubmission(classExercise, submission, TEST_PREFIX + "student2");

var plagiarismComparison = new PlagiarismComparison<ModelingSubmissionElement>();
var submissionA = new PlagiarismSubmission<ModelingSubmissionElement>();
submissionA.setStudentLogin(TEST_PREFIX + "student2");
submissionA.setSubmissionId(submission.getId());
plagiarismComparison.setSubmissionA(submissionA);

plagiarismComparisonRepository.save(plagiarismComparison);

request.get("/api/modeling-submissions/" + submission.getId(), HttpStatus.FORBIDDEN, ModelingSubmission.class);
}

@Test
@WithMockUser(username = TEST_PREFIX + "tutor1", roles = "TA")
void getModelSubmission_lockLimitReached_success() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,7 @@ void testGetFilesAsDifferentStudentWithRelevantPlagiarismCase() throws Exception
@Test
@WithMockUser(username = TEST_PREFIX + "student2", roles = "USER")
void testGetFileAsDifferentStudentWithRelevantPlagiarismCase() throws Exception {
programmingExercise.setDueDate(ZonedDateTime.now().minusHours(1));
programmingExerciseRepository.save(programmingExercise);

addPlagiarismCaseToProgrammingExercise(TEST_PREFIX + "student1", TEST_PREFIX + "student2");
Expand All @@ -521,6 +522,19 @@ void testGetFileAsDifferentStudentWithRelevantPlagiarismCase() throws Exception
assertThat(new String(file)).isEqualTo(currentLocalFileContent);
}

@Test
@WithMockUser(username = TEST_PREFIX + "student2", roles = "USER")
void testCannotGetFileAsDifferentStudentWithRelevantPlagiarismCaseBeforeExerciseDueDate() throws Exception {
programmingExercise.setDueDate(ZonedDateTime.now().plusHours(1));
programmingExerciseRepository.save(programmingExercise);

addPlagiarismCaseToProgrammingExercise(TEST_PREFIX + "student1", TEST_PREFIX + "student2");

LinkedMultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("file", currentLocalFileName);
request.get(studentRepoBaseUrl + participation.getId() + "/file", HttpStatus.FORBIDDEN, byte[].class, params);
}

@Test
@WithMockUser(username = TEST_PREFIX + "student1", roles = "USER")
void testGetFileWithRelevantPlagiarismCaseAfterExam() throws Exception {
Expand All @@ -529,7 +543,8 @@ void testGetFileWithRelevantPlagiarismCaseAfterExam() throws Exception {

// The calculated exam end date (startDate of exam + workingTime of studentExam (7200 seconds))
// should be in the past for this test.
exam.setStartDate(ZonedDateTime.now().minusHours(3));
exam.setStartDate(ZonedDateTime.now().minusHours(4));
exam.setEndDate(ZonedDateTime.now().minusHours(1));
examRepository.save(exam);

addPlagiarismCaseToProgrammingExercise(TEST_PREFIX + "student2", TEST_PREFIX + "student1");
Expand Down Expand Up @@ -566,7 +581,8 @@ void testGetFilesWithRelevantPlagiarismCaseAfterExam() throws Exception {

// The calculated exam end date (startDate of exam + workingTime of studentExam (7200 seconds))
// should be in the past for this test.
exam.setStartDate(ZonedDateTime.now().minusHours(3));
exam.setStartDate(ZonedDateTime.now().minusHours(4));
exam.setEndDate(ZonedDateTime.now().minusHours(1));
examRepository.save(exam);

// student1 is notified.
Expand Down
Loading

0 comments on commit 798620a

Please sign in to comment.