I needed to create a custom form in Django admin for batch creating record by uploading a CSV file. The example I use here is upload a CSV of product keys for an existing product in the database. I wanted the custom form styling to match the default Django admin form styling. I’m new to Django so there may be a better way to do it, but this worked for me.
The first thing is to create a custom form class. Mine is pretty simple, a CSV file of product keys and a link to the product. We’re deriving from forms.Form since this is a custom form and it won’t match up to the model fields.
# storefront/products/admin.py from django import forms from products.models import Product from products.models import ProductKey class ProductKeyUploadForm(forms.Form): product = forms.ModelChoiceField(queryset=Product.objects.all()) file = forms.FileField(label='Product Keys')
The next thing we need to do is add a new URL for this page to our ModelAdmin instance.
# storefront/products/admin.py from django.contrib import admin class ProductKeyAdmin(admin.ModelAdmin): def get_urls(self): urls = super(ProductKeyAdmin, self).get_urls() add_urls = [ url( r'^upload/$', self.admin_site.admin_view(self.upload), name='products_productkey_upload' ) ] return add_urls + urls def upload(self, request): # Coming soon pass admin.site.register(ProductKey, ProductKeyAdmin)
When creating the url it’s important to decorate the view with self.admin_site.admin_view() to incorporate admin permissions. The name= parameters provides a name for linking to the page with the url() function. The Django standard is ‘app_model_action’. We’ll see how to link to this in view template.
Now in the view template file we to extend admin/base_site.html, but use the form styling of admin/change_form.html.
<!-- storefront/products/templates/admin/products/productkey/upload.html --> {% extends "admin/base_site.html" %} {% load i18n admin_urls admin_static %} {% block extrastyle %} {{ block.super }} <link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}"/> {% endblock %} {% block breadcrumbs %} <div class="breadcrumbs"> <a href="{% url 'admin:index' %}">{% trans 'Home' %}</a> › <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ app_label|capfirst|escape }}</a> › {% if has_change_permission %}<a href="{% url opts|admin_urlname:'changelist' %}"> {{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %} › {% trans 'Upload' %} </div> {% endblock %} {% block content %} <form action="{% url opts|admin_urlname:'upload' %}" method="post" id="productkey_upload_form" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %}> {% csrf_token %} <div> {% for fieldset in adminform %} {% include "admin/includes/fieldset.html" %} {% endfor %} </div> <div class="submit-row"> <input type="submit" value="Submit" class="default"/> </div> </form> {% endblock %}
This adds the forms CSS that’s used by change_form.html.
{% block extrastyle %} {{ block.super }} <link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}"/> {% endblock %}
This block updates the breadcrumbs like change_form.html, but adds ‘Upload’ as the last crumb.
{% block breadcrumbs %} <div class="breadcrumbs"> <a href="{% url 'admin:index' %}">{% trans 'Home' %}</a> › <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ app_label|capfirst|escape }}</a> › {% if has_change_permission %}<a href="{% url opts|admin_urlname:'changelist' %}"> {{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %} › {% trans 'Upload' %} </div> {% endblock %}
This section adds the form. The ‘admin/includes/fieldset.html’ template outputs the form fields in the same format as admin/change_form.html. Notice the admin_urlname:'upload'
, that translates into ‘admin:products_productkey_upload’ and matches the url we added. The one thing we are missing is the adminform object. We need to create that in our view method.
{% block content %} <form action="{% url opts|admin_urlname:'upload' %}" method="post" id="productkey_upload_form" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %}> {% csrf_token %} <div> {% for fieldset in adminform %} {% include "admin/includes/fieldset.html" %} {% endfor %} </div> <div class="submit-row"> <input type="submit" value="Submit" class="default"/> </div> </form> {% endblock %}
In our view method we need to mimic the same view context as the change admin requests.
# storefront/products/admin.py from django.shortcuts import render from django.contrib.admin import helpers class ProductKeyAdmin(admin.ModelAdmin): def upload(self, request): context = { 'title': 'Upload Product Keys', 'app_label': self.model._meta.app_label, 'opts': self.model._meta, 'has_change_permission': self.has_change_permission(request) } # Handle form request if request.method == 'POST': form = ProductKeyUploadForm(request.POST, request.FILES) if form.is_valid(): # Do CSV processing and create ProductKey records else: form = ProductKeyUploadForm() context['form'] = form context['adminform'] = helpers.AdminForm(form, list([(None, {'fields': form.base_fields})]), self.get_prepopulated_fields(request)) return render(request, 'admin/products/productkey/upload.html', context) admin.site.register(ProductKey, ProductKeyAdmin)
We need to populate the view context with some data used by admin/base.html and in our template to create the breadcrumbs.
context = { 'title': 'Upload Product Keys', 'app_label': self.model._meta.app_label, 'opts': self.model._meta, 'has_change_permission': self.has_change_permission(request) }
The key piece in mimicking the admin/change_form.html is recreating the adminform object. To do that we use the AdminForm helper and pass it a custom fieldset argument. The fieldset is created from our upload form’s fields rather than the default model form. Reference django.contrib.admin.options for how the adminform is normally created.
context['adminform'] = helpers.AdminForm(form, list([(None, {'fields': form.base_fields})]), self.get_prepopulated_fields(request))
With all those pieces put together you’ll end up with a custom form that has styling just like the default change form. Here’s what my example looks like:
I’m also new to Django and have been looking for a solution like this for a while now. I really don’t get why django does everything for us on the admin site and leaves us alone outside admin. It shouldn’t be too hard to create a form with the same looks of the admin site!
Thank you!
Yeah, I can’t believe how tough it is to simply replicate the style of the admin forms.
Thanks for the clear explanations – this is probably the most elegant method I’ve seen for adding custom forms that mimicks Django’s own admin.
I’ve largely copy-and-pasted your code for the new bulk photo upload interface on Django-photologue – see http://www.django-photologue.net/
Thanks again!
Thanks, very nice article.
Thanks for sharing this!
One improvement tough: The upload method should be decorated with @method_decorator(staff_member_required) else anyone can access the upload form.
Thanks for sharing, mate!
Hey, thanks for the article. Found many solutions and this one was the only one that worked.
Note for people that customized the admin title: you can pass “site_title” and “site_header” in the context, or else it will appear “Django management”