Django forms and edit_inline models
In my Django models I tag just about everything except tags themselves. Most of my models have a counterpart that looks like this:
[toggle code]
-
class PageKey(models.Model):
- keyword = models.ForeignKey(Tag, core=True)
- page = models.ForeignKey(Page, edit_inline=models.TABULAR, min_num_in_admin=4, num_extra_on_change=2)
-
class Meta:
- unique_together = (('page', 'keyword'),)
-
def __str__(self):
- return self.page.title + " : " + self.keyword.title
This example adds an indefinite number of tags to each Page; in HTML parlance, these are the page’s keywords.
Adding them in the built-in administrative interface in Django is easy; Django shows them inline with the page’s entry/edit interface. In a custom form, adding a single select menu based on another model is also easy, using ModelChoiceField. ModelChoiceField automatically adds a select menu with the IDs and names based on a queryset, such as Tags.objects.all().
But adding indefinite inline model-based select menus to a custom form is more difficult. The answer appears to be to use MultiValueField and MultiWidget. The documentation is sparse at best on the subject, and the pieces necessary to do it are hidden in the source code and cryptic Google Groups postings. It took me a long time to figure this out, so I’m documenting it here to (a) make it easier for others, and (b) find out what I did wrong.
I did this in Django 0.96.
The basic edit page
One of the many nice things about Django is that the template system makes it easy to subclass templates. So in this example, I have a template for displaying pages, and I can base a new template off of that for editing pages. The edit page will have the same shell as the normal view page, with the content swapped out for a form for editing the page’s title, keywords, description, and content.
[toggle code]
- {% extends "pages/subpage.html" %}
-
{% block headers %}
- {{ block.super }}
- <link rel="stylesheet" type="text/css" href="http://www.hoboes.com/css/edit.css" />
- {% endblock %}
-
{% block content %}
- {{ form.errors }}
-
<form method="POST" action="/pages/edit/{{ page.slug }}/">
- <input type="submit" name="save" value="Save Changes" />
-
<div class="editcontent">
- <p>Title:{{ form.title.help_text }}
- {{ form.title }}</p>
- {{ form.errors.title }}
- <p>Keywords:{{ form.keywords.help_text }}
- {{ form.keywords }}</p>
- {{ form.errors.keywords }}
- <p>Short description:{{ form.description.help_text }}<br />
- {{ form.description }}</p>
- {{ form.errors.description }}
- <p>Main content:{{ form.content.help_text }}<br />
- {{ form.content }}</p>
- {{ form.errors.content }}
- </div>
- </form>
- {% endblock %}
So, to display this, I’m going to need to do something like this:
[toggle code]
- from django.shortcuts import get_object_or_404, render_to_response
- from django.contrib.auth.decorators import login_required
- from django import newforms as forms
- from mysite.pages.models import Page, PageKey, Tag
- @login_required
-
def editpage(request, page_slug):
- page = get_object_or_404(Page, slug=page_slug)
- pageForm = CMSForm(page=page, request=request)
- pageForm.post_changes()
- pageContext = {'page': page, 'form': pageForm}
- return render_to_response("edit/subpage.html", pageContext)
This is pretty basic, and if you’re looking at making custom Django forms you should already understand everything except for the pageForm parts. You’ll need to set this up in urls.py just as you would any other view.
Customizing forms.Form
In Django, a lot of form management can be pushed off into forms.Form (as I write this, this is specifically newforms.Form). Subclassing forms.Form to handle simple text fields is very easy.
[toggle code]
-
class CMSForm(forms.Form):
- title = forms.CharField(max_length=120)
- content = forms.CharField(widget=forms.Textarea, required=False)
- description = forms.CharField(max_length=500, widget=forms.Textarea)
-
def __init__(self, page, request, **kwargs):
- self.page = page
- self.request = request
- #if we just submitted a form, the data comes from the form
- #otherwise, it comes from the page itself
-
if self.request.method == "POST":
- kwargs['data'] = self.request.POST
-
else:
- pagedata = {'title': self.page.title, 'content': self.page.content, 'description': self.page.description}
- kwargs['data'] = pagedata
- super(CMSForm, self).__init__(**kwargs)
- #self.initialize_tags(self.request.method)
This example creates a text input for title, and a textarea for content and description. I’ve commented out the tag initialization call, because we haven’t created that yet, and that’s more complex. You’ll want to make sure you understand the simple stuff before you go on to the harder stuff.
The editpage function also calls “post_changes” on the CMSForm.
[toggle code]
-
-
def post_changes(self):
-
if self.request.method == 'POST':
-
if self.is_valid():
- changed = False
- title = self.cleanText('title')
- content = self.cleanText('content')
- description = self.cleanText('description')
-
if title != self.page.title:
- self.page.title = title
- changed = True
-
if content != self.page.content:
- self.page.content = content
- changed = True
-
if description != self.page.description:
- self.page.description = description
- changed = True
-
if changed:
- self.page.save()
- #self.save_tags()
-
if self.is_valid():
-
if self.request.method == 'POST':
-
def cleanText(self, field):
- text = self.clean_data[field]
-
if type(text) == unicode:
- text = text.encode("utf-8")
- return text
-
def post_changes(self):
Again, I’ve commented out the save_tags call. We’ll get to that in just a bit. This is otherwise pretty simple. When asked to save changes, it first checks to see if a form has been submitted; it then checks to see if the form submission is valid; and then it checks to see if the form submission actually changed anything.
The reason I do an “encode("utf-8")” on each piece of text is that otherwise, the comparison always comes back unequal. This is probably something I’m doing wrong on my HTML page or in my Model; I’ve never really understood encode/decode on Python strings. So you may or may not need that function.
MultiValueField and MultiWidget
Without MultiValueField, an indefinite number of keywords would require checking against an indefinite number of form fields. With MultiValueField, a lot of that code is handled for us. This, however, is where things start getting sticky. This works for me; whether it is right is another story entirely.
What I’ve done is subclass MultiValueField to accept the number of existing tags. The form will need this many select menus, plus a few more for entering additional tags. Each select menu shows every tag in the Tag model.
[toggle code]
-
class CMSTagFields(forms.MultiValueField):
-
def __init__(self, tagcount=0):
- formField = forms.ModelChoiceField(queryset=Tag.objects.all())
- choices = formField._get_choices()
- tagcount = tagcount + 2
- fields = (formField,)*tagcount
- widgets = CMSTagWidget(choices=choices, tagcount=tagcount)
- super(CMSTagFields, self).__init__(fields, widget=widgets, required=False)
-
def compress(self, data_list):
- return data_list
-
def __init__(self, tagcount=0):
Selects from a model are handled by ModelChoiceField. One of the weird bits about this is that while ModelChoiceField normally defaults to using a Select widget, we still have to tell MultiValueField that it needs to use Select widgets for each of them. I don’t know why we need to tell it to use what it would have normally used anyway (outside of a MultiValueField) but if we don’t, we just get a text input field.
So, I’ve subclassed MultiWidget as well:
[toggle code]
-
class CMSTagWidget(forms.MultiWidget):
-
def __init__(self, choices=(), tagcount=0):
- widgets = (forms.Select(choices=choices),)*tagcount
- super(CMSTagWidget, self).__init__(widgets)
-
def decompress(self, value):
- return value
-
def __init__(self, choices=(), tagcount=0):
The “compress” and “decompress” methods are used for returning the data in different formats if necessary. For example, probably the canonical use of MultiValueField is for a combination date and time entry. It will be displayed on the form as two fields: one for the date, and one for the time. But the “compress” method will take both of those values and return a timestamp or a SQL datetime.
The “decompress” method (I think) is the opposite: it takes the single value and returns the multiple values that make it up. For example, taking a SQL datetime and returning the date and the time that each need to be displayed on the form.
In our case, we don’t want to do any compression. If there’s a list of tags, we want that list of tags back. However, those methods are required, so in the method I just return the value back.
Creating indefinite selects
We still have the problem that even MultiValueField expects a known number of values. But the number of values for an edit_inline model is not known.
The form model stores form fields in a dictionary called “fields”. We can insert extra form fields into that dictionary. I added this method to the CMSForm model:
[toggle code]
-
- #add keywords choices
-
def initialize_tags(self, method=None):
- tagcount = 0
-
if self.page:
- taglist = Tag.objects.filter(pagekey__page=self.page).values('id')
-
if taglist:
-
if method == "POST":
- tagcount = len(taglist)
-
else:
- #if the form has not been submitted, we need to fake it
-
for tag in taglist:
- tagkey = "keywords_" + str(tagcount)
- self.data[tagkey] = tag['id']
- tagcount = tagcount + 1
-
if method == "POST":
- self.fields['keywords'] = CMSTagFields(tagcount)
If you’re following along at home, remember to uncomment the initialize_tags call in CMSForm’s “__init__” method.
The tricky bit here is that not only are we adding a MultiValueField called “keywords” to the form, we also need to get the tag IDs in so that each select menu defaults to the correct tag. In a MultiValueField, the field names are the base field name, with an underscore and number appended to it. For example, since I call this one keywords, if there are three keyword select menus they will be keywords_0, keywords_1, and keywords_2.
Just as the list of form fields are in a dictionary called “fields”, the form data is in a dictionary called “data”. We can add our own fake data into it. There’s no need to add fake data in if the form was just submitted—Django does that for us.
There’s probably a better way of doing this with “initial” data, but I couldn’t get that to work.
Using indefinite selects
Using the results of MultiValueField is easier than making the fields. The clean_data method on a form object doesn’t just contain the IDs of the tags, it contains the actual Tag model records. This means we can loop through what the form gives us and compare against what the page actually has.
[toggle code]
-
-
def save_tags(self):
- keywords = self.cleanText('keywords')
- prev_tags = Tag.objects.filter(pagekey__page=self.page)
- #what tags need to be removed?
-
for tag in prev_tags:
-
if not tag in keywords:
- pagekey = PageKey.objects.get(page=self.page, keyword=tag)
- pagekey.delete()
-
if not tag in keywords:
- #what tags need to be added?
-
for tag in keywords:
-
if tag and not tag in prev_tags:
- PageKey.objects.create(page=self.page, keyword=tag)
-
if tag and not tag in prev_tags:
-
def save_tags(self):
Again, if you’re using this on your own models, don’t forget to uncomment the save_tags call in CMSForm’s “post_changes” method.
- December 7, 2007: MultiWidgets and templates in Django
-
MultiValueFields and MultiWidgets are easy to use for making simple combined inputs such as two adjacent text boxes, but what if the data needs more than just multiple inputs strung together? Text, for example, will often be necessary to describe the function of each input.
Templates can be used for custom inputs just as they’re used everywhere else in Django. In a MultiWidget, override the format_output method to provide the HTML. The method receives an array of the rendered inputs in the same order that they were specified when created.
Here’s a simple example that I used to create a changelog:
[toggle code]
-
class CMSChangeWidget(forms.MultiWidget):
-
def __init__(self):
- widgets = (forms.HiddenInput(), forms.TextInput(), forms.Textarea())
- super(CMSChangeWidget, self).__init__(widgets)
-
def decompress(self, value):
- return value
-
def format_output(self, rendered_widgets):
- widgetContext = {'ID': rendered_widgets[0], 'title': rendered_widgets[1], 'summary': rendered_widgets[2]}
- return render_to_string("edit/parts/changelog.html", widgetContext)
-
def __init__(self):
This widget will have a hidden field for the changelog entry’s ID, an input of type “text” for the title of the change, and a textarea for a description of the change.
It can be used in a MultiValueField like this:
[toggle code]
-
class CMSChangeFields(forms.MultiValueField):
-
def __init__(self):
- fields=(forms.CharField(max_length=4, label="ID"), forms.CharField(max_length=48, label="Change"), forms.CharField(max_length=400, label="Summary"))
- widgets = CMSChangeWidget()
- super(CMSChangeFields, self).__init__(fields, widget=widgets, required=False)
-
def compress(self, data_list):
- return data_list
-
def __init__(self):
Don’t forget to include “from django.template.loader import render_to_string” to get the render_to_string function from Django.
-
class CMSChangeWidget(forms.MultiWidget):
- Django
- “Django is a high-level Python Web framework that encourages rapid development and clean, pragmatic design.” Oh, the sweet smell of pragmatism.
More Django
- Converting an existing Django model to Django-MPTT
- Using a SQL database to mimic a filesystem will, eventually, create bottlenecks when it comes to traversing the filesystem. One solution is modified preordered tree traversal, which saves the tree structure in an easily-used manner inside the model.
- Two search bookmarklets for Django
- Bookmarklets—JavaScript code in a bookmark—can make working with big Django databases much easier.
- Fixing Django’s feed generator without hacking Django
- It looks like it’s going to be a while before the RSS feed generator in Django is going to get fixed, so I looked into subclassing as a way of getting a working guid in my Django RSS feeds.
- ModelForms and FormViews
- This is just a notice because when I did a search, nothing came up. Don’t use ModelForm with FormView, use UpdateView instead.
- Django: fix_ampersands and abbreviations
- The fix_ampersands filter will miss some cases where ampersands need to be replaced.
- 29 more pages with the topic Django, and other related pages
More Python
- Quick-and-dirty old-school island script
- Here’s a Python-based island generator using the tables from the Judges Guild Island Book 1.
- Astounding Scripts on Monterey
- Monterey removes Python 2, which means that you’ll need to replace it if you’re still using any Python 2 scripts; there’s also a minor change with Layer Windows and GraphicConverter.
- Goodreads: What books did I read last week and last month?
- I occasionally want to look in Goodreads for what I read last month or last week, and that currently means sorting by date read and counting down to the beginning and end of the period in question. This Python script will do that search on an exported Goodreads csv file.
- Test classes and objects in python
- One of the advantages of object-oriented programming is that objects can masquerade as each other.
- Timeout class with retry in Python
- In Paramiko’s ssh client, timeouts don’t seem to work; a signal can handle this—and then can also perform a retry.
- 30 more pages with the topic Python, and other related pages