Skip to content

Commit

Permalink
Merge branch 'release/0.6.4'
Browse files Browse the repository at this point in the history
  • Loading branch information
pavlov99 committed Feb 10, 2015
2 parents 0b79c5c + 28400a6 commit 7aa3684
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 80 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, 3)
__version = (0, 6, 4)

__version__ = version = '.'.join(map(str, __version))
__project__ = PROJECT = __name__
84 changes: 31 additions & 53 deletions jsonapi/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,42 +50,36 @@ class Meta:
logger = logging.getLogger(__name__)


class ResourceManager(object):
def get_concrete_model(model):
""" Get model defined in Meta.
""" Resource utils functionality."""
:param str or django.db.models.Model model:
:return: model or None
:rtype django.db.models.Model or None:
:raise ValueError: model is not found or abstract
@staticmethod
def get_concrete_model(model):
""" Get model defined in Meta.
:param str or django.db.models.Model model:
:return: model or None
:rtype django.db.models.Model or None:
:raise ValueError: model is not found or abstract
"""
if not(inspect.isclass(model) and issubclass(model, models.Model)):
model = get_model_by_name(model)

"""
if not(inspect.isclass(model) and issubclass(model, models.Model)):
model = get_model_by_name(model)
return model

return model

@staticmethod
def get_resource_name(meta):
""" Define resource name based on Meta information.
def get_resource_name(meta):
""" Define resource name based on Meta information.
:param Resource.Meta meta: resource meta information
:return: name of resource
:rtype: str
:raises ValueError:
:param Resource.Meta meta: resource meta information
:return: name of resource
:rtype: str
:raises ValueError:
"""
if meta.name is None and not meta.is_model:
msg = "Either name or model for resource.Meta shoud be provided"
raise ValueError(msg)
"""
if meta.name is None and not meta.is_model:
msg = "Either name or model for resource.Meta shoud be provided"
raise ValueError(msg)

name = meta.name or get_model_name(
ResourceManager.get_concrete_model(meta.model))
return name
name = meta.name or get_model_name(get_concrete_model(meta.model))
return name


def merge_metas(*metas):
Expand Down Expand Up @@ -133,10 +127,10 @@ def __new__(mcs, name, bases, attrs):
return cls

cls.Meta.is_model = bool(getattr(cls.Meta, 'model', False))
cls.Meta.name = ResourceManager.get_resource_name(cls.Meta)
cls.Meta.name = get_resource_name(cls.Meta)

