Building a Customizable Admin Interface in Django

Initializing the Admin Module

Start by creating a new Django app to encapsulate the custom admin logic.

python manage.py startapp core_admin

Add core_admin to your INSTALLED_APPS list in the settings file. Then, include the app's URLs in the project's main urls.py:

url(r'^admin_panel/', include("core_admin.urls"))

Dynamic Model Registration

Create a configuration file, admin_registry.py, to define how models are displayed. The structure will map app labels and model names to their respective admin configurations.

from django.utils.safestring import mark_safe
from core import models

registry = {}

class BaseAdmin:
    list_display = []
    list_filters = []
    list_per_page = 20

class ClientAdmin(BaseAdmin):
    list_display = ['id', 'name', 'status', 'contact_date']
    list_filters = ['status', 'source']

class FollowUpAdmin(BaseAdmin):
    list_display = ['client', 'note', 'date']

def register(model_cls, config_cls=None):
    if config_cls is None:
        config_cls = BaseAdmin
    
    app_label = model_cls._meta.app_label
    model_name = model_cls._meta.model_name
    
    if app_label not in registry:
        registry[app_label] = {}
    
    config_cls.model = model_cls
    registry[app_label][model_name] = config_cls

# Registering models
register(models.Client, ClientAdmin)
register(models.FollowUp, FollowUpAdmin)

Dashboard View Implementation

In core_admin/views.py, create a view to pass the registry to the template.

from django.shortcuts import render
from . import admin_registry

def dashboard(request):
    return render(request, 'core_admin/dashboard.html', {'registry': admin_registry.registry})

Configure the URL in core_admin/urls.py:

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^$', views.dashboard, name='admin_dashboard'),
]

Custom Template Tags for Metadata

Since Django templates cannot direct access attributes starting with underscores (like _meta), create custom tags. Inside core_admin/templatetags/admin_tags.py:

from django import template

register = template.Library()

@register.simple_tag
def get_model_verbose_name(admin_config):
    return admin_config.model._meta.verbose_name

The HTML template dashboard.html iterates through the registry:

{% load admin_tags %}
{% for app_label, models in registry.items %}
    <table class="table table-bordered">
        <thead>
            <tr>
                <th colspan="3">{{ app_label }}</th>
            </tr>
        </thead>
        <tbody>
            {% for model_name, config in models.items %}
                <tr>
                    <td>
                        <a href="{% url 'model_detail' app_label model_name %}">
                            {% get_model_verbose_name config %}
                        </a>
                    </td>
                    <td>Add</td>
                    <td>Change</td>
                </tr>
            {% endfor %}
        </tbody>
    </table>
{% endfor %}

Rendering Model Data

Create a view to handle individual table display. This view will need to handle pagination and filtering logic later.

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.shortcuts import render
from . import admin_registry
from .utils import get_filtered_queryset

def model_detail(request, app_label, model_name):
    config = admin_registry.registry[app_label][model_name]
    
    # Apply filters
    queryset, active_filters = get_filtered_queryset(request, config)
    
    paginator = Paginator(queryset, config.list_per_page)
    page = request.GET.get('page')
    
    try:
        paginated_set = paginator.page(page)
    except PageNotAnInteger:
        paginated_set = paginator.page(1)
    except EmptyPage:
        paginated_set = paginator.page(paginator.num_pages)
        
    return render(request, 'core_admin/model_detail.html', {
        'config': config, 
        'queryset': paginated_set,
        'active_filters': active_filters
    })

Add the URL pattern:

url(r'^(\w+)/(\w+)/$', views.model_detail, name="model_detail"),

To render the table rows dynamically, add another custom tag to handle choices and date formatting:

import datetime
from django.utils.safestring import mark_safe

@register.simple_tag
def render_table_row(instance, config):
    row_html = ""
    for field_name in config.list_display:
        field_obj = instance._meta.get_field(field_name)
        
        if field_obj.choices:
            # Display the human-readable value for choices
            field_val = getattr(instance, f"get_{field_name}_display")()
        else:
            field_val = getattr(instance, field_name)
            
        if isinstance(field_val, datetime.datetime):
            field_val = field_val.strftime("%Y-%m-%d %H:%M:%S")
            
        row_html += f"<td>{field_val}</td>"
        
    return mark_safe(row_html)

In model_detail.html:

{% load admin_tags %}
<table class="table">
    <thead>
        <tr>
            {% for field in config.list_display %}
                <th>{{ field }}</th>
            {% endfor %}
        </tr>
    </thead>
    <tbody>
        {% for obj in queryset %}
            <tr>
                {% render_table_row obj config %}
            </tr>
        {% endfor %}
    </tbody>
</table>

Implementing Pagination

Add a custom tag to render pagination controls, displaying a window of page numbers around the curent page.

@register.simple_tag
def render_pagination(queryset):
    current_page = queryset.number
    total_pages = queryset.paginator.num_pages
    page_range = queryset.paginator.page_range
    
    html = ""
    
    if queryset.has_previous():
        html += f'<li><a href="?page={queryset.previous_page_number()}">Previous</a></li>'
        
    for page_num in page_range:
        if abs(page_num - current_page) <= 2: # Show current page and 2 adjacent pages
            active_class = "active" if page_num == current_page else ""
            html += f'<li class="{active_class}"><a href="?page={page_num}">{page_num}</a></li>'
            
    if queryset.has_next():
        html += f'<li><a href="?page={queryset.next_page_number()}">Next</a></li>'
        
    return mark_safe(f'<ul class="pagination">{html}</ul>')

Adding Data Filtering

Create a utility function to handle query parameters for filtering in core_admin/utils.py.

def get_filtered_queryset(request, config):
    filter_dict = {}
    for key, value in request.GET.items():
        if value and key != 'page': # Exclude pagination param
            filter_dict[key] = value
    return config.model.objects.filter(**filter_dict), filter_dict

Render the filter form using a custom tag that handles select inputs for choice fields and foreign keys.

@register.simple_tag
def render_filter_field(field_name, config, active_filters):
    field_obj = config.model._meta.get_field(field_name)
    selected_value = active_filters.get(field_name, '')
    
    html = f'<select class="form-control" name="{field_name}"><option value="">-------</option>'
    
    if field_obj.choices:
        for val, label in field_obj.choices:
            is_selected = 'selected' if str(val) == selected_value else ''
            html += f'<option value="{val}">{label}</option>'
            
    elif hasattr(field_obj, 'get_choices'):
        # Handle ForeignKey or similar
        for val, label in field_obj.get_choices()[1:]: # Skip the empty choice
            is_selected = 'selected' if str(val) == selected_value else ''
            html += f'<option value="{val}">{label}</option>'
            
    html += '</select>'
    return mark_safe(html)

The filter form in the tmeplate:

<form class="form-inline" method="get">
    {% for field in config.list_filters %}
        <div class="form-group">
            <label>{{ field }}</label>
            {% render_filter_field field config active_filters %}
        </div>
    {% endfor %}
    <button class="btn btn-primary" type="submit">Filter</button>
</form>

Tags: Django python web development Admin Interface

Posted on Sat, 30 May 2026 18:02:42 +0000 by grimmier