Sfoglia il codice sorgente

Merge pull request #29 from miki725/drf3

DRF3 support
Miroslav Shubernetskiy 10 anni fa
parent
commit
eeead957e5

+ 7 - 1
.travis.yml

@@ -5,8 +5,14 @@ python:
   - "2.7"
   - "2.7"
   - "pypy"
   - "pypy"
 
 
+env:
+  - "DRF='djangorestframework<3'"
+  - "DRF='djangorestframework>=3'"
+
 # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors
 # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors
-install: pip install -r requirements-dev.txt
+install:
+  - pip install $DRF
+  - pip install -r requirements-dev.txt
 
 
 # command to run tests, e.g. python setup.py test
 # command to run tests, e.g. python setup.py test
 script: make check
 script: make check

+ 1 - 0
AUTHORS.rst

@@ -13,6 +13,7 @@ Contributors
 * Davide Mendolia - https://github.com/davideme
 * Davide Mendolia - https://github.com/davideme
 * Kevin Brown - https://github.com/kevin-brown
 * Kevin Brown - https://github.com/kevin-brown
 * Martin Cavoj - https://github.com/macav
 * Martin Cavoj - https://github.com/macav
+* Matthias Erll - https://github.com/merll
 * Mjumbe Poe - https://github.com/mjumbewu
 * Mjumbe Poe - https://github.com/mjumbewu
 * Thomas Wajs - https://github.com/thomasWajs
 * Thomas Wajs - https://github.com/thomasWajs
 * Xavier Ordoquy - https://github.com/xordoquy
 * Xavier Ordoquy - https://github.com/xordoquy

+ 9 - 1
HISTORY.rst

@@ -3,7 +3,15 @@
 History
 History
 -------
 -------
 
 
-0.1.4 (2014-02-01)
+0.2 (2015-02-09)
+~~~~~~~~~~~~~~~~
+
+* Added DRF3 support. Please note that DRF2 is still supported.
+  Now we support both DRF2 and DRF3!
+* Fixed an issue when using viewsets, single resource update was not working due
+  to ``get_object()`` overwrite in viewset.
+
+0.1.4 (2015-02-01)
 ~~~~~~~~~~~~~~~~~~
 ~~~~~~~~~~~~~~~~~~
 
 
 * Added base model viewset.
 * Added base model viewset.

+ 4 - 0
Makefile

@@ -7,6 +7,7 @@ COVER_FLAGS=${COVER_CONFIG_FLAGS} ${COVER_REPORT_FLAGS}
 
 
 help:
 help:
 	@echo "install - install all requirements including for testing"
 	@echo "install - install all requirements including for testing"
+	@echo "install-quite - same as install but pipes all output to /dev/null"
 	@echo "clean - remove all artifacts"
 	@echo "clean - remove all artifacts"
 	@echo "clean-build - remove build artifacts"
 	@echo "clean-build - remove build artifacts"
 	@echo "clean-pyc - remove Python file artifacts"
 	@echo "clean-pyc - remove Python file artifacts"
@@ -23,6 +24,9 @@ help:
 install:
 install:
 	pip install -r requirements-dev.txt
 	pip install -r requirements-dev.txt
 
 
+install-quite:
+	pip install -r requirements-dev.txt > /dev/null
+
 clean: clean-build clean-pyc clean-test-all
 clean: clean-build clean-pyc clean-test-all
 
 
 clean-build:
 clean-build:

+ 42 - 5
README.rst

@@ -21,9 +21,10 @@ within the framework. That is the purpose of this project.
 Requirements
 Requirements
 ------------
 ------------
 
 
-* Python (2.6, 2.7 and 3.3)
+* Python 2.7+
 * Django 1.3+
 * Django 1.3+
-* Django REST Framework >= 2.2.5 (when bulk features were added to serializers), < 3.0
+* 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!)
 
 
 Installing
 Installing
 ----------
 ----------
@@ -42,9 +43,21 @@ Example
 The bulk views (and mixins) are very similar to Django REST Framework's own
 The bulk views (and mixins) are very similar to Django REST Framework's own
 generic views (and mixins)::
 generic views (and mixins)::
 
 
