Repository: encode/django-rest-framework

Issue:
Keyword arguments to @action lost.
When attempting to use `@action` to override `permission_classes` on a per-method basis (as the documentation [suggests](http://django-rest-framework.org/api-guide/viewsets.html#marking-extra-methods-for-routing) is possible), it appears that the additional keyword arguments are lost. 

Test case:

```
class BookViewSet(mixins.RetrieveModelMixin, mixins.UpdateModelMixin,
                  viewsets.GenericViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    permission_classes = []

    @action(permission_classes=[AuthorBookset])
    def update(self, request, *args, **kwargs):
        return super(BookViewSet, self). update(request, *args, **kwargs)
```

This happens because `APIView.get_permissions()` uses `self.permission_classes` when creating permission instances. `@action` decorates a method on a subclass of `APIView`, and [assigns](https://github.com/tomchristie/django-rest-framework/blob/ae2887ffc41b1e05d6706f51b00266efccad7a58/rest_framework/decorators.py#L127) those additional keyword arguments as a property of the decorated method. Since they are applied to the method and not used to update the class, `APIView.get_permissions()` never sees them and they are lost.



Discussion:
CC @andymckay

Note: Please ignore the shitty commit history on the reference commit - that's me being braindead :-/

This is now tested for [here](https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/tests/test_routers.py#L167) and works as expected.

I didn't notice when this ticket was filed that the method you're referring to is named `update`.

The `@action` decorator is intended to create _new_ routes, not to modify the existing standard routes.
I'm slightly confused about exactly what you'd expect to happen in the example you've used - a additional route such as `'/book/123/update/`, or modify the existing standard PUT on `/book/123/`?

I'm leaving this ticket open for the moment, because this probably either needs better documentation, or the example you've given should raise an assertion error, or both.

Ah, it seemed to me that `@action` could be used to modify a standard route by, for example, allowing a `Permission` class to be selectively applied to a single route. Seems like a common-enough use case: allow all users to retrieve an object, but only allow owners that object to update or delete it. [From the docs](http://django-rest-framework.org/api-guide/viewsets.html#marking-extra-methods-for-routing), `@action` seemed to be the way to get that done (though admittedly, the name doesn't imply that).

Though definitely my mistake, the docs would likely benefit from a little clarification.


Resolving patch:
diff --git a/rest_framework/routers.py b/rest_framework/routers.py
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -15,7 +15,9 @@
 """
 from __future__ import unicode_literals
 
+import itertools
 from collections import namedtuple
+from django.core.exceptions import ImproperlyConfigured
 from rest_framework import views
 from rest_framework.compat import patterns, url
 from rest_framework.response import Response
@@ -38,6 +40,13 @@ def replace_methodname(format_string, methodname):
     return ret
 
 
+def flatten(list_of_lists):
+    """
+    Takes an iterable of iterables, returns a single iterable containing all items
+    """
+    return itertools.chain(*list_of_lists)
+
+
 class BaseRouter(object):
     def __init__(self):
         self.registry = []
@@ -130,12 +139,17 @@ def get_routes(self, viewset):
         Returns a list of the Route namedtuple.
         """
 
+        known_actions = flatten([route.mapping.values() for route in self.routes])
+
         # Determine any `@action` or `@link` decorated methods on the viewset
         dynamic_routes = []
         for methodname in dir(viewset):
             attr = getattr(viewset, methodname)
             httpmethods = getattr(attr, 'bind_to_methods', None)
             if httpmethods:
+                if methodname in known_actions:
+                    raise ImproperlyConfigured('Cannot use @action or @link decorator on '
+                                               'method "%s" as it is an existing route' % methodname)
                 httpmethods = [method.lower() for method in httpmethods]
                 dynamic_routes.append((httpmethods, methodname))
 


Test patch:
diff --git a/rest_framework/tests/test_routers.py b/rest_framework/tests/test_routers.py
--- a/rest_framework/tests/test_routers.py
+++ b/rest_framework/tests/test_routers.py
@@ -2,6 +2,7 @@
 from django.db import models
 from django.test import TestCase
 from django.test.client import RequestFactory
+from django.core.exceptions import ImproperlyConfigured
 from rest_framework import serializers, viewsets, permissions
 from rest_framework.compat import include, patterns, url
 from rest_framework.decorators import link, action
@@ -191,3 +192,24 @@ def test_action_kwargs(self):
             response.data,
             {'permission_classes': [permissions.AllowAny]}
         )
+
+class TestActionAppliedToExistingRoute(TestCase):
+    """
+    Ensure `@action` decorator raises an except when applied
+    to an existing route
+    """
+
+    def test_exception_raised_when_action_applied_to_existing_route(self):
+        class TestViewSet(viewsets.ModelViewSet):
+
+            @action()
+            def retrieve(self, request, *args, **kwargs):
+                return Response({
+                    'hello': 'world'
+                })
+
+        self.router = SimpleRouter()
+        self.router.register(r'test', TestViewSet, base_name='test')
+
+        with self.assertRaises(ImproperlyConfigured):
+            self.router.urls