Skip to content

Commit

Permalink
Merge branch 'release/0.7.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
pavlov99 committed Feb 27, 2015
2 parents 17547bf + b06ff1f commit 6bf2b85
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 117 deletions.
2 changes: 1 addition & 1 deletion jsonapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
""" JSON:API realization."""
__version = (0, 6, 12)
__version = (0, 7, 0)

__version__ = version = '.'.join(map(str, __version))
__project__ = PROJECT = __name__
12 changes: 12 additions & 0 deletions jsonapi/model_inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,17 @@ def __init__(self, name, category, related_model=None):
self.category = category
self.is_bidirectional = None

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

@property
def related_resource_name(self):
name = self.query_name
name += "s" if self.category == self.CATEGORIES.TO_MANY else ""
return name

def __repr__(self):
suffix = "({})".format(get_model_name(self.related_model))\
if self.related_model else ""
Expand Down Expand Up @@ -96,6 +103,11 @@ def inspect(self):
else:
self._update_auth_user_paths_model(model)

model_info.field_resource_map = {
f.related_resource_name: f
for f in model_info.fields_to_one + model_info.fields_to_many
}

@classmethod
def _filter_child_model_fields(cls, fields):
""" Keep only related model fields.
Expand Down
53 changes: 37 additions & 16 deletions jsonapi/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ def get_queryset(cls, user=None, **kwargs):
"""
queryset = cls.Meta.model.objects
queryset = cls.update_user_queryset(queryset, user=user, **kwargs)
if cls.Meta.authenticators:
queryset = cls.update_user_queryset(queryset, user, **kwargs)
return queryset

@classmethod
Expand All @@ -194,13 +195,12 @@ def update_user_queryset(cls, queryset, user=None, **kwargs):
Method is used to control permissions during resource management.
"""
if cls.Meta.authenticators:
user_filter = models.Q()
for path in cls.Meta.model_info.auth_user_paths:
querydict = {path: user} if path else {"id": user.id}
user_filter = user_filter | models.Q(**querydict)
user_filter = models.Q()
for path in cls.Meta.model_info.auth_user_paths:
querydict = {path: user} if path else {"id": user.id}
user_filter = user_filter | models.Q(**querydict)

queryset = queryset.filter(user_filter)
queryset = queryset.filter(user_filter)

return queryset

Expand Down Expand Up @@ -243,6 +243,31 @@ def get_form(cls, fields=None):
})
return Form

@classmethod
def _get_include_structure(cls, include=None):
result = []
include = include or []

for include_path in include:
current_model = cls.Meta.model
field_path = []

for include_name in include_path.split('.'):
model_info = model_inspector.models[current_model]
field = model_info.field_resource_map[include_name]
field_path.append(field)
current_model = field.related_model

result.append({
"field_path": field_path,
"model_info": model_inspector.models[current_model],
"resource": cls.Meta.api.model_resource_map[current_model],
"type": field_path[-1].related_resource_name,
"query": "__".join([f.query_name for f in field_path])
})

return result

@classmethod
def get(cls, request=None, **kwargs):
""" Get resource http response.
Expand All @@ -266,13 +291,16 @@ def get(cls, request=None, **kwargs):
if 'sort' in kwargs:
queryset = queryset.order_by(*kwargs['sort'])

include = queryargs.get("include", [])
include_structure = cls._get_include_structure(include)
# TODO: update queryset based on include parameters.

# Fields serialisation
# NOTE: currently filter only own fields
model_info = cls.Meta.model_info
fields_own = model_info.fields_own
if queryargs['fields']:
fieldnames = queryargs['fields']
fieldnames.append("id") # add id to fieldset
fields_own = [f for f in fields_own if f.name in fieldnames]

objects = queryset
Expand All @@ -291,18 +319,11 @@ def get(cls, request=None, **kwargs):
meta["page_prev"] = objects.previous_page_number() \
if objects.has_previous() else None

