Support Our Site

To ensure we can continue delivering content and maintaining a free platform for all users, we kindly request that you disable your adblocker. Your contribution greatly supports our site's growth and development.

How to Use @action Decorator in Django Rest Framework

5 min read

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.

When a GET request is made to the favorite_books endpoint, the viewset queries the database for books marked as favorites and returns a serialized response containing the details of these books.

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.


DJANGO

Latest Articles

Latest from djangocentral

How to Use Subquery() in Django With Practical Examples

In the realm of web development, Django stands as a powerful and versatile framework for building robust applications. One of the key aspects of developing efficient and optimized web applications is handling database queries effectively. In this article…
Read more →

4 min read

DRF Serializer: Handling OrderedDict and Converting It to a Dictionary or JSON

In Django Rest Framework (DRF) tests, when you access serializer.data, you might encounter an OrderedDict instead of a regular dictionary. This behavior is intentional and reflects the design of DRF's serialization process.Understanding the Problem The u…
Read more →

3 min read

Django Rest Framework CheetSheet: Mastering API Development

Django Rest Framework (DRF) is a powerful toolkit that makes building robust and scalable web APIs with Django a breeze. Whether you're a seasoned Django developer or a newcomer, having a comprehensive cheat sheet at your disposal can be a game-changer. …
Read more →

5 min read

How to Perform NOT Queries in Django ORM

In Django, performing NOT queries allows you to exclude certain records from the query results based on specific conditions. The NOT operator, represented by the tilde (~) when used in conjunction with the Django ORM's Q object, helps you construct compl…
Read more →

3 min read