Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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.

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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.

Slide 5

Slide 5 text

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.

Slide 6

Slide 6 text

➡ 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.

Slide 7

Slide 7 text

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...

Slide 8

Slide 8 text

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.

Slide 9

Slide 9 text

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.

Slide 10

Slide 10 text

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.

Slide 11

Slide 11 text

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).

Slide 12

Slide 12 text

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.

Slide 13

Slide 13 text

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.

Slide 14

Slide 14 text

TEMPLATES 14 Tuesday, September 4, 12

Slide 15

Slide 15 text

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.

Slide 16

Slide 16 text

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...

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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.

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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.

Slide 21

Slide 21 text

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.

Slide 22

Slide 22 text

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.

Slide 23

Slide 23 text

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.

Slide 24

Slide 24 text

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.

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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.

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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.

Slide 30

Slide 30 text

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.

Slide 31

Slide 31 text

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.

Slide 32

Slide 32 text

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.

Slide 33

Slide 33 text

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.

Slide 34

Slide 34 text

➡ 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.

Slide 35

Slide 35 text

➡ 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`

Slide 36

Slide 36 text

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.

Slide 37

Slide 37 text

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.

Slide 38

Slide 38 text

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.

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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.

Slide 41

Slide 41 text

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)

Slide 42

Slide 42 text

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)

Slide 43

Slide 43 text

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.

Slide 44

Slide 44 text

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.

Slide 45

Slide 45 text

➡ 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.

Slide 46

Slide 46 text

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.

Slide 47

Slide 47 text

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.

Slide 48

Slide 48 text

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.

Slide 49

Slide 49 text

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.

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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.

Slide 52

Slide 52 text

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.

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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.

Slide 55

Slide 55 text

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.

Slide 56

Slide 56 text

QUESTIONS? 56 Tuesday, September 4, 12