Django Rest Framework (DRF) provides powerful tools to build robust APIs. While the standard CRUD operations are usually sufficient, there are cases where you might need to add custom actions or endpoints to perform specific operations.
This is where the @action
decorator comes into play, allowing you to extend your ViewSets with additional functionalities.
In this article, we will explore the concept of @action
and demonstrate how to create custom actions using the GET and POST methods with practical code examples.
Using @action with DRF ViewSet
Let's start by defining a basic ViewSet for our example.
In the views.py
file of the your app, create a ViewSet that extends from DRF's ViewSet:
# myapp/views.py
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet
class MyViewSet(ViewSet):
# Standard ViewSet actions (e.g., list, create, retrieve, update, delete)
# will be added here later
pass
To use our ViewSet, we need to register it with Django's URL configuration.
In the urls.py
file of the project, add the following code:
# myproject/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from myapp.views import MyViewSet
router = DefaultRouter()
router.register(r'myviewset', MyViewSet)
urlpatterns = [
path('', include(router.urls)),
]
Adding a Custom GET Action:
Now, let's create a custom action that responds to a GET request.
We'll implement a method called custom_get_action
in our ViewSet and use the @action
decorator to map it to the GET method:
# myapp/views.py
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet
class MyViewSet(ViewSet):
# ... Standard ViewSet actions ...
@action(detail=True, methods=['GET'])
def custom_get_action(self, request, pk=None):
# Custom action logic for GET request
# Retrieve data or perform any operation based on the instance with 'pk'
return Response({'message': f'Custom GET action executed for instance {pk}.'})
In Django Rest Framework, the detail
argument in the @action
decorator specifies whether the custom action is bound to a single instance of the resource (detail view) or to the entire resource collection (list view).
- When
detail=True
: The custom action is associated with a single instance of the resource and is accessed through the URL pattern containing the instance's primary key. This means the action is specific to a particular resource object and typically involves performing an operation on that specific object. - When
detail=False
: The custom action is associated with the entire resource collection and is accessed through the URL pattern without any instance's primary key. This means the action is not tied to a specific resource instance and typically involves performing an operation on the entire collection of resources.
Adding a Custom POST Action:
Next, let's add a custom action that responds to a POST request.
We'll implement a method called custom_post_action
in our ViewSet and use the @action
decorator to map it to the POST method:
# myapp/views.py
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet
class MyViewSet(ViewSet):
# ... Standard ViewSet actions ...
@action(detail=False, methods=['POST'])
def custom_post_action(self, request):
# Custom action logic for POST request
# Perform any operation with the posted data
return Response({'message': 'Custom POST action executed successfully.'})
Now, our custom actions are ready to be tested.
Start the development server
python manage.py runserver
Open your browser or use a tool like cURL or Postman to make GET and POST requests to the custom actions:
To trigger the custom GET action, make a GET request to: http://localhost:8000/myviewset/{pk}/custom_get_action/ (Replace {pk} with an actual instance ID).
To trigger the custom POST action, make a POST request to: http://localhost:8000/myviewset/custom_post_action/ with any data in the request body.
Reversing @action URLs
When you define custom actions using the @action
decorator in your viewset, you can use the reverse()
function to generate the URLs for these actions programmatically.
The reverse()
function in Django is used to retrieve the URL patterns associated with a given view or viewset.
The reverse()
function takes the view or viewset name and the action name as arguments and returns the corresponding URL.
Here's how you can use reverse to generate the URLs:
from django.urls import reverse
# Generating the URL for custom_post_action
url_post = reverse('my-viewset-custom-post-action')
print(url_post) # Output: '/my-viewset/custom-post-action/'
# Assuming you have a MyObject instance with pk=1
# Generating the URL for custom_get_action with a specific object
url_get = reverse('my-viewset-custom-get-action', kwargs={'pk': 1})
print(url_get)
# Output: '/my-viewset/1/custom-get-action/'
Practical Example of @action
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from .models import Book
from .serializers import BookSerializer
class BookViewSet(ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
# Regular viewset actions for CRUD operations
@action(detail=True, methods=['POST'])
def mark_as_favorite(self, request, pk=None):
try:
book = self.get_object()
except Book.DoesNotExist:
return Response({"error": "Book not found."}, status=404)
# Perform the action to mark the book as a favorite for the current user
# You can add your custom logic here, like updating the database or any other operation
# For this example, we'll just set a 'favorite' flag to True
book.favorite = True
book.save()
return Response({"message": f"Book '{book.title}' marked as favorite."}, status=200)
@action(detail=False, methods=['GET'])
def favorite_books(self, request):
# Get all books marked as favorite
favorite_books = self.queryset.filter(favorite=True)
serializer = self.get_serializer(favorite_books, many=True)
return Response(serializer.data)
When a POST request is made to the mark_as_favorite
endpoint with the book's primary key (pk), the viewset fetches the book instance, updates its favorite field, and saves it.
Unit testing @action
To unit test @action
methods in Django Rest Framework (DRF), you can use the DRF's test utilities provided in the rest_framework.test
module.
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from .models import Book # Import your Book model
from .views import BookViewSet # Import your BookViewSet
class BookViewSetTestCase(APITestCase):
def setUp(self):
# Create test data for the Book model
self.book = Book.objects.create(title='Test Book', author='Test Author', favorite=False)
self.url = reverse('book-mark-as-favorite', args=[self.book.pk]) # URL for the mark_as_favorite action
def test_mark_as_favorite(self):
# Ensure the mark_as_favorite action works as expected
data = {} # You can provide any necessary data in the request, if required
response = self.client.post(self.url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['message'], f"Book '{self.book.title}' marked as favorite.")
# Check if the book was updated in the database
updated_book = Book.objects.get(pk=self.book.pk)
self.assertTrue(updated_book.favorite)
def test_favorite_books(self):
# Ensure the favorite_books action returns the correct data
# Create additional favorite books for the test
favorite_book1 = Book.objects.create(title='Favorite Book 1', author='Author 1', favorite=True)
favorite_book2 = Book.objects.create(title='Favorite Book 2', author='Author 2', favorite=True)
url = reverse('book-favorite-books') # URL for the favorite_books action
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 2)
This example demonstrates how to write unit tests for @action
methods in a Django Rest Framework viewset. You can expand these tests based on your specific actions and business logic in the viewset.
Conclusion
In this article, we explored how to use the @action
decorator in Django Rest Framework to create custom actions for our ViewSets. By adding custom functionalities, you can extend the capabilities of your APIs and tailor them to your specific needs.
The @action
decorator is a powerful tool that empowers developers to build more versatile and flexible APIs.