From a62a9855407b72a2a2f7241da6900dd6ed91e82c Mon Sep 17 00:00:00 2001 From: Rainshaw Date: Sun, 26 Dec 2021 21:24:16 +0800 Subject: [PATCH] Revert "refactor: remove graphene dependence" This reverts commit a4f56195e268635dbe2dab92a6cb081a8043ac85. --- MeetPlan/schema.py | 19 + MeetPlan/settings.py | 35 +- MeetPlan/urls.py | 4 + apps/meet_plan/models.py | 11 +- apps/meet_plan/schema/__init__.py | 17 + apps/meet_plan/schema/mutation.py | 150 ++ apps/meet_plan/schema/query.py | 106 + apps/meet_plan/tests.py | 1697 +++++++++++++++ apps/pku_auth/schema/__init__.py | 25 + apps/pku_auth/schema/mutation.py | 125 ++ apps/pku_auth/schema/query.py | 32 + apps/pku_auth/tests.py | 164 +- apps/user/schema/__init__.py | 29 + apps/user/schema/mutation.py | 113 + apps/user/schema/query.py | 113 + apps/user/tests.py | 1885 +++++++++++++++++ poetry.lock | 173 +- pyproject.toml | 4 + .../graphql_jwt/zh_Hans/LC_MESSAGES/django.po | 59 + .../zh_Hans/LC_MESSAGES/django.po | 84 + 20 files changed, 4830 insertions(+), 15 deletions(-) create mode 100644 MeetPlan/schema.py create mode 100644 apps/meet_plan/schema/__init__.py create mode 100644 apps/meet_plan/schema/mutation.py create mode 100644 apps/meet_plan/schema/query.py create mode 100644 apps/pku_auth/schema/__init__.py create mode 100644 apps/pku_auth/schema/mutation.py create mode 100644 apps/pku_auth/schema/query.py create mode 100644 apps/user/schema/__init__.py create mode 100644 apps/user/schema/mutation.py create mode 100644 apps/user/schema/query.py create mode 100644 translation/graphql_jwt/zh_Hans/LC_MESSAGES/django.po create mode 100644 translation/refresh_token/zh_Hans/LC_MESSAGES/django.po diff --git a/MeetPlan/schema.py b/MeetPlan/schema.py new file mode 100644 index 0000000..6496801 --- /dev/null +++ b/MeetPlan/schema.py @@ -0,0 +1,19 @@ +import graphene +from django.conf import settings +from graphene_django.debug import DjangoDebug + +from apps.meet_plan.schema import Query as MeetPlanQuery, Mutation as MeetPlanMutation +from apps.pku_auth.schema import Query as AuthQuery, Mutation as AuthMutation +from apps.user.schema import Query as UserQuery, Mutation as UserMutation + + +class Query(MeetPlanQuery, UserQuery, AuthQuery, graphene.ObjectType): + if settings.DEBUG: + debug = graphene.Field(DjangoDebug, name="_debug") + + +class Mutation(MeetPlanMutation, UserMutation, AuthMutation, graphene.ObjectType): + pass + + +schema = graphene.Schema(query=Query, mutation=Mutation) diff --git a/MeetPlan/settings.py b/MeetPlan/settings.py index 106a125..f99bafe 100644 --- a/MeetPlan/settings.py +++ b/MeetPlan/settings.py @@ -9,6 +9,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.2/ref/settings/ """ +from datetime import timedelta from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -34,7 +35,9 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "graphene_django", "django_filters", + "graphql_jwt.refresh_token", "guardian", "apps.user", "apps.pku_auth", @@ -151,21 +154,51 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ -STATIC_URL = "static/" +STATIC_URL = "/static/" # Default primary key field type # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +# the schema location for Graphene +# https://docs.graphene-python.org/projects/django/en/latest/installation/ +GRAPHENE = { + "SCHEMA": "MeetPlan.schema.schema", + "ATOMIC_MUTATIONS": True, + "MIDDLEWARE": [ + "graphql_jwt.middleware.JSONWebTokenMiddleware", + ], +} +if DEBUG: + GRAPHENE["MIDDLEWARE"].append("graphene_django.debug.DjangoDebugMiddleware") + AUTH_USER_MODEL = "user.User" AUTHENTICATION_BACKENDS = [ "apps.pku_auth.backends.OpenIDClientBackend", + "graphql_jwt.backends.JSONWebTokenBackend", "guardian.backends.ObjectPermissionBackend", "django.contrib.auth.backends.ModelBackend", ] +GRAPHQL_JWT = { + "JWT_VERIFY_EXPIRATION": True, + "JWT_LONG_RUNNING_REFRESH_TOKEN": True, + "JWT_REUSE_REFRESH_TOKENS": True, + "JWT_EXPIRATION_DELTA": timedelta(hours=1), + "JWT_REFRESH_EXPIRATION_DELTA": timedelta(days=7), + "JWT_ALLOW_ANY_CLASSES": [ + "apps.pku_auth.schema.ObtainJSONWebToken", + "apps.pku_auth.schema.Verify", + "apps.pku_auth.schema.Refresh", + "apps.pku_auth.schema.Revoke", + "apps.pku_auth.schema.RevokeAll", + ], +} + +GRAPHENE_DJANGO_PLUS = {"MUTATIONS_INCLUDE_REVERSE_RELATIONS": False} + # Django Guardian # https://django-guardian.readthedocs.io/en/stable/configuration.html#anonymous-user-name ANONYMOUS_USER_NAME = "0000000000" diff --git a/MeetPlan/urls.py b/MeetPlan/urls.py index 071f8b3..4993a5b 100644 --- a/MeetPlan/urls.py +++ b/MeetPlan/urls.py @@ -17,8 +17,12 @@ from django.contrib import admin from django.urls import path, include +from django.views.decorators.csrf import csrf_exempt +from graphene_django.views import GraphQLView + urlpatterns = [ path("admin/", admin.site.urls), + path("graphql/", csrf_exempt(GraphQLView.as_view(graphiql=True))), ] if settings.DEBUG: diff --git a/apps/meet_plan/models.py b/apps/meet_plan/models.py index 847cdab..2164949 100644 --- a/apps/meet_plan/models.py +++ b/apps/meet_plan/models.py @@ -5,6 +5,7 @@ from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ +from graphene_django_plus.models import GuardedModel, GuardedModelManager def get_start_date(): @@ -19,7 +20,7 @@ def get_start_date(): return start_date -class MeetPlanManager(models.Manager): +class MeetPlanManager(GuardedModelManager): def get_queryset(self, start_date=None): if start_date is None: return super(MeetPlanManager, self).get_queryset() @@ -27,8 +28,8 @@ def get_queryset(self, start_date=None): return super(MeetPlanManager, self).get_queryset().filter(start_time__gt=start_date) -class MeetPlan(models.Model): - teacher = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, related_name="meet_plan_set") +class MeetPlan(GuardedModel): + teacher = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, related_name="meet_plan") place = models.CharField(_("place"), max_length=100) start_time = models.DateTimeField(_("start time")) duration = models.PositiveSmallIntegerField( @@ -38,7 +39,7 @@ class MeetPlan(models.Model): ) t_message = models.TextField(_("teacher message"), blank=True) student = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, related_name="meet_plan_order_set", null=True, blank=True + settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, related_name="meet_plan_order", null=True, blank=True ) s_message = models.TextField(_("student message"), blank=True) complete = models.BooleanField(_("status"), default=False) @@ -60,7 +61,7 @@ def save(self, **kwargs): super().save(kwargs) -class TermDate(models.Model): +class TermDate(GuardedModel): start_date = models.DateTimeField(_("term start date")) class Meta: diff --git a/apps/meet_plan/schema/__init__.py b/apps/meet_plan/schema/__init__.py new file mode 100644 index 0000000..fb66056 --- /dev/null +++ b/apps/meet_plan/schema/__init__.py @@ -0,0 +1,17 @@ +from .query import ( + TermDateType, + MeetPlanType, + Query, +) +from .mutation import TermDateCreate, MeetPlanCreate, MeetPlanUpdate, MeetPlanDelete, Mutation + +__all__ = [ + "TermDateType", + "MeetPlanType", + "Query", + "TermDateCreate", + "MeetPlanCreate", + "MeetPlanUpdate", + "MeetPlanDelete", + "Mutation", +] diff --git a/apps/meet_plan/schema/mutation.py b/apps/meet_plan/schema/mutation.py new file mode 100644 index 0000000..de6ec47 --- /dev/null +++ b/apps/meet_plan/schema/mutation.py @@ -0,0 +1,150 @@ +import graphene +from django.core.exceptions import ValidationError +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ +from graphene_django_plus.exceptions import PermissionDenied +from graphene_django_plus.mutations import ModelCreateMutation, ModelUpdateMutation, ModelDeleteMutation +from guardian.shortcuts import assign_perm + +from apps.meet_plan.models import TermDate, MeetPlan + + +class TermDateCreate(ModelCreateMutation): + class Meta: + model = TermDate + permissions = ["meet_plan.add_termdate"] + only_fields = ["start_date"] + + +class MeetPlanCreate(ModelCreateMutation): + class Meta: + model = MeetPlan + only_fields = [ + "teacher", + "place", + "start_time", + "duration", + "t_message", + "student", + "s_message", + "complete", + ] + + @classmethod + def before_save(cls, info, instance, cleaned_input=None): + user = info.context.user + if user.is_admin: + pass + elif user.is_teacher: + if user.id != instance.teacher_id: + raise ValidationError({"teacher": _("You can only create your own meet plan.")}) + else: + if instance.student_id is None or user.id != instance.student_id: + raise ValidationError({"student": _("You can only create your own meet plan.")}) + if instance.start_time > timezone.now(): + raise ValidationError({"start_time": _("You can only create the previous plan.")}) + if instance.complete: + raise ValidationError( + {"complete": _("You can only create incomplete plan and ask the teacher to confirm it.")} + ) + + @classmethod + def after_save(cls, info, instance, cleaned_input=None): + assign_perm("meet_plan.change_meetplan", instance.teacher, instance) + assign_perm("meet_plan.delete_meetplan", instance.teacher, instance) + + +class MeetPlanUpdate(ModelUpdateMutation): + class Meta: + model = MeetPlan + + @classmethod + def clean_input(cls, info, instance, data): + cleaned_input = super().clean_input(info, instance, data) + user = info.context.user + if user.is_admin: + pass + elif user.is_teacher: + if "teacher" in cleaned_input and cleaned_input["teacher"].id != user.id: + # 教师只能修改自己的安排 + raise ValidationError({"teacher": _("You can not update the teacher field.")}) + else: + if ( + ("teacher" in cleaned_input and cleaned_input["teacher"].id != instance.teacher_id) + or ("start_time" in cleaned_input and cleaned_input["start_time"] != instance.start_time) + or ("place" in cleaned_input and cleaned_input["place"] != instance.place) + or ("duration" in cleaned_input and cleaned_input["duration"] != instance.duration) + or ("t_message" in cleaned_input and cleaned_input["t_message"] != instance.t_message) + ): + # 学生不能修改安排信息 + raise ValidationError({"meet_plan": _("You can not modify meet plan details.")}) + + if instance.student_id is not None and user.id != instance.student_id: + # 学生不能修改他人的预约信息 + raise ValidationError({"student": _("You can not modify this.")}) + + if instance.student_id is not None and "student" in cleaned_input and cleaned_input["student"] is None: + # 不允许学生自己取消预约 + raise ValidationError( + {"student": _("You can not delete your order, please connect the teacher using email or phone.")} + ) + + if ( + instance.student_id is not None + and "student" in cleaned_input + and user.id != cleaned_input["student"].id + ): + # 不允许学生将预约信息改成其他人 + raise ValidationError({"student": _("You can not change student field to other student.")}) + + if "student" in cleaned_input and user.id != cleaned_input["student"].id: + # 学生不能帮他人预约信息 + raise ValidationError({"student": _("You can not make order for other student.")}) + + if "complete" in cleaned_input and cleaned_input["complete"] and not instance.complete: + # 学生不能标记预约为已完成 + raise ValidationError({"complete": _("You can not change complete field.")}) + + return cleaned_input + + @classmethod + def before_save(cls, info, instance, cleaned_input=None): + user = info.context.user + if user.is_admin: + pass + elif user.is_teacher: + if not user.has_perm("meet_plan.change_meetplan", instance): + raise ValidationError({"teacher": _("You can only update your own meet plan.")}) + else: + if instance.start_time < timezone.now(): + # 学生不能修改之前的预约信息 + raise ValidationError({"start_time": _("You can not change previous plan.")}) + + +class MeetPlanDelete(ModelDeleteMutation): + class Meta: + model = MeetPlan + + @classmethod + def get_instance(cls, info, obj_id): + instance = super().get_instance(info, obj_id) + user = info.context.user + if user.is_admin: + pass + elif user.is_teacher: + if not user.has_perm("meet_plan.delete_meetplan", instance): + raise PermissionDenied() + else: + raise PermissionDenied() + if instance.student and instance.complete: + raise PermissionDenied(message=_("This plan should not be deleted as it is completed!")) + return instance + + +class Mutation(graphene.ObjectType): + # use create instead of update!!! + term_date_update = TermDateCreate.Field() + + meet_plan_create = MeetPlanCreate.Field() + meet_plan_update = MeetPlanUpdate.Field() + meet_plan_delete = MeetPlanDelete.Field() diff --git a/apps/meet_plan/schema/query.py b/apps/meet_plan/schema/query.py new file mode 100644 index 0000000..c98db34 --- /dev/null +++ b/apps/meet_plan/schema/query.py @@ -0,0 +1,106 @@ +import graphene +from graphene import relay +from graphene_django.filter import DjangoFilterConnectionField +from graphene_django_plus.types import ModelType +from graphql_jwt.exceptions import PermissionDenied + +from apps.meet_plan.models import MeetPlan, TermDate +from apps.pku_auth.meta import PKTypeMixin, AbstractMeta +from apps.user.schema import UserType + + +class TermDateType(ModelType): + class Meta(AbstractMeta): + model = TermDate + fields = ["start_date"] + allow_unauthenticated = True + + +class MeetPlanType(PKTypeMixin, ModelType): + class Meta(AbstractMeta): + model = MeetPlan + fields = [ + "teacher", + "place", + "start_time", + "duration", + "t_message", + # "available", + # "student", + # "s_message", + # "complete", + ] + filter_fields = { + "teacher__id": ["exact", "in"], + "start_time": ["lt", "gt"], + "duration": ["exact", "in", "gte", "lte"], + # TODO: make student__pku_id filter only for admin user to protect privacy + "student__pku_id": ["exact", "contains", "startswith"], + "complete": ["exact"], + } + + available = graphene.Boolean() + + @staticmethod + def resolve_available(parent, info): + return parent.is_available() + + student = graphene.Field(UserType) + + @staticmethod + def resolve_student(parent, info): + user = info.context.user + if user.is_admin: + return parent.student + if user.is_teacher and user.id == parent.teacher_id: + return parent.student + if user.id == parent.student_id: + return parent.student + raise PermissionDenied + + s_message = graphene.String() + + @staticmethod + def resolve_s_message(parent, info): + user = info.context.user + if user.is_admin: + return parent.s_message + if user.is_teacher and user.id == parent.teacher_id: + return parent.s_message + if user.id == parent.student_id: + return parent.s_message + raise PermissionDenied + + complete = graphene.Boolean() + + @staticmethod + def resolve_complete(parent, info): + user = info.context.user + if user.is_admin: + return parent.complete + if user.is_teacher and user.id == parent.teacher_id: + return parent.complete + if user.id == parent.student_id: + return parent.complete + raise PermissionDenied + + @classmethod + def get_queryset(cls, qs, info): + qs = super().get_queryset(qs, info) + user = info.context.user + if user.is_admin: + return qs + if user.is_teacher: + return qs.filter(teacher_id=user.id) + return qs + + +class Query(graphene.ObjectType): + term_date = graphene.Field(TermDateType) + + @staticmethod + def resolve_term_date(parent, info): + return TermDate.objects.last() + + meet_plan = relay.Node.Field(MeetPlanType) + meet_plans = DjangoFilterConnectionField(MeetPlanType) diff --git a/apps/meet_plan/tests.py b/apps/meet_plan/tests.py index fdbd40e..eb9c27b 100644 --- a/apps/meet_plan/tests.py +++ b/apps/meet_plan/tests.py @@ -1,12 +1,20 @@ +import json from datetime import timedelta from django.test import TestCase, Client from django.urls import reverse from django.utils import timezone from freezegun import freeze_time +from graphene_django.utils import GraphQLTestCase +from graphql_jwt.settings import jwt_settings +from graphql_jwt.shortcuts import get_token +from graphql_relay import to_global_id +from guardian.shortcuts import assign_perm from apps.meet_plan.models import MeetPlan, TermDate, get_start_date +from apps.meet_plan.schema import MeetPlanType from apps.user.models import User +from apps.user.schema import UserType class AdminTest(TestCase): @@ -123,3 +131,1692 @@ def test_save(self): self.assertTrue(mp.is_available()) with freeze_time(lambda: now + timedelta(minutes=1)): self.assertFalse(mp.is_available()) + + +class QueryApiTest(GraphQLTestCase): + @staticmethod + def get_headers(user): + return { + jwt_settings.JWT_AUTH_HEADER_NAME: f"{jwt_settings.JWT_AUTH_HEADER_PREFIX} {get_token(user)}", + } + + @classmethod + def setUpTestData(cls): + cls.admin = User.objects.create(pku_id="1999999999", name="admin", email="admin@pku.edu.cn", is_admin=True) + cls.student = User.objects.create( + pku_id="2000000000", + name="student", + email="student@pku.edu.cn", + ) + cls.student2 = User.objects.create( + pku_id="2000000001", + name="student2", + email="student2@pku.edu.cn", + ) + cls.teacher1 = User.objects.create( + pku_id="2000000002", + name="teacher", + email="teacher@pku.edu.cn", + address="teacher office", + is_teacher=True, + ) + cls.teacher2 = User.objects.create( + pku_id="2000000003", + name="teacher2", + email="teacher2@pku.edu.cn", + address="teacher2 office", + is_teacher=True, + ) + TermDate.objects.create(start_date=timezone.now()) + MeetPlan.objects.create( + teacher=cls.teacher1, + place=cls.teacher1.address, + start_time=timezone.now() + timedelta(hours=1), + duration=1, + student=cls.student, + complete=False, + ) + MeetPlan.objects.create( + teacher=cls.teacher2, + place=cls.teacher2.address, + start_time=timezone.now() + timedelta(hours=1), + duration=1, + student=cls.student2, + complete=True, + ) + MeetPlan.objects.create( + teacher=cls.teacher1, + place=cls.teacher1.address, + start_time=timezone.now() - timedelta(hours=1), + duration=2, + ) + MeetPlan.objects.create( + teacher=cls.teacher2, + place=cls.teacher2.address, + start_time=timezone.now() + timedelta(hours=1), + duration=3, + ) + + def test_term_date_without_token(self): + response = self.query( + """ + query{ + termDate{ + startDate + } + } + """ + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual(content["data"]["termDate"]["startDate"], get_start_date().isoformat()) + + def test_meet_plans_without_token(self): + response = self.query( + """ + { + meetPlans { + totalCount + edges { + node { + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + } + } + } + } + """ + ) + content = json.loads(response.content) + self.assertResponseHasErrors(response) + self.assertIsNone(content["data"]["meetPlans"]) + + def test_meet_plans_stu(self): + response = self.query( + """ + { + meetPlans { + totalCount + edges { + node { + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + } + } + } + } + """, + headers=self.get_headers(self.student), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual(content["data"]["meetPlans"]["totalCount"], 4) + self.assertFalse(content["data"]["meetPlans"]["edges"][0]["node"]["available"]) + self.assertFalse(content["data"]["meetPlans"]["edges"][1]["node"]["available"]) + self.assertFalse(content["data"]["meetPlans"]["edges"][2]["node"]["available"]) + self.assertTrue(content["data"]["meetPlans"]["edges"][3]["node"]["available"]) + + response = self.query( + """ + { + meetPlans { + totalCount + edges { + node { + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + name + } + sMessage + complete + } + } + } + } + """, + headers=self.get_headers(self.student), + ) + content = json.loads(response.content) + self.assertResponseHasErrors(response) + self.assertEqual(len(content["errors"]), 9) + self.assertIsNotNone(content["data"]["meetPlans"]["edges"][0]["node"]["student"]) + self.assertIsNone(content["data"]["meetPlans"]["edges"][1]["node"]["student"]) + self.assertIsNone(content["data"]["meetPlans"]["edges"][2]["node"]["student"]) + self.assertIsNone(content["data"]["meetPlans"]["edges"][3]["node"]["student"]) + + def test_meet_plans_tea(self): + query_stat = """ + { + meetPlans { + totalCount + edges { + node { + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + pkuId + name + } + sMessage + complete + } + } + } + } + """ + response = self.query(query_stat, headers=self.get_headers(self.teacher1)) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual(content["data"]["meetPlans"]["totalCount"], 2) + self.assertFalse(content["data"]["meetPlans"]["edges"][0]["node"]["available"]) + self.assertFalse(content["data"]["meetPlans"]["edges"][1]["node"]["available"]) + + response = self.query(query_stat, headers=self.get_headers(self.teacher2)) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual(content["data"]["meetPlans"]["totalCount"], 2) + self.assertFalse(content["data"]["meetPlans"]["edges"][0]["node"]["available"]) + self.assertTrue(content["data"]["meetPlans"]["edges"][1]["node"]["available"]) + + def test_meet_plans_admin(self): + query_stat = """ + { + meetPlans { + totalCount + edges { + node { + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + pkuId + name + dateJoined + } + sMessage + complete + } + } + } + } + """ + response = self.query(query_stat, headers=self.get_headers(self.admin)) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual(content["data"]["meetPlans"]["totalCount"], 4) + self.assertFalse(content["data"]["meetPlans"]["edges"][0]["node"]["available"]) + self.assertFalse(content["data"]["meetPlans"]["edges"][1]["node"]["available"]) + self.assertFalse(content["data"]["meetPlans"]["edges"][2]["node"]["available"]) + self.assertTrue(content["data"]["meetPlans"]["edges"][3]["node"]["available"]) + + def test_meet_plans_filter_teacher_id_exact(self): + for i in range(2, 7): + response = self.query( + """ + query myQuery($id: Float!){ + meetPlans(teacher_Id: $id) { + totalCount + edges { + node { + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + } + } + } + } + """, + headers=self.get_headers(self.student), + variables={"id": i}, + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual(content["data"]["meetPlans"]["totalCount"], MeetPlan.objects.filter(teacher_id=i).count()) + + def test_meet_plans_filter_teacher_id_in(self): + def test(id, count): + response = self.query( + """ + query myQuery($id: [String]){ + meetPlans(teacher_Id_In: $id) { + totalCount + edges { + node { + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + } + } + } + } + """, + headers=self.get_headers(self.student), + variables={"id": id}, + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual(content["data"]["meetPlans"]["totalCount"], count) + + test(["2"], MeetPlan.objects.filter(teacher_id__in=[2]).count()) + test(["3"], MeetPlan.objects.filter(teacher_id__in=[3]).count()) + test(["4"], MeetPlan.objects.filter(teacher_id__in=[4]).count()) + test(["5"], MeetPlan.objects.filter(teacher_id__in=[5]).count()) + test(["6"], MeetPlan.objects.filter(teacher_id__in=[6]).count()) + test(["2", "3"], MeetPlan.objects.filter(teacher_id__in=[2, 3]).count()) + test(["2", "5"], MeetPlan.objects.filter(teacher_id__in=[2, 5]).count()) + test(["6", "5"], MeetPlan.objects.filter(teacher_id__in=[6, 5]).count()) + + def test_meet_plans_filter_start_time_lt(self): + def test(time): + response = self.query( + """ + query myQuery($time: DateTime!){ + meetPlans(startTime_Lt: $time) { + totalCount + edges { + node { + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + } + } + } + } + """, + headers=self.get_headers(self.student), + variables={"time": time.isoformat()}, + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual( + content["data"]["meetPlans"]["totalCount"], MeetPlan.objects.filter(start_time__lt=time).count() + ) + + test(timezone.now()) + test(timezone.now() - timedelta(hours=2)) + test(get_start_date()) + + def test_meet_plans_filter_start_time_gt(self): + def test(time): + response = self.query( + """ + query myQuery($time: DateTime!){ + meetPlans(startTime_Gt: $time) { + totalCount + edges { + node { + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + } + } + } + } + """, + headers=self.get_headers(self.student), + variables={"time": time.isoformat()}, + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual( + content["data"]["meetPlans"]["totalCount"], MeetPlan.objects.filter(start_time__gt=time).count() + ) + + test(timezone.now()) + test(timezone.now() - timedelta(minutes=10)) + test(timezone.now() - timedelta(hours=2)) + test(get_start_date()) + + def test_meet_plans_filter_duration_exact(self): + def test(duration): + response = self.query( + """ + query myQuery($duration: String!){ + meetPlans(duration: $duration) { + totalCount + edges { + node { + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + } + } + } + } + """, + headers=self.get_headers(self.student), + variables={"duration": str(duration)}, + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual( + content["data"]["meetPlans"]["totalCount"], MeetPlan.objects.filter(duration__exact=duration).count() + ) + + test(1) + test(2) + test(3) + test(4) + + def test_meet_plans_filter_duration_in(self): + def test(duration): + response = self.query( + """ + query myQuery($duration: [String]){ + meetPlans(duration_In: $duration) { + totalCount + edges { + node { + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + } + } + } + } + """, + headers=self.get_headers(self.student), + variables={"duration": duration}, + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual( + content["data"]["meetPlans"]["totalCount"], MeetPlan.objects.filter(duration__in=duration).count() + ) + + test(["1"]) + test(["2"]) + test(["3"]) + test(["1", "4"]) + test(["1", "3"]) + test(["2", "3"]) + test(["3", "4"]) + + def test_meet_plans_filter_student_pku_id_exact(self): + def test(pku_id, user): + response = self.query( + """ + query myQuery($student_PkuId: String!){ + meetPlans(student_PkuId: $student_PkuId) { + totalCount + edges { + node { + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + pkuId + name + dateJoined + } + } + } + } + } + """, + headers=self.get_headers(user), + variables={"student_PkuId": pku_id}, + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual( + content["data"]["meetPlans"]["totalCount"], + MeetPlan.objects.filter(student__pku_id__exact=pku_id).count(), + ) + + # TODO: when make this filter only for admin, uncomment next line + # test(self.student.pku_id, self.student) + # test(self.student2.pku_id, self.student2) + # test(self.student2.pku_id, self.student) + test(self.student.pku_id, self.admin) + test(self.student2.pku_id, self.admin) + + def test_meet_plans_filter_student_pku_id_contains(self): + def test(pku_id, user): + response = self.query( + """ + query myQuery($student_PkuId: String!){ + meetPlans(student_PkuId_Contains: $student_PkuId) { + totalCount + edges { + node { + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + pkuId + name + dateJoined + } + } + } + } + } + """, + headers=self.get_headers(user), + variables={"student_PkuId": pku_id}, + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual( + content["data"]["meetPlans"]["totalCount"], + MeetPlan.objects.filter(student__pku_id__contains=pku_id).count(), + ) + + # TODO: when make this filter only for admin, uncomment next and fix them + # test("123123", self.student) + # test("123123", self.student2) + # test(self.student2.pku_id, self.student) + test("123123", self.admin) + test("20", self.admin) + test("000000000", self.admin) + test("000000001", self.admin) + + def test_meet_plans_filter_student_pku_id_startswith(self): + def test(pku_id, user): + response = self.query( + """ + query myQuery($student_PkuId: String!){ + meetPlans(student_PkuId_Contains: $student_PkuId) { + totalCount + edges { + node { + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + pkuId + name + dateJoined + } + } + } + } + } + """, + headers=self.get_headers(user), + variables={"student_PkuId": pku_id}, + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual( + content["data"]["meetPlans"]["totalCount"], + MeetPlan.objects.filter(student__pku_id__startswith=pku_id).count(), + ) + + # TODO: when make this filter only for admin, uncomment next and fix them + # test("123123", self.student) + # test("123123", self.student2) + # test(self.student2.pku_id, self.student) + test("123123", self.admin) + test("20", self.admin) + test("19", self.admin) + test("2000000000", self.admin) + + def test_meet_plans_filter_complete_exact(self): + def test(complete, user): + response = self.query( + """ + query myQuery($complete: Boolean!){ + meetPlans(complete: $complete) { + totalCount + edges { + node { + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + pkuId + name + dateJoined + } + complete + } + } + } + } + """, + headers=self.get_headers(user), + variables={"complete": complete}, + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual( + content["data"]["meetPlans"]["totalCount"], MeetPlan.objects.filter(complete__exact=complete).count() + ) + + test(True, self.admin) + test(False, self.admin) + + +class MutationApiTest(GraphQLTestCase): + @staticmethod + def get_headers(user): + return { + jwt_settings.JWT_AUTH_HEADER_NAME: f"{jwt_settings.JWT_AUTH_HEADER_PREFIX} {get_token(user)}", + } + + @classmethod + def setUpTestData(cls): + cls.admin = User.objects.create(pku_id="1999999999", name="admin", email="admin@pku.edu.cn", is_admin=True) + assign_perm("meet_plan.add_termdate", cls.admin) + cls.student = User.objects.create( + pku_id="2000000000", + name="student", + email="student@pku.edu.cn", + ) + cls.teacher = User.objects.create( + pku_id="2000000001", + name="teacher", + email="teacher@pku.edu.cn", + address="teacher office", + is_teacher=True, + ) + TermDate.objects.create(start_date=timezone.now()) + + def test_term_date_update(self): + response = self.query( + """ + query{ + termDate{ + startDate + } + } + """ + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual(content["data"]["termDate"]["startDate"], TermDate.objects.last().start_date.isoformat()) + now = timezone.now() + response = self.query( + """ + mutation myMutation($input: TermDateCreateInput!){ + termDateUpdate(input: $input){ + errors{ + field + message + } + clientMutationId + termDate { + id + startDate + } + } + } + """, + input_data={"clientMutationId": "without token", "startDate": now.isoformat()}, + ) + self.assertResponseNoErrors(response) + content = json.loads(response.content) + self.assertGreater(len(content["data"]["termDateUpdate"]["errors"]), 0) + + response = self.query( + """ + mutation myMutation($input: TermDateCreateInput!){ + termDateUpdate(input: $input){ + errors{ + field + message + } + clientMutationId + termDate { + id + startDate + } + } + } + """, + headers=self.get_headers(self.admin), + input_data={"clientMutationId": "with token", "startDate": now.isoformat()}, + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual(content["data"]["termDateUpdate"]["termDate"]["startDate"], now.isoformat()) + + def test_meet_plan_create_admin(self): + response = self.query( + """ + mutation myMutation($input: MeetPlanCreateInput!){ + meetPlanCreate(input: $input){ + errors { + field + message + } + clientMutationId + meetPlan{ + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + name + } + sMessage + complete + } + } + } + """, + input_data={ + "teacher": to_global_id(UserType._meta.name, str(self.teacher.id)), + "place": self.teacher.address, + "startTime": timezone.now().isoformat(), + "duration": 1, + "tMessage": "test meet plan", + }, + headers=self.get_headers(self.admin), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + mt = MeetPlan.objects.get(pk=content["data"]["meetPlanCreate"]["meetPlan"]["pk"]) + self.assertTrue(self.teacher.has_perms(["meet_plan.change_meetplan", "meet_plan.delete_meetplan"], mt)) + + def test_meet_plan_create_teacher(self): + teacher = User.objects.create( + pku_id="2000000002", + name="teacher2", + email="teacher2@pku.edu.cn", + address="teacher2 office", + is_teacher=True, + ) + response = self.query( + """ + mutation myMutation($input: MeetPlanCreateInput!){ + meetPlanCreate(input: $input){ + errors { + field + message + } + clientMutationId + meetPlan{ + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + name + } + sMessage + complete + } + } + } + """, + input_data={ + "teacher": to_global_id(UserType._meta.name, str(self.teacher.id)), + "place": self.teacher.address, + "startTime": timezone.now().isoformat(), + "duration": 1, + "tMessage": "test meet plan", + }, + headers=self.get_headers(self.teacher), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + mt = MeetPlan.objects.get(pk=content["data"]["meetPlanCreate"]["meetPlan"]["pk"]) + self.assertTrue(self.teacher.has_perms(["meet_plan.change_meetplan", "meet_plan.delete_meetplan"], mt)) + + response = self.query( + """ + mutation myMutation($input: MeetPlanCreateInput!){ + meetPlanCreate(input: $input){ + errors { + field + message + } + clientMutationId + meetPlan{ + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + name + } + sMessage + complete + } + } + } + """, + input_data={ + "teacher": to_global_id(UserType._meta.name, str(self.teacher.id)), + "place": self.teacher.address, + "startTime": timezone.now().isoformat(), + "duration": 1, + "tMessage": "test meet plan", + }, + headers=self.get_headers(teacher), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertNotEqual(content["data"]["meetPlanCreate"]["errors"], []) + + def test_meet_plan_create_student(self): + student = User.objects.create( + pku_id="2000000002", + name="student2", + email="student2@pku.edu.cn", + ) + response = self.query( + """ + mutation myMutation($input: MeetPlanCreateInput!){ + meetPlanCreate(input: $input){ + errors { + field + message + } + clientMutationId + meetPlan{ + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + name + } + sMessage + complete + } + } + } + """, + input_data={ + "teacher": to_global_id(UserType._meta.name, str(self.teacher.id)), + "place": self.teacher.address, + "startTime": timezone.now().isoformat(), + "duration": 1, + "tMessage": "test meet plan", + }, + headers=self.get_headers(self.student), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertNotEqual(content["data"]["meetPlanCreate"]["errors"], []) + + response = self.query( + """ + mutation myMutation($input: MeetPlanCreateInput!){ + meetPlanCreate(input: $input){ + errors { + field + message + } + clientMutationId + meetPlan{ + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + name + } + sMessage + complete + } + } + } + """, + input_data={ + "teacher": to_global_id(UserType._meta.name, str(self.teacher.id)), + "place": self.teacher.address, + "startTime": timezone.now().isoformat(), + "duration": 1, + "tMessage": "test meet plan", + "student": to_global_id(UserType._meta.name, str(student.id)), + }, + headers=self.get_headers(self.student), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertNotEqual(content["data"]["meetPlanCreate"]["errors"], []) + + response = self.query( + """ + mutation myMutation($input: MeetPlanCreateInput!){ + meetPlanCreate(input: $input){ + errors { + field + message + } + clientMutationId + meetPlan{ + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + name + } + sMessage + complete + } + } + } + """, + input_data={ + "teacher": to_global_id(UserType._meta.name, str(self.teacher.id)), + "place": self.teacher.address, + "startTime": (timezone.now() + timedelta(minutes=1)).isoformat(), + "duration": 1, + "tMessage": "test meet plan", + "student": to_global_id(UserType._meta.name, str(self.student.id)), + }, + headers=self.get_headers(self.student), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertNotEqual(content["data"]["meetPlanCreate"]["errors"], []) + + response = self.query( + """ + mutation myMutation($input: MeetPlanCreateInput!){ + meetPlanCreate(input: $input){ + errors { + field + message + } + clientMutationId + meetPlan{ + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + name + } + sMessage + complete + } + } + } + """, + input_data={ + "teacher": to_global_id(UserType._meta.name, str(self.teacher.id)), + "place": self.teacher.address, + "startTime": timezone.now().isoformat(), + "duration": 1, + "tMessage": "test meet plan", + "student": to_global_id(UserType._meta.name, str(self.student.id)), + "complete": True, + }, + headers=self.get_headers(self.student), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertNotEqual(content["data"]["meetPlanCreate"]["errors"], []) + + response = self.query( + """ + mutation myMutation($input: MeetPlanCreateInput!){ + meetPlanCreate(input: $input){ + errors { + field + message + } + clientMutationId + meetPlan{ + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + name + } + sMessage + complete + } + } + } + """, + input_data={ + "teacher": to_global_id(UserType._meta.name, str(self.teacher.id)), + "place": self.teacher.address, + "startTime": timezone.now().isoformat(), + "duration": 1, + "tMessage": "test meet plan", + "student": to_global_id(UserType._meta.name, str(self.student.id)), + }, + headers=self.get_headers(self.student), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual(content["data"]["meetPlanCreate"]["errors"], []) + mt = MeetPlan.objects.get(pk=content["data"]["meetPlanCreate"]["meetPlan"]["pk"]) + self.assertTrue(self.teacher.has_perms(["meet_plan.change_meetplan", "meet_plan.delete_meetplan"], mt)) + + def test_meet_plan_update_admin(self): + mt = MeetPlan.objects.create( + teacher=self.teacher, + place=self.teacher.address, + start_time=timezone.now(), + duration=1, + ) + response = self.query( + """ + mutation myMutation($input: MeetPlanUpdateInput!){ + meetPlanUpdate(input: $input){ + errors { + field + message + } + clientMutationId + meetPlan{ + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + name + } + sMessage + complete + } + } + } + """, + input_data={ + "id": to_global_id(MeetPlanType._meta.name, str(mt.id)), + "startTime": timezone.now().isoformat(), + "student": to_global_id(UserType._meta.name, str(self.student.id)), + }, + headers=self.get_headers(self.admin), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual(content["data"]["meetPlanUpdate"]["errors"], []) + + def test_meet_plan_update_teacher(self): + mt = MeetPlan.objects.create( + teacher=self.teacher, + place=self.teacher.address, + start_time=timezone.now(), + duration=1, + ) + assign_perm("meet_plan.change_meetplan", self.teacher, mt) + teacher = User.objects.create( + pku_id="2000000002", + name="teacher2", + email="teacher2@pku.edu.cn", + address="teacher2 office", + is_teacher=True, + ) + + query_str = """ + mutation myMutation($input: MeetPlanUpdateInput!){ + meetPlanUpdate(input: $input){ + errors { + field + message + } + clientMutationId + meetPlan{ + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + name + } + sMessage + complete + } + } + } + """ + + response = self.query( + query_str, + input_data={ + "id": to_global_id(MeetPlanType._meta.name, str(mt.id)), + "startTime": timezone.now().isoformat(), + "student": to_global_id(UserType._meta.name, str(self.student.id)), + "complete": True, + }, + headers=self.get_headers(self.teacher), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual(content["data"]["meetPlanUpdate"]["errors"], []) + + response = self.query( + query_str, + input_data={ + "id": to_global_id(MeetPlanType._meta.name, str(mt.id)), + "teacher": to_global_id(UserType._meta.name, str(teacher.id)), + "startTime": timezone.now().isoformat(), + "student": to_global_id(UserType._meta.name, str(self.student.id)), + "complete": True, + }, + headers=self.get_headers(self.teacher), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertNotEqual(content["data"]["meetPlanUpdate"]["errors"], []) + + response = self.query( + query_str, + input_data={ + "id": to_global_id(MeetPlanType._meta.name, str(mt.id)), + "startTime": timezone.now().isoformat(), + "student": to_global_id(UserType._meta.name, str(self.student.id)), + "complete": True, + }, + headers=self.get_headers(teacher), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertNotEqual(content["data"]["meetPlanUpdate"]["errors"], []) + + def test_meet_plan_update_student(self): + mt = MeetPlan.objects.create( + teacher=self.teacher, + place=self.teacher.address, + start_time=timezone.now() + timedelta(hours=1), + duration=1, + ) + student = User.objects.create( + pku_id="2000000002", + name="student2", + email="student2@pku.edu.cn", + ) + + query_str = """ + mutation myMutation($input: MeetPlanUpdateInput!){ + meetPlanUpdate(input: $input){ + errors { + field + message + } + clientMutationId + meetPlan{ + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + name + } + sMessage + complete + } + } + } + """ + + # 安排未被选取时的测试逻辑 + response = self.query( + query_str, + input_data={ + "id": to_global_id(MeetPlanType._meta.name, str(mt.id)), + "teacher": to_global_id(UserType._meta.name, str(student.id)), + "student": to_global_id(UserType._meta.name, str(self.student.id)), + }, + headers=self.get_headers(self.student), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertNotEqual(content["data"]["meetPlanUpdate"]["errors"], []) + + response = self.query( + query_str, + input_data={ + "id": to_global_id(MeetPlanType._meta.name, str(mt.id)), + "startTime": timezone.now().isoformat(), + "student": to_global_id(UserType._meta.name, str(self.student.id)), + }, + headers=self.get_headers(self.student), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertNotEqual(content["data"]["meetPlanUpdate"]["errors"], []) + + response = self.query( + query_str, + input_data={ + "id": to_global_id(MeetPlanType._meta.name, str(mt.id)), + "place": self.student.address, + "student": to_global_id(UserType._meta.name, str(self.student.id)), + }, + headers=self.get_headers(self.student), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertNotEqual(content["data"]["meetPlanUpdate"]["errors"], []) + + response = self.query( + query_str, + input_data={ + "id": to_global_id(MeetPlanType._meta.name, str(mt.id)), + "duration": 2, + "student": to_global_id(UserType._meta.name, str(self.student.id)), + }, + headers=self.get_headers(self.student), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertNotEqual(content["data"]["meetPlanUpdate"]["errors"], []) + + response = self.query( + query_str, + input_data={ + "id": to_global_id(MeetPlanType._meta.name, str(mt.id)), + "tMessage": "test hhhh", + "student": to_global_id(UserType._meta.name, str(self.student.id)), + }, + headers=self.get_headers(self.student), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertNotEqual(content["data"]["meetPlanUpdate"]["errors"], []) + + response = self.query( + query_str, + input_data={ + "id": to_global_id(MeetPlanType._meta.name, str(mt.id)), + "student": to_global_id(UserType._meta.name, str(self.student.id)), + }, + headers=self.get_headers(student), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertNotEqual(content["data"]["meetPlanUpdate"]["errors"], []) + + mt.start_time = timezone.now() + mt.save() + + response = self.query( + query_str, + input_data={ + "id": to_global_id(MeetPlanType._meta.name, str(mt.id)), + "student": to_global_id(UserType._meta.name, str(self.student.id)), + }, + headers=self.get_headers(self.student), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertNotEqual(content["data"]["meetPlanUpdate"]["errors"], []) + + mt.start_time = timezone.now() + timedelta(hours=1) + mt.student = self.student + mt.save() + + response = self.query( + query_str, + input_data={ + "id": to_global_id(MeetPlanType._meta.name, str(mt.id)), + "student": to_global_id(UserType._meta.name, str(student.id)), + "sMessage": "test hhh", + }, + headers=self.get_headers(self.student), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertNotEqual(content["data"]["meetPlanUpdate"]["errors"], []) + + response = self.query( + query_str, + input_data={ + "id": to_global_id(MeetPlanType._meta.name, str(mt.id)), + "student": None, + "sMessage": "test hhh", + }, + headers=self.get_headers(self.student), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertNotEqual(content["data"]["meetPlanUpdate"]["errors"], []) + + response = self.query( + query_str, + input_data={"id": to_global_id(MeetPlanType._meta.name, str(mt.id)), "sMessage": "test hhh"}, + headers=self.get_headers(student), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertNotEqual(content["data"]["meetPlanUpdate"]["errors"], []) + + response = self.query( + query_str, + input_data={"id": to_global_id(MeetPlanType._meta.name, str(mt.id)), "complete": True}, + headers=self.get_headers(self.student), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertNotEqual(content["data"]["meetPlanUpdate"]["errors"], []) + + mt.student = None + mt.save() + + response = self.query( + query_str, + input_data={ + "id": to_global_id(MeetPlanType._meta.name, str(mt.id)), + "student": to_global_id(UserType._meta.name, str(self.student.id)), + }, + headers=self.get_headers(self.student), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual(content["data"]["meetPlanUpdate"]["errors"], []) + + response = self.query( + query_str, + input_data={ + "id": to_global_id(MeetPlanType._meta.name, str(mt.id)), + "student": to_global_id(UserType._meta.name, str(self.student.id)), + "sMessage": "test", + }, + headers=self.get_headers(self.student), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual(content["data"]["meetPlanUpdate"]["errors"], []) + + def test_meet_plan_delete_admin(self): + mt = MeetPlan.objects.create( + teacher=self.teacher, + place=self.teacher.address, + start_time=timezone.now() + timedelta(hours=1), + duration=1, + student=self.student, + complete=True, + ) + + query_str = """ + mutation myMutation($input: MeetPlanDeleteInput!){ + meetPlanDelete(input: $input){ + errors { + field + message + } + clientMutationId + meetPlan{ + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + name + } + sMessage + complete + } + } + } + """ + + response = self.query( + query_str, + input_data={"id": to_global_id(MeetPlanType._meta.name, str(mt.id))}, + headers=self.get_headers(self.admin), + ) + self.assertResponseNoErrors(response) + content = json.loads(response.content) + self.assertGreater(len(content["data"]["meetPlanDelete"]["errors"]), 0) + self.assertEqual(MeetPlan.objects.all().count(), 1) + + mt.student = None + mt.save() + + response = self.query( + query_str, + input_data={"id": to_global_id(MeetPlanType._meta.name, str(mt.id))}, + headers=self.get_headers(self.admin), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual(content["data"]["meetPlanDelete"]["errors"], []) + self.assertEqual(MeetPlan.objects.all().count(), 0) + + def test_meet_plan_delete_teacher(self): + mt = MeetPlan.objects.create( + teacher=self.teacher, + place=self.teacher.address, + start_time=timezone.now() + timedelta(hours=1), + duration=1, + student=self.student, + complete=True, + ) + + query_str = """ + mutation myMutation($input: MeetPlanDeleteInput!){ + meetPlanDelete(input: $input){ + errors { + field + message + } + clientMutationId + meetPlan{ + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + name + } + sMessage + complete + } + } + } + """ + + response = self.query( + query_str, + input_data={"id": to_global_id(MeetPlanType._meta.name, str(mt.id))}, + headers=self.get_headers(self.teacher), + ) + self.assertResponseNoErrors(response) + content = json.loads(response.content) + self.assertGreater(len(content["data"]["meetPlanDelete"]["errors"]), 0) + self.assertEqual(MeetPlan.objects.all().count(), 1) + + mt.student = None + mt.save() + + response = self.query( + query_str, + input_data={"id": to_global_id(MeetPlanType._meta.name, str(mt.id))}, + headers=self.get_headers(self.teacher), + ) + self.assertResponseNoErrors(response) + content = json.loads(response.content) + self.assertGreater(len(content["data"]["meetPlanDelete"]["errors"]), 0) + self.assertEqual(MeetPlan.objects.all().count(), 1) + + assign_perm("meet_plan.delete_meetplan", self.teacher, mt) + + response = self.query( + query_str, + input_data={"id": to_global_id(MeetPlanType._meta.name, str(mt.id))}, + headers=self.get_headers(self.teacher), + ) + self.assertResponseNoErrors(response) + self.assertEqual(MeetPlan.objects.all().count(), 0) + + def test_meet_plan_delete_student(self): + mt = MeetPlan.objects.create( + teacher=self.teacher, + place=self.teacher.address, + start_time=timezone.now() + timedelta(hours=1), + duration=1, + student=self.student, + complete=True, + ) + + query_str = """ + mutation myMutation($input: MeetPlanDeleteInput!){ + meetPlanDelete(input: $input){ + errors { + field + message + } + clientMutationId + meetPlan{ + id + pk + teacher { + id + name + } + place + startTime + duration + tMessage + available + student { + id + name + } + sMessage + complete + } + } + } + """ + response = self.query( + query_str, + input_data={"id": to_global_id(MeetPlanType._meta.name, str(mt.id))}, + headers=self.get_headers(self.student), + ) + self.assertResponseNoErrors(response) + content = json.loads(response.content) + self.assertGreater(len(content["data"]["meetPlanDelete"]["errors"]), 0) + self.assertEqual(MeetPlan.objects.all().count(), 1) diff --git a/apps/pku_auth/schema/__init__.py b/apps/pku_auth/schema/__init__.py new file mode 100644 index 0000000..c973cf6 --- /dev/null +++ b/apps/pku_auth/schema/__init__.py @@ -0,0 +1,25 @@ +from .query import ( + OpenIDClientType, + Query, +) +from .mutation import ( + token_auth, + ObtainJSONWebToken, + Verify, + Refresh, + Revoke, + RevokeAll, + Mutation, +) + +__all__ = [ + "OpenIDClientType", + "Query", + "token_auth", + "ObtainJSONWebToken", + "Verify", + "Refresh", + "Revoke", + "RevokeAll", + "Mutation", +] diff --git a/apps/pku_auth/schema/mutation.py b/apps/pku_auth/schema/mutation.py new file mode 100644 index 0000000..b0a038a --- /dev/null +++ b/apps/pku_auth/schema/mutation.py @@ -0,0 +1,125 @@ +from calendar import timegm +from functools import wraps + +import graphene +import graphql_jwt +from django.contrib.auth import authenticate, user_logged_in +from django.utils.translation import gettext as _ +from graphene.utils.thenables import maybe_thenable +from graphql_jwt import exceptions, signals +from graphql_jwt.decorators import setup_jwt_cookie, csrf_rotation, refresh_expiration, on_token_auth_resolve +from graphql_jwt.mixins import ObtainJSONWebTokenMixin +from graphql_jwt.refresh_token.decorators import ensure_refresh_token +from graphql_jwt.refresh_token.shortcuts import get_refresh_token +from graphql_jwt.refresh_token.utils import get_refresh_token_model + +from apps.user.schema import UserType + + +def token_auth(f): + @wraps(f) + @setup_jwt_cookie + @csrf_rotation + @refresh_expiration + def wrapper(cls, root, info, code, **kwargs): + context = info.context + context._jwt_token_auth = True + + user = authenticate( + request=context, + code=code, + ) + if user is None: + raise exceptions.JSONWebTokenError( + _("Please enter valid credentials"), + ) + + if hasattr(context, "user"): + context.user = user + + result = f(cls, root, info, **kwargs) + signals.token_issued.send(sender=cls, request=context, user=user) + user_logged_in.send(sender=cls, user=user) + return maybe_thenable((context, user, result), on_token_auth_resolve) + + return wrapper + + +class ObtainJSONWebToken(ObtainJSONWebTokenMixin, graphene.Mutation): + """ + Use this for login user in.\n + Then add a option in http header:\n + \tAuthorization: JWT \n + payload include user.ID & token expire timestamp + """ + + user = graphene.Field(UserType) + + @classmethod + def resolve(cls, root, info, **kwargs): + return cls(user=info.context.user) + + @classmethod + def Field(cls, *args, **kwargs): + cls._meta.arguments.update( + { + "code": graphene.String(required=True), + } + ) + return super().Field(*args, **kwargs) + + @classmethod + @token_auth + def mutate(cls, root, info, **kwargs): + return cls.resolve(root, info, **kwargs) + + +class Verify(graphql_jwt.Verify): + """ + Use this to get payload from token.\n + """ + + +class Refresh(graphql_jwt.Refresh): + """ + Use this to get new token with refreshToken. + """ + + +class Revoke(graphql_jwt.Revoke): + """ + Use this to revoke fresh token. + """ + + +class RevokeAll(graphene.Mutation): + """ + Use this to revoke all fresh tokens issued to the user. + """ + + class Arguments: + refresh_token = graphene.String() + + revoked = graphene.Int(required=True) + + @classmethod + @ensure_refresh_token + def revoke(cls, root, info, refresh_token, **kwargs): + context = info.context + refresh_token_obj = get_refresh_token(refresh_token, context) + refresh_token_objs = get_refresh_token_model().objects.filter(user=refresh_token_obj.user) + for refresh_token_obj in refresh_token_objs: + refresh_token_obj.revoke(context) + return cls(revoked=timegm(refresh_token_obj.revoked.timetuple())) + + @classmethod + def mutate(cls, *args, **kwargs): + return cls.revoke(*args, **kwargs) + + +class Mutation(graphene.ObjectType): + code_auth = ObtainJSONWebToken.Field() + verify_token = Verify.Field() + refresh_token = Refresh.Field() + revoke_token = Revoke.Field() + revoke_token_all = RevokeAll.Field() diff --git a/apps/pku_auth/schema/query.py b/apps/pku_auth/schema/query.py new file mode 100644 index 0000000..2b3c24b --- /dev/null +++ b/apps/pku_auth/schema/query.py @@ -0,0 +1,32 @@ +import graphene +from graphene_django_plus.types import ModelType + +from apps.pku_auth.meta import AbstractMeta, FieldWithDocs +from apps.pku_auth.models import OpenIDClient + + +class OpenIDClientType(ModelType): + """ + Offer enough information for frontend to build redirect auth url.\n + ``https://{authorization_endpoint}?response_type=code&client_id={clientId} + &scope={scopes}&redirect_uri={redirectUri}[&state={some character}]`` + """ + + class Meta(AbstractMeta): + model = OpenIDClient + fields = ["authorization_endpoint", "client_id", "redirect_uri", "scopes"] + allow_unauthenticated = True + + +class Query(graphene.ObjectType): + openid_client = FieldWithDocs(OpenIDClientType) + + @staticmethod + def resolve_openid_client(root, info): + """ + TODO: when https://github.com/tfoxy/graphene-django-optimizer release support graphene v3 + try this + import graphene_django_optimizer as gql_optimizer + return gql_optimizer.query(OpenIDClient.objects.last(), info) + """ + return OpenIDClient.objects.last() diff --git a/apps/pku_auth/tests.py b/apps/pku_auth/tests.py index fdfa540..e093572 100644 --- a/apps/pku_auth/tests.py +++ b/apps/pku_auth/tests.py @@ -1,6 +1,15 @@ +import json +from datetime import timedelta from unittest import mock -from django.test import TestCase +from django.test import TestCase, override_settings +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ +from freezegun import freeze_time +from graphene_django.utils.testing import GraphQLTestCase +from graphql_jwt.settings import jwt_settings +from graphql_jwt.shortcuts import get_token +from graphql_jwt.utils import get_payload from apps.pku_auth.backends import OpenIDClientBackend from apps.pku_auth.models import OpenIDClient @@ -116,7 +125,7 @@ def authenticate(self, request, code, **kwargs): return User.objects.get(pku_id=code) -class SignalTest(TestCase): +class SignalTest(GraphQLTestCase): @classmethod def setUpTestData(cls): cls.user = User.objects.create(pku_id="2000000000") @@ -138,9 +147,148 @@ def test_user_create_signal_triggered(self, signal): self.assertTrue(signal.called) self.assertEqual(signal.call_count, 1) - # @override_settings(AUTHENTICATION_BACKENDS=["apps.pku_auth.tests.TestBackend"]) - # def test_user_logged_in_signal_triggered(self): - # - # content = json.loads(response.content) - # self.assertResponseNoErrors(response) - # self.assertIsNotNone(content["data"]["codeAuth"]["user"]["lastLogin"]) + @override_settings(AUTHENTICATION_BACKENDS=["apps.pku_auth.tests.TestBackend"]) + def test_user_logged_in_signal_triggered(self): + response = self.query( + """ + mutation myMutation($input: String!) { + codeAuth(code: $input) { + token + user { + lastLogin + } + refreshToken + } + } + """, + operation_name="myMutation", + variables={"input": "2000000000"}, + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertIsNotNone(content["data"]["codeAuth"]["user"]["lastLogin"]) + + +class ApiTest(GraphQLTestCase): + @classmethod + def setUpTestData(cls): + OpenIDClient.objects.create( + client_id="id1", + client_secret="password1", + authorization_endpoint="http://some.com/1", + token_endpoint="http://some.com/1", + userinfo_endpoint="http://some.com/1", + redirect_uri="http://localhost/1", + scopes="openid profile email", + ) + OpenIDClient.objects.create( + client_id="123", + client_secret="password", + authorization_endpoint="http://some.com/", + token_endpoint="http://some.com/", + userinfo_endpoint="http://some.com/", + redirect_uri="http://localhost/", + scopes="openid profile", + ) + cls.user = User.objects.create(pku_id="2000000000") + + def test_openid_client(self): + response = self.query( + """ + query{ + openidClient{ + id + clientId + authorizationEndpoint + redirectUri + scopes + } + } + """ + ) + + content = json.loads(response.content) + + # This validates the status code and if you get errors + self.assertResponseNoErrors(response) + self.assertIsNotNone(content["data"]) + data = content["data"] + self.assertIsNotNone(data["openidClient"]) + openid_client = data["openidClient"] + self.assertEqual(openid_client["clientId"], "123") + self.assertEqual(openid_client["authorizationEndpoint"], "http://some.com/") + self.assertEqual(openid_client["redirectUri"], "http://localhost/") + self.assertEqual(openid_client["scopes"], "openid profile") + + @override_settings(AUTHENTICATION_BACKENDS=["apps.pku_auth.tests.TestBackend"]) + def test_code_auth(self): + response = self.query( + """ + mutation myMutation($input: String!) { + codeAuth(code: $input) { + token + payload + user { + pkuId + } + refreshToken + refreshExpiresIn + } + } + """, + operation_name="myMutation", + variables={"input": "2000000000"}, + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertIsNotNone(content["data"]) + data = content["data"] + self.assertIsNotNone(data["codeAuth"]) + code_auth = data["codeAuth"] + self.assertEqual(code_auth["payload"]["pku_id"], "2000000000") + self.assertEqual(code_auth["payload"], get_payload(token=code_auth["token"])) + + +class ApiTestWithJWT(GraphQLTestCase): + @classmethod + def setUp(cls): + cls.user = User.objects.create(pku_id="2000000000") + + def test_verify_token(self): + response = self.query( + """ + mutation myMutation($token: String!) { + verifyToken(token: $token) { + payload + } + } + """, + variables={"token": get_token(self.user)}, + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertIsNotNone(content["data"]) + data = content["data"] + self.assertIsNotNone(data["verifyToken"]) + verify_token = data["verifyToken"] + self.assertIsNotNone(verify_token["payload"]) + payload = verify_token["payload"] + self.assertEqual(payload["pku_id"], self.user.pku_id) + + def test_verify_token_expired(self): + token = get_token(self.user) + with freeze_time(lambda: timezone.now() + jwt_settings.JWT_EXPIRATION_DELTA + timedelta(seconds=1)): + response = self.query( + """ + mutation myMutation($token: String!) { + verifyToken(token: $token) { + payload + } + } + """, + variables={"token": token}, + ) + content = json.loads(response.content) + self.assertResponseHasErrors(response) + self.assertIsNone(content["data"]["verifyToken"]) + self.assertEqual(content["errors"][0]["message"], _("Signature has expired")) diff --git a/apps/user/schema/__init__.py b/apps/user/schema/__init__.py new file mode 100644 index 0000000..c84ba0b --- /dev/null +++ b/apps/user/schema/__init__.py @@ -0,0 +1,29 @@ +from .query import ( + DepartmentType, + UserType, + Query, +) +from .mutation import ( + DepartmentCreate, + DepartmentUpdate, + DepartmentDelete, + MeMutation, + UserCreate, + UserUpdate, + UserDelete, + Mutation, +) + +__all__ = [ + "DepartmentType", + "UserType", + "Query", + "DepartmentCreate", + "DepartmentUpdate", + "DepartmentDelete", + "MeMutation", + "UserCreate", + "UserUpdate", + "UserDelete", + "Mutation", +] diff --git a/apps/user/schema/mutation.py b/apps/user/schema/mutation.py new file mode 100644 index 0000000..c8f27d2 --- /dev/null +++ b/apps/user/schema/mutation.py @@ -0,0 +1,113 @@ +import graphene +from django.utils.translation import gettext_lazy as _ +from graphene_django_plus.mutations import ModelCreateMutation, ModelUpdateMutation, ModelDeleteMutation +from graphql_jwt.exceptions import PermissionDenied + +from apps.user.models import Department, User + + +class DepartmentCreate(ModelCreateMutation): + class Meta: + model = Department + permissions = ["user.add_department"] + only_fields = ["department"] + + +class DepartmentUpdate(ModelUpdateMutation): + class Meta: + model = Department + permissions = ["user.change_department"] + only_fields = ["department"] + + +class DepartmentDelete(ModelDeleteMutation): + class Meta: + model = Department + permissions = ["user.delete_department"] + + +class MeMutation(ModelUpdateMutation): + class Meta: + model = User + object_permissions = [ + "user.change_user", + ] + only_fields = [ + "name", + "email", + "website", + "phone_number", + "address", + "department", + "introduce", + ] + + @classmethod + def get_instance(cls, info, obj_id): + instance = super().get_instance(info, obj_id) + if instance != info.context.user: + # 双保险 + raise PermissionDenied(_("This api only allow update yourself.")) + return instance + + +class UserCreate(ModelCreateMutation): + class Meta: + model = User + permissions = ["user.add_user"] + only_fields = [ + "pku_id", + "name", + "email", + "website", + "phone_number", + "address", + "introduce", + "department", + "is_teacher", + "is_admin", + "is_active", + "groups", + "user_permissions", + ] + exclude_fields = ["password"] + + +class UserUpdate(ModelUpdateMutation): + class Meta: + model = User + permissions = ["user.change_user"] + only_fields = [ + "pku_id", + "name", + "email", + "website", + "phone_number", + "address", + "introduce", + "department", + "is_teacher", + "is_admin", + "is_active", + "groups", + "user_permissions", + ] + exclude_fields = ["password"] + + +class UserDelete(ModelDeleteMutation): + class Meta: + model = User + permissions = ["user.delete_user"] + + +class Mutation(graphene.ObjectType): + me = MeMutation.Field() + + department_create = DepartmentCreate.Field() + department_update = DepartmentUpdate.Field() + department_delete = DepartmentDelete.Field() + + user_create = UserCreate.Field() + user_update = UserUpdate.Field() + user_delete = UserDelete.Field() diff --git a/apps/user/schema/query.py b/apps/user/schema/query.py new file mode 100644 index 0000000..15d9307 --- /dev/null +++ b/apps/user/schema/query.py @@ -0,0 +1,113 @@ +import graphene +from django.utils.translation import gettext_lazy as _ +from graphene import relay +from graphene_django.filter import DjangoFilterConnectionField +from graphene_django_plus.types import ModelType +from graphql_jwt.exceptions import PermissionDenied + +from apps.pku_auth.meta import AbstractMeta, PKTypeMixin +from apps.user.models import User, Department + + +class DepartmentType(PKTypeMixin, ModelType): + class Meta(AbstractMeta): + model = Department + fields = ["id", "pk", "department", "user_set"] + filter_fields = {"id": ["exact", "in"], "department": ["icontains"]} + allow_unauthenticated = True + + +class UserType(PKTypeMixin, ModelType): + class Meta(AbstractMeta): + model = User + fields = [ + "id", + "pk", + # 'pku_id', + "name", + "email", + "website", + "phone_number", + "address", + "is_teacher", + "department", + "introduce", + "is_admin", + # 'is_active', + # 'date_joined', + # 'last_login', + ] + filter_fields = { + "pku_id": ["exact", "contains", "startswith"], + "name": ["icontains"], + "department__id": ["exact", "in"], + "department__department": ["icontains"], + "is_teacher": ["exact"], + "is_admin": ["exact"], + "is_active": ["exact"], + } + + pku_id = graphene.String(description=_("Only allow user query himself or teacher query student on this field.")) + + @staticmethod + def resolve_pku_id(parent, info): + if info.context.user.is_admin: + return parent.pku_id + if info.context.user.is_teacher: + if not parent.is_teacher: + return parent.pku_id + if info.context.user.id == parent.id: + return parent.pku_id + raise PermissionDenied + + is_active = graphene.Boolean(description=_("Only allow user query himself on this field.")) + + @staticmethod + def resolve_is_active(parent, info): + if info.context.user.is_admin: + return parent.is_active + if info.context.user.id == parent.id: + return parent.is_active + raise PermissionDenied + + date_joined = graphene.DateTime(description=_("Only allow user query himself on this field.")) + + @staticmethod + def resolve_date_joined(parent, info): + if info.context.user.is_admin: + return parent.date_joined + if info.context.user.id == parent.id: + return parent.date_joined + raise PermissionDenied + + last_login = graphene.DateTime(description=_("Only allow user query himself on this field.")) + + @staticmethod + def resolve_last_login(parent, info): + if info.context.user.is_admin: + return parent.last_login + if info.context.user.id == parent.id: + return parent.last_login + raise PermissionDenied + + @classmethod + def get_queryset(cls, qs, info): + if info.context.user.is_authenticated: + return super().get_queryset(qs, info) + return User.objects.none() + + +class Query(graphene.ObjectType): + me = graphene.Field(UserType) + + @staticmethod + def resolve_me(parent, info): + if info.context.user.is_authenticated: + return info.context.user + return None + + department = relay.Node.Field(DepartmentType) + departments = DjangoFilterConnectionField(DepartmentType) + + user = relay.Node.Field(UserType) + users = DjangoFilterConnectionField(UserType) diff --git a/apps/user/tests.py b/apps/user/tests.py index 6fb026a..3e44396 100644 --- a/apps/user/tests.py +++ b/apps/user/tests.py @@ -1,11 +1,18 @@ +import json from io import StringIO from unittest import mock from django.core.management import call_command from django.test import TestCase +from graphene_django.utils.testing import GraphQLTestCase +from graphql_jwt.settings import jwt_settings +from graphql_jwt.shortcuts import get_token +from graphql_relay import to_global_id +from guardian.shortcuts import assign_perm from apps.pku_auth.signals import user_create from apps.user.models import User, Department +from apps.user.schema import DepartmentType, UserType class ModelTest(TestCase): @@ -42,3 +49,1881 @@ def setUpTestData(cls): def test_signal_callback(self): self.assertTrue(self.user.has_perm("change_user", self.user)) + + +class QueryApiTest(GraphQLTestCase): + @classmethod + def setUpTestData(cls): + cls.department1 = Department.objects.create(department="student") + cls.department2 = Department.objects.create(department="teacher") + cls.department3 = Department.objects.create(department="admin") + cls.student = User.objects.create( + pku_id="2000000000", + name="student", + email="student@pku.edu.cn", + website="https://www.pku.edu.cn", + phone_number="123456789", + address="student office", + department=cls.department1, + introduce="student introduce", + is_teacher=False, + is_admin=False, + is_active=True, + ) + cls.teacher = User.objects.create( + pku_id="2000000001", + name="teacher", + email="teacher@pku.edu.cn", + website="https://www.pku.edu.cn", + phone_number="123456789", + address="teacher office", + department=cls.department2, + introduce="teacher introduce", + is_teacher=True, + is_admin=False, + is_active=True, + ) + cls.s_admin = User.objects.create( + pku_id="2000000002", + name="s_admin", + email="s_admin@pku.edu.cn", + website="https://www.pku.edu.cn", + phone_number="123456789", + address="s_admin office", + department=cls.department1, + introduce="s_admin introduce", + is_teacher=False, + is_admin=True, + is_active=True, + ) + cls.t_admin = User.objects.create( + pku_id="2000000003", + name="t_admin", + email="t_admin@pku.edu.cn", + website="https://www.pku.edu.cn", + phone_number="123456789", + address="t_admin office", + department=cls.department3, + introduce="t_admin introduce", + is_teacher=True, + is_admin=True, + is_active=True, + ) + cls.users = [cls.student, cls.teacher, cls.s_admin, cls.t_admin] + + @staticmethod + def get_headers(user): + return { + jwt_settings.JWT_AUTH_HEADER_NAME: f"{jwt_settings.JWT_AUTH_HEADER_PREFIX} {get_token(user)}", + } + + def test_query_me_without_token(self): + response = self.query( + """ + query{ + me{ + id + pkuId + lastLogin + dateJoined + } + } + """ + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertIsNotNone(content["data"]) + data = content["data"] + self.assertIsNone(data["me"]) + + def test_query_me(self): + for user in self.users: + department = user.department + response = self.query( + """ + query{ + me{ + id + pkuId + name + email + website + department { + department + } + phoneNumber + introduce + address + isActive + isTeacher + isAdmin + lastLogin + dateJoined + } + } + """, + headers=self.get_headers(user=user), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertIsNotNone(content["data"]) + data = content["data"] + self.assertIsNotNone(data["me"]) + me = data["me"] + self.assertEqual(me["pkuId"], user.pku_id) + self.assertEqual(me["name"], user.name) + self.assertEqual(me["email"], user.email) + self.assertEqual(me["website"], user.website) + self.assertEqual(me["phoneNumber"], user.phone_number) + self.assertEqual(me["introduce"], user.introduce) + self.assertEqual(me["address"], user.address) + self.assertEqual(me["isActive"], user.is_active) + self.assertEqual(me["isTeacher"], user.is_teacher) + self.assertEqual(me["isAdmin"], user.is_admin) + self.assertIsNone(me["lastLogin"]) + self.assertIsNotNone(me["dateJoined"]) + self.assertIsNotNone(me["department"]) + self.assertEqual(me["department"]["department"], department.department) + + def test_departments_without_token(self): + response = self.query( + """ + query{ + departments{ + totalCount + edges{ + node{ + id + department + userSet { + edges { + node { + id + } + } + } + } + } + } + } + """ + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertIsNotNone(content["data"]) + data = content["data"] + self.assertIsNotNone(data["departments"]) + departments = data["departments"] + self.assertEqual(departments["totalCount"], 3) + edges = departments["edges"] + for edge in edges: + node = edge["node"] + self.assertIsNotNone(node) + self.assertIsNotNone(node["department"]) + self.assertIsNotNone(node["userSet"]) + self.assertEqual(len(node["userSet"]["edges"]), 0) + + def test_departments(self): + for user in self.users: + response = self.query( + """ + query{ + departments{ + totalCount + edges{ + node{ + id + department + userSet { + totalCount + edges { + node { + id + name + department { + department + } + } + } + } + } + } + } + } + """, + headers=self.get_headers(user), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertIsNotNone(content["data"]) + data = content["data"] + self.assertIsNotNone(data["departments"]) + departments = data["departments"] + self.assertEqual(departments["totalCount"], 3) + edges = departments["edges"] + for edge in edges: + node = edge["node"] + self.assertIsNotNone(node) + self.assertIsNotNone(node["department"]) + department = Department.objects.get(department=node["department"]) + self.assertIsNotNone(node["userSet"]) + user_set = node["userSet"] + self.assertEqual( + user_set["totalCount"], + len(User.objects.filter(department=department)), + ) + user_set = user_set["edges"] + for node2 in user_set: + user2 = node2["node"] + self.assertEqual(user2["department"]["department"], department.department) + + def test_departments_on_pkuId_field(self): + # student query user in 'student' department + response = self.query( + """ + query{ + departments(department_Icontains: "student"){ + edges{ + node{ + userSet { + edges { + node { + pkuId + } + } + } + } + } + } + } + """, + headers=self.get_headers(self.student), + ) + self.assertResponseHasErrors(response) + + # student query user in 'teacher' department + response = self.query( + """ + query{ + departments(department_Icontains: "teacher"){ + edges{ + node{ + userSet { + edges { + node { + pkuId + } + } + } + } + } + } + } + """, + headers=self.get_headers(self.student), + ) + self.assertResponseHasErrors(response) + + # student query user in 'admin' department + response = self.query( + """ + query{ + departments(department_Icontains: "admin"){ + edges{ + node{ + userSet { + edges { + node { + pkuId + } + } + } + } + } + } + } + """, + headers=self.get_headers(self.student), + ) + self.assertResponseHasErrors(response) + + # teacher query user in 'student' department + response = self.query( + """ + query{ + departments(department_Icontains: "student"){ + edges{ + node{ + userSet { + edges { + node { + pkuId + } + } + } + } + } + } + } + """, + headers=self.get_headers(self.teacher), + ) + self.assertResponseNoErrors(response) + + # teacher query user in 'teacher' department + response = self.query( + """ + query{ + departments(department_Icontains: "teacher"){ + edges{ + node{ + userSet { + edges { + node { + pkuId + } + } + } + } + } + } + } + """, + headers=self.get_headers(self.teacher), + ) + self.assertResponseNoErrors(response) + + # teacher query user in 'admin' department + response = self.query( + """ + query{ + departments(department_Icontains: "admin"){ + edges{ + node{ + userSet { + edges { + node { + pkuId + } + } + } + } + } + } + } + """, + headers=self.get_headers(self.teacher), + ) + self.assertResponseHasErrors(response) + + for user in [self.s_admin, self.t_admin]: + # teacher query user in 'student' department + response = self.query( + """ + query{ + departments(department_Icontains: "student"){ + edges{ + node{ + userSet { + edges { + node { + pkuId + } + } + } + } + } + } + } + """, + headers=self.get_headers(user), + ) + self.assertResponseNoErrors(response) + + # teacher query user in 'teacher' department + response = self.query( + """ + query{ + departments(department_Icontains: "teacher"){ + edges{ + node{ + userSet { + edges { + node { + pkuId + } + } + } + } + } + } + } + """, + headers=self.get_headers(user), + ) + self.assertResponseNoErrors(response) + + # teacher query user in 'admin' department + response = self.query( + """ + query{ + departments(department_Icontains: "admin"){ + edges{ + node{ + userSet { + edges { + node { + pkuId + } + } + } + } + } + } + } + """, + headers=self.get_headers(user), + ) + self.assertResponseNoErrors(response) + + def test_departments_on_self_limit_field(self): + for field in ["isActive", "dateJoined", "lastLogin"]: + # student query user in 'student' department + response = self.query( + """ + query{{ + departments(department_Icontains: "student"){{ + edges{{ + node{{ + userSet {{ + edges {{ + node {{ + {field} + }} + }} + }} + }} + }} + }} + }} + """.format( + field=field + ), + headers=self.get_headers(self.student), + ) + self.assertResponseHasErrors(response) + + # student query user in 'teacher' department + response = self.query( + """ + query{{ + departments(department_Icontains: "teacher"){{ + edges{{ + node{{ + userSet {{ + edges {{ + node {{ + {field} + }} + }} + }} + }} + }} + }} + }} + """.format( + field=field + ), + headers=self.get_headers(self.student), + ) + self.assertResponseHasErrors(response) + + # student query user in 'admin' department + response = self.query( + """ + query{{ + departments(department_Icontains: "admin"){{ + edges{{ + node{{ + userSet {{ + edges {{ + node {{ + {field} + }} + }} + }} + }} + }} + }} + }} + """.format( + field=field + ), + headers=self.get_headers(self.student), + ) + self.assertResponseHasErrors(response) + + # teacher query user in 'student' department + response = self.query( + """ + query{{ + departments(department_Icontains: "student"){{ + edges{{ + node{{ + userSet {{ + edges {{ + node {{ + {field} + }} + }} + }} + }} + }} + }} + }} + """.format( + field=field + ), + headers=self.get_headers(self.teacher), + ) + self.assertResponseHasErrors(response) + + # teacher query user in 'teacher' department + response = self.query( + """ + query{{ + departments(department_Icontains: "teacher"){{ + edges{{ + node{{ + userSet {{ + edges {{ + node {{ + {field} + }} + }} + }} + }} + }} + }} + }} + """.format( + field=field + ), + headers=self.get_headers(self.teacher), + ) + self.assertResponseNoErrors(response) + + # teacher query user in 'admin' department + response = self.query( + """ + query{{ + departments(department_Icontains: "admin"){{ + edges{{ + node{{ + userSet {{ + edges {{ + node {{ + {field} + }} + }} + }} + }} + }} + }} + }} + """.format( + field=field + ), + headers=self.get_headers(self.teacher), + ) + self.assertResponseHasErrors(response) + + for user in [self.s_admin, self.t_admin]: + # teacher query user in 'student' department + response = self.query( + """ + query{{ + departments(department_Icontains: "student"){{ + edges{{ + node{{ + userSet {{ + edges {{ + node {{ + {field} + }} + }} + }} + }} + }} + }} + }} + """.format( + field=field + ), + headers=self.get_headers(user), + ) + self.assertResponseNoErrors(response) + + # teacher query user in 'teacher' department + response = self.query( + """ + query{{ + departments(department_Icontains: "teacher"){{ + edges{{ + node{{ + userSet {{ + edges {{ + node {{ + {field} + }} + }} + }} + }} + }} + }} + }} + """.format( + field=field + ), + headers=self.get_headers(user), + ) + self.assertResponseNoErrors(response) + + # teacher query user in 'admin' department + response = self.query( + """ + query{{ + departments(department_Icontains: "admin"){{ + edges{{ + node{{ + userSet {{ + edges {{ + node {{ + {field} + }} + }} + }} + }} + }} + }} + }} + """.format( + field=field + ), + headers=self.get_headers(user), + ) + self.assertResponseNoErrors(response) + + def test_departments_with_filter_id_exact(self): + def func(user, id, assert_func, count): + response = self.query( + """ + query myQuery($id: Float!){ + departments (id: $id) { + totalCount + edges { + node { + id + } + } + } + } + """, + variables={"id": id}, + headers=self.get_headers(user), + ) + content = json.loads(response.content) + assert_func(response) + self.assertEqual(content["data"]["departments"]["totalCount"], count) + + for user in self.users: + func(user, 1, self.assertResponseNoErrors, 1) + func(user, 2, self.assertResponseNoErrors, 1) + func(user, 3, self.assertResponseNoErrors, 1) + func(user, 4, self.assertResponseNoErrors, 0) + func(user, 0, self.assertResponseNoErrors, 0) + + def test_departments_with_filter_id_in(self): + def func(user, id, assert_func, count): + response = self.query( + """ + query myQuery($id: [String]!){ + departments (id_In: $id) { + totalCount + edges { + node { + id + } + } + } + } + """, + variables={"id": id}, + headers=self.get_headers(user), + ) + content = json.loads(response.content) + assert_func(response) + self.assertEqual(content["data"]["departments"]["totalCount"], count) + + for user in self.users: + func(user, "1", self.assertResponseNoErrors, 1) + func(user, "2", self.assertResponseNoErrors, 1) + func(user, "3", self.assertResponseNoErrors, 1) + func(user, "4", self.assertResponseNoErrors, 0) + func(user, "0", self.assertResponseNoErrors, 0) + func(user, ["0", "1"], self.assertResponseNoErrors, 1) + func(user, ["1", "2"], self.assertResponseNoErrors, 2) + func(user, ["1", "2", "3"], self.assertResponseNoErrors, 3) + + def test_departments_with_filter_department_icontains(self): + def func(user, name, assert_func, count): + response = self.query( + """ + query myQuery($name: String!){ + departments (department_Icontains: $name) { + totalCount + edges { + node { + id + } + } + } + } + """, + variables={"name": name}, + headers=self.get_headers(user), + ) + content = json.loads(response.content) + assert_func(response) + self.assertEqual(content["data"]["departments"]["totalCount"], count) + + for user in self.users: + func(user, "teacher", self.assertResponseNoErrors, 1) + func(user, "student", self.assertResponseNoErrors, 1) + func(user, "admin", self.assertResponseNoErrors, 1) + func(user, "e", self.assertResponseNoErrors, 2) + func(user, "t", self.assertResponseNoErrors, 2) + func(user, "n", self.assertResponseNoErrors, 2) + + def test_department_without_token(self): + for id in range(1, 4): + response = self.query( + """ + query myModel($id: ID!){ + department(id: $id) { + id + department + userSet { + edges { + node { + id + } + } + } + } + } + """, + variables={"id": to_global_id(DepartmentType._meta.name, str(id))}, + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertIsNotNone(content["data"]) + data = content["data"] + self.assertIsNotNone(data["department"]) + department = data["department"] + self.assertIsNotNone(department["id"]) + self.assertIsNotNone(department["department"]) + self.assertIsNotNone(department["userSet"]) + self.assertEqual(len(department["userSet"]["edges"]), 0) + + def test_department(self): + for id in range(1, 4): + for user in self.users: + response = self.query( + """ + query myModel($id: ID!){ + department(id: $id) { + id + department + userSet { + edges { + node { + id + } + } + } + } + } + """, + variables={"id": to_global_id(DepartmentType._meta.name, str(id))}, + headers=self.get_headers(user), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertIsNotNone(content["data"]) + data = content["data"] + self.assertIsNotNone(data["department"]) + department = data["department"] + self.assertIsNotNone(department["id"]) + self.assertIsNotNone(department["department"]) + self.assertIsNotNone(department["userSet"]) + self.assertEqual( + len(department["userSet"]["edges"]), + User.objects.filter(department_id=id).count(), + ) + + def test_users_without_token(self): + for user in self.users: + response = self.query( + """ + query { + users { + totalCount + edges { + node { + id + pkuId + } + } + } + } + """ + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertIsNotNone(content["data"]) + data = content["data"] + self.assertIsNotNone(data["users"]) + users = data["users"] + self.assertEqual(users["totalCount"], 0) + self.assertEqual(users["edges"], []) + + def test_users(self): + for user in self.users: + response = self.query( + """ + query { + users { + totalCount + edges { + node { + id + name + email + website + phoneNumber + isTeacher + department { + department + } + introduce + isAdmin + } + } + } + } + """, + headers=self.get_headers(user), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertIsNotNone(content["data"]) + data = content["data"] + self.assertIsNotNone(data["users"]) + users = data["users"] + self.assertEqual(users["totalCount"], 5) + edges = users["edges"] + for edge in edges: + self.assertIsNotNone(edge["node"]) + + def test_users_on_pkuId_field(self): + for user in self.users: + response = self.query( + """ + query { + users (isTeacher: false){ + totalCount + edges { + node { + pkuId + } + } + } + } + """, + headers=self.get_headers(user), + ) + content = json.loads(response.content) + if user.is_teacher or user.is_admin: + self.assertResponseNoErrors(response) + else: + self.assertResponseHasErrors(response) + self.assertEqual(content["data"]["users"]["totalCount"], 3) + + response = self.query( + """ + query { + users (isTeacher: true){ + totalCount + edges { + node { + pkuId + } + } + } + } + """, + headers=self.get_headers(user), + ) + content = json.loads(response.content) + if user.is_admin: + self.assertResponseNoErrors(response) + else: + self.assertResponseHasErrors(response) + self.assertEqual(content["data"]["users"]["totalCount"], 2) + + def test_users_on_self_limit_field(self): + for user in self.users: + for field in ["isActive", "dateJoined", "lastLogin"]: + response = self.query( + """ + query {{ + users {{ + totalCount + edges {{ + node {{ + {} + }} + }} + }} + }} + """.format( + field + ), + headers=self.get_headers(user), + ) + content = json.loads(response.content) + if user.is_admin: + self.assertResponseNoErrors(response) + else: + self.assertResponseHasErrors(response) + self.assertEqual(content["data"]["users"]["totalCount"], 5) + + def test_users_with_filter_pkuId_exact(self): + for user in self.users: + for pku_id in ["2000000000", "2000000001", "2000000002", "2000000003", "2000000004"]: + response = self.query( + """ + query myQuery($id: String!){ + users (pkuId: $id) { + totalCount + edges { + node { + id + } + } + } + } + """, + variables={"id": pku_id}, + headers=self.get_headers(user), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + self.assertEqual(content["data"]["users"]["totalCount"], 0 if pku_id == "2000000004" else 1) + + def test_users_with_filter_pkuId_contains(self): + def func(user, pku_id, assert_func, count): + response = self.query( + """ + query myQuery($id: String!){ + users (pkuId_Contains: $id) { + totalCount + edges { + node { + id + } + } + } + } + """, + variables={"id": pku_id}, + headers=self.get_headers(user), + ) + content = json.loads(response.content) + assert_func(response) + self.assertEqual(content["data"]["users"]["totalCount"], count) + + for user in self.users: + func(user, "2000000000", self.assertResponseNoErrors, 1) + func(user, "200000000", self.assertResponseNoErrors, 4) + func(user, "0", self.assertResponseNoErrors, 5) + func(user, "1", self.assertResponseNoErrors, 1) + func(user, "0000000000", self.assertResponseNoErrors, 1) + func(user, "2", self.assertResponseNoErrors, 4) + func(user, "3", self.assertResponseNoErrors, 1) + + def test_users_with_filter_pkuId_startswith(self): + def func(user, pku_id, assert_func, count): + response = self.query( + """ + query myQuery($id: String!){ + users (pkuId_Startswith: $id) { + totalCount + edges { + node { + id + } + } + } + } + """, + variables={"id": pku_id}, + headers=self.get_headers(user), + ) + content = json.loads(response.content) + assert_func(response) + self.assertEqual(content["data"]["users"]["totalCount"], count) + + for user in self.users: + func(user, "2000000000", self.assertResponseNoErrors, 1) + func(user, "200000000", self.assertResponseNoErrors, 4) + func(user, "0", self.assertResponseNoErrors, 1) + func(user, "1", self.assertResponseNoErrors, 0) + func(user, "0000000000", self.assertResponseNoErrors, 1) + func(user, "2", self.assertResponseNoErrors, 4) + func(user, "19", self.assertResponseNoErrors, 0) + + def test_users_with_filter_name_icontains(self): + def func(user, name, assert_func, count): + response = self.query( + """ + query myQuery($name: String!){ + users (name_Icontains: $name) { + totalCount + edges { + node { + id + } + } + } + } + """, + variables={"name": name}, + headers=self.get_headers(user), + ) + content = json.loads(response.content) + assert_func(response) + self.assertEqual(content["data"]["users"]["totalCount"], count) + + for user in self.users: + func(user, "stu", self.assertResponseNoErrors, 1) + func(user, "", self.assertResponseNoErrors, 5) + func(user, "tea", self.assertResponseNoErrors, 1) + func(user, "admin", self.assertResponseNoErrors, 2) + func(user, "s", self.assertResponseNoErrors, 2) + func(user, "t", self.assertResponseNoErrors, 3) + func(user, "a", self.assertResponseNoErrors, 3) + + def test_users_with_filter_department_id_exact(self): + def func(user, id, assert_func, count): + response = self.query( + """ + query myQuery($id: Float!){ + users (department_Id: $id) { + totalCount + edges { + node { + id + } + } + } + } + """, + variables={"id": id}, + headers=self.get_headers(user), + ) + content = json.loads(response.content) + assert_func(response) + self.assertEqual(content["data"]["users"]["totalCount"], count) + + for user in self.users: + func(user, 1, self.assertResponseNoErrors, 2) + func(user, 2, self.assertResponseNoErrors, 1) + func(user, 3, self.assertResponseNoErrors, 1) + func(user, 4, self.assertResponseNoErrors, 0) + func(user, 0, self.assertResponseNoErrors, 0) + + def test_users_with_filter_department_id_in(self): + def func(user, id, assert_func, count): + response = self.query( + """ + query myQuery($id: [String]!){ + users (department_Id_In: $id) { + totalCount + edges { + node { + id + } + } + } + } + """, + variables={"id": id}, + headers=self.get_headers(user), + ) + content = json.loads(response.content) + assert_func(response) + self.assertEqual(content["data"]["users"]["totalCount"], count) + + for user in self.users: + func(user, "1", self.assertResponseNoErrors, 2) + func(user, "2", self.assertResponseNoErrors, 1) + func(user, "3", self.assertResponseNoErrors, 1) + func(user, "4", self.assertResponseNoErrors, 0) + func(user, "0", self.assertResponseNoErrors, 0) + func(user, ["0", "1"], self.assertResponseNoErrors, 2) + func(user, ["1", "2"], self.assertResponseNoErrors, 3) + func(user, ["1", "2", "3"], self.assertResponseNoErrors, 4) + + def test_users_with_filter_department_department_icontains(self): + def func(user, name, assert_func, count): + response = self.query( + """ + query myQuery($name: String!){ + users (department_Department_Icontains: $name) { + totalCount + edges { + node { + id + } + } + } + } + """, + variables={"name": name}, + headers=self.get_headers(user), + ) + content = json.loads(response.content) + assert_func(response) + self.assertEqual(content["data"]["users"]["totalCount"], count) + + for user in self.users: + func(user, "stu", self.assertResponseNoErrors, 2) + func(user, "", self.assertResponseNoErrors, 5) + func(user, "tea", self.assertResponseNoErrors, 1) + func(user, "admin", self.assertResponseNoErrors, 1) + func(user, "a", self.assertResponseNoErrors, 2) + func(user, "s", self.assertResponseNoErrors, 2) + func(user, "t", self.assertResponseNoErrors, 3) + + def test_users_with_filter_is_teacher_exact(self): + def func(user, id, assert_func, count): + response = self.query( + """ + query myQuery($id: Boolean!){ + users (isTeacher: $id) { + totalCount + edges { + node { + id + } + } + } + } + """, + variables={"id": id}, + headers=self.get_headers(user), + ) + content = json.loads(response.content) + assert_func(response) + self.assertEqual(content["data"]["users"]["totalCount"], count) + + for user in self.users: + func(user, True, self.assertResponseNoErrors, 2) + func(user, False, self.assertResponseNoErrors, 3) + + def test_users_with_filter_is_admin_exact(self): + def func(user, id, assert_func, count): + response = self.query( + """ + query myQuery($id: Boolean!){ + users (isAdmin: $id) { + totalCount + edges { + node { + id + } + } + } + } + """, + variables={"id": id}, + headers=self.get_headers(user), + ) + content = json.loads(response.content) + assert_func(response) + self.assertEqual(content["data"]["users"]["totalCount"], count) + + for user in self.users: + func(user, True, self.assertResponseNoErrors, 2) + func(user, False, self.assertResponseNoErrors, 3) + + def test_users_with_filter_is_active_exact(self): + def func(user, id, assert_func, count): + response = self.query( + """ + query myQuery($id: Boolean!){ + users (isActive: $id) { + totalCount + edges { + node { + id + } + } + } + } + """, + variables={"id": id}, + headers=self.get_headers(user), + ) + content = json.loads(response.content) + assert_func(response) + self.assertEqual(content["data"]["users"]["totalCount"], count) + + for user in self.users: + func(user, True, self.assertResponseNoErrors, 5) + func(user, False, self.assertResponseNoErrors, 0) + + +class MutationApiTest(GraphQLTestCase): + @classmethod + def setUpTestData(cls): + cls.department = Department.objects.create(department="student") + cls.user = User.objects.create( + pku_id="2000000000", + name="student", + email="student@pku.edu.cn", + website="https://www.pku.edu.cn", + phone_number="123456789", + address="student office", + department=cls.department, + introduce="student introduce", + is_teacher=False, + is_admin=False, + is_active=True, + ) + cls.user.set_unusable_password() + cls.user.save() + + @staticmethod + def get_headers(user): + return { + jwt_settings.JWT_AUTH_HEADER_NAME: f"{jwt_settings.JWT_AUTH_HEADER_PREFIX} {get_token(user)}", + } + + def test_me_without_token(self): + response = self.query( + """ + mutation myMutation($input: MeMutationInput!){ + me(input: $input){ + errors{ + field + message + } + clientMutationId + user{ + id + name + } + } + } + """, + input_data={ + "id": to_global_id(UserType._meta.name, str(2)), + "clientMutationId": "without token", + }, + ) + self.assertResponseNoErrors(response) + content = json.loads(response.content) + self.assertGreater(len(content["data"]["me"]["errors"]), 0) + + def test_me(self): + department = Department.objects.create(department="test") + response = self.query( + """ + mutation myMutation($input: MeMutationInput!){ + me(input: $input){ + errors{ + field + message + } + clientMutationId + user{ + id + name + email + website + phoneNumber + address + introduce + department { + department + } + } + } + } + """, + input_data={ + "clientMutationId": "with token", + "id": to_global_id(UserType._meta.name, str(self.user.pk)), + "name": "m student", + "email": "m.student@pku.edu.cn", + "website": "https://wwws.pku.edu.cn", + "phoneNumber": "987654321", + "address": "m student office", + "department": to_global_id(DepartmentType._meta.name, str(department.id)), + "introduce": "m student introduce", + }, + headers=self.get_headers(self.user), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + me = content["data"]["me"] + self.assertEqual(me["errors"], []) + self.assertEqual(me["clientMutationId"], "with token") + user = me["user"] + self.assertEqual(user["name"], "m student") + self.assertEqual(user["email"], "m.student@pku.edu.cn") + self.assertEqual(user["website"], "https://wwws.pku.edu.cn") + self.assertEqual(user["phoneNumber"], "987654321") + self.assertEqual(user["address"], "m student office") + self.assertEqual(user["department"]["department"], department.department) + self.assertEqual(user["introduce"], "m student introduce") + self.user = User.objects.get(id=self.user.id) + self.assertEqual(self.user.name, "m student"), + self.assertEqual(self.user.email, "m.student@pku.edu.cn") + self.assertEqual(self.user.website, "https://wwws.pku.edu.cn") + self.assertEqual(self.user.phone_number, "987654321") + self.assertEqual(self.user.address, "m student office") + self.assertEqual(self.user.department.id, department.id) + self.assertEqual(self.user.introduce, "m student introduce") + + def test_me_on_no_permission_field(self): + for item, key in { + "pkuId": "123", + "isTeacher": True, + "isAdmin": True, + "isActive": True, + "dateJoined": None, + "lastLogin": None, + "password": "123", + }.items(): + response = self.query( + """ + mutation myMutation($input: MeMutationInput!){ + me(input: $input){ + errors{ + field + message + } + clientMutationId + user{ + id + name + } + } + } + """, + input_data={ + "clientMutationId": "with token", + "id": to_global_id(UserType._meta.name, str(self.user.pk)), + item: key, + }, + headers=self.get_headers(self.user), + ) + self.assertResponseHasErrors(response) + + def test_user_create_without_token(self): + assign_perm("user.add_user", self.user) + response = self.query( + """ + mutation myMutation($input: UserCreateInput!){ + userCreate(input: $input){ + errors{ + field + message + } + clientMutationId + user{ + id + name + } + } + } + """, + input_data={"clientMutationId": "without token", "email": "test@pku.edu.cn", "pkuId": "2000000001"}, + ) + self.assertResponseNoErrors(response) + content = json.loads(response.content) + self.assertGreater(len(content["data"]["userCreate"]["errors"]), 0) + + def test_user_create(self): + self.user.is_admin = True + self.user.save() + assign_perm("user.add_user", self.user) + response = self.query( + """ + mutation myMutation($input: UserCreateInput!){ + userCreate(input: $input){ + errors{ + field + message + } + clientMutationId + user{ + pkuId + name + website + email + phoneNumber + introduce + department { + department + } + isTeacher + isAdmin + isActive + dateJoined + } + } + } + """, + input_data={ + "clientMutationId": "with token", + "pkuId": "2000000001", + "email": "test@pku.edu.cn", + "website": "https://phy.pku.edu.cn", + "phoneNumber": "987654321", + "introduce": "new user", + "department": to_global_id(DepartmentType._meta.name, str(self.department.id)), + "isTeacher": True, + "isAdmin": False, + "isActive": True, + }, + headers=self.get_headers(self.user), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + data = content["data"] + self.assertIsNotNone(data["userCreate"]) + user_create = data["userCreate"] + self.assertEqual(user_create["errors"], []) + self.assertEqual(user_create["clientMutationId"], "with token") + self.assertIsNotNone(user_create["user"]) + user = user_create["user"] + self.assertEqual(user["pkuId"], "2000000001") + self.assertEqual(user["email"], "test@pku.edu.cn") + self.assertEqual(user["website"], "https://phy.pku.edu.cn") + self.assertEqual(user["phoneNumber"], "987654321") + self.assertEqual(user["introduce"], "new user") + self.assertEqual(user["department"]["department"], self.department.department) + self.assertEqual(user["isTeacher"], True) + self.assertEqual(user["isAdmin"], False) + self.assertEqual(user["isActive"], True) + self.assertIsNotNone(user["dateJoined"]) + + def test_user_update_without_token(self): + assign_perm("user.change_user", self.user) + user = User.objects.create( + pku_id="2000000001", + name="teacher", + email="teacher@pku.edu.cn", + website="https://www.pku.edu.cn", + phone_number="123456789", + address="teacher office", + department=None, + introduce="teacher introduce", + is_teacher=True, + is_admin=False, + is_active=True, + ) + response = self.query( + """ + mutation myMutation($input: UserUpdateInput!){ + userUpdate(input: $input){ + errors{ + field + message + } + clientMutationId + user{ + id + name + } + } + } + """, + input_data={ + "clientMutationId": "without token", + "id": to_global_id(UserType._meta.name, str(user.id)), + "email": "test@pku.edu.cn", + "pkuId": "2000000001", + }, + ) + self.assertResponseNoErrors(response) + content = json.loads(response.content) + self.assertGreater(len(content["data"]["userUpdate"]["errors"]), 0) + + def test_user_update(self): + self.user.is_admin = True + self.user.save() + assign_perm("user.change_user", self.user) + user = User.objects.create( + pku_id="2000000001", + name="teacher", + email="teacher@pku.edu.cn", + website="https://www.pku.edu.cn", + phone_number="123456789", + address="teacher office", + department=None, + introduce="teacher introduce", + is_teacher=True, + is_admin=False, + is_active=True, + ) + response = self.query( + """ + mutation myMutation($input: UserUpdateInput!){ + userUpdate(input: $input){ + errors{ + field + message + } + clientMutationId + user{ + pkuId + name + website + email + phoneNumber + introduce + department { + department + } + isTeacher + isAdmin + isActive + dateJoined + } + } + } + """, + input_data={ + "clientMutationId": "with token", + "id": to_global_id(UserType._meta.name, str(user.id)), + "department": to_global_id(DepartmentType._meta.name, str(self.department.id)), + "isTeacher": False, + }, + headers=self.get_headers(self.user), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + data = content["data"] + self.assertIsNotNone(data["userUpdate"]) + user_update = data["userUpdate"] + self.assertEqual(user_update["errors"], []) + self.assertEqual(user_update["clientMutationId"], "with token") + self.assertIsNotNone(user_update["user"]) + user2 = user_update["user"] + self.assertEqual(user2["department"]["department"], self.department.department) + self.assertEqual(user2["isTeacher"], False) + + def test_user_delete_without_token(self): + assign_perm("user.delete_user", self.user) + user = User.objects.create( + pku_id="2000000001", + name="teacher", + email="teacher@pku.edu.cn", + website="https://www.pku.edu.cn", + phone_number="123456789", + address="teacher office", + department=None, + introduce="teacher introduce", + is_teacher=True, + is_admin=False, + is_active=True, + ) + response = self.query( + """ + mutation myMutation($input: UserDeleteInput!){ + userDelete(input: $input){ + errors{ + field + message + } + clientMutationId + user{ + id + name + } + } + } + """, + input_data={ + "clientMutationId": "without token", + "id": to_global_id(UserType._meta.name, str(user.id)), + }, + ) + self.assertResponseNoErrors(response) + content = json.loads(response.content) + self.assertGreater(len(content["data"]["userDelete"]["errors"]), 0) + + def test_user_delete(self): + self.user.is_admin = True + self.user.save() + assign_perm("user.delete_user", self.user) + user = User.objects.create( + pku_id="2000000001", + name="teacher", + email="teacher@pku.edu.cn", + website="https://www.pku.edu.cn", + phone_number="123456789", + address="teacher office", + department=None, + introduce="teacher introduce", + is_teacher=True, + is_admin=False, + is_active=True, + ) + response = self.query( + """ + mutation myMutation($input: UserDeleteInput!){ + userDelete(input: $input){ + errors{ + field + message + } + clientMutationId + user{ + pkuId + name + website + email + phoneNumber + introduce + department { + department + } + isTeacher + isAdmin + isActive + dateJoined + } + } + } + """, + input_data={ + "clientMutationId": "with token", + "id": to_global_id(UserType._meta.name, str(user.id)), + }, + headers=self.get_headers(self.user), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + data = content["data"] + self.assertIsNotNone(data["userDelete"]) + user_update = data["userDelete"] + self.assertEqual(user_update["errors"], []) + self.assertEqual(user_update["clientMutationId"], "with token") + self.assertIsNotNone(user_update["user"]) + with self.assertRaises(User.DoesNotExist): + User.objects.get(pk=user.id) + + def test_department_create_without_token(self): + assign_perm("user.add_department", self.user) + response = self.query( + """ + mutation myMutation($input: DepartmentCreateInput!){ + departmentCreate(input: $input){ + errors{ + field + message + } + clientMutationId + user{ + id + name + } + } + } + """, + input_data={"clientMutationId": "without token", "department": "teacher"}, + ) + self.assertResponseHasErrors(response) + + def test_department_create(self): + assign_perm("user.add_department", self.user) + response = self.query( + """ + mutation myMutation($input: DepartmentCreateInput!){ + departmentCreate(input: $input){ + errors{ + field + message + } + clientMutationId + department{ + id + department + } + } + } + """, + input_data={ + "clientMutationId": "with token", + "department": "teacher", + }, + headers=self.get_headers(self.user), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + data = content["data"] + self.assertIsNotNone(data["departmentCreate"]) + department_create = data["departmentCreate"] + self.assertEqual(department_create["errors"], []) + self.assertEqual(department_create["clientMutationId"], "with token") + self.assertIsNotNone(department_create["department"]) + department = department_create["department"] + self.assertEqual(department["department"], "teacher") + + def test_department_update_without_token(self): + assign_perm("user.change_department", self.user) + department = Department.objects.create(department="test") + response = self.query( + """ + mutation myMutation($input: DepartmentUpdateInput!){ + departmentUpdate(input: $input){ + errors{ + field + message + } + clientMutationId + department { + id + department + } + } + } + """, + input_data={ + "clientMutationId": "without token", + "id": to_global_id(DepartmentType._meta.name, str(department.id)), + "department": "m test", + }, + ) + self.assertResponseNoErrors(response) + content = json.loads(response.content) + self.assertGreater(len(content["data"]["departmentUpdate"]["errors"]), 0) + + def test_department_update(self): + assign_perm("user.change_department", self.user) + department = Department.objects.create(department="test") + response = self.query( + """ + mutation myMutation($input: DepartmentUpdateInput!){ + departmentUpdate(input: $input){ + errors{ + field + message + } + clientMutationId + department{ + id + department + } + } + } + """, + input_data={ + "clientMutationId": "with token", + "id": to_global_id(DepartmentType._meta.name, str(department.id)), + "department": "m test", + }, + headers=self.get_headers(self.user), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + data = content["data"] + self.assertIsNotNone(data["departmentUpdate"]) + department_update = data["departmentUpdate"] + self.assertEqual(department_update["errors"], []) + self.assertEqual(department_update["clientMutationId"], "with token") + self.assertIsNotNone(department_update["department"]) + department2 = department_update["department"] + department = Department.objects.get(id=department.id) + self.assertEqual(department2["department"], department.department) + + def test_department_delete_without_token(self): + assign_perm("user.delete_department", self.user) + department = Department.objects.create(department="test") + response = self.query( + """ + mutation myMutation($input: DepartmentDeleteInput!){ + departmentDelete(input: $input){ + errors{ + field + message + } + clientMutationId + department{ + id + department + } + } + } + """, + input_data={ + "clientMutationId": "without token", + "id": to_global_id(DepartmentType._meta.name, str(department.id)), + }, + ) + self.assertResponseNoErrors(response) + content = json.loads(response.content) + self.assertGreater(len(content["data"]["departmentDelete"]["errors"]), 0) + + def test_department_delete(self): + assign_perm("user.delete_department", self.user) + department = Department.objects.create(department="test") + response = self.query( + """ + mutation myMutation($input: DepartmentDeleteInput!){ + departmentDelete(input: $input){ + errors{ + field + message + } + clientMutationId + department { + id + department + } + } + } + """, + input_data={ + "clientMutationId": "with token", + "id": to_global_id(DepartmentType._meta.name, str(department.id)), + }, + headers=self.get_headers(self.user), + ) + content = json.loads(response.content) + self.assertResponseNoErrors(response) + data = content["data"] + self.assertIsNotNone(data["departmentDelete"]) + department_update = data["departmentDelete"] + self.assertEqual(department_update["errors"], []) + self.assertEqual(department_update["clientMutationId"], "with token") + self.assertIsNotNone(department_update["department"]) + with self.assertRaises(Department.DoesNotExist): + Department.objects.get(pk=department.id) diff --git a/poetry.lock b/poetry.lock index dbb51b6..7a208b9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -9,6 +9,14 @@ python-versions = ">=3.6" [package.dependencies] vine = "5.0.0" +[[package]] +name = "aniso8601" +version = "8.1.1" +description = "A library for parsing ISO 8601 strings." +category = "main" +optional = false +python-versions = "*" + [[package]] name = "appdirs" version = "1.4.4" @@ -277,6 +285,19 @@ python-versions = ">=3.5" [package.dependencies] Django = ">=2.2" +[[package]] +name = "django-graphql-jwt" +version = "0.3.2" +description = "JSON Web Token for Django GraphQL" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +Django = ">=1.11" +graphene-django = ">=3.0.0b1" +PyJWT = ">=2,<3" + [[package]] name = "django-guardian" version = "2.4.0" @@ -327,6 +348,78 @@ python-versions = ">=3.5" [package.dependencies] python-dateutil = ">=2.7" +[[package]] +name = "graphene" +version = "3.0b7" +description = "GraphQL Framework for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +aniso8601 = ">=8,<9" +graphql-core = ">=3.1.2,<4" +graphql-relay = ">=3.0,<4" + +[package.extras] +dev = ["black (==19.10b0)", "flake8 (>=3.7,<4)", "pytest (>=5.3,<6)", "pytest-benchmark (>=3.2,<4)", "pytest-cov (>=2.8,<3)", "pytest-mock (>=2,<3)", "pytest-asyncio (>=0.10,<2)", "snapshottest (>=0.5,<1)", "coveralls (>=1.11,<2)", "promise (>=2.3,<3)", "mock (>=4.0,<5)", "pytz (==2019.3)", "iso8601 (>=0.1,<2)"] +test = ["pytest (>=5.3,<6)", "pytest-benchmark (>=3.2,<4)", "pytest-cov (>=2.8,<3)", "pytest-mock (>=2,<3)", "pytest-asyncio (>=0.10,<2)", "snapshottest (>=0.5,<1)", "coveralls (>=1.11,<2)", "promise (>=2.3,<3)", "mock (>=4.0,<5)", "pytz (==2019.3)", "iso8601 (>=0.1,<2)"] + +[[package]] +name = "graphene-django" +version = "3.0.0b7" +description = "Graphene Django integration" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +Django = ">=2.2" +graphene = ">=3.0.0b5,<4" +graphql-core = ">=3.1.0,<4" +promise = ">=2.1" +text-unidecode = "*" + +[package.extras] +dev = ["black (==19.10b0)", "flake8 (==3.7.9)", "flake8-black (==0.1.1)", "flake8-bugbear (==20.1.4)", "pytest (>=3.6.3)", "pytest-cov", "pytest-random-order", "coveralls", "mock", "pytz", "django-filter (>=2)", "pytest-django (>=3.3.2)", "djangorestframework (>=3.6.3)"] +rest_framework = ["djangorestframework (>=3.6.3)"] +test = ["pytest (>=3.6.3)", "pytest-cov", "pytest-random-order", "coveralls", "mock", "pytz", "django-filter (>=2)", "pytest-django (>=3.3.2)", "djangorestframework (>=3.6.3)"] + +[[package]] +name = "graphene-django-optimizer" +version = "0.8.0" +description = "Optimize database access inside graphene queries." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "graphene-django-plus" +version = "2.6.1" +description = "Tools to easily create permissioned CRUD endpoints in graphene." +category = "main" +optional = false +python-versions = ">=3.7,<4.0" + +[[package]] +name = "graphql-core" +version = "3.1.5" +description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." +category = "main" +optional = false +python-versions = ">=3.6,<4" + +[[package]] +name = "graphql-relay" +version = "3.1.0" +description = "Relay library for graphql-core" +category = "main" +optional = false +python-versions = ">=3.6,<4" + +[package.dependencies] +graphql-core = ">=3.1" + [[package]] name = "idna" version = "2.10" @@ -425,6 +518,20 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.extras] dev = ["pre-commit", "tox"] +[[package]] +name = "promise" +version = "2.3" +description = "Promises/A+ implementation for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" + +[package.extras] +test = ["pytest (>=2.7.3)", "pytest-cov", "coveralls", "futures", "pytest-benchmark", "mock"] + [[package]] name = "prompt-toolkit" version = "3.0.18" @@ -468,6 +575,20 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pyjwt" +version = "2.1.0" +description = "JSON Web Token implementation in Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +crypto = ["cryptography (>=3.3.1,<4.0.0)"] +dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1,<4.0.0)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"] + [[package]] name = "pyparsing" version = "2.4.7" @@ -604,6 +725,14 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "toml" version = "0.10.2" @@ -648,13 +777,17 @@ pgsql = ["psycopg2"] [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "e1b835fdd3fb017345a4db544ca311a41ea3c03453b3e2c757f30b533b2a0b99" +content-hash = "0e17613bc7b7fa9b328f05d239c071de0ecb443119ce450a950e5d80d157ddaa" [metadata.files] amqp = [ {file = "amqp-5.0.6-py3-none-any.whl", hash = "sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"}, {file = "amqp-5.0.6.tar.gz", hash = "sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2"}, ] +aniso8601 = [ + {file = "aniso8601-8.1.1-py2.py3-none-any.whl", hash = "sha256:f59914762c5049ffd956cad037aa82fe0cabf8baf51900e2af24026761090b0b"}, + {file = "aniso8601-8.1.1.tar.gz", hash = "sha256:be08b19c19ca527af722f2d4ba4dc569db292ec96f7de963746df4bb0bff9250"}, +] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, @@ -784,6 +917,10 @@ django-filter = [ {file = "django-filter-2.4.0.tar.gz", hash = "sha256:84e9d5bb93f237e451db814ed422a3a625751cbc9968b484ecc74964a8696b06"}, {file = "django_filter-2.4.0-py3-none-any.whl", hash = "sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1"}, ] +django-graphql-jwt = [ + {file = "django-graphql-jwt-0.3.2.tar.gz", hash = "sha256:36735d7d53dca2ec888f3587c05c412efb4a98c9a3f49f19771c5afcc959058d"}, + {file = "django_graphql_jwt-0.3.2-py2.py3-none-any.whl", hash = "sha256:3ba1a9472daf8c2464a521e4d415a54e820a30fdcf7be79ccb7d0fa39c00452f"}, +] django-guardian = [ {file = "django-guardian-2.4.0.tar.gz", hash = "sha256:c58a68ae76922d33e6bdc0e69af1892097838de56e93e78a8361090bcd9f89a0"}, {file = "django_guardian-2.4.0-py3-none-any.whl", hash = "sha256:440ca61358427e575323648b25f8384739e54c38b3d655c81d75e0cd0d61b697"}, @@ -800,6 +937,29 @@ freezegun = [ {file = "freezegun-1.1.0-py2.py3-none-any.whl", hash = "sha256:2ae695f7eb96c62529f03a038461afe3c692db3465e215355e1bb4b0ab408712"}, {file = "freezegun-1.1.0.tar.gz", hash = "sha256:177f9dd59861d871e27a484c3332f35a6e3f5d14626f2bf91be37891f18927f3"}, ] +graphene = [ + {file = "graphene-3.0b7-py2.py3-none-any.whl", hash = "sha256:70293cd3cd301eb8ab69f43b18bff0223bbb458ed74904bd3dffa69f802bf6ef"}, + {file = "graphene-3.0b7.tar.gz", hash = "sha256:03e1cb640ad48669eaf5a4228be216f7dce72b6ca3e9b1dd568e429001897642"}, +] +graphene-django = [ + {file = "graphene-django-3.0.0b7.tar.gz", hash = "sha256:b1a4ce1a2227b1e77f67f0bc2fadd59c1d05016cb9aced45ab65f8612fba2c87"}, + {file = "graphene_django-3.0.0b7-py2.py3-none-any.whl", hash = "sha256:0f226ec7db744a54dbc5d6db2aa52d945701ae800e1055046dec2b76a539550a"}, +] +graphene-django-optimizer = [ + {file = "graphene-django-optimizer-0.8.0.tar.gz", hash = "sha256:79269880d59d0a35d41751ddcb419220c4ad3871960416371119f447cb2e1a77"}, +] +graphene-django-plus = [ + {file = "graphene-django-plus-2.6.1.tar.gz", hash = "sha256:33a9a4156c062043c7992cd16693ec2d43dffe7a69b99bcabb4c68fca3a87c4d"}, + {file = "graphene_django_plus-2.6.1-py3-none-any.whl", hash = "sha256:f538939f6d0c4e04dd74a895ec6a8e844c0b47afe40ab320389cc94f99df3ae1"}, +] +graphql-core = [ + {file = "graphql-core-3.1.5.tar.gz", hash = "sha256:a755635d1d364a17e8d270347000722351aaa03f1ab7d280878aae82fc68b1f3"}, + {file = "graphql_core-3.1.5-py3-none-any.whl", hash = "sha256:91d96ef0e86665777bb7115d3bbb6b0326f43dc7dbcdd60da5486a27a50cfb11"}, +] +graphql-relay = [ + {file = "graphql-relay-3.1.0.tar.gz", hash = "sha256:70d5a7ee5995ea7c2a9a37e51227663b1a464f1f40e98fdde950be5415dfe0b4"}, + {file = "graphql_relay-3.1.0-py3-none-any.whl", hash = "sha256:2cda0ac0199dd56c28ca4f6e0381cdcf5787809c06d1507df3c2a738f9ad846f"}, +] idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, @@ -839,6 +999,9 @@ pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] +promise = [ + {file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"}, +] prompt-toolkit = [ {file = "prompt_toolkit-3.0.18-py3-none-any.whl", hash = "sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04"}, {file = "prompt_toolkit-3.0.18.tar.gz", hash = "sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc"}, @@ -872,6 +1035,10 @@ pyflakes = [ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] +pyjwt = [ + {file = "PyJWT-2.1.0-py3-none-any.whl", hash = "sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1"}, + {file = "PyJWT-2.1.0.tar.gz", hash = "sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130"}, +] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, @@ -954,6 +1121,10 @@ sqlparse = [ {file = "sqlparse-0.4.1-py3-none-any.whl", hash = "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0"}, {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"}, ] +text-unidecode = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, diff --git a/pyproject.toml b/pyproject.toml index ef33486..59ddcd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,9 +7,13 @@ authors = ["Rainshaw "] [tool.poetry.dependencies] python = "^3.9" Django = "^3.2" +graphene-django = "^3.0.0b1" django-filter = "^2.4.0" +django-graphql-jwt = "^0.3.2" requests = "^2.25.1" django-guardian = "^2.4.0" +graphene-django-optimizer = "^0.8.0" +graphene-django-plus = "^2.6.1" celery = "^5.1.0" django-celery-beat = "^2.2.0" django-celery-results = "^2.0.1" diff --git a/translation/graphql_jwt/zh_Hans/LC_MESSAGES/django.po b/translation/graphql_jwt/zh_Hans/LC_MESSAGES/django.po new file mode 100644 index 0000000..09abc4a --- /dev/null +++ b/translation/graphql_jwt/zh_Hans/LC_MESSAGES/django.po @@ -0,0 +1,59 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-22 02:09+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: decorators.py:102 +msgid "Please enter valid credentials" +msgstr "请输入有效的凭证" + +#: decorators.py:195 +msgid "Token is required" +msgstr "需要令牌" + +#: exceptions.py:15 +msgid "You do not have permission to perform this action" +msgstr "您无权执行此操作" + +#: exceptions.py:19 +msgid "Signature has expired" +msgstr "签名已过期" + +#: mixins.py:80 +msgid "origIat field is required" +msgstr "需要 origIat 字段" + +#: mixins.py:83 +msgid "Refresh has expired" +msgstr "刷新令牌已过期" + +#: utils.py:94 +msgid "Error decoding signature" +msgstr "解码签名时出错" + +#: utils.py:96 +msgid "Invalid token" +msgstr "令牌无效" + +#: utils.py:112 +msgid "Invalid payload" +msgstr "无效载荷" + +#: utils.py:117 +msgid "User is disabled" +msgstr "用户被禁用" diff --git a/translation/refresh_token/zh_Hans/LC_MESSAGES/django.po b/translation/refresh_token/zh_Hans/LC_MESSAGES/django.po new file mode 100644 index 0000000..cef2074 --- /dev/null +++ b/translation/refresh_token/zh_Hans/LC_MESSAGES/django.po @@ -0,0 +1,84 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-04-22 02:13+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: admin/__init__.py:20 +#, python-format +msgid "Revoke selected %(verbose_name_plural)s" +msgstr "吊销选定的%(verbose_name_plural)" + +#: admin/__init__.py:26 +msgid "is expired" +msgstr "已过期" + +#: admin/filters.py:14 +msgid "Yes" +msgstr "是" + +#: admin/filters.py:15 +msgid "No" +msgstr "否" + +#: admin/filters.py:20 +msgid "Expired" +msgstr "过期" + +#: admin/filters.py:32 +msgid "Revoked" +msgstr "吊销" + +#: apps.py:7 +msgid "Refresh token" +msgstr "刷新令牌" + +#: decorators.py:18 +msgid "Refresh token is required" +msgstr "需要刷新令牌" + +#: mixins.py:39 +msgid "Refresh token is expired" +msgstr "刷新令牌已过期" + +#: models.py:20 +msgid "user" +msgstr "用户" + +#: models.py:22 +msgid "token" +msgstr "令牌" + +#: models.py:23 +msgid "created" +msgstr "创建时间" + +#: models.py:24 +msgid "revoked" +msgstr "吊销时间" + +#: models.py:30 +msgid "refresh token" +msgstr "刷新令牌" + +#: models.py:31 +msgid "refresh tokens" +msgstr "刷新令牌" + +#: shortcuts.py:20 +msgid "Invalid refresh token" +msgstr "无效的刷新令牌"