-    from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView
+    from rest_framework_bulk import (
+        BulkListSerializer,
+        BulkSerializerMixin,
+        ListBulkCreateUpdateDestroyAPIView,
+    )
+
+    class FooSerializer(BulkSerializerMixin, ModelSerializer):
+        class Meta(object):
+            model = FooModel
+            # only necessary in DRF3
+            list_serializer_class = BulkListSerializer
+
     class FooView(ListBulkCreateUpdateDestroyAPIView):
     class FooView(ListBulkCreateUpdateDestroyAPIView):
-        model = FooModel
+        queryset = FooModel.objects.all()
+        serializer_class = FooSerializer
 
 
 The above will allow to create the following queries
 The above will allow to create the following queries
 
 
@@ -85,7 +98,7 @@ The above will allow to create the following queries
 Router
 Router
 ------
 ------
 
 
-The bulk router can map automatically the bulk actions::
+The bulk router can automatically map the bulk actions::
 
 
     from rest_framework_bulk.routes import BulkRouter
     from rest_framework_bulk.routes import BulkRouter
 
 
@@ -98,6 +111,30 @@ The bulk router can map automatically the bulk actions::
     router = BulkRouter()
     router = BulkRouter()
     router.register(r'users', UserViewSet)
     router.register(r'users', UserViewSet)
 
 
+DRF3
+----
+
+Django REST Framework made many API changes which included major changes
+in serializers. As a result, please note the following in order to use
+DRF-bulk with DRF3:
+
+* You must specify custom ``list_serializer_class`` if your view(set)
+  will require update functionality (when using ``BulkUpdateModelMixin``)
+* DRF3 removes read-only fields from ``serializer.validated_data``.
+  As a result, it is impossible to correlate each ``validated_data``
+  in ``ListSerializer`` with a model instance to update since ``validated_data``
+  will be missing the model primary key since that is a read-only field.
+  To deal with that, you must use ``BulkSerializerMixin`` mixin in your serializer
+  class which will add the model primary key field back to the ``validated_data``.
+  By default ``id`` field is used however you can customize that field
+  by using ``update_lookup_field`` in the serializers ``Meta``::
+
+    class FooSerializer(BulkSerializerMixin, ModelSerializer):
+        class Meta(object):
+            model = FooModel
+            list_serializer_class = BulkListSerializer
+            update_lookup_field = 'slug'
+
 Notes
 Notes
 -----
 -----
 
 

+ 1 - 1
requirements.txt

@@ -1,2 +1,2 @@
 django
 django
-djangorestframework<3
+djangorestframework

+ 2 - 1
rest_framework_bulk/__init__.py

@@ -1,8 +1,9 @@
-__version__ = '0.1.4'
+__version__ = '0.2'
 __author__ = 'Miroslav Shubernetskiy'
 __author__ = 'Miroslav Shubernetskiy'
 
 
 try:
 try:
     from .generics import *  # noqa
     from .generics import *  # noqa
     from .mixins import *  # noqa
     from .mixins import *  # noqa
+    from .serializers import *  # noqa
 except Exception:
 except Exception:
     pass
     pass

+ 1 - 0
rest_framework_bulk/drf2/__init__.py

@@ -0,0 +1 @@
+from __future__ import print_function, unicode_literals

+ 122 - 0
rest_framework_bulk/drf2/mixins.py

