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¶
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 forNoneor 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:
With custom label:
With empty value:
With CSS classes:
With filtering (requires djadmin-filters):
With ordering (requires djadmin-filters):
Complete configuration:
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¶
The field name (string) or callable that provides the column value.
Type: str or Callable
String field examples:
Callable field examples:
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¶
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¶
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¶
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¶
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¶
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()¶
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¶
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¶
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
Columninstances at class definition time - Templates always receive
Columnobjects 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¶
- ModelAdmin API Reference - list_display configuration
- ListView Guide - Customizing list views
- Template Reference - Template customization
- djadmin-filters Plugin - Filtering and ordering configuration
- Filtering Guide - Complete filtering documentation
- Ordering Guide - Complete ordering documentation