Skip to content

feat!: check Django model has a default ordering when used in a Relay Connection #1495

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/starwars/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ def __str__(self):


class Ship(models.Model):
class Meta:
ordering = ["pk"]

name = models.CharField(max_length=50)
faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name="ships")

Expand Down
8 changes: 7 additions & 1 deletion graphene_django/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,19 @@ def type(self):
non_null = True
assert issubclass(
_type, DjangoObjectType
), "DjangoConnectionField only accepts DjangoObjectType types"
), "DjangoConnectionField only accepts DjangoObjectType types as underlying type"
assert _type._meta.connection, "The type {} doesn't have a connection".format(
_type.__name__
)
connection_type = _type._meta.connection
if non_null:
return NonNull(connection_type)
# Since Relay Connections require to have a predictible ordering for pagination,
# check on init that the Django model provided has a default ordering declared.
model = connection_type._meta.node._meta.model
assert (
len(getattr(model._meta, "ordering", [])) > 0
), f"Django model {model._meta.app_label}.{model.__name__} has to have a default ordering to be used in a Connection."
return connection_type

@property
Expand Down
3 changes: 3 additions & 0 deletions graphene_django/filter/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@


class Event(models.Model):
class Meta:
ordering = ["pk"]

name = models.CharField(max_length=50)
tags = ArrayField(models.CharField(max_length=50))
tag_ids = ArrayField(models.IntegerField())
Expand Down
12 changes: 12 additions & 0 deletions graphene_django/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@


class Person(models.Model):
class Meta:
ordering = ["pk"]

name = models.CharField(max_length=30)
parent = models.ForeignKey(
"self", on_delete=models.CASCADE, null=True, blank=True, related_name="children"
)


class Pet(models.Model):
class Meta:
ordering = ["pk"]

name = models.CharField(max_length=30)
age = models.PositiveIntegerField()
owner = models.ForeignKey(
Expand All @@ -31,6 +37,9 @@ class FilmDetails(models.Model):


class Film(models.Model):
class Meta:
ordering = ["pk"]

genre = models.CharField(
max_length=2,
help_text="Genre",
Expand All @@ -46,6 +55,9 @@ def get_queryset(self):


class Reporter(models.Model):
class Meta:
ordering = ["pk"]

first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
email = models.EmailField()
Expand Down
36 changes: 34 additions & 2 deletions graphene_django/tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
import re

import pytest
from django.db.models import Count, Prefetch
from django.db.models import Count, Model, Prefetch

from graphene import List, NonNull, ObjectType, Schema, String
from graphene.relay import Node

from ..fields import DjangoListField
from ..fields import DjangoConnectionField, DjangoListField
from ..types import DjangoObjectType
from .models import (
Article as ArticleModel,
Expand Down Expand Up @@ -716,3 +717,34 @@ def resolve_articles(root, info):
r'SELECT .* FROM "tests_film" INNER JOIN "tests_film_reporters" .* LEFT OUTER JOIN "tests_filmdetails"',
captured.captured_queries[1]["sql"],
)


class TestDjangoConnectionField:
def test_model_ordering_assertion(self):
class Chaos(Model):
class Meta:
app_label = "test"

class ChaosType(DjangoObjectType):
class Meta:
model = Chaos
interfaces = (Node,)

class Query(ObjectType):
chaos = DjangoConnectionField(ChaosType)

with pytest.raises(
TypeError,
match=r"Django model test\.Chaos has to have a default ordering to be used in a Connection\.",
):
Schema(query=Query)

def test_only_django_object_types(self):
class Query(ObjectType):
something = DjangoConnectionField(String)

with pytest.raises(
TypeError,
match="DjangoConnectionField only accepts DjangoObjectType types as underlying type",
):
Schema(query=Query)
23 changes: 10 additions & 13 deletions graphene_django/tests/test_types.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import warnings
from collections import OrderedDict, defaultdict
from textwrap import dedent
from unittest.mock import patch
Expand Down Expand Up @@ -399,15 +400,16 @@ class Meta:
with pytest.warns(
UserWarning,
match=r"Field name .* matches an attribute on Django model .* but it's not a model field",
) as record:
):

class Reporter2(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ["first_name", "some_method", "email"]

# Don't warn if selecting a custom field
with pytest.warns(None) as record:
with warnings.catch_warnings():
warnings.simplefilter("error")

class Reporter3(DjangoObjectType):
custom_field = String()
Expand All @@ -416,8 +418,6 @@ class Meta:
model = ReporterModel
fields = ["first_name", "custom_field", "email"]

assert len(record) == 0


@with_local_registry
def test_django_objecttype_exclude_fields_exist_on_model():
Expand Down Expand Up @@ -445,15 +445,14 @@ class Meta:
exclude = ["custom_field"]

# Don't warn on exclude fields
with pytest.warns(None) as record:
with warnings.catch_warnings():
warnings.simplefilter("error")

class Reporter4(DjangoObjectType):
class Meta:
model = ReporterModel
exclude = ["email", "first_name"]

assert len(record) == 0


@with_local_registry
def test_django_objecttype_neither_fields_nor_exclude():
Expand All @@ -467,24 +466,22 @@ class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel

with pytest.warns(None) as record:
with warnings.catch_warnings():
warnings.simplefilter("error")

class Reporter2(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ["email"]

assert len(record) == 0

with pytest.warns(None) as record:
with warnings.catch_warnings():
warnings.simplefilter("error")

class Reporter3(DjangoObjectType):
class Meta:
model = ReporterModel
exclude = ["email"]

assert len(record) == 0


def custom_enum_name(field):
return f"CustomEnum{field.name.title()}"
Expand Down