Using djangos newforms

If you start a django-app just now (January 2007) you are in a dilemma since using the old form engine is discouraged:

If you're starting from scratch, we strongly encourage you not to waste your time learning this

But then, the documentation of new form engine, called "newforms", isn't complete at all.

You may argue «take a framework above 1.0», and you'd probably be right. As I wanted to stick with django, I implemented form handling the hard way by figuring out how to use newforms.

Anyway, as I finally got formhandling working with newforms, I thought I'd share this with you, my fellow readers..

The following code..

screenshot of the form
this examples' form after validating user input
  • ..displays a form
  • ..validates user input
  • ..shows validation errors
  • ..writes the user data into the database


Update: Newforms has been added to Django 0.96 and the api has itself proved to be quite stable. The following examples work out of the box with Django 0.96 so you don't need the patches I mentioned before.

Model

PYTHON:
  1. class Entry(models.Model):
  2.     title = models.CharField("Titel", maxlength=100)
  3.     detail = models.TextField()
  4.     owner = models.ForeignKey(User, verbose_name="Verfasser", editable=False)
  5.     added = models.DateTimeField("hinzugefügt am", editable=False)
  6.     changed = models.DateTimeField("geändert am", editable=False)
  7.     public = models.BooleanField("Öffentlich")
  8.     category = models.ForeignKey(Category, verbose_name="Kategorie")
  9.  
  10.     def save(self):
  11.         if not self.id:
  12.             self.added = datetime.date.today()
  13.         self.changed = datetime.datetime.today()
  14.         return super(Entry, self).save()

Note that with the editable parameter I define which fields can be edited

Create

PYTHON:
  1. from django import newforms as forms
  2. from django.newforms import widgets
  3.  
  4. def add_entry(request):
  5.     EntryForm = forms.models.form_for_model(Entry)
  6.     EntryForm.base_fields['detail'].widget = TinyMCE()
  7.  
  8.     if request.method == 'POST':
  9.         form = EntryForm(request.POST)
  10.         if form.is_valid():
  11.             entry = form.save(commit=False)
  12.             entry.owner = request.user
  13.             entry.save()
  14.             return HttpResponseRedirect("/")
  15.  
  16.     else:
  17.         form = EntryForm()
  18.  
  19.     t = loader.get_template('add_entry.html')
  20.  
  21.     c = Context({
  22.         'form': form,
  23.         'html_head': '<script src="/media/tiny_mce/tiny_mce_src.js" type="text/javascript"></script>'
  24.     })
  25.     return HttpResponse(t.render(c))

This code displays the form if called by GET, otherwise its inserting the data (that is: creates an entry) in the database.

A few explanations:

  • line 5: the model is converted into a form class by calling forms.model.form_for_model.
  • line 6: I explicitely want to turn the detail form field into a richtext field (the widget code is taken from here). In future version, there might be the possibility to specify the widget in the model by setting a form_widget= argument.
  • line 11: to alter/extend the data before writing it into the db, use form.clean_data use commit=False and alter the returned model instance and save this instance into the db.
    form.save() returns a model instance also without setting commit to False, this comes in handy if you want to access the just generated primary key.

The rest should be very straightforward.

Update

PYTHON:
  1. def edit_entry(request, id=None):
  2.     entry = Entry.objects.get(id=id)
  3.     EntryForm = forms.models.form_for_instance(entry)
  4.     EntryForm.fields['detail'].widget = TinyMCE()
  5.  
  6.     if request.method == 'POST':
  7.         form = EntryForm(request.POST)
  8.         if form.is_valid():
  9.             form.save()
  10.             return HttpResponseRedirect("/")
  11.     else:
  12.         form = EntryForm()
  13.  
  14.     t = loader.get_template('add_entry.html')
  15.  
  16.     c = Context({
  17.         'form': form,
  18.         'html_head': '<script src="/media/tiny_mce/tiny_mce_src.js" type="text/javascript"></script>'
  19.     })
  20.     return HttpResponse(t.render(c))

