Forms and Views

In this article by Aidas Bendoraitis, author of the book Web Development with Django Cookbook - Second Edition we will cover the following topics:

  • Passing HttpRequest to the form
  • Utilizing the save method of the form

(For more resources related to this topic, see here.)

Introduction

When the database structure is defined in the models, we need some views to let the users enter data or show the data to the people. In this chapter, we will focus on the views managing forms, the list views, and views generating an alternative output than HTML. For the simplest examples, we will leave the creation of URL rules and templates up to you.

Passing HttpRequest to the form

The first argument of every Django view is the HttpRequest object that is usually named request. It contains metadata about the request. For example, current language code, current user, current cookies, and current session. By default, the forms that are used in the views accept the GET or POST parameters, files, initial data, and other parameters; however, not the HttpRequest object. In some cases, it is useful to additionally pass HttpRequest to the form, especially when you want to filter out the choices of form fields using the request data or handle saving something such as the current user or IP in the form.

In this recipe, we will see an example of a form where a person can choose a user and write a message for them. We will pass the HttpRequest object to the form in order to exclude the current user from the recipient choices; we don't want anybody to write a message to themselves.

Getting ready

Let's create a new app called email_messages and put it in INSTALLED_APPS in the settings. This app will have no models, just forms and views.

How to do it...

To complete this recipe, execute the following steps:

  1. Add a new forms.py file with the message form containing two fields: the recipient selection and message text. Also, this form will have an initialization method, which will accept the request object and then, modify QuerySet for the recipient's selection field:
    # email_messages/forms.py
    # -*- coding: UTF-8 -*-
    from __future__ import unicode_literals
    from django import forms
    from django.utils.translation import ugettext_lazy as _
    from django.contrib.auth.models import User
    
    class MessageForm(forms.Form):
       recipient = forms.ModelChoiceField(
           label=_("Recipient"),
           queryset=User.objects.all(),
           required=True,
       )
       message = forms.CharField(
           label=_("Message"),
           widget=forms.Textarea,
           required=True,
       )
    
       def __init__(self, request, *args, **kwargs):
           super(MessageForm, self).__init__(*args, **kwargs)
           self.request = request
           self.fields["recipient"].queryset = \
               self.fields["recipient"].queryset.\
               exclude(pk=request.user.pk)
  2. Then, create views.py with the message_to_user() view in order to handle the form. As you can see, the request object is passed as the first parameter to the form, as follows:
    # email_messages/views.py
    # -*- coding: UTF-8 -*-
    from __future__ import unicode_literals
    from django.contrib.auth.decorators import login_required
    from django.shortcuts import render, redirect
    
    from .forms import MessageForm
    
    @login_required
    def message_to_user(request):
       if request.method == "POST":
           form = MessageForm(request, data=request.POST)
           if form.is_valid():
               # do something with the form
               return redirect("message_to_user_done")
       else:
           form = MessageForm(request)
    
       return render(request,
           "email_messages/message_to_user.html",
           {"form": form}
       )

How it works...

In the initialization method, we have the self variable that represents the instance of the form itself, we also have the newly added request variable, and then we have the rest of the positional arguments (*args) and named arguments (**kwargs). We call the super() initialization method passing all the positional and named arguments to it so that the form is properly initiated. We will then assign the request variable to a new request attribute of the form for later access in other methods of the form. Then, we modify the queryset attribute of the recipient's selection field, excluding the current user from the request.

In the view, we will pass the HttpRequest object as the first argument in both situations: when the form is posted as well as when it is loaded for the first time.

See also

  • The Utilizing the save method of the form recipe

Utilizing the save method of the form

To make your views clean and simple, it is good practice to move the handling of the form data to the form itself whenever possible and makes sense. The common practice is to have a save() method that will save the data, perform search, or do some other smart actions. We will extend the form that is defined in the previous recipe with the save() method, which will send an e-mail to the selected recipient.

Getting ready

We will build upon the example that is defined in the Passing HttpRequest to the form recipe.

How to do it...

