Skip to content

Commit

Permalink
Merge branch 'release/0.8.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
pavlov99 committed Mar 26, 2015
2 parents 8b0bf61 + e9bbf4d commit 820653c
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 40 deletions.
2 changes: 0 additions & 2 deletions .travis.yml
Expand Up @@ -3,10 +3,8 @@ language: python
python: "2.7"

env:
- TOXENV=py27-d15
- TOXENV=py27-d16
- TOXENV=py27-d17
- TOXENV=py33-d15
- TOXENV=py33-d16
- TOXENV=py33-d17
- TOXENV=cov
Expand Down
12 changes: 12 additions & 0 deletions ChangeLog
@@ -1,3 +1,15 @@
2015-03-26 Kirill Pavlov <kirill.pavlov@phystech.edu>

* Version 0.8.2, add atomic transactions support for post and put. If
model raises error during form.save(), other models commited before
would be rolled back.

2015-03-26 Kirill Pavlov <kirill.pavlov@phystech.edu>

* Version 0.8.1, remove Django 1.5 support. Currently Django
1.6 and Django 1.7 are available. Version 1.5 is considered outdated.
Prepare for atomic transactions integration.

2015-03-26 Kirill Pavlov <kirill.pavlov@phystech.edu>

* Version 0.8.0, update document primary information key. Use 'data'
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Expand Up @@ -38,7 +38,7 @@ register:
# target: upload - Upload module on PyPi
upload:
@git push && git push --tags
@python setup.py sdist upload || echo 'Upload already'
@python setup.py sdist bdist_wheel upload || echo 'Upload already'

.PHONY: test
# target: test - Runs tests
Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Expand Up @@ -14,7 +14,7 @@ Welcome to json:api's documentation!

Installation
============
Requires: Django (1.5, 1.6, 1.7); python (2.7, 3.3).
Requires: Django (1.6, 1.7); python (2.7, 3.3).

.. code-block:: python
Expand Down
2 changes: 1 addition & 1 deletion jsonapi/__init__.py
@@ -1,5 +1,5 @@
""" JSON:API realization."""
__version = (0, 8, 0)
__version = (0, 8, 2)

__version__ = version = '.'.join(map(str, __version))
__project__ = PROJECT = __name__
50 changes: 42 additions & 8 deletions jsonapi/resource.py
Expand Up @@ -32,9 +32,8 @@ class Meta:
"""
from . import six
from django.core.paginator import Paginator
from django.db import models
from django.db import models, transaction
from django.forms import ModelForm
import django
import inspect
import json
import logging
Expand Down Expand Up @@ -234,8 +233,7 @@ def get_form(cls, fields=None):
"""
meta_attributes = {"model": cls.Meta.model}
if django.VERSION[:2] >= (1, 6):
meta_attributes["fields"] = '__all__'
meta_attributes["fields"] = '__all__'

if fields is not None:
meta_attributes["fields"] = fields
Expand Down Expand Up @@ -353,13 +351,31 @@ def post(cls, request=None, **kwargs):
response = {
"errors": [{
"status": 400,
"title": "Validation Error",
"title": "Validation error",
"data": form.errors
}]
}
return response

data = [cls.dump_document(f.save()) for f in forms]
data = []
try:
with transaction.atomic():
for form in forms:
instance = form.save()
data.append(cls.dump_document(instance))
except Exception as e:
response = {
"errors": [{
"status": 400,
"title": "Instance save error",
"data": {
"type": e.__class__.__name__,
"args": e.args,
"message": str(e)
}
}]
}
return response

if not is_collection:
data = data[0]
Expand Down Expand Up @@ -407,13 +423,31 @@ def put(cls, request=None, **kwargs):
response = {
"errors": [{
"status": 400,
"title": "Validation Error",
"title": "Validation error",
"data": form.errors
}]
}
return response