Notice that the only differences to the create view are in line 2/3 (form_for_model becomes form_for_instance) and in omitting the detour via commit=False (since we don't want to set the owner when updating the entry).
Since that's not DRY at all, lets merge those two functions into one.

Create and Update together freehand without a net

PYTHON:
  1. def add_edit_entry(request, id=None):
  2.     if id is None:
  3.         EntryForm = forms.models.form_for_model(Entry)
  4.     else:
  5.         entry = Entry.objects.get(id=id)
  6.         EntryForm = forms.models.form_for_instance(entry)
  7.  
  8.     EntryForm.fields['detail'].widget = TinyMCE()
  9.  
  10.     if request.method == 'POST':
  11.         form = EntryForm(request.POST)
  12.         if form.is_valid():
  13.             entry = form.save(commit=False)
  14.             if id is None:
  15.                 entry.owner = request.user
  16.             entry.save()
  17.  
  18.             return HttpResponseRedirect("/")
  19.     else:
  20.         form = EntryForm()
  21.  
  22.     t = loader.get_template('add_entry.html')
  23.  
  24.     c = Context({
  25.         'form': form,
  26.         'html_head': '<script src="/media/tiny_mce/tiny_mce_src.js" type="text/javascript"></script>'
  27.     })
  28.     return HttpResponse(t.render(c))

That's it. This code is all you need to display an empty form, a form populated with the data from the db, validating user input, displaying an error page and writing the user input into db. And furthermore you have complete control over the whole process so you could add a bell or whistle here and there as I did with setting the owner of an entry.

I bet this gives an idea how powerful and yet simple newforms is. I'm looking forward to the implementation in Django 1.0!

P.S. For completeness, the template that displays the form:

CODE:
  1. {% extends "base.html" %}
  2.  
  3. {% block content %}
  4. <form action="." method="post">
  5.   <table class="form">
  6.     {{ form }}
  7.   </table>
  8.   <input type="submit" value="speichern" />
  9. </form>
  10. {% endblock %}

63 Comments

  1. falcon Says:

    May I suggest a screen shot of the generated form as well.

  2. philipp.keller Says:

    added a screenshot

  3. Jeff Bauer Says:

    In your "Update" source, you need to insert this before line 2:

    entry = Entry.objects.get(id=id)

  4. philipp.keller Says:

    thanks, updated the code.

  5. Robert Says:

    Hello,

    as for form.create() please take
    a look at:

    http://code.djangoproject.com/changeset/4299

    Great article !

  6. rezzrovv Says:

    Thanks for this. It is a good starting point. Curious why you don't use auto_now_add and auto_now for the added and changed fields versus overriding the save method?

  7. philipp.keller Says:

    rezzrow: I avoided using auto_now_add and auto_now because they seem a little voodoo to me, see http://www.b-list.org/weblog/2006/11/02/django-tips-auto-populated-fields#c2650

    I came to the same conclusion while skimming through the django code: there are tons of if not auto_now_add. To me this look like
    1) this feature has many potential bugs
    2) this feature will probably be removed in future releases

  8. Nesta Says:

    Hello,

    Nit picking but you can rewrite these two lines

    >> entry = request.POST.copy()
    >> form = EntryForm(entry)

    as

    >> form = EntryForm(request.POST)

    See http://tinyurl.com/y3r3ku

  9. philipp.keller Says:

    Robert, Nesta: incorporated your suggestions. Thanks a lot.

  10. Jon Colverson Says:

    Hello. Thanks for this article; it's extremely useful.

    I was wondering, is there a reason why the line:

    EntryForm = forms.models.form_for_model(Entry)

    is inside the view function instead of outside? It seems to work just as well, and I presume that having it outside causes the Form to be built only once, instead of on every request.

  11. Ramdas Says:

    Sorry. Please delete above comment

    My question is the SVN patch you have written, has that been added to the main trunk.

    We are on 4306 now.....

  12. philipp.keller Says:

    Jon Covelson: I'm fairly new to python so I don't know if the call was outside, the class would be built only once and not for every request.
    But now thinking about this your argument makes sense.
    But I'm curious to know..! Do you have any references?

  13. philipp.keller Says:

    Ramdas: No, the patches are not in the main trunk yet. I found out that this two defects my patch is fixing have already been posted. I've changed the disclaimer-paragraph and added the links to the two corresponding tickets.

  14. Malcolm Says:

    Thanks for the good tutorial,

    I've stopped working on some Django projects focused on forms since the switch, but looks like I'll be able to get back to work now thanks to this tutorial.

  15. Paul Egges Says:

    Stupid question probably, but were do
    Category and User get defined?

  16. philipp.keller Says:

    Paul: you're right, I left out some details to make this tutorial focus on the usage of newforms.

    User is a model of the authentiaction-application (part of django)
    Category is a model I defined myself.

    Here's some of the missing code:

    from django.contrib.auth.models import User
    
    class Category(models.Model):
        name = models.CharField(maxlength=20)
        description = models.TextField()
        comments_enabled = models.BooleanField()
    
        def __str__(self):
            return self.name
    
  17. Paul Egges Says:

    I resolved my prior problems. Now if my form would just not come up blank.

    Do you think you could add the "add_entry.html" template to the text above.

    Thanks.

  18. philipp.keller Says:

    add_entry.html

    {% extends "base.html" %}
    
    {% block content %}
    <form action="." method="post">
      <table class="form">
        {{ form }}
      </table>
      <input type="submit" value="speichern" />
    </form>
    {% endblock %}
    
  19. Henrik Says:

    Hi, and thanks for this tutorial!

    I have a problem with the characters Å, Ä and Ö. When form.save() is executed and I've entered "Ä" in a field, I get an error saying:

    "UnicodeEncodeError, 'ascii' codec can't encode character u'\xc4' in position 0: ordinal not in range(128)"
    in ...site-packages\django\db\backends\util.py in execute, line 12

    I've set Django setting LANGUAGE_CODE = 'se_sv' (swedish) and added a file to python site-packages directory called "sitecustomize.py" which contains:

    import sys
    sys.setdefaultencoding('iso-8859-1')

    You have Ä and Ö in german too, right? What happens on your site if you enter Ä in the title field?

  20. philipp.keller Says:

    Henrik: I've switched to utf-8 so I didn't run into this problems at all.
    I've set sys.setdefaultencoding("utf-8") in sitecustomize.py (although I don't know if this is needed).
    I've got LANGUAGE_CODE = 'de-ch' and then I needed to patch django with ticket 1356 in order to save Umlauts into db.

    Have you sorted out if this is a problem with newforms or a problem with django?
    does the following code snippet work:

    instance = MyModel(title=u'characters Å, Ä and Ö')
    instance.save()
    
  21. Henrik Says:

    I changed encoding to utf-8, but I'm using postgreSQL so I can't apply the patch you are referring to. I also tried the testcode you suggested and that gives me the same error as before.

    I tried to use the Poll app. from the django tutorial and save a poll with question="ÅÄÖ" and that worked through the shell. But when using the same code through the webpage gives the "ascii codec can't encode"-error.

  22. philipp.keller Says:

    I don't know PostgreSQL that well but that sounds like a encoding problem of your particular table.
    But maybe you're better of asking in the mailing list..

  23. Henrik Says:

    I'll do that. Anyway, thanks for your efforts!

  24. Henrik Says:

    Hi again! I solved it, thanks to some threads on Google Django group. This is not a very satisfying solution, but it works. I have a form called f where the user enters a name. When the form is processed, I first do this:

    f.clean_data['name'] = f.clean_data['name'].encode('utf-8')

    Then I can execute f.save(), using the newforms library. Sorry for spamming here, I'll use the Google group in the future! :)

  25. robotify Says:

    So, your example does a good job of making the distinction between using "form_for_model" and "form_for_instance", but I'm having a hard time seeing how form.save() manages to correctly edit an entry instead of creating a new one. Care to elaborate? Is the objects 'id' getting passed through the form somehow?

  26. Quentin Stafford-Fraser Says:

    Any suggestions on the best way to implement the following, anyone?

    I have some field which I want people using the admin interface to be able to edit (because they're 'power users') but not people using forms in my normal user-facing pages.

    In a way, I'd like to be able to set 'editable' off for certain fields in certain views.

    (Thanks for a great page, Philipp!)

  27. Amit Chakradeo Says:

    Thanks for the article. I was struggling with the newforms for some time. This clears a lot of doubts...

  28. philipp.keller Says:

    Quentin: You can take part in the discussion on exactly that objection in a in the django-developers mailing list. I likt your idea "to be able to set 'editable' off for certain fields in certain views."

  29. philipp.keller Says:

    robotify:
    EntryForm = forms.models.form_for_instance(entry)
    takes the instance 'entry' and creates a new Class with the save-method defined as a closure, bound to the instance.

    Am I right that you're uneasy with the idea of a Class bound to a certain instance. As I am new to python, this makes me a bit uneasy but I guess that's the power of closures..

  30. Brooks Says:

    I'm currently trying to use newforms to construct a view that creates/edits two models via one page, using form prefixes and form_for_(model|instance) to create my form objects. The problem I'm having is that one of the models I'm trying to create/edit has a
    +OneToOne relationship with the other, and I can't find a way to assign the proper id to a specific object when it's created. Anybody have an idea? What's already been here has been a great help so far, so thank you.

  31. philipp.keller Says:

    Brooks:
    Am I right to understand that what you're trying to do is call save() on the first instance, get its id and set the foreign-key-field on the other instance by form2.clean_data['foreign_key'] = first_id?
    Which part of this process causes problems? Accessing the id which is generated by the save() call?

  32. Timm Says:

    Hi Philipp,

    that is a really nice tutorial for people who have already some basic Django skills!

    Thanks for that, and I'm looking forward to more tutorials from you ;)

    BTW: Maybe you should add you blog to the Django Community Aggregator.

    Dankeschön ;-)

  33. James M Says:

    This is probably one of those things which changed recently. This line:

    TicketForm.fields['description'].widget = TinyMCE()

    should be changed to this:

    TicketForm.base_fields['description'].widget = TinyMCE()

    Also, you can specify a dict of attributes for the widget:

    TicketForm.base_fields['description'].widget = TinyMCE({'rows':'10', 'cols':'40'})

  34. James M Says:

    Oops, names changed to reflect my project but otherwise it's the same code...

  35. Eric Says:

    I find it interesting that the docs currently say
    """
    If you have a bound Form instance and want to change the data somehow, or if you want to bind an unbound Form instance to some data, create another Form instance. There is no way to change data in a Form instance. Once a Form instance has been created, you should consider its data immutable, whether it has data or not.
    """

    But in your example above, you are assigning the user id from the request to the 'owner' field in the clean_data:

    """
    if request.method == 'POST':
    form = EntryForm(request.POST)
    if form.is_valid():
    form.clean_data['owner'] = request.user.id
    form.save()
    """

    Do you have any idea why Adrian writes that the form data should be considered immutable?

  36. philipp.keller Says:

    Eric: I don't know the exact reason why Adrian considers the forms as immutable.
    The only explanation I can think of is that it is sort of cleaner that way. The way I changed the data by manipulating clean_data feels kind of hackish to me..

  37. Mark Says:

    To prevent a field from showing ahead of the decision on what happens with "editable=False", you can set the field's widget to HiddenInput:

    from django.newforms.widgets import HiddenInput

    EntryForm.base_fields['detail'].widget = HiddenInput()

    This will still show up in the HTML output as .

  38. philipp.keller Says:

    James: Thanks a lot! I altered the tutorial. You readers are helping this tutorial being up to date..
    Mark: Thanks for the tip. Btw: The editable=False feature is already decided to be implemented and will hopefully be included in the trunk SVN version in a week or so.

  39. Brooks Says:

    Yeah, Phillip, that's pretty much it. What I can't seem to figure out is how to retrieve the id; also, when I try to assign the id using the syntax you gave, it tells me that the POST data is not appendable. And, yes, I'm using request.POST.copy(). Any help you can give would be much appreciated.

  40. philipp.keller Says:

    Eric: I just noticed, the "hack" with clean_data no longer works. Having your comment in mind, I altered the source. Search for the line containing commit=False. I don't know if this feature will stay but I think it's more elegant this way.

    Brooks: I just noticed that form.save() returns the saved model instance, this should solve your problem.

  41. Jay States Says:

    I would just like to say thanks for you blog. This helped me with newforms... Django kicks ass!

  42. Jay States Says:

    I do have one question. How do you set default values for the forms?

    joker = models.CharField(maxlength=180, default="if not applicable skip") does not work

    Thanks.

  43. Ciantic Says:

    I was wondering has anyone idea will this change principle "admin is not your app"? It already starts to look like it could some day be your app ;) Or at least "add_edit_entry is your app", in future?

    With power of newforms Django could create two different admins, automatically, one that is limited by newforms definitions and add_edit_entry and one admin that allows you to do stupid things too. I'm not personally very fond of the idea creating "save and add another", "save and continue editing", "Save", places to POST the edited data and such to each of your applications, as Djangos own admin has implemented already such basic features, and it comes so damn close to be working as app itself.

  44. Dorian Says:

    Thanx for the examples, they helped realy!
    Is it possible to simple skip a formfield?
    I mean like MyForm.base_fields['myfield'].widget = HiddenImput()
    but *realy* have the form not rendered as html and still keep it in the Form for later overrides ?

  45. Michael Samoylov Says:

    how can implement form for related model? with _all_ fields
    class Consumer(models.Model):
    """ Consumer model (extends User model from django auth framework) """
    user = models.ForeignKey(User, editable=False)
    country = models.ForeignKey(Country, help_text=_('Consumer country'))
    phone_number = models.PhoneNumberField(_('Phone number'), help_text=_('Consumer phone number'), blank=True, null=True)
    phone_ext = models.IntegerField(_('Ext. phone number'), help_text=_('Consumer ext. phone number'), blank=True, null=True)
    zip_code = models.IntegerField(_('ZIP'), help_text=_('Consumer ZIP code'))
    contact_by_email = models.BooleanField(_('Contact by email'), help_text=_('Can be contacted by email'))
    contact_by_phone = models.BooleanField(_('Contact by phone'), help_text=_('Can be contacted by phone'))
    newsletter_subscription = models.BooleanField(_('Newsletter subscription'), help_text=_('Would like to recieve newsletter'))

  46. Brooks Says:

    Philipp, I guess I don't get what you're saying. How would I reference the id from form.save(), and assign it to a new key in the request.POST.copy(), so that it can be used in a second form in the same view to provide a foreign key for the second form.

  47. philipp.keller Says:

    Dorian: I don't understand your question. You can omit the fields with editable=false and the mentioned patch. What do you want more?

    Michael: I don't get your question. What is wrong when you generate the form with form_by_model(Consumer)?

    Brooks: I thought of something like this (I didn't try it though.. go sure that model1 and model2 don't have fields with the same name)

    Form = form_for_model(Model1)
    form = Form(request.POST)
    model1Instance = form.save()
    Form2 = form_for_model(Model2WithForeignkeyToModel1)
    form2 = Form2(request.POST)
    model2Instance = form2.save(commit=false)
    model2Instance.foreign_key_to_model_1 = model1Instance.id
    model2Instance.save()
    
  48. philipp.keller Says:

    Jay: I think it is not possible to define a default value in the model. But you can alter the formfield after you generated the form by form_by_model similar to the way I altered the widget of one of the fields. (search the newform documentation for "initial")

  49. Mark Says:

    Hmmm. I'm trying to set an initial value on a form_from_model field that I'm trying to hide with editable=False (using the patch from ticket 3247). I can't seem to find a way to do this - I just get a key error. Anyone managed this yet?

    For example, I need to set a foreign key related to the user, but don't want the user to see the field.

  50. philipp.keller Says:

    Mark: would it be possible to do it "the other way 'round": not via the "invisible field" but just before saving into the database with the "commit=false" trick? Or is the information you're trying to save to the db just available when building the form?

  51. Alexxajk Says:

    Hello, my name is Alex, i'm a newbie here. I really do like your resource and really interested in things you discuss here, also would like to enter your community, hope it is possible:-) Cya around, best regards, Alex!

  52. Mark Says:

    Philipp: that commit=False thing looks ideal, although it feels a little kludgey. Without any prior experience of the code, I was thinking that it would be good to have a separate create() method that creates but doesn't save the object.

    So long as is_valid() only validates the supplied fields, we could then create a form with a subset of the model fields, get those validated, create an object, fill in the missing bits, then save it to the db.

    It sounds like this commit property does the same job. I'll look into that - thanks.

    One concern with all this fluidity is that I'm going to have a load of code with interim fixes and workarounds applied at different levels, and will need to go through and repair it all later. I suppose that can't be helped...

  53. Andrew Says:

    Great tutorial. What is going to happen if form is not valid? I think that part is missing.

  54. philipp.keller Says:

    Andrew: if the form isn't valid, the entry isn't saved in the db but the form gets drawn again by t.render(c). If a nonvalidating form is rendered, the validation errors are printed inside the form, see official documentation

    Mark: exactly: commit=False omits the save() call

  55. Christian Joergensen Says:

    In the current version of newforms, I think 'base_fields' is renamed to just 'fields'.

  56. Jay States Says:

    Philipp:

    How about rendering password input with your widget. Something like - EntryForm.base_fields['password'].widget = PasswordInput()??

    Thanks in advance,
    J

  57. Baum Says:

    A Question related to Michael Samoylov:
    How can I create a form for Models like
    class Father:
    name = CharField(maxlength=66)
    son = ForeignKey(Son)

    I know its possible to set editable = False but then I won't have any input-widget for the Son! I want a form to create both: Father and son

  58. Jens Diemer Says:

    In the model class you should use gettext -> _("blabla")

    You have posted the template in the comment 18. It's better you put this in the page above ;)

  59. philipp.keller Says:

    Jens: Thanks for the hint. I put the template into the article. About gettext: As I don't plan to i18n my web application, I won't use gettext. Additionally: adding gettext to the examples would just add more complexity..

    Jay, Baum: Please pose this kind of questions in django-users: http://groups.google.com/group/django-users

    Christian: I just updated to the latest version and my code still seems to work.. are you sure?

  60. Ghostwheel Says:

    Nice tutorial. I'm just starting with Django. Can you please show me what the urlconf looks like for this?

    thanks

  61. Joe Says:

  62. sean Says:

    type object 'ProductForm' has no attribute 'fields'
    why this is happening?

  63. john Says:

    @ sean,

    make sure you inherit from admin.ModelAdmin

    class FileAdmin:
    pass # this gives your error

    class FileAdmin(admin.ModelAdmin):
    pass

Leave a Reply