@@ -0,0 +1,122 @@
+from __future__ import unicode_literals, print_function
+from django.core.exceptions import ValidationError
+from rest_framework import status
+from rest_framework.mixins import CreateModelMixin
+from rest_framework.response import Response
+
+
+__all__ = [
+    'BulkCreateModelMixin',
+    'BulkDestroyModelMixin',
+    'BulkUpdateModelMixin',
+]
+
+
+class BulkCreateModelMixin(CreateModelMixin):
+    """
+    Either create a single or many model instances in bulk by using the
+    Serializers ``many=True`` ability from Django REST >= 2.2.5.
+
+    .. note::
+        This mixin uses the same method to create model instances
+        as ``CreateModelMixin`` because both non-bulk and bulk
+        requests will use ``POST`` request method.
+    """
+
+    def create(self, request, *args, **kwargs):
+        bulk = isinstance(request.DATA, list)
+
+        if not bulk:
+            return super(BulkCreateModelMixin, self).create(request, *args, **kwargs)
+
+        else:
+            serializer = self.get_serializer(data=request.DATA, many=True)
+            if serializer.is_valid():
+                for obj in serializer.object:
+                    self.pre_save(obj)
+                self.object = serializer.save(force_insert=True)
+                for obj in self.object:
+                    self.post_save(obj, created=True)
+                return Response(serializer.data, status=status.HTTP_201_CREATED)
+
+        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+
+class BulkUpdateModelMixin(object):
+    """
+    Update model instances in bulk by using the Serializers
+    ``many=True`` ability from Django REST >= 2.2.5.
+    """
+
+    def get_object(self, queryset=None):
+        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
+
+        if any((lookup_url_kwarg in self.kwargs,
+                self.pk_url_kwarg in self.kwargs,
+                self.slug_url_kwarg in self.kwargs)):
+            return super(BulkUpdateModelMixin, self).get_object(queryset)
+
+        # If the lookup_url_kwarg (or other deprecated variations)
+        # are not present, get_object() is most likely called
+        # as part of metadata() which by default simply checks
+        # for object permissions and raises permission denied if necessary.
+        # Here we don't need to check for general permissions
+        # and can simply return None since general permissions
+        # are checked in initial() which always gets executed
+        # before any of the API actions (e.g. create, update, etc)
+        return
+
+    def bulk_update(self, request, *args, **kwargs):
+        partial = kwargs.pop('partial', False)
+
+        # restrict the update to the filtered queryset
+        serializer = self.get_serializer(self.filter_queryset(self.get_queryset()),
+                                         data=request.DATA,
+                                         many=True,
+                                         partial=partial)
+
+        if serializer.is_valid():
+            try:
+                for obj in serializer.object:
+                    self.pre_save(obj)
+            except ValidationError as err:
+                # full_clean on model instances may be called in pre_save
+                # so we have to handle eventual errors.
+                return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST)
+            self.object = serializer.save(force_update=True)
+            for obj in self.object:
+                self.post_save(obj, created=False)
+            return Response(serializer.data, status=status.HTTP_200_OK)
+
+        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+    def partial_bulk_update(self, request, *args, **kwargs):
+        kwargs['partial'] = True
+        return self.bulk_update(request, *args, **kwargs)
+
+
+class BulkDestroyModelMixin(object):
+    """
+    Destroy model instances.
+    """
+
+    def allow_bulk_destroy(self, qs, filtered):
+        """
+        Hook to ensure that the bulk destroy should be allowed.
+
+        By default this checks that the destroy is only applied to
+        filtered querysets.
+        """
+        return qs is not filtered
+
+    def bulk_destroy(self, request, *args, **kwargs):
+        qs = self.get_queryset()
+        filtered = self.filter_queryset(qs)
+        if not self.allow_bulk_destroy(qs, filtered):
+            return Response(status=status.HTTP_400_BAD_REQUEST)
+
+        for obj in filtered:
+            self.pre_delete(obj)
+            obj.delete()
+            self.post_delete(obj)
+        return Response(status=status.HTTP_204_NO_CONTENT)

+ 15 - 0
rest_framework_bulk/drf2/serializers.py

@@ -0,0 +1,15 @@
+from __future__ import print_function, unicode_literals
+
+
+__all__ = [
+    'BulkListSerializer',
+    'BulkSerializerMixin',
+]
+
+
+class BulkSerializerMixin(object):
+    pass
+
+
+class BulkListSerializer(object):
+    pass

+ 1 - 0
rest_framework_bulk/drf3/__init__.py

@@ -0,0 +1 @@
+from __future__ import print_function, unicode_literals

+ 118 - 0
rest_framework_bulk/drf3/mixins.py

