diff --git a/client/src/graphql/gql/mutations/redeemCoupon.gql b/client/src/graphql/gql/mutations/redeemCoupon.gql index 34153ed0..9afa1c2e 100644 --- a/client/src/graphql/gql/mutations/redeemCoupon.gql +++ b/client/src/graphql/gql/mutations/redeemCoupon.gql @@ -1,5 +1,13 @@ mutation Coupon($input: CouponInput!) { coupon(input: $input) { - success + result { + __typename + ... on Success { + message + } + ... on InvalidCoupon { + reason + } + } } } diff --git a/server/oauth/tests/test_coupon.py b/server/oauth/tests/test_coupon.py index fc9a85eb..7134bc58 100644 --- a/server/oauth/tests/test_coupon.py +++ b/server/oauth/tests/test_coupon.py @@ -109,7 +109,9 @@ class CouponTests(TestCase): school_class = SchoolClass.objects.get(users__in=[self.user]) 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) @patch.object( diff --git a/server/schema.graphql b/server/schema.graphql index 479e5550..68ba475d 100644 --- a/server/schema.graphql +++ b/server/schema.graphql @@ -354,7 +354,7 @@ input CouponInput { } type CouponPayload { - success: Boolean + result: RedeemCouponResult clientMutationId: String } @@ -508,7 +508,7 @@ type DuplicateContentBlockPayload { clientMutationId: String } -type DuplicateName { +type DuplicateName implements FailureNode { reason: String } @@ -578,6 +578,10 @@ type InstrumentTypeNode implements Node { type: String! } +type InvalidCoupon implements FailureNode { + reason: String +} + scalar JSONString input JoinClassInput { @@ -938,6 +942,8 @@ type Query { _debug: DjangoDebug } +union RedeemCouponResult = Success | InvalidCoupon + type RoomEntryNode implements Node { id: ID! title: String! diff --git a/server/users/schema/mutations.py b/server/users/schema/mutations.py index 93761178..f2c439e2 100644 --- a/server/users/schema/mutations.py +++ b/server/users/schema/mutations.py @@ -5,17 +5,27 @@ from django.db import IntegrityError from django.db.models import Q from graphene import relay from graphql_relay import from_global_id +from api.types import FailureNode from api.utils import get_object from core.logger import get_logger from users.inputs import PasswordUpdateInput 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 logger = get_logger(__name__) -DUPLICATE_REASON = {"reason": "Dieser Name wird bereits verwendet."} + +DuplicateFailure = DuplicateName(reason="Dieser Name wird bereits verwendet.") class CodeNotFoundException(Exception): @@ -29,8 +39,8 @@ class TeacherOnlyMutation(relay.ClientIDMutation): @classmethod def mutate(cls, root, info, input): user = info.context.user - if 'users.can_manage_school_class_content' not in user.get_role_permissions(): - raise PermissionError('Permission denied') + if "users.can_manage_school_class_content" not in user.get_role_permissions(): + raise PermissionError("Permission denied") return super().mutate(root, info, input) @@ -44,12 +54,12 @@ class UpdatePassword(relay.ClientIDMutation): @classmethod def mutate_and_get_payload(cls, root, info, **kwargs): user = info.context.user - password_data = kwargs.get('password_input') + password_data = kwargs.get("password_input") serializer = PasswordSerialzer(data=password_data, context=user) if serializer.is_valid(): - user.set_password(password_data['new_password']) + user.set_password(password_data["new_password"]) user.save() update_session_auth_hash(info.context, user) return cls(success=True) @@ -75,9 +85,9 @@ class UpdateAvatar(relay.ClientIDMutation): @classmethod def mutate_and_get_payload(cls, root, info, **kwargs): 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(): user.avatar_url = avatar_data user.save() @@ -85,7 +95,6 @@ class UpdateAvatar(relay.ClientIDMutation): errors = [] for key, value in serializer.errors.items(): - error = UpdateError(field=key, errors=[]) for field_error in serializer.errors[key]: @@ -102,12 +111,12 @@ class UpdateSetting(relay.ClientIDMutation): @classmethod 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) user = info.context.user 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) return cls(success=True) @@ -126,22 +135,23 @@ class JoinClass(relay.ClientIDMutation): @classmethod def mutate_and_get_payload(cls, root, info, **kwargs): user = info.context.user - code = kwargs.get('code') + code = kwargs.get("code") try: school_class = SchoolClass.objects.get(Q(code__iexact=code)) if user not in list(school_class.users.all()): - SchoolClassMember.objects.create( - user=user, - school_class=school_class - ) + SchoolClassMember.objects.create(user=user, school_class=school_class) user.set_selected_class(school_class) 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) 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): @@ -154,18 +164,20 @@ class AddRemoveMember(relay.ClientIDMutation): @classmethod def mutate_and_get_payload(cls, root, info, **kwargs): - member_id = kwargs.get('member') - school_class_id = kwargs.get('school_class') - active = kwargs.get('active') + member_id = kwargs.get("member") + school_class_id = kwargs.get("school_class") + active = kwargs.get("active") user = info.context.user member_pk = from_global_id(member_id)[1] school_class = get_object(SchoolClass, school_class_id) 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.save() @@ -182,8 +194,8 @@ class UpdateSchoolClass(TeacherOnlyMutation): @classmethod def mutate_and_get_payload(cls, root, info, **kwargs): - id = kwargs.get('id') - name = kwargs.get('name') + id = kwargs.get("id") + name = kwargs.get("name") user = info.context.user # todo: only allow to edit your own school class @@ -202,7 +214,7 @@ class CreateSchoolClass(TeacherOnlyMutation): @classmethod def mutate_and_get_payload(cls, root, info, **kwargs): - name = kwargs.get('name') + name = kwargs.get("name") user = info.context.user try: @@ -222,7 +234,7 @@ class CreateTeam(TeacherOnlyMutation): @classmethod def mutate_and_get_payload(cls, root, info, **kwargs): - name = kwargs.get('name') + name = kwargs.get("name") user = info.context.user try: @@ -232,7 +244,7 @@ class CreateTeam(TeacherOnlyMutation): user.save() return cls(result=team) except IntegrityError: - return cls(result=DUPLICATE_REASON) + return cls(result=DuplicateFailure) class UpdateTeam(TeacherOnlyMutation): @@ -245,14 +257,14 @@ class UpdateTeam(TeacherOnlyMutation): @classmethod def mutate_and_get_payload(cls, root, info, **kwargs): - id = kwargs.get('id') - name = kwargs.get('name') + id = kwargs.get("id") + name = kwargs.get("name") user = info.context.user team = get_object(Team, id) if user not in team.members.all(): - logger.info('User not part of this team') - raise PermissionError('Permission denied') + logger.info("User not part of this team") + raise PermissionError("Permission denied") team.name = name team.save() @@ -269,7 +281,7 @@ class JoinTeam(TeacherOnlyMutation): @classmethod def mutate_and_get_payload(cls, root, info, **kwargs): user = info.context.user - code = kwargs.get('code') + code = kwargs.get("code") try: team = Team.objects.get(Q(code__iexact=code)) user.team = team @@ -277,7 +289,9 @@ class JoinTeam(TeacherOnlyMutation): return cls(success=True, team=team) 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): diff --git a/server/users/schema/types.py b/server/users/schema/types.py index 61dddcb8..d2510659 100644 --- a/server/users/schema/types.py +++ b/server/users/schema/types.py @@ -8,6 +8,7 @@ from graphene import ObjectType, relay from graphene_django import DjangoObjectType from graphene_django.filter import DjangoFilterConnectionField from graphql_relay import to_global_id +from api.types import FailureNode from books.models import Module from books.schema.queries import ModuleNode @@ -17,25 +18,21 @@ from users.models import SchoolClass, SchoolClassMember, Team, User class RecentModuleFilter(FilterSet): class Meta: model = Module - fields = ('recent_modules',) + fields = ("recent_modules",) - order_by = OrderingFilter( - fields=( - ('recent_modules__visited', 'visited'), - ) - ) + order_by = OrderingFilter(fields=(("recent_modules__visited", "visited"),)) class SchoolClassNode(DjangoObjectType): pk = graphene.Int() - members = graphene.List('users.schema.ClassMemberNode') + members = graphene.List("users.schema.ClassMemberNode") code = graphene.String() read_only = graphene.Boolean() class Meta: model = SchoolClass - only_fields = ['name', 'code', 'members', 'pk', 'read_only'] - filter_fields = ['name'] + only_fields = ["name", "code", "members", "pk", "read_only"] + filter_fields = ["name"] interfaces = (relay.Node,) def resolve_pk(self, *args, **kwargs): @@ -61,11 +58,11 @@ class SchoolClassNode(DjangoObjectType): class TeamNode(DjangoObjectType): class Meta: model = Team - filter_fields = ['name'] + filter_fields = ["name"] interfaces = (relay.Node,) pk = graphene.Int() - members = graphene.List('users.schema.PublicUserNode') + members = graphene.List("users.schema.PublicUserNode") def resolve_pk(self, *args, **kwargs): return self.id @@ -80,7 +77,7 @@ class PublicUserNode(DjangoObjectType): class Meta: 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,) @staticmethod @@ -91,10 +88,22 @@ class PublicUserNode(DjangoObjectType): class PrivateUserNode(DjangoObjectType): class Meta: model = User - filter_fields = ['username', 'email'] - only_fields = ['username', 'email', 'first_name', 'last_name', 'school_classes', 'last_module', - 'last_topic', 'avatar_url', - 'selected_class', 'expiry_date', 'onboarding_visited', 'team', 'read_only'] + filter_fields = ["username", "email"] + only_fields = [ + "username", + "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,) pk = graphene.Int() @@ -104,7 +113,9 @@ class PrivateUserNode(DjangoObjectType): is_teacher = graphene.Boolean() old_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) read_only = graphene.Boolean() @@ -121,7 +132,7 @@ class PrivateUserNode(DjangoObjectType): @staticmethod def resolve_expiry_date(root: User, info): 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: 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 return SchoolClass.objects.none() 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): - 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): # 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 Connection or Edge for a UserNodeConnection is difficult. """ - user = graphene.Field('users.schema.PublicUserNode') + + user = graphene.Field("users.schema.PublicUserNode") active = graphene.Boolean() first_name = graphene.String() last_name = graphene.String() @@ -160,7 +176,7 @@ class ClassMemberNode(ObjectType): is_me = graphene.Boolean() 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): return self.active @@ -183,7 +199,8 @@ class ClassMemberNode(ObjectType): class DuplicateName(graphene.ObjectType): - reason = graphene.String() + class Meta: + interfaces = (FailureNode,) class CreateTeamResult(graphene.Union):