Skip to content

Basic Usage

This guide covers the essential ModelAdmin configuration options for everyday use.

Overview

The ModelAdmin class is the heart of django-admin-deux. It encapsulates all configuration and behavior for administering a Django model through the interface.

list_display Configuration

The list_display attribute controls which columns appear in your ListView.

Simple Field Names

The most basic usage is a list of field names:

from djadmin import ModelAdmin, register
from .models import Product

@register(Product)
class ProductAdmin(ModelAdmin):
    list_display = ['name', 'sku', 'price', 'status']

This displays the name, sku, price, and status fields as columns in the list view.

Using the Column Dataclass

For more control, use the Column dataclass:

from djadmin import ModelAdmin, register, Column
from .models import Product

@register(Product)
class ProductAdmin(ModelAdmin):
    list_display = [
        'name',
        Column('sku', label='SKU Code'),
        Column('price', label='Price (USD)', classes='text-right'),
        Column('status', empty_value='N/A'),
    ]

Column Parameters: - field: Field name or callable (required) - label: Custom column header (optional) - empty_value: Display value for None/empty (default: '-') - classes: CSS classes for the column (optional)

Mixed Style

You can mix simple strings and Column objects:

list_display = [
    'name',
    Column('price', label='Price (USD)'),
    'created_at',
]

Custom Columns with Callables

Add computed columns using methods:

@register(Product)
class ProductAdmin(ModelAdmin):
    list_display = ['name', 'price', 'stock_indicator']

    def stock_indicator(self, obj):
        if obj.stock_quantity == 0:
            return "Out of Stock"
        elif obj.stock_quantity < 10:
            return f"Low Stock ({obj.stock_quantity})"
        else:
            return f"In Stock ({obj.stock_quantity})"

    stock_indicator.short_description = "Stock Status"

The method receives the model instance as obj and returns the display value.

Note: Add a short_description attribute to customize the column header.

Relationship Fields

Access related model fields using Django's double-underscore notation:

@register(Order)
class OrderAdmin(ModelAdmin):
    list_display = [
        'order_number',
        'customer__full_name',  # Access related field
        'customer__email',
        'total',
        'created_at',
    ]

Pagination

Control how many records appear per page:

@register(Product)
class ProductAdmin(ModelAdmin):
    list_display = ['name', 'sku', 'price']
    paginate_by = 50  # Default: 100

Custom Pagination Class

For advanced pagination needs, provide a custom pagination class:

from django.core.paginator import Paginator

class CustomPaginator(Paginator):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Custom pagination logic

@register(Product)
class ProductAdmin(ModelAdmin):
    paginate_by = 50
    pagination_class = CustomPaginator

Form Configuration

Basic Fields Configuration

Control which fields appear in forms:

@register(Product)
class ProductAdmin(ModelAdmin):
    # All forms show these fields by default
    fields = ['name', 'sku', 'category', 'price', 'stock_quantity']

Special Values: - '__all__' (default) - Include all model fields - ['field1', 'field2'] - Include only specified fields

Different Fields for Create vs Update

Often you want different fields for creating vs editing records:

@register(Product)
class ProductAdmin(ModelAdmin):
    # Default for both create and update
    fields = '__all__'

    # Simplified create form (only essential fields)
    create_fields = ['name', 'sku', 'category', 'price']

    # Update form uses 'fields' (shows all fields)

Resolution Order: - CreateView: create_fieldsfields'__all__' - UpdateView: update_fieldsfields'__all__'

Example from webshop

# examples/webshop/djadmin.py
@register(Product)
class ProductAdmin(ModelAdmin):
    list_display = ['name', 'sku', 'category', 'price', 'stock_quantity', 'status']
    paginate_by = 10

    # Simplified create form
    create_fields = [
        'name', 'slug', 'sku', 'category', 'tags',
        'description', 'price', 'cost', 'stock_quantity'
    ]

    # Update shows all fields
    update_fields = '__all__'

This pattern keeps the create form focused while allowing full editing later.

Custom Form Classes

For more control over form behavior and validation:

from django import forms
from .models import Product

class ProductForm(forms.ModelForm):
    class Meta:
        model = Product
        fields = '__all__'
        widgets = {
            'description': forms.Textarea(attrs={'rows': 4}),
            'tags': forms.CheckboxSelectMultiple(),
        }

    def clean_price(self):
        price = self.cleaned_data['price']
        if price <= 0:
            raise forms.ValidationError("Price must be positive")
        return price

@register(Product)
class ProductAdmin(ModelAdmin):
    form_class = ProductForm

Different Form Classes for Create vs Update

class ProductCreateForm(forms.ModelForm):
    class Meta:
        model = Product
        fields = ['name', 'sku', 'category', 'price']

class ProductUpdateForm(forms.ModelForm):
    class Meta:
        model = Product
        fields = '__all__'

    # Update form includes custom validation
    def clean_stock_quantity(self):
        quantity = self.cleaned_data['stock_quantity']
        if quantity < 0:
            raise forms.ValidationError("Cannot have negative stock")
        return quantity

@register(Product)
class ProductAdmin(ModelAdmin):
    create_form_class = ProductCreateForm
    update_form_class = ProductUpdateForm

Resolution Order: - CreateView: create_form_classform_class → auto-generated - UpdateView: update_form_classform_class → auto-generated

