Skip to content

Commit

Permalink
Plagiarism checks: Add direct link to case view (#9747)
Browse files Browse the repository at this point in the history
  • Loading branch information
AjayvirS authored Dec 24, 2024
1 parent 97c0636 commit 8ace45f
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 48 deletions.
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 exerciseIndex = $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 plagiarismCaseIndex = $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 { Component, OnInit } from '@angular/core';
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;

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

Expand All @@ -39,31 +45,7 @@ export class PlagiarismCasesInstructorViewComponent implements OnInit {
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,29 @@ export class PlagiarismCasesInstructorViewComponent implements OnInit {
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 {
return cases.reduce((acc: { [exerciseId: number]: PlagiarismCase[] }, 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;
}, {});
}
}
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']);
}));
});

0 comments on commit 8ace45f

Please sign in to comment.