Skip to content

Commit 96c09ac

Browse files
tcleonardThomas Leonard
and
Thomas Leonard
authored
feat!: check django model has a default ordering when used in a relay connection (#1495)
Co-authored-by: Thomas Leonard <[email protected]>
1 parent b85177c commit 96c09ac

File tree

6 files changed

+69
-16
lines changed

6 files changed

+69
-16
lines changed

examples/starwars/models.py

+3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ def __str__(self):
2424

2525

2626
class Ship(models.Model):
27+
class Meta:
28+
ordering = ["pk"]
29+
2730
name = models.CharField(max_length=50)
2831
faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name="ships")
2932

graphene_django/fields.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,19 @@ def type(self):
101101
non_null = True
102102
assert issubclass(
103103
_type, DjangoObjectType
104-
), "DjangoConnectionField only accepts DjangoObjectType types"
104+
), "DjangoConnectionField only accepts DjangoObjectType types as underlying type"
105105
assert _type._meta.connection, "The type {} doesn't have a connection".format(
106106
_type.__name__
107107
)
108108
connection_type = _type._meta.connection
109109
if non_null:
110110
return NonNull(connection_type)
111+
# Since Relay Connections require to have a predictible ordering for pagination,
112+
# check on init that the Django model provided has a default ordering declared.
113+
model = connection_type._meta.node._meta.model
114+
assert (
115+
len(getattr(model._meta, "ordering", [])) > 0
116+
), f"Django model {model._meta.app_label}.{model.__name__} has to have a default ordering to be used in a Connection."
111117
return connection_type
112118

113119
@property

graphene_django/filter/tests/conftest.py

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626

2727

2828
class Event(models.Model):
29+
class Meta:
30+
ordering = ["pk"]
31+
2932
name = models.CharField(max_length=50)
3033
tags = ArrayField(models.CharField(max_length=50))
3134
tag_ids = ArrayField(models.IntegerField())

graphene_django/tests/models.py

+12
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@
55

66

77
class Person(models.Model):
8+
class Meta:
9+
ordering = ["pk"]
10+
811
name = models.CharField(max_length=30)
912
parent = models.ForeignKey(
1013
"self", on_delete=models.CASCADE, null=True, blank=True, related_name="children"
1114
)
1215

1316

1417
class Pet(models.Model):
18+
class Meta:
19+
ordering = ["pk"]
20+
1521
name = models.CharField(max_length=30)
1622
age = models.PositiveIntegerField()
1723
owner = models.ForeignKey(
@@ -31,6 +37,9 @@ class FilmDetails(models.Model):
3137

3238

3339
class Film(models.Model):
40+
class Meta:
41+
ordering = ["pk"]
42+
3443
genre = models.CharField(
3544
max_length=2,
3645
help_text="Genre",
@@ -46,6 +55,9 @@ def get_queryset(self):
4655

4756

4857
class Reporter(models.Model):
58+
class Meta:
59+
ordering = ["pk"]
60+
4961
first_name = models.CharField(max_length=30)
5062
last_name = models.CharField(max_length=30)
5163
email = models.EmailField()

graphene_django/tests/test_fields.py

+34-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
import re
33

44
import pytest
5-
from django.db.models import Count, Prefetch
5+
from django.db.models import Count, Model, Prefetch
66

77
from graphene import List, NonNull, ObjectType, Schema, String
8+
from graphene.relay import Node
89

9-
from ..fields import DjangoListField
10+
from ..fields import DjangoConnectionField, DjangoListField
1011
from ..types import DjangoObjectType
1112
from .models import (
1213
Article as ArticleModel,
@@ -716,3 +717,34 @@ def resolve_articles(root, info):
716717
r'SELECT .* FROM "tests_film" INNER JOIN "tests_film_reporters" .* LEFT OUTER JOIN "tests_filmdetails"',
717718
captured.captured_queries[1]["sql"],
718719
)
720+
721+
722+
class TestDjangoConnectionField:
723+
def test_model_ordering_assertion(self):
724+
class Chaos(Model):
725+
class Meta:
726+
app_label = "test"
727+
728+
class ChaosType(DjangoObjectType):
729+
class Meta:
730+
model = Chaos
731+
interfaces = (Node,)
732+
733+
class Query(ObjectType):
734+
chaos = DjangoConnectionField(ChaosType)
735+
736+
with pytest.raises(
737+
TypeError,
738+
match=r"Django model test\.Chaos has to have a default ordering to be used in a Connection\.",
739+
):
740+
Schema(query=Query)
741+
742+
def test_only_django_object_types(self):
743+
class Query(ObjectType):
744+
something = DjangoConnectionField(String)
745+
746+
with pytest.raises(
747+
TypeError,
748+
match="DjangoConnectionField only accepts DjangoObjectType types as underlying type",
749+
):
750+
Schema(query=Query)

graphene_django/tests/test_types.py

+10-13
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import warnings
12
from collections import OrderedDict, defaultdict
23
from textwrap import dedent
34
from unittest.mock import patch
@@ -399,15 +400,16 @@ class Meta:
399400
with pytest.warns(
400401
UserWarning,
401402
match=r"Field name .* matches an attribute on Django model .* but it's not a model field",
402-
) as record:
403+
):
403404

404405
class Reporter2(DjangoObjectType):
405406
class Meta:
406407
model = ReporterModel
407408
fields = ["first_name", "some_method", "email"]
408409

409410
# Don't warn if selecting a custom field
410-
with pytest.warns(None) as record:
411+
with warnings.catch_warnings():
412+
warnings.simplefilter("error")
411413

412414
class Reporter3(DjangoObjectType):
413415
custom_field = String()
@@ -416,8 +418,6 @@ class Meta:
416418
model = ReporterModel
417419
fields = ["first_name", "custom_field", "email"]
418420

419-
assert len(record) == 0
420-
421421

422422
@with_local_registry
423423
def test_django_objecttype_exclude_fields_exist_on_model():
@@ -445,15 +445,14 @@ class Meta:
445445
exclude = ["custom_field"]
446446

447447
# Don't warn on exclude fields
448-
with pytest.warns(None) as record:
448+
with warnings.catch_warnings():
449+
warnings.simplefilter("error")
449450

450451
class Reporter4(DjangoObjectType):
451452
class Meta:
452453
model = ReporterModel
453454
exclude = ["email", "first_name"]
454455

455-
assert len(record) == 0
456-
457456

458457
@with_local_registry
459458
def test_django_objecttype_neither_fields_nor_exclude():
@@ -467,24 +466,22 @@ class Reporter(DjangoObjectType):
467466
class Meta:
468467
model = ReporterModel
469468

470-
with pytest.warns(None) as record:
469+
with warnings.catch_warnings():
470+
warnings.simplefilter("error")
471471

472472
class Reporter2(DjangoObjectType):
473473
class Meta:
474474
model = ReporterModel
475475
fields = ["email"]
476476

477-
assert len(record) == 0
478-
479-
with pytest.warns(None) as record:
477+
with warnings.catch_warnings():
478+
warnings.simplefilter("error")
480479

481480
class Reporter3(DjangoObjectType):
482481
class Meta:
483482
model = ReporterModel
484483
exclude = ["email"]
485484

486-
assert len(record) == 0
487-
488485

489486
def custom_enum_name(field):
490487
return f"CustomEnum{field.name.title()}"

0 commit comments

Comments
 (0)