Skip to content

Commit

Permalink
Merge branch 'feature/model_inspection' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
pavlov99 committed Jan 6, 2015
2 parents d139d80 + aece212 commit cd06009
Show file tree
Hide file tree
Showing 9 changed files with 757 additions and 385 deletions.
3 changes: 3 additions & 0 deletions jsonapi/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Meta:

from . import statuses
from .utils import Choices
from .model_inspector import ModelInspector

logger = logging.getLogger(__name__)

Expand All @@ -48,6 +49,8 @@ def __init__(self):
self._resources = []
self.base_url = None # base server url
self.api_url = None # api root url
self.model_inspector = ModelInspector()
self.model_inspector.inspect()

@property
def resource_map(self):
Expand Down
173 changes: 147 additions & 26 deletions jsonapi/model_inspector.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
from collections import namedtuple
from django.db import models
from django.contrib.auth import get_user_model
from .utils import Choices
from .django_utils import get_model_name, get_model_by_name
from .django_utils import get_model_name


ModelInfo = namedtuple("ModelInfo", [
"model", "fields_own", "fields_to_one", "fields_to_many"
])
class ModelInfo(object):

def __init__(self, fields_own=None, fields_to_one=None, fields_to_many=None,
auth_user_paths=None, is_user=None):
self.fields_own = fields_own or []
self.fields_to_one = fields_to_one or []
self.fields_to_many = fields_to_many or []
self.auth_user_paths = auth_user_paths or []
self.is_user = is_user

@property
def relation_fields(self):
return self.fields_to_one + self.fields_to_many


class Field(object):

""" Field information.
is_bidirectional is True if related model has reference to current model as
well.
Example:
A -@ B -> BChild
A has reference to B, but not BChild, but both B and BChild have reference
to A. A to B fields are bidirectional, BChild to A field is not
bidirectional.
"""

CATEGORIES = Choices(
('own', 'OWN'),
('to_one', 'TO_ONE'),
Expand All @@ -21,9 +45,22 @@ def __init__(self, name, category, related_model=None):
self.name = name
self.related_model = related_model
self.category = category
self.is_bidirectional = None

def query_name(self):
""" Get field name used in queries."""
return get_model_name(get_parent(self.related_model))

def __repr__(self):
return self.name
suffix = "({})".format(get_model_name(self.related_model))\
if self.related_model else ""
return "<Field: {}{}>".format(self.name, suffix)

def __hash__(self):
return hash((self.name, self.related_model, self.category))

def __eq__(self, other):
return hash(self) == hash(other)


def get_parent(model):
Expand All @@ -37,23 +74,54 @@ class ModelInspector(object):

""" Inspect Django models."""

FIELD_TYPES = Choices(
('own', 'OWN'),
('to_one', 'TO_ONE'),
('to_many', 'TO_MANY'),
)
def inspect(self):
user_model = get_user_model()

self.models = {
model: ModelInfo(
fields_own=self._get_fields_own(model),
fields_to_one=self._get_fields_self_foreign_key(model),
fields_to_many=self._get_fields_others_foreign_key(model) +
self._get_fields_self_many_to_many(model) +
self._get_fields_others_many_to_many(model),
is_user=(model is user_model or issubclass(model, user_model))
) for model in models.get_models()
}

for model, model_info in self.models.items():
if model_info.is_user:
model_info.auth_user_paths = ['']
else:
self._update_auth_user_paths_model(model)

@classmethod
def _filter_child_model_fields(cls, fields):
""" Keep only related model fields.
def __init__(self):
self.models = [ModelInfo(
model=model,
fields_own=self._get_fields_own(model),
fields_to_one=self._get_fields_self_foreign_key(model),
fields_to_many=self._get_fields_others_foreign_key(model) +
self._get_fields_others_foreign_key(model)
) for model in models.get_models()]
Example: Inherited models: A -> B -> C
B has one-to-many relationship to BMany.
after inspection BMany would have links to B and C. Keep only B. Parent
model A could not be used (It would not be in fields)
def inspect(self):
pass
:param list fields: model fields.
:return list fields: filtered fields.
"""
indexes_to_remove = set([])
for index1, field1 in enumerate(fields):
for index2, field2 in enumerate(fields):
if index1 < index2 and index1 not in indexes_to_remove and\
index2 not in indexes_to_remove:
if issubclass(field1.related_model, field2.related_model):
indexes_to_remove.add(index1)

if issubclass(field2.related_model, field1.related_model):
indexes_to_remove.add(index2)

fields = [field for index, field in enumerate(fields)
if index not in indexes_to_remove]

return fields

@classmethod
def _get_fields_own(cls, model):
Expand Down Expand Up @@ -81,24 +149,32 @@ def _get_fields_self_foreign_key(cls, model):

@classmethod
def _get_fields_others_foreign_key(cls, model):
""" Get to-namy related field.
If related model has children, link current model only to related. Child
links make relationship complicated.
"""
fields = [
Field(
name=field.rel.related_name or "{}_set".format(
get_model_name(get_parent(related_model))),
get_model_name(related_model)),
related_model=related_model,
category=Field.CATEGORIES.TO_MANY
) for related_model in models.get_models()
if not related_model._meta.proxy
for field in related_model._meta.fields
if related_model != model and field.rel
and field.rel.to == model and field.rel.multiple
if field.rel and field.rel.to is model._meta.concrete_model and
field.rel.multiple
]
fields = cls._filter_child_model_fields(fields)
return fields

