Creating Custom Admin Actions with Extra Data in Django: A Step-by-Step Guide
Tags: webdev, django, python, backend
Published: 2022-12-29 10:48:26

Welcome to our tutorial on creating custom actions with extra data in Django! In this tutorial, we will learn how to create custom actions that allow us to perform specific tasks on a queryset of objects in the Django admin dashboard. We will also learn how to capture extra data from the user through a form and use it in our custom action.

We will be using a simple example of a shop app with a Product model. Our custom action will be a "Decrease Price" action that allows us to decrease the price of a product by a percentage. We will create a form that captures the discount percentage from the user and use it to update the price of the selected products.

Here is a summary of what we will cover in this tutorial:

1. Setting up the Django project and app
2. Setting up the Product model and adding it to the Django admin
3. Setting up the Intermediate Form and view for capturing the discount percentage
4. Create View Template
5. Add URLS
6. Defining the custom action and action function


Let's get started!

## Setting Up the Project
First, we will need to set up a Django project and app. If you already have a Django project and app set up, you can skip this step.

Create a new Django project by running the following command:
```
django-admin startproject myproject
```

Navigate to the project directory:
```
cd myproject
```

Create a new Django app by running the following command:
```
python3 manage.py startapp shop
```

Open the myproject/settings.py file and add shop to the INSTALLED_APPS list.

```python
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'shop.apps.ShopConfig',
]
```

## Setting Up the Product Model
Next, we will need to set up a model for our app. For this tutorial, we will create a simple Product model with a name and price field.

Open the shop/models.py file and add the following code:
```python
from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=200)
    price = models.DecimalField(max_digits=10, decimal_places=2)

    def __str__(self):
        return self.name

```

Run the following command to create a migration file for the model:
```
python3 manage.py makemigrations
```

Run the following command to apply the migration:
```
python3 manage.py migrate
```

Adding the Model to the Admin Dashboard
Now that we have our model set up, we can add it to the Django admin dashboard.


Open the shop/admin.py file and add the following code:
```python
from django.contrib import admin
from .models import Product

admin.site.register(Product)
```

Run the following command to create a superuser:
```
python3 manage.py createsuperuser
```

Run the development server by running the following command:
```
python3 manage.py runserver
```
Navigate to http://127.0.0.1:8000/admin in your web browser and log in with the superuser credentials you created. You should now see the Product model in the dashboard.

![django dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0l4rbq3h0kt6a1czpddy.png)

Add Some Products using admin dashboard

![products list admin view](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xcbvhn9he6ain8tevoy4.png)

## Setting up the Intermediate Form

The first step is to create a form for the intermediate page that will capture the data that we need to perform the action. In this case, we will create a form that allows the user to enter a percentage value that will be used to update the price of the products.

create new file in shop app and call it forms.py **shop/forms.py**
Here is the code for the form:

```python
# shop/forms.py
from django import forms

class ProductPriceUpdateForm(forms.Form):
    percentage = forms.DecimalField(label='Percentage', min_value=0, max_value=100)

```

This form has a single field, percentage, which is a float field that allows the user to enter a decimal value.

## Create view to handle custom action form

**Note don't forget to check is staff user and have permission to change product model **

Next, we need to create the view that will display the form and handle the form submission. Here is the code for the view:

```python
# shop/views.py
from django.contrib import messages
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView
from django.contrib.auth.mixins import PermissionRequiredMixin
from .forms import ProductPriceUpdateForm
from .models import Product


class ProductPriceUpdateView(PermissionRequiredMixin,FormView):
    """
    View for updating product prices in the shop.
    Only users with the `shop.change_product` permission can access this view.
    """
    form_class = ProductPriceUpdateForm
    template_name = 'admin/shop/price_update.html'
    permission_required = ['shop.change_product']

    def form_valid(self, form):
        """
        Update the prices of all products in the shop by the percentage specified in the form.
        """
        percentage = form.cleaned_data['percentage']
        products = Product.objects.all()
        for product in products:
            product.price = product.price - (product.price * percentage / 100)
            product.save()
        messages.success(self.request, _('Prices updated successfully'))
        return redirect('admin:shop_product_changelist')

    def form_invalid(self, form):
        """
        Display an error message if the form is invalid.
        """
        messages.error(self.request, _('Error updating prices'))
        return super().form_invalid(form)

    def get_context_data(self, **kwargs):
        """
        Add the title and form URL to the context data.
        """
        context = super().get_context_data(**kwargs)
        context['title'] = _('Update Prices')
        context['form_url'] = reverse('shop:shop_product_price_update')
        context['products'] = Product.objects.all()
        return context

```

This is a Django view for updating the prices of products in the database. It is a **FormView** that uses the **ProductPriceUpdateForm** form class to handle user input. 

This view requires a specific permission to be accessed, specified by the permission_required attribute.

The **form_valid** method is called when the form is valid and successfully submitted. It retrieves all products in the database and updates their prices based on the percentage specified in the form. If the update is successful, it displays a success message and redirects the user to the list view of products in the admin panel.

The **form_invalid** method is called when the form is invalid or there is an error in processing the form. It displays an error message and calls the parent **form_invalid** method.