data = [cls.dump_document(f.save()) for f in forms]
data = []
try:
with transaction.atomic():
for form in forms:
instance = form.save()
data.append(cls.dump_document(instance))
except Exception as e:
response = {
"errors": [{
"status": 400,
"title": "Instance save error",
"data": {
"type": e.__class__.__name__,
"args": e.args,
"message": str(e)
}
}]
}
return response

if not is_collection:
data = data[0]
Expand Down
4 changes: 2 additions & 2 deletions jsonapi/templates/index.html
Expand Up @@ -33,7 +33,7 @@ <h1>API Documentation</h1>
</aside>
<div id="content">
<h1>Getting Started</h1>
Generated date: {% now 'c' %}
Generated date: {% now 'r' %}

<h1>Resources</h1>
{% for resource_name, resource in resources %}
Expand All @@ -45,7 +45,7 @@ <h2 class="bg-primary">{{ resource_name|capfirst }}</h2>
<table class="table table-bordered">
<tr>
<td>Location</td>
<td>{{ resource.Meta.api.api_url }}/{{ resource_name }}</td>
<td><a href="{{ resource.Meta.api.api_url }}/{{ resource_name }}">{{ resource.Meta.api.api_url }}/{{ resource_name }}</a></td>
</tr>
<tr>
<td>Document Location</td>
Expand Down
3 changes: 3 additions & 0 deletions requirements.txt
Expand Up @@ -10,3 +10,6 @@ mock==1.0.1
testfixtures==4.1.2
ipython==2.1.0
ipdb==0.8

# Build
wheel==0.24.0
5 changes: 5 additions & 0 deletions tests/testapp/models.py
Expand Up @@ -41,6 +41,11 @@
class Author(models.Model):
name = models.CharField(max_length=100)

def save(self, *args, **kwargs):
if self.name == "forbidden name":
raise ValueError("Name {} is not allowed".format(self.name))
return super(Author, self).save(*args, **kwargs)


class Post(models.Model):
title = models.CharField(max_length=100)
Expand Down
114 changes: 105 additions & 9 deletions tests/testapp/tests/test_api.py
Expand Up @@ -4,7 +4,7 @@
from jsonapi.resource import Resource
from mixer.backend.django import mixer
from testfixtures import compare
import django
from mock import patch
import json
import unittest

Expand Down Expand Up @@ -143,14 +143,10 @@ def test_content_type_validation(self):
str(response.content),
"Content-Type SHOULD be application/vnd.api+json")

@unittest.skipIf(django.VERSION[:2] == (1, 5),
"FIXME: For some reason does not work. Tested manually")
def test_base_url(self):
self.client.get('/api', content_type='application/vnd.api+json')
self.assertEqual(api.base_url, "http://testserver")

