mirror of
https://github.com/graphql-python/graphene-django.git
synced 2024-11-29 13:03:44 +03:00
b7e4937775
* Create new fields and exclude options that are aliased to exclude_fields and only_fields * Update docs * Add some checking around fields and exclude definitions * Add all fields option * Update docs to include `__all__` option * Actual order of fields is not stable * Update docs/queries.rst Co-Authored-By: Semyon Pupkov <semen.pupkov@gmail.com> * Fix example code * Format code * Start raising PendingDeprecationWarnings for using only_fields and exclude_fields * Update tests
240 lines
7.8 KiB
Python
240 lines
7.8 KiB
Python
import warnings
|
|
from collections import OrderedDict
|
|
|
|
import six
|
|
from django.db.models import Model
|
|
from django.utils.functional import SimpleLazyObject
|
|
|
|
import graphene
|
|
from graphene import Field
|
|
from graphene.relay import Connection, Node
|
|
from graphene.types.objecttype import ObjectType, ObjectTypeOptions
|
|
from graphene.types.utils import yank_fields_from_attrs
|
|
|
|
from .converter import convert_django_field_with_choices
|
|
from .registry import Registry, get_global_registry
|
|
from .settings import graphene_settings
|
|
from .utils import (
|
|
DJANGO_FILTER_INSTALLED,
|
|
camelize,
|
|
get_model_fields,
|
|
is_valid_django_model,
|
|
)
|
|
|
|
if six.PY3:
|
|
from typing import Type
|
|
|
|
|
|
ALL_FIELDS = "__all__"
|
|
|
|
|
|
def construct_fields(
|
|
model, registry, only_fields, exclude_fields, convert_choices_to_enum
|
|
):
|
|
_model_fields = get_model_fields(model)
|
|
|
|
fields = OrderedDict()
|
|
for name, field in _model_fields:
|
|
is_not_in_only = only_fields and name not in only_fields
|
|
# is_already_created = name in options.fields
|
|
is_excluded = name in exclude_fields # or is_already_created
|
|
# https://docs.djangoproject.com/en/1.10/ref/models/fields/#django.db.models.ForeignKey.related_query_name
|
|
is_no_backref = str(name).endswith("+")
|
|
if is_not_in_only or is_excluded or is_no_backref:
|
|
# We skip this field if we specify only_fields and is not
|
|
# in there. Or when we exclude this field in exclude_fields.
|
|
# Or when there is no back reference.
|
|
continue
|
|
|
|
_convert_choices_to_enum = convert_choices_to_enum
|
|
if not isinstance(_convert_choices_to_enum, bool):
|
|
# then `convert_choices_to_enum` is a list of field names to convert
|
|
if name in _convert_choices_to_enum:
|
|
_convert_choices_to_enum = True
|
|
else:
|
|
_convert_choices_to_enum = False
|
|
|
|
converted = convert_django_field_with_choices(
|
|
field, registry, convert_choices_to_enum=_convert_choices_to_enum
|
|
)
|
|
fields[name] = converted
|
|
|
|
return fields
|
|
|
|
|
|
class DjangoObjectTypeOptions(ObjectTypeOptions):
|
|
model = None # type: Model
|
|
registry = None # type: Registry
|
|
connection = None # type: Type[Connection]
|
|
|
|
filter_fields = ()
|
|
filterset_class = None
|
|
|
|
|
|
class DjangoObjectType(ObjectType):
|
|
@classmethod
|
|
def __init_subclass_with_meta__(
|
|
cls,
|
|
model=None,
|
|
registry=None,
|
|
skip_registry=False,
|
|
only_fields=(), # deprecated in favour of `fields`
|
|
fields=(),
|
|
exclude_fields=(), # deprecated in favour of `exclude`
|
|
exclude=(),
|
|
filter_fields=None,
|
|
filterset_class=None,
|
|
connection=None,
|
|
connection_class=None,
|
|
use_connection=None,
|
|
interfaces=(),
|
|
convert_choices_to_enum=True,
|
|
_meta=None,
|
|
**options
|
|
):
|
|
assert is_valid_django_model(model), (
|
|
'You need to pass a valid Django Model in {}.Meta, received "{}".'
|
|
).format(cls.__name__, model)
|
|
|
|
if not registry:
|
|
registry = get_global_registry()
|
|
|
|
assert isinstance(registry, Registry), (
|
|
"The attribute registry in {} needs to be an instance of "
|
|
'Registry, received "{}".'
|
|
).format(cls.__name__, registry)
|
|
|
|
if filter_fields and filterset_class:
|
|
raise Exception("Can't set both filter_fields and filterset_class")
|
|
|
|
if not DJANGO_FILTER_INSTALLED and (filter_fields or filterset_class):
|
|
raise Exception(
|
|
(
|
|
"Can only set filter_fields or filterset_class if "
|
|
"Django-Filter is installed"
|
|
)
|
|
)
|
|
|
|
assert not (fields and exclude), (
|
|
"Cannot set both 'fields' and 'exclude' options on "
|
|
"DjangoObjectType {class_name}.".format(class_name=cls.__name__)
|
|
)
|
|
|
|
# Alias only_fields -> fields
|
|
if only_fields and fields:
|
|
raise Exception("Can't set both only_fields and fields")
|
|
if only_fields:
|
|
warnings.warn(
|
|
"Defining `only_fields` is deprecated in favour of `fields`.",
|
|
PendingDeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
fields = only_fields
|
|
if fields and fields != ALL_FIELDS and not isinstance(fields, (list, tuple)):
|
|
raise TypeError(
|
|
'The `fields` option must be a list or tuple or "__all__". '
|
|
"Got %s." % type(fields).__name__
|
|
)
|
|
|
|
if fields == ALL_FIELDS:
|
|
fields = None
|
|
|
|
# Alias exclude_fields -> exclude
|
|
if exclude_fields and exclude:
|
|
raise Exception("Can't set both exclude_fields and exclude")
|
|
if exclude_fields:
|
|
warnings.warn(
|
|
"Defining `exclude_fields` is deprecated in favour of `exclude`.",
|
|
PendingDeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
exclude = exclude_fields
|
|
if exclude and not isinstance(exclude, (list, tuple)):
|
|
raise TypeError(
|
|
"The `exclude` option must be a list or tuple. Got %s."
|
|
% type(exclude).__name__
|
|
)
|
|
|
|
django_fields = yank_fields_from_attrs(
|
|
construct_fields(model, registry, fields, exclude, convert_choices_to_enum),
|
|
_as=Field,
|
|
)
|
|
|
|
if use_connection is None and interfaces:
|
|
use_connection = any(
|
|
(issubclass(interface, Node) for interface in interfaces)
|
|
)
|
|
|
|
if use_connection and not connection:
|
|
# We create the connection automatically
|
|
if not connection_class:
|
|
connection_class = Connection
|
|
|
|
connection = connection_class.create_type(
|
|
"{}Connection".format(cls.__name__), node=cls
|
|
)
|
|
|
|
if connection is not None:
|
|
assert issubclass(connection, Connection), (
|
|
"The connection must be a Connection. Received {}"
|
|
).format(connection.__name__)
|
|
|
|
if not _meta:
|
|
_meta = DjangoObjectTypeOptions(cls)
|
|
|
|
_meta.model = model
|
|
_meta.registry = registry
|
|
_meta.filter_fields = filter_fields
|
|
_meta.filterset_class = filterset_class
|
|
_meta.fields = django_fields
|
|
_meta.connection = connection
|
|
|
|
super(DjangoObjectType, cls).__init_subclass_with_meta__(
|
|
_meta=_meta, interfaces=interfaces, **options
|
|
)
|
|
|
|
if not skip_registry:
|
|
registry.register(cls)
|
|
|
|
def resolve_id(self, info):
|
|
return self.pk
|
|
|
|
@classmethod
|
|
def is_type_of(cls, root, info):
|
|
if isinstance(root, SimpleLazyObject):
|
|
root._setup()
|
|
root = root._wrapped
|
|
if isinstance(root, cls):
|
|
return True
|
|
if not is_valid_django_model(type(root)):
|
|
raise Exception(('Received incompatible instance "{}".').format(root))
|
|
|
|
if cls._meta.model._meta.proxy:
|
|
model = root._meta.model
|
|
else:
|
|
model = root._meta.model._meta.concrete_model
|
|
|
|
return model == cls._meta.model
|
|
|
|
@classmethod
|
|
def get_queryset(cls, queryset, info):
|
|
return queryset
|
|
|
|
@classmethod
|
|
def get_node(cls, info, id):
|
|
queryset = cls.get_queryset(cls._meta.model.objects, info)
|
|
try:
|
|
return queryset.get(pk=id)
|
|
except cls._meta.model.DoesNotExist:
|
|
return None
|
|
|
|
|
|
class ErrorType(ObjectType):
|
|
field = graphene.String(required=True)
|
|
messages = graphene.List(graphene.NonNull(graphene.String), required=True)
|
|
|
|
@classmethod
|
|
def from_errors(cls, errors):
|
|
data = camelize(errors) if graphene_settings.CAMELCASE_ERRORS else errors
|
|
return [cls(field=key, messages=value) for key, value in data.items()]
|