============================================
Customising ``IndexView`` - the listing view
============================================

For the sake of consistency, this section of the docs will refer to the listing
view as ``IndexView``, because that is the view class that does all the heavy
lifting.

You can use the following attributes and methods on the ``ModelAdmin`` class to
alter how your model data is treated and represented by the ``IndexView``.

.. contents::
    :local:
    :depth: 1

.. _modeladmin_list_display:

---------------------------
``ModelAdmin.list_display``
---------------------------

**Expected value**: A list or tuple, where each item is the name of a field or
single-argument callable on your model, or a similarly simple method defined
on the ``ModelAdmin`` class itself.

Default value: ``('__str__',)``

Set ``list_display`` to control which fields are displayed in the ``IndexView``
for your model.

You have three possible values that can be used in ``list_display``:

-   A field of the model. For example:

    .. code-block:: python

        from wagtail.contrib.modeladmin.options import ModelAdmin
        from .models import Person

        class PersonAdmin(ModelAdmin):
            model = Person
            list_display = ('first_name', 'last_name')


-   The name of a custom method on your ``ModelAdmin`` class, that accepts a
    single parameter for the model instance. For example:

    .. code-block:: python

        from wagtail.contrib.modeladmin.options import ModelAdmin
        from .models import Person


        class PersonAdmin(ModelAdmin):
            model = Person
            list_display = ('upper_case_name',)

            def upper_case_name(self, obj):
                return ("%s %s" % (obj.first_name, obj.last_name)).upper()
            upper_case_name.short_description = 'Name'


-   The name of a method on your ``Model`` class that accepts only ``self`` as
    an argument. For example:

    .. code-block:: python

        from django.db import models
        from wagtail.contrib.modeladmin.options import ModelAdmin

        class Person(models.Model):
            name = models.CharField(max_length=50)
            birthday = models.DateField()

            def decade_born_in(self):
                return self.birthday.strftime('%Y')[:3] + "0's"
            decade_born_in.short_description = 'Birth decade'


        class PersonAdmin(ModelAdmin):
            model = Person
            list_display = ('name', 'decade_born_in')


A few special cases to note about ``list_display``:

-   If the field is a ``ForeignKey``, Django will display the output of
    ``__str__()`` of the related object.

-   If the string provided is a method of the model or ``ModelAdmin`` class,
    Django will HTML-escape the output by default. To escape user input and
    allow your own unescaped tags, use ``format_html()``. For example:

    .. code-block:: python

        from django.db import models
        from django.utils.html import format_html
        from wagtail.contrib.modeladmin.options import ModelAdmin

        class Person(models.Model):
            first_name = models.CharField(max_length=50)
            last_name = models.CharField(max_length=50)
            color_code = models.CharField(max_length=6)

            def colored_name(self):
                return format_html(
                    '<span style="color: #{};">{} {}</span>',
                    self.color_code,
                    self.first_name,
                    self.last_name,
                )


        class PersonAdmin(ModelAdmin):
            model = Person
            list_display = ('first_name', 'last_name', 'colored_name')


