Skip to content
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

Basic Proxy model support #373

Merged
merged 3 commits into from
Feb 11, 2018
Merged
Show file tree
Hide file tree
Changes from 2 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
19 changes: 16 additions & 3 deletions examples/starwars/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,36 @@

class Character(models.Model):
name = models.CharField(max_length=50)
ship = models.ForeignKey('Ship', blank=True, null=True, related_name='characters')
ship = models.ForeignKey(
'Ship',
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name='characters'
)

def __str__(self):
return self.name


class Faction(models.Model):
name = models.CharField(max_length=50)
hero = models.ForeignKey(Character)
hero = models.ForeignKey(
Character,
on_delete=models.SET_NULL,
)

def __str__(self):
return self.name


class Ship(models.Model):
name = models.CharField(max_length=50)
faction = models.ForeignKey(Faction, related_name='ships')
faction = models.ForeignKey(
Faction,
on_delete=models.SET_NULL,
related_name='ships'
)

def __str__(self):
return self.name
45 changes: 42 additions & 3 deletions graphene_django/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ class Pet(models.Model):

class FilmDetails(models.Model):
location = models.CharField(max_length=30)
film = models.OneToOneField('Film', related_name='details')
film = models.OneToOneField(
'Film',
on_delete=models.CASCADE,
related_name='details'
)


class Film(models.Model):
Expand All @@ -30,15 +34,50 @@ class Reporter(models.Model):
pets = models.ManyToManyField('self')
a_choice = models.CharField(max_length=30, choices=CHOICES)

reporter_type = models.IntegerField(
'Reporter Type',
null=True,
blank=True,
choices=[(1, u'Regular'), (2, u'CNN Reporter')]
)

def __str__(self): # __unicode__ on Python 2
return "%s %s" % (self.first_name, self.last_name)

def __init__(self, *args, **kwargs):
"""
Override the init method so that during runtime, Django
can know that this object can be a CNNReporter by casting
it to the proxy model. Otherwise, as far as Django knows,
when a CNNReporter is pulled from the database, it is still
of type Reporter. This was added to test proxy model support.
"""
super(Reporter, self).__init__(*args, **kwargs)
if self.reporter_type == 2: # quick and dirty way without enums
self.__class__ = CNNReporter

class CNNReporter(Reporter):
"""
This class is a proxy model for Reporter, used for testing
proxy model support
"""
class Meta:
proxy = True


class Article(models.Model):
headline = models.CharField(max_length=100)
pub_date = models.DateField()
reporter = models.ForeignKey(Reporter, related_name='articles')
editor = models.ForeignKey(Reporter, related_name='edited_articles_+')
reporter = models.ForeignKey(
Reporter,
on_delete=models.SET_NULL,
related_name='articles'
)
editor = models.ForeignKey(
Reporter,
on_delete=models.SET_NULL,
related_name='edited_articles_+'
)
lang = models.CharField(max_length=2, help_text='Language', choices=[
('es', 'Spanish'),
('en', 'English')
Expand Down
143 changes: 142 additions & 1 deletion graphene_django/tests/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
from ..fields import DjangoConnectionField
from ..types import DjangoObjectType
from ..settings import graphene_settings
from .models import Article, Reporter
from .models import (
Article,
CNNReporter,
Reporter,
)

pytestmark = pytest.mark.django_db

Expand Down Expand Up @@ -689,6 +693,7 @@ class Query(graphene.ObjectType):
email='johndoe@example.com',
a_choice=1
)

Article.objects.create(
headline='Article Node 1',
pub_date=datetime.date.today(),
Expand Down Expand Up @@ -780,3 +785,139 @@ class Query(graphene.ObjectType):
'''
result = schema.execute(query)
assert not result.errors


def test_proxy_model_support():
"""
This test asserts that we can query for all Reporters,
even if some are of a proxy model type at runtime.
"""
class ReporterType(DjangoObjectType):

class Meta:
model = Reporter
interfaces = (Node, )
use_connection = True

reporter_1 = Reporter.objects.create(
first_name='John',
last_name='Doe',
email='johndoe@example.com',
a_choice=1
)

reporter_2 = CNNReporter.objects.create(
first_name='Some',
last_name='Guy',
email='someguy@cnn.com',
a_choice=1,
reporter_type=2, # set this guy to be CNN
)

class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType)

schema = graphene.Schema(query=Query)
query = '''
query ProxyModelQuery {
allReporters {
edges {
node {
id
}
}
}
}
'''

expected = {
'allReporters': {
'edges': [{
'node': {
'id': 'UmVwb3J0ZXJUeXBlOjE=',
},
},
{
'node': {
'id': 'UmVwb3J0ZXJUeXBlOjI=',
},
}
]
}
}

result = schema.execute(query)
assert not result.errors
assert result.data == expected


def test_proxy_model_fails():
"""
This test asserts that if you try to query for a proxy model,
that query will fail with:
GraphQLError('Expected value of type "CNNReporterType" but got:
CNNReporter.',)

This is because a proxy model has the identical model definition
to its superclass, and defines its behavior at runtime, rather than
at the database level. Currently, filtering objects of the proxy models'
type isn't supported. It would require a field on the model that would
represent the type, and it doesn't seem like there is a clear way to
enforce this pattern across all projects
"""
class CNNReporterType(DjangoObjectType):

class Meta:
model = CNNReporter
interfaces = (Node, )
use_connection = True

reporter_1 = Reporter.objects.create(
first_name='John',
last_name='Doe',
email='johndoe@example.com',
a_choice=1
)

reporter_2 = CNNReporter.objects.create(
first_name='Some',
last_name='Guy',
email='someguy@cnn.com',
a_choice=1,
reporter_type=2, # set this guy to be CNN
)

class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(CNNReporterType)

schema = graphene.Schema(query=Query)
query = '''
query ProxyModelQuery {
allReporters {
edges {
node {
id
}
}
}
}
'''

expected = {
'allReporters': {
'edges': [{
'node': {
'id': 'UmVwb3J0ZXJUeXBlOjE=',
},
},
{
'node': {
'id': 'UmVwb3J0ZXJUeXBlOjI=',
},
}
]
}
}

result = schema.execute(query)
assert result.errors
1 change: 1 addition & 0 deletions graphene_django/tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Meta:
'email',
'pets',
'a_choice',
'reporter_type'
]

assert sorted(fields[-2:]) == [
Expand Down
8 changes: 7 additions & 1 deletion graphene_django/tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def test_django_get_node(get):
def test_django_objecttype_map_correct_fields():
fields = Reporter._meta.fields
fields = list(fields.keys())
assert fields[:-2] == ['id', 'first_name', 'last_name', 'email', 'pets', 'a_choice']
assert fields[:-2] == ['id', 'first_name', 'last_name', 'email', 'pets', 'a_choice', 'reporter_type']
assert sorted(fields[-2:]) == ['articles', 'films']


Expand Down Expand Up @@ -124,6 +124,7 @@ def test_schema_representation():
email: String!
pets: [Reporter]
aChoice: ReporterAChoice!
reporterType: ReporterReporterType
articles(before: String, after: String, first: Int, last: Int): ArticleConnection
}

Expand All @@ -132,6 +133,11 @@ def test_schema_representation():
A_2
}

enum ReporterReporterType {
A_1
A_2
}

type RootQuery {
node(id: ID!): Node
}
Expand Down
3 changes: 2 additions & 1 deletion graphene_django/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ def is_type_of(cls, root, info):
raise Exception((
'Received incompatible instance "{}".'
).format(root))
model = root._meta.model

model = root._meta.model._meta.concrete_model
return model == cls._meta.model

@classmethod
Expand Down