➡ 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
= 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
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
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.
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.
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.
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.
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.
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.
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`
*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.
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.
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.
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)
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)
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.
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.
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.
➡ 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.
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.
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.
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.
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.
= 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.
➡ 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.