Skip to content

Column API Reference

The Column dataclass provides enhanced configuration for list_display columns in django-admin-deux. It extends Django admin's simple string-based column specification with additional metadata like labels, CSS classes, and empty value handling.

Dataclass Definition

from djadmin import Column

@dataclass
class Column:
    """
    Specification for a list_display column.

    Supports both Django admin style (strings/callables) and
    enhanced configuration with additional metadata.
    """

Constructor

Column(field, label=None, empty_value='-', classes='', filter=None, order=None)

Parameters

  • field (str | Callable): Field name or callable that returns the column value
  • label (str, optional): Column header label (overrides auto-detected label). Default: None
  • empty_value (str, optional): Display value for None or empty values. Default: '-'
  • classes (str, optional): CSS classes for this column. Default: ''
  • filter (bool | Filter, optional): Enable filtering for this column (requires djadmin-filters plugin). Default: None
  • order (bool | Order, optional): Enable ordering for this column (requires djadmin-filters plugin). Default: None

Examples

Basic column:

Column('name')

With custom label:

Column('sku', label='SKU Code')

With empty value:

Column('price', empty_value='N/A')

With CSS classes:

Column('status', classes='badge badge-primary')

With filtering (requires djadmin-filters):

Column('name', filter=True)  # Simple exact match filter

With ordering (requires djadmin-filters):

Column('price', order=True)  # Sortable column

Complete configuration:

Column('sku', label='SKU Code', empty_value='—', classes='font-mono text-sm')

Complete with filtering and ordering:

from djadmin.dataclasses import Filter, Order

Column('name',
       label='Product Name',
       filter=Filter(lookup_expr='icontains'),
       order=True,
       classes='font-semibold')

Attributes

field

field: str | Callable

The field name (string) or callable that provides the column value.

Type: str or Callable

String field examples:

Column('name')           # Model field
Column('category__name') # Related field lookup

Callable field examples:

def formatted_price(obj):
    return f"${obj.price:.2f}"

Column(formatted_price)

In ModelAdmin context:

class ProductAdmin(ModelAdmin):
    def price_with_tax(self, obj):
        return f"${obj.price * 1.2:.2f}"

    list_display = [
        Column('name'),
        Column('price'),
        Column(price_with_tax, label='Price (incl. Tax)'),
    ]

label

label: str | None = None

Column header label displayed in the list view.

Type: str or None Default: None (auto-detected from field)

Auto-detection: - For string fields: Uses model field's verbose_name - For callables: Uses function's __name__ or callable's short_description attribute (Django admin compat) - For None: Falls back to field_name

Examples:

# Auto-detected labels
Column('name')          # Label: "Name" (from model field)
Column('created_at')    # Label: "Created at" (from model field)

# Custom labels
Column('sku', label='SKU Code')
Column('created_at', label='Date Created')

# Callable with Django admin style attribute
def status_badge(obj):
    return f'<span class="badge">{obj.status}</span>'
status_badge.short_description = 'Status'  # Django admin compat

Column(status_badge)  # Label: "Status"

# Callable with Column override
Column(status_badge, label='Current Status')  # Label: "Current Status"

empty_value

empty_value: str = '-'

Display value when the field is None or empty.

Type: str Default: '-'

Examples:

# Default dash
Column('description')  # Shows "-" if description is None/empty

# Custom empty value
Column('description', empty_value='No description')
Column('price', empty_value='N/A')
Column('category', empty_value='—')
Column('notes', empty_value='')  # Empty string (nothing shown)

Template handling:

The default theme template handles empty values:

{% for column in list_display %}
  <td>
    {% if column.get_value(object) %}
      {{ column.get_value(object) }}
    {% else %}
      {{ column.empty_value }}
    {% endif %}
  </td>
{% endfor %}

classes

classes: str = ''

CSS classes applied to table cells in this column.

Type: str Default: '' (no classes)

Examples:

# Single class
Column('status', classes='badge')

# Multiple classes
Column('sku', classes='font-mono text-sm')
Column('price', classes='text-right font-bold')
Column('is_active', classes='text-center')

# Utility classes (Tailwind example)
Column('created_at', classes='text-gray-500 text-xs')

Template rendering:

{% for column in list_display %}
  <td class="{{ column.classes }}">
    {{ column.get_value(object)|default:column.empty_value }}
  </td>
{% endfor %}

filter

filter: bool | Filter | None = None

Enable filtering for this column (requires djadmin-filters plugin).

Type: bool, Filter, or None Default: None (no filtering) Requires: djadmin-filters plugin installed