The **get_context_data** method is used to add additional context data to the template context. It adds the title of the page and the URL of the form to the context.

This view requires the **shop.change_product** permission to access.

## Create View Template

This template extends the admin/base_site.html template, which provides the basic layout and styling for the Django Admin interface.

To display the form fields, we use the {/{ form.percentage }/} template tag, which will render the percentage field from the ProductPriceUpdateForm form.

To create the template, follow these steps:

1. Create a new directory named templates at the root level of your Django app **shop/templates** .
2. Inside the templates directory, create a new directory named admin.
3. Inside the admin directory, create a new directory named shop.
4. Inside the shop directory, create a new file named price_update.html.
5. Copy and paste the template code into the price_update.html file.
6. That's it! The template should now be set up and ready to use.

**Note:
you can place template in any template folder just navigate to file in view**

template code :

```html
{% extends "admin/base_site.html" %}
{% load i18n %}

{% block title %}{{ title }}{% endblock %}

{% block content %}
    <form method="post" novalidate>
        {% csrf_token %}
        <div class="form-group">
            {{ form.percentage }}
        </div>
        <button type="submit" class="btn btn-primary">{% trans 'Update Prices' %}</button>
    </form>
{% endblock %}

```

## Add URLS
First create  file inside shop app directory call it urls.py 

```python
#shop/urls.py
from . import views
from django.urls import path

app_name='shop'
urlpatterns = [
    path("update-products-price/", views.ProductPriceUpdateView.as_view(), name="shop_product_price_update")
]

```
Then update urls.py file in myproject directory

```python
#myproject/urls.py
from django.contrib import admin
from django.urls import path,include

urlpatterns = [
    path('admin/',include('shop.urls')),
    path('admin/', admin.site.urls)
]

```

now run our server
```
python3 manage.py runserver
```

then navigate to
[http://127.0.0.1:8000/admin/update-products-price/](http://127.0.0.1:8000/admin/update-products-price/ )

![update price view](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jznl17frkruhmv2yeh81.png)

**Wait a minute this view will update all products prices but it must just update products selected by action is that right?**

Yes, you are correct. Currently, the view is updating the price of all products in the database. To update only the selected products, we need to create action to pass the selected Product objects to the intermediate view and use those Products to update the prices.

Here is how we can do that:

## Create Custom Admin Action
First, we need to create custom admin action to pass the selected Product objects to the intermediate view. Here is the code for the admin action:

```python
# shop/admin.py
from django.contrib import admin
from django.http import HttpResponseRedirect
from django.urls import reverse

from .models import Product

def update_prices(modeladmin, request, queryset):
    # Get the list of selected product IDs
    product_ids = queryset.values_list('id', flat=True)
    # Redirect to the intermediate view with the selected product IDs
    return HttpResponseRedirect(reverse('shop:shop_product_price_update') + '?ids=' + ','.join(str(p) for p in product_ids))

update_prices.short_description = "Update prices"

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ('name', 'price')
    actions = [update_prices]
```

In the update_prices action, we first retrieve the list of IDs for the selected Product objects. Then, we redirect the user to the intermediate view with the list of IDs as a query parameter.

Next, we need to modify the intermediate view to retrieve the list of IDs and use them to update the prices of the selected products so let's update form_valid and get_context_data in intermediate view. 

Here is the updated code for the intermediate view: 
```python
# shop/views.py
from django.contrib import messages
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView
from django.contrib.auth.mixins import PermissionRequiredMixin
from .forms import ProductPriceUpdateForm
from .models import Product


class ProductPriceUpdateView(PermissionRequiredMixin,FormView):
    """
    View for updating product prices in the shop.
    Only users with the `shop.change_product` permission can access this view.
    """
    form_class = ProductPriceUpdateForm
    template_name = 'admin/shop/price_update.html'
    permission_required = ['shop.change_product']

    def form_valid(self, form):
        """
        Update the prices of all products in the shop by the percentage specified in the form.
        """
        percentage = form.cleaned_data['percentage']
        # get product ids from url
        product_ids = self.request.GET.get('ids').split(',')

        # Get the selected products
        products = Product.objects.filter(id__in=product_ids)
        for product in products:
            product.price = product.price - (product.price * percentage / 100)
            product.save()
        messages.success(self.request, _('Prices updated successfully'))
        return redirect('admin:shop_product_changelist')

    def form_invalid(self, form):
        """
        Display an error message if the form is invalid.
        """
        messages.error(self.request, _('Error updating prices'))
        return super().form_invalid(form)

    def get_context_data(self, **kwargs):
        """
        Add the title and form URL to the context data.
        """
        context = super().get_context_data(**kwargs)
        context['title'] = _('Update Prices')
        context['form_url'] = reverse('shop:shop_product_price_update')

        # get product ids from url
        product_ids = self.request.GET.get('ids').split(',')

        context['products'] = Product.objects.filter(id__in=product_ids)
        return context

```

In the form_valid method, we first retrieve the list of selected product IDs from the query parameters using self.request.GET.get('ids').split(',') . Then, we use the Product.objects.filter() method to get the selected Product objects using the id__in filter. Finally, we loop through the selected products and update their prices using the percentage value entered by the user.

So now try to use the action and test if it will just update selected products or not.