diff --git a/server/core/tests/base_test.py b/server/core/tests/base_test.py index 90201eaf..63468630 100644 --- a/server/core/tests/base_test.py +++ b/server/core/tests/base_test.py @@ -2,9 +2,15 @@ from django.test import TestCase, RequestFactory from graphene.test import Client from api.schema import schema +from core.tests.helpers import GQLResult from users.models import SchoolClass, User from users.services import create_users +class GQLClient(Client): + def get_result(self, *args, **kwargs): + return GQLResult(self.execute(*args, **kwargs)) + + class SkillboxTestCase(TestCase): def createDefault(self) -> None: @@ -22,4 +28,6 @@ class SkillboxTestCase(TestCase): if user is None: user = self.teacher request.user = user - return Client(schema=schema, context_value=request) + return GQLClient(schema=schema, context_value=request) + + diff --git a/server/portfolio/models.py b/server/portfolio/models.py index d369d20b..7cae2516 100644 --- a/server/portfolio/models.py +++ b/server/portfolio/models.py @@ -1,9 +1,20 @@ from django.contrib.auth import get_user_model from django.db import models from django_extensions.db.models import TitleSlugDescriptionModel +from graphql_relay import to_global_id + +from users.models import User -class Project(TitleSlugDescriptionModel): +class GraphqlNodeMixin: + def default_node_name(self): + return f'{self.__class__.__name__}Node' + + @property + def graphql_id(self): + return to_global_id(self.default_node_name(), self.id) + +class Project(TitleSlugDescriptionModel, GraphqlNodeMixin): objectives = models.TextField(blank=True) appearance = models.CharField(blank=True, null=False, max_length=255) student = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name='projects') @@ -13,6 +24,10 @@ class Project(TitleSlugDescriptionModel): def __str__(self): return self.title + def is_viewable_by(self, user: User): + return user.id == self.student.id or ( + self.final and self.student.get_teacher().id == user.id + ) class ProjectEntry(models.Model): activity = models.TextField(blank=True) diff --git a/server/portfolio/schema.py b/server/portfolio/schema.py index 1ebc10a4..c647b510 100644 --- a/server/portfolio/schema.py +++ b/server/portfolio/schema.py @@ -5,7 +5,7 @@ from graphene_django import DjangoObjectType from api.utils import get_by_id_or_slug from portfolio.models import Project, ProjectEntry -from users.models import Role, UserRole +from users.models import Role, UserRole, User class ProjectEntryNode(DjangoObjectType): @@ -52,4 +52,9 @@ class PortfolioQuery(object): return Project.objects.filter(student=user) def resolve_project(self, info, **kwargs): - return get_by_id_or_slug(Project, **kwargs) + user = info.context.user # type: User + project = get_by_id_or_slug(Project, **kwargs) #type: Project + + if project.is_viewable_by(user): + return project + return None diff --git a/server/portfolio/tests/test_project_query.py b/server/portfolio/tests/test_project_query.py index 18f637ad..b49c6452 100644 --- a/server/portfolio/tests/test_project_query.py +++ b/server/portfolio/tests/test_project_query.py @@ -1,18 +1,29 @@ -from django.test import TestCase, RequestFactory -from graphene.test import Client -from graphql_relay import to_global_id - -from api.schema import schema from core.tests.base_test import SkillboxTestCase from portfolio.factories import ProjectFactory from portfolio.models import Project -from rooms.models import Room from users.factories import SchoolClassFactory -from users.models import User, SchoolClass -from users.services import create_users +from users.models import User + +project_query = """ +query ProjectQuery($id: ID!) { + project(id: $id) { + id + } +} +""" -class ProjectQuery(SkillboxTestCase): +class ProjectQueryTestCaswe(SkillboxTestCase): + def _test_direct_project_access(self, user: User, should_have_access: bool): + result = self.get_client(user).get_result(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) + else: + self.assertIsNone(result.data.get('project')) + def setUp(self): self.createDefault() school_class1 = SchoolClassFactory(users=[self.teacher, self.student1]) @@ -71,14 +82,34 @@ class ProjectQuery(SkillboxTestCase): self.assertEqual(result.get('data').get('projects')[0].get('title'), self.project1.title) - def test_other_teacher_should_not_see_projects(self): self.project1.final = True self.project1.save() self.assertEqual(Project.objects.count(), 1) - result = self.get_client(self.teacher2).execute(self.query) self.assertIsNone(result.get('errors')) self.assertEqual(len(result.get('data').get('projects')), 0) + + def test_direct_project_access(self): + # student can access own project directly + self._test_direct_project_access(self.student1, True) + # teacher can't access project, as it's not final + self._test_direct_project_access(self.teacher, False) + self._test_direct_project_access(self.teacher2, False) + # 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() + # student can access own project directly + self._test_direct_project_access(self.student1, True) + # teacher of student can access project, as it's final + self._test_direct_project_access(self.teacher, True) + # other teacher can't access project, as it's not final + self._test_direct_project_access(self.teacher2, False) + # non-owner can't access project + self._test_direct_project_access(self.student2, False)