diff --git a/example/serializers.py b/example/serializers.py index c0ed70ca..90f05aa3 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -51,8 +51,7 @@ def get_body_format(self, obj): class Meta: model = Entry fields = ('blog', 'headline', 'body_text', 'pub_date', 'mod_date', - 'authors', 'comments', 'suggested',) - meta_fields = ('body_format',) + 'authors', 'comments', 'suggested',) class AuthorBioSerializer(serializers.ModelSerializer): diff --git a/example/tests/integration/test_model_resource_name.py b/example/tests/integration/test_model_resource_name.py new file mode 100644 index 00000000..979b55b5 --- /dev/null +++ b/example/tests/integration/test_model_resource_name.py @@ -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 diff --git a/example/urls_test.py b/example/urls_test.py index 96f415fd..208334ad 100644 --- a/example/urls_test.py +++ b/example/urls_test.py @@ -2,7 +2,7 @@ from rest_framework import routers from example.views import BlogViewSet, EntryViewSet, AuthorViewSet, EntryRelationshipView, BlogRelationshipView, \ - CommentRelationshipView, AuthorRelationshipView + CommentRelationshipView, AuthorRelationshipView, CommentViewSet from .api.resources.identity import Identity, GenericIdentity router = routers.DefaultRouter(trailing_slash=False) @@ -10,6 +10,7 @@ router.register(r'blogs', BlogViewSet) router.register(r'entries', EntryViewSet) router.register(r'authors', AuthorViewSet) +router.register(r'comments', CommentViewSet) # for the old tests router.register(r'identities', Identity) diff --git a/example/views.py b/example/views.py index 59ca1a05..6a3fb505 100644 --- a/example/views.py +++ b/example/views.py @@ -41,4 +41,3 @@ class CommentRelationshipView(RelationshipView): class AuthorRelationshipView(RelationshipView): queryset = Author.objects.all() self_link_view_name = 'author-relationships' - diff --git a/rest_framework_json_api/__init__.py b/rest_framework_json_api/__init__.py index eeb61ce2..c1b7e75a 100644 --- a/rest_framework_json_api/__init__.py +++ b/rest_framework_json_api/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- __title__ = 'djangorestframework-jsonapi' -__version__ = '2.0.0-alpha.3' +__version__ = '3.0.0-mt' __author__ = '' __license__ = 'MIT' __copyright__ = '' diff --git a/rest_framework_json_api/exceptions.py b/rest_framework_json_api/exceptions.py index 1e976dfe..935fecdb 100644 --- a/rest_framework_json_api/exceptions.py +++ b/rest_framework_json_api/exceptions.py @@ -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: diff --git a/rest_framework_json_api/metadata.py b/rest_framework_json_api/metadata.py index 2355f2cf..9563f180 100644 --- a/rest_framework_json_api/metadata.py +++ b/rest_framework_json_api/metadata.py @@ -138,6 +138,7 @@ def get_field_info(self, field): for choice_value, choice_name in field.choices.items() ] + #Allow includes for all Relations unless explicitly if hasattr(serializer, 'included_serializers') and 'relationship_resource' in field_info: field_info['allows_include'] = field.field_name in serializer.included_serializers diff --git a/rest_framework_json_api/mixins.py b/rest_framework_json_api/mixins.py index 16af92a4..6f03e3ce 100644 --- a/rest_framework_json_api/mixins.py +++ b/rest_framework_json_api/mixins.py @@ -14,7 +14,12 @@ def get_queryset(self): ids = dict(self.request.query_params).get('ids[]') else: ids = dict(self.request.QUERY_PARAMS).get('ids[]') + + if 'filter[id]' in self.request.query_params: + ids = self.request.query_params.get('filter[id]').split(',') + if ids: - self.queryset = self.queryset.filter(id__in=ids) + self.queryset = self.queryset.filter(pk__in=ids) + return self.queryset diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py index a918aaeb..ff9f0559 100644 --- a/rest_framework_json_api/parsers.py +++ b/rest_framework_json_api/parsers.py @@ -39,11 +39,13 @@ def parse_relationships(data): # Parse the relationships parsed_relationships = dict() for field_name, field_data in relationships.items(): - field_data = field_data.get('data') + field_data = field_data.get('data', None) if isinstance(field_data, dict): parsed_relationships[field_name] = field_data elif isinstance(field_data, list): parsed_relationships[field_name] = list(relation for relation in field_data) + elif field_data == None: + parsed_relationships[field_name] = field_data return parsed_relationships def parse(self, stream, media_type=None, parser_context=None): @@ -51,41 +53,48 @@ def parse(self, stream, media_type=None, parser_context=None): Parses the incoming bytestream as JSON and returns the resulting data """ result = super(JSONParser, self).parse(stream, media_type=media_type, parser_context=parser_context) + data = result.get('data') if data: - from rest_framework_json_api.views import RelationshipView - if isinstance(parser_context['view'], RelationshipView): - # We skip parsing the object as JSONAPI Resource Identifier Object and not a regular Resource Object - if isinstance(data, list): - for resource_identifier_object in data: - if not (resource_identifier_object.get('id') and resource_identifier_object.get('type')): - raise ParseError( - 'Received data contains one or more malformed JSONAPI Resource Identifier Object(s)' - ) - elif not (data.get('id') and data.get('type')): - raise ParseError('Received data is not a valid JSONAPI Resource Identifier Object') + type = data.get('type') + # if type is defined, treat it like a full object, otherwise just pass through the data + if type: + from rest_framework_json_api.views import RelationshipView + if isinstance(parser_context['view'], RelationshipView): + # We skip parsing the object as JSONAPI Resource Identifier Object and not a regular Resource Object + if isinstance(data, list): + for resource_identifier_object in data: + if not (resource_identifier_object.get('id') and resource_identifier_object.get('type')): + raise ParseError( + 'Received data contains one or more malformed JSONAPI Resource Identifier Object(s)' + ) + elif not (data.get('id') and data.get('type')): + raise ParseError('Received data is not a valid JSONAPI Resource Identifier Object') - return data + return data - request = parser_context.get('request') + request = parser_context.get('request') - # Check for inconsistencies - resource_name = utils.get_resource_name(parser_context) - if data.get('type') != resource_name and request.method in ('PUT', 'POST', 'PATCH'): - raise exceptions.Conflict( - "The resource object's type ({data_type}) is not the type " - "that constitute the collection represented by the endpoint ({resource_type}).".format( - data_type=data.get('type'), - resource_type=resource_name + # Check for inconsistencies + resource_name = utils.get_resource_name(parser_context) + if data.get('type') != resource_name and request.method in ('PUT', 'POST', 'PATCH'): + raise exceptions.Conflict( + "The resource object's type ({data_type}) is not the type " + "that constitute the collection represented by the endpoint ({resource_type}).".format( + data_type=data.get('type'), + resource_type=resource_name + ) ) - ) - # Construct the return data - parsed_data = {'id': data.get('id')} - parsed_data.update(self.parse_attributes(data)) - parsed_data.update(self.parse_relationships(data)) - return parsed_data + # Construct the return data + parsed_data = {'id': data.get('id')} + parsed_data.update(self.parse_attributes(data)) + parsed_data.update(self.parse_relationships(data)) + return parsed_data + + else: + return data else: - raise ParseError('Received document does not contain primary data') + return {} diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 61b9ecd7..e256c5cc 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -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_override, get_resource_type_from_serializer class ResourceRelatedField(PrimaryKeyRelatedField): @@ -107,19 +108,20 @@ def get_links(self, obj=None, lookup_field='pk'): return_data.update({'related': related_link}) return return_data - def to_internal_value(self, data): - if isinstance(data, six.text_type): - try: - data = json.loads(data) - except ValueError: - # show a useful error if they send a `pk` instead of resource object - self.fail('incorrect_type', data_type=type(data).__name__) - 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 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_internal_value(self, data): + # if isinstance(data, six.text_type): + # try: + # data = json.loads(data) + # except ValueError: + # # show a useful error if they send a `pk` instead of resource object + # self.fail('incorrect_type', data_type=type(data).__name__) + # 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 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): if getattr(self, 'pk_field', None) is not None: @@ -127,7 +129,22 @@ 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) + root = self.parent + field_name = self.field_name if self.field_name else self.parent.field_name + if getattr(root, 'included_serializers_override', None) is not None: + include_overrides = get_included_serializers_override(root) + if field_name in include_overrides.keys(): + resource_type = get_resource_type_from_serializer(include_overrides[field_name]) + + # if included_serializers_override is defined, use that one instead of the default serializer defined on the instance model + + + 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): @@ -138,12 +155,24 @@ def choices(self): return {} return OrderedDict([ - ( - json.dumps(self.to_representation(item)), - self.display_value(item) - ) - for item in queryset - ]) + ( + json.dumps(self.to_representation(item)), + self.display_value(item) + ) + for item in queryset + ]) + + def validate_empty_values(self, data): + if isinstance(data, dict) and 'id' in data and data['id'] is None: + return super(ResourceRelatedField, self).validate_empty_values(data['id']) + + return super(ResourceRelatedField, self).validate_empty_values(data) + + def to_internal_value(self, data): + if isinstance(data, dict) and 'id' in data: + return super(ResourceRelatedField, self).to_internal_value(data['id']) + + return super(ResourceRelatedField, self).to_internal_value(data) class SerializerMethodResourceRelatedField(ResourceRelatedField): diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 61ff20b8..0a3f1de2 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -2,13 +2,16 @@ Renderers """ import copy -from collections import OrderedDict +from collections import OrderedDict, Iterable from django.utils import six, encoding +from django.db.models.query import QuerySet + from rest_framework import relations from rest_framework import renderers from rest_framework.serializers import BaseSerializer, ListSerializer, ModelSerializer from rest_framework.settings import api_settings +from rest_framework.fields import get_attribute from . import utils @@ -84,7 +87,7 @@ def extract_relationships(fields, resource, resource_instance): source = field.source try: - relation_instance_or_manager = getattr(resource_instance, source) + relation_instance_or_manager = get_attribute(resource_instance, source.split('.')) except AttributeError: # if the field is not defined on the model then we check the serializer # and if no value is there we skip over the field completely @@ -94,9 +97,10 @@ def extract_relationships(fields, resource, resource_instance): else: continue - relation_type = utils.get_related_resource_type(field) + # relation_type = utils.get_related_resource_type(field) if isinstance(field, relations.HyperlinkedIdentityField): + relation_type = utils.get_related_resource_type(field) # special case for HyperlinkedIdentityField relation_data = list() @@ -106,7 +110,7 @@ def extract_relationships(fields, resource, resource_instance): for related_object in relation_queryset: relation_data.append( - OrderedDict([('type', relation_type), ('id', encoding.force_text(related_object.pk))]) + OrderedDict([('type', relation_type), ('id', encoding.force_text(related_object.pk))]) ) data.update({field_name: { @@ -127,14 +131,15 @@ def extract_relationships(fields, resource, resource_instance): field_links = field.get_links(resource_instance) relation_data.update( - {'links': field_links} - if field_links else dict() + {'links': field_links} + if field_links else dict() ) data.update({field_name: relation_data}) continue if isinstance(field, (relations.PrimaryKeyRelatedField, relations.HyperlinkedRelatedField)): relation_id = relation_instance_or_manager.pk if resource.get(field_name) else None + relation_type = utils.get_related_resource_type(field) relation_data = { 'data': ( @@ -143,8 +148,8 @@ def extract_relationships(fields, resource, resource_instance): } relation_data.update( - {'links': {'related': resource.get(field_name)}} - if isinstance(field, relations.HyperlinkedRelatedField) and resource.get(field_name) else dict() + {'links': {'related': resource.get(field_name)}} + if isinstance(field, relations.HyperlinkedRelatedField) and resource.get(field_name) else dict() ) data.update({field_name: relation_data}) continue @@ -153,21 +158,26 @@ def extract_relationships(fields, resource, resource_instance): if isinstance(field.child_relation, ResourceRelatedField): # special case for ResourceRelatedField + + if field_name not in resource: + continue + + relation_data = { 'data': resource.get(field_name) } field_links = field.child_relation.get_links(resource_instance) relation_data.update( - {'links': field_links} - if field_links else dict() + {'links': field_links} + if field_links else dict() ) relation_data.update( - { - 'meta': { - 'count': len(resource.get(field_name)) + { + 'meta': { + 'count': len(resource.get(field_name)) + } } - } ) data.update({field_name: relation_data}) continue @@ -197,7 +207,7 @@ def extract_relationships(fields, resource, resource_instance): if isinstance(serializer_data, list): for position in range(len(serializer_data)): nested_resource_instance = resource_instance_queryset[position] - nested_resource_instance_type = utils.get_resource_type_from_instance(nested_resource_instance) + nested_resource_instance_type = utils.get_resource_type_from_serializer(field.child) relation_data.append(OrderedDict([ ('type', nested_resource_instance_type), ('id', encoding.force_text(nested_resource_instance.pk)) @@ -207,9 +217,6 @@ def extract_relationships(fields, resource, resource_instance): continue if isinstance(field, ModelSerializer): - relation_model = field.Meta.model - relation_type = utils.format_relation_name(relation_model.__name__) - data.update({ field_name: { 'data': ( @@ -232,7 +239,7 @@ def extract_included(fields, resource, resource_instance, included_resources): included_data = list() current_serializer = fields.serializer context = current_serializer.context - included_serializers = utils.get_included_serializers(current_serializer) + include_config = utils.get_included_configuration(current_serializer) included_resources = copy.copy(included_resources) for field_name, field in six.iteritems(fields): @@ -244,6 +251,10 @@ def extract_included(fields, resource, resource_instance, included_resources): if not isinstance(field, (relations.RelatedField, relations.ManyRelatedField, BaseSerializer)): continue + # Skip disabled fields + if include_config[field_name] == False: + continue + try: included_resources.remove(field_name) except ValueError: @@ -251,15 +262,15 @@ def extract_included(fields, resource, resource_instance, included_resources): continue try: - relation_instance_or_manager = getattr(resource_instance, field_name) + relation_instance_or_manager = get_attribute(resource_instance, field.source.split('.')) except AttributeError: try: # For ManyRelatedFields if `related_name` is not set we need to access `foo_set` from `source` - relation_instance_or_manager = getattr(resource_instance, field.child_relation.source) + relation_instance_or_manager = get_attribute(resource_instance, field.child_relation.source.split('.')) except AttributeError: if not hasattr(current_serializer, field.source): continue - serializer_method = getattr(current_serializer, field.source) + serializer_method = get_attribute(current_serializer, field.source.split('.')) relation_instance_or_manager = serializer_method(resource_instance) new_included_resources = [key.replace('%s.' % field_name, '', 1) @@ -267,56 +278,54 @@ def extract_included(fields, resource, resource_instance, included_resources): if field_name == key.split('.')[0]] serializer_data = resource.get(field_name) + serializer_instances = [] + if isinstance(field, relations.ManyRelatedField): - serializer_class = included_serializers.get(field_name) - field = serializer_class(relation_instance_or_manager.all(), many=True, context=context) - serializer_data = field.data + # # serializer_class = include_config.get(field_name) + # serializer_class = utils.get_serializer_from_instance_and_serializer() + # serializer_instance = serializer_class(relation_instance_or_manager.all(), many=True, context=context) + + if isinstance(current_serializer.instance, Iterable): + iterable = [] + for obj in current_serializer.instance: + iterable += field.get_attribute(obj) + else: + iterable = field.get_attribute(current_serializer.instance) + + for item in iterable: + serializer_class = utils.get_serializer_from_instance_and_serializer(item, + current_serializer, + field_name) + serializer_instance = serializer_class(item, context=context) + serializer_instances.append(serializer_instance) if isinstance(field, relations.RelatedField): - serializer_class = included_serializers.get(field_name) + # serializer_class = include_config.get(field_name) if relation_instance_or_manager is None: continue - field = serializer_class(relation_instance_or_manager, context=context) - serializer_data = field.data - if isinstance(field, ListSerializer): - serializer = field.child - model = serializer.Meta.model - relation_type = utils.format_relation_name(model.__name__) - relation_queryset = list(relation_instance_or_manager.all()) + serializer_class = utils.get_serializer_from_instance_and_serializer(relation_instance_or_manager, current_serializer, field_name) + serializer_instance = serializer_class(relation_instance_or_manager, context=context) + serializer_instances.append(serializer_instance) - # Get the serializer fields - serializer_fields = utils.get_serializer_fields(serializer) - if serializer_data: - for position in range(len(serializer_data)): - serializer_resource = serializer_data[position] - nested_resource_instance = relation_queryset[position] - included_data.append( - JSONRenderer.build_json_resource_obj( - serializer_fields, serializer_resource, nested_resource_instance, relation_type - ) - ) - included_data.extend( - JSONRenderer.extract_included( - serializer_fields, serializer_resource, nested_resource_instance, new_included_resources - ) - ) + for serializer_instance in serializer_instances: - if isinstance(field, ModelSerializer): - model = field.Meta.model - relation_type = utils.format_relation_name(model.__name__) + serializer_data = serializer_instance.data + + relation_type = utils.get_resource_type_from_serializer(serializer_instance.__class__) # Get the serializer fields - serializer_fields = utils.get_serializer_fields(field) + serializer_fields = utils.get_serializer_fields(serializer_instance) if serializer_data: included_data.append( - JSONRenderer.build_json_resource_obj(serializer_fields, serializer_data, relation_instance_or_manager, - relation_type) + JSONRenderer.build_json_resource_obj( + serializer_fields, serializer_data, + serializer_instance.instance, relation_type) ) included_data.extend( - JSONRenderer.extract_included( - serializer_fields, serializer_data, relation_instance_or_manager, new_included_resources - ) + JSONRenderer.extract_included( + serializer_fields, serializer_data, serializer_instance.instance, new_included_resources + ) ) return utils.format_keys(included_data) @@ -370,7 +379,7 @@ def render_relationship_view(self, data, accepted_media_type=None, renderer_cont if links: render_data.update({'links': links}), return super(JSONRenderer, self).render( - render_data, accepted_media_type, renderer_context + render_data, accepted_media_type, renderer_context ) def render_errors(self, data, accepted_media_type=None, renderer_context=None): @@ -378,7 +387,7 @@ def render_errors(self, data, accepted_media_type=None, renderer_context=None): if len(data) > 1 and isinstance(data, list): data.sort(key=lambda x: x.get('source', {}).get('pointer', '')) return super(JSONRenderer, self).render( - {'errors': data}, accepted_media_type, renderer_context + {'errors': data}, accepted_media_type, renderer_context ) def render(self, data, accepted_media_type=None, renderer_context=None): @@ -397,7 +406,7 @@ def render(self, data, accepted_media_type=None, renderer_context=None): # wants to build the output format manually. if resource_name is None or resource_name is False: return super(JSONRenderer, self).render( - data, accepted_media_type, renderer_context + data, accepted_media_type, renderer_context ) # If this is an error response, skip the rest. @@ -435,7 +444,21 @@ def render(self, data, accepted_media_type=None, renderer_context=None): json_api_data = list() for position in range(len(serializer_data)): resource = serializer_data[position] # Get current resource - resource_instance = resource_serializer.instance[position] # Get current instance + + # If this is a queryset, ensure that the query is evaluated only once, + # and do not assume the ordering of the serializer_data is in the same order, + # since queries without an 'order_by' may not return in the same order. Match + # by pk instead. + if isinstance(resource_serializer.instance, QuerySet): + id = resource['id'] + + # If 'id' field is sourced from another field, use the source field in the lookup + lookup_field = resource_serializer.child.fields['id'].source + + resource_instance = filter(lambda item: getattr(item, lookup_field) == id, + list(resource_serializer.instance))[0] + else: + resource_instance = resource_serializer.instance[position] # Get current instance json_resource_obj = self.build_json_resource_obj(fields, resource, resource_instance, resource_name) meta = self.extract_meta(resource_serializer, resource) @@ -495,5 +518,5 @@ def render(self, data, accepted_media_type=None, renderer_context=None): render_data['meta'] = utils.format_keys(json_api_meta) return super(JSONRenderer, self).render( - render_data, accepted_media_type, renderer_context + render_data, accepted_media_type, renderer_context ) diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index 94e01c0d..bed3d392 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -3,8 +3,9 @@ from rest_framework.serializers import * from rest_framework_json_api.relations import ResourceRelatedField -from rest_framework_json_api.utils import format_relation_name, get_resource_type_from_instance, \ - get_resource_type_from_serializer, get_included_serializers +from rest_framework_json_api.utils import ( + get_resource_type_from_model, get_resource_type_from_instance, + get_resource_type_from_serializer, get_included_configuration) class ResourceIdentifierObjectSerializer(BaseSerializer): @@ -24,12 +25,12 @@ def __init__(self, *args, **kwargs): def to_representation(self, instance): return { - 'type': format_relation_name(get_resource_type_from_instance(instance)), + 'type': get_resource_type_from_instance(instance), 'id': str(instance.pk) } def to_internal_value(self, data): - if data['type'] != format_relation_name(self.model_class.__name__): + if data['type'] != get_resource_type_from_model(self.model_class): self.fail('incorrect_model_type', model_type=self.model_class, received_type=data['type']) pk = data['id'] try: @@ -71,7 +72,7 @@ def __init__(self, *args, **kwargs): view = context.get('view') if context else None def validate_path(serializer_class, field_path, path): - serializers = get_included_serializers(serializer_class) + serializers = get_included_configuration(serializer_class) if serializers is None: raise ParseError('This endpoint does not support the include parameter') this_field_name = field_path[0] diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index da88b5e8..56a1fe85 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -11,6 +11,8 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework.exceptions import APIException +import string + try: from rest_framework.serializers import ManyRelatedField except ImportError: @@ -50,7 +52,7 @@ def get_resource_name(context): return get_resource_type_from_serializer(serializer) except AttributeError: try: - resource_name = view.model.__name__ + resource_name = get_resource_type_from_model(view.model) except AttributeError: resource_name = view.__class__.__name__ @@ -82,6 +84,7 @@ def get_serializer_fields(serializer): pass return fields + def format_keys(obj, format_type=None): """ Takes either a dict or list and returns it with camelized keys only if @@ -150,7 +153,9 @@ def format_relation_name(value, format_type=None): def get_related_resource_type(relation): - if hasattr(relation, '_meta'): + if type(relation) == ManyRelatedField: + return get_related_resource_type(relation.child_relation) + elif hasattr(relation, '_meta'): relation_model = relation._meta.model elif hasattr(relation, 'model'): # the model type was explicitly passed as a kwarg to ResourceRelatedField @@ -182,7 +187,7 @@ def get_related_resource_type(relation): relation_model = parent_model_relation.field.related.model else: return get_related_resource_type(parent_model_relation) - return format_relation_name(relation_model.__name__) + return get_resource_type_from_model(relation_model) def get_instance_or_manager_resource_type(resource_instance_or_manager): @@ -193,38 +198,85 @@ def get_instance_or_manager_resource_type(resource_instance_or_manager): pass +def get_resource_type_from_model(model): + serializer_class = get_default_serializer_from_model(model) + + return get_resource_type_from_serializer(serializer_class) + + +def get_default_serializer_from_model(model): + json_api_meta = getattr(model, 'JSONAPIMeta', None) + + if hasattr(json_api_meta, 'get_default_serializer'): + return json_api_meta.get_default_serializer() + + serializer_string = getattr(json_api_meta, 'default_serializer', None) + if serializer_string is None: + # return format_relation_name(model.__name__) + raise Exception("Must define a default_serializer on %s" % model.__name__) + serializer_class = import_class_from_dotted_path(serializer_string) + return serializer_class + + def get_resource_type_from_queryset(qs): - return format_relation_name(qs.model._meta.model.__name__) + return get_resource_type_from_model(qs.model) def get_resource_type_from_instance(instance): - return format_relation_name(instance._meta.model.__name__) + return get_resource_type_from_model(instance._meta.model) + + +def get_serializer_from_instance_and_serializer(instance, serializer, field_name): + overrides = get_included_serializers_override(serializer) + if field_name in overrides: + return overrides[field_name] + return get_default_serializer_from_model(type(instance)) def get_resource_type_from_manager(manager): - return format_relation_name(manager.model.__name__) + return get_resource_type_from_model(manager.model) def get_resource_type_from_serializer(serializer): - try: - # Check the meta class for resource_name - return serializer.Meta.resource_name - except AttributeError: - # Use the serializer model then pluralize and format - return format_relation_name(serializer.Meta.model.__name__) + resource_name = getattr( + serializer.Meta, + 'resource_name', + None) + + if not resource_name: + resource_name = format_relation_name(string.replace(serializer.__name__, 'Serializer', '')) + # resource_name = get_resource_type_from_model(serializer.Meta.model) + + return resource_name + + +def get_included_configuration(serializer): + included_resources_override = copy.copy(getattr(serializer, 'included_resources_override', dict())) + + included_resources_config = {} + + # get all the related resource fields on this serializer + for field in serializer.fields.fields.keys(): + if field in included_resources_override.keys() and included_resources_override[field] == False: + # Skip fields that are explicitly disabled + continue + + included_resources_config[field] = True + + return included_resources_config -def get_included_serializers(serializer): - included_serializers = copy.copy(getattr(serializer, 'included_serializers', dict())) +def get_included_serializers_override(serializer): + included_serializers_override = copy.copy(getattr(serializer, 'included_serializers_override', dict())) - for name, value in six.iteritems(included_serializers): + for name, value in six.iteritems(included_serializers_override): if not isinstance(value, type): if value == 'self': - included_serializers[name] = serializer if isinstance(serializer, type) else serializer.__class__ + included_serializers_override[name] = serializer if isinstance(serializer, type) else serializer.__class__ else: - included_serializers[name] = import_class_from_dotted_path(value) + included_serializers_override[name] = import_class_from_dotted_path(value) - return included_serializers + return included_serializers_override class Hyperlink(six.text_type): diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 36c89687..a43fad89 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -12,7 +12,7 @@ from rest_framework_json_api.exceptions import Conflict from rest_framework_json_api.serializers import ResourceIdentifierObjectSerializer -from rest_framework_json_api.utils import format_relation_name, get_resource_type_from_instance, OrderedDict, Hyperlink +from rest_framework_json_api.utils import get_resource_type_from_instance, OrderedDict, Hyperlink class RelationshipView(generics.GenericAPIView): @@ -154,7 +154,7 @@ def _instantiate_serializer(self, instance): def get_resource_name(self): if not hasattr(self, '_resource_name'): instance = getattr(self.get_object(), self.kwargs['related_field']) - self._resource_name = format_relation_name(get_resource_type_from_instance(instance)) + self._resource_name = get_resource_type_from_instance(instance) return self._resource_name def set_resource_name(self, value):