-   If the value of a field is ``None``, an empty string, or an iterable
    without elements, Wagtail will display a dash (-) for that column. You can
    override this by setting ``empty_value_display`` on your ``ModelAdmin``
    class. For example:

    .. code-block:: python

        from wagtail.contrib.modeladmin.options import ModelAdmin

        class PersonAdmin(ModelAdmin):
            empty_value_display = 'N/A'
            ...


    Or, if you'd like to change the value used depending on the field, you can
    override ``ModelAdmin``'s ``get_empty_value_display()`` method, like so:

    .. code-block:: python

        from django.db import models
        from wagtail.contrib.modeladmin.options import ModelAdmin


        class Person(models.Model):
            name = models.CharField(max_length=100)
            nickname = models.CharField(blank=True, max_length=100)
            likes_cat_gifs = models.NullBooleanField()


        class PersonAdmin(ModelAdmin):
            model = Person
            list_display = ('name', 'nickname', 'likes_cat_gifs')

            def get_empty_value_display(self, field_name=None):
                if field_name == 'nickname':
                    return 'None given'
                if field_name == 'likes_cat_gifs':
                    return 'Unanswered'
                return super().get_empty_value_display(field_name)


    The ``__str__()`` method is just as valid
    in ``list_display`` as any other model method, so it’s perfectly OK to do
    this:

    .. code-block:: python

        list_display = ('__str__', 'some_other_field')


    By default, the ability to sort results by an item in ``list_display`` is
    only offered when it's a field that has an actual database value (because
    sorting is done at the database level). However, if the output of the
    method is representative of a database field, you can indicate this fact by
    setting the ``admin_order_field`` attribute on that method, like so:

    .. code-block:: python

        from django.db import models
        from django.utils.html import format_html
        from wagtail.contrib.modeladmin.options import ModelAdmin

        class Person(models.Model):
            first_name = models.CharField(max_length=50)
            last_name = models.CharField(max_length=50)
            color_code = models.CharField(max_length=6)

            def colored_first_name(self):
                return format_html(
                    '<span style="color: #{};">{}</span>',
                    self.color_code,
                    self.first_name,
                )
            colored_first_name.admin_order_field = 'first_name'


        class PersonAdmin(ModelAdmin):
            model = Person
            list_display = ('colored_first_name', 'last_name')


    The above will tell Wagtail to order by the ``first_name`` field when
    trying to sort by ``colored_first_name`` in the index view.

    To indicate descending order with ``admin_order_field`` you can use a
    hyphen prefix on the field name. Using the above example, this would look
    like:

    .. code-block:: python

        colored_first_name.admin_order_field = '-first_name'


    ``admin_order_field`` supports query lookups to sort by values on related
    models, too. This example includes an “author first name” column in the
    list display and allows sorting it by first name:

    .. code-block:: python

        from django.db import models


        class Blog(models.Model):
            title = models.CharField(max_length=255)
            author = models.ForeignKey(Person, on_delete=models.CASCADE)

            def author_first_name(self, obj):
                return obj.author.first_name

            author_first_name.admin_order_field = 'author__first_name'


-   Elements of ``list_display`` can also be properties. Please note however,
    that due to the way properties work in Python, setting
    ``short_description`` on a property is only possible when using the
    ``property()`` function and **not** with the ``@property`` decorator.

    For example:

    .. code-block:: python

        from django.db import models
        from wagtail.contrib.modeladmin.options import ModelAdmin

        class Person(models.Model):
            first_name = models.CharField(max_length=50)
            last_name = models.CharField(max_length=50)

            def full_name_property(self):
                return self.first_name + ' ' + self.last_name
            full_name_property.short_description = "Full name of the person"

            full_name = property(full_name_property)


        class PersonAdmin(ModelAdmin):
            list_display = ('full_name',)


.. _modeladmin_list_filter:

---------------------------
``ModelAdmin.list_filter``
---------------------------

**Expected value**: A list or tuple, where each item is the name of model field
of type ``BooleanField``, ``CharField``, ``DateField``, ``DateTimeField``,
``IntegerField`` or ``ForeignKey``.

Set ``list_filter`` to activate filters in the right sidebar of the list page
for your model. For example:

.. code-block:: python

    class PersonAdmin(ModelAdmin):
        list_filter = ('is_staff', 'company')


.. _modeladmin_search_fields:

----------------------------
``ModelAdmin.search_fields``
----------------------------

**Expected value**: A list or tuple, where each item is the name of a model
field of type ``CharField``, ``TextField``, ``RichTextField`` or
``StreamField``.

Set ``search_fields`` to enable a search box at the top of the index page
for your model. You should add names of any fields on the model that should
be searched whenever somebody submits a search query using the search box.

Searching is handled via Django's QuerySet API by default,
see `ModelAdmin.search_handler_class`_ about changing this behaviour.
This means by default it will work for all models, whatever search backend
your project is using, and without any additional setup or configuration.


.. _modeladmin_search_handler_class:

-----------------------------------
``ModelAdmin.search_handler_class``
-----------------------------------

**Expected value**: A subclass of
``wagtail.contrib.modeladmin.helpers.search.BaseSearchHandler``

The default value is ``DjangoORMSearchHandler``, which uses the Django ORM to
perform lookups on the fields specified by ``search_fields``.

If you would prefer to use the built-in Wagtail search backend to search your
models, you can use the ``WagtailBackendSearchHandler`` class instead. For
example:

