From 8ace45f55248b19208c0c73bb8c4a89c1adc22bd Mon Sep 17 00:00:00 2001 From: Ajayvir Singh <38434017+AjayvirS@users.noreply.github.com> Date: Tue, 24 Dec 2024 08:24:22 +0100 Subject: [PATCH 01/11] `Plagiarism checks`: Add direct link to case view (#9747) --- ...arism-cases-instructor-view.component.html | 37 +++++++---- ...arism-cases-instructor-view.component.scss | 0 ...giarism-cases-instructor-view.component.ts | 61 +++++++++++-------- .../plagiarism/types/GroupedPlagiarismCase.ts | 5 ++ src/main/webapp/i18n/de/plagiarism.json | 3 +- src/main/webapp/i18n/en/plagiarism.json | 3 +- ...sm-cases-instructor-view.component.spec.ts | 57 ++++++++++++++--- 7 files changed, 118 insertions(+), 48 deletions(-) create mode 100644 src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.scss create mode 100644 src/main/webapp/app/exercises/shared/plagiarism/types/GroupedPlagiarismCase.ts diff --git a/src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.html b/src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.html index 6bad6dfcb843..ba73e8f31b4f 100644 --- a/src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.html +++ b/src/main/webapp/app/course/plagiarism-cases/instructor-view/plagiarism-cases-instructor-view.component.html @@ -10,7 +10,7 @@

- @for (exercise of exercisesWithPlagiarismCases; track exercise; let i = $index) { + @for (exercise of exercisesWithPlagiarismCases; track exercise.id; let exerciseIndex = $index) {
@@ -20,6 +20,14 @@
} {{ exercise.title }} + + + +
@@ -49,20 +57,25 @@
- @for (plagiarismCase of groupedPlagiarismCases[exercise!.id!]; track plagiarismCase) { + @for (plagiarismCase of groupedPlagiarismCases[exercise!.id!]; track plagiarismCase; let plagiarismCaseIndex = $index) {
- + @if (plagiarismCase.student) { + + } @if (plagiarismCase.plagiarismSubmissions) { -
+
+ + {{ + 'artemisApp.plagiarism.plagiarismCases.appearsInComparisons' | artemisTranslate: { count: plagiarismCase.plagiarismSubmissions?.length } + }} + +
} + @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 @@
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(false); + form: FormGroup; + // Icons + protected readonly faSpinner = faSpinner; + getAsChannel = getAsChannelDTO; mode: 'individual' | 'group' = 'individual'; @@ -37,8 +43,9 @@ export class ConversationAddUsersFormComponent implements OnInit, OnChanges { get isSubmitPossible() { return ( - (this.mode === 'individual' && !this.form.invalid) || - (this.mode === 'group' && (this.form.value?.addAllStudents || this.form.value?.addAllTutors || this.form.value?.addAllInstructors)) + !this.isLoading() && + ((this.mode === 'individual' && !this.form.invalid) || + (this.mode === 'group' && (this.form.value?.addAllStudents || this.form.value?.addAllTutors || this.form.value?.addAllInstructors))) ); } diff --git a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-add-users-dialog/conversation-add-users-dialog.component.html b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-add-users-dialog/conversation-add-users-dialog.component.html index 32f3096ca9f0..7082cf10ef0d 100644 --- a/src/main/webapp/app/overview/course-conversations/dialogs/conversation-add-users-dialog/conversation-add-users-dialog.component.html +++ b/src/main/webapp/app/overview/course-conversations/dialogs/conversation-add-users-dialog/conversation-add-users-dialog.component.html @@ -17,6 +17,7 @@