From fb13997d75295c07901ea2a452f2b0bb1d82b0b0 Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Sat, 2 Dec 2023 19:01:06 +0100 Subject: [PATCH 01/22] feat: add shared attribute, add markshared mutation, add templateCourses mutation --- graphql/course/fields.ts | 37 +++++++++++++++++++ graphql/course/mutations.ts | 15 +++++++- .../migration.sql | 2 + prisma/schema.prisma | 2 + 4 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20231129181612_add_shared_attribute_to_course/migration.sql diff --git a/graphql/course/fields.ts b/graphql/course/fields.ts index a2ae10684..31d30cf17 100644 --- a/graphql/course/fields.ts +++ b/graphql/course/fields.ts @@ -71,4 +71,41 @@ export class ExtendedFieldsCourseResolver { const student = await getSessionStudent(context, studentId); return (await prisma.course_instructors_student.count({ where: { courseId: course.id, studentId: student.id } })) > 0; } + + @FieldResolver((returns) => [Course]) + @Authorized(Role.ADMIN, Role.OWNER, Role.INSTRUCTOR) + async templateCourses( + @Arg('studentId', { nullable: true }) studentId: number, + @Arg('search') search: string, + @Arg('take', () => GraphQLInt) take, + @Arg('skip', () => GraphQLInt, { nullable: true }) skip: number = 0 + ) { + const courses = await prisma.course.findMany({ + where: { + OR: [ + { + AND: [ + { + course_instructors_student: { + some: { + studentId: studentId, + }, + }, + }, + { + OR: [{ name: { contains: search } }, { description: { contains: search } }], + }, + ], + }, + { + shared: true, + }, + ], + }, + take, + skip, + }); + + return courses; + } } diff --git a/graphql/course/mutations.ts b/graphql/course/mutations.ts index 86c03d025..cbd2cc327 100644 --- a/graphql/course/mutations.ts +++ b/graphql/course/mutations.ts @@ -12,7 +12,7 @@ import * as GraphQLModel from '../generated/models'; import { getCourse, getStudent, getSubcoursesForCourse } from '../util'; import { putFile, DEFAULT_BUCKET } from '../../common/file-bucket'; -import { course_schooltype_enum as CourseSchooltype, course_subject_enum as CourseSubject, course_coursestate_enum as CourseState } from '../generated'; +import { course_schooltype_enum as CourseSchooltype, course_subject_enum as CourseSubject, course_coursestate_enum as CourseState, Course } from '../generated'; import { ForbiddenError } from '../error'; import { addCourseInstructor, allowCourse, denyCourse, subcourseOver } from '../../common/courses/states'; import { getCourseImageKey } from '../../common/courses/util'; @@ -82,6 +82,19 @@ export class MutateCourseResolver { return result; } + @Mutation((returns) => GraphQLModel.Course) + @Authorized(Role.ADMIN, Role.INSTRUCTOR) + async courseMarkShared(@Ctx() context: GraphQLContext, @Arg('courseId') courseId: number, @Arg('shared') shared: boolean): Promise { + const course = await getCourse(courseId); + await hasAccess(context, 'Course', course); + + const updatedCourse = await prisma.course.update({ + where: { id: course.id }, + data: { shared: shared }, + }); + return updatedCourse; + } + @Mutation((returns) => GraphQLModel.Course) @AuthorizedDeferred(Role.ADMIN, Role.OWNER) async courseEdit( diff --git a/prisma/migrations/20231129181612_add_shared_attribute_to_course/migration.sql b/prisma/migrations/20231129181612_add_shared_attribute_to_course/migration.sql new file mode 100644 index 000000000..26fbaa1ce --- /dev/null +++ b/prisma/migrations/20231129181612_add_shared_attribute_to_course/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "course" ADD COLUMN "shared" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 16cd2401b..9620b4c3c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -179,6 +179,8 @@ model course { course_instructors_student course_instructors_student[] course_tags_course_tag course_tags_course_tag[] subcourse subcourse[] + shared Boolean @default(false) + } // DEPRECATED: This was filled from BBB Meetings, replaced with the zoomMeetingReports field of lectures From 1837993a9b02bfee92b632357621da4e925adc1f Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Sat, 2 Dec 2023 19:27:01 +0100 Subject: [PATCH 02/22] fix: fix authorization --- graphql/course/mutations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphql/course/mutations.ts b/graphql/course/mutations.ts index cbd2cc327..0830ce6f2 100644 --- a/graphql/course/mutations.ts +++ b/graphql/course/mutations.ts @@ -83,7 +83,7 @@ export class MutateCourseResolver { } @Mutation((returns) => GraphQLModel.Course) - @Authorized(Role.ADMIN, Role.INSTRUCTOR) + @AuthorizedDeferred(Role.ADMIN, Role.OWNER) async courseMarkShared(@Ctx() context: GraphQLContext, @Arg('courseId') courseId: number, @Arg('shared') shared: boolean): Promise { const course = await getCourse(courseId); await hasAccess(context, 'Course', course); From 4bbd33172bf154311843e3d6754cd2b3c2ceaada Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Sat, 2 Dec 2023 19:29:32 +0100 Subject: [PATCH 03/22] feat: add logging to courseMarkShared mutation --- graphql/course/mutations.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/graphql/course/mutations.ts b/graphql/course/mutations.ts index 0830ce6f2..b17892100 100644 --- a/graphql/course/mutations.ts +++ b/graphql/course/mutations.ts @@ -92,6 +92,7 @@ export class MutateCourseResolver { where: { id: course.id }, data: { shared: shared }, }); + logger.info(`Course(${course.id} was ${shared ? 'shared' : 'unshared'} by User(${context.user.userID})`); return updatedCourse; } From 7ed6d73d258fceca668e3a84f26a38d50db98b0b Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Sat, 2 Dec 2023 20:03:38 +0100 Subject: [PATCH 04/22] add shared to public fiields --- graphql/authorizations.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/graphql/authorizations.ts b/graphql/authorizations.ts index 9da216e73..5653b6146 100644 --- a/graphql/authorizations.ts +++ b/graphql/authorizations.ts @@ -575,6 +575,7 @@ export const authorizationModelEnhanceMap: ModelsEnhanceMap = { | 'description' | 'createdAt' | 'updatedAt' + | 'shared' >({ screeningComment: adminOrOwner, correspondentId: adminOrOwner, From 17f52f18fcb58fa08668ee9a0f97be99e3b756bd Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Sat, 2 Dec 2023 20:21:52 +0100 Subject: [PATCH 05/22] integrate courseSearch method call in templateCourses mutation --- graphql/course/fields.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/graphql/course/fields.ts b/graphql/course/fields.ts index 31d30cf17..acdd43dc7 100644 --- a/graphql/course/fields.ts +++ b/graphql/course/fields.ts @@ -80,6 +80,7 @@ export class ExtendedFieldsCourseResolver { @Arg('take', () => GraphQLInt) take, @Arg('skip', () => GraphQLInt, { nullable: true }) skip: number = 0 ) { + const courseSearchFilters = await courseSearch(search); const courses = await prisma.course.findMany({ where: { OR: [ @@ -92,9 +93,7 @@ export class ExtendedFieldsCourseResolver { }, }, }, - { - OR: [{ name: { contains: search } }, { description: { contains: search } }], - }, + courseSearchFilters, ], }, { From b72f00d0804690355308435a79c117586ab1f51f Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Wed, 17 Jan 2024 10:00:22 +0100 Subject: [PATCH 06/22] feat: implement pr comments --- graphql/subcourse/mutations.ts | 48 +++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/graphql/subcourse/mutations.ts b/graphql/subcourse/mutations.ts index a682c793d..0848f6bf3 100644 --- a/graphql/subcourse/mutations.ts +++ b/graphql/subcourse/mutations.ts @@ -71,7 +71,7 @@ class PublicLectureInput { @Resolver((of) => GraphQLModel.Subcourse) export class MutateSubcourseResolver { @Mutation((returns) => GraphQLModel.Subcourse) - @AuthorizedDeferred(Role.ADMIN, Role.OWNER) + @AuthorizedDeferred(Role.INSTRUCTOR) async subcourseCreate( @Ctx() context: GraphQLContext, @Arg('courseId') courseId: number, @@ -80,27 +80,39 @@ export class MutateSubcourseResolver { ): Promise { const course = await getCourse(courseId); await hasAccess(context, 'Course', course); - - const { joinAfterStart, minGrade, maxGrade, maxParticipants, allowChatContactParticipants, allowChatContactProspects, groupChatType } = subcourse; - const result = await prisma.subcourse.create({ - data: { - courseId, - published: false, - joinAfterStart, - minGrade, - maxGrade, - maxParticipants, - allowChatContactParticipants, - allowChatContactProspects, - groupChatType, + const courseInstructorAssociation = await prisma.course_instructors_student.findFirst({ + where: { + courseId: courseId, + studentId: studentId, }, }); + const isCourseSharedOrOwned = !!courseInstructorAssociation || course.shared; + if (!isCourseSharedOrOwned) { + logger.error(`Subcourse(${courseId}) is not shared or Student(${studentId}) is not an instructor of this subcourse`); + throw new PrerequisiteError('This subcourse is not shared or you are not the instructor of this subcourse!'); + } - const student = await getSessionStudent(context, studentId); - await prisma.subcourse_instructors_student.create({ data: { subcourseId: result.id, studentId: student.id } }); + if (isCourseSharedOrOwned) { + const { joinAfterStart, minGrade, maxGrade, maxParticipants, allowChatContactParticipants, allowChatContactProspects, groupChatType } = subcourse; + const result = await prisma.subcourse.create({ + data: { + courseId, + published: false, + joinAfterStart, + minGrade, + maxGrade, + maxParticipants, + allowChatContactParticipants, + allowChatContactProspects, + groupChatType, + }, + }); + const student = await getSessionStudent(context, studentId); + await prisma.subcourse_instructors_student.create({ data: { subcourseId: result.id, studentId: student.id } }); - logger.info(`Subcourse(${result.id}) was created for Course(${courseId}) and Student(${student.id})`); - return result; + logger.info(`Subcourse(${result.id}) was created for Course(${courseId}) and Student(${student.id})`); + return result; + } } @Mutation((returns) => Boolean) From f0d7984315ab1d3365c3892a3da96575afaebec3 Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Thu, 18 Jan 2024 18:14:44 +0100 Subject: [PATCH 07/22] fix: pr comments --- graphql/subcourse/mutations.ts | 44 ++++++++++++++++------------------ 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/graphql/subcourse/mutations.ts b/graphql/subcourse/mutations.ts index a84ccdbe2..9f7eb2d52 100644 --- a/graphql/subcourse/mutations.ts +++ b/graphql/subcourse/mutations.ts @@ -71,7 +71,7 @@ class PublicLectureInput { @Resolver((of) => GraphQLModel.Subcourse) export class MutateSubcourseResolver { @Mutation((returns) => GraphQLModel.Subcourse) - @AuthorizedDeferred(Role.INSTRUCTOR) + @Authorized(Role.INSTRUCTOR) async subcourseCreate( @Ctx() context: GraphQLContext, @Arg('courseId') courseId: number, @@ -88,31 +88,29 @@ export class MutateSubcourseResolver { }); const isCourseSharedOrOwned = !!courseInstructorAssociation || course.shared; if (!isCourseSharedOrOwned) { - logger.error(`Subcourse(${courseId}) is not shared or Student(${studentId}) is not an instructor of this subcourse`); - throw new PrerequisiteError('This subcourse is not shared or you are not the instructor of this subcourse!'); + logger.error(`Course(${courseId}) is not shared or Student(${studentId}) is not an instructor of this course`); + throw new PrerequisiteError('This course is not shared or you are not the instructor of this course!'); } - if (isCourseSharedOrOwned) { - const { joinAfterStart, minGrade, maxGrade, maxParticipants, allowChatContactParticipants, allowChatContactProspects, groupChatType } = subcourse; - const result = await prisma.subcourse.create({ - data: { - courseId, - published: false, - joinAfterStart, - minGrade, - maxGrade, - maxParticipants, - allowChatContactParticipants, - allowChatContactProspects, - groupChatType, - }, - }); - const student = await getSessionStudent(context, studentId); - await prisma.subcourse_instructors_student.create({ data: { subcourseId: result.id, studentId: student.id } }); + const { joinAfterStart, minGrade, maxGrade, maxParticipants, allowChatContactParticipants, allowChatContactProspects, groupChatType } = subcourse; + const result = await prisma.subcourse.create({ + data: { + courseId, + published: false, + joinAfterStart, + minGrade, + maxGrade, + maxParticipants, + allowChatContactParticipants, + allowChatContactProspects, + groupChatType, + }, + }); + const student = await getSessionStudent(context, studentId); + await prisma.subcourse_instructors_student.create({ data: { subcourseId: result.id, studentId: student.id } }); - logger.info(`Subcourse(${result.id}) was created for Course(${courseId}) and Student(${student.id})`); - return result; - } + logger.info(`Subcourse(${result.id}) was created for Course(${courseId}) and Student(${student.id})`); + return result; } @Mutation((returns) => Boolean) From 0a234d606bccd4fde97d0cb9bd92c232fa2700ad Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Thu, 18 Jan 2024 21:07:37 +0100 Subject: [PATCH 08/22] feat: add integration tests for templateCourses --- integration-tests/07_course.ts | 42 ++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/integration-tests/07_course.ts b/integration-tests/07_course.ts index d3a883ccb..d62fca887 100644 --- a/integration-tests/07_course.ts +++ b/integration-tests/07_course.ts @@ -1,11 +1,12 @@ import { test } from './base'; import { adminClient } from './base/clients'; -import { pupilOne } from './01_user'; +import { pupilOne, studentOne } from './01_user'; import * as assert from 'assert'; import { screenedInstructorOne, screenedInstructorTwo } from './02_screening'; import { ChatType } from '../common/chat/types'; import { expectFetch } from './base/mock'; import { course_coursestate_enum as CourseState } from '@prisma/client'; +import { prisma } from '../common/prisma'; const appointmentTitle = 'Group Appointment 1'; @@ -21,6 +22,7 @@ const courseOne = test('Create Course One', async () => { description: "Why should I test if my users can do that for me in production?" category: club allowContact: true + shared: true subject: Informatik schooltype: gymnasium }) { @@ -113,6 +115,7 @@ export const subcourseOne = test('Create Subcourse', async () => { const { client, instructor } = await screenedInstructorOne; const { courseId } = await courseOne; + const { student } = await studentOne; const { subcourseCreate: { id: subcourseId }, @@ -126,7 +129,7 @@ export const subcourseOne = test('Create Subcourse', async () => { allowChatContactProspects: true allowChatContactParticipants: true groupChatType: ${ChatType.NORMAL} - }) { id } + } studentId: ${student.userID}) { id } } `); @@ -390,3 +393,38 @@ void test('Add / Remove another instructor', async () => { subcourseDeleteInstructor(subcourseId: ${subcourseId} studentId: ${instructor2.student.id}) }`); }); + +void test('Find shared course', async () => { + const { instructor: instructor1 } = await screenedInstructorTwo; + const { instructor, courseId } = await subcourseOne; + + const { courseSearch: courseSearch } = await adminClient.request(` + query FindSharedCourses { + templateCourses(studentId: ${instructor1.student.id}) { id } + } +`); + + assert.ok(courseSearch.some((it) => it.id === courseId)); +}); + +void test('Test template course search', async () => { + const { instructor: instructor1 } = await screenedInstructorTwo; + const { instructor, courseId } = await subcourseOne; + + await prisma.course.update({ where: { id: courseId }, data: { shared: false } }); + + const { courseSearch: courseSearch } = await adminClient.request(` + query FindSharedCourses { + templateCourses(studentId: ${instructor1.student.id}) { id } + } +`); + + assert.ok(courseSearch.every((it) => it.id != courseId)); + + const { courseSearch: courseSearch2 } = await adminClient.request(` + query FindSharedCourses { + templateCourses(studentId: ${instructor.student.id}) { id } + } +`); + assert.ok(courseSearch2.some((it) => it.id === courseId)); +}); From 018272879c94093a6d136decfb6fd9f474bb0a78 Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Fri, 19 Jan 2024 11:17:15 +0100 Subject: [PATCH 09/22] fix: pr comment --- graphql/subcourse/mutations.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/graphql/subcourse/mutations.ts b/graphql/subcourse/mutations.ts index 9f7eb2d52..058533819 100644 --- a/graphql/subcourse/mutations.ts +++ b/graphql/subcourse/mutations.ts @@ -79,11 +79,13 @@ export class MutateSubcourseResolver { @Arg('studentId', { nullable: true }) studentId?: number ): Promise { const course = await getCourse(courseId); + const student = await getSessionStudent(context, studentId); + await hasAccess(context, 'Course', course); const courseInstructorAssociation = await prisma.course_instructors_student.findFirst({ where: { courseId: courseId, - studentId: studentId, + studentId: student.id, }, }); const isCourseSharedOrOwned = !!courseInstructorAssociation || course.shared; @@ -106,7 +108,6 @@ export class MutateSubcourseResolver { groupChatType, }, }); - const student = await getSessionStudent(context, studentId); await prisma.subcourse_instructors_student.create({ data: { subcourseId: result.id, studentId: student.id } }); logger.info(`Subcourse(${result.id}) was created for Course(${courseId}) and Student(${student.id})`); From 45415b26ff579d9b67de2bfdbea18cf690d2ea02 Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Mon, 22 Jan 2024 19:59:28 +0100 Subject: [PATCH 10/22] fix: pr comments --- integration-tests/07_course.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/07_course.ts b/integration-tests/07_course.ts index d62fca887..ab18b53a5 100644 --- a/integration-tests/07_course.ts +++ b/integration-tests/07_course.ts @@ -129,7 +129,7 @@ export const subcourseOne = test('Create Subcourse', async () => { allowChatContactProspects: true allowChatContactParticipants: true groupChatType: ${ChatType.NORMAL} - } studentId: ${student.userID}) { id } + } { id } } `); From 81db3599387436ccee9d5b4d44a8052a1251e4b7 Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Thu, 1 Feb 2024 21:14:18 +0100 Subject: [PATCH 11/22] fix integration test --- integration-tests/07_course.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/integration-tests/07_course.ts b/integration-tests/07_course.ts index ab18b53a5..033ceaa82 100644 --- a/integration-tests/07_course.ts +++ b/integration-tests/07_course.ts @@ -22,7 +22,6 @@ const courseOne = test('Create Course One', async () => { description: "Why should I test if my users can do that for me in production?" category: club allowContact: true - shared: true subject: Informatik schooltype: gymnasium }) { @@ -395,9 +394,18 @@ void test('Add / Remove another instructor', async () => { }); void test('Find shared course', async () => { - const { instructor: instructor1 } = await screenedInstructorTwo; + const { client, instructor: instructor1 } = await screenedInstructorTwo; const { instructor, courseId } = await subcourseOne; + await client.request(` + mutation MarkCourseShared { + courseMarkShared( + courseId: ${courseId}, + shared: true + ) + } + `); + const { courseSearch: courseSearch } = await adminClient.request(` query FindSharedCourses { templateCourses(studentId: ${instructor1.student.id}) { id } From 33f7da8bbebe7e1f3f97ca8e0c2178557ef5da29 Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Sun, 4 Feb 2024 20:53:56 +0100 Subject: [PATCH 12/22] add ) back --- integration-tests/07_course.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/07_course.ts b/integration-tests/07_course.ts index 033ceaa82..4f453d864 100644 --- a/integration-tests/07_course.ts +++ b/integration-tests/07_course.ts @@ -128,7 +128,7 @@ export const subcourseOne = test('Create Subcourse', async () => { allowChatContactProspects: true allowChatContactParticipants: true groupChatType: ${ChatType.NORMAL} - } { id } + }) { id } } `); From 381fbbf7a40a35b71d3831c810b5f24c73bf24f1 Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Sun, 4 Feb 2024 21:02:30 +0100 Subject: [PATCH 13/22] pass studentid to createsubcourse --- integration-tests/07_course.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/07_course.ts b/integration-tests/07_course.ts index 4f453d864..9c8629b59 100644 --- a/integration-tests/07_course.ts +++ b/integration-tests/07_course.ts @@ -120,7 +120,7 @@ export const subcourseOne = test('Create Subcourse', async () => { subcourseCreate: { id: subcourseId }, } = await client.request(` mutation CreateSubcourse { - subcourseCreate(courseId: ${courseId} subcourse: { + subcourseCreate(courseId: ${courseId} studentId: ${student.userID} subcourse: { minGrade: 5 maxGrade: 10 maxParticipants: 1 From a3db03c9e5eb68f3292e90b3741da9f24f4bab0b Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Sun, 4 Feb 2024 21:12:57 +0100 Subject: [PATCH 14/22] add , --- integration-tests/07_course.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/07_course.ts b/integration-tests/07_course.ts index 9c8629b59..4c1ecfd79 100644 --- a/integration-tests/07_course.ts +++ b/integration-tests/07_course.ts @@ -120,7 +120,7 @@ export const subcourseOne = test('Create Subcourse', async () => { subcourseCreate: { id: subcourseId }, } = await client.request(` mutation CreateSubcourse { - subcourseCreate(courseId: ${courseId} studentId: ${student.userID} subcourse: { + subcourseCreate(courseId: ${courseId}, studentId: ${student.userID}, subcourse: { minGrade: 5 maxGrade: 10 maxParticipants: 1 From ebebc1df21b81fcd69cac364fb6c9a97a549f378 Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Sun, 11 Feb 2024 20:08:48 +0100 Subject: [PATCH 15/22] fix integration tests --- graphql/course/fields.ts | 3 +-- graphql/subcourse/mutations.ts | 2 +- integration-tests/07_course.ts | 42 ++++++++++++++++------------------ 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/graphql/course/fields.ts b/graphql/course/fields.ts index acdd43dc7..8f8413685 100644 --- a/graphql/course/fields.ts +++ b/graphql/course/fields.ts @@ -72,7 +72,7 @@ export class ExtendedFieldsCourseResolver { return (await prisma.course_instructors_student.count({ where: { courseId: course.id, studentId: student.id } })) > 0; } - @FieldResolver((returns) => [Course]) + @Query((returns) => [Course]) @Authorized(Role.ADMIN, Role.OWNER, Role.INSTRUCTOR) async templateCourses( @Arg('studentId', { nullable: true }) studentId: number, @@ -104,7 +104,6 @@ export class ExtendedFieldsCourseResolver { take, skip, }); - return courses; } } diff --git a/graphql/subcourse/mutations.ts b/graphql/subcourse/mutations.ts index 828ce2597..912be7113 100644 --- a/graphql/subcourse/mutations.ts +++ b/graphql/subcourse/mutations.ts @@ -73,7 +73,7 @@ class PublicLectureInput { @Resolver((of) => GraphQLModel.Subcourse) export class MutateSubcourseResolver { @Mutation((returns) => GraphQLModel.Subcourse) - @Authorized(Role.INSTRUCTOR) + @AuthorizedDeferred(Role.INSTRUCTOR) async subcourseCreate( @Ctx() context: GraphQLContext, @Arg('courseId') courseId: number, diff --git a/integration-tests/07_course.ts b/integration-tests/07_course.ts index 4c1ecfd79..a2b3a0cfc 100644 --- a/integration-tests/07_course.ts +++ b/integration-tests/07_course.ts @@ -120,16 +120,17 @@ export const subcourseOne = test('Create Subcourse', async () => { subcourseCreate: { id: subcourseId }, } = await client.request(` mutation CreateSubcourse { - subcourseCreate(courseId: ${courseId}, studentId: ${student.userID}, subcourse: { + subcourseCreate(courseId: ${courseId}, subcourse: { minGrade: 5 maxGrade: 10 - maxParticipants: 1 + maxParticipants: 1 joinAfterStart: true allowChatContactProspects: true allowChatContactParticipants: true groupChatType: ${ChatType.NORMAL} - }) { id } - } + }) + {id} + } `); const { subcoursesPublic } = await client.request(` @@ -394,25 +395,23 @@ void test('Add / Remove another instructor', async () => { }); void test('Find shared course', async () => { - const { client, instructor: instructor1 } = await screenedInstructorTwo; - const { instructor, courseId } = await subcourseOne; + const { courseId, client } = await subcourseOne; await client.request(` mutation MarkCourseShared { - courseMarkShared( - courseId: ${courseId}, - shared: true - ) + courseMarkShared(courseId: ${courseId}, shared: true){id} } `); - const { courseSearch: courseSearch } = await adminClient.request(` - query FindSharedCourses { - templateCourses(studentId: ${instructor1.student.id}) { id } - } -`); + const { templateCourses } = await adminClient.request(` + query FindSharedCourses { + templateCourses(search: "", take: 1) { + id + } + } + `); - assert.ok(courseSearch.some((it) => it.id === courseId)); + assert.ok(templateCourses.some((it) => it.id === courseId)); }); void test('Test template course search', async () => { @@ -420,19 +419,18 @@ void test('Test template course search', async () => { const { instructor, courseId } = await subcourseOne; await prisma.course.update({ where: { id: courseId }, data: { shared: false } }); - - const { courseSearch: courseSearch } = await adminClient.request(` + const { templateCourses: courseSearch } = await adminClient.request(` query FindSharedCourses { - templateCourses(studentId: ${instructor1.student.id}) { id } + templateCourses(studentId: ${instructor1.student.id}, search: "", take: 10) { id } } `); - assert.ok(courseSearch.every((it) => it.id != courseId)); - const { courseSearch: courseSearch2 } = await adminClient.request(` + const { templateCourses: courseSearch2 } = await adminClient.request(` query FindSharedCourses { - templateCourses(studentId: ${instructor.student.id}) { id } + templateCourses(studentId: ${instructor.student.id}, search: "", take: 10) { id } } `); + assert.ok(courseSearch2.some((it) => it.id === courseId)); }); From 927e7fcdfaae87d99644d2892793fe95712e0929 Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Sun, 11 Feb 2024 20:24:37 +0100 Subject: [PATCH 16/22] fix linting --- graphql/subcourse/mutations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphql/subcourse/mutations.ts b/graphql/subcourse/mutations.ts index c18382623..e79b4a439 100644 --- a/graphql/subcourse/mutations.ts +++ b/graphql/subcourse/mutations.ts @@ -73,7 +73,7 @@ class PublicLectureInput { @Resolver((of) => GraphQLModel.Subcourse) export class MutateSubcourseResolver { @Mutation((returns) => GraphQLModel.Subcourse) - @AuthorizedDeferred(Role.INSTRUCTOR) + @AuthorizedDeferred(Role.INSTRUCTOR, Role.OWNER) async subcourseCreate( @Ctx() context: GraphQLContext, @Arg('courseId') courseId: number, From e51f4289fc7bd1b25d3aad524d4881824f88fc1e Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Mon, 12 Feb 2024 20:25:29 +0100 Subject: [PATCH 17/22] fix pr comments --- graphql/subcourse/mutations.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/graphql/subcourse/mutations.ts b/graphql/subcourse/mutations.ts index e79b4a439..96d3af6c6 100644 --- a/graphql/subcourse/mutations.ts +++ b/graphql/subcourse/mutations.ts @@ -73,7 +73,7 @@ class PublicLectureInput { @Resolver((of) => GraphQLModel.Subcourse) export class MutateSubcourseResolver { @Mutation((returns) => GraphQLModel.Subcourse) - @AuthorizedDeferred(Role.INSTRUCTOR, Role.OWNER) + @Authorized(Role.INSTRUCTOR, Role.ADMIN) async subcourseCreate( @Ctx() context: GraphQLContext, @Arg('courseId') courseId: number, @@ -83,7 +83,6 @@ export class MutateSubcourseResolver { const course = await getCourse(courseId); const student = await getSessionStudent(context, studentId); - await hasAccess(context, 'Course', course); const courseInstructorAssociation = await prisma.course_instructors_student.findFirst({ where: { courseId: courseId, From 9904d1ad0cfda2685f7a209e326b158214a7e073 Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Mon, 12 Feb 2024 20:26:33 +0100 Subject: [PATCH 18/22] remove whitespace --- integration-tests/07_course.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/07_course.ts b/integration-tests/07_course.ts index a2b3a0cfc..a775b7a4c 100644 --- a/integration-tests/07_course.ts +++ b/integration-tests/07_course.ts @@ -123,7 +123,7 @@ export const subcourseOne = test('Create Subcourse', async () => { subcourseCreate(courseId: ${courseId}, subcourse: { minGrade: 5 maxGrade: 10 - maxParticipants: 1 + maxParticipants: 1 joinAfterStart: true allowChatContactProspects: true allowChatContactParticipants: true From db6607518598b219e17d651866a4488290067542 Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Mon, 12 Feb 2024 20:35:19 +0100 Subject: [PATCH 19/22] fix pr comments --- integration-tests/07_course.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/07_course.ts b/integration-tests/07_course.ts index a775b7a4c..0aec80404 100644 --- a/integration-tests/07_course.ts +++ b/integration-tests/07_course.ts @@ -415,13 +415,13 @@ void test('Find shared course', async () => { }); void test('Test template course search', async () => { - const { instructor: instructor1 } = await screenedInstructorTwo; + const { instructor: instructor2 } = await screenedInstructorTwo; const { instructor, courseId } = await subcourseOne; await prisma.course.update({ where: { id: courseId }, data: { shared: false } }); const { templateCourses: courseSearch } = await adminClient.request(` query FindSharedCourses { - templateCourses(studentId: ${instructor1.student.id}, search: "", take: 10) { id } + templateCourses(studentId: ${instructor2.student.id}, search: "", take: 10) { id } } `); assert.ok(courseSearch.every((it) => it.id != courseId)); From 449fe9484e325f4cb74075a21ec230bdf833592d Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Mon, 12 Feb 2024 21:01:04 +0100 Subject: [PATCH 20/22] test query templateCourses eith non empty search string --- integration-tests/07_course.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/integration-tests/07_course.ts b/integration-tests/07_course.ts index 0aec80404..bf935d2fe 100644 --- a/integration-tests/07_course.ts +++ b/integration-tests/07_course.ts @@ -403,7 +403,7 @@ void test('Find shared course', async () => { } `); - const { templateCourses } = await adminClient.request(` + const { templateCourses: templateCoursesEmptySearch } = await adminClient.request(` query FindSharedCourses { templateCourses(search: "", take: 1) { id @@ -411,7 +411,20 @@ void test('Find shared course', async () => { } `); - assert.ok(templateCourses.some((it) => it.id === courseId)); + assert.ok(templateCoursesEmptySearch.some((it) => it.id === courseId)); + + const course = await prisma.course.findUnique({ + where: { id: courseId }, + }); + + const { templateCourses: templateCoursesNonEmptySearch } = await adminClient.request(` + query FindSharedCourses { + templateCourses(search: "${course.name}", take: 1) { + id + } + } +`); + assert.ok(templateCoursesNonEmptySearch.some((it) => it.id === courseId)); }); void test('Test template course search', async () => { From 5bd4a7e1a23ee6b0c472a533e635101dbbe57309 Mon Sep 17 00:00:00 2001 From: Paul Jaworski Date: Sun, 18 Feb 2024 18:25:46 +0100 Subject: [PATCH 21/22] remove secondtest --- integration-tests/07_course.ts | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/integration-tests/07_course.ts b/integration-tests/07_course.ts index bf935d2fe..992cc6916 100644 --- a/integration-tests/07_course.ts +++ b/integration-tests/07_course.ts @@ -395,7 +395,8 @@ void test('Add / Remove another instructor', async () => { }); void test('Find shared course', async () => { - const { courseId, client } = await subcourseOne; + const { instructor: instructor2, client: instructor2client } = await screenedInstructorTwo; + const { courseId, client, instructor: subcourseInstructor } = await subcourseOne; await client.request(` mutation MarkCourseShared { @@ -403,47 +404,52 @@ void test('Find shared course', async () => { } `); - const { templateCourses: templateCoursesEmptySearch } = await adminClient.request(` + const { templateCourses: templateCoursesEmptySearch } = await instructor2client.request(` query FindSharedCourses { templateCourses(search: "", take: 1) { id } } `); - + // Instructor from subcourseOne creates subcourse, Different instructor (instructor2) must find it. assert.ok(templateCoursesEmptySearch.some((it) => it.id === courseId)); const course = await prisma.course.findUnique({ where: { id: courseId }, }); - const { templateCourses: templateCoursesNonEmptySearch } = await adminClient.request(` + const { templateCourses: templateCoursesNonEmptySearch } = await instructor2client.request(` query FindSharedCourses { templateCourses(search: "${course.name}", take: 1) { id } } `); + // Instructor 2 must be able to find shared course by name assert.ok(templateCoursesNonEmptySearch.some((it) => it.id === courseId)); -}); -void test('Test template course search', async () => { - const { instructor: instructor2 } = await screenedInstructorTwo; - const { instructor, courseId } = await subcourseOne; + await client.request(` + mutation MarkCourseShared { + courseMarkShared(courseId: ${courseId}, shared: false){id} + } +`); - await prisma.course.update({ where: { id: courseId }, data: { shared: false } }); const { templateCourses: courseSearch } = await adminClient.request(` query FindSharedCourses { templateCourses(studentId: ${instructor2.student.id}, search: "", take: 10) { id } } -`); + `); + + //Subcourse is not shared anymore. Instructor 2 must not find the course since it was created + //by the subcourseInstructor assert.ok(courseSearch.every((it) => it.id != courseId)); const { templateCourses: courseSearch2 } = await adminClient.request(` query FindSharedCourses { - templateCourses(studentId: ${instructor.student.id}, search: "", take: 10) { id } + templateCourses(studentId: ${subcourseInstructor.student.id}, search: "", take: 10) { id } } `); + //SubcourseOneInstructor must be able to find his own subcourse, even if it is not shared assert.ok(courseSearch2.some((it) => it.id === courseId)); }); From ddd06845ef5fbe3fed14019f459b09c64135939d Mon Sep 17 00:00:00 2001 From: --global <--global> Date: Fri, 29 Mar 2024 17:10:32 +0100 Subject: [PATCH 22/22] rewrite prisma graphql query --- graphql/course/fields.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/graphql/course/fields.ts b/graphql/course/fields.ts index 8f8413685..548f368f9 100644 --- a/graphql/course/fields.ts +++ b/graphql/course/fields.ts @@ -81,11 +81,13 @@ export class ExtendedFieldsCourseResolver { @Arg('skip', () => GraphQLInt, { nullable: true }) skip: number = 0 ) { const courseSearchFilters = await courseSearch(search); + const courses = await prisma.course.findMany({ where: { - OR: [ + AND: [ + courseSearchFilters, { - AND: [ + OR: [ { course_instructors_student: { some: { @@ -93,17 +95,17 @@ export class ExtendedFieldsCourseResolver { }, }, }, - courseSearchFilters, + { + shared: true, + }, ], }, - { - shared: true, - }, ], }, take, skip, }); + return courses; } }