Update error handling in mutation for school class creation

This commit is contained in:
Ramon Wenger 2022-04-08 17:32:10 +02:00
parent d09844a67b
commit 4973037486
13 changed files with 148 additions and 85 deletions

View File

@ -1,9 +1,14 @@
mutation CreateSchoolClass($input: CreateSchoolClassInput!) { mutation CreateSchoolClass($input: CreateSchoolClassInput!) {
createSchoolClass(input: $input) { createSchoolClass(input: $input) {
success result {
schoolClass { __typename
...on SchoolClassNode {
id id
name name
} }
...on DuplicateName {
reason
}
}
} }
} }

View File

@ -22,8 +22,7 @@ from surveys.schema import SurveysQuery
from surveys.mutations import SurveyMutations from surveys.mutations import SurveyMutations
from rooms.mutations import RoomMutations from rooms.mutations import RoomMutations
from rooms.schema import RoomsQuery, ModuleRoomsQuery from rooms.schema import RoomsQuery, ModuleRoomsQuery
from users.schema import AllUsersQuery, UsersQuery from users.schema import AllUsersQuery, UsersQuery, ProfileMutations
from users.mutations import ProfileMutations
class Query(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery, class Query(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery,

View File

@ -2,7 +2,7 @@ import graphene
from django.conf import settings from django.conf import settings
from graphene_django.debug import DjangoDebug from graphene_django.debug import DjangoDebug
from users.mutations_public import UserMutations from users.schema import UserMutations
class PublicMutation(UserMutations, graphene.ObjectType): class PublicMutation(UserMutations, graphene.ObjectType):

View File

@ -2,6 +2,7 @@ import graphene
from graphene import relay from graphene import relay
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from api.graphene_wagtail import GenericStreamFieldType
from api.utils import get_object from api.utils import get_object
from notes.models import InstrumentBookmark from notes.models import InstrumentBookmark
from notes.schema import InstrumentBookmarkNode from notes.schema import InstrumentBookmarkNode
@ -25,6 +26,7 @@ class InstrumentTypeNode(DjangoObjectType):
class InstrumentNode(DjangoObjectType): class InstrumentNode(DjangoObjectType):
bookmarks = graphene.List(InstrumentBookmarkNode) bookmarks = graphene.List(InstrumentBookmarkNode)
type = graphene.Field(InstrumentTypeNode) type = graphene.Field(InstrumentTypeNode)
contents = GenericStreamFieldType()
class Meta: class Meta:
model = BasicKnowledge model = BasicKnowledge

View File

@ -52,6 +52,7 @@ class Module(StrictHierarchyPage):
parent_page_types = ['books.Topic'] parent_page_types = ['books.Topic']
subpage_types = ['books.Chapter'] subpage_types = ['books.Chapter']
#todo: isn't this a duplicate definition?
def get_child_ids(self): def get_child_ids(self):
return self.get_children().values_list('id', flat=True) return self.get_children().values_list('id', flat=True)

View File

@ -29,5 +29,3 @@ class SkillboxTestCase(TestCase):
user = self.teacher user = self.teacher
request.user = user request.user = user
return GQLClient(schema=schema, context_value=request) return GQLClient(schema=schema, context_value=request)

View File

@ -181,9 +181,9 @@ input AssignmentInput {
} }
type AssignmentNode implements Node { type AssignmentNode implements Node {
id: ID!
created: DateTime! created: DateTime!
modified: DateTime! modified: DateTime!
id: ID!
title: String! title: String!
assignment: String! assignment: String!
solution: String solution: String
@ -197,9 +197,9 @@ type AssignmentNode implements Node {
} }
type ChapterBookmarkNode implements Node { type ChapterBookmarkNode implements Node {
id: ID!
user: PrivateUserNode! user: PrivateUserNode!
note: NoteNode note: NoteNode
id: ID!
chapter: ChapterNode! chapter: ChapterNode!
} }
@ -343,11 +343,12 @@ input CreateSchoolClassInput {
} }
type CreateSchoolClassPayload { type CreateSchoolClassPayload {
success: Boolean result: CreateSchoolClassResult
schoolClass: SchoolClassNode
clientMutationId: String clientMutationId: String
} }
union CreateSchoolClassResult = SchoolClassNode | DuplicateName
input CreateSnapshotInput { input CreateSnapshotInput {
module: String! module: String!
selectedClass: ID! selectedClass: ID!
@ -463,6 +464,10 @@ type DjangoDebugSQL {
encoding: String encoding: String
} }
type DuplicateName {
reason: String
}
type FieldError { type FieldError {
code: String code: String
} }
@ -482,9 +487,9 @@ enum InputTypes {
} }
type InstrumentBookmarkNode implements Node { type InstrumentBookmarkNode implements Node {
id: ID!
user: PrivateUserNode! user: PrivateUserNode!
note: NoteNode note: NoteNode
id: ID!
uuid: UUID uuid: UUID
instrument: InstrumentNode! instrument: InstrumentNode!
} }
@ -555,9 +560,9 @@ type Logout {
} }
type ModuleBookmarkNode { type ModuleBookmarkNode {
id: ID!
user: PrivateUserNode! user: PrivateUserNode!
note: NoteNode note: NoteNode
id: ID!
module: ModuleNode! module: ModuleNode!
} }
@ -869,10 +874,10 @@ type Query {
} }
type RoomEntryNode implements Node { type RoomEntryNode implements Node {
id: ID!
title: String! title: String!
description: String description: String
slug: String! slug: String!
id: ID!
room: RoomNode! room: RoomNode!
author: PublicUserNode author: PublicUserNode
contents: GenericStreamFieldType contents: GenericStreamFieldType
@ -891,10 +896,10 @@ type RoomEntryNodeEdge {
} }
type RoomNode implements Node { type RoomNode implements Node {
id: ID!
title: String! title: String!
description: String description: String
slug: String! slug: String!
id: ID!
schoolClass: SchoolClassNode! schoolClass: SchoolClassNode!
appearance: String! appearance: String!
userCreated: Boolean! userCreated: Boolean!
@ -1017,9 +1022,9 @@ type SpellCheckStepNode {
} }
type StudentSubmissionNode implements Node { type StudentSubmissionNode implements Node {
id: ID!
created: DateTime! created: DateTime!
modified: DateTime! modified: DateTime!
id: ID!
text: String! text: String!
document: String! document: String!
assignment: AssignmentNode! assignment: AssignmentNode!
@ -1088,10 +1093,10 @@ type SyncModuleVisibilityPayload {
} }
type TeamNode implements Node { type TeamNode implements Node {
name: String!
code: String
id: ID! id: ID!
name: String!
isDeleted: Boolean! isDeleted: Boolean!
code: String
creator: PrivateUserNode creator: PrivateUserNode
members: [PublicUserNode] members: [PublicUserNode]
pk: Int pk: Int

View File

@ -0,0 +1,4 @@
from .queries import *
from .types import *
from .mutations import *
from .mutations_public import *

View File

@ -1,6 +1,7 @@
import graphene import graphene
from django.contrib.auth import update_session_auth_hash from django.contrib.auth import update_session_auth_hash
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
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
@ -9,8 +10,9 @@ 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 SchoolClassNode, TeamNode from users.schema import SchoolClassNode, TeamNode, UpdateError, FieldError, CreateSchoolClassResult
from users.serializers import PasswordSerialzer, AvatarUrlSerializer from users.serializers import AvatarUrlSerializer, PasswordSerialzer
logger = get_logger(__name__) logger = get_logger(__name__)
@ -19,15 +21,6 @@ class CodeNotFoundException(Exception):
pass pass
class FieldError(graphene.ObjectType):
code = graphene.String()
class UpdateError(graphene.ObjectType):
field = graphene.String()
errors = graphene.List(FieldError)
class TeacherOnlyMutation(relay.ClientIDMutation): class TeacherOnlyMutation(relay.ClientIDMutation):
class Meta: class Meta:
abstract = True abstract = True
@ -204,18 +197,20 @@ class CreateSchoolClass(TeacherOnlyMutation):
class Input: class Input:
name = graphene.String() name = graphene.String()
success = graphene.Boolean() result = CreateSchoolClassResult()
school_class = graphene.Field(SchoolClassNode)
@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:
school_class = SchoolClass.objects.create(name=name) school_class = SchoolClass.objects.create(name=name)
SchoolClassMember.objects.create(school_class=school_class, user=user) SchoolClassMember.objects.create(school_class=school_class, user=user)
user.set_selected_class(school_class) user.set_selected_class(school_class)
return cls(success=True, school_class=school_class) return cls(result=school_class)
except IntegrityError:
return cls(result={"reason": "Name wird bereits verwendet"})
class CreateTeam(TeacherOnlyMutation): class CreateTeam(TeacherOnlyMutation):

View File

@ -0,0 +1,43 @@
import graphene
from graphene_django.filter import DjangoFilterConnectionField
from basicknowledge.models import BasicKnowledge
from basicknowledge.queries import InstrumentNode
from books.models import Module
from books.schema.queries import ModuleNode
from users.models import User
from .types import PrivateUserNode
class UsersQuery(object):
me = graphene.Field(PrivateUserNode)
all_users = DjangoFilterConnectionField(PrivateUserNode)
my_activity = DjangoFilterConnectionField(ModuleNode)
my_instrument_activity = DjangoFilterConnectionField(InstrumentNode)
def resolve_me(self, info, **kwargs):
return info.context.user
def resolve_all_users(self, info, **kwargs):
if not info.context.user.is_superuser:
return User.objects.none()
else:
return User.objects.all()
def resolve_my_activity(self, info, **kwargs):
return Module.objects.all()
def resolve_my_instrument_activity(self, info, **kwargs):
return BasicKnowledge.objects.all()
class AllUsersQuery(object):
me = graphene.Field(PrivateUserNode)
all_users = DjangoFilterConnectionField(PrivateUserNode)
def resolve_all_users(self, info, **kwargs):
if not info.context.user.is_superuser:
return User.objects.none()
else:
return User.objects.all()

View File

@ -9,13 +9,23 @@ 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 basicknowledge.models import BasicKnowledge
from basicknowledge.queries import InstrumentNode
from books.models import Module from books.models import Module
from books.schema.queries import ModuleNode from books.schema.queries import ModuleNode
from users.models import SchoolClass, SchoolClassMember, Team, User from users.models import SchoolClass, SchoolClassMember, Team, User
class RecentModuleFilter(FilterSet):
class Meta:
model = Module
fields = ('recent_modules',)
order_by = OrderingFilter(
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')
@ -64,18 +74,6 @@ class TeamNode(DjangoObjectType):
return self.members.all() return self.members.all()
class RecentModuleFilter(FilterSet):
class Meta:
model = Module
fields = ('recent_modules',)
order_by = OrderingFilter(
fields=(
('recent_modules__visited', 'visited'),
)
)
class PublicUserNode(DjangoObjectType): class PublicUserNode(DjangoObjectType):
full_name = graphene.String(required=True) full_name = graphene.String(required=True)
is_me = graphene.Boolean() is_me = graphene.Boolean()
@ -184,34 +182,25 @@ class ClassMemberNode(ObjectType):
return info.context.user.pk == parent.user.pk return info.context.user.pk == parent.user.pk
class UsersQuery(object): class DuplicateName(graphene.ObjectType):
me = graphene.Field(PrivateUserNode) reason = graphene.String()
all_users = DjangoFilterConnectionField(PrivateUserNode)
my_activity = DjangoFilterConnectionField(ModuleNode)
my_instrument_activity = DjangoFilterConnectionField(InstrumentNode)
def resolve_me(self, info, **kwargs):
return info.context.user
def resolve_all_users(self, info, **kwargs):
if not info.context.user.is_superuser:
return User.objects.none()
else:
return User.objects.all()
def resolve_my_activity(self, info, **kwargs):
return Module.objects.all()
def resolve_my_instrument_activity(self, info, **kwargs):
return BasicKnowledge.objects.all()
class AllUsersQuery(object): class CreateSchoolClassResult(graphene.Union):
me = graphene.Field(PrivateUserNode) class Meta:
all_users = DjangoFilterConnectionField(PrivateUserNode) types = (SchoolClassNode, DuplicateName)
def resolve_all_users(self, info, **kwargs): @classmethod
if not info.context.user.is_superuser: def resolve_type(cls, instance, info):
return User.objects.none() if type(instance).__name__ == "SchoolClass":
else: return SchoolClassNode
return User.objects.all() return DuplicateName
class FieldError(graphene.ObjectType):
code = graphene.String()
class UpdateError(graphene.ObjectType):
field = graphene.String()
errors = graphene.List(FieldError)

View File

@ -1,11 +1,12 @@
from django.test import TestCase, RequestFactory from django.db import transaction
from django.test import TestCase
from graphene import Context from graphene import Context
from graphene.test import Client from graphene.test import Client
from graphql_relay import to_global_id from graphql_relay import to_global_id
from api.schema import schema from api.schema import schema
from api.utils import get_graphql_mutation, get_object from api.utils import get_graphql_mutation, get_object
from core.factories import UserFactory, TeacherFactory from core.factories import TeacherFactory, UserFactory
from core.tests.base_test import SkillboxTestCase from core.tests.base_test import SkillboxTestCase
from users.models import SchoolClass, User from users.models import SchoolClass, User
from users.services import create_users from users.services import create_users
@ -101,20 +102,41 @@ class ModifySchoolClassTest(SkillboxTestCase):
self.assertEqual(SchoolClass.objects.count(), 2) self.assertEqual(SchoolClass.objects.count(), 2)
class_name = 'Moordale' class_name = 'Moordale'
mutation = get_graphql_mutation('createClass.gql') mutation = get_graphql_mutation('createClass.gql')
result = self.client.execute(mutation, variables={ query_result = self.client.execute(mutation, variables={
'input': { 'input': {
'name': class_name 'name': class_name
} }
}) })
self.assertIsNone(result.get('errors')) self.assertIsNone(query_result.get('errors'))
id = result.get('data').get('createSchoolClass').get('schoolClass').get('id') result = query_result.get('data').get('createSchoolClass').get('result')
self.assertEqual(result.get('__typename'), 'SchoolClassNode')
id = result.get('id')
self.assertEqual(SchoolClass.objects.count(), 3) self.assertEqual(SchoolClass.objects.count(), 3)
school_class = get_object(SchoolClass, id) school_class = get_object(SchoolClass, id)
self.assertEqual(school_class.name, class_name) self.assertEqual(school_class.name, class_name)
self.assertEqual(school_class.get_teacher(), self.teacher) self.assertEqual(school_class.get_teacher(), self.teacher)
self.assertEqual(self.teacher.selected_class.name, class_name) self.assertEqual(self.teacher.selected_class.name, class_name)
def test_create_school_class_fail(self): def test_create_school_class_duplicate_name_fail(self):
self.assertEqual(SchoolClass.objects.count(), 2)
class_name = 'skillbox'
mutation = get_graphql_mutation('createClass.gql')
# if we don't do this, django wraps the whole test in an atomic operation,
# and we trigger an exception so the query later in the test would fail
with transaction.atomic():
query_result = self.client.execute(mutation, variables={
'input': {
'name': class_name
}
})
self.assertIsNone(query_result.get('errors'))
result = query_result.get('data').get('createSchoolClass').get('result')
self.assertEqual(result.get('__typename'), 'DuplicateName')
reason = result.get('reason')
self.assertEqual(reason, 'Name wird bereits verwendet')
self.assertEqual(SchoolClass.objects.count(), 2)
def test_create_school_class_fail_permission(self):
self.assertEqual(SchoolClass.objects.count(), 2) self.assertEqual(SchoolClass.objects.count(), 2)
mutation = get_graphql_mutation('createClass.gql') mutation = get_graphql_mutation('createClass.gql')
result = self.student_client.execute(mutation, variables={ result = self.student_client.execute(mutation, variables={