+ @if (plagiarismCase.student) {
+
+ }
@if (plagiarismCase.plagiarismSubmissions) {
-
}
+
@if (plagiarismCase.post) {
{{ 'artemisApp.plagiarism.plagiarismCases.notifiedAt' | artemisTranslate }} {{ plagiarismCase.post.creationDate | artemisDate }}
diff --git a/src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.scss b/src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.scss
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.ts b/src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.ts
index 760d8e6567fb..efa0d14d147e 100644
--- a/src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.ts
+++ b/src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.ts
@@ -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';
@@ -39,31 +45,7 @@ export class PlagiarismCasesInstructorViewComponent implements OnInit {
plagiarismCasesForInstructor$.subscribe({
next: (res: HttpResponse
) => {
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);
},
});
}
@@ -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;
+ }, {});
+ }
}
diff --git a/src/main/webapp/app/exercises/shared/plagiarism/types/GroupedPlagiarismCase.ts b/src/main/webapp/app/exercises/shared/plagiarism/types/GroupedPlagiarismCase.ts
new file mode 100644
index 000000000000..81fffc35017b
--- /dev/null
+++ b/src/main/webapp/app/exercises/shared/plagiarism/types/GroupedPlagiarismCase.ts
@@ -0,0 +1,5 @@
+import { PlagiarismCase } from 'app/exercises/shared/plagiarism/types/PlagiarismCase';
+
+export interface GroupedPlagiarismCases {
+ [exerciseId: number]: PlagiarismCase[];
+}
diff --git a/src/main/webapp/i18n/de/plagiarism.json b/src/main/webapp/i18n/de/plagiarism.json
index becba41634fc..31563c61a5cd 100644
--- a/src/main/webapp/i18n/de/plagiarism.json
+++ b/src/main/webapp/i18n/de/plagiarism.json
@@ -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",
diff --git a/src/main/webapp/i18n/en/plagiarism.json b/src/main/webapp/i18n/en/plagiarism.json
index ba73b67b9f37..db57419b919e 100644
--- a/src/main/webapp/i18n/en/plagiarism.json
+++ b/src/main/webapp/i18n/en/plagiarism.json
@@ -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",
diff --git a/src/test/javascript/spec/component/plagiarism/plagiarism-cases-instructor-view.component.spec.ts b/src/test/javascript/spec/component/plagiarism/plagiarism-cases-instructor-view.component.spec.ts
index 45749bcbdf3a..ce9c798c79d3 100644
--- a/src/test/javascript/spec/component/plagiarism/plagiarism-cases-instructor-view.component.spec.ts
+++ b/src/test/javascript/spec/component/plagiarism/plagiarism-cases-instructor-view.component.spec.ts
@@ -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';
@@ -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', () => ({
@@ -25,6 +33,7 @@ describe('Plagiarism Cases Instructor View Component', () => {
let component: PlagiarismCasesInstructorViewComponent;
let fixture: ComponentFixture;
let plagiarismCasesService: PlagiarismCasesService;
+ let router: MockRouter;
let route: ActivatedRoute;
@@ -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;
+
const plagiarismCase1 = {
id: 1,
exercise: exercise1,
+
student: { id: 1, login: 'Student 1' },
verdict: PlagiarismVerdict.PLAGIARISM,
verdictBy: {
@@ -56,6 +74,7 @@ describe('Plagiarism Cases Instructor View Component', () => {
},
],
},
+ plagiarismSubmissions: [plagiarismSubmission1],
} as PlagiarismCase;
const plagiarismCase2 = {
id: 2,
@@ -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 },
],
@@ -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(() => {
@@ -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']);
+ }));
});
From 0f1bf00bdb7dbdafaef4c90a3b753bb4b6dd0f13 Mon Sep 17 00:00:00 2001
From: Paul Rangger <48455539+PaRangger@users.noreply.github.com>
Date: Tue, 24 Dec 2024 13:50:33 +0100
Subject: [PATCH 02/11] Communication: Add loading indicator when adding users
to channel (#10032)
---
.../conversation-add-users-form.component.html | 3 +++
.../conversation-add-users-form.component.ts | 13 ++++++++++---
.../conversation-add-users-dialog.component.html | 1 +
.../conversation-add-users-dialog.component.ts | 9 +++++++++
.../conversation-add-users-dialog.component.spec.ts | 4 +++-
5 files changed, 26 insertions(+), 4 deletions(-)
diff --git a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-add-users-dialog/add-users-form/conversation-add-users-form.component.html b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-add-users-dialog/add-users-form/conversation-add-users-form.component.html
index b22435bd18ab..123cb0dc37ea 100644
--- a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-add-users-dialog/add-users-form/conversation-add-users-form.component.html
+++ b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-add-users-dialog/add-users-form/conversation-add-users-form.component.html
@@ -63,6 +63,9 @@
+ @if (isLoading()) {
+
+ }
diff --git a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-add-users-dialog/add-users-form/conversation-add-users-form.component.ts b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-add-users-dialog/add-users-form/conversation-add-users-form.component.ts
index d465889f1cc0..08c13cd628ab 100644
--- a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-add-users-dialog/add-users-form/conversation-add-users-form.component.ts
+++ b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-add-users-dialog/add-users-form/conversation-add-users-form.component.ts
@@ -1,8 +1,9 @@
-import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
+import { Component, EventEmitter, Input, OnChanges, OnInit, Output, input } from '@angular/core';
import { UserPublicInfoDTO } from 'app/core/user/user.model';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ConversationDTO } from 'app/entities/metis/conversation/conversation.model';
import { getAsChannelDTO } from 'app/entities/metis/conversation/channel.model';
+import { faSpinner } from '@fortawesome/free-solid-svg-icons';
export interface AddUsersFormData {
selectedUsers?: UserPublicInfoDTO[];
@@ -24,8 +25,13 @@ export class ConversationAddUsersFormComponent implements OnInit, OnChanges {
@Input()
activeConversation: ConversationDTO;
+ protected readonly isLoading = input