Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
jerel committed Aug 18, 2016
2 parents b9ccd9d + 97e1a6c commit 3471569
Show file tree
Hide file tree
Showing 27 changed files with 545 additions and 3,278 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ pip-delete-this-directory.txt
# Pycharm project files
.idea/

# PyTest cache
.cache/

# Tox
.tox/

Expand Down
62 changes: 35 additions & 27 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
---
language: python
python: 3.5
sudo: false
install: pip install tox
script: tox
cache: pip
matrix:
exclude:
- python: "3.3"
env: DJANGO=">=1.9,<1.10" DRF=">=3.3,<3.4"
- python: "3.3"
env: DJANGO=">=1.9,<1.10" DRF=">=3.4,<3.5"
- python: "3.3"
env: DJANGO=">=1.10,<1.11" DRF=">=3.4,<3.5"
python:
- "2.7"
- "3.3"
- "3.4"
- "3.5"
env:
- TOXENV=py27-django17-drf31
- TOXENV=py27-django17-drf32
- TOXENV=py33-django17-drf31
- TOXENV=py33-django17-drf32
- TOXENV=py34-django17-drf31
- TOXENV=py34-django17-drf32
- TOXENV=py27-django18-drf31
- TOXENV=py27-django18-drf32
- TOXENV=py27-django18-drf33
- TOXENV=py33-django18-drf31
- TOXENV=py33-django18-drf32
- TOXENV=py33-django18-drf33
- TOXENV=py34-django18-drf31
- TOXENV=py34-django18-drf32
- TOXENV=py34-django18-drf33
- TOXENV=py27-django19-drf31
- TOXENV=py27-django19-drf32
- TOXENV=py27-django19-drf33
- TOXENV=py34-django19-drf31
- TOXENV=py34-django19-drf32
- TOXENV=py34-django19-drf33
- TOXENV=py35-django19-drf31
- TOXENV=py35-django19-drf32
- TOXENV=py35-django19-drf33
- DJANGO=">=1.8,<1.9" DRF=">=3.1,<3.2"
- DJANGO=">=1.8,<1.9" DRF=">=3.2,<3.3"
- DJANGO=">=1.8,<1.9" DRF=">=3.3,<3.4"
- DJANGO=">=1.8,<1.9" DRF=">=3.4,<3.5"

- DJANGO=">=1.9,<1.10" DRF=">=3.3,<3.4"
- DJANGO=">=1.9,<1.10" DRF=">=3.4,<3.5"

- DJANGO=">=1.10,<1.11" DRF=">=3.4,<3.5"
before_install:
# Force an upgrade of py to avoid VersionConflict
- pip install --upgrade py
- pip install codecov
install:
- pip install Django${DJANGO} djangorestframework${DRF}
- python setup.py install
script:
- coverage run setup.py -v test
after_success:
- codecov
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,22 @@

v2.1.0

* Parse `meta` in JSONParser
* Added code coverage reporting and updated Django versions tested against
* Fixed Django 1.10 compatibility
* Added support for regular non-ModelSerializers
* Added performance enhancements to reduce the number of queries in related payloads
* Fixed bug where related `SerializerMethodRelatedField` fields were not included even if in `include`
* Convert `include` field names back to snake_case
* Documented built in `url` field for generating a `self` link in the `links` key
* Fixed bug that prevented `fields = ()` in a serializer from being valid
* Fixed stale data returned in PATCH to-one relation
* Raise a `ParseError` if an `id` is not included in a PATCH request

v2.0.1

* Fixed naming error that caused ModelSerializer relationships to fail

v2.0.0

* Fixed bug where write_only fields still had their keys rendered
Expand Down
11 changes: 11 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,14 @@ Calls a `get_root_meta` function on a serializer, if it exists.
`build_json_resource_obj(fields, resource, resource_instance, resource_name)`

Builds the resource object (type, id, attributes) and extracts relationships.

## rest_framework_json_api.parsers.JSONParser

Similar to `JSONRenderer`, the `JSONParser` you may override the following methods if you need
highly custom parsing control.

#### parse_metadata

`parse_metadata(result)`

Returns a dictionary which will be merged into parsed data of the request. By default, it reads the `meta` content in the request body and returns it in a dictionary with a `_meta` top level key.
11 changes: 9 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ class LineItemViewSet(viewsets.ModelViewSet):

