Skip to content

Commit

Permalink
Merge branch 'develop' of github.com:django-json-api/django-rest-fram…
Browse files Browse the repository at this point in the history
…ework-json-api
  • Loading branch information
jerel committed Feb 11, 2016
2 parents fa266f1 + 6bf944b commit 8c4db64
Show file tree
Hide file tree
Showing 14 changed files with 250 additions and 46 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

v2.0.0-beta.2

* Added JSONAPIMeta class option to models for overriding `resource_name`. #197

20 changes: 8 additions & 12 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@ JSON API and Django Rest Framework

.. image:: https://travis-ci.org/django-json-api/django-rest-framework-json-api.svg?branch=develop
:target: https://travis-ci.org/django-json-api/django-rest-framework-json-api

.. image:: https://readthedocs.org/projects/django-rest-framework-json-api/badge/?version=latest
:alt: Read the docs
:target: http://django-rest-framework-json-api.readthedocs.org/

.. image:: https://codeclimate.com/github/django-json-api/django-rest-framework-json-api/badges/gpa.svg
:target: https://codeclimate.com/github/django-json-api/django-rest-framework-json-api
:alt: Code Climate

.. image:: https://badges.gitter.im/Join%20Chat.svg
:alt: Join the chat at https://gitter.im/django-json-api/django-rest-framework-json-api
:target: https://gitter.im/django-json-api/django-rest-framework-json-api


Documentation: http://django-rest-framework-json-api.readthedocs.org/

Live demo (resets every hour): http://json-api.jerel.co/
Expand Down Expand Up @@ -126,19 +126,15 @@ Settings

One can either add ``rest_framework_json_api.parsers.JSONParser`` and
``rest_framework_json_api.renderers.JSONRenderer`` to each ``ViewSet`` class, or
override ``settings.REST_FRAMEWORK``::
override ``settings.REST_FRAMEWORK``

::