fields_include = set(queryargs.get("include", []))
fields_to_one = [f for f in model_info.fields_to_one
if f.name in fields_include]
fields_to_many = [f for f in model_info.fields_to_many
if f.name in fields_include]

response = cls.dump_documents(
cls,
objects,
fields_own=fields_own,
fields_to_one=fields_to_one,
fields_to_many=fields_to_many
include_structure=include_structure
)
if meta:
response["meta"] = meta
Expand Down
137 changes: 71 additions & 66 deletions jsonapi/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,13 @@ class Serializer(object):
Meta = SerializerMeta

@classmethod
def dump_document(cls, model_instance, fields_own=None, fields_to_one=None,
fields_to_many=None):
def dump_document(cls, instance, fields_own=None, fields_to_many=None):
""" Get document for model_instance.
redefine dump rule for field x: def dump_document_x
:param django.db.models.Model model_instance: model instance
:param list<str> or None fields: model_instance field to dump
:param django.db.models.Model instance: model instance
:param list<Field> or None fields: model_instance field to dump
:return dict: document
Related documents are not included to current one. In case of to-many
Expand All @@ -85,26 +84,33 @@ def dump_document(cls, model_instance, fields_own=None, fields_to_one=None,
Add them to fields_own.
"""
default_fields_own = [
f.name for f in model_instance._meta.fields if not f.rel]
fields_own = fields_own or default_fields_own
fields_own = (set(fields_own) | set(cls.Meta.fieldnames_include))\
if fields_own is not None:
fields_own = {f.name for f in fields_own}
else:
fields_own = {
f.name for f in instance._meta.fields
if f.rel is None and f.serialize
}
fields_own.add('id')

fields_own = (fields_own | set(cls.Meta.fieldnames_include))\
- set(cls.Meta.fieldnames_exclude)

document = {}
# Include own fields
for fieldname in fields_own:
value = getattr(model_instance, fieldname)
value = getattr(instance, fieldname)
field_serializer = getattr(
cls, "dump_document_{}".format(fieldname), None)

if field_serializer is not None:
value = field_serializer(model_instance)
value = field_serializer(instance)
else:
try:
field = model_instance._meta.get_field(fieldname)
field = instance._meta.get_field(fieldname)
except models.fields.FieldDoesNotExist:
# Field is property
value = getattr(model_instance, fieldname)
value = getattr(instance, fieldname)
else:
if isinstance(field, models.fields.files.FileField):
# TODO: Serializer depends on API here.
Expand All @@ -114,39 +120,51 @@ def dump_document(cls, model_instance, fields_own=None, fields_to_one=None,

document[fieldname] = value

fields_to_many = fields_to_many or []
for field in model_instance._meta.fields:
if field.rel:
# Include to-one fields. It does not require database calls
for field in instance._meta.fields:
fieldname = "{}_id".format(field.name)
if field.rel and fieldname not in cls.Meta.fieldnames_exclude:
document["links"] = document.get("links") or {}
document["links"][field.name] = getattr(
model_instance, "{}_id".format(field.name))
document["links"][field.name] = getattr(instance, fieldname)

for fieldname in fields_to_many:
# Include to-many fields. It requires database calls. At this point we
# assume that model was prefetch_related with child objects, which would
# be included into 'linked' attribute. Here we need to add ids of linked
# objects. To avoid database calls, iterate over objects manually and
# get ids.
fields_to_many = fields_to_many or []
for field in fields_to_many:
document["links"] = document.get("links") or {}
document["links"][fieldname] = list(
getattr(model_instance, fieldname).
values_list("id", flat=True)
)
document["links"][field.related_resource_name] = [
obj.id for obj in getattr(instance, field.name).all()]

return document

@classmethod
def dump_documents(cls, resource, model_instances, fields_own=None,
fields_to_one=None, fields_to_many=None):
include_structure=None):
model_instances = list(model_instances)
model_info = resource.Meta.model_info
include_structure = include_structure or []

fields_to_many = set()
for include_object in include_structure:
f = include_object["field_path"][0]
if f.category == f.CATEGORIES.TO_MANY:
fields_to_many.add(f)

