$30 off During Our Annual Pro Sale. View Details »

Views can be Classy

Kenneth Love
September 04, 2012

Views can be Classy

The slides from my DjangoCon 2012 talk about class-based views.

Kenneth Love

September 04, 2012
Tweet

More Decks by Kenneth Love

Other Decks in Programming

Transcript

  1. VIEWS CAN
    BE CLASSY!
    1
    Tuesday, September 4, 12

    View Slide

  2. WHO AM I?
    ➡ Kenneth Love
    ➡ @kennethlove
    ➡ __love__
    ➡ brack3t.com
    2
    Tuesday, September 4, 12
    Aside from my social media dominance, what makes me a good person to talk about CBVs?
    I've been writing CBVs, and pretty much *only* CBVs, for a giant CMS contract for more than a
    year.

    View Slide

  3. CLASS-BASED
    VIEWS ARE
    BAD, RIGHT?
    3
    Tuesday, September 4, 12

    View Slide

  4. THE BAD
    STUFF
    ➡ By default, decorators are
    wrapped around dispatch(), the
    view in urls.py, or an extra
    variable (We'll solve this in a
    minute or two).
    4
    Tuesday, September 4, 12
    So, what are some trouble areas?
    The first that most people run into is that it's awkward to decorate a CBV.

    View Slide

  5. THE BAD
    STUFF
    ➡ Inheritance chains makes code
    more concise, which means
    new-to-the-project developers
    have more domain knowledge to
    learn. Solve this with
    documentation.
    5
    Tuesday, September 4, 12
    The next issue is one that gets raised a lot but I actually find to have a simple solution. Since
    you've abstracted out your common bits into mixins, there's a lot of code that's "invisible".
    This is especially troublesome for people new to your code base. Really, though, I see this as
    a problem of investment (both for current and new employees) and documentation. This isn't
    an unfixable or unsurmountable problem.

    View Slide

  6. ➡ Combining mixins and views
    sometimes results in Method
    Resolution Order exceptions.
    ➡ Definitely more going on behind
    the scenes than with function-
    based views.
    THE BAD
    STUFF
    6
    Tuesday, September 4, 12
    Method Resolution Order is one place that you can really be bit in the butt. *BUT*, I've only
    had it show up once or twice in over a year of writing nothing but CBVs, so it's not amazingly
    common.
    And, yes, there is more going on than meets the eye, but, again, documentation and the
    creation of common workflows/solutions helps to mitigate this.

    View Slide

  7. PYTHON HAS
    THE SCARIEST
    TLAS
    7
    Tuesday, September 4, 12
    Besides having a great community and fun project names, Python is cursed with some scary
    three letter acronyms.
    PIL, GIL, and...

    View Slide

  8. MRO
    ➡ Method Resolution Order
    ➡ Luckily, fairly rare
    ➡ Often caused by inheriting from
    2+ classes that extend the same
    classes, but in different orders
    ➡ More info: http://bit.ly/PythonMRO
    8
    Tuesday, September 4, 12
    Method Resolution Order, as mentioned, is scary and definitely a potential stumbling block
    when building CBVs.
    The easiest way, I've found, to avoid MRO problems is to avoid mixing classes that define/
    extend the same methods but call them in different orders. I usually start my mixins off by
    extending `object` and not another view/mixin so I don't have to worry as much about this
    possibility.
    For more information, you can search the internet for "Python MRO" and you'll get a great
    article on python.org explaining the issue and ways to work around it.

    View Slide

  9. NCC-1701
    ➡ Something being enterprise-ish
    doesn't make it bad.
    ➡ In small doses, enterprise makes
    things quicker to build and use.
    9
    Tuesday, September 4, 12
    Yes, using more classes is more enterprise-y, but Django's use of them makes a lot of sense.
    It doesn't make the project more cumbersome, if done correctly, but makes it much faster to
    implement features.

    View Slide

  10. SO, WHY USE
    CBVS?
    ➡ Extra work up front pays off
    down the line.
    ➡ Keeps views.py concise.
    10
    Tuesday, September 4, 12
    If you've seen most of the conversation online about CBVs, or tried to use them yourself
    without much success, you probably think they're a lot of extra work, so why bother with
    them?
    Yes, they are more work, up front. When you're learning to use them and finding where you
    need custom mixins, it seems like you don't make much progress. The real power comes
    later, though, when you've created those needed mixins and gotten the workflow down. What
    used to take hours of copy/paste or retyping now becomes a few minutes of implementing
    the same mixins and base classes and changing variables to match.

    View Slide

  11. EVERYTHING*
    IS A CLASS
    11
    Tuesday, September 4, 12
    Make a proposition: everything substantial in Django is a class (for some definition of the
    word).

    View Slide

  12. MODELS
    class Foo(models.Model):
    classy = models.BooleanField(
    default=True
    )
    12
    Tuesday, September 4, 12
    Models are obviously a class. We all think nothing of doing multi-table inheritance or abstract
    base classes for models.

    View Slide

  13. FORMS
    class FooForm(forms.ModelForm):
    class Meta:
    fields = ["foo"]
    13
    Tuesday, September 4, 12
    Forms are another given. Anyone who has had to make model forms for similar models or
    with very similar functionality has created base form classes and mixins.

    View Slide

  14. TEMPLATES
    14
    Tuesday, September 4, 12

    View Slide

  15. TEMPLATES
    {% extends "layouts/base.html" %}
    {% block content %}
    Puttin' on the ritz
    {% endblock %}
    15
    Tuesday, September 4, 12
    Templates are a type of class, if you'll indulge me. They extend master templates (like MTI or
    ABCs), they have blocks (methods) that are overridden, and they can have mixins
    (templatetags) to add new functionality.

    View Slide

  16. TAO OF
    PYTHON
    “Special cases aren't special
    enough to break the rules.”
    ➡ Tim Peters
    16
    Tuesday, September 4, 12
    And since we've had this "rule" around for long time...

    View Slide

  17. VIEWS
    SHOULD BE
    CLASSES, TOO
    17
    Tuesday, September 4, 12

    View Slide

  18. BATTERIES
    INCLUDED
    You still have deadlines, right?
    18
    Tuesday, September 4, 12
    To ease you into the idea of building views as classes, Django provides some good head
    starts with basic views and mixins.

    View Slide

  19. OBJECT-
    BASED VIEWS
    SingleObjectMixin
    MultipleObjectMixin
    DetailView
    ListView
    19
    Tuesday, September 4, 12
    These views deal with single and multiple model instances.

    View Slide

  20. FORM-BASED
    VIEWS
    FormMixin
    ModelFormMixin
    FormView
    CreateView
    UpdateView
    20
    Tuesday, September 4, 12
    This group deals with handling and rendering forms and using them to create or update
    model instances.

    View Slide

  21. DATE-BASED
    VIEWS
    YearMixin
    MonthMixin
    DayMixin
    DateMixin
    ArchiveIndexView
    YearArchiveView
    MonthArchiveView
    DateDetailView
    21
    Tuesday, September 4, 12
    I'm sure some of this is becoming pretty obvious by now. These classes handle selecting
    instances based on their dates.

    View Slide

  22. UTILITY
    VIEWS
    TemplateResponseMixin
    TemplateView
    RedirectView
    View
    22
    Tuesday, September 4, 12
    And to wrap up our quick tour, these handle other common use cases like rendering a
    template, redirection, or just a standard base view that you can build your custom views on
    top of.

    View Slide

  23. COMMON
    METHODS
    dispatch
    get, post, put,
    delete
    get_context_data
    get_object
    get_queryset
    get_form_class
    get_form_kwargs
    get_success_url
    form_valid
    form_invalid
    23
    Tuesday, September 4, 12
    When working with CBVs, these methods are ones you commonly find yourself overwriting.
    Dispatch handles calling the correct method based on the HTTP verb requested. These would
    be get, post, put, or delete.
    Get_context_data fills out the kwargs passed to your template. You can override this to add in
    new keys.
    Get_object handles getting the object from the database or, more likely, the queryset.
    Get_queryset controls the generation of the query to the ORM.
    Get_form_class and get_form_kwargs handle instantiating the correct form class with the
    correct args.
    Get_success_url gets the URL to use when a submitted form is successful, and is called by:
    Form_valid, which handles what to do when a form is valid (save the instance, run a Celery
    task, etc).
    Form_invalid, lastly, handles the opposite of that, usually including re-rendering the view
    with errors.

    View Slide

  24. MOVING ON
    With thanks to Daniel Greenfeld (pydanny)
    github.com/opencomparison/opencomparison
    24
    Tuesday, September 4, 12
    So you've decided to drink my Kool-aid and you want to work on converting your views. Let's
    look at how that might work.

    View Slide

  25. def grid_detail_landscape(request, slug,
    template_name="grid/grid_detail2.html"):
    grid = get_object_or_404(
    Grid, slug=slug)
    features = grid.feature_set.all()
    grid_packages = grid.grid_packages
    elements = Element.objects.filter(
    feature__in=features,
    grid_package__in=grid_packages)
    25
    Tuesday, September 4, 12
    This is a fairly straight-forward function-based view.
    It has a template, gets an object, grabs a couple of related items off through that object, and
    then grabs another item from the ORM

    View Slide

  26. element_map = build_element_map(elements)
    default_attributes = [
    (…)
    ]
    return render(request, template_name, {
    'grid': grid,
    'features': features,
    'grid_packages': grid_packages,
    'attributes': default_attributes,
    'elements': element_map,
    })
    26
    Tuesday, September 4, 12
    Then it calls a method in the views.py file, sets up a big list (that I've omitted due to time
    constraints) and finally passes all of that through to a render method

    View Slide

  27. def build_element_map(elements):
    element_map = {}
    for element in elements:
    element_map.setdefault(
    element.feature_id, {})
    element_map[
    element.feature_id][
    element.grid_package_id
    ] = element
    return element_map
    27
    Tuesday, September 4, 12
    This, btw, is that method that was called from inside the view.

    View Slide

  28. CONVERSION
    TO CBV
    28
    Tuesday, September 4, 12
    How do we take all of that from FBV to CBV?

    View Slide

  29. class BuildElementMapMixin(object):
    def build_element_map(self,
    elements):
    element_map = {}
    for element in elements:
    element_map.setdefault(
    element.feature_id, {})
    element_map[
    element.feature_id][
    element.grid_package_id
    ] = element
    return element_map
    29
    Tuesday, September 4, 12
    First, let's rewrite that method. In the original views.py file, it was called several times, so
    making it a mixin would help us make sure it's always available to the views.

    View Slide

  30. class GridDetailView(BuildElementMapMixin,
    DetailView):
    model = Grid
    template_name = "grid/grid_detail2.html"
    def get_context_data(self, **kwargs):
    kwargs = super(GridDetailView, self
    ).get_context_data(**kwargs)
    features =
    self.object.feature_set.all()
    grid_packages =
    self.object.grid_packages
    30
    Tuesday, September 4, 12
    Now for the view itself. It's a DetailView because we're fetching a single item, which is now
    self.object.
    We set up the model & template and then dig into the context dict. We fetch the original one
    and then get our related models from our object.

    View Slide

  31. elements = Element.objects.filter(
    feature__in=features,
    grid_package__in=grid_packages)
    element_map = self.build_element_map(
    elements
    )
    default_attributes = [
    (…)
    ]
    31
    Tuesday, September 4, 12
    Then we get the extra related model and call out now-local method to build the element map.

    View Slide

  32. kwargs.update({
    'features': features,
    'grid_packages': grid_packages,
    'attributes': default_attributes,
    'elements': element_map
    })
    return kwargs
    32
    Tuesday, September 4, 12
    Finally, we update the context dict and return it. The view itself will handle passing it through
    to the template and rendering the page.

    View Slide

  33. class GridDetailView(DetailView):
    context_object_name = "grid"
    model = Grid
    CUSTOMIZING
    YOUR CBVS
    33
    Tuesday, September 4, 12
    If you don't like the names provided to generic objects, like `object`, `object_list`, or
    `form`, you can override these quite easily in your view class.
    The same goes for the template used, of course.

    View Slide

  34. ➡ Python makes no distinction
    ➡ Mixins are usually single-
    purpose
    ➡ Views might contain multiple
    mixins
    MIXINS VS
    BASE CLASSES
    34
    Tuesday, September 4, 12
    What's the difference between mixins and base classes? There isn't one.
    Usually it's a use-case distinction. Mixins hold onto one function or manipulation of data.
    They do a single job.
    Base classes, though, usually extend two or more mixins to create a unique workflow.

    View Slide

  35. ➡ You can’t just wrap a function in
    a CBV with a decorator
    ➡ Mixins often replace decorators
    MIXINS VS
    DECORATORS
    35
    Tuesday, September 4, 12
    You can't just wrap a CBV function with a decorator; the decorator needs to be transformed
    to a method decorator first.
    This preserves `self` and passes through `*args` and `**kwargs`

    View Slide

  36. CREATING
    YOUR OWN
    MIXINS
    36
    Tuesday, September 4, 12
    Let's talk about making your own mixins, because that's where CBVs really start to shine.

    View Slide

  37. class LoginRequiredMixin(object):
    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
    return super(LoginRequiredMixin,
    self).dispatch(
    *args, **kwargs)
    37
    Tuesday, September 4, 12
    This, for example, is a mixin that I use constantly.
    We'll step through it line-by-line.
    First, we extend `object` to make our mixin more generic and MRO-safe.
    We wrap `dispatch` with `login_required` by using the `method_decorator` decorator. We
    use method_decorator in order to maintain the self argument in all of our decorated
    methods.
    Then, to wrap it up, we return the super of our method with all the passed-in arguments.
    This all results in any view that extends this mixins requiring authentication before it can be
    accessed.

    View Slide

  38. class ProjectFilterMixin(object):
    def get_queryset(self):
    queryset = super(
    ProjectFilterMixin, self
    ).get_queryset()
    queryset = queryset.filter(
    project__pk= ⏎
    self.request.session["project"])
    return queryset
    38
    Tuesday, September 4, 12
    Of course, not all mixins are as simple. Let's look at one that modifies the queryset of a view.
    Again, we start with `object`.
    We're extending the `get_queryset` method so the first thing we want to do is get the default
    queryset for this view. This is controlled by the type of view (single vs. multiple), the model
    specified, and the URL. It can also be affected by the view itself further overriding the
    `get_queryset` method.
    In this mythical application and mixin, we have a session variable that holds on to the PK of
    the currently selected project. We filter the default queryset that we've inherited to find only
    those where the project's PK matches the one in our user's session, and then return that
    queryset.
    We're going to talk a bit about user-specific data a bit later, and I'm sure you can already see
    how this will come into play.

    View Slide

  39. DESIGN
    PATTERNS
    39
    Tuesday, September 4, 12
    There are some patterns to keep in mind when you're creating a mixin

    View Slide

  40. class SetHeadlineMixin(object):
    headline = None
    def get_context_data(self, **kwargs):
    kwargs = super(SetHeadlineMixin,
    self).get_context_data(**kwargs)
    kwargs.update({"headline":
    self.get_headline()})
    return kwargs
    def get_headline(self):
    if self.headline is None:
    raise ImproperlyConfigured(
    u"Missing a headline.")
    return self.headline
    40
    Tuesday, September 4, 12
    This is the SetHeadlineMixin.
    Notice how we set a default on `headline` so it's always around, even if it's not set.
    We also provide a method, prepended with `get` for setting the attribute. If a user doesn't
    provide the attribute a value and doesn't override this method, you should throw an error.
    Probably something much more explanatory than what I've used here.
    This mixin also injects the supplied headline, either from the overridden method or from the
    class attribute, into the context.

    View Slide

  41. class BlogView(SetHeadlineMixin,
    DetailView):
    headline = "My Awesome Blog"
    41
    Tuesday, September 4, 12
    This is not a realistic view.
    You wouldn't provide both the `headline` attribute and the `get_headline` method in the
    same view, unless you wanted to override the value set in the attribute (which I've done and
    comes in kind of handy)

    View Slide

  42. class BlogView(SetHeadlineMixin,
    DetailView):
    def get_headline(self):
    return self.object.title
    42
    Tuesday, September 4, 12
    This is not a realistic view.
    You wouldn't provide both the `headline` attribute and the `get_headline` method in the
    same view, unless you wanted to override the value set in the attribute (which I've done and
    comes in kind of handy)

    View Slide

  43. DECORATION
    urls.py
    (r'^blog/', login_required(
    BlogView.as_view())),
    views.py
    blog_view = login_required(
    BlogView.as_view())
    43
    Tuesday, September 4, 12
    If I want to require a login for this *without* django-braces (or the previously-shown mixin), I
    have to either wrap the view in my urls.py or assign it to a new, throw-away variable in my
    views.py, decorate it there, and then include that variable in my URLs like with function-
    based views. Messy, messy.

    View Slide

  44. MIXINS
    class BlogView(
    LoginRequriedMixin,
    DetailView):
    model = Blog
    […]
    44
    Tuesday, September 4, 12
    But with django-braces or the previous mixin, I just add a mixin to my view. No extra
    variables laying around or multiple files to edit when I want to change this behavior.

    View Slide

  45. ➡ Establishes an execution order
    ➡ Prevents code from running if it
    doesn’t meet requirements
    MIXINS TO
    THE FRONT
    45
    Tuesday, September 4, 12
    We tend to always put mixins at the front of our inheritance chains.
    This seems to help reduce MRO errors (I've, seriously, only ran into it once or twice in over a
    year of writing nothing but CBVs) and helps to establish a logical order for how you're going
    to process your view code.
    It's also very handy if, for example, you have a view that requires a logged-in user, to have
    the first mixin kill the view if the user isn't logged in.

    View Slide

  46. DJANGO-
    BRACES
    ➡ github.com/brack3t/django-braces
    ➡ pip install django-braces
    ➡ django-braces.readthedocs.org
    ➡ Created with Chris Jones (@tehjones)
    46
    Tuesday, September 4, 12
    Now I'd like to introduce you to a project of ours. It's aimed at being a repository of generic
    and useful CBV mixins. You can install it from PyPI or Crate.io, and you can fork it on Github
    if you want to add more mixins. All future examples from here on expect django-braces to
    be installed.

    View Slide

  47. COMMON
    USE CASES
    ➡ Login-required
    ➡ Permission-required
    ➡ Superuser/staff-required
    ➡ and more…
    47
    Tuesday, September 4, 12
    With django-braces, we've tried to cover the most common use cases. We have mixins for
    requiring logins, configurable permissions, and user classes.

    View Slide

  48. FUNCTION-
    BASED VIEWS
    ➡ Login/logout views
    ➡ Session manipulation
    ➡ Can be avoided completely,
    though, if you want.
    48
    Tuesday, September 4, 12
    With all this said, I don't think function-based views are dead.
    Views that manipulate session only, like login/logout, or ones that set variables in your
    session, like the project variable we dreamed up earlier, are still prime candidates for being
    FBVs.
    But, you can avoid them even there if you want. I usually do, using RedirectView as the base
    for all of these.

    View Slide

  49. A COUPLE OF
    SPECIAL
    CASES
    49
    Tuesday, September 4, 12
    So let's look at a couple of cases that people often say are too hard for CBVs.

    View Slide

  50. MULTIPLE
    FORMS
    ➡ Show two forms in one view and
    template.
    50
    Tuesday, September 4, 12

    View Slide

  51. class MyFormsView(TemplateView):
    template = "forms.html"
    def get_context_data(self, **kwargs):
    kwargs = super(MyFormsView,
    self).get_context_data(**kwargs)
    post = self.request.POST or None
    form1 = Form1(post)
    form2 = Form2(post)
    kwargs.update({"form1": form1,
    "form2": form2})
    return kwargs
    51
    Tuesday, September 4, 12
    To make this simpler, we're using a TemplateView instead of a FormView. Yes, we lose a little
    bit of built-in functionality this way, but the logic is much simpler to follow.
    In our context data, we want to include both of our forms. We'll seed them both with the
    POST data, if it exists.

    View Slide

  52. def post(self, request, *args, **kwargs):
    if kwargs["form1"].is_valid() and
    kwargs["form2"].is_valid():
    kwargs["form1"].save()
    kwargs["form2"].save()
    return HttpResponseRedirect(
    self.get_success_url())
    else:
    return super(MyFormsView,
    self).post(request, *args, **kwargs)
    52
    Tuesday, September 4, 12
    Then, in our post method, we'll check to see if the forms, which have already been seeded
    with data, are valid. If they are, we'll save them (or whatever your form needs to do,
    obviously), and then redirect to our success url.
    If they're not valid, we'll just continue on with our view like normal.

    View Slide

  53. USER-OWNED
    CONTENT
    ➡ Change querysets based on user
    permissions
    53
    Tuesday, September 4, 12

    View Slide

  54. class ProjectsView(LoginRequiredMixin,
    ListView):
    model = Project
    def get_queryset(self, **kwargs):
    queryset = super(ProjectsView,
    self).get_queryset(**kwargs)
    if not ⏎
    self.request.user.is_superuser:
    queryset = queryset.filter(
    user=self.request.user)
    return queryset
    54
    Tuesday, September 4, 12
    Here we have a login-protected view listing all of our project model instances. Obviously we
    don't want everyone to have access to everyone else's projects, so we need to filter the list so
    a user only sees what belongs to him or her unless, according to our application logic, they're
    a superuser.
    Again, we override `get_queryset`. After we get the default queryset, we check to see if the
    requesting user is a superuser. If they are *not*, we filter the queryset down to where the
    user that the project is tied to matches the user in our request object.
    Then we return the queryset, whether it was filtered or not.

    View Slide

  55. THANKS!
    ➡ Kenneth Love
    ➡ @kennethlove
    ➡ __love__
    ➡ brack3t.com
    ➡ Come say “HI!”
    55
    Tuesday, September 4, 12
    That finishes my talk, thank you all for coming. I know CBVs is a large area and I didn't cover
    every corner of it, but hopefully this encourages you to jump into this newer area of Django.

    View Slide

  56. QUESTIONS?
    56
    Tuesday, September 4, 12

    View Slide