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

Overcoming Troubles with Class-Based (Generic) Views

Andrew Pinkham
September 05, 2013

Overcoming Troubles with Class-Based (Generic) Views

An introduction to Class-Based Views and Class-Based Generic Views. Talk given at DjangoCon 2013.

Andrew Pinkham

September 05, 2013
Tweet

More Decks by Andrew Pinkham

Other Decks in Technology

Transcript

  1. Overcoming Troubles with
    Class Based (Generic) Views
    A short guide to managing the beast

    View Slide

  2. afrg.co/otwcbv

    View Slide

  3. shameless

    View Slide

  4. © DREAMWORKS 2010

    View Slide

  5. Basic Project

    View Slide

  6. Project Description
    Two Models: Account & Transaction
    Account: slug, get_absolute_url()
    Transaction: two FK’s to Account
    ModelForms for both

    View Slide

  7. Pages
    Account List
    Account Detail
    Account Create
    Transaction Create

    View Slide

  8. Refactoring to
    Class-Based Views

    View Slide

  9. # bank/views.py
    from django.views.generic import View

    View Slide

  10. # bank/views.py
    class AccountList(View):
    def get(self, request):
    acct_list = Account.objects.all()
    return render(request,
    'bank/account_list.html',
    {'account_list': acct_list})
    class AccountDetail(View):
    def get(self, request, slug):
    acct = get_object_or_404(Account, slug=slug)
    return render(request,
    'bank/account_detail.html',
    {'account': acct})

    View Slide

  11. # bank/views.py
    class AccountCreate(View):
    def get(self, request):
    form = AccountForm()
    return render(request,
    'bank/account_form.html',
    {'form': form})
    def post(self, request):
    form = AccountForm(request.POST)
    if form.is_valid():
    new_acct = form.save()
    return redirect(new_acct)
    else:
    return render(request,
    'bank/account_form.html',
    {'form': form})

    View Slide

  12. # bank/views.py
    class TransactionCreate(View):
    def get(self, request):
    form = TransactionForm()
    return render(request,
    'bank/account_form.html',
    {'form': form})
    def post(self, request):
    form = TransactionForm(request.POST)
    if form.is_valid():
    new_acct = form.save()
    return redirect('bank_account_list')
    else:
    return render(request,
    'bank/account_form.html',
    {'form': form})

    View Slide

  13. # bank/views.py
    class TransactionCreate(View):
    def get(self, request):
    form = TransactionForm()
    return render(request,
    'bank/account_form.html',
    {'form': form})
    def post(self, request):
    form = TransactionForm(request.POST)
    if form.is_valid():
    new_acct = form.save()
    return redirect('bank_account_list')
    else:
    return render(request,
    'bank/account_form.html',
    {'form': form})

    View Slide

  14. # bank/urls.py
    from django.conf.urls import patterns, url
    from .models import Account
    from .views import AccountList, AccountDetail, \
    AccountCreate, TransactionCreate
    urlpatterns = patterns('',
    url(r'^account/$',
    AccountList.as_view(),
    name='bank_account_list'),
    url(r'^account/create/$',
    AccountCreate.as_view(),
    name='bank_account_create'),
    url(r'^account/(?P[\w\-]+)/$',
    AccountDetail.as_view(),
    name='bank_account_detail'),
    url(r'^transaction/$',
    TransactionCreate.as_view(),
    name='bank_trans_create'),
    )

    View Slide

  15. Taking advantage of CBVs

    View Slide

  16. # bank/views.py
    class PostFormMixin(object):
    form = None
    template = ''
    redirect = ''

    View Slide

  17. # bank/views.py
    class PostFormMixin(object):
    ...
    def get(self, request):
    form = self.form()
    return render(request,
    self.template,
    {'form': form})
    def post(self, request):
    form = self.form(request.POST)
    if form.is_valid():
    new_obj = form.save()
    if self.redirect:
    return redirect(self.redirect)
    else:
    return redirect(new_obj)
    else:
    return render(request,
    self.template,
    {'form': form})

    View Slide

  18. # bank/views.py
    class AccountCreate(PostFormMixin, View):
    form = AccountForm
    template = 'bank/account_form.html'
    class TransactionCreate(PostFormMixin, View):
    form = TransactionForm
    template = 'bank/account_form.html'
    redirect = 'bank_account_list'

    View Slide

  19. # bank/views.py
    class PostFormMixin(object):
    ...
    def get_template(self):
    if self.template == '':
    raise ImproperlyConfigured(
    '"template" variable not defined in %s'
    % self.__class__.__name__)
    return self.template

    View Slide

  20. # bank/views.py
    class PostFormMixin(object):
    ...
    def get_redirect_url(self,obj):
    if self.redirect:
    url = self.redirect
    else:
    try:
    url = obj.get_absolute_url()
    except AttributeError:
    raise ImproperlyConfigured(
    '"redirect" variable must be defined '
    'in %s when redirecting %s objects.'
    % (self.__class__.__name__,
    obj.__class__.__name__))
    return url

    View Slide

  21. # bank/views.py
    class PostFormMixin(object):
    ...
    def get(self, request):
    form = self.form()
    return render(request,
    self.get_template(),
    {'form': form})
    def post(self, request):
    form = self.form(request.POST)
    if form.is_valid():
    new_obj = form.save()
    return redirect(self.get_redirect_url(new_obj))
    else:
    return render(request,
    self.get_template(),
    {'form': form})

    View Slide

  22. Class-Based Generic Views

    View Slide

  23. # bank/views.py
    from django.core.urlresolvers import reverse_lazy
    from django.views.generic import (ListView, DetailView,
    CreateView)
    from .models import Account, Transaction
    class AccountList(ListView):
    model = Account
    class AccountDetail(DetailView):
    model = Account
    class AccountCreate(CreateView):
    model = Account
    class TransactionCreate(CreateView):
    model = Transaction
    success_url = reverse_lazy('bank_account_list')

    View Slide

  24. from django.conf.urls import patterns, url
    from django.core.urlresolvers import reverse_lazy
    from django.views.generic import (ListView, DetailView,
    CreateView)
    from .models import Account, Transaction
    urlpatterns = patterns('',
    url(r'^account/$',
    ListView.as_view(model=Account),
    name='bank_account_list'),
    url(r'^account/create/$',
    CreateView.as_view(model=Account),
    name='bank_account_create'),
    url(r'^account/(?P[\w\-]+)/$',
    DetailView.as_view(model=Account),
    name='bank_account_detail'),
    url(r'^transaction/$',
    CreateView.as_view(model=Transaction,
    success_url=reverse_lazy(
    'bank_account_list')),
    name='bank_trans_create'),
    )

    View Slide

  25. Quick Recap

    View Slide

  26. Customizing CBGVs

    View Slide

  27. Feature Changes
    List Accounts and Transactions in AccountList
    Split TransactionCreate
    Send To link
    Send From link
    Generate Slug Automatically

    View Slide

  28. View Slide

  29. View Slide

  30. Multiple Inheritance
    New Style-classes (object) in Python 2.2
    C3 MRO in Python 2.3

    View Slide

  31. View Slide

  32. View Slide

  33. View Slide

  34. @classonlymethod
    def as_view(cls, **initkwargs):
    for key in initkwargs:
    if key in cls.http_method_names:
    raise TypeError('...')
    if not hasattr(cls, key):
    raise TypeError('...')
    def view(request, *args, **kwargs):
    ...
    update_wrapper(view, cls, updated=())
    update_wrapper(view, cls.dispatch, assigned=())
    return view

    View Slide

  35. def as_view(cls, **initkwargs):
    ...
    def view(request, *args, **kwargs):
    self = cls(**initkwargs)
    if hasattr(self, 'get') and not hasattr(self, 'head'):
    self.head = self.get
    self.request = request
    self.args = args
    self.kwargs = kwargs
    return self.dispatch(request, *args, **kwargs)

    View Slide

  36. def __init__(self, **kwargs):
    for key, value in six.iteritems(kwargs):
    setattr(self, key, value)

    View Slide

  37. def dispatch(self, request, *args, **kwargs):
    if request.method.lower() in self.http_method_names:
    handler = getattr(self, request.method.lower(),
    self.http_method_not_allowed)
    else:
    handler = self.http_method_not_allowed
    return handler(request, *args, **kwargs)

    View Slide

  38. http_method_names = ['get', 'post', 'put',
    'delete', 'head', 'options', 'trace']
    def dispatch(self, request, *args, **kwargs):
    if request.method.lower() in self.http_method_names:
    handler = getattr(self, request.method.lower(),
    self.http_method_not_allowed)
    else:
    handler = self.http_method_not_allowed
    return handler(request, *args, **kwargs)

    View Slide

  39. © TECHNABOB.COM 2012

    View Slide

  40. CBGV
    TemplateView
    RedirectView
    DetailView
    ListView
    FormView
    CreateView
    UpdateView
    DeleteView
    ArchiveIndexView
    YearArchiveView
    MonthArchiveView
    WeekArchiveView
    DayArchiveView
    TodayArchiveView
    DateDetailView

    View Slide

  41. View Slide

  42. © DISNEY 2012

    View Slide

  43. © MARVEL 2013

    View Slide

  44. View Slide

  45. © WARNER BROS 1942

    View Slide

  46. © MGM 1995

    View Slide

  47. Transaction List
    ListView.get()
    ListView.get_context_data()
    MultipleObjectMixin
    ContextMixin

    View Slide

  48. # bank/views.py
    class AccountList(ListView):
    model = Account
    def get_context_data(self, **kwargs):
    context = super(AccountList, self)\
    .get_context_data(**kwargs)
    context['transaction_list'] = Transaction\
    .objects\
    .order_by('-date')\
    [:10]
    return context

    View Slide

  49. Automating Slug
    slugify()
    clean()

    View Slide

  50. from django.forms import ModelForm
    from django.utils.text import slugify
    from .models import Account
    class AccountForm(ModelForm):
    class Meta:
    model = Account
    def clean(self):
    cleaned_data = super(AccountForm, self).clean()
    name = cleaned_data.get("name")
    slug = cleaned_data.get("slug")
    if not slug and name:
    cleaned_data['slug'] = slugify(name)
    return cleaned_data

    View Slide

  51. from django.forms import ModelForm
    from django.utils.text import slugify
    from .models import Account
    class AccountForm(ModelForm):
    class Meta:
    model = Account
    exclude = ('slug',)
    def clean(self):
    cleaned_data = super(AccountForm, self).clean()
    name = cleaned_data.get("name")
    slug = cleaned_data.get("slug")
    if not slug and name:
    cleaned_data['slug'] = slugify(name)
    return cleaned_data

    View Slide

  52. Saving slug
    django-src/forms/models.py
    ModelForm
    ModelFormMetaclass
    BaseModelForm

    View Slide

  53. Saving slug
    BaseModelForm.save()
    save_instance
    save_m2m
    construct_instance

    View Slide

  54. class AccountForm(ModelForm):
    class Meta:
    model = Account
    exclude = ('slug',)
    def save(self, commit=True):
    instance = super(AccountForm, self)\
    .save(commit=False)
    instance.slug = slugify(
    self.cleaned_data\
    .get('name', ''))
    if commit:
    instance.save()
    self.save_m2m()
    return instance

    View Slide

  55. Transaction Forms
    To Account
    From Account

    View Slide

  56. Overrides
    Introduce two new forms
    get_initial()
    get_context_data()
    view() -> dispatch() -> get()/post()
    get_success_url()

    View Slide

  57. class TransactionCreate(CreateView):
    model = Transaction

    View Slide

  58. class TransactionCreate(CreateView):
    ...
    def dispatch(self, request, *args, **kwargs):
    slug = kwargs.get('slug', None)
    account_obj = get_object_or_404(Account,\
    slug__iexact=slug)
    self.acct_name = account_obj.name
    self.acct_pk = account_obj.pk
    self.acct_url = getattr(account_obj,\
    'get_absolute_url', '')
    return super(TransactionCreate, self)\
    .dispatch(request, *args, **kwargs)

    View Slide

  59. class TransactionCreate(CreateView):
    ...
    def get_initial(self):
    initial_data = super(TransactionCreate, self)\
    .get_initial()
    if self.form_class == TransferFormFrom:
    initial_data['from_account'] = self.acct_pk
    elif self.form_class == TransferFormTo:
    initial_data['to_account'] = self.acct_pk
    else:
    raise ImproperlyConfigured(
    '"form_class" variable must be defined
    'in %s for correct initial behavior.'
    % (self.__class__.__name__,
    obj.__class__.__name__))
    return initial_data

    View Slide

  60. class TransactionCreate(CreateView):
    ...
    def get_context_data(self, **kwargs):
    context = {}
    context['account_name'] = self.acct_name
    if self.form_class == TransferFormFrom:
    context['trans_dir'] = 'From:'
    elif self.form_class == TransferFormTo:
    context['trans_dir'] = 'To:'
    else:
    raise ImproperlyConfigured(
    '"form_class" variable must be defined
    'in %s for correct initial behavior.'
    % (self.__class__.__name__,
    obj.__class__.__name__))
    context.update(kwargs)
    return super(TransactionCreate, self)\
    .get_context_data(**context)

    View Slide

  61. class TransactionCreate(CreateView):
    ...
    def get_success_url(self):
    if (hasattr(self, 'acct_url')
    and self.acct_url != ''):
    return self.acct_url()
    else:
    return reverse('bank_account_list')

    View Slide

  62. Congratulations!

    View Slide

  63. © DREAMWORKS 2010

    View Slide

  64. Take Aways
    CBV ≠ CBGV
    Full understanding: MRO, View
    ccbv.co.uk
    CBGV: Lasagna is a sometimes food

    View Slide

  65. View Slide

  66. Take Aways
    CBV ≠ CBGV
    Full understanding: MRO, View
    ccbv.co.uk
    CBGV: Lasagna is a sometimes food

    View Slide

  67. Thank you!
    afrg.co/otwcbv
    @andrewsforge

    View Slide