Update types and schema

This commit is contained in:
Ramon Wenger 2023-05-24 20:28:01 +02:00
parent d1664592ce
commit 266356b510
5 changed files with 108 additions and 61 deletions

View File

@ -1,5 +1,13 @@
mutation Coupon($input: CouponInput!) { mutation Coupon($input: CouponInput!) {
coupon(input: $input) { coupon(input: $input) {
success result {
__typename
... on Success {
message
}
... on InvalidCoupon {
reason
}
}
} }
} }

View File

@ -109,7 +109,9 @@ class CouponTests(TestCase):
school_class = SchoolClass.objects.get(users__in=[self.user]) school_class = SchoolClass.objects.get(users__in=[self.user])
self.assertIsNotNone(school_class) self.assertIsNotNone(school_class)
self.assertTrue(result.get("data").get("coupon").get("success")) self.assertEqual(
result.get("data").get("coupon").get("result").get("__typename"), "Success"
)
self.assertTrue(self.user.is_authenticated) self.assertTrue(self.user.is_authenticated)
@patch.object( @patch.object(

View File

@ -354,7 +354,7 @@ input CouponInput {
} }
type CouponPayload { type CouponPayload {
success: Boolean result: RedeemCouponResult
clientMutationId: String clientMutationId: String
} }
@ -508,7 +508,7 @@ type DuplicateContentBlockPayload {
clientMutationId: String clientMutationId: String
} }
type DuplicateName { type DuplicateName implements FailureNode {
reason: String reason: String
} }
@ -578,6 +578,10 @@ type InstrumentTypeNode implements Node {
type: String! type: String!
} }
type InvalidCoupon implements FailureNode {
reason: String
}
scalar JSONString scalar JSONString
input JoinClassInput { input JoinClassInput {
@ -938,6 +942,8 @@ type Query {
_debug: DjangoDebug _debug: DjangoDebug
} }
union RedeemCouponResult = Success | InvalidCoupon
type RoomEntryNode implements Node { type RoomEntryNode implements Node {
id: ID! id: ID!
title: String! title: String!

View File

@ -5,17 +5,27 @@ from django.db import IntegrityError
from django.db.models import Q from django.db.models import Q
from graphene import relay from graphene import relay
from graphql_relay import from_global_id from graphql_relay import from_global_id
from api.types import FailureNode
from api.utils import get_object from api.utils import get_object
from core.logger import get_logger from core.logger import get_logger
from users.inputs import PasswordUpdateInput from users.inputs import PasswordUpdateInput
from users.models import SchoolClass, SchoolClassMember, Team from users.models import SchoolClass, SchoolClassMember, Team
from users.schema import CreateSchoolClassResult, CreateTeamResult, FieldError, SchoolClassNode, TeamNode, UpdateError from users.schema import (
CreateSchoolClassResult,
CreateTeamResult,
FieldError,
SchoolClassNode,
TeamNode,
UpdateError,
DuplicateName,
)
from users.serializers import AvatarUrlSerializer, PasswordSerialzer from users.serializers import AvatarUrlSerializer, PasswordSerialzer
logger = get_logger(__name__) logger = get_logger(__name__)
DUPLICATE_REASON = {"reason": "Dieser Name wird bereits verwendet."}
DuplicateFailure = DuplicateName(reason="Dieser Name wird bereits verwendet.")
class CodeNotFoundException(Exception): class CodeNotFoundException(Exception):
@ -29,8 +39,8 @@ class TeacherOnlyMutation(relay.ClientIDMutation):
@classmethod @classmethod
def mutate(cls, root, info, input): def mutate(cls, root, info, input):
user = info.context.user user = info.context.user
if 'users.can_manage_school_class_content' not in user.get_role_permissions(): if "users.can_manage_school_class_content" not in user.get_role_permissions():
raise PermissionError('Permission denied') raise PermissionError("Permission denied")
return super().mutate(root, info, input) return super().mutate(root, info, input)
@ -44,12 +54,12 @@ class UpdatePassword(relay.ClientIDMutation):
@classmethod @classmethod
def mutate_and_get_payload(cls, root, info, **kwargs): def mutate_and_get_payload(cls, root, info, **kwargs):
user = info.context.user user = info.context.user
password_data = kwargs.get('password_input') password_data = kwargs.get("password_input")
serializer = PasswordSerialzer(data=password_data, context=user) serializer = PasswordSerialzer(data=password_data, context=user)
if serializer.is_valid(): if serializer.is_valid():
user.set_password(password_data['new_password']) user.set_password(password_data["new_password"])
user.save() user.save()
update_session_auth_hash(info.context, user) update_session_auth_hash(info.context, user)
return cls(success=True) return cls(success=True)
@ -75,9 +85,9 @@ class UpdateAvatar(relay.ClientIDMutation):
@classmethod @classmethod
def mutate_and_get_payload(cls, root, info, **kwargs): def mutate_and_get_payload(cls, root, info, **kwargs):
user = info.context.user user = info.context.user
avatar_data = kwargs.get('avatar_url') avatar_data = kwargs.get("avatar_url")
serializer = AvatarUrlSerializer(data={'avatar_url': avatar_data}) serializer = AvatarUrlSerializer(data={"avatar_url": avatar_data})
if serializer.is_valid(): if serializer.is_valid():
user.avatar_url = avatar_data user.avatar_url = avatar_data
user.save() user.save()
@ -85,7 +95,6 @@ class UpdateAvatar(relay.ClientIDMutation):
errors = [] errors = []
for key, value in serializer.errors.items(): for key, value in serializer.errors.items():
error = UpdateError(field=key, errors=[]) error = UpdateError(field=key, errors=[])
for field_error in serializer.errors[key]: for field_error in serializer.errors[key]:
@ -102,12 +111,12 @@ class UpdateSetting(relay.ClientIDMutation):
@classmethod @classmethod
def mutate_and_get_payload(cls, root, info, **kwargs): def mutate_and_get_payload(cls, root, info, **kwargs):
class_id = kwargs.get('id') class_id = kwargs.get("id")
school_class = get_object(SchoolClass, class_id) school_class = get_object(SchoolClass, class_id)
user = info.context.user user = info.context.user
if school_class and school_class not in user.school_classes.all(): if school_class and school_class not in user.school_classes.all():
raise PermissionDenied('Permission denied: Incorrect school class') raise PermissionDenied("Permission denied: Incorrect school class")
user.set_selected_class(school_class) user.set_selected_class(school_class)
return cls(success=True) return cls(success=True)
@ -126,22 +135,23 @@ class JoinClass(relay.ClientIDMutation):
@classmethod @classmethod
def mutate_and_get_payload(cls, root, info, **kwargs): def mutate_and_get_payload(cls, root, info, **kwargs):
user = info.context.user user = info.context.user
code = kwargs.get('code') code = kwargs.get("code")
try: try:
school_class = SchoolClass.objects.get(Q(code__iexact=code)) school_class = SchoolClass.objects.get(Q(code__iexact=code))
if user not in list(school_class.users.all()): if user not in list(school_class.users.all()):
SchoolClassMember.objects.create( SchoolClassMember.objects.create(user=user, school_class=school_class)
user=user,
school_class=school_class
)
user.set_selected_class(school_class) user.set_selected_class(school_class)
else: else:
raise CodeNotFoundException('[CAJ] Schüler ist bereits in Klasse') # CAJ = Class Already Joined raise CodeNotFoundException(
"[CAJ] Schüler ist bereits in Klasse"
) # CAJ = Class Already Joined
return cls(success=True, school_class=school_class) return cls(success=True, school_class=school_class)
except SchoolClass.DoesNotExist: except SchoolClass.DoesNotExist:
raise CodeNotFoundException('[CNV] Code ist nicht gültig') # CNV = Code Not Valid raise CodeNotFoundException(
"[CNV] Code ist nicht gültig"
) # CNV = Code Not Valid
class AddRemoveMember(relay.ClientIDMutation): class AddRemoveMember(relay.ClientIDMutation):
@ -154,18 +164,20 @@ class AddRemoveMember(relay.ClientIDMutation):
@classmethod @classmethod
def mutate_and_get_payload(cls, root, info, **kwargs): def mutate_and_get_payload(cls, root, info, **kwargs):
member_id = kwargs.get('member') member_id = kwargs.get("member")
school_class_id = kwargs.get('school_class') school_class_id = kwargs.get("school_class")
active = kwargs.get('active') active = kwargs.get("active")
user = info.context.user user = info.context.user
member_pk = from_global_id(member_id)[1] member_pk = from_global_id(member_id)[1]
school_class = get_object(SchoolClass, school_class_id) school_class = get_object(SchoolClass, school_class_id)
if not user.is_teacher() or not school_class.users.filter(pk=user.pk).exists(): if not user.is_teacher() or not school_class.users.filter(pk=user.pk).exists():
raise PermissionError('Permission denied') raise PermissionError("Permission denied")
school_class_member = SchoolClassMember.objects.get(user__pk=member_pk, school_class=school_class) school_class_member = SchoolClassMember.objects.get(
user__pk=member_pk, school_class=school_class
)
school_class_member.active = active school_class_member.active = active
school_class_member.save() school_class_member.save()
@ -182,8 +194,8 @@ class UpdateSchoolClass(TeacherOnlyMutation):
@classmethod @classmethod
def mutate_and_get_payload(cls, root, info, **kwargs): def mutate_and_get_payload(cls, root, info, **kwargs):
id = kwargs.get('id') id = kwargs.get("id")
name = kwargs.get('name') name = kwargs.get("name")
user = info.context.user user = info.context.user
# todo: only allow to edit your own school class # todo: only allow to edit your own school class
@ -202,7 +214,7 @@ class CreateSchoolClass(TeacherOnlyMutation):
@classmethod @classmethod
def mutate_and_get_payload(cls, root, info, **kwargs): def mutate_and_get_payload(cls, root, info, **kwargs):
name = kwargs.get('name') name = kwargs.get("name")
user = info.context.user user = info.context.user
try: try:
@ -222,7 +234,7 @@ class CreateTeam(TeacherOnlyMutation):
@classmethod @classmethod
def mutate_and_get_payload(cls, root, info, **kwargs): def mutate_and_get_payload(cls, root, info, **kwargs):
name = kwargs.get('name') name = kwargs.get("name")
user = info.context.user user = info.context.user
try: try:
@ -232,7 +244,7 @@ class CreateTeam(TeacherOnlyMutation):
user.save() user.save()
return cls(result=team) return cls(result=team)
except IntegrityError: except IntegrityError:
return cls(result=DUPLICATE_REASON) return cls(result=DuplicateFailure)
class UpdateTeam(TeacherOnlyMutation): class UpdateTeam(TeacherOnlyMutation):
@ -245,14 +257,14 @@ class UpdateTeam(TeacherOnlyMutation):
@classmethod @classmethod
def mutate_and_get_payload(cls, root, info, **kwargs): def mutate_and_get_payload(cls, root, info, **kwargs):
id = kwargs.get('id') id = kwargs.get("id")
name = kwargs.get('name') name = kwargs.get("name")
user = info.context.user user = info.context.user
team = get_object(Team, id) team = get_object(Team, id)
if user not in team.members.all(): if user not in team.members.all():
logger.info('User not part of this team') logger.info("User not part of this team")
raise PermissionError('Permission denied') raise PermissionError("Permission denied")
team.name = name team.name = name
team.save() team.save()
@ -269,7 +281,7 @@ class JoinTeam(TeacherOnlyMutation):
@classmethod @classmethod
def mutate_and_get_payload(cls, root, info, **kwargs): def mutate_and_get_payload(cls, root, info, **kwargs):
user = info.context.user user = info.context.user
code = kwargs.get('code') code = kwargs.get("code")
try: try:
team = Team.objects.get(Q(code__iexact=code)) team = Team.objects.get(Q(code__iexact=code))
user.team = team user.team = team
@ -277,7 +289,9 @@ class JoinTeam(TeacherOnlyMutation):
return cls(success=True, team=team) return cls(success=True, team=team)
except Team.DoesNotExist: except Team.DoesNotExist:
raise CodeNotFoundException('[CNV] Code ist nicht gültig') # CNV = Code Not Valid raise CodeNotFoundException(
"[CNV] Code ist nicht gültig"
) # CNV = Code Not Valid
class LeaveTeam(graphene.Mutation): class LeaveTeam(graphene.Mutation):