To complete this recipe, execute the following two steps:

  1. From Django, import the function in order to send an e-mail. Then, add the save() method to MessageForm. It will try to send an e-mail to the selected recipient and will fail quietly if any errors occur:
    # email_messages/forms.py
    # -*- coding: UTF-8 -*-
    from __future__ import unicode_literals
    from django import forms
    from django.utils.translation import ugettext,\
       ugettext_lazy as _
    from django.core.mail import send_mail
    from django.contrib.auth.models import User
    
    class MessageForm(forms.Form):
       recipient = forms.ModelChoiceField(
           label=_("Recipient"),
           queryset=User.objects.all(),
           required=True,
       )
       message = forms.CharField(
           label=_("Message"),
           widget=forms.Textarea,
           required=True,
       )
    
       def __init__(self, request, *args, **kwargs):
           super(MessageForm, self).__init__(*args, **kwargs)
           self.request = request
           self.fields["recipient"].queryset = \
               self.fields["recipient"].queryset.\
               exclude(pk=request.user.pk)
    
       def save(self):
           cleaned_data = self.cleaned_data
           send_mail(
               subject=ugettext("A message from %s") % \
                   self.request.user,
               message=cleaned_data["message"],
               from_email=self.request.user.email,
               recipient_list=[
                   cleaned_data["recipient"].email
               ],
               fail_silently=True,
           )
  1. Then, call the save() method from the form in the view if the posted data is valid:
    # email_messages/views.py
    # -*- coding: UTF-8 -*-
    from __future__ import unicode_literals
    from django.contrib.auth.decorators import login_required
    from django.shortcuts import render, redirect
    
    from .forms import MessageForm
    
    @login_required
    def message_to_user(request):
       if request.method == "POST":
           form = MessageForm(request, data=request.POST)
           if form.is_valid():
               form.save()
               return redirect("message_to_user_done")
       else:
           form = MessageForm(request)
    
       return render(request,
            "email_messages/message_to_user.html",
           {"form": form}
       )

How it works...

Let's take a look at the form. The save() method uses the cleaned data from the form to read the recipient's e-mail address and the message. The sender of the e-mail is the current user from the request. If the e-mail cannot be sent due to an incorrect mail server configuration or another reason, it will fail silently; that is, no error will be raised.

Now, let's look at the view. When the posted form is valid, the save() method of the form will be called and the user will be redirected to the success page.

See also

  • The Passing HttpRequest to the form recipe

Uploading images

In this recipe, we will take a look at the easiest way to handle image uploads. You will see an example of an app, where the visitors can upload images with inspirational quotes.

Getting ready

Make sure to have Pillow or PIL installed in your virtual environment or globally.

Then, let's create a quotes app and put it in INSTALLED_APPS in the settings. Then, we will add an InspirationalQuote model with three fields: the author, quote text, and picture, as follows:

# quotes/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import os
from django.db import models
from django.utils.timezone import now as timezone_now
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible

def upload_to(instance, filename):
   now = timezone_now()
   filename_base, filename_ext = os.path.splitext(filename)
   return "quotes/%s%s" % (
       now.strftime("%Y/%m/%Y%m%d%H%M%S"),
       filename_ext.lower(),
   )

@python_2_unicode_compatible
class InspirationalQuote(models.Model):
   author = models.CharField(_("Author"), max_length=200)
   quote = models.TextField(_("Quote"))
   picture = models.ImageField(_("Picture"),
       upload_to=upload_to,
       blank=True,
       null=True,
   )

   class Meta:
       verbose_name = _("Inspirational Quote")
       verbose_name_plural = _("Inspirational Quotes")

   def __str__(self):
       return self.quote

In addition, we created an upload_to function, which sets the path of the uploaded picture to be something similar to quotes/2015/04/20150424140000.png. As you can see, we use the date timestamp as the filename to ensure its uniqueness. We pass this function to the picture image field.

How to do it...

