Chapter 1: Django Admin
The Django Admin
Many beginners see the admin as “just a toy for learning” — but in real Indian startups, internal tools, content platforms, SaaS products, inventory systems, and even early versions of fairly large apps, the Django admin is often the main backend interface used by non-technical team members (content writers, ops, support, founders) for months or even years.
Today I’m going to teach you how to go from “empty admin login screen” to beautiful, powerful, production-ready admin for your polls app — step by step, very slowly, like I’m sitting next to you looking at your screen.
Let’s do this properly.
Step 0 – Quick Reality Check (What You Should Already Have)
Before we start, confirm these 3 things:
- django.contrib.admin is in INSTALLED_APPS (it is by default)
- path(‘admin/’, admin.site.urls) is in mysite/urls.py (also default)
- You already ran python manage.py createsuperuser at least once
If any is missing → fix it now.
Go to http://127.0.0.1:8000/admin/ Login → you should see the default dashboard (but no Polls yet).
Step 1 – The Minimal “Make It Appear” Version
Open polls/admin.py
Replace everything with this (old-school simple way):
|
0 1 2 3 4 5 6 7 8 9 10 |
from django.contrib import admin from .models import Question, Choice admin.site.register(Question) admin.site.register(Choice) |
Save → refresh admin page.
→ Boom! You now see “Questions” and “Choices” under a new “Polls” section.
You can already:
- Add new questions
- Add choices (but you have to choose question manually each time)
- Edit / delete existing ones
This is already useful — but ugly and not efficient.
Step 2 – Modern & Beautiful Way (Recommended 2026 Style)
Replace polls/admin.py with this much better version:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
from django.contrib import admin from django.utils.html import format_html from .models import Question, Choice # ── Inline choices inside question edit page ──────────────────────────────── class ChoiceInline(admin.TabularInline): model = Choice extra = 3 # show 3 empty rows ready to fill fields = ('choice_text', 'votes') ordering = ['id'] verbose_name = "Choice" verbose_name_plural = "Choices" @admin.register(Question) class QuestionAdmin(admin.ModelAdmin): # ── List view columns ─────────────────────────────────────────────────── list_display = ( 'question_text_truncated', 'category_colored', 'pub_date', 'was_published_recently_colored', 'is_active_icon', 'vote_count', 'slug', ) list_display_links = ('question_text_truncated',) # clickable title # ── Filters (right sidebar) ───────────────────────────────────────────── list_filter = ( 'is_active', 'category', 'pub_date', ) # ── Search box (top right) ────────────────────────────────────────────── search_fields = ( 'question_text', 'slug', 'category', ) # ── Show choices inline when editing a question ───────────────────────── inlines = [ChoiceInline] # ── Field grouping on change form ─────────────────────────────────────── fieldsets = ( (None, { 'fields': ('question_text', 'slug') }), ('Publication & Visibility', { 'fields': ('pub_date', 'is_active', 'category'), 'classes': ('collapse',) # collapsible section }), ) # ── Auto-fill slug from question text ─────────────────────────────────── prepopulated_fields = {'slug': ('question_text',)} # ── Calendar-style date filter ────────────────────────────────────────── date_hierarchy = 'pub_date' # ── Items per page ────────────────────────────────────────────────────── list_per_page = 20 # ── Custom methods for nicer list display ─────────────────────────────── @admin.display(description="Question", ordering='question_text') def question_text_truncated(self, obj): return (obj.question_text[:60] + "...") if len(obj.question_text) > 60 else obj.question_text @admin.display(description="Category") def category_colored(self, obj): colors = { 'fun': '#28a745', 'politics': '#dc3545', 'sports': '#007bff', 'tech': '#6f42c1', } color = colors.get(obj.category, '#6c757d') return format_html('<span style="color: {};">{}</span>', color, obj.category.title()) @admin.display(description="Recent?", boolean=True) def was_published_recently_colored(self, obj): return obj.was_published_recently() @admin.display(description="Active") def is_active_icon(self, obj): if obj.is_active: return format_html('<span style="color:green;">✔ Yes</span>') return format_html('<span style="color:red;">✘ No</span>') @admin.display(description="Total Votes", ordering='choices__votes__sum') def vote_count(self, obj): return obj.choices.aggregate(total=Sum('votes'))['total'] or 0 @admin.register(Choice) class ChoiceAdmin(admin.ModelAdmin): list_display = ( 'choice_text', 'question_link', 'votes', ) list_filter = ('question',) search_fields = ('choice_text',) # Faster foreign key widget when thousands of questions raw_id_fields = ('question',) @admin.display(description="Question") def question_link(self, obj): url = reverse('admin:polls_question_change', args=[obj.question.pk]) return format_html('<a href="{}">{}</a>', url, obj.question) |
What You Just Got (After Refreshing Admin)
- Truncated question text (no super-long titles breaking layout)
- Colored category (visual cue)
- Green/Red recent icon
- Active/Inactive icons
- Total votes column (aggregated from choices)
- Choices editable inline (add/edit/delete choices without leaving question page)
- Pre-filled slug
- Collapsible fieldsets
- Search, filters, date hierarchy, pagination
- Bulk actions (select multiple → activate/deactivate)
Step 3 – Add Bulk Actions (Very Useful in Real Life)
Add this inside QuestionAdmin class:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
actions = ['make_active', 'make_inactive'] @admin.action(description="Mark selected questions as Active") def make_active(self, request, queryset): queryset.update(is_active=True) @admin.action(description="Mark selected questions as Inactive") def make_inactive(self, request, queryset): queryset.update(is_active=False) |
Now you can select many questions → Actions dropdown → deactivate old polls in bulk.
Step 4 – Customize Admin Titles & Look
Create or edit mysite/admin.py:
|
0 1 2 3 4 5 6 7 8 9 10 |
from django.contrib import admin admin.site.site_header = "Hyderabad Polls Administration" admin.site.site_title = "Polls Admin Portal" admin.site.index_title = "Welcome to the Backend – Manage Polls Here" |
→ Much friendlier header.
Step 5 – Quick Test Drive (Do This Now)
- Go to /admin/
- Login
- Click “Questions” → see nice columns, filters, search
- Click “Add Question” → see inline choices + auto-slug
- Create 3–4 questions + choices
- Select 2 → Actions → make inactive → see them disappear from frontend list (if filtered)
- Search for a keyword → see instant results
Real-World Pro Tips (2026 Edition)
- Install django-jazzmin or django-admin-interface (very popular in India right now)
Bash0123456pip install django-jazzmin
Add ‘jazzmin’before‘django.contrib.admin’ in INSTALLED_APPS → beautiful sidebar, dark mode, quick stats.
- Use list_editable = (‘is_active’, ‘category’) → edit fields directly in list view
- Add list_select_related = (‘category’,) → faster queries if you have foreign keys
- For large data: raw_id_fields, autocomplete_fields, django-admin-autocomplete-filter
Common “Why Isn’t It Working?” Moments
| Problem | Cause | Fix |
|---|---|---|
| No Polls section | Forgot @admin.register or admin.site.register | Add registration |
| Choices not inline | No inlines | Add inlines = [ChoiceInline] |
| Slug not auto-fill | No prepopulated_fields | Add the line |
| No vote count column | No aggregation method | Add vote_count method |
| “You don’t have permission” | Logged in with normal user | Use superuser or give is_staff=True + permissions |
Your Quick Task Right Now
- Replace your polls/admin.py with the full code above
- Create 5 questions + choices via admin
- Play with filters, search, bulk actions
- Change one question to inactive → check if frontend list respects is_active=True filter
Tell me what you want next:
- “Admin looks awesome! Now show me staff users & permissions”
- “How to add charts (vote stats) to admin dashboard?”
- “I want autocomplete for questions in Choice admin”
- “Got error when saving – here’s the message”
- Or finally: “Let’s implement the full voting system – form, POST, F(), results”
You now have a production-grade, beautiful admin backend in ~60 lines of code. This is one of the reasons people say “Django batteries are included”.
You’re doing incredible — let’s keep this energy! 🚀🇮🇳