View File

@ -8,6 +8,7 @@ from graphene import ObjectType, relay
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField from graphene_django.filter import DjangoFilterConnectionField
from graphql_relay import to_global_id from graphql_relay import to_global_id
from api.types import FailureNode
from books.models import Module from books.models import Module
from books.schema.queries import ModuleNode from books.schema.queries import ModuleNode
@ -17,25 +18,21 @@ from users.models import SchoolClass, SchoolClassMember, Team, User
class RecentModuleFilter(FilterSet): class RecentModuleFilter(FilterSet):
class Meta: class Meta:
model = Module model = Module
fields = ('recent_modules',) fields = ("recent_modules",)
order_by = OrderingFilter( order_by = OrderingFilter(fields=(("recent_modules__visited", "visited"),))
fields=(
('recent_modules__visited', 'visited'),
)
)
class SchoolClassNode(DjangoObjectType): class SchoolClassNode(DjangoObjectType):
pk = graphene.Int() pk = graphene.Int()
members = graphene.List('users.schema.ClassMemberNode') members = graphene.List("users.schema.ClassMemberNode")
code = graphene.String() code = graphene.String()
read_only = graphene.Boolean() read_only = graphene.Boolean()
class Meta: class Meta:
model = SchoolClass model = SchoolClass
only_fields = ['name', 'code', 'members', 'pk', 'read_only'] only_fields = ["name", "code", "members", "pk", "read_only"]
filter_fields = ['name'] filter_fields = ["name"]
interfaces = (relay.Node,) interfaces = (relay.Node,)
def resolve_pk(self, *args, **kwargs): def resolve_pk(self, *args, **kwargs):
@ -61,11 +58,11 @@ class SchoolClassNode(DjangoObjectType):
class TeamNode(DjangoObjectType): class TeamNode(DjangoObjectType):
class Meta: class Meta:
model = Team model = Team
filter_fields = ['name'] filter_fields = ["name"]
interfaces = (relay.Node,) interfaces = (relay.Node,)
pk = graphene.Int() pk = graphene.Int()
members = graphene.List('users.schema.PublicUserNode') members = graphene.List("users.schema.PublicUserNode")
def resolve_pk(self, *args, **kwargs): def resolve_pk(self, *args, **kwargs):
return self.id return self.id
@ -80,7 +77,7 @@ class PublicUserNode(DjangoObjectType):
class Meta: class Meta:
model = User model = User
only_fields = ['full_name', 'first_name', 'last_name', 'avatar_url'] only_fields = ["full_name", "first_name", "last_name", "avatar_url"]
interfaces = (relay.Node,) interfaces = (relay.Node,)
@staticmethod @staticmethod
@ -91,10 +88,22 @@ class PublicUserNode(DjangoObjectType):
class PrivateUserNode(DjangoObjectType): class PrivateUserNode(DjangoObjectType):
class Meta: class Meta:
model = User model = User
filter_fields = ['username', 'email'] filter_fields = ["username", "email"]
only_fields = ['username', 'email', 'first_name', 'last_name', 'school_classes', 'last_module', only_fields = [
'last_topic', 'avatar_url', "username",
'selected_class', 'expiry_date', 'onboarding_visited', 'team', 'read_only'] "email",
"first_name",
"last_name",
"school_classes",
"last_module",
"last_topic",
"avatar_url",
"selected_class",
"expiry_date",
"onboarding_visited",
"team",
"read_only",
]
interfaces = (relay.Node,) interfaces = (relay.Node,)
pk = graphene.Int() pk = graphene.Int()
@ -104,7 +113,9 @@ class PrivateUserNode(DjangoObjectType):
is_teacher = graphene.Boolean() is_teacher = graphene.Boolean()
old_classes = graphene.List(SchoolClassNode) old_classes = graphene.List(SchoolClassNode)
school_classes = graphene.List(SchoolClassNode) school_classes = graphene.List(SchoolClassNode)
recent_modules = DjangoFilterConnectionField(ModuleNode, filterset_class=RecentModuleFilter) recent_modules = DjangoFilterConnectionField(
ModuleNode, filterset_class=RecentModuleFilter
)
team = graphene.Field(TeamNode) team = graphene.Field(TeamNode)
read_only = graphene.Boolean() read_only = graphene.Boolean()
@ -121,7 +132,7 @@ class PrivateUserNode(DjangoObjectType):
@staticmethod @staticmethod
def resolve_expiry_date(root: User, info): def resolve_expiry_date(root: User, info):
if not root.hep_id: # concerns users that already have an (old) account if not root.hep_id: # concerns users that already have an (old) account
return format(datetime(2020, 7, 31), 'U') # just set some expiry date return format(datetime(2020, 7, 31), "U") # just set some expiry date
else: else:
return root.license_expiry_date return root.license_expiry_date
@ -133,10 +144,14 @@ class PrivateUserNode(DjangoObjectType):
if root.selected_class is None: # then we don't have any class to return if root.selected_class is None: # then we don't have any class to return
return SchoolClass.objects.none() return SchoolClass.objects.none()
return SchoolClass.objects.filter( return SchoolClass.objects.filter(
Q(schoolclassmember__active=True, schoolclassmember__user=root) | Q(pk=root.selected_class.pk)).distinct() Q(schoolclassmember__active=True, schoolclassmember__user=root)
| Q(pk=root.selected_class.pk)
).distinct()
def resolve_old_classes(self: User, info): def resolve_old_classes(self: User, info):
return SchoolClass.objects.filter(schoolclassmember__active=False, schoolclassmember__user=self) return SchoolClass.objects.filter(
schoolclassmember__active=False, schoolclassmember__user=self
)
def resolve_recent_modules(self, info, **kwargs): def resolve_recent_modules(self, info, **kwargs):
# see https://docs.graphene-python.org/projects/django/en/latest/filtering/ # see https://docs.graphene-python.org/projects/django/en/latest/filtering/
@ -151,7 +166,8 @@ class ClassMemberNode(ObjectType):
We need to build this ourselves, because we want the active property on the node, because providing it on the We need to build this ourselves, because we want the active property on the node, because providing it on the
Connection or Edge for a UserNodeConnection is difficult. Connection or Edge for a UserNodeConnection is difficult.
""" """
user = graphene.Field('users.schema.PublicUserNode')
user = graphene.Field("users.schema.PublicUserNode")
active = graphene.Boolean() active = graphene.Boolean()
first_name = graphene.String() first_name = graphene.String()
last_name = graphene.String() last_name = graphene.String()
@ -160,7 +176,7 @@ class ClassMemberNode(ObjectType):
is_me = graphene.Boolean() is_me = graphene.Boolean()
def resolve_id(self, *args): def resolve_id(self, *args):
return to_global_id('PublicUserNode', self.user.pk) return to_global_id("PublicUserNode", self.user.pk)
def resolve_active(self, *args): def resolve_active(self, *args):
return self.active return self.active
@ -183,7 +199,8 @@ class ClassMemberNode(ObjectType):
class DuplicateName(graphene.ObjectType): class DuplicateName(graphene.ObjectType):
reason = graphene.String() class Meta:
interfaces = (FailureNode,)
class CreateTeamResult(graphene.Union): class CreateTeamResult(graphene.Union):