From 98771b2db20bd4169d957cf34a4f3234dea36f90 Mon Sep 17 00:00:00 2001 From: Ramon Wenger Date: Thu, 28 Mar 2024 16:12:48 +0100 Subject: [PATCH] Fix project page schema --- client/src/__generated__/graphql.ts | 3 +- client/src/pages/portfolio/project.vue | 5 +- server/portfolio/tests/test_project_query.py | 58 ++++++++++++-------- server/schema.graphql | 51 ++++++++--------- server/users/schema/types.py | 10 ++++ 5 files changed, 76 insertions(+), 51 deletions(-) diff --git a/client/src/__generated__/graphql.ts b/client/src/__generated__/graphql.ts index 1c1b372d..fade702b 100644 --- a/client/src/__generated__/graphql.ts +++ b/client/src/__generated__/graphql.ts @@ -1671,6 +1671,7 @@ export type PublicUserNode = Node & { id: Scalars['ID']['output']; isMe?: Maybe; lastName: Scalars['String']['output']; + schoolClasses?: Maybe>>; }; export type Query = { @@ -2212,7 +2213,7 @@ export type SyncModuleVisibilityPayload = { export type TeamNode = Node & { __typename?: 'TeamNode'; code?: Maybe; - creator: PublicUserNode; + creator?: Maybe; /** The ID of the object */ id: Scalars['ID']['output']; isDeleted: Scalars['Boolean']['output']; diff --git a/client/src/pages/portfolio/project.vue b/client/src/pages/portfolio/project.vue index c75fc056..10c21ba7 100644 --- a/client/src/pages/portfolio/project.vue +++ b/client/src/pages/portfolio/project.vue @@ -15,6 +15,7 @@ :final="project.final" data-cy="project-share-link" class="project__share" + v-if="canEdit" @share="updateProjectShareState(project.slug, !project.final)" /> @@ -109,7 +110,9 @@ export default { return [cls ? `project--${cls}` : '']; }, isOwner() { - return this.me.id === this.project.student.id; + const myId = window.atob(this.me.id).split(':')[1]; + const projectId = window.atob(this.project.student.id).split(':')[1]; + return myId === projectId; }, projectEntryCount() { return this.project.entries ? this.project.entries.length : 0; diff --git a/server/portfolio/tests/test_project_query.py b/server/portfolio/tests/test_project_query.py index 34b07a98..d7e5dea8 100644 --- a/server/portfolio/tests/test_project_query.py +++ b/server/portfolio/tests/test_project_query.py @@ -17,14 +17,16 @@ query ProjectQuery($id: ID!) { class ProjectQueryTestCase(SkillboxTestCase): def _test_direct_project_access(self, user: User, should_have_access: bool): - result = self.get_client(user).execute(project_query, variables={ - 'id': self.project1.graphql_id - }) + result = self.get_client(user).execute( + project_query, variables={"id": self.project1.graphql_id} + ) self.assertIsNone(result.errors) if should_have_access: - self.assertEqual(result.data.get('project').get('id'), self.project1.graphql_id) + self.assertEqual( + result.data.get("project").get("id"), self.project1.graphql_id + ) else: - self.assertIsNone(result.data.get('project')) + self.assertIsNone(result.data.get("project")) def setUp(self): self.createDefault() @@ -32,8 +34,8 @@ class ProjectQueryTestCase(SkillboxTestCase): school_class2 = SchoolClassFactory(users=[self.teacher2, self.student2]) self.project1 = ProjectFactory(student=self.student1) - self.project1_id = to_global_id('ProjectNode', self.project1.id) - self.query = ''' + self.project1_id = to_global_id("ProjectNode", self.project1.id) + self.query = """ query ProjectsQuery { projects { ...ProjectParts @@ -50,7 +52,7 @@ class ProjectQueryTestCase(SkillboxTestCase): __typename } - ''' + """ def test_should_see_own_projects(self): self.assertEqual(Project.objects.count(), 1) @@ -58,7 +60,9 @@ class ProjectQueryTestCase(SkillboxTestCase): result = self.get_client(self.student1).execute(self.query) self.assertIsNone(result.errors) - self.assertEqual(result.data.get('projects')[0].get('title'), self.project1.title) + self.assertEqual( + result.data.get("projects")[0].get("title"), self.project1.title + ) def test_should_not_see_other_projects(self): self.assertEqual(Project.objects.count(), 1) @@ -66,13 +70,13 @@ class ProjectQueryTestCase(SkillboxTestCase): result = self.get_client(self.student2).execute(self.query) self.assertIsNone(result.errors) - self.assertEqual(len(result.data.get('projects')), 0) + self.assertEqual(len(result.data.get("projects")), 0) def test_teacher_should_not_see_unfinished_projects(self): result = self.get_client().execute(self.query) self.assertIsNone(result.errors) - self.assertEqual(len(result.data.get('projects')), 0) + self.assertEqual(len(result.data.get("projects")), 0) def test_teacher_should_only_see_finished_projects(self): self.project1.final = True @@ -82,8 +86,9 @@ class ProjectQueryTestCase(SkillboxTestCase): result = self.get_client().execute(self.query) self.assertIsNone(result.errors) - self.assertEqual(result.data.get('projects')[0].get('title'), - self.project1.title) + self.assertEqual( + result.data.get("projects")[0].get("title"), self.project1.title + ) def test_other_teacher_should_not_see_projects(self): self.project1.final = True @@ -93,11 +98,13 @@ class ProjectQueryTestCase(SkillboxTestCase): result = self.get_client(self.teacher2).execute(self.query) self.assertIsNone(result.errors) - self.assertEqual(len(result.data.get('projects')), 0) + self.assertEqual(len(result.data.get("projects")), 0) def test_class_with_two_teachers_both_can_see_project(self): # create class with two teachers - school_class3 = SchoolClassFactory(users=[self.teacher, self.teacher2, self.student1]) + school_class3 = SchoolClassFactory( + users=[self.teacher, self.teacher2, self.student1] + ) self.project1.final = True self.project1.save() @@ -105,16 +112,18 @@ class ProjectQueryTestCase(SkillboxTestCase): # teacher can see project result = self.get_client(self.teacher).execute(self.query) self.assertIsNone(result.errors) - self.assertEqual(len(result.data.get('projects')), 1) + self.assertEqual(len(result.data.get("projects")), 1) # teacher2 can see project result = self.get_client(self.teacher2).execute(self.query) self.assertIsNone(result.errors) - self.assertEqual(len(result.data.get('projects')), 1) + self.assertEqual(len(result.data.get("projects")), 1) def test_class_with_two_teachers_direct_final_project_access(self): # create class with two teachers - school_class3 = SchoolClassFactory(users=[self.teacher, self.teacher2, self.student1]) + school_class3 = SchoolClassFactory( + users=[self.teacher, self.teacher2, self.student1] + ) self.project1.final = True self.project1.save() @@ -136,7 +145,6 @@ class ProjectQueryTestCase(SkillboxTestCase): # non-owner can't access project self._test_direct_project_access(self.student2, False) - def test_direct_final_project_access(self): self.project1.final = True self.project1.save() @@ -155,12 +163,14 @@ query ProjectQuery($id: ID!) { project(id: $id) { id student { - email + fullName } } } """ - result = self.get_client(self.student1).execute(query, variables={ - 'id': self.project1.graphql_id - }) - self.assertEqual(result.data['project']['student']['email'], self.student1.email) + result = self.get_client(self.student1).execute( + query, variables={"id": self.project1.graphql_id} + ) + self.assertEqual( + result.data["project"]["student"]["fullName"], self.student1.full_name + ) diff --git a/server/schema.graphql b/server/schema.graphql index 9d4755ab..f6d40221 100644 --- a/server/schema.graphql +++ b/server/schema.graphql @@ -191,6 +191,32 @@ type PublicUserNode implements Node { id: ID! fullName: String! isMe: Boolean + schoolClasses: [SchoolClassNode] +} + +type SchoolClassNode implements Node { + name: String! + code: String + + """The ID of the object""" + id: ID! + pk: Int + members: [ClassMemberNode] + readOnly: Boolean +} + +""" +We need to build this ourselves, because we want the active property on the node, because providing it on the +Connection or Edge for a UserNodeConnection is difficult. +""" +type ClassMemberNode { + user: PublicUserNode + active: Boolean + firstName: String + lastName: String + isTeacher: Boolean + id: ID + isMe: Boolean } union HighlightableNode = ContentBlockNode | InstrumentNode | ModuleNode | ChapterNode @@ -226,31 +252,6 @@ interface ContentBlockInterface { scalar GenericStreamFieldType -type SchoolClassNode implements Node { - name: String! - code: String - - """The ID of the object""" - id: ID! - pk: Int - members: [ClassMemberNode] - readOnly: Boolean -} - -""" -We need to build this ourselves, because we want the active property on the node, because providing it on the -Connection or Edge for a UserNodeConnection is difficult. -""" -type ClassMemberNode { - user: PublicUserNode - active: Boolean - firstName: String - lastName: String - isTeacher: Boolean - id: ID - isMe: Boolean -} - type ContentBlockBookmarkNode implements Node { """The ID of the object""" id: ID! diff --git a/server/users/schema/types.py b/server/users/schema/types.py index f3989aef..f1809311 100644 --- a/server/users/schema/types.py +++ b/server/users/schema/types.py @@ -157,6 +157,7 @@ class PrivateUserNode(DjangoObjectType): class PublicUserNode(DjangoObjectType): full_name = graphene.String(required=True) is_me = graphene.Boolean() + school_classes = graphene.List(SchoolClassNode) class Meta: model = User @@ -167,6 +168,15 @@ class PublicUserNode(DjangoObjectType): def resolve_is_me(parent: User, info, **kwargs): return info.context.user.pk == parent.pk + @staticmethod + def resolve_school_classes(root: User, info): + if root.selected_class is None: # then we don't have any class to return + return SchoolClass.objects.none() + return SchoolClass.objects.filter( + Q(schoolclassmember__active=True, schoolclassmember__user=root) + | Q(pk=root.selected_class.pk) + ).distinct() + class ClassMemberNode(ObjectType): """