@classmethod
def _get_fields_self_many_to_many(cls, model):
fields = [
Field(
name=fields.name,
name=field.name,
related_model=field.rel.to,
category=Field.CATEGORIES.TO_MANY
) for field in model._meta.many_to_many
Expand All @@ -114,7 +190,52 @@ def _get_fields_others_many_to_many(cls, model):
related_model=related_model,
category=Field.CATEGORIES.TO_MANY
) for related_model in models.get_models()
if related_model is not model
for field in related_model._meta.many_to_many
if related_model != model and field.rel.to == model
if field.rel.to is model._meta.concrete_model
]
fields = cls._filter_child_model_fields(fields)
return fields

def _update_auth_user_paths_model(self, model):
# (field from previous model, related field of this model, model)
paths = [[(None, None, model)]]

while paths:
current_paths = paths
paths = []

for current_path in current_paths:
current_model = current_path[-1][-1]
current_model_info = self.models[current_model]

# NOTE: calculate used models links. Link is defined by model
# and field used.
used_links = set()
for node1, node2 in zip(current_path[:-1], current_path[1:]):
used_links.add((node1[2], node1[1]))
used_links.add((node2[2], node2[1]))
used_links.add((node1[2], node2[0]))

for field in current_model_info.relation_fields:
related_model = field.related_model
related_model_info = self.models[related_model]

for related_field in related_model_info.relation_fields:
related_related_model = related_field.related_model
if (related_related_model is current_model or
issubclass(current_model, related_related_model)) \
and (current_model, field) not in used_links \
and (related_model, related_field) not in \
used_links:

path = current_path + [
(field, related_field, related_model)]

if related_model_info.is_user:
self.models[model].auth_user_paths.append(
"__".join([
get_model_name(p[2]) for p in path[1:]
]))
else:
paths.append(path)
81 changes: 81 additions & 0 deletions tests/testapp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,25 @@
@ @
Comment
Also Setup classes with different relationship types for tests.
B is inherited from A
All of the relationship for A class are defined in A
All of the relationship for B class are defined in related classes
There is no OneToMany relationship in Django, so there are no AMany
and BOne classes.
AAbstractOne AOne BManyToMany -> BManyToManyChild
| | @
| | |
@ @ @
User--@AAbstract => AA -> A -----> B ------- BProxy
@ @ |
| | |
@ @ @
AAbstractManyToMany AManyToMany BMany
"""
from django.db import models
from django.contrib.auth.models import User
Expand Down Expand Up @@ -64,3 +83,65 @@ class TestSerializerAllFields(models.Model):
text = models.TextField()
time = models.TimeField()
url = models.URLField()


class AAbstractOne(models.Model):
field = models.IntegerField()


class AAbstractManyToMany(models.Model):
field = models.IntegerField()


class AAbstract(models.Model):
class Meta:
abstract = True

field_abstract = models.IntegerField()
user = models.ForeignKey(User)
a_abstract_one = models.ForeignKey(AAbstractOne)
a_abstract_many_to_manys = models.ManyToManyField(
AAbstractManyToMany,
related_name="%(app_label)s_%(class)s_related"
)


class AA(AAbstract):
pass


class AOne(models.Model):
field = models.IntegerField()


class AManyToMany(models.Model):
field = models.IntegerField()


class A(AA):
field_a = models.IntegerField()
a_one = models.ForeignKey(AOne)
a_many_to_manys = models.ManyToManyField(AManyToMany)


class B(A):
field_b = models.IntegerField()


class BMany(models.Model):
field = models.IntegerField()
b = models.ForeignKey(B, related_name="bmanys")


class BManyToMany(models.Model):
field = models.IntegerField()
bs = models.ManyToManyField(B, related_name="bmanytomanys")


class BManyToManyChild(BManyToMany):
pass


class BProxy(B):
class Meta:
proxy = True
1 change: 0 additions & 1 deletion tests/testapp/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
INSTALLED_APPS += (
'tests.testapp.test_api',
'tests.testapp.test_resource_meta',
'tests.testapp.test_resource_relationship',
)

if django.VERSION[:2] < (1, 6):
Expand Down
Empty file.
Loading

0 comments on commit cd06009

Please sign in to comment.