@@ -0,0 +1,118 @@
+from __future__ import print_function, unicode_literals
+from rest_framework import status
+from rest_framework.mixins import CreateModelMixin
+from rest_framework.response import Response
+
+
+__all__ = [
+    'BulkCreateModelMixin',
+    'BulkDestroyModelMixin',
+    'BulkUpdateModelMixin',
+]
+
+
+class BulkCreateModelMixin(CreateModelMixin):
+    """
+    Either create a single or many model instances in bulk by using the
+    Serializers ``many=True`` ability from Django REST >= 2.2.5.
+
+    .. note::
+        This mixin uses the same method to create model instances
+        as ``CreateModelMixin`` because both non-bulk and bulk
+        requests will use ``POST`` request method.
+    """
+
+    def create(self, request, *args, **kwargs):
+        bulk = isinstance(request.data, list)
+
+        if not bulk:
+            return super(BulkCreateModelMixin, self).create(request, *args, **kwargs)
+
+        else:
+            serializer = self.get_serializer(data=request.data, many=True)
+            serializer.is_valid(raise_exception=True)
+            self.perform_bulk_create(serializer)
+            return Response(serializer.data, status=status.HTTP_201_CREATED)
+
+    def perform_bulk_create(self, serializer):
+        return self.perform_create(serializer)
+
+
+class BulkUpdateModelMixin(object):
+    """
+    Update model instances in bulk by using the Serializers
+    ``many=True`` ability from Django REST >= 2.2.5.
+    """
+
+    def get_object(self):
+        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
+
+        if lookup_url_kwarg in self.kwargs:
+            return super(BulkUpdateModelMixin, self).get_object()
+
+        # If the lookup_url_kwarg is not present
+        # get_object() is most likely called as part of options()
+        # which by default simply checks for object permissions
+        # and raises permission denied if necessary.
+        # Here we don't need to check for general permissions
+        # and can simply return None since general permissions
+        # are checked in initial() which always gets executed
+        # before any of the API actions (e.g. create, update, etc)
+        return
+
+    def bulk_update(self, request, *args, **kwargs):
+        partial = kwargs.pop('partial', False)
+
+        # restrict the update to the filtered queryset
+        serializer = self.get_serializer(
+            self.filter_queryset(self.get_queryset()),
+            data=request.data,
+            many=True,
+            partial=partial,
+        )
+        serializer.is_valid(raise_exception=True)
+        self.perform_bulk_update(serializer)
+        return Response(serializer.data, status=status.HTTP_200_OK)
+
+    def partial_bulk_update(self, request, *args, **kwargs):
+        kwargs['partial'] = True
+        return self.bulk_update(request, *args, **kwargs)
+
+    def perform_update(self, serializer):
+        serializer.save()
+
+    def perform_bulk_update(self, serializer):
+        return self.perform_update(serializer)
+
+
+class BulkDestroyModelMixin(object):
+    """
+    Destroy model instances.
+    """
+
+    def allow_bulk_destroy(self, qs, filtered):
+        """
+        Hook to ensure that the bulk destroy should be allowed.
+
+        By default this checks that the destroy is only applied to
+        filtered querysets.
+        """
+        return qs is not filtered
+
+    def bulk_destroy(self, request, *args, **kwargs):
+        qs = self.get_queryset()
+
+        filtered = self.filter_queryset(qs)
+        if not self.allow_bulk_destroy(qs, filtered):
+            return Response(status=status.HTTP_400_BAD_REQUEST)
+
+        self.perform_bulk_destroy(filtered)
+
+        return Response(status=status.HTTP_204_NO_CONTENT)
+
+    def perform_destroy(self, instance):
+        instance.delete()
+
+    def perform_bulk_destroy(self, objects):
+        for obj in objects:
+            self.perform_destroy(obj)

+ 64 - 0
rest_framework_bulk/drf3/serializers.py

@@ -0,0 +1,64 @@
+from __future__ import print_function, unicode_literals
+from rest_framework.exceptions import ValidationError
+from rest_framework.serializers import ListSerializer
+
+
+__all__ = [
+    'BulkListSerializer',
+    'BulkSerializerMixin',
+]
+
+
+class BulkSerializerMixin(object):
+    def to_internal_value(self, data):
+        ret = super(BulkSerializerMixin, self).to_internal_value(data)
+
+        id_attr = getattr(self.Meta, 'update_lookup_field', 'id')
+        request_method = getattr(getattr(self.context.get('view'), 'request'), 'method', '')
+
+        # add update_lookup_field field back to validated data
+        # since super by default strips out read-only fields
+        # hence id will no longer be present in validated_data
+        if all((isinstance(self.root, BulkListSerializer),
+                id_attr,
+                request_method in ('PUT', 'PATCH'))):
+            id_field = self.fields[id_attr]
+            id_value = id_field.get_value(data)
+
+            ret[id_attr] = id_value
+
+        return ret
+
+
+class BulkListSerializer(ListSerializer):
+    update_lookup_field = 'id'
+
+    def update(self, queryset, all_validated_data):
+        id_attr = getattr(self.child.Meta, 'update_lookup_field', 'id')
+
+        all_validated_data_by_id = {
+            i.pop(id_attr): i
+            for i in all_validated_data
+        }
+
+        # 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
+        objects_to_update = queryset.filter(**{
+            '{}__in'.format(id_attr): all_validated_data_by_id.keys(),
+        })
+
+        if len(all_validated_data_by_id) != objects_to_update.count():
+            raise ValidationError('Could not find all objects to update.')
+
+        updated_objects = []
+
+        for obj in objects_to_update:
+            obj_id = getattr(obj, id_attr)
+            obj_validated_data = all_validated_data_by_id.get(obj_id)
+
+            # use model serializer to actually update the model
+            # in case that method is overwritten
+            updated_objects.append(self.child.update(obj, obj_validated_data))
+
+        return updated_objects

