Swizec Teller - a geek with a hatswizec.com

Senior Mindset Book

Get promoted, earn a bigger salary, work for top companies

Senior Engineer Mindset cover
Learn more

    Django protip #2: Forms are awesome

    Welcome to another installment of Swizec's Django protip. Previously we discussed a better way to structure your django apps, but nobody cared about that because everybody is rather silly. This time we'll be talking about how awesome forms are and why you should be using them for pretty much everything. At the end, I'll show you some neat tips and tricks I discovered during my Django Epiphany.

    Why forms

    When you look at code a lot of web developers produce (and yes, even I did it plenty of times back in the day) you will notice a lot of work goes into retrieving data from GET and POST parameters. Now despite most developers simply ignoring GET parameters as anything really special or dangerous and just handling them as if they were regular variables, because hey, what could go wrong about retrieving a page number right? At best you'll see code having a bunch of lines sort of like this:

    $page = (isset($_GET['page'])) ? intval($_GET['page']) : 0;
    

    Anyone notice a security flaw? Then think of this, what happens if the page is set to -1? Sure, if errors aren't being displayed right to the user nothing too important. But if they get shown an SQL error ... or worse ...

    However people are usually at least a little bit more careful about POST data because they realise that hey, this is something a person filled in a web form and perhaps the data should go through a series of a little bit more stringent tests before it gets chucked into the database. Hell, maybe we could even tell the user where they screwed up and if the planets are in constelation, why not also make sure those required fields are actually filled out ... you know, so we don't get any weird inconsistencies in our database.

    But still, it's a lot of work to do all of that by hand every god damn time. If only there were something easier, more transparent and plain old automagical ...

    Cue Django Forms

    In django, all of this comes automagically. There is this thing called a "form", which basically lets you define what parameters a request needs, be it GET or POST based, what they should validate against and most importantly, what's required and what is not.

    If you're into that sort of stuff you even get building the form in a html, so the users can use it, completely for free and magically with all the required "Hey bozo, you filled so and so field wrong. Fix it!". For every field! Magic.

    But since most of my time is spent on developing API's rather than user-facing websites let me show you how to use forms effectively in that sort of environment. For the html stuff just go check out the django form docs.

    The basic use of forms goes a little bit like this (hopefully your forms are in a separate file from your views, this is just an example :P)

    from django import forms
    
    class ListForm(forms.Form):
        feed = forms.IntegerField(required=False)
        category = forms.IntegerField(required=False)
        since = forms.DateTimeField(required=False)
        count = forms.IntegerField(required=False)
        page = forms.IntegerField(required=False)
        include_content = forms.BooleanField(required=False)
    
    def list(request, format='json'):
        form = ListForm(request.user, request.GET)
    
        if form.is_valid():
           articles = Article.objects.filter(feed__in=form.cleaned_data['feeds']
                                                      ).order_by('-time')[page*count:page*count+count]
           return HttpResponse(json.dumps({'status': 'OK',
                                            'count': len(articles),
                                            'articles': articles}))
        else:
    	return HttpResponseBadRequest(json.dumps({'status': 'ERROR'}))
    

    Well something along those lines anyhow. What you can see is that I basically define a form and check that it's valid. Once I know it's valid I can go on using its cleaned_data without much regard for anything.

    Some tips&tricks

    And now let's get onto some tips&tricks :)

    First thing you'll notice once you start using forms like you properly should is that all of your views follow this pattern: get form; validate form; do something; or do something else;

    So I wrote up a descriptor for that, now I can be certain that when I'm in my view the form is valid and I can use the data.

    def form_valid(form_type, data_type):
        def inner(view_func):
            def wrapper(request, *args, **kwargs):
                form = form_type(request.__getattribute__(data_type))
    
                if form.is_valid():
                    request.form = form
                    return view_func(request, *args, **kwargs)
                else:
                    return HttpResponseBadRequest(json.dumps({'status': 'ERROR'}))
            return wraps(view_func)(wrapper)
        return inner
    
    ## the usage goes like so
    @form_valid(ImageForm, 'GET')
    def image(request):
        blob = BlobInfo.gql("WHERE filename='%s' LIMIT 1" % request.form.cleaned_data['id'])[0]
    
        return HttpResponse(BlobReader(blob.key()).read(),
                            content_type=blob.content_type)
    

    Using the decorator thus makes for much much cleaner code.

    Now let's look at some magic done with custom clean functions inside forms :P

    ## this enables us to handle <a class="zem_slink freebase/en/comma-separated_values" title="Comma-separated values" rel="wikipedia" href="http://en.wikipedia.org/wiki/Comma-separated_values">comma separated values</a> seamlessly
        def clean_feed(self):
            try:
                feed = [int(id) for id in str(self.cleaned_data['feed']).split(',')]
            except (AttributeError, ValueError, KeyError):
                feed = None
            return feed
    
    ## automagically parsing json parameters can be done too
        def clean_feeds(self):
            feeds = json.loads(self.cleaned_data['feeds'])
            if type(feeds) != list:
                raise forms.ValidationError("list of feeds expected")
            return feeds
    
    ## or how about logging in the user while we're checking the user/pass is correct
    ## don't manually log in users unless you know at least somewhat what you're doing, usually django handles this
        def clean(self):
            cleaned_data = self.cleaned_data
    
            if len(self.errors) != 0:
                return cleaned_data
    
            user = authenticate(username=cleaned_data['email'],
                                password=cleaned_data['password'])
            if user is None:
                del cleaned_data['email']
                del cleaned_data['password']
                raise forms.ValidationError('Bad login')
            else:
                cleaned_data['user'] = user
    
            return cleaned_data
    
    ## now the strangest thing, when you have to handle grabbing data by different parameters
    ##(like being given a set of feed ids, or a feed category id, you can do this by returning
    ## a QuerySet in the form's cleaned_data
    def clean(self):
            cleaned_data = self.cleaned_data
    
            if not (cleaned_data.get('feed', None) != None or cleaned_data.get('category', None) != None):
                raise forms.ValidationError("feed or category required")
    
            if cleaned_data['feed'] != None:
                user_feeds = UserFeed.objects.filter(user=self.user,
                                                     id__in=cleaned_data['feed'])
            else:
                user_feeds = UserFeed.objects.filter(user=self.user,
                                                     categories__contains="'%d'" % cleaned_data['category'])
    
            cleaned_data['feeds'] = map(lambda f: f.id,
                                        Feed.objects.filter(id__in=map(lambda f: f.feed, user_feeds)))
    
            reverse_feeds = {}
            for feed in user_feeds:
                reverse_feeds[feed.feed] = feed.id
            cleaned_data['reverse_feeds'] = reverse_feeds
    
            return cleaned_data
    

    Conclusion

    Anyhow, that's it as far as forms are concerned. Do sound off in the comments or on twitter if I fucked up somewhere. I know geeks like to argue. Come back next week when I'll be talking about different magical things you can do with decorators and why they are uber awesome to use in django (or well any other type of python development really)

    Enhanced by Zemanta
    Published on August 3rd, 2010 in Application programming interface, django, Login, RSS, SQL, Uncategorized, Website

    Did you enjoy this article?

    Continue reading about Django protip #2: Forms are awesome

    Semantically similar articles hand-picked by GPT-4

    Senior Mindset Book

    Get promoted, earn a bigger salary, work for top companies

    Learn more

    Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.

    Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.

    Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.

    Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev

    Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization

    Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections

    Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog

    Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com

    By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️

    Created by Swizec with ❤️