Execute these steps to complete the recipe:

  1. Create the forms.py file and put a simple model form there:
    # quotes/forms.py
    # -*- coding: UTF-8 -*-
    from __future__ import unicode_literals
    from django import forms
    from .models import InspirationalQuote
    
    class InspirationalQuoteForm(forms.ModelForm):
       class Meta:
           model = InspirationalQuote
           fields = ["author", "quote", "picture", "language"]
  1. In the views.py file, put a view that handles the form. Don't forget to pass the FILES dictionary-like object to the form. When the form is valid, trigger the save() method as follows:
    # quotes/views.py
    # -*- coding: UTF-8 -*-
    from __future__ import unicode_literals
    from django.shortcuts import redirect
    from django.shortcuts import render
    from .forms import InspirationalQuoteForm
    
    def add_quote(request):
       if request.method == "POST":
           form = InspirationalQuoteForm(
               data=request.POST,
               files=request.FILES,
           )
           if form.is_valid():
               quote = form.save()
               return redirect("add_quote_done")
       else:
           form = InspirationalQuoteForm()
       return render(request,
           "quotes/change_quote.html",
           {"form": form}
       )
  1. Lastly, create a template for the view in templates/quotes/change_quote.html. It is very important to set the enctype attribute to multipart/form-data for the HTML form, otherwise the file upload won't work:
    {# templates/quotes/change_quote.html #}
    {% extends "base.html" %}
    {% load i18n %}
    
    {% block content %}
       <form method="post" action="" enctype="multipart/form-data">
           {% csrf_token %}
           {{ form.as_p }}
           <button type="submit">{% trans "Save" %}</button>
       </form>
    {% endblock %}

How it works...

Django model forms are forms that are created from models. They provide all the fields from the model so you don't need to define them again. In the preceding example, we created a model form for the InspirationalQuote model. When we save the form, the form knows how to save each field in the database as well as upload the files and save them in the media directory.

There's more

As a bonus, we will see an example of how to generate a thumbnail out of the uploaded image. Using this technique, you could also generate several other specific versions of the image, such as the list version, mobile version, and desktop computer version.

We will add three methods to the InspirationalQuote model (quotes/models.py). They are save(), create_thumbnail(), and get_thumbnail_picture_url(). When the model is being saved, we will trigger the creation of the thumbnail. When we need to show the thumbnail in a template, we can get its URL using {{ quote.get_thumbnail_picture_url }}. The method definitions are as follows:

# quotes/models.py
# …
from PIL import Image
from django.conf import settings
from django.core.files.storage import default_storage as storage
THUMBNAIL_SIZE = getattr(
   settings,
   "QUOTES_THUMBNAIL_SIZE",
   (50, 50)
)

class InspirationalQuote(models.Model):
   # ...
   def save(self, *args, **kwargs):
       super(InspirationalQuote, self).save(*args, **kwargs)
       # generate thumbnail picture version
       self.create_thumbnail()

   def create_thumbnail(self):
       if not self.picture:
           return ""
       file_path = self.picture.name
       filename_base, filename_ext = os.path.splitext(file_path)
       thumbnail_file_path = "%s_thumbnail.jpg" % filename_base
       if storage.exists(thumbnail_file_path):
           # if thumbnail version exists, return its url path
           return "exists"
       try:
           # resize the original image and
           # return URL path of the thumbnail version
           f = storage.open(file_path, 'r')
           image = Image.open(f)
           width, height = image.size

           if width > height:
               delta = width - height
               left = int(delta/2)
               upper = 0
               right = height + left
               lower = height
           else:
               delta = height - width
               left = 0
               upper = int(delta/2)
               right = width
               lower = width + upper

           image = image.crop((left, upper, right, lower))
           image = image.resize(THUMBNAIL_SIZE, Image.ANTIALIAS)

           f_mob = storage.open(thumbnail_file_path, "w")
           image.save(f_mob, "JPEG")
           f_mob.close()
           return "success"
       except:
           return "error"

   def get_thumbnail_picture_url(self):
       if not self.picture:
           return ""
       file_path = self.picture.name
       filename_base, filename_ext = os.path.splitext(file_path)
       thumbnail_file_path = "%s_thumbnail.jpg" % filename_base
       if storage.exists(thumbnail_file_path):
           # if thumbnail version exists, return its URL path
           return storage.url(thumbnail_file_path)
       # return original as a fallback
       return self.picture.url

In the preceding methods, we are using the file storage API instead of directly juggling the filesystem, as we could then exchange the default storage with Amazon S3 buckets or other storage services and the methods will still work.

How does the creating the thumbnail work? If we had the original file saved as quotes/2014/04/20140424140000.png, we are checking whether the quotes/2014/04/20140424140000_thumbnail.jpg file doesn't exist and, in that case, we are opening the original image, cropping it from the center, resizing it to 50 x 50 pixels, and saving it to the storage.

The get_thumbnail_picture_url() method checks whether the thumbnail version exists in the storage and returns its URL. If the thumbnail version does not exist, the URL of the original image is returned as a fallback.

Summary

In this article, we learned about passing an HttpRequest to the form and utilizing the save method of the form.

You can find various book on Django on our website:

Resources for Article:


Further resources on this subject:


You've been reading an excerpt of:

Web Development with Django Cookbook - Second Edition

Explore Title
comments powered by Disqus