Chapter 4: Django Admin – Set Fields to Display
Django Admin – Set Fields to Display: How to control exactly which fields appear in the list view (also called the change list, the table view you see when you click on “Questions” or “Choices”)
Right now if you only did admin.site.register(Question) you see something very basic and ugly:
- id
- question_text (maybe truncated badly)
- pub_date
- … and nothing else useful
Today we’re going to make that list beautiful, informative, clickable, filter-friendly, and production-ready — step by step, very slowly, like I’m explaining it to you in person.
We’ll use your existing Question and Choice models.
Goal – What a Nice Admin List Should Have
Before we write code, let’s agree what we want to see when we open “Questions”:
- Shortened question text (clickable to edit)
- Category with color
- Publication date
- “Recent?” icon (green/red)
- Active/Inactive status icon
- Total vote count (aggregated)
- Slug (for debugging)
- Maybe edit inline for is_active
Same idea for Choices.
Step 1 – The Minimal “list_display” (Start Here)
Open polls/admin.py
Replace with this clean base:
|
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 |
from django.contrib import admin from .models import Question, Choice @admin.register(Question) class QuestionAdmin(admin.ModelAdmin): list_display = ( 'question_text', # default — full text 'pub_date', 'is_active', ) @admin.register(Choice) class ChoiceAdmin(admin.ModelAdmin): list_display = ( 'choice_text', 'question', 'votes', ) |
→ Refresh admin → already better than default (id gone, useful fields)
But still boring. Let’s level it up.
Step 2 – Make It Really Nice (Full Recommended Setup)
Replace the whole QuestionAdmin class with this:
|
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 |
@admin.register(Question) class QuestionAdmin(admin.ModelAdmin): # ── The heart of the list view ───────────────────────────────────────── list_display = ( 'question_text_short', # custom method – truncated + clickable 'category_colored', 'pub_date', 'was_published_recently_icon', 'is_active_badge', 'vote_count', 'slug_preview', ) # Which columns are clickable (go to edit page) list_display_links = ('question_text_short',) # ── Other useful settings ─────────────────────────────────────────────── list_filter = ('is_active', 'category', 'pub_date') search_fields = ('question_text', 'slug') list_per_page = 25 date_hierarchy = 'pub_date' # ── Custom methods that appear as columns ─────────────────────────────── @admin.display(description='Question', ordering='question_text') def question_text_short(self, obj): text = obj.question_text return text[:70] + '...' if len(text) > 70 else text @admin.display(description='Category') def category_colored(self, obj): colors = { 'fun': '#28a745', 'politics': '#dc3545', 'sports': '#0d6efd', 'tech': '#6f42c1', 'general': '#6c757d', } color = colors.get(obj.category, '#6c757d') return format_html( '<span style="color: {}; font-weight: 500;">{}</span>', color, obj.category.title() ) @admin.display(description='Recent?', boolean=True) def was_published_recently_icon(self, obj): return obj.was_published_recently() @admin.display(description='Active') def is_active_badge(self, obj): if obj.is_active: return format_html('<span style="color:#198754;">✔ Active</span>') return format_html('<span style="color:#dc3545;">✘ Inactive</span>') @admin.display(description='Votes', ordering='choices__votes__sum') def vote_count(self, obj): total = obj.choices.aggregate(total=Sum('votes'))['total'] return total or 0 @admin.display(description='Slug preview') def slug_preview(self, obj): return obj.slug[:20] + '...' if len(obj.slug) > 20 else obj.slug |
And for ChoiceAdmin:
|
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 |
@admin.register(Choice) class ChoiceAdmin(admin.ModelAdmin): list_display = ( 'choice_text_short', 'question_link', 'votes', ) list_filter = ('question',) search_fields = ('choice_text',) raw_id_fields = ('question',) # better when thousands of questions @admin.display(description='Choice', ordering='choice_text') def choice_text_short(self, obj): text = obj.choice_text return text[:60] + '...' if len(text) > 60 else text @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 Gained (Refresh Admin → Questions List)
- Shortened question text (clickable to edit)
- Category in nice colors
- Green check / red X for recent
- Active/Inactive badge
- Total votes column (real aggregation!)
- Slug preview
- Clean, scannable, informative
Step 3 – Quick Explanations of Important Tricks
| Line / Concept | Why it’s useful |
|---|---|
| list_display | Defines columns in the table |
| list_display_links | Which columns are clickable (usually the title) |
| @admin.display(description=…) | Custom column header (instead of method name) |
| ordering=… | Sort column when clicking header |
| boolean=True | Shows nice green check / red X icon instead of True/False |
| format_html(…) | Safely inject HTML (color, links, icons) |
| aggregate(total=Sum(‘votes’)) | Calculate total votes across related choices |
| raw_id_fields | Better foreign-key widget when parent table is huge |
Step 4 – Test Drive (Do This Right Now)
- Go to /admin/ → Questions
- See nice columns, colors, icons, vote counts
- Click column headers → sort
- Use search box → type part of question text
- Use filters (left or right sidebar) → show only active, only fun category
- Add new question → see slug auto-fill
Bonus – Make It Even Better (Optional but Loved)
Add this to QuestionAdmin:
|
0 1 2 3 4 5 6 7 8 9 10 |
# Inline edit some fields directly in list list_editable = ('is_active',) # Faster loading when lots of questions list_select_related = () # add foreign keys if needed later |
→ Now you can toggle is_active directly in the list without opening edit page.
Common “Why Doesn’t It Show?” Problems
| Problem | Likely Cause | Fix |
|---|---|---|
| Column shows method name not value | Forgot @admin.display(description=…) | Add the decorator |
| Vote count always 0 | No related choices or wrong aggregation | Check choices__votes__sum syntax |
| No color / HTML | Used str() instead of format_html | Use format_html() |
| Clickable column not working | Forgot list_display_links | Add (‘question_text_short’,) |
| Old data not updated | Cache or browser refresh | Ctrl+F5 or incognito |
Your Quick Homework (Do This – It Will Stick)
- Copy-paste the full QuestionAdmin and ChoiceAdmin code above
- Refresh admin → Questions list
- Create 4–5 questions with different categories & vote counts
- Play with search, filters, sorting, click columns
- Notice how much more useful the list becomes
Tell me what you want next:
- “Looks great! Now show me how to add custom admin actions (bulk publish/unpublish)”
- “How to make some columns read-only or editable inline?”
- “I want to add charts or summary stats on the change list”
- “Got weird column / error – here’s what I see”
- Or finally ready for: “Let’s finish the public voting flow – form + POST + F() + results”
You now have a clean, powerful, informative admin list view — exactly what real teams use every day.
You’re doing excellent work — let’s keep going! 🚀🇮🇳