.. code-block:: python

    from wagtail.contrib.modeladmin.helpers import WagtailBackendSearchHandler

    from .models import Person

    class PersonAdmin(ModelAdmin):
        model = Person
        search_handler_class = WagtailBackendSearchHandler


Extra considerations when using ``WagtailBackendSearchHandler``
===============================================================


``ModelAdmin.search_fields`` is used differently
------------------------------------------------

The value of ``search_fields`` is passed to the underlying search backend to
limit the fields used when matching. Each item in the list must be indexed
on your model using :ref:`wagtailsearch_index_searchfield`.

To allow matching on **any** indexed field, set the ``search_fields`` attribute
on your ``ModelAdmin`` class to ``None``, or remove it completely.


Indexing extra fields using ``index.FilterField``
-------------------------------------------------

The underlying search backend must be able to interpret all of the fields and
relationships used in the queryset created by ``IndexView``, including those
used in ``prefetch()`` or ``select_related()`` queryset methods, or used in
``list_display``, ``list_filter`` or ``ordering``.

Be sure to test things thoroughly in a development environment (ideally
using the same search backend as you use in production). Wagtail will raise
an ``IndexError`` if the backend encounters something it does not understand,
and will tell you what you need to change.


.. _modeladmin_extra_search_kwargs:

----------------------------------
``ModelAdmin.extra_search_kwargs``
----------------------------------

**Expected value**: A dictionary of keyword arguments that will be passed on to the ``search()`` method of
``search_handler_class``.

For example, to override the ``WagtailBackendSearchHandler`` default operator you could do the following:

.. code-block:: python

    from wagtail.contrib.modeladmin.helpers import WagtailBackendSearchHandler
    from wagtail.search.utils import OR

    from .models import IndexedModel

    class DemoAdmin(ModelAdmin):
        model = IndexedModel
        search_handler_class = WagtailBackendSearchHandler
        extra_search_kwargs = {'operator': OR}


.. _modeladmin_ordering:

---------------------------
``ModelAdmin.ordering``
---------------------------

**Expected value**: A list or tuple in the same format as a model’s
:attr:`~django.db.models.Options.ordering` parameter.

Set ``ordering`` to specify the default ordering of objects when listed by
IndexView.  If not provided, the model’s default ordering will be respected.

If you need to specify a dynamic order (for example, depending on user or
language) you can override the ``get_ordering()`` method instead.


.. _modeladmin_list_per_page:

----------------------------
``ModelAdmin.list_per_page``
----------------------------

**Expected value**: A positive integer

Set ``list_per_page`` to control how many items appear on each paginated page
of the index view. By default, this is set to ``100``.


.. _modeladmin_get_queryset:

-----------------------------
``ModelAdmin.get_queryset()``
-----------------------------

**Must return**: A QuerySet

The ``get_queryset`` method returns the 'base' QuerySet for your model, to
which any filters and search queries are applied. By default, the ``all()``
method of your model's default manager is used. But, if for any reason you
only want a certain sub-set of objects to appear in the IndexView listing,
overriding the ``get_queryset`` method on your ``ModelAdmin`` class can help
you with that. The method takes an ``HttpRequest`` object as a parameter, so
limiting objects by the current logged-in user is possible.

For example:

.. code-block:: python

    from django.db import models
    from wagtail.contrib.modeladmin.options import ModelAdmin

    class Person(models.Model):
        first_name = models.CharField(max_length=50)
        last_name = models.CharField(max_length=50)
        managed_by = models.ForeignKey(`auth.User`, on_delete=models.CASCADE)


    class PersonAdmin(ModelAdmin):
        model = Person
        list_display = ('first_name', 'last_name')

        def get_queryset(self, request):
            qs = super().get_queryset(request)
            # Only show people managed by the current user
            return qs.filter(managed_by=request.user)


.. _modeladmin_get_extra_attrs_for_row:

----------------------------------------------------
``ModelAdmin.get_extra_attrs_for_row()``
----------------------------------------------------

**Must return**: A dictionary

The ``get_extra_attrs_for_row`` method allows you to add html attributes to
the opening ``<tr>`` tag for each result, in addition to the ``data-object_pk`` and
``class`` attributes already added by the ``result_row_display`` template tag.