Behavior: - None - No filter (default) - True - Simple exact match filter - False - Explicitly disable filtering - Filter(...) - Advanced filter configuration

Examples:

from djadmin.dataclasses import Filter
from django import forms

# Simple exact match filter
Column('category', filter=True)

# Contains filter (case-insensitive)
Column('name', filter=Filter(lookup_expr='icontains'))

# Range filter
Column('price', filter=Filter(lookup_expr=['gte', 'lte']))

# Custom widget
Column('status',
       filter=Filter(
           lookup_expr='exact',
           widget=forms.Select(choices=[
               ('active', 'Active'),
               ('inactive', 'Inactive'),
           ])
       ))

# Method filter
def filter_in_stock(queryset, name, value):
    if value:
        return queryset.filter(stock__gt=0)
    return queryset

Column('in_stock',
       filter=Filter(method=filter_in_stock))

# Explicitly disabled
Column('description', filter=False)

# Exclude filter (NOT condition)
Column('status',
       filter=Filter(
           lookup_expr='exact',
           exclude=True,  # Excludes items with this status
           widget=forms.Select(choices=[...])
       ))

# Distinct results (useful for M2M relationships)
Column('tags__name',
       filter=Filter(
           lookup_expr='icontains',
           distinct=True  # Prevents duplicate rows
       ))

# Custom field class
Column('price',
       filter=Filter(
           lookup_expr='gte',
           field_class=forms.DecimalField
       ))

# Extra kwargs for advanced django-filter options
Column('date',
       filter=Filter(
           lookup_expr='gte',
           extra={'help_text': 'Filter by start date'}
       ))

Filter dataclass:

from dataclasses import dataclass

@dataclass
class Filter:
    lookup_expr: str | list[str] = 'exact'
    widget: type | None = None
    label: str | None = None
    method: str | Callable | None = None
    exclude: bool = False
    distinct: bool = False
    field_class: type | None = None
    extra: dict = field(default_factory=dict)

Filter attributes: - lookup_expr: Lookup expression(s) to use ('exact', 'icontains', ['gte', 'lte'], etc.) - widget: Django form widget type for filter input - label: Custom label for filter field - method: Method name or callable for custom filtering logic - exclude: If True, creates an exclude filter instead of a regular filter - distinct: If True, adds .distinct() to the queryset - field_class: Custom field class to use for the filter - extra: Additional kwargs to pass to django-filter

Normalization by ModelAdmin metaclass: - Column.filter = None → stays None (no filter) - Column.filter = False → normalized to None (no filter) - Column.filter = True → normalized to Filter() with default settings - Column.filter = Filter(...) → used as-is

See Also: Filtering Guide for complete documentation

order

order: bool | Order | None = None

Enable ordering/sorting for this column (requires djadmin-filters plugin).

Type: bool, Order, or None Default: None (not sortable - normalized to Order(enabled=False)) Requires: djadmin-filters plugin installed

Behavior: - None - Not sortable (default, normalized to Order(enabled=False)) - False - Explicitly not sortable (normalized to Order(enabled=False)) - True - Simple sortable column (normalized to Order() with enabled=True) - Order(...) - Advanced ordering configuration

Important: Columns are not orderable by default. You must explicitly set order=True or order=Order() to enable sorting.

Examples:

from djadmin.dataclasses import Order

# Simple sortable column
Column('name', order=True)

# Custom sort labels
Column('price',
       order=Order(
           label='Price (low to high)',
           descending_label='Price (high to low)'
       ))

# Multi-field ordering
Column('full_name',
       order=Order(fields=['last_name', 'first_name']))

# Related field ordering
Column('category',
       order=Order(fields=['category__name']))

# Explicitly disabled (using boolean)
Column('description', order=False)

# Explicitly disabled (using Order with enabled=False)
Column('description', order=Order(enabled=False))

# Enabled with custom fields (enabled=True is default)
Column('author',
       order=Order(
           enabled=True,  # Explicitly enable (optional, this is default)
           fields=['author__last_name', 'author__first_name']
       ))

Order dataclass:

from dataclasses import dataclass

@dataclass
class Order:
    enabled: bool = True
    fields: list[str] | None = None
    label: str | None = None
    descending_label: str | None = None

