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>