data = {
resource.Meta.name_plural: [
cls.dump_document(
m,
fields_own=[f.name for f in fields_own],
fields_to_one=[f.name for f in fields_to_one],
fields_to_many=[f.name for f in fields_to_many]
fields_own=fields_own,
fields_to_many=fields_to_many
)
for m in model_instances
]
}

model_info = resource.Meta.model_info

# TODO: move links generation to other method.
if model_info.fields_to_one or fields_to_many:
data["links"] = {}
for field in model_info.fields_to_one:
Expand All @@ -156,43 +174,30 @@ def dump_documents(cls, resource, model_instances, fields_own=None,
"/{" + linkname + "}"
})

fields_to_one = fields_to_one or []
fields_to_many = fields_to_many or []

if fields_to_one or fields_to_many:
data["linked"] = {}

for field in fields_to_one:
related_resource = cls.Meta.api.model_resource_map[
field.related_model]
related_model_info = related_resource.Meta.model_info

# NOTE: could not use select+distinct because objects are prefetched
# from the database and queryset is evaluated only once.
linked_ids = set()
linked_objs = []
for m in model_instances:
rel_model = getattr(m, field.name)
if rel_model is not None and rel_model.id not in linked_ids:
linked_objs.append(related_resource.dump_document(
rel_model,
[f.name for f in related_model_info.fields_own]
))
linked_ids.add(rel_model.id)

if linked_objs:
data["linked"][related_resource.Meta.name_plural] = linked_objs

for field in fields_to_many:
related_resource = cls.Meta.api.model_resource_map[
field.related_model]
related_model_info = related_resource.Meta.model_info
data["linked"][related_resource.Meta.name_plural] = [
related_resource.dump_document(
x,
[f.name for f in related_model_info.fields_own]
) for m in model_instances
for x in getattr(getattr(m, field.name), "all").__call__()
]
if include_structure:
data["linked"] = []

for include_object in include_structure:
current_models = set(model_instances)
for field in include_object["field_path"]:
related_models = set()
for m in current_models:
if field.category == field.CATEGORIES.TO_MANY:
related_models |= set(getattr(m, field.name).all())
if field.category == field.CATEGORIES.TO_ONE:
related_model = getattr(m, field.name)
if related_model is not None:
related_models.add(related_model)

current_models = related_models

related_model_info = include_object["model_info"]
related_resource = include_object["resource"]
for rel_model in current_models:
linked_obj = related_resource.dump_document(
rel_model, related_model_info.fields_own
)
linked_obj["type"] = include_object["type"]
data["linked"].append(linked_obj)

return data
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ django-debug-toolbar==1.2.1
django-nose==1.2
coverage==3.7.1
mock==1.0.1
testfixtures==4.1.2
ipython==2.1.0
ipdb==0.8
15 changes: 14 additions & 1 deletion tests/testapp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
@ @
Comment
Also Setup classes with different relationship types for tests.
B is inherited from A
Expand All @@ -31,8 +33,9 @@
AAbstractManyToMany AManyToMany BMany
"""
from django.db import models
from django.contrib.auth.models import User
from django.db import models
import django


class Author(models.Model):
Expand Down Expand Up @@ -85,6 +88,16 @@ class TestSerializerAllFields(models.Model):
url = models.URLField()


class Group(models.Model):
name = models.CharField(max_length=255)
members = models.ManyToManyField(Author, through='Membership')


class Membership(models.Model):
group = models.ForeignKey(Group)
author = models.ForeignKey(Author)


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

Expand Down
12 changes: 12 additions & 0 deletions tests/testapp/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,15 @@ class CommentResource(Resource):
class Meta:
model = 'testapp.Comment'
page_size = 3


@api.register
class GroupResource(Resource):
class Meta:
model = 'testapp.Group'


@api.register
class MembershipResource(Resource):
class Meta:
model = 'testapp.Membership'

0 comments on commit 6bf2b85

Please sign in to comment.