Order attributes: - enabled: Whether ordering is enabled for this column (Order is truthy when enabled, falsy when disabled). Default: True when creating Order() directly, but Column.order=None gets normalized to Order(enabled=False) by the ModelAdmin metaclass. - fields: Optional list of field names to use for ordering (if None, uses the column's field) - label: Optional custom label for ascending order - descending_label: Optional custom label for descending order

Normalization by ModelAdmin metaclass: - Column.order = None → normalized to Order(enabled=False) (not sortable) - Column.order = False → normalized to Order(enabled=False) (not sortable) - Column.order = True → normalized to Order() with enabled=True (sortable) - Column.order = Order(...) → used as-is

See Also: Ordering Guide for complete documentation

Class Methods

from_field()

@classmethod
def from_field(cls, field) -> Column

Create Column from string, callable, or existing Column instance.

Parameters: - field (str | Callable | Column): Field specification

Returns: Column instance

Purpose: Provides Django admin compatibility by normalizing different input formats.

Examples:

# String field
column = Column.from_field('name')
# Returns: Column(field='name')

# Callable
def get_status(obj):
    return obj.status

column = Column.from_field(get_status)
# Returns: Column(field=get_status)

# Existing Column (returns as-is)
original = Column('name', label='Product Name')
column = Column.from_field(original)
# Returns: original (same instance)

Used by ModelAdminMetaclass:

The metaclass automatically normalizes list_display:

class ProductAdmin(ModelAdmin):
    # Mixed styles (all converted to Column instances)
    list_display = [
        'name',                                    # → Column('name')
        Column('sku', label='SKU Code'),          # → Unchanged
        get_price,                                 # → Column(get_price)
    ]

Properties

field_name

@property
def field_name(self) -> str

Get field name as a string (for template use).

Returns: str - Field name

Behavior: - For string fields: Returns the field name - For callables: Returns __name__ attribute or string representation

Examples:

# String field
column = Column('name')
print(column.field_name)  # 'name'

# Callable
def formatted_price(obj):
    return f"${obj.price}"

column = Column(formatted_price)
print(column.field_name)  # 'formatted_price'

Template usage:

{% for column in list_display %}
  <th data-field="{{ column.field_name }}">
    {{ column.field_label }}
  </th>
{% endfor %}

field_label

@property
def field_label(self) -> str

Get label, falling back to field_name if not set.

Returns: str - Display label

Behavior: - If label is set: Returns label - If label is None: Returns field_name

Examples:

# With explicit label
column = Column('sku', label='SKU Code')
print(column.field_label)  # 'SKU Code'

# Without label (fallback to field_name)
column = Column('name')
print(column.field_label)  # 'name'

Template usage:

<thead>
  <tr>
    {% for column in list_display %}
      <th>{{ column.field_label }}</th>
    {% endfor %}
  </tr>
</thead>

Usage in ModelAdmin

Basic Usage

from djadmin import ModelAdmin, Column

class ProductAdmin(ModelAdmin):
    list_display = [
        Column('name'),
        Column('sku', label='SKU Code'),
        Column('price', empty_value='N/A'),
    ]

Django Admin Compatibility

# Django admin style (all string fields)
class ProductAdmin(ModelAdmin):
    list_display = ['name', 'sku', 'price']  # Auto-converted to Columns

# Enhanced style (explicit Columns)
class ProductAdmin(ModelAdmin):
    list_display = [
        Column('name'),
        Column('sku', label='SKU Code', classes='font-mono'),
        Column('price', empty_value='N/A', classes='text-right'),
    ]

# Mixed style (automatically normalized)
class ProductAdmin(ModelAdmin):
    list_display = [
        'name',                                  # Auto-converted
        Column('sku', label='SKU Code'),        # Explicit
        'price',                                 # Auto-converted
        Column('status', classes='badge'),      # Explicit
    ]

With Callables

from djadmin import ModelAdmin, Column

class ProductAdmin(ModelAdmin):
    def formatted_price(self, obj):
        """Display price with currency"""
        return f"${obj.price:.2f}"

    def stock_status(self, obj):
        """Display stock badge"""
        if obj.stock > 100:
            return '<span class="badge success">In Stock</span>'
        elif obj.stock > 0:
            return '<span class="badge warning">Low Stock</span>'
        return '<span class="badge danger">Out of Stock</span>'

    list_display = [
        Column('name'),
        Column('sku', label='SKU', classes='font-mono'),
        Column(formatted_price, label='Price', classes='text-right'),
        Column(stock_status, label='Availability', classes='text-center'),
    ]

Advanced Configuration

from djadmin import ModelAdmin, Column

class ProductAdmin(ModelAdmin):
    # Helper method with Django admin compat
    def category_name(self, obj):
        return obj.category.name if obj.category else None
    category_name.short_description = 'Category'  # Django admin style

    # Another helper
    def is_available(self, obj):
        return '✓' if obj.is_active and obj.stock > 0 else '✗'

    list_display = [
        Column('id', label='ID', classes='text-gray-500'),
        Column('name', classes='font-semibold'),
        Column('sku', label='SKU', classes='font-mono text-sm'),
        Column(category_name, empty_value='Uncategorized'),
        Column('price', label='Price ($)', empty_value='TBD', classes='text-right'),
        Column('stock', label='Stock', empty_value='0', classes='text-center'),
        Column(is_available, label='Available', classes='text-center'),
        Column('created_at', label='Created', classes='text-sm text-gray-500'),
    ]

With Filtering and Ordering

When the djadmin-filters plugin is installed, columns can include filtering and ordering:

from djadmin import ModelAdmin, Column
from djadmin.dataclasses import Filter, Order

class ProductAdmin(ModelAdmin):
    list_display = [
        # Searchable name with ordering
        Column('name',
               filter=Filter(lookup_expr='icontains'),
               order=True),

        # Dropdown filter, not sortable
        Column('category',
               filter=Filter(
                   lookup_expr='exact',
                   widget=forms.Select(choices=Category.choices)
               ),
               order=False),

        # Price range filter with custom order labels
        Column('price',
               filter=Filter(lookup_expr=['gte', 'lte']),
               order=Order(
                   label='Price (low to high)',
                   descending_label='Price (high to low)'
               )),

        # Simple filter and order
        Column('stock',
               filter=True,
               order=True),

        # Display only, no filter or order
        Column('description',
               filter=False,
               order=False),

        # Related field filtering and ordering
        Column('category__name',
               label='Category',
               filter=Filter(lookup_expr='icontains'),
               order=Order(fields=['category__name'])),
    ]

    def get_queryset(self, request):
        # Optimize queries for related fields
        return super().get_queryset(request).select_related('category')

See Also: - djadmin-filters Configuration - Filtering Guide - Ordering Guide

Metaclass Processing

The ModelAdminMetaclass automatically normalizes list_display:

class ModelAdminMetaclass(type):
    """
    Metaclass that normalizes list_display to Column objects.
    """

    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)

        # Normalize list_display to Column objects
        if hasattr(cls, 'list_display') and cls.list_display:
            cls.list_display = [
                Column.from_field(field) for field in cls.list_display
            ]

        return cls