If you want to add additional CSS classes, simply provide those class names
as a string value using the ``'class'`` key, and the ``odd``/``even`` will be appended
to your custom class names when rendering.

For example, if you wanted to add some additional class names based on field
values, you could do something like:

.. code-block:: python

    from decimal import Decimal
    from django.db import models
    from wagtail.contrib.modeladmin.options import ModelAdmin

    class BankAccount(models.Model):
        name = models.CharField(max_length=50)
        account_number = models.CharField(max_length=50)
        balance = models.DecimalField(max_digits=5, num_places=2)


    class BankAccountAdmin(ModelAdmin):
        list_display = ('name', 'account_number', 'balance')

        def get_extra_attrs_for_row(self, obj, context):
            if obj.balance < Decimal('0.00'):
                classname = 'balance-negative'
            else:
                classname = 'balance-positive'
            return {
                'class': classname,
            }


.. _modeladmin_get_extra_class_names_for_field_col:

----------------------------------------------------
``ModelAdmin.get_extra_class_names_for_field_col()``
----------------------------------------------------

**Must return**: A list

The ``get_extra_class_names_for_field_col`` method allows you to add additional
CSS class names to any of the columns defined by ``list_display`` for your
model. The method takes two parameters:

-   ``obj``: the object being represented by the current row
-   ``field_name``: the item from ``list_display`` being represented by the
    current column

For example, if you'd like to apply some conditional formatting to a cell
depending on the row's value, you could do something like:

.. code-block:: python

    from decimal import Decimal
    from django.db import models
    from wagtail.contrib.modeladmin.options import ModelAdmin

    class BankAccount(models.Model):
        name = models.CharField(max_length=50)
        account_number = models.CharField(max_length=50)
        balance = models.DecimalField(max_digits=5, num_places=2)


    class BankAccountAdmin(ModelAdmin):
        list_display = ('name', 'account_number', 'balance')

        def get_extra_class_names_for_field_col(self, obj, field_name):
            field_name == 'balance':
                if balance <= Decimal('-100.00'):
                    return ['brand-danger']
                if balance <= Decimal('-0.00'):
                    return ['brand-warning']
                if balance <= Decimal('-50.00'):
                    return ['brand-info']
                else:
                    return ['brand-success']
            return []


.. _modeladmin_get_extra_attrs_for_field_col:

----------------------------------------------------
``ModelAdmin.get_extra_attrs_for_field_col()``
----------------------------------------------------

**Must return**: A dictionary

The ``get_extra_attrs_for_field_col`` method allows you to add additional HTML
attributes to any of the columns defined in ``list_display``. Like the
``get_extra_class_names_for_field_col`` method above, this method takes two
parameters:

-   ``obj``: the object being represented by the current row
-   ``field_name``: the item from ``list_display`` being represented by the
    current column

For example, you might like to add some tooltip text to a certain column, to
help give the value more context:

.. code-block:: python

    from django.db import models
    from wagtail.contrib.modeladmin.options import ModelAdmin


    class Person(models.Model):
        name = models.CharField(max_length=100)
        likes_cat_gifs = models.NullBooleanField()


    class PersonAdmin(ModelAdmin):
        model = Person
        list_display = ('name', 'likes_cat_gifs')

        def get_extra_attrs_for_field_col(self, obj, field_name=None):
            attrs = super().get_extra_attrs_for_field_col(obj, field_name)
            if field_name == 'likes_cat_gifs' and obj.likes_cat_gifs is None:
                attrs.update({
                    'title': (
                        'The person was shown several cat gifs, but failed to '
                        'indicate a preference.'
                    ),
                })
            return attrs


Or you might like to add one or more data attributes to help implement some
kind of interactivity using JavaScript:

.. code-block:: python

    from django.db import models
    from wagtail.contrib.modeladmin.options import ModelAdmin


    class Event(models.Model):
        title = models.CharField(max_length=255)
        start_date = models.DateField()
        end_date = models.DateField()
        start_time = models.TimeField()
        end_time = models.TimeField()


    class EventAdmin(ModelAdmin):
        model = Event
        list_display = ('title', 'start_date', 'end_date')

        def get_extra_attrs_for_field_col(self, obj, field_name=None):
            attrs = super().get_extra_attrs_for_field_col(obj, field_name)
            if field_name == 'start_date':
                # Add the start time as data to the 'start_date' cell
                attrs.update({ 'data-time': obj.start_time.strftime('%H:%M') })
            elif field_name == 'end_date':
                # Add the end time as data to the 'end_date' cell
                attrs.update({ 'data-time': obj.end_time.strftime('%H:%M') })
            return attrs


