Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plagiarism checks: Add direct link to plagiarism detection case view #9747

Open
wants to merge 20 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9192981
Development: add direct link to plagiarism detection case view from c…
AjayvirS Nov 9, 2024
6223d6e
Development: add test for plagiarism detection navigation
AjayvirS Nov 11, 2024
2df2fb0
Merge branch 'develop' into chore/general/5974-link-plagiarism-detect…
AjayvirS Nov 11, 2024
9574d76
remove redundant comment
AjayvirS Nov 14, 2024
aeae957
add coderabbit quality suggestions
AjayvirS Nov 14, 2024
f8695ef
Merge branch 'develop' into chore/general/5974-link-plagiarism-detect…
AjayvirS Nov 14, 2024
6d411d7
Merge branch 'develop' into chore/general/5974-link-plagiarism-detect…
AjayvirS Nov 19, 2024
b0fab14
add code reviewed changes
AjayvirS Nov 19, 2024
dc6ca72
Merge remote-tracking branch 'origin/chore/general/5974-link-plagiari…
AjayvirS Nov 19, 2024
0104895
Merge branch 'develop' into chore/general/5974-link-plagiarism-detect…
AjayvirS Nov 22, 2024
15c04c0
Merge branch 'develop' into chore/general/5974-link-plagiarism-detect…
sachmii Dec 2, 2024
a608be0
adapt changes for updated requirement
AjayvirS Dec 3, 2024
6a852e8
Merge branch 'develop' into chore/general/5974-link-plagiarism-detect…
AjayvirS Dec 10, 2024
8d867d5
simon changes
AjayvirS Dec 10, 2024
d2762cb
Merge remote-tracking branch 'origin/chore/general/5974-link-plagiari…
AjayvirS Dec 10, 2024
5d93346
simon changes
AjayvirS Dec 11, 2024
6ce4eec
prettier
AjayvirS Dec 11, 2024
823c6ff
Merge branch 'develop' into chore/general/5974-link-plagiarism-detect…
sachmii Dec 11, 2024
9b21bc9
coderabbit changes
AjayvirS Dec 11, 2024
2430998
coderabbit changes
AjayvirS Dec 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ <h4 jhiTranslate="artemisApp.plagiarism.cases.pageSubtitle"></h4>
<button class="btn btn-primary" (click)="exportPlagiarismCases()" jhiTranslate="artemisApp.plagiarism.cases.exportCsv"></button>
</div>
</div>
@for (exercise of exercisesWithPlagiarismCases; track exercise; let i = $index) {
@for (exercise of exercisesWithPlagiarismCases; track exercise.id; let i = $index) {
<div class="card mb-2">
<div class="card-header">
<div class="row">
Expand All @@ -20,6 +20,14 @@ <h5 class="mb-0">
<fa-icon [icon]="getIcon(exercise.type)" />
}
{{ exercise.title }}
<small>
<a
id="plagiarism-detection-link-{{ exercise.id }}"
[routerLink]="['/course-management', courseId, getExerciseUrlSegment(exercise.type), exercise.id, 'plagiarism']"
jhiTranslate="artemisApp.plagiarism.plagiarismCases.viewComparisons"
>
</a>
</small>
</h5>
</div>
<div class="col-3">
Expand Down Expand Up @@ -49,20 +57,25 @@ <h5 class="mb-0">
</div>
</div>
<div class="card-body">
@for (plagiarismCase of groupedPlagiarismCases[exercise!.id!]; track plagiarismCase) {
@for (plagiarismCase of groupedPlagiarismCases[exercise!.id!]; track plagiarismCase; let j = $index) {
<div class="row mb-3">
<div class="col-1 text-center">
<a [routerLink]="['/course-management', courseId, 'plagiarism-cases', plagiarismCase.id]">
{{ plagiarismCase.student.name }} ({{ plagiarismCase.student.login }})
</a>
</div>
@if (plagiarismCase.student) {
<div class="col-1 text-center">
<a [routerLink]="['/course-management', courseId, 'plagiarism-cases', plagiarismCase.id]">
{{ plagiarismCase.student!.name }} ({{ plagiarismCase.student!.login }})
</a>
</div>
}
@if (plagiarismCase.plagiarismSubmissions) {
<div
class="col-2 text-center"
jhiTranslate="artemisApp.plagiarism.plagiarismCases.appearsInComparisons"
[translateValues]="{ count: plagiarismCase.plagiarismSubmissions.length }"
></div>
<div class="col-2 text-center">
<span>
{{
'artemisApp.plagiarism.plagiarismCases.appearsInComparisons' | artemisTranslate: { count: plagiarismCase.plagiarismSubmissions?.length }
}}
</span>
</div>
}

@if (plagiarismCase.post) {
<div class="col-3 text-center">
{{ 'artemisApp.plagiarism.plagiarismCases.notifiedAt' | artemisTranslate }} {{ plagiarismCase.post.creationDate | artemisDate }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,28 @@
import { ActivatedRoute } from '@angular/router';
import { PlagiarismCasesService } from 'app/course/plagiarism-cases/shared/plagiarism-cases.service';
import { PlagiarismCase } from 'app/exercises/shared/plagiarism/types/PlagiarismCase';
import { Exercise, getIcon } from 'app/entities/exercise.model';
import { Exercise, getExerciseUrlSegment, getIcon } from 'app/entities/exercise.model';
import { downloadFile } from 'app/shared/util/download.util';
import { DocumentationType } from 'app/shared/components/documentation-button/documentation-button.component';
import { GroupedPlagiarismCases } from 'app/exercises/shared/plagiarism/types/GroupedPlagiarismCase';
import { AlertService } from 'app/core/util/alert.service';

@Component({
selector: 'jhi-plagiarism-cases-instructor-view',
templateUrl: './plagiarism-cases-instructor-view.component.html',
styleUrls: ['./plagiarism-cases-instructor-view.component.scss'],
})
export class PlagiarismCasesInstructorViewComponent implements OnInit {
courseId: number;
examId?: number;
plagiarismCases: PlagiarismCase[] = [];
groupedPlagiarismCases: any; // maybe? { [key: number]: PlagiarismCase[] }
groupedPlagiarismCases: GroupedPlagiarismCases;
exercisesWithPlagiarismCases: Exercise[] = [];

// method called as html template variable, angular only recognises reference variables in html if they are a property
// of the corresponding component class
getExerciseUrlSegment = getExerciseUrlSegment;
AjayvirS marked this conversation as resolved.
Show resolved Hide resolved

readonly getIcon = getIcon;
readonly documentationType: DocumentationType = 'PlagiarismChecks';

Expand All @@ -39,31 +45,7 @@
plagiarismCasesForInstructor$.subscribe({
next: (res: HttpResponse<PlagiarismCase[]>) => {
this.plagiarismCases = res.body!;
this.groupedPlagiarismCases = this.plagiarismCases.reduce(
(
acc: {
[exerciseId: number]: PlagiarismCase[];
},
plagiarismCase,
) => {
const caseExerciseId = plagiarismCase.exercise?.id;
if (caseExerciseId === undefined) {
return acc;
}

// Group initialization
if (!acc[caseExerciseId]) {
acc[caseExerciseId] = [];
this.exercisesWithPlagiarismCases.push(plagiarismCase.exercise!);
}

// Grouping
acc[caseExerciseId].push(plagiarismCase);

return acc;
},
{},
);
this.groupedPlagiarismCases = this.getGroupedPlagiarismCasesByExercise(this.plagiarismCases);
},
});
}
Expand Down Expand Up @@ -185,4 +167,32 @@
this.alertService.error('artemisApp.plagiarism.plagiarismCases.export.error');
}
}

/**
* groups plagiarism cases by exercise for view
* @param cases to be grouped by exerises
* @private return object containing grouped cases
*/
private getGroupedPlagiarismCasesByExercise(cases: PlagiarismCase[]): GroupedPlagiarismCases {
// Pre-collect exercises to avoid side effects in reduce
this.exercisesWithPlagiarismCases = cases
.map(plagiarismCase => plagiarismCase.exercise)

Check failure on line 179 in src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.ts

View workflow job for this annotation

GitHub Actions / client-style

Replace `plagiarismCase` with `(plagiarismCase)`
.filter((exercise): exercise is Exercise => exercise !== null && exercise !== undefined);

return cases.reduce((acc: GroupedPlagiarismCases, plagiarismCase) => {
const exercise = plagiarismCase.exercise;
if (!exercise?.id) {
return acc;
}

// Group initialization
if (!acc[exercise.id]) {
acc[exercise.id] = [];
}

// Grouping
acc[exercise.id].push(plagiarismCase);
return acc;
}, {});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { PlagiarismCase } from 'app/exercises/shared/plagiarism/types/PlagiarismCase';

export interface GroupedPlagiarismCases {
[exerciseId: number]: PlagiarismCase[];
}
3 changes: 2 additions & 1 deletion src/main/webapp/i18n/de/plagiarism.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@
"noCourseCases": "Keine Plagiatsfälle in diesem Kurs",
"noExamCases": "Keine Plagiatsfälle in dieser Prüfung",
"notifyStudent": "Studierende:n benachrichtigen",
"studentNotified": "Studierende:r wurde benachrichtigt."
"studentNotified": "Studierende:r wurde benachrichtigt.",
"viewComparisons": "Vergleiche ansehen"
},
"cases": {
"pageTitle": "Plagiatsfälle",
Expand Down
3 changes: 2 additions & 1 deletion src/main/webapp/i18n/en/plagiarism.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@
"noCourseCases": "No plagiarism cases in this course",
"noExamCases": "No plagiarism cases in this exam",
"notifyStudent": "Notify student",
"studentNotified": "Student has been notified."
"studentNotified": "Student has been notified.",
"viewComparisons": "View comparisons"
},
"cases": {
"pageTitle": "Plagiarism Cases",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { PlagiarismCasesInstructorViewComponent } from 'app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component';
import { ArtemisTestModule } from '../../test.module';
import { MockTranslateService, TranslateTestingModule } from '../../helpers/mocks/service/mock-translate.service';
import { PlagiarismCasesService } from 'app/course/plagiarism-cases/shared/plagiarism-cases.service';
import { ActivatedRoute, ActivatedRouteSnapshot, convertToParamMap } from '@angular/router';
import { ActivatedRoute, ActivatedRouteSnapshot, Router, RouterModule, convertToParamMap } from '@angular/router';
import { Observable, of } from 'rxjs';
import { HttpResponse } from '@angular/common/http';
import { PlagiarismCase } from 'app/exercises/shared/plagiarism/types/PlagiarismCase';
Expand All @@ -13,8 +11,18 @@ import { PlagiarismVerdict } from 'app/exercises/shared/plagiarism/types/Plagiar
import * as DownloadUtil from 'app/shared/util/download.util';
import dayjs from 'dayjs/esm';
import { DocumentationButtonComponent } from 'app/shared/components/documentation-button/documentation-button.component';
import { MockComponent } from 'ng-mocks';
import { MockComponent, MockModule } from 'ng-mocks';
import { NotificationService } from 'app/shared/notification/notification.service';
import { ExerciseType } from 'app/entities/exercise.model';
import { MockRouter } from '../../helpers/mocks/mock-router';
import { PlagiarismSubmission } from 'app/exercises/shared/plagiarism/types/PlagiarismSubmission';
import { TextSubmissionElement } from 'app/exercises/shared/plagiarism/types/text/TextSubmissionElement';
import { ArtemisTestModule } from '../../test.module';
import { MockTranslateService, TranslateTestingModule } from '../../helpers/mocks/service/mock-translate.service';
import { ArtemisDatePipe } from '../../../../../main/webapp/app/shared/pipes/artemis-date.pipe';
import { MockRouterLinkDirective } from '../../helpers/mocks/directive/mock-router-link.directive';
import { ProgressBarComponent } from 'app/shared/dashboards/tutor-participation-graph/progress-bar/progress-bar.component';
import { PlagiarismCaseVerdictComponent } from 'app/course/plagiarism-cases/shared/verdict/plagiarism-case-verdict.component';
import { MockNotificationService } from '../../helpers/mocks/service/mock-notification.service';

jest.mock('app/shared/util/download.util', () => ({
Expand All @@ -25,6 +33,7 @@ describe('Plagiarism Cases Instructor View Component', () => {
let component: PlagiarismCasesInstructorViewComponent;
let fixture: ComponentFixture<PlagiarismCasesInstructorViewComponent>;
let plagiarismCasesService: PlagiarismCasesService;
let router: MockRouter;

let route: ActivatedRoute;

Expand All @@ -33,15 +42,24 @@ describe('Plagiarism Cases Instructor View Component', () => {
const exercise1 = {
id: 1,
title: 'Test Exercise 1',
type: ExerciseType.TEXT,
} as TextExercise;
const exercise2 = {
id: 2,
title: 'Test Exercise 2',
type: ExerciseType.TEXT,
} as TextExercise;

const studentLoginA = 'studentA';
const plagiarismSubmission1 = {
id: 1,
studentLogin: studentLoginA,
} as PlagiarismSubmission<TextSubmissionElement>;

const plagiarismCase1 = {
id: 1,
exercise: exercise1,

student: { id: 1, login: 'Student 1' },
verdict: PlagiarismVerdict.PLAGIARISM,
verdictBy: {
Expand All @@ -56,6 +74,7 @@ describe('Plagiarism Cases Instructor View Component', () => {
},
],
},
plagiarismSubmissions: [plagiarismSubmission1],
} as PlagiarismCase;
const plagiarismCase2 = {
id: 2,
Expand Down Expand Up @@ -84,13 +103,21 @@ describe('Plagiarism Cases Instructor View Component', () => {
} as PlagiarismCase;

beforeEach(() => {
router = new MockRouter();
route = { snapshot: { paramMap: convertToParamMap({ courseId: 1 }) } } as any as ActivatedRoute;

TestBed.configureTestingModule({
imports: [ArtemisTestModule, TranslateTestingModule],
declarations: [PlagiarismCasesInstructorViewComponent, MockComponent(DocumentationButtonComponent)],
imports: [ArtemisTestModule, TranslateTestingModule, ArtemisDatePipe, MockModule(RouterModule)],
declarations: [
PlagiarismCasesInstructorViewComponent,
MockComponent(DocumentationButtonComponent),
MockRouterLinkDirective,
MockComponent(ProgressBarComponent),
MockComponent(PlagiarismCaseVerdictComponent),
],
providers: [
{ provide: ActivatedRoute, useValue: route },
{ provide: Router, useValue: router },
{ provide: NotificationService, useClass: MockNotificationService },
{ provide: TranslateService, useClass: MockTranslateService },
],
Expand All @@ -115,7 +142,10 @@ describe('Plagiarism Cases Instructor View Component', () => {
expect(component.examId).toBe(0);
expect(component.plagiarismCases).toEqual([plagiarismCase1, plagiarismCase2, plagiarismCase3, plagiarismCase4]);
expect(component.exercisesWithPlagiarismCases).toEqual([exercise1, exercise2]);
expect(component.groupedPlagiarismCases).toEqual({ 1: [plagiarismCase1, plagiarismCase2], 2: [plagiarismCase3, plagiarismCase4] });
expect(component.groupedPlagiarismCases).toEqual({
1: [plagiarismCase1, plagiarismCase2],
2: [plagiarismCase3, plagiarismCase4],
});
}));

it('should get plagiarism cases for course when exam id is not set', fakeAsync(() => {
Expand Down Expand Up @@ -198,4 +228,17 @@ describe('Plagiarism Cases Instructor View Component', () => {
expect(downloadSpy).toHaveBeenCalledOnce();
expect(downloadSpy).toHaveBeenCalledWith(new Blob(expectedBlob, { type: 'text/csv' }), 'plagiarism-cases.csv');
});

it('should navigate to plagiarism detection page on click', fakeAsync(() => {
const courseId = route.snapshot.paramMap.get('courseId');
// exercise id = exercise1.id for first element of first group (0-0)
const exerciseId = exercise1.id;

fixture.detectChanges();
const plagiarismDetectionLink = fixture.debugElement.nativeElement.querySelector('#plagiarism-detection-link-' + exercise1.id);
expect(plagiarismDetectionLink).toBeTruthy();
plagiarismDetectionLink.click();
const routePath = router.navigateByUrl.mock.calls[0][0];
expect(routePath).toStrictEqual(['/course-management', courseId, exercise1.type + '-exercises', exerciseId, 'plagiarism']);
}));
});
Loading