The Add-Item Form Outside the Admin: Django Forms & Type-Specific Fields

Personal Catalogue: Django & PostgreSQL

Chapter 5 · The Add-Item Form Outside the Admin: Django Forms & Type-Specific Fields

Chapter 4 gave the catalogue real public list and detail pages, but adding an item still means logging into /admin/. This chapter builds a real, public-facing add-item form — and makes that one shared form feel genuinely type-aware, without forking it into four separate forms.

The Core Idea: Data-Driven Labels, Not a Fork in the Form

A tempting but wrong approach here is to build a genuinely separate form for each item type — a "book form," a "CD form," a "DVD form" — each with its own field names. That would mean four times the template markup to maintain, and four separate view branches to validate and save. Since the underlying Item model is deliberately shared (Chapter 2), the form stays shared too: one set of fields, with their labels — not their underlying model fields — changing based on whatever item type is currently selected.

Recap: One Shared Schema, Several Real Meanings
Chapter 2 already established this table — worth having in view again before building the UI on top of it:
FieldBookCDDVD / Blu-ray
creatorAuthorArtistDirector
format_detailHardcover / paperbackSingle / albumRegion code

A Real Django ModelForm

# catalogue/forms.py from django import forms from .models import Item class ItemForm(forms.ModelForm): class Meta: model = Item fields = ['item_type', 'title', 'creator', 'format_detail', 'release_year', 'tags', 'notes'] widgets = { 'tags': forms.CheckboxSelectMultiple(), }

A ModelForm derives its own fields, widgets, and — critically — its own validation rules directly from the Item model itself, rather than requiring them to be redeclared by hand. release_year's own real PositiveSmallIntegerField constraint from Chapter 2 is enforced automatically here, with no extra validation code needed.

The CreateView

# catalogue/views.py (added to the existing file) from django.views.generic.edit import CreateView from django.urls import reverse_lazy from .models import Item, ItemType from .forms import ItemForm class ItemCreateView(CreateView): model = Item form_class = ItemForm template_name = 'catalogue/item_form.html' success_url = reverse_lazy('item_list') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['creator_labels'] = {t.name: LABELS.get(t.name, {}).get('creator', 'Creator') for t in ItemType.objects.all()} context['format_labels'] = {t.name: LABELS.get(t.name, {}).get('format', 'Format detail') for t in ItemType.objects.all()} return context LABELS = { 'book': {'creator': 'Author', 'format': 'Binding (hardcover / paperback)'}, 'cd': {'creator': 'Artist', 'format': 'Release type (single / album)'}, 'dvd': {'creator': 'Director', 'format': 'Region code'}, 'bluray': {'creator': 'Director', 'format': 'Region code'}, }

Add the URL to catalogue/urls.py:

path('add/', views.ItemCreateView.as_view(), name='item_add'),

Building the Label Map with json_script

Rather than hand-typing a JSON string directly into the template — a real, documented cross-site-scripting risk if any of that data ever contains untrusted content — Django provides a dedicated template filter built specifically for this exact job:

catalogue/templates/catalogue/item_form.html
{% extends 'catalogue/base.html' %} {% block content %} {{ creator_labels|json_script:"creator-labels-data" }} {{ format_labels|json_script:"format-labels-data" }} <form method="post"> {% csrf_token %} {{ form.as_p }} <button type="submit">Add Item</button> </form> {% endblock %}
What json_script Actually Does
{{ creator_labels|json_script:"creator-labels-data" }} renders a real <script id="creator-labels-data" type="application/json"> tag, with its own JSON content correctly HTML-escaped by Django — safe to include directly in a template even if the underlying data were ever user-controlled, unlike manually interpolating a JSON string into a page. It's the Django-native equivalent of what PHP Intermediate's own json_encode() technique accomplishes in the PHP variant of this project (catalog-php1) — the same underlying goal, real server-rendered data handed safely to client-side JavaScript, achieved through Django's own dedicated tooling rather than a manually-built inline script.

Wiring the Dropdown to the Labels

A small vanilla JS block reads the two JSON script tags and updates the form's own labels whenever the item type dropdown changes:

<script> const creatorLabels = JSON.parse(document.getElementById('creator-labels-data').textContent); const formatLabels = JSON.parse(document.getElementById('format-labels-data').textContent); const select = document.getElementById('id_item_type'); const creatorLabel = document.querySelector('label[for="id_creator"]'); const formatLabel = document.querySelector('label[for="id_format_detail"]'); function updateLabels() { const typeName = select.options[select.selectedIndex].text.toLowerCase(); creatorLabel.textContent = (creatorLabels[typeName] ?? 'Creator') + ':'; formatLabel.textContent = (formatLabels[typeName] ?? 'Format detail') + ':'; } select.addEventListener('change', updateLabels); updateLabels(); </script>

id_item_type and id_creator are Django's own default, automatically-generated field IDs — {{ form.as_p }} already produces them without any extra configuration, which is why the JS above can rely on them directly.

Client-Side Relabeling Is Cosmetic Only
This JavaScript changes what the form looks like — it does not, and cannot, change what data is actually accepted when the form is submitted. A user with JavaScript disabled, or one deliberately tampering with the submitted request, still hits the exact same ItemForm validation on the server, defined once in Chapter 2's own model constraints and re-used automatically by the ModelForm. The label-swapping script is a real usability improvement, not a security or validation mechanism — the server-side form is what actually enforces the rules, exactly as it does in the PHP variant of this project.

Trying It Out

Visit /add/, select each item type in turn, and confirm the "Creator" and "Format detail" labels genuinely change to Author/Artist/Director and the matching format hint. Submit a real book with a title, author, and at least one tag, then confirm it appears correctly on the list page from Chapter 4.

Hands-On Exercises

Exercise 1

Build the form, view, template, and label-swapping script from this chapter, then add one real item of each of the four types through this new public form.

📄 View solution
Exercise 2

Explain what json_script actually produces in the rendered HTML, and why it's genuinely safer than building the equivalent <script> tag by hand-interpolating a JSON string into the template.

📄 View solution
Exercise 3

Explain why the label-swapping JavaScript from this chapter is not, and cannot be, a substitute for the server-side validation the ModelForm already provides.

📄 View solution

Chapter 5 Quick Reference

  • ItemForm — a real ModelForm deriving its fields and validation directly from the Item model
  • ItemCreateView — Django's own generic CreateView, redirecting to the list page via success_url on success
  • One shared form, several labels — labels change per item type via JavaScript; the underlying fields and their names never change
  • json_script — Django's own dedicated, safely-escaped way to hand server-rendered data to client-side JavaScript
  • Real security note — client-side relabeling is cosmetic only; the ModelForm's own server-side validation is what actually enforces the rules
  • Next chapter: Tags — filtering the catalogue by tag