### RelationshipView
`rest_framework_json_api.views.RelationshipView` is used to build
relationship views (see the
relationship views (see the
[JSON API spec](http://jsonapi.org/format/#fetching-relationships)).
The `self` link on a relationship object should point to the corresponding
relationship view.
Expand Down Expand Up @@ -449,9 +449,16 @@ def get_root_meta(self, resource, many):
```
to the serializer. It must return a dict and will be merged with the existing top level `meta`.

To access metadata in incoming requests, the `JSONParser` will add the metadata under a top level `_meta` key in the parsed data dictionary. For instance, to access meta data from a `serializer` object, you may use `serializer.initial_data.get("_meta")`. To customize the `_meta` key, see [here](api.md).

### Links

Adding `url` to `fields` on a serializer will add a `self` link to the `links` key.

Related links will be created automatically when using the Relationship View.

<!--
### Relationships
### Links
### Included
### Errors
-->
Binary file modified drf_example
Binary file not shown.
94 changes: 94 additions & 0 deletions example/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-05-02 08:26
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='Author',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('modified_at', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=50)),
('email', models.EmailField(max_length=254)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='AuthorBio',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('modified_at', models.DateTimeField(auto_now=True)),
('body', models.TextField()),
('author', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='bio', to='example.Author')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Blog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('modified_at', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=100)),
('tagline', models.TextField()),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Comment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('modified_at', models.DateTimeField(auto_now=True)),
('body', models.TextField()),
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='example.Author')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Entry',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('modified_at', models.DateTimeField(auto_now=True)),
('headline', models.CharField(max_length=255)),
('body_text', models.TextField(null=True)),
('pub_date', models.DateField(null=True)),
('mod_date', models.DateField(null=True)),
('n_comments', models.IntegerField(default=0)),
('n_pingbacks', models.IntegerField(default=0)),
('rating', models.IntegerField(default=0)),
('authors', models.ManyToManyField(to='example.Author')),
('blog', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='example.Blog')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='comment',
name='entry',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='example.Entry'),
),
]
Empty file added example/migrations/__init__.py
Empty file.
3 changes: 2 additions & 1 deletion example/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def get_root_meta(self, resource, many):

class Meta:
model = Blog
fields = ('name', )
fields = ('name', 'url',)
meta_fields = ('copyright',)


Expand All @@ -35,6 +35,7 @@ def __init__(self, *args, **kwargs):
'authors': 'example.serializers.AuthorSerializer',
'comments': 'example.serializers.CommentSerializer',
'featured': 'example.serializers.EntrySerializer',
'suggested': 'example.serializers.EntrySerializer',
}

body_format = serializers.SerializerMethodField()
Expand Down
23 changes: 23 additions & 0 deletions example/settings/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,29 @@
'example',
]

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
# insert your TEMPLATE_DIRS here
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
# Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this
# list if you haven't customized them:
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.debug',
'django.template.context_processors.i18n',
'django.template.context_processors.media',
'django.template.context_processors.static',
'django.template.context_processors.tz',
'django.contrib.messages.context_processors.messages',
],
},
},
]

STATIC_URL = '/static/'

ROOT_URLCONF = 'example.urls'
Expand Down
32 changes: 28 additions & 4 deletions example/tests/integration/test_includes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,21 @@

from example.tests.utils import load_json

try:
from unittest import mock
except ImportError:
import mock

pytestmark = pytest.mark.django_db


def test_included_data_on_list(multiple_entries, client):
response = client.get(reverse("entry-list") + '?include=comments&page_size=5')
@mock.patch('rest_framework_json_api.utils.get_default_included_resources_from_serializer', new=lambda s: ['comments'])
def test_default_included_data_on_list(multiple_entries, client):
return test_included_data_on_list(multiple_entries=multiple_entries, client=client, query='?page_size=5')


def test_included_data_on_list(multiple_entries, client, query='?include=comments&page_size=5'):
response = client.get(reverse("entry-list") + query)
included = load_json(response.content).get('included')

assert len(load_json(response.content)['data']) == len(multiple_entries), 'Incorrect entry count'
Expand All @@ -18,8 +28,13 @@ def test_included_data_on_list(multiple_entries, client):
assert comment_count == expected_comment_count, 'List comment count is incorrect'


def test_included_data_on_detail(single_entry, client):
response = client.get(reverse("entry-detail", kwargs={'pk': single_entry.pk}) + '?include=comments')
@mock.patch('rest_framework_json_api.utils.get_default_included_resources_from_serializer', new=lambda s: ['comments'])
def test_default_included_data_on_detail(single_entry, client):
return test_included_data_on_detail(single_entry=single_entry, client=client, query='')


def test_included_data_on_detail(single_entry, client, query='?include=comments'):
response = client.get(reverse("entry-detail", kwargs={'pk': single_entry.pk}) + query)
included = load_json(response.content).get('included')

assert [x.get('type') for x in included] == ['comments'], 'Detail included types are incorrect'
Expand All @@ -38,6 +53,15 @@ def test_dynamic_related_data_is_included(single_entry, entry_factory, client):
assert len(included) == 1, 'The dynamically included blog entries are of an incorrect count'


def test_dynamic_many_related_data_is_included(single_entry, entry_factory, client):
entry_factory()
response = client.get(reverse("entry-detail", kwargs={'pk': single_entry.pk}) + '?include=suggested')
included = load_json(response.content).get('included')

assert included
assert [x.get('type') for x in included] == ['entries'], 'Dynamic included types are incorrect'


def test_missing_field_not_included(author_bio_factory, author_factory, client):
# First author does not have a bio
author = author_factory(bio=None)
Expand Down
6 changes: 6 additions & 0 deletions example/tests/integration/test_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ def test_top_level_meta_for_list_view(blog, client):
"attributes": {
"name": blog.name
},
"links": {
"self": 'http://testserver/blogs/1'
},
"meta": {
"copyright": datetime.now().year
},
Expand Down Expand Up @@ -48,6 +51,9 @@ def test_top_level_meta_for_detail_view(blog, client):
"attributes": {
"name": blog.name
},
"links": {
"self": "http://testserver/blogs/1"
},
"meta": {
"copyright": datetime.now().year
},
Expand Down
19 changes: 19 additions & 0 deletions example/tests/test_model_viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,25 @@ def test_key_in_detail_result(self):

assert expected_dump == content_dump

def test_patch_requires_id(self):
"""
Verify that 'id' is required to be passed in an update request.
"""
data = {
'data': {
'type': 'users',
'attributes': {
'first-name': 'DifferentName'
}
}
}

response = self.client.patch(self.detail_url,
content_type='application/vnd.api+json',
data=dump_json(data))

self.assertEqual(response.status_code, 400)

def test_key_in_post(self):
"""
Ensure a key is in the post.
Expand Down
Loading

0 comments on commit 3471569

Please sign in to comment.