-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #111 from CentreForDigitalHumanities/feature/episo…
…de-form Feature/update episode form
- Loading branch information
Showing
46 changed files
with
1,195 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
from graphene import ID, Boolean, InputObjectType, List, NonNull, ResolveInfo, String | ||
from django.core.exceptions import ObjectDoesNotExist | ||
from event.models import Episode | ||
from graphql_app.LettercraftMutation import LettercraftMutation | ||
|
||
from graphql_app.types.LettercraftErrorType import LettercraftErrorType | ||
|
||
|
||
class UpdateEpisodeMutationInput(InputObjectType): | ||
id = ID(required=True) | ||
name = String() | ||
book = String() | ||
chapter = String() | ||
page = String() | ||
designators = List(NonNull(String)) | ||
summary = String() | ||
categories = List(NonNull(ID)) | ||
|
||
|
||
class UpdateEpisodeMutation(LettercraftMutation): | ||
ok = Boolean(required=True) | ||
errors = List(NonNull(LettercraftErrorType), required=True) | ||
|
||
django_model = Episode | ||
|
||
class Arguments: | ||
input = UpdateEpisodeMutationInput(required=True) | ||
|
||
@classmethod | ||
def mutate(cls, root: None, info: ResolveInfo, input: UpdateEpisodeMutationInput): | ||
try: | ||
retrieved_object = cls.get_or_create_object(info, input) | ||
except ObjectDoesNotExist as e: | ||
error = LettercraftErrorType(field="id", messages=[str(e)]) | ||
return cls(ok=False, errors=[error]) # type: ignore | ||
|
||
episode = retrieved_object.object | ||
|
||
try: | ||
cls.mutate_object(input, episode, info) | ||
except ObjectDoesNotExist as field: | ||
error = LettercraftErrorType( | ||
field=str(field), messages=["Related object cannot be found."] | ||
) | ||
return cls(ok=False, errors=[error]) # type: ignore | ||
|
||
episode.save() | ||
|
||
return cls(ok=True, errors=[]) # type: ignore |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
from typing import Type | ||
from dataclasses import dataclass | ||
from graphene import InputObjectType, Mutation, ResolveInfo | ||
from django.db.models import Model | ||
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist | ||
from graphene_django.types import DjangoObjectType | ||
from graphene_django.registry import get_global_registry | ||
|
||
|
||
@dataclass | ||
class RetrievedObject: | ||
object: Model | ||
created: bool | ||
|
||
|
||
class LettercraftMutation(Mutation): | ||
""" | ||
An extension of Graphene's base Mutation class. | ||
""" | ||
|
||
django_model: Type[Model] | None = None | ||
|
||
@classmethod | ||
def get_or_create_object( | ||
cls, info: ResolveInfo, mutation_input: InputObjectType | ||
) -> RetrievedObject: | ||
""" | ||
Retrieves a Django model instance by id. If the instance does not exist, a new instance is created (but not saved to the database). The instance will be empty, i.e. will not have any (obligatory) fields set. | ||
Any mutation making use of this method should have a class field 'django_model' defined referring to the class of the object that is being mutated. | ||
Args: | ||
- mutation_input (InputObjectType): The mutation input. | ||
- info (ResolveInfo): An object containing the request and user information. | ||
Exceptions: | ||
- ImproperlyConfigured is raised in two cases: | ||
1. when the mutation class implementing LettercraftMutation does not have the required class field 'django_model' defined; | ||
2. when the corresponding Graphene type cannot be found. This exception should not be caught. | ||
- ObjectDoesNotExist is raised when an ID is provided but the corresponding object is not found or inaccessible to the user making the request. | ||
""" | ||
|
||
if cls.django_model is None: | ||
raise ImproperlyConfigured( | ||
"Mutation class must have a class field 'django_model' defined." | ||
) | ||
|
||
model = cls.django_model | ||
|
||
id = getattr(mutation_input, "id", None) | ||
|
||
# The registry is a global object that holds all the models and types. | ||
# It is used to get the corresponding Graphene type for a Django model. | ||
registry = get_global_registry() | ||
graphene_type = registry.get_type_for_model(model) | ||
|
||
if graphene_type is None: | ||
raise ImproperlyConfigured("Graphene type not found for model.") | ||
|
||
if id is None: | ||
return RetrievedObject(object=model(), created=True) | ||
|
||
try: | ||
retrieved = graphene_type.get_queryset(model.objects, info).get(pk=id) | ||
except model.DoesNotExist: | ||
raise ObjectDoesNotExist("Object not found.") | ||
|
||
return RetrievedObject(object=retrieved, created=False) | ||
|
||
@staticmethod | ||
def mutate_object( | ||
input: InputObjectType, | ||
object_instance: Model, | ||
resolve_info: ResolveInfo, | ||
excluded_fields: list[str] = [], | ||
) -> None: | ||
""" | ||
Updates both simple and related fields in a GraphQL mutation. | ||
Simple fields are simply set on the mutated object. | ||
For related fields, the corresponding Django models and Graphene types are retrieved, | ||
and type.get_queryset() is called to ensure the user has access to those particular models. | ||
Args: | ||
- input (dict): A dictionary containing the updated values for the related fields. | ||
object_instance (Model): The Django model instance to be updated. | ||
- resolve_info (ResolveInfo): An object containing the request and user information. | ||
excluded_fields (List[str]): A list of fields to be excluded from the update. | ||
Exceptions: | ||
- ObjectDoesNotExist is raised when an ID of a ForeignKey field is provided but the corresponding object is not found or inaccessible to the user making the request. This exception exposes the name of the field. | ||
""" | ||
|
||
# The Graphene registry maps Django models to Graphene types. | ||
registry = get_global_registry() | ||
|
||
simple_fields = [] | ||
one_to_many_fields = [] | ||
many_to_many_fields = [] | ||
|
||
for key, value in input.items(): | ||
# Skip excluded fields | ||
if key in excluded_fields: | ||
continue | ||
|
||
field = object_instance._meta.get_field(key) | ||
|
||
# Triage | ||
if field.many_to_many is True: | ||
many_to_many_fields.append(key) | ||
elif field.many_to_one is True: | ||
one_to_many_fields.append(key) | ||
else: | ||
simple_fields.append(key) | ||
|
||
# Update simple fields | ||
for key in simple_fields: | ||
try: | ||
value = getattr(input, key) | ||
setattr(object_instance, key, value) | ||
except AttributeError: | ||
pass | ||
|
||
# Update one-to-many fields. | ||
# Only selects existing objects on the 'many' side | ||
# of the relationship. | ||
for key in one_to_many_fields: | ||
try: | ||
value = getattr(input, key) | ||
except AttributeError: | ||
continue | ||
|
||
if value is None or value == "": | ||
setattr(object_instance, key, None) | ||
continue | ||
|
||
field = object_instance._meta.get_field(key) | ||
django_model = field.related_model # type: Model | None | ||
|
||
if django_model is None: | ||
continue | ||
|
||
graphene_type = registry.get_type_for_model(django_model) | ||
|
||
try: | ||
accessible_object = graphene_type.get_queryset( | ||
django_model.objects, resolve_info | ||
).get(id=value) | ||
except django_model.DoesNotExist: | ||
raise ObjectDoesNotExist(key) | ||
|
||
setattr(object_instance, key, accessible_object) | ||
|
||
# Saving the object instance so we can use it for many-to-many fields. | ||
object_instance.save() | ||
|
||
# Update many-to-many fields | ||
for key in many_to_many_fields: | ||
try: | ||
value = getattr(input, key) | ||
except AttributeError: | ||
continue | ||
|
||
if value is None: | ||
getattr(object_instance, key).set([]) | ||
continue | ||
|
||
field = object_instance._meta.get_field(key) | ||
django_model = field.related_model | ||
graphene_type = registry.get_type_for_model(django_model) | ||
|
||
accessible_objects = graphene_type.get_queryset( | ||
django_model.objects, resolve_info | ||
) | ||
|
||
new_ids = [ | ||
item["id"] | ||
for item in accessible_objects.filter(id__in=value).values("id") | ||
] | ||
|
||
getattr(object_instance, key).set(new_ids) | ||
|
||
# All subclasses of Mutation must implement the mutate method. | ||
@classmethod | ||
def mutate(cls, root: None, info: ResolveInfo, *args, **kwargs): | ||
cls.mutate(root, info, *args, **kwargs) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
from typing import List | ||
from graphene_django.types import ErrorType | ||
|
||
|
||
class LettercraftErrorType(ErrorType): | ||
""" | ||
A simple wrapper around Graphene-Django's ErrorType with a constructor. | ||
""" | ||
|
||
def __init__(self, field: str, messages: List[str]): | ||
super().__init__() | ||
self.field = field | ||
self.messages = messages |
Oops, something went wrong.