REST_FRAMEWORK = {
'PAGINATE_BY': 10,
'PAGINATE_BY_PARAM': 'page_size',
'MAX_PAGINATE_BY': 100,
# DRF v3.1+
'PAGE_SIZE': 10,
'EXCEPTION_HANDLER': 'rest_framework_json_api.exceptions.exception_handler',
'DEFAULT_PAGINATION_CLASS':
'rest_framework_json_api.pagination.PageNumberPagination',
# older than DRF v3.1
'DEFAULT_PAGINATION_SERIALIZER_CLASS':
'rest_framework_json_api.pagination.PaginationSerializer',
'DEFAULT_PARSER_CLASSES': (
'rest_framework_json_api.parsers.JSONParser',
'rest_framework.parsers.FormParser',
Expand Down
28 changes: 25 additions & 3 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,15 @@ per request via the `PAGINATE_BY_PARAM` query parameter (`page_size` by default)

### Setting the resource_name

You may manually set the `resource_name` property on views or serializers to
specify the `type` key in the json output. It is automatically set for you as the
plural of the view or model name except on resources that do not subclass
You may manually set the `resource_name` property on views, serializers, or
models to specify the `type` key in the json output. In the case of setting the
`resource_name` property for models you must include the property inside a
`JSONAPIMeta` class on the model. It is automatically set for you as the plural
of the view or model name except on resources that do not subclass
`rest_framework.viewsets.ModelViewSet`:


Example - `resource_name` on View:
``` python
class Me(generics.GenericAPIView):
"""
Expand All @@ -56,6 +61,23 @@ If you set the `resource_name` property on the object to `False` the data
will be returned without modification.


Example - `resource_name` on Model:
``` python
class Me(models.Model):
"""
A simple model
"""
name = models.CharField(max_length=100)

class JSONAPIMeta:
resource_name = "users"
```
If you set the `resource_name` on a combination of model, serializer, or view
in the same hierarchy, the name will be resolved as following: view >
serializer > model. (Ex: A view `resource_name` will always override a
`resource_name` specified on a serializer or model)


### Inflecting object and relation keys

This package includes the ability (off by default) to automatically convert json
Expand Down
2 changes: 1 addition & 1 deletion example/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def get_body_format(self, obj):
class Meta:
model = Entry
fields = ('blog', 'headline', 'body_text', 'pub_date', 'mod_date',
'authors', 'comments', 'suggested',)
'authors', 'comments', 'suggested',)
meta_fields = ('body_format',)


Expand Down
137 changes: 137 additions & 0 deletions example/tests/integration/test_model_resource_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import pytest
from django.core.urlresolvers import reverse

from example.tests.utils import load_json

from example import models, serializers, views
pytestmark = pytest.mark.django_db


class _PatchedModel:
class JSONAPIMeta:
resource_name = "resource_name_from_JSONAPIMeta"


def _check_resource_and_relationship_comment_type_match(django_client):
entry_response = django_client.get(reverse("entry-list"))
comment_response = django_client.get(reverse("comment-list"))

comment_resource_type = load_json(comment_response.content).get('data')[0].get('type')
comment_relationship_type = load_json(entry_response.content).get(
'data')[0].get('relationships').get('comments').get('data')[0].get('type')

assert comment_resource_type == comment_relationship_type, "The resource type seen in the relationships and head resource do not match"


def _check_relationship_and_included_comment_type_are_the_same(django_client, url):
response = django_client.get(url + "?include=comments")
data = load_json(response.content).get('data')[0]
comment = load_json(response.content).get('included')[0]

comment_relationship_type = data.get('relationships').get('comments').get('data')[0].get('type')
comment_included_type = comment.get('type')

assert comment_relationship_type == comment_included_type, "The resource type seen in the relationships and included do not match"


@pytest.mark.usefixtures("single_entry")
class TestModelResourceName:

def test_model_resource_name_on_list(self, client):
models.Comment.__bases__ += (_PatchedModel,)
response = client.get(reverse("comment-list"))
data = load_json(response.content)['data'][0]
# name should be super-author instead of model name RenamedAuthor
assert (data.get('type') == 'resource_name_from_JSONAPIMeta'), (
'resource_name from model incorrect on list')

# Precedence tests
def test_resource_name_precendence(self, client):
# default
response = client.get(reverse("comment-list"))
data = load_json(response.content)['data'][0]
assert (data.get('type') == 'comments'), (
'resource_name from model incorrect on list')

# model > default
models.Comment.__bases__ += (_PatchedModel,)
response = client.get(reverse("comment-list"))
data = load_json(response.content)['data'][0]
assert (data.get('type') == 'resource_name_from_JSONAPIMeta'), (
'resource_name from model incorrect on list')

# serializer > model
serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer"
response = client.get(reverse("comment-list"))
data = load_json(response.content)['data'][0]
assert (data.get('type') == 'resource_name_from_serializer'), (
'resource_name from serializer incorrect on list')

# view > serializer > model
views.CommentViewSet.resource_name = 'resource_name_from_view'
response = client.get(reverse("comment-list"))
data = load_json(response.content)['data'][0]
assert (data.get('type') == 'resource_name_from_view'), (
'resource_name from view incorrect on list')

def teardown_method(self, method):
models.Comment.__bases__ = (models.Comment.__bases__[0],)
try:
delattr(serializers.CommentSerializer.Meta, "resource_name")
except AttributeError:
pass
try:
delattr(views.CommentViewSet, "resource_name")
except AttributeError:
pass


@pytest.mark.usefixtures("single_entry")
class TestResourceNameConsistency:

# Included rename tests
def test_type_match_on_included_and_inline_base(self, client):
_check_relationship_and_included_comment_type_are_the_same(client, reverse("entry-list"))

def test_type_match_on_included_and_inline_with_JSONAPIMeta(self, client):
models.Comment.__bases__ += (_PatchedModel,)

_check_relationship_and_included_comment_type_are_the_same(client, reverse("entry-list"))

def test_type_match_on_included_and_inline_with_serializer_resource_name(self, client):
serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer"

_check_relationship_and_included_comment_type_are_the_same(client, reverse("entry-list"))

def test_type_match_on_included_and_inline_with_serializer_resource_name_and_JSONAPIMeta(self, client):
models.Comment.__bases__ += (_PatchedModel,)
serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer"

_check_relationship_and_included_comment_type_are_the_same(client, reverse("entry-list"))

# Relation rename tests
def test_resource_and_relationship_type_match(self, client):
_check_resource_and_relationship_comment_type_match(client)

def test_resource_and_relationship_type_match_with_serializer_resource_name(self, client):
serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer"

_check_resource_and_relationship_comment_type_match(client)

def test_resource_and_relationship_type_match_with_JSONAPIMeta(self, client):
models.Comment.__bases__ += (_PatchedModel,)

_check_resource_and_relationship_comment_type_match(client)

def test_resource_and_relationship_type_match_with_serializer_resource_name_and_JSONAPIMeta(self, client):
models.Comment.__bases__ += (_PatchedModel,)
serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer"

_check_resource_and_relationship_comment_type_match(client)

def teardown_method(self, method):
models.Comment.__bases__ = (models.Comment.__bases__[0],)
try:
delattr(serializers.CommentSerializer.Meta, "resource_name")
except AttributeError:
pass
10 changes: 10 additions & 0 deletions example/tests/test_relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from rest_framework_json_api.exceptions import Conflict
from rest_framework_json_api.utils import format_relation_name
from example.models import Blog, Entry, Comment, Author
from example.serializers import CommentSerializer
from rest_framework_json_api.relations import ResourceRelatedField


Expand Down Expand Up @@ -115,6 +116,15 @@ def test_read_only(self):
serializer.is_valid(raise_exception=True)
self.assertNotIn('comment_set', serializer.validated_data)

def test_invalid_resource_id_object(self):
comment = {'body': 'testing 123', 'entry': {'type': 'entry'}, 'author': {'id': '5'}}
serializer = CommentSerializer(data=comment)
assert not serializer.is_valid()
assert serializer.errors == {
'author': ["Invalid resource identifier object: missing 'type' attribute"],
'entry': ["Invalid resource identifier object: missing 'id' attribute"]
}


class BlogFKSerializer(serializers.Serializer):
blog = ResourceRelatedField(queryset=Blog.objects)
Expand Down
3 changes: 1 addition & 2 deletions example/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from rest_framework_json_api.views import RelationshipView
from example.models import Blog, Entry, Author, Comment
from example.serializers import (
BlogSerializer, EntrySerializer, AuthorSerializer, CommentSerializer)
BlogSerializer, EntrySerializer, AuthorSerializer, CommentSerializer)


class BlogViewSet(viewsets.ModelViewSet):
Expand Down Expand Up @@ -41,4 +41,3 @@ class CommentRelationshipView(RelationshipView):
class AuthorRelationshipView(RelationshipView):
queryset = Author.objects.all()
self_link_view_name = 'author-relationships'

2 changes: 1 addition & 1 deletion rest_framework_json_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-

__title__ = 'djangorestframework-jsonapi'
__version__ = '2.0.0-beta.1'
__version__ = '2.0.0-beta.2'
__author__ = ''
__license__ = 'MIT'
__copyright__ = ''
Expand Down
8 changes: 7 additions & 1 deletion rest_framework_json_api/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@
from django.utils import six, encoding
from django.utils.translation import ugettext_lazy as _
from rest_framework import status, exceptions
from rest_framework.views import exception_handler as drf_exception_handler

from rest_framework_json_api.utils import format_value


def exception_handler(exc, context):
# Import this here to avoid potential edge-case circular imports, which
# crashes with:
# "ImportError: Could not import 'rest_framework_json_api.parsers.JSONParser' for API setting
# 'DEFAULT_PARSER_CLASSES'. ImportError: cannot import name 'exceptions'.'"
#
# Also see: https://github.com/django-json-api/django-rest-framework-json-api/issues/158
from rest_framework.views import exception_handler as drf_exception_handler
response = drf_exception_handler(exc, context)

if not response:
Expand Down
28 changes: 25 additions & 3 deletions rest_framework_json_api/relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
from django.utils.translation import ugettext_lazy as _

from rest_framework_json_api.exceptions import Conflict
from rest_framework_json_api.utils import format_relation_name, Hyperlink, \
get_resource_type_from_queryset, get_resource_type_from_instance
from rest_framework_json_api.utils import Hyperlink, \
get_resource_type_from_queryset, get_resource_type_from_instance, \
get_included_serializers, get_resource_type_from_serializer


class ResourceRelatedField(PrimaryKeyRelatedField):
Expand All @@ -19,6 +20,8 @@ class ResourceRelatedField(PrimaryKeyRelatedField):
'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'),
'incorrect_type': _('Incorrect type. Expected resource identifier object, received {data_type}.'),
'incorrect_relation_type': _('Incorrect relation type. Expected {relation_type}, received {received_type}.'),
'missing_type': _('Invalid resource identifier object: missing \'type\' attribute'),
'missing_id': _('Invalid resource identifier object: missing \'id\' attribute'),
'no_match': _('Invalid hyperlink - No URL match.'),
}

Expand Down Expand Up @@ -117,8 +120,16 @@ def to_internal_value(self, data):
if not isinstance(data, dict):
self.fail('incorrect_type', data_type=type(data).__name__)
expected_relation_type = get_resource_type_from_queryset(self.queryset)

if 'type' not in data:
self.fail('missing_type')

if 'id' not in data:
self.fail('missing_id')

if data['type'] != expected_relation_type:
self.conflict('incorrect_relation_type', relation_type=expected_relation_type, received_type=data['type'])

return super(ResourceRelatedField, self).to_internal_value(data['id'])

def to_representation(self, value):
Expand All @@ -127,7 +138,18 @@ def to_representation(self, value):
else:
pk = value.pk

return OrderedDict([('type', format_relation_name(get_resource_type_from_instance(value))), ('id', str(pk))])
# check to see if this resource has a different resource_name when
# included and use that name
resource_type = None
root = getattr(self.parent, 'parent', self.parent)
field_name = self.field_name if self.field_name else self.parent.field_name
if getattr(root, 'included_serializers', None) is not None:
includes = get_included_serializers(root)
if field_name in includes.keys():
resource_type = get_resource_type_from_serializer(includes[field_name])

resource_type = resource_type if resource_type else get_resource_type_from_instance(value)
return OrderedDict([('type', resource_type), ('id', str(pk))])

@property
def choices(self):
Expand Down
Loading

0 comments on commit 8c4db64

Please sign in to comment.