from collections import OrderedDict from django.shortcuts import get_object_or_404 import graphene from graphene.types import Field, InputField from graphene.types.mutation import MutationOptions from graphene.relay.mutation import ClientIDMutation from graphene.types.objecttype import ( yank_fields_from_attrs ) from .serializer_converter import ( convert_serializer_field ) from .types import ErrorType class SerializerMutationOptions(MutationOptions): lookup_field = None model_class = None model_operations = ['create', 'update'] serializer_class = None def fields_for_serializer(serializer, only_fields, exclude_fields, is_input=False): fields = OrderedDict() for name, field in serializer.fields.items(): is_not_in_only = only_fields and name not in only_fields is_excluded = ( name in exclude_fields # or # name in already_created_fields ) if is_not_in_only or is_excluded: continue fields[name] = convert_serializer_field(field, is_input=is_input) return fields class SerializerMutation(ClientIDMutation): class Meta: abstract = True errors = graphene.List( ErrorType, description='May contain more than one error for same field.' ) @classmethod def __init_subclass_with_meta__(cls, lookup_field=None, serializer_class=None, model_class=None, model_operations=['create', 'update'], only_fields=(), exclude_fields=(), **options): if not serializer_class: raise Exception('serializer_class is required for the SerializerMutation') if 'update' not in model_operations and 'create' not in model_operations: raise Exception('model_operations must contain "create" and/or "update"') serializer = serializer_class() if model_class is None: serializer_meta = getattr(serializer_class, 'Meta', None) if serializer_meta: model_class = getattr(serializer_meta, 'model', None) if lookup_field is None and model_class: lookup_field = model_class._meta.pk.name input_fields = fields_for_serializer(serializer, only_fields, exclude_fields, is_input=True) output_fields = fields_for_serializer(serializer, only_fields, exclude_fields, is_input=False) _meta = SerializerMutationOptions(cls) _meta.lookup_field = lookup_field _meta.model_operations = model_operations _meta.serializer_class = serializer_class _meta.model_class = model_class _meta.fields = yank_fields_from_attrs( output_fields, _as=Field, ) input_fields = yank_fields_from_attrs( input_fields, _as=InputField, ) super(SerializerMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options) @classmethod def get_serializer_kwargs(cls, root, info, **input): lookup_field = cls._meta.lookup_field model_class = cls._meta.model_class if model_class: if 'update' in cls._meta.model_operations and lookup_field in input: instance = get_object_or_404(model_class, **{ lookup_field: input[lookup_field]}) elif 'create' in cls._meta.model_operations: instance = None else: raise Exception( 'Invalid update operation. Input parameter "{}" required.'.format( lookup_field )) return { 'instance': instance, 'data': input, 'context': {'request': info.context} } return {'data': input, 'context': {'request': info.context}} @classmethod def mutate_and_get_payload(cls, root, info, **input): kwargs = cls.get_serializer_kwargs(root, info, **input) serializer = cls._meta.serializer_class(**kwargs) if serializer.is_valid(): return cls.perform_mutate(serializer, info) else: errors = [ ErrorType(field=key, messages=value) for key, value in serializer.errors.items() ] return cls(errors=errors) @classmethod def perform_mutate(cls, serializer, info): obj = serializer.save() kwargs = {} for f, field in serializer.fields.items(): kwargs[f] = field.get_attribute(obj) return cls(errors=None, **kwargs)