Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. 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.
  2. 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.
  3. 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.
  4. ➡ 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.
  5. 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...
  6. 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.
  7. 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.
  8. 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.
  9. 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).
  10. 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.
  11. 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.
  12. TEMPLATES {% extends "layouts/base.html" %} {% block content %} <h1>Puttin'

    on the ritz</h1> {% 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.
  13. 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...
  14. 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.
  15. OBJECT- BASED VIEWS SingleObjectMixin MultipleObjectMixin DetailView ListView 19 Tuesday, September

    4, 12 These views deal with single and multiple model instances.
  16. 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.
  17. 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.
  18. 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.
  19. 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.
  20. 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.
  21. 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
  22. 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
  23. 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.
  24. CONVERSION TO CBV 28 Tuesday, September 4, 12 How do

    we take all of that from FBV to CBV?
  25. 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.
  26. 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.
  27. 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.
  28. 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.
  29. 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.
  30. ➡ 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.
  31. ➡ 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`
  32. 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.
  33. 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.
  34. 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.
  35. DESIGN PATTERNS 39 Tuesday, September 4, 12 There are some

    patterns to keep in mind when you're creating a mixin
  36. 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.
  37. 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)
  38. 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)
  39. 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.
  40. 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.
  41. ➡ 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.
  42. 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.
  43. 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.
  44. 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.
  45. 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.
  46. MULTIPLE FORMS ➡ Show two forms in one view and

    template. 50 Tuesday, September 4, 12
  47. 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.
  48. 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.
  49. 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.
  50. 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.