Friday, July 23, 2021

Django admin customizations

By default, the Django admin displays each object by calling str() on it. You can make this screen a little more helpful by adding a .__str__() method to the Person class in core/models.py:



class Person(models.Model):

    last_name = models.TextField()

    first_name = models.TextField()

    courses = models.ManyToManyField("Course", blank=True)


    def __str__(self):

        return f"{self.last_name}, {self.first_name}"



Most of the customization you can do with the Django admin is done by modifying ModelAdmin, and you sure can modify it!


ModelAdmin has over thirty attributes and almost fifty methods. You can use each one of these to fine-tune the admin’s presentation and control your objects’ interfaces. Every one of these options is described in detail in the documentation.

To top it all off, the admin is built using Django’s templating interface. The Django template mechanism allows you to override existing templates, and because the admin is just another set of templates, this means you can completely change its HTML.



The Django admin is split into three major areas:

  1. App index
  2. Change lists
  3. Change forms


The app index lists your registered models. A change list is automatically created for each registered model and lists the objects for that model. When you add or edit one of those objects, you do so with a change form.



Modifying a Change List Using list_display


You can customize change list pages in far more ways than just modifying an object’s string representation. The list_display attribute of an admin.ModelAdmin object specifies what columns are shown in the change list. This value is a tuple of attributes of the object being modeled. For example, in core/admin.py, modify PersonAdmin as follows:


@admin.register(Person)

class PersonAdmin(admin.ModelAdmin):

    list_display = ("last_name", "first_name")


The two columns are clickable, allowing you to sort the page by the column data. The admin also respects the ordering attribute of a Meta section:


class Person(models.Model):

    # ...


    class Meta:

        ordering = ("last_name", "first_name")


    # ...


Adding the ordering attribute will default all queries on Person to be ordered by last_name then first_name. Django will respect this default order both in the admin and when fetching objects.


The list_display tuple can reference any attribute of the object being listed. It can also reference a method in the admin.ModelAdmin itself. Modify PersonAdmin again:


@admin.register(Person)

class PersonAdmin(admin.ModelAdmin):

    list_display = ("last_name", "first_name", "show_average")


    def show_average(self, obj):

        from django.db.models import Avg

        result = Grade.objects.filter(person=obj).aggregate(Avg("grade"))

        return result["grade__avg"]



By default, only those columns that are object attributes are sortable. show_average() is not. This is because sorting is performed by an underlying QuerySet, not on the displayed results.


The title for the column is based on the name of the method. You can alter the title by adding an attribute to the method:


def show_average(self, obj):

    result = Grade.objects.filter(person=obj).aggregate(Avg("grade"))

    return result["grade__avg"]


show_average.short_description = "Average Grade"



By default, Django protects you from HTML in strings in case the string is from user input. To have the display include HTML, you must use format_html():


def show_average(self, obj):

    from django.utils.html import format_html


    result = Grade.objects.filter(person=obj).aggregate(Avg("grade"))

    return format_html("<b><i>{}</i></b>", result["grade__avg"])


show_average.short_description = "Average"



Providing Links to Other Object Pages


from django.urls import reverse

from django.utils.http import urlencode


@admin.register(Course)

class CourseAdmin(admin.ModelAdmin):

    list_display = ("name", "year", "view_students_link")


    def view_students_link(self, obj):

        count = obj.person_set.count()

        url = (

            reverse("admin:core_person_changelist")

            + "?"

            + urlencode({"courses__id": f"{obj.id}"})

        )

        return format_html('<a href="{}">{} Students</a>', url, count)


    view_students_link.short_description = "Students"



Adding Filters to the List Screen


@admin.register(Course)

class CourseAdmin(admin.ModelAdmin):

    list_display = ("name", "year", "view_students_link")

    list_filter = ("year", )

# ...



Adding Search to the List Screen


Filters aren’t the only way to reduce the amount of data on the screen. Django admin also supports searching through the search_fields option, which adds a search box to the screen. You set it with a tuple containing the names of fields to be used for constructing a search query in the database.


Anything the user types in the search box is used in an OR clause of the fields filtering the QuerySet. By default, each search parameter is surrounded by % signs, meaning if you search for r, then any word with an r inside will appear in the results. You can be more precise by specifying a __ modifier on the search field.


@admin.register(Person)

class PersonAdmin(admin.ModelAdmin):

    search_fields = ("last_name__startswith", )



Changing How Models Are Edited


You can control which fields are included, as well as their order, by editing the fields option. Modify your PersonAdmin object, adding a fields attribute:

@admin.register(Person)

class PersonAdmin(admin.ModelAdmin):

    fields = ("first_name", "last_name", "courses")

# ...




ModelAdmin.get_form() is responsible for creating the ModelForm for your object. You can override this method to change the form. Add the following method to PersonAdmin:


def get_form(self, request, obj=None, **kwargs):

    form = super().get_form(request, obj, **kwargs)

    form.base_fields["first_name"].label = "First Name (Humans only!):"

    return form



If you don’t like the ModelForm that the Django admin created for you, then you can use the form attribute to register a custom form. Make the following additions and changes to core/admin.py:



from django import forms


class PersonAdminForm(forms.ModelForm):

    class Meta:

        model = Person

        fields = "__all__"


    def clean_first_name(self):

        if self.cleaned_data["first_name"] == "Spike":

            raise forms.ValidationError("No Vampires")


        return self.cleaned_data["first_name"]


@admin.register(Person)

class PersonAdmin(admin.ModelAdmin):

    form = PersonAdminForm





References:

https://realpython.com/customize-django-admin-python/


No comments:

Post a Comment