What this means:

  • You can use Django admin style (strings) or enhanced style (Columns)
  • All fields are normalized to Column instances at class definition time
  • Templates always receive Column objects with consistent interface

Template Usage

Rendering Column Headers

<thead>
  <tr>
    {% for column in list_display %}
      <th class="{{ column.classes }}">
        {{ column.field_label }}
      </th>
    {% endfor %}
  </tr>
</thead>

Rendering Column Values

<tbody>
  {% for object in object_list %}
    <tr>
      {% for column in list_display %}
        <td class="{{ column.classes }}">
          {% if column.field is string %}
            {# String field - use getattr #}
            {{ object|attr:column.field|default:column.empty_value }}
          {% else %}
            {# Callable field - call it #}
            {% with value=column.field(object) %}
              {{ value|default:column.empty_value }}
            {% endwith %}
          {% endif %}
        </td>
      {% endfor %}
    </tr>
  {% endfor %}
</tbody>

Complete Table Example

<table class="table">
  <thead>
    <tr>
      {% for column in list_display %}
        <th class="{{ column.classes }}" data-field="{{ column.field_name }}">
          {{ column.field_label }}
        </th>
      {% endfor %}
    </tr>
  </thead>
  <tbody>
    {% for object in object_list %}
      <tr>
        {% for column in list_display %}
          <td class="{{ column.classes }}">
            {# Simplified - actual implementation would handle callable vs string #}
            {{ column.get_value(object)|default:column.empty_value }}
          </td>
        {% endfor %}
      </tr>
    {% endfor %}
  </tbody>
</table>

Comparison with Django Admin

Django Admin (String-based)

class ProductAdmin(admin.ModelAdmin):
    list_display = ['name', 'sku', 'price']

    # Customization requires method attributes
    def formatted_price(self, obj):
        return f"${obj.price}"
    formatted_price.short_description = 'Price'
    formatted_price.admin_order_field = 'price'

    list_display = ['name', 'sku', formatted_price]

django-admin-deux (Column-based)

class ProductAdmin(ModelAdmin):
    def formatted_price(self, obj):
        return f"${obj.price}"

    list_display = [
        Column('name'),
        Column('sku', label='SKU Code', classes='font-mono'),
        Column(formatted_price, label='Price', classes='text-right', empty_value='N/A'),
    ]

Benefits: - More explicit configuration - CSS classes per column - Custom empty values per column - Both styles supported (Django compat + enhanced)

See Also