.. _modeladmin_thumbnailmixin:

----------------------------------------------------
``wagtail.contrib.modeladmin.mixins.ThumbnailMixin``
----------------------------------------------------

If you're using ``wagtailimages.Image`` to define an image for each item in
your model, ``ThumbnailMixin`` can help you add thumbnail versions of that
image to each row in ``IndexView``. To use it, simply extend ``ThumbnailMixin``
as well as ``ModelAdmin`` when defining your ``ModelAdmin`` class, and
change a few attributes to change the thumbnail to your liking, like so:

.. code-block:: python

    from django.db import models
    from wagtail.contrib.modeladmin.mixins import ThumbnailMixin
    from wagtail.contrib.modeladmin.options import ModelAdmin

    class Person(models.Model):
        name = models.CharField(max_length=255)
        avatar = models.ForeignKey('wagtailimages.Image', on_delete=models.SET_NULL, null=True)
        likes_cat_gifs = models.NullBooleanField()

    class PersonAdmin(ThumbnailMixin, ModelAdmin):

        # Add 'admin_thumb' to list_display, where you want the thumbnail to appear
        list_display = ('admin_thumb', 'name', 'likes_cat_gifs')

        # Optionally tell IndexView to add buttons to a different column (if the
        # first column contains the thumbnail, the buttons are likely better off
        # displayed elsewhere)
        list_display_add_buttons = 'name'

        """
        Set 'thumb_image_field_name' to the name of the ForeignKey field that
        links to 'wagtailimages.Image'
        """
        thumb_image_field_name = 'avatar'

        # Optionally override the filter spec used to create each thumb
        thumb_image_filter_spec = 'fill-100x100' # this is the default

        # Optionally override the 'width' attribute value added to each img tag
        thumb_image_width = 50 # this is the default

        # Optionally override the class name added to each img tag
        thumb_classname = 'admin-thumb' # this is the default

        # Optionally override the text that appears in the column header
        thumb_col_header_text = 'image' # this is the default

        # Optionally specify a fallback image to be used when the object doesn't
        # have an image set, or the image has been deleted. It can an image from
        # your static files folder, or an external URL.
        thumb_default = 'http://lorempixel.com/100/100'


.. _modeladmin_list_display_add_buttons:

---------------------------------------
``ModelAdmin.list_display_add_buttons``
---------------------------------------

**Expected value**: A string matching one of the items in ``list_display``.

If for any reason you'd like to change which column the action buttons appear
in for each row, you can specify a different column using
``list_display_add_buttons`` on your ``ModelAdmin`` class. The value must
match one of the items your class's ``list_display`` attribute. By default,
buttons are added to the first column of each row.

See the ``ThumbnailMixin`` example above to see how
``list_display_add_buttons`` can be used.

.. _modeladmin_index_view_extra_css:

-----------------------------------
``ModelAdmin.index_view_extra_css``
-----------------------------------

**Expected value**: A list of path names of additional stylesheets to be added
to the ``IndexView``

See the following part of the docs to find out more:
:ref:`modeladmin_adding_css_and_js`

.. _modeladmin_index_view_extra_js:

-----------------------------------
``ModelAdmin.index_view_extra_js``
-----------------------------------

**Expected value**: A list of path names of additional js files to be added
to the ``IndexView``

See the following part of the docs to find out more:
:ref:`modeladmin_adding_css_and_js`

.. _modeladmin_index_template_name:

---------------------------------------
``ModelAdmin.index_template_name``
---------------------------------------

**Expected value**: The path to a custom template to use for ``IndexView``

See the following part of the docs to find out more:
:ref:`modeladmin_overriding_templates`

.. _modeladmin_index_view_class:

---------------------------------------
``ModelAdmin.index_view_class``
---------------------------------------

**Expected value**: A custom ``view`` class to replace
``modeladmin.views.IndexView``

See the following part of the docs to find out more:
:ref:`modeladmin_overriding_views`