@unittest.skipIf(django.VERSION[:2] == (1, 5),
"FIXME: For some reason does not work. Tested manually")
def test_api_url(self):
self.client.get('/api', content_type='application/vnd.api+json')
self.assertEqual(api.api_url, "http://testserver/api")
Expand Down Expand Up @@ -221,7 +217,7 @@ def test_create_models(self):
self.assertEqual(Author.objects.count(), 0)
# TODO: try to decrease number of queries
# NOTE: send resource collection
with self.assertNumQueries(3):
with self.assertNumQueries(5):
response = self.client.post(
'/api/author',
json.dumps({
Expand Down Expand Up @@ -304,7 +300,7 @@ def test_create_model_validation_error(self):
expected_data = {
"errors": [{
"status": 400,
"title": "Validation Error",
"title": "Validation error",
"data": {'title': ['This field is required.']},
}]
}
Expand All @@ -327,14 +323,76 @@ def test_create_model_validation_error(self):
expected_data = {
"errors": [{
"status": 400,
"title": "Validation Error",
"title": "Validation error",
"data": {'author': ['This field is required.']},
}]
}

data = json.loads(response.content.decode("utf-8"))
self.assertEqual(data, expected_data)

def test_create_models_validation_error(self):
""" Ensure models are not created if one of them is not validated."""
response = self.client.post(
'/api/author',
json.dumps({
"data": [{
"name": "short name"
}, {
"name": "long name" * 20
}],
}),
content_type='application/vnd.api+json',
HTTP_ACCEPT='application/vnd.api+json'
)
self.assertEqual(response.status_code, 400)

expected_data = {
"errors": [{
"status": 400,
"title": "Validation error",
"data": {'name': ['Ensure this value has at most 100 ' +
'characters (it has 180).']}
}]
}

data = json.loads(response.content.decode("utf-8"))
self.assertEqual(data, expected_data)
self.assertEqual(Post.objects.count(), 0)

def test_create_models_save_error_atomic(self):
""" Ensure models are not created if one of them raises exception."""
response = self.client.post(
'/api/author',
json.dumps({
"data": [{
"name": "short name"
}, {
"name": "forbidden name"
}],
}),
content_type='application/vnd.api+json',
HTTP_ACCEPT='application/vnd.api+json'
)

self.assertEqual(response.status_code, 400)

expected_data = {
"errors": [{
"status": 400,
"title": "Instance save error",
"data": {
"type": "ValueError",
"args": ["Name forbidden name is not allowed"],
"message": 'Name forbidden name is not allowed'
},
}]
}

data = json.loads(response.content.decode("utf-8"))
self.assertEqual(data, expected_data)
self.assertEqual(Author.objects.count(), 0)

def test_update_model(self):
author = mixer.blend("testapp.author", name="")
response = self.client.put(
Expand Down Expand Up @@ -470,7 +528,7 @@ def test_update_model_validation_error(self):
expected_data = {
"errors": [{
"status": 400,
"title": "Validation Error",
"title": "Validation error",
"data": {'name': ['Ensure this value has at most 100 ' +
'characters (it has 101).']},
}]
Expand All @@ -479,6 +537,44 @@ def test_update_model_validation_error(self):
data = json.loads(response.content.decode("utf-8"))
self.assertEqual(data, expected_data)

def test_update_models_save_error_atomic(self):
""" Ensure models are not created if one of them raises exception."""
authors = mixer.cycle(2).blend('testapp.author', name="name")
response = self.client.put(
'/api/author/{}'.format(",".join([str(a.id) for a in authors])),
json.dumps({
"data": [{
"id": authors[0].id,
"name": "allowed name",
}, {
"id": authors[1].id,
"name": "forbidden name",
}],
}),
content_type='application/vnd.api+json',
HTTP_ACCEPT='application/vnd.api+json'
)

self.assertEqual(response.status_code, 400)

expected_data = {
"errors": [{
"status": 400,
"title": "Instance save error",
"data": {
"type": "ValueError",
"args": ["Name forbidden name is not allowed"],
"message": 'Name forbidden name is not allowed'
},
}]
}

data = json.loads(response.content.decode("utf-8"))
self.assertEqual(data, expected_data)
self.assertEqual(Author.objects.count(), 2)
self.assertEqual(
set(Author.objects.values_list("name", flat=True)), {'name'})

def test_delete_model(self):
author = mixer.blend("testapp.author")
response = self.client.delete(
Expand Down
18 changes: 2 additions & 16 deletions tox.ini
@@ -1,7 +1,7 @@
[tox]
envlist =
py27-d15, py27-d16, py27-d17,
py33-d15, py33-d16, py33-d17,
py27-d16, py27-d17,
py33-d16, py33-d17,
pylama, cov

[testenv]
Expand All @@ -12,13 +12,6 @@ deps =
mock==1.0.1
testfixtures==4.1.2

[testenv:py27-d15]
basepython = python2.7
deps =
django==1.5.8
django-discover-runner==1.0
{[testenv]deps}

[testenv:py27-d16]
basepython = python2.7
deps =
Expand All @@ -31,13 +24,6 @@ deps =
django==1.7
{[testenv]deps}

[testenv:py33-d15]
basepython = python3.3
deps =
django==1.5.8
django-discover-runner==1.0
{[testenv]deps}

[testenv:py33-d16]
basepython = python3.3
deps =
Expand Down

0 comments on commit 820653c

Please sign in to comment.