+ 12 - 103
rest_framework_bulk/mixins.py

@@ -1,103 +1,12 @@
-from __future__ import unicode_literals, print_function
-from django.core.exceptions import ValidationError
-from rest_framework import status
-from rest_framework.mixins import CreateModelMixin
-from rest_framework.response import Response
-
-
-__all__ = [
-    'BulkCreateModelMixin',
-    'BulkDestroyModelMixin',
-    'BulkUpdateModelMixin',
-]
-
-
-class BulkCreateModelMixin(CreateModelMixin):
-    """
-    Either create a single or many model instances in bulk by using the
-    Serializer's ``many=True`` ability from Django REST >= 2.2.5.
-
-    .. note::
-        This mixin uses the same method to create model instances
-        as ``CreateModelMixin`` because both non-bulk and bulk
-        requests will use ``POST`` request method.
-    """
-
-    def create(self, request, *args, **kwargs):
-        bulk = isinstance(request.DATA, list)
-
-        if not bulk:
-            return super(BulkCreateModelMixin, self).create(request, *args, **kwargs)
-
-        else:
-            serializer = self.get_serializer(data=request.DATA, many=True)
-            if serializer.is_valid():
-                [self.pre_save(obj) for obj in serializer.object]
-                self.object = serializer.save(force_insert=True)
-                [self.post_save(obj, created=True) for obj in self.object]
-                return Response(serializer.data, status=status.HTTP_201_CREATED)
-
-        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
-
-class BulkUpdateModelMixin(object):
-    """
-    Update model instances in bulk by using the Serializer's
-    ``many=True`` ability from Django REST >= 2.2.5.
-    """
-
-    def get_object(self):
-        return self.get_queryset()
-
-    def bulk_update(self, request, *args, **kwargs):
-        partial = kwargs.pop('partial', False)
-
-        # restrict the update to the filtered queryset
-        serializer = self.get_serializer(self.filter_queryset(self.get_queryset()),
-                                         data=request.DATA,
-                                         many=True,
-                                         partial=partial)
-
-        if serializer.is_valid():
-            try:
-                [self.pre_save(obj) for obj in serializer.object]
-            except ValidationError as err:
-                # full_clean on model instances may be called in pre_save
-                # so we have to handle eventual errors.
-                return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST)
-            self.object = serializer.save(force_update=True)
-            [self.post_save(obj, created=False) for obj in self.object]
-            return Response(serializer.data, status=status.HTTP_200_OK)
-
-        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
-    def partial_bulk_update(self, request, *args, **kwargs):
-        kwargs['partial'] = True
-        return self.bulk_update(request, *args, **kwargs)
-
-
-class BulkDestroyModelMixin(object):
-    """
-    Destroy model instances.
-    """
-
-    def allow_bulk_destroy(self, qs, filtered):
-        """
-        Hook to ensure that the bulk destroy should be allowed.
-
-        By default this checks that the destroy is only applied to
-        filtered querysets.
-        """
-        return qs is not filtered
-
-    def bulk_destroy(self, request, *args, **kwargs):
-        qs = self.get_queryset()
-        filtered = self.filter_queryset(qs)
-        if not self.allow_bulk_destroy(qs, filtered):
-            return Response(status=status.HTTP_400_BAD_REQUEST)
-
-        for obj in filtered:
-            self.pre_delete(obj)
-            obj.delete()
-            self.post_delete(obj)
-        return Response(status=status.HTTP_204_NO_CONTENT)
+from __future__ import print_function, unicode_literals
+import rest_framework
+
+
+# import appropriate mixins depending on the DRF version
+# this allows to maintain clean code for each DRF version
+# without doing any magic
+# a little more code but a lit clearer what is going on
+if str(rest_framework.__version__).startswith('2'):
+    from .drf2.mixins import *  # noqa
+else:
+    from .drf3.mixins import *  # noqa

+ 12 - 0
rest_framework_bulk/serializers.py