Note: When using custom form classes, the fields and create_fields/update_fields attributes are ignored. Define fields in the form's Meta.fields.

Multiple ModelAdmin Registrations

You can register the same model multiple times for different views:

# Simple view for quick access
@register(Product)
class SimpleProductAdmin(ModelAdmin):
    list_display = ['name', 'sku', 'price']
    fields = ['name', 'sku', 'price']

# Detailed view with all fields
@register(Product)
class DetailedProductAdmin(ModelAdmin):
    list_display = ['name', 'sku', 'category', 'price', 'cost', 'stock_quantity', 'status']
    fields = '__all__'

# Read-only view
@register(Product)
class ReadOnlyProductAdmin(ModelAdmin):
    list_display = ['name', 'sku', 'price', 'status']
    list_actions = []      # No add button
    record_actions = []    # No edit/delete
    bulk_actions = []      # No bulk operations

Both will appear in the dashboard as separate entries: - Product (SimpleProductAdmin) - Product (DetailedProductAdmin) - Product (ReadOnlyProductAdmin)

Each gets its own URL: - /djadmin/myapp/product/0/ - /djadmin/myapp/product/1/ - /djadmin/myapp/product/2/

Default Values

When you don't specify configuration, these defaults are used:

class ModelAdmin:
    list_display = ['__str__']        # Show model's __str__ method
    general_actions = [ListAction]  # Provided by core plugin
    bulk_actions = [DeleteBulkAction]   # Provided by core plugin
    record_actions = [EditAction, DeleteAction]  # Provided by core plugin
    paginate_by = 100
    fields = '__all__'

Best Practices

1. Start Simple, Add Complexity

Begin with basic configuration and add features as needed:

# Start here
@register(Product)
class ProductAdmin(ModelAdmin):
    list_display = ['name', 'price']

# Add more as you need it
@register(Product)
class ProductAdmin(ModelAdmin):
    list_display = ['name', 'sku', 'price', 'stock_indicator']
    paginate_by = 50
    create_fields = ['name', 'sku', 'price']

    def stock_indicator(self, obj):
        return "In Stock" if obj.stock_quantity > 0 else "Out of Stock"

2. Use Column for Better Control

Prefer Column when you need custom labels or styling:

# Good
list_display = [
    Column('sku', label='SKU Code'),
    Column('price', label='Price (USD)', classes='text-right'),
]

# Basic (works, but less control)
list_display = ['sku', 'price']

3. Separate Create and Update Fields

Keep create forms simple, allow full editing on update:

create_fields = ['name', 'sku', 'category', 'price']  # Essential only
fields = '__all__'  # Update shows everything

4. Custom Methods for Computed Values

Add computed columns instead of cluttering your model:

def total_value(self, obj):
    return obj.price * obj.stock_quantity

total_value.short_description = "Total Value"

5. Multiple ModelAdmins for Different Use Cases

Create specialized views for different user needs:

@register(Product)
class QuickProductAdmin(ModelAdmin):
    """Quick view for daily operations"""
    list_display = ['name', 'sku', 'stock_quantity']
    create_fields = ['name', 'sku', 'price']

@register(Product)
class FullProductAdmin(ModelAdmin):
    """Complete view for product management"""
    list_display = ['name', 'sku', 'category', 'price', 'cost', 'margin', 'stock_quantity']
    fields = '__all__'

Real-World Examples

E-commerce Product Admin

from djadmin import ModelAdmin, register, Column

@register(Product)
class ProductAdmin(ModelAdmin):
    list_display = [
        Column('name', label='Product Name'),
        Column('sku', label='SKU'),
        'category',
        Column('price', label='Price (USD)', classes='text-right'),
        'stock_status',
        'created_at',
    ]

    paginate_by = 25

    create_fields = [
        'name', 'slug', 'sku', 'category', 'tags',
        'description', 'price', 'cost', 'stock_quantity'
    ]

    update_fields = '__all__'

    def stock_status(self, obj):
        if obj.stock_quantity == 0:
            return "Out of Stock"
        elif obj.stock_quantity < 10:
            return f"Low ({obj.stock_quantity})"
        return f"In Stock ({obj.stock_quantity})"

    stock_status.short_description = "Stock"

Customer Management

@register(Customer)
class CustomerAdmin(ModelAdmin):
    list_display = [
        'full_name',
        'email',
        Column('city', classes='text-muted'),
        Column('country', classes='text-muted'),
        'order_count',
        'is_active',
    ]

    paginate_by = 50

    create_fields = [
        'full_name', 'email', 'phone',
        'address', 'city', 'country', 'postal_code'
    ]

    def order_count(self, obj):
        return obj.orders.count()

    order_count.short_description = "Orders"

Blog Post Admin

@register(Post)
class PostAdmin(ModelAdmin):
    list_display = [
        'title',
        'author',
        'published_status',
        'view_count',
        'published_date',
    ]

    create_fields = ['title', 'slug', 'author', 'content']
    update_fields = '__all__'

    def published_status(self, obj):
        if obj.published_date and obj.published_date <= timezone.now():
            return "Published"
        elif obj.published_date:
            return f"Scheduled ({obj.published_date})"
        return "Draft"

    published_status.short_description = "Status"

See Also