From 2fb3648505bb31f4bc55f57a3fd0bbd4846c1951 Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Wed, 18 Feb 2015 23:19:27 -0500 Subject: [PATCH 1/9] added unique_together model including tests to verify that PATCH works --- .../tests/simple_app/models.py | 8 +++++++ .../tests/simple_app/serializers.py | 10 ++++++++- rest_framework_bulk/tests/simple_app/urls.py | 3 ++- rest_framework_bulk/tests/simple_app/views.py | 10 +++++++-- rest_framework_bulk/tests/test_generics.py | 21 ++++++++++++++++++- tests/__init__.py | 2 ++ 6 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 tests/__init__.py diff --git a/rest_framework_bulk/tests/simple_app/models.py b/rest_framework_bulk/tests/simple_app/models.py index 8c3eafa..243b1c9 100644 --- a/rest_framework_bulk/tests/simple_app/models.py +++ b/rest_framework_bulk/tests/simple_app/models.py @@ -5,3 +5,11 @@ class SimpleModel(models.Model): number = models.IntegerField() contents = models.CharField(max_length=16) + + +class UniqueTogetherModel(models.Model): + foo = models.IntegerField() + bar = models.IntegerField() + + class Meta(object): + unique_together = ('foo', 'bar') diff --git a/rest_framework_bulk/tests/simple_app/serializers.py b/rest_framework_bulk/tests/simple_app/serializers.py index 423083c..c182407 100644 --- a/rest_framework_bulk/tests/simple_app/serializers.py +++ b/rest_framework_bulk/tests/simple_app/serializers.py @@ -2,7 +2,7 @@ from rest_framework.serializers import ModelSerializer from rest_framework_bulk.serializers import BulkListSerializer, BulkSerializerMixin -from .models import SimpleModel +from .models import SimpleModel, UniqueTogetherModel class SimpleSerializer(BulkSerializerMixin, # only required in DRF3 @@ -11,3 +11,11 @@ class Meta(object): model = SimpleModel # only required in DRF3 list_serializer_class = BulkListSerializer + + +class UniqueTogetherSerializer(BulkSerializerMixin, # only required in DRF3 + ModelSerializer): + class Meta(object): + model = UniqueTogetherModel + # only required in DRF3 + list_serializer_class = BulkListSerializer diff --git a/rest_framework_bulk/tests/simple_app/urls.py b/rest_framework_bulk/tests/simple_app/urls.py index f1628d5..6ace076 100644 --- a/rest_framework_bulk/tests/simple_app/urls.py +++ b/rest_framework_bulk/tests/simple_app/urls.py @@ -2,11 +2,12 @@ from django.conf.urls import patterns, url, include from rest_framework_bulk.routes import BulkRouter -from .views import SimpleViewSet +from .views import SimpleViewSet, UniqueTogetherViewSet router = BulkRouter() router.register('simple', SimpleViewSet, 'simple') +router.register('unique-together', UniqueTogetherViewSet, 'unique-together') urlpatterns = patterns( '', diff --git a/rest_framework_bulk/tests/simple_app/views.py b/rest_framework_bulk/tests/simple_app/views.py index 7bc16c8..b550f2a 100644 --- a/rest_framework_bulk/tests/simple_app/views.py +++ b/rest_framework_bulk/tests/simple_app/views.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals, print_function from rest_framework_bulk import generics -from .models import SimpleModel -from .serializers import SimpleSerializer +from .models import SimpleModel, UniqueTogetherModel +from .serializers import SimpleSerializer, UniqueTogetherSerializer class SimpleMixin(object): @@ -23,3 +23,9 @@ def filter_queryset(self, queryset): class SimpleViewSet(SimpleMixin, generics.BulkModelViewSet): def filter_queryset(self, queryset): return queryset.filter(number__gt=5) + + +class UniqueTogetherViewSet(generics.BulkModelViewSet): + model = UniqueTogetherModel + queryset = UniqueTogetherModel.objects.all() + serializer_class = UniqueTogetherSerializer diff --git a/rest_framework_bulk/tests/test_generics.py b/rest_framework_bulk/tests/test_generics.py index 5522948..5daff4f 100644 --- a/rest_framework_bulk/tests/test_generics.py +++ b/rest_framework_bulk/tests/test_generics.py @@ -5,7 +5,7 @@ from django.test.client import RequestFactory from rest_framework import status -from .simple_app.models import SimpleModel +from .simple_app.models import SimpleModel, UniqueTogetherModel from .simple_app.views import FilteredBulkAPIView, SimpleBulkAPIView @@ -256,6 +256,25 @@ def test_patch(self): self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_patch_unique_together(self): + """ + Test that PATCH with multiple partial resources returns 200 + even on model with unique together columns + """ + obj1 = UniqueTogetherModel.objects.create(foo=1, bar=2) + obj2 = UniqueTogetherModel.objects.create(foo=3, bar=4) + + response = self.client.patch( + reverse('api:unique-together-list'), + data=json.dumps([ + {'foo': 5, 'id': obj1.pk}, + {'foo': 6, 'id': obj2.pk}, + ]), + content_type='application/json', + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_delete(self): """ Test that PATCH with multiple partial resources returns 200 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..8f4a3fc --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function, unicode_literals From f0b768ef8f05a2119c1df35cafdef10c9a41750a Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Sun, 26 Apr 2015 17:41:18 -0400 Subject: [PATCH 2/9] switched to using py.test as test suite since django-nose does not support django1.8 --- Makefile | 18 ++++++++++-------- requirements-dev.txt | 3 ++- tests/settings.py | 5 +++-- tox.ini | 2 +- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 2a5771b..7d4160c 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,7 @@ .PHONY: clean-pyc clean-build docs clean -NOSE_FLAGS=-s --verbosity=2 -COVER_CONFIG_FLAGS=--with-coverage --cover-package=rest_framework_bulk --cover-erase -COVER_REPORT_FLAGS=--cover-html --cover-html-dir=htmlcov -COVER_FLAGS=${COVER_CONFIG_FLAGS} ${COVER_REPORT_FLAGS} +TEST_FLAGS=--verbosity=2 +COVER_FLAGS=--source=rest_framework_bulk help: @echo "install - install all requirements including for testing" @@ -51,10 +49,14 @@ lint: flake8 rest_framework_bulk test: - python tests/manage.py test ${NOSE_FLAGS} - -test-coverage: - python tests/manage.py test ${NOSE_FLAGS} ${COVER_FLAGS} + python tests/manage.py test ${TEST_FLAGS} + +test-coverage: clean-test + -coverage run ${COVER_FLAGS} tests/manage.py test ${TEST_FLAGS} + @exit_code=$? + @-coverage report + @-coverage html + @exit ${exit_code} test-all: tox diff --git a/requirements-dev.txt b/requirements-dev.txt index 9060574..35c00dd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,6 @@ -r requirements.txt coverage -django-nose +django-pytest flake8 +pytest tox diff --git a/tests/settings.py b/tests/settings.py index 48f7ae6..3c541c8 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -12,14 +12,15 @@ MIDDLEWARE_CLASSES = () INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', 'django.contrib.staticfiles', - 'django_nose', 'rest_framework', 'rest_framework_bulk', 'rest_framework_bulk.tests.simple_app', ) -TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' +TEST_RUNNER = 'django_pytest.test_runner.TestRunner' STATIC_URL = '/static/' SECRET_KEY = 'foo' diff --git a/tox.ini b/tox.ini index d088851..eb4c9a1 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ basepython = pypy: pypy pypy3: pypy3 setenv = - PYTHONPATH = {toxinidir}:{toxinidir}/multinosetests + PYTHONPATH = {toxinidir} commands = make install-quite pip freeze From 1154b58b4024119a45039f746d76be80791aefc2 Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Sun, 26 Apr 2015 17:46:21 -0400 Subject: [PATCH 3/9] added pip freeze to travis install script --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 6318584..00ede8d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ env: install: - pip install $DRF - pip install -r requirements-dev.txt + - pip freeze # command to run tests, e.g. python setup.py test script: make check From fd07a9fd14d7dee46bf78ecd68e99df78d515582 Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Sun, 26 Apr 2015 17:51:57 -0400 Subject: [PATCH 4/9] removed pytest dependency since since it does not create proper test db --- requirements-dev.txt | 2 -- tests/settings.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 35c00dd..257c17d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,4 @@ -r requirements.txt coverage -django-pytest flake8 -pytest tox diff --git a/tests/settings.py b/tests/settings.py index 3c541c8..2fcd255 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -20,8 +20,6 @@ 'rest_framework_bulk.tests.simple_app', ) -TEST_RUNNER = 'django_pytest.test_runner.TestRunner' - STATIC_URL = '/static/' SECRET_KEY = 'foo' From 914258945c034c7dae780b3f7237dffafba03373 Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Sun, 26 Apr 2015 18:15:23 -0400 Subject: [PATCH 5/9] testing in travis DRF<3 only with Django<1.8 --- .travis.yml | 6 +++--- README.rst | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 00ede8d..3e2c049 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,12 +6,12 @@ python: - "pypy" env: - - "DRF='djangorestframework<3'" - - "DRF='djangorestframework>=3'" + - "$DJANGO_DRF='django<1.8' 'djangorestframework<3'" + - "$DJANGO_DRF='djangorestframework>=3'" # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors install: - - pip install $DRF + - pip install $DJANGO_DRF - pip install -r requirements-dev.txt - pip freeze diff --git a/README.rst b/README.rst index 0c6027b..af70822 100644 --- a/README.rst +++ b/README.rst @@ -21,10 +21,11 @@ within the framework. That is the purpose of this project. Requirements ------------ -* Python 2.7+ -* Django 1.3+ -* Django REST Framework >= 2.2.5 (when bulk features were added to serializers) -* Django REST Framework >= 3.0.0 (DRF-bulk supports both DRF2 and DRF3!) +* Python>=2.7 +* Django>=1.3 +* Django REST Framework >= 3.0.0 +* REST Framework >= 2.2.5 + (**only with** Django<1.8 since DRF<3 does not support Django1.8) Installing ---------- From e1d01ca6feee472458f7e73cd762eb5356aec6b5 Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Sun, 26 Apr 2015 18:34:56 -0400 Subject: [PATCH 6/9] changed tox config to use Django<1.8 for DRF<3 [ci skip] --- rest_framework_bulk/tests/test_generics.py | 18 ++++++++++++++++++ tox.ini | 16 ++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/rest_framework_bulk/tests/test_generics.py b/rest_framework_bulk/tests/test_generics.py index 5daff4f..7fe5d72 100644 --- a/rest_framework_bulk/tests/test_generics.py +++ b/rest_framework_bulk/tests/test_generics.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals, print_function import json +import unittest + from django.core.urlresolvers import reverse from django.test import TestCase from django.test.client import RequestFactory @@ -83,6 +85,22 @@ def test_put(self): ] ) + @unittest.skip('') + def test_put_without_update_key(self): + """ + Test that PUT request updates all submitted resources. + """ + response = self.view(self.request.put( + '', + json.dumps([ + {'contents': 'foo', 'number': 3}, + {'contents': 'bar', 'number': 4, 'id': 555}, # non-existing id + ]), + content_type='application/json', + )) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_patch(self): """ Test that PATCH request partially updates all submitted resources. diff --git a/tox.ini b/tox.ini index eb4c9a1..0af40aa 100644 --- a/tox.ini +++ b/tox.ini @@ -20,5 +20,21 @@ deps = whitelist_externals = make +[testenv:py27-drf2] +deps = + django<1.8 + +[testenv:py34-drf2] +deps = + django<1.8 + +[testenv:pypy-drf2] +deps = + django<1.8 + +[testenv:pypy3-drf2] +deps = + django<1.8 + [flake8] max-line-length = 100 From fa1e9faa4eee977cd354ca277020dea0ab8bbd8d Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Sun, 26 Apr 2015 18:44:51 -0400 Subject: [PATCH 7/9] fixes #34 --- rest_framework_bulk/drf3/serializers.py | 6 ++++++ rest_framework_bulk/tests/test_generics.py | 3 +-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/rest_framework_bulk/drf3/serializers.py b/rest_framework_bulk/drf3/serializers.py index 9098829..31fff46 100644 --- a/rest_framework_bulk/drf3/serializers.py +++ b/rest_framework_bulk/drf3/serializers.py @@ -1,4 +1,6 @@ from __future__ import print_function, unicode_literals +import inspect + from rest_framework.exceptions import ValidationError from rest_framework.serializers import ListSerializer @@ -41,6 +43,10 @@ def update(self, queryset, all_validated_data): for i in all_validated_data } + if not all((bool(i) and not inspect.isclass(i) + for i in all_validated_data_by_id.keys())): + raise ValidationError('') + # since this method is given a queryset which can have many # model instances, first find all objects to update # and only then update the models diff --git a/rest_framework_bulk/tests/test_generics.py b/rest_framework_bulk/tests/test_generics.py index 7fe5d72..87dbf3e 100644 --- a/rest_framework_bulk/tests/test_generics.py +++ b/rest_framework_bulk/tests/test_generics.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals, print_function import json -import unittest from django.core.urlresolvers import reverse from django.test import TestCase @@ -85,7 +84,6 @@ def test_put(self): ] ) - @unittest.skip('') def test_put_without_update_key(self): """ Test that PUT request updates all submitted resources. @@ -94,6 +92,7 @@ def test_put_without_update_key(self): '', json.dumps([ {'contents': 'foo', 'number': 3}, + {'contents': 'rainbows', 'number': 4}, # multiple objects without id {'contents': 'bar', 'number': 4, 'id': 555}, # non-existing id ]), content_type='application/json', From c8f210671e5235387f4218e82f3ec76cc53465aa Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Sun, 26 Apr 2015 18:49:15 -0400 Subject: [PATCH 8/9] bumped version and updated history [ci skip] --- HISTORY.rst | 8 ++++++++ rest_framework_bulk/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7bf4f04..7d4f47b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,14 @@ History ------- +0.2.1 (2015-04-26) +~~~~~~~~~~~~~~~~~~ + +* Fixed a bug which allowed to submit data for update to serializer + without update field. + See `#34 `_. +* Removed support for Django1.8 with DRF2.x + 0.2 (2015-02-09) ~~~~~~~~~~~~~~~~ diff --git a/rest_framework_bulk/__init__.py b/rest_framework_bulk/__init__.py index 7dceaaf..1d7f9a3 100644 --- a/rest_framework_bulk/__init__.py +++ b/rest_framework_bulk/__init__.py @@ -1,4 +1,4 @@ -__version__ = '0.2' +__version__ = '0.2.1' __author__ = 'Miroslav Shubernetskiy' try: From 4c64813cdff12969e8ed5320807976ac92674be0 Mon Sep 17 00:00:00 2001 From: Damon Jablons Date: Wed, 4 Nov 2015 12:21:33 -0500 Subject: [PATCH 9/9] Work around for #31 --- rest_framework_bulk/drf3/serializers.py | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/rest_framework_bulk/drf3/serializers.py b/rest_framework_bulk/drf3/serializers.py index 31fff46..504c88d 100644 --- a/rest_framework_bulk/drf3/serializers.py +++ b/rest_framework_bulk/drf3/serializers.py @@ -35,6 +35,34 @@ def to_internal_value(self, data): class BulkListSerializer(ListSerializer): update_lookup_field = 'id' + def to_internal_value(self, data): + try: + return super(BulkListSerializer, self).to_internal_value(data) + except AttributeError: + pass + + instance_map = { + getattr(i, self.update_lookup_field): i for i in self.instance + } + + ret = [] + errors = [] + for item in data: + field = item[self.update_lookup_field] + self.child.instance = instance_map.get(field) + try: + validated = self.child.run_validation(item) + except ValidationError as exc: + errors.append(exc.detail) + else: + ret.append(validated) + errors.append({}) + + if any(errors): + raise ValidationError(errors) + + return ret + def update(self, queryset, all_validated_data): id_attr = getattr(self.child.Meta, 'update_lookup_field', 'id')