@@ -0,0 +1,12 @@
+from __future__ import print_function, unicode_literals
+import rest_framework
+
+
+# import appropriate mixins depending on the DRF version
+# this allows to maintain clean code for each DRF version
+# without doing any magic
+# a little more code but a lit clearer what is going on
+if str(rest_framework.__version__).startswith('2'):
+    from .drf2.serializers import *  # noqa
+else:
+    from .drf3.serializers import *  # noqa

+ 13 - 0
rest_framework_bulk/tests/simple_app/serializers.py

@@ -0,0 +1,13 @@
+from __future__ import print_function, unicode_literals
+from rest_framework.serializers import ModelSerializer
+from rest_framework_bulk.serializers import BulkListSerializer, BulkSerializerMixin
+
+from .models import SimpleModel
+
+
+class SimpleSerializer(BulkSerializerMixin,  # only required in DRF3
+                       ModelSerializer):
+    class Meta(object):
+        model = SimpleModel
+        # only required in DRF3
+        list_serializer_class = BulkListSerializer

+ 11 - 8
rest_framework_bulk/tests/simple_app/views.py

@@ -1,22 +1,25 @@
 from __future__ import unicode_literals, print_function
 from __future__ import unicode_literals, print_function
 from rest_framework_bulk import generics
 from rest_framework_bulk import generics
 
 
-from . import models
+from .models import SimpleModel
+from .serializers import SimpleSerializer
 
 
 
 
-class SimpleBulkAPIView(generics.ListBulkCreateUpdateDestroyAPIView):
-    model = models.SimpleModel
+class SimpleMixin(object):
+    model = SimpleModel
+    queryset = SimpleModel.objects.all()
+    serializer_class = SimpleSerializer
 
 
 
 
-class FilteredBulkAPIView(generics.ListBulkCreateUpdateDestroyAPIView):
-    model = models.SimpleModel
+class SimpleBulkAPIView(SimpleMixin, generics.ListBulkCreateUpdateDestroyAPIView):
+    pass
 
 
+
+class FilteredBulkAPIView(SimpleMixin, generics.ListBulkCreateUpdateDestroyAPIView):
     def filter_queryset(self, queryset):
     def filter_queryset(self, queryset):
         return queryset.filter(number__gt=5)
         return queryset.filter(number__gt=5)
 
 
 
 
-class SimpleViewSet(generics.BulkModelViewSet):
-    model = models.SimpleModel
-
+class SimpleViewSet(SimpleMixin, generics.BulkModelViewSet):
     def filter_queryset(self, queryset):
     def filter_queryset(self, queryset):
         return queryset.filter(number__gt=5)
         return queryset.filter(number__gt=5)

+ 11 - 1
rest_framework_bulk/tests/test_generics.py

@@ -152,11 +152,21 @@ class TestBulkAPIViewSet(TestCase):
         super(TestBulkAPIViewSet, self).setUp()
         super(TestBulkAPIViewSet, self).setUp()
         self.url = reverse('api:simple-list')
         self.url = reverse('api:simple-list')
 
 
+    def test_get_single(self):
+        """
+        Test that we are still able to query single resource
+        """
+        response = self.client.get(self.url)
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+
     def test_get(self):
     def test_get(self):
         """
         """
         Test that GET returns 200
         Test that GET returns 200
         """
         """
-        response = self.client.get(self.url)
+        obj = SimpleModel.objects.create(contents='hello world', number=7)
+
+        response = self.client.get(reverse('api:simple-detail', args=[obj.pk]))
 
 
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
 
 

+ 9 - 2
tox.ini

@@ -1,15 +1,22 @@
 [tox]
 [tox]
 envlist =
 envlist =
-    py27, py34, pypy, pypy3
+    {py27,py34,pypy,pypy3}-drf{2,3}
 
 
 [testenv]
 [testenv]
+basepython =
+    py27: python2.7
+    py34: python3.4
+    pypy: pypy
+    pypy3: pypy3
 setenv =
 setenv =
     PYTHONPATH = {toxinidir}:{toxinidir}/multinosetests
     PYTHONPATH = {toxinidir}:{toxinidir}/multinosetests
 commands =
 commands =
+    make install-quite
     pip freeze
     pip freeze
     make check
     make check
 deps =
 deps =
-    -rrequirements-dev.txt
+    drf2: djangorestframework<3
+    drf3: djangorestframework>=3
 whitelist_externals =
 whitelist_externals =
     make
     make