if cls.Meta.is_model:
model = ResourceManager.get_concrete_model(cls.Meta.model)
model = get_concrete_model(cls.Meta.model)
cls.Meta.model = model
if model._meta.abstract:
raise ValueError(
Expand All @@ -152,10 +146,11 @@ class Resource(Serializer, Authenticator):

class Meta:
name = None
fieldnames_include = None
fieldnames_exclude = None
# fieldnames_include = None # NOTE: moved to Serializer.
# fieldnames_exclude = None
page_size = None
allowed_methods = 'GET',
form = None

@classproperty
def name_plural(cls):
Expand Down Expand Up @@ -283,7 +278,6 @@ def get(cls, request=None, **kwargs):
fields_to_many = [f for f in model_info.fields_to_many
if f.name in fields_include]


response = cls.dump_documents(
cls,
objects,
Expand All @@ -306,20 +300,12 @@ def post(cls, request=None, **kwargs):
items = [items]

objects = []
Form = cls.get_form()
Form = cls.Meta.form or cls.get_form()
for item in items:
form = Form(item)
objects.append(form.save())

model_info = cls.Meta.api.model_inspector.models[cls.Meta.model]
data = [
cls.dump_document(
m,
fields_own=model_info.fields_own,
fields_to_one=model_info.fields_to_one,
)
for m in objects
]
data = [cls.dump_document(o) for o in objects]

if not is_collection:
data = data[0]
Expand All @@ -343,21 +329,13 @@ def put(cls, request=None, **kwargs):
objects_map = cls.Meta.model.objects.in_bulk(kwargs["ids"])

objects = []
Form = cls.get_form()
Form = cls.Meta.form or cls.get_form()
for item in items:
instance = objects_map[item["id"]]
form = Form(item, instance=instance)
objects.append(form.save())

model_info = cls.Meta.api.model_inspector.models[cls.Meta.model]
data = [
cls.dump_document(
m,
fields_own=model_info.fields_own,
fields_to_one=model_info.fields_to_one,
)
for m in objects
]
data = [cls.dump_document(o) for o in objects]

if not is_collection:
data = data[0]
Expand Down
54 changes: 35 additions & 19 deletions jsonapi/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ def default(self, o):

class SerializerMeta:
encoder = DatetimeDecimalEncoder
fieldnames_include = []
fieldnames_exclude = []


class Serializer(object):
Expand Down Expand Up @@ -68,46 +70,60 @@ def dump_document(cls, model_instance, fields_own=None, fields_to_one=None,
redefine dump rule for field x: def dump_document_x
:param django.db.models.Model model_instance: model instance
:param list of None fields: model_instance field to dump
:param list<str> or None fields: model_instance field to dump
:return dict: document
Related documents are not included to current one. In case of to-many
field serialization ensure that models_instance has been select_related
so, no database calls would be executed.
Method ensures that document has cls.Meta.fieldnames_include and does
not have cls.Meta.fieldnames_exclude
Steps:
1) fieldnames_include could be properties, but not related models.
Add them to fields_own.
"""
fields_own = fields_own or []
fields_to_one = fields_to_one or []
fields_to_many = fields_to_many or []
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))\
- set(cls.Meta.fieldnames_exclude)

document = {}
for field in fields_own:
value = getattr(model_instance, field.name)

for fieldname in fields_own:
value = getattr(model_instance, fieldname)
field_serializer = getattr(
cls, "dump_document_{}".format(field.name), None)
cls, "dump_document_{}".format(fieldname), None)

if field_serializer is not None:
value = field_serializer(model_instance)
else:
try:
field = model_instance._meta.get_field(field.name)
field = model_instance._meta.get_field(fieldname)
except models.fields.FieldDoesNotExist:
# Field is property
pass
value = getattr(model_instance, fieldname)
else:
if isinstance(field, models.fields.files.FileField):
# TODO: Serializer depends on API here.
value = cls.Meta.api.base_url + value.url
elif isinstance(field, models.CommaSeparatedIntegerField):
value = [v for v in value]

document[field.name] = value
document[fieldname] = value

fields_to_many = fields_to_many or []
for field in model_instance._meta.fields:
if field.rel:
document["links"] = document.get("links") or {}
document["links"][field.name] = getattr(
model_instance, "{}_id".format(field.name))

for field in fields_to_many:
document["links"][field.name] = list(
getattr(model_instance, field.name).
for fieldname in fields_to_many:
document["links"][fieldname] = list(
getattr(model_instance, fieldname).
values_list("id", flat=True)
)

Expand All @@ -120,9 +136,9 @@ def dump_documents(cls, resource, model_instances, fields_own=None,
resource.Meta.name_plural: [
cls.dump_document(
m,
fields_own=fields_own,
fields_to_one=fields_to_one,
fields_to_many=fields_to_many
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]
)
for m in model_instances
]
Expand Down Expand Up @@ -154,7 +170,7 @@ def dump_documents(cls, resource, model_instances, fields_own=None,
data["linked"][related_resource.Meta.name_plural] = [
related_resource.dump_document(
getattr(m, field.name),
related_model_info.fields_own
[f.name for f in related_model_info.fields_own]
) for m in model_instances
if getattr(m, field.name) is not None
]
Expand All @@ -167,7 +183,7 @@ def dump_documents(cls, resource, model_instances, fields_own=None,
data["linked"][related_resource.Meta.name_plural] = [
related_resource.dump_document(
x,
related_model_info.fields_own
[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__()
]
Expand Down
1 change: 1 addition & 0 deletions tests/testapp/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class PostWithPictureResource(Resource):
class Meta:
model = 'testapp.PostWithPicture'
fieldnames_include = 'title_uppercased',
fieldnames_exclude = 'title',


@api.register
Expand Down
29 changes: 28 additions & 1 deletion tests/testapp/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,6 @@ def test_get_include_null(self):
'is_superuser': user.is_superuser,
'last_login': user.last_login.isoformat(),
'last_name': user.last_name,
'password': user.password,
'username': user.username,
}]
},
Expand Down Expand Up @@ -433,3 +432,31 @@ def test_get_include_many(self):
}]
}
self.assertEqual(data, expected_data)

def test_get_include_fields(self):
mixer.blend("testapp.postwithpicture")
response = self.client.get(
'/api/postwithpicture',
content_type='application/vnd.api+json',
HTTP_ACCEPT='application/vnd.api+json'
)
data = json.loads(response.content.decode("utf-8"))
self.assertIn("title_uppercased", data["postwithpictures"][0])

response = self.client.get(
'/api/post',
content_type='application/vnd.api+json',
HTTP_ACCEPT='application/vnd.api+json'
)
data = json.loads(response.content.decode("utf-8"))
self.assertNotIn("title_uppercased", data["posts"][0])

def test_get_exclude_fields(self):
mixer.blend("testapp.postwithpicture")
response = self.client.get(
'/api/postwithpicture',
content_type='application/vnd.api+json',
HTTP_ACCEPT='application/vnd.api+json'
)
data = json.loads(response.content.decode("utf-8"))
self.assertNotIn("title", data["postwithpictures"][0])
9 changes: 3 additions & 6 deletions tests/testapp/tests/test_serializers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from django.test import TestCase
from mixer.backend.django import mixer
import datetime
import decimal
import json
from mixer.backend.django import mixer

from jsonapi.serializers import Serializer, DatetimeDecimalEncoder
from jsonapi.model_inspector import Field

from ..models import TestSerializerAllFields

Expand All @@ -18,8 +17,7 @@ def setUp(self):

def test_django_fields_serialization(self):
fields_own = [
Field(f.name, Field.CATEGORIES.OWN)
for f in self.obj._meta.fields if f.serialize
f.name for f in self.obj._meta.fields if f.serialize
]
obj = Serializer.dump_document(self.obj, fields_own=fields_own)

Expand Down Expand Up @@ -52,8 +50,7 @@ def test_django_fields_serialization(self):
self.assertEqual(obj['url'], self.obj.url)

def test_dump_document_field_redefinition(self):
fields_own = [
Field(name, Field.CATEGORIES.OWN) for name in ["id", "char"]]
fields_own = [name for name in ["id", "char"]]
obj_dump = Serializer.dump_document(self.obj, fields_own)
expected_dump = {
"id": self.obj.id,
Expand Down

0 comments on commit 7aa3684

Please sign in to comment.