Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

[DO NOT MERGE] #19898 - Document why/when of class-based views #840

Closed
wants to merge 10 commits into from

7 participants

@estebistec

Beginning the pull request early for documentation updates for class-based views.

Currently:

  • I've written out some thoughts that I think are important to express on the front topic page, but I need to do some work so weave this new content into the rest of the page better (i.e., make sure there's no overlap/repetition)
  • Currently working on the usage-patterns page that will hopefully demonstrate the ideas stated in this new topic front-page material.
@carljm carljm commented on the diff
docs/topics/class-based-views/usage-patterns.txt
((4 lines not shown))
+
+When is it useful to depart from function based views and trade simple
+flow-control for modularity? The following usage patterns attempt to
+demonstrate cases where this makes sense.
+
+Usage patterns documented elsewhere:
+ * :doc:`Generic display</topics/class-based-views/generic-display>`
+ * :doc:`Generic editing</topics/class-based-views/generic-editing>`
+
+Each of the usage examples below demonstrates specific benefits of class-based
+views. It is implied that all of the examples demonstrate composition of
+featuers into views.
+
+
+
+Requiring authenticated user or super-user
@carljm Owner
carljm added a note

I think this is a poor choice of usage pattern, as it can be handled so much more straightforwardly using a decorator on a function view. It may be useful to document how to do something similar with class-based views, but a cross-cutting concern that is easy to implement as a decorator should not be put forward as a reason to use CBVs.

yeah, I can pull these simpler ones. Hopefully my testing section gives people a better idea for their choices at that level anyway. I'm still gathering good examples of more complex views, but there's a fine line at the other end too where at a certain complexity, the code just needs to be not in the view.

I think that, to complete this page, I'm going to need to take feedback from the community somehow. Even if I can come up with my own great examples, they're simply my own patterns but I'd prefer to gather more and validate patterns with wider usage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@carljm carljm commented on the diff
docs/topics/class-based-views/index.txt
((31 lines not shown))
+high-level methods that do easy-to-understand things, like
+:meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response()`.
+Then there are specialized methods that let you hook into that "template method"
+such as
+:meth:`~django.views.generic.detail.SingleObjectMixin.get_context_data()`
+(which is a more commonly extended method).
+
+When should I use class-based views?
+====================================
+
+The :doc:`usage patterns</topics/class-based-views/usage-patterns>` should help
+you draw a useful comparison, but here are some questions that may help guide
+you:
+
+#. Is your view supported by a generic view?
+#. Do you have a function-based view that is greater than 20-30 lines of code?
@carljm Owner
carljm added a note

I think this documentation needs to mention that in many cases if your view is complex you will be better served to figure out whether some parts of the view can or should be moved into supporting objects (models, forms, viewmodels), rather than choosing a more complex structure for your view itself.

Yeah, that's fair. Basically explain that the decision tree is not just types of views to write, but that there are other places you should push logic to.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@timgraham
Owner

I'm going to close this for now due to inactivity and the fact that it doesn't appear to be ready for merge given the [DO NOT MERGE] tag. Feel free to open a new PR when you are ready for more feedback. Thanks!

@timgraham timgraham closed this
@estebistec

yeah, once I got into this I discovered I needed to gather more/better examples, as many of my own may be more preferential where Django would still recommend function views.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Feb 23, 2013
  1. @apollo13
  2. @apollo13
  3. @HonzaKral

    Merge pull request #791 from khalas/ticket_19811

    HonzaKral authored
    Changed %r to %s in get_language_info error message
  4. @HonzaKral
  5. @claudep

    Fixed #19698 -- Cleared sites cache with signals

    claudep authored
    Thanks ddavies at nomensa.com for the report and the patch and
    serdaroncode for reviewing the patch.
  6. @jacobian

    Added a draft document explaining how to release Django.

    jacobian authored
    Thanks to James for the first draft; I made a few changes (svn->git) and
    some supporting links, but mostly I added FIXME's.
  7. @estebistec
Commits on Feb 24, 2013
  1. @estebistec
  2. @estebistec

    extend > override

    estebistec authored
  3. @estebistec

    Beginning of usage patterns

    estebistec authored
This page is out of date. Refresh to see the latest.
View
28 django/contrib/sites/models.py
@@ -1,4 +1,5 @@
from django.db import models
+from django.db.models.signals import pre_save, pre_delete
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
@@ -49,20 +50,6 @@ class Meta:
def __str__(self):
return self.domain
- def save(self, *args, **kwargs):
- super(Site, self).save(*args, **kwargs)
- # Cached information will likely be incorrect now.
- if self.id in SITE_CACHE:
- del SITE_CACHE[self.id]
-
- def delete(self):
- pk = self.pk
- super(Site, self).delete()
- try:
- del SITE_CACHE[pk]
- except KeyError:
- pass
-
@python_2_unicode_compatible
class RequestSite(object):
@@ -96,3 +83,16 @@ def get_current_site(request):
else:
current_site = RequestSite(request)
return current_site
+
+
+def clear_site_cache(sender, **kwargs):
+ """
+ Clears the cache (if primed) each time a site is saved or deleted
+ """
+ instance = kwargs['instance']
+ try:
+ del SITE_CACHE[instance.pk]
+ except KeyError:
+ pass
+pre_save.connect(clear_site_cache, sender=Site)
+pre_delete.connect(clear_site_cache, sender=Site)
View
7 django/contrib/sites/tests.py
@@ -42,6 +42,13 @@ def test_site_cache(self):
site = Site.objects.get_current()
self.assertEqual("Example site", site.name)
+ def test_delete_all_sites_clears_cache(self):
+ # When all site objects are deleted the cache should also
+ # be cleared and get_current() should raise a DoesNotExist.
+ self.assertIsInstance(Site.objects.get_current(), Site)
+ Site.objects.all().delete()
+ self.assertRaises(Site.DoesNotExist, Site.objects.get_current)
+
@override_settings(ALLOWED_HOSTS=['example.com'])
def test_get_current_site(self):
# Test that the correct Site object is returned
View
2  django/core/validators.py
@@ -129,7 +129,7 @@ def __call__(self, value):
slug_re = re.compile(r'^[-a-zA-Z0-9_]+$')
validate_slug = RegexValidator(slug_re, _("Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."), 'invalid')
-ipv4_re = re.compile(r'^\s*(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\s*$')
+ipv4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$')
validate_ipv4_address = RegexValidator(ipv4_re, _('Enter a valid IPv4 address.'), 'invalid')
View
16 django/forms/fields.py
@@ -1096,9 +1096,10 @@ class IPAddressField(CharField):
}
default_validators = [validators.validate_ipv4_address]
- def clean(self, value):
- value = self.to_python(value).strip()
- return super(IPAddressField, self).clean(value)
+ def to_python(self, value):
+ if value in EMPTY_VALUES:
+ return ''
+ return value.strip()
class GenericIPAddressField(CharField):
@@ -1111,16 +1112,13 @@ def __init__(self, protocol='both', unpack_ipv4=False, *args, **kwargs):
self.default_error_messages['invalid'] = invalid_error_message
super(GenericIPAddressField, self).__init__(*args, **kwargs)
- def clean(self, value):
- value = self.to_python(value).strip()
- return super(GenericIPAddressField, self).clean(value)
-
def to_python(self, value):
if value in validators.EMPTY_VALUES:
return ''
+ value = value.strip()
if value and ':' in value:
- return clean_ipv6_address(value,
- self.unpack_ipv4, self.error_messages['invalid'])
+ return clean_ipv6_address(value,
+ self.unpack_ipv4, self.error_messages['invalid'])
return value
View
280 docs/internals/howto-release-django.txt
@@ -0,0 +1,280 @@
+=====================
+How is Django Formed?
+=====================
+
+This document explains how to release Django. If you're unluky enough to
+be driving a release, you should follow these instructions to get the
+package out.
+
+**Please, keep these instructions up-to-date if you make changes!** The point
+here is to be descriptive, not proscriptive, so feel free to streamline or
+otherwise make changes, but **update this document accordingly!**
+
+Overview
+========
+
+There are three types of releases that you might need to make
+
+* Security releases, disclosing and fixing a vulnerability. This'll
+ generally involve two or three simultaneous releases -- e.g.
+ 1.5.X, 1.6.X, and, depending on timing, perhaps a 1.7 alpha/beta/rc.
+
+* Regular version releases, either a final release (e.g. 1.5) or a
+ bugfix update (e.g. 1.5.1).
+
+* Pre-releases, e.g. 1.6 beta or something.
+
+In general the steps are about the same reguardless, but there are a few
+differences noted. The short version is:
+
+#. If this is a security release, pre-notify the security distribution list
+ at least one week before the actual release.
+
+#. Proofread (and create if needed) the release notes, looking for
+ organiztion, writing errors, deprecation timelines, etc. Draft a blog post
+ and email announcement.
+
+#. Update version numbers and create the release package(s)!
+
+#. Upload the package(s) to the the ``djangoproject.com`` server and creating
+ some redirects for download/checksum links.
+
+#. Unless this is a pre-release, add the new version(s) to PyPI.
+
+#. Update the home page and download page to link to the new version(s).
+
+#. Post the blog entry and send out the email announcements.
+
+#. Update version numbers post-release.
+
+There's a lot of details, so please read on.
+
+Prerequisites
+=============
+
+You'll need a few things hooked up to make this work:
+
+* A GPG key. *FIXME: sort out exactly whose keys are acceptable for a
+ release.*
+
+* Access to Django's record on PyPI.
+
+* Access to the ``djangoproject.com`` server to upload files and trigger a
+ deploy.
+
+* Access to the admin on ``djangoproject.com``.
+
+* Access to post to ``django-announe``.
+
+* If this is a security release, access to the pre-notification distribution
+ list.
+
+If this is your first release, you'll need to corrdinate with James and Jacob
+to get all these things ready to go.
+
+Pre-release tasks
+=================
+
+A few items need to be taken care of before even beginning the release process.
+This stuff starts about a week before the release; most of it can be done
+any time leading up to the actual release:
+
+#. If this is a security release, send out pre-notification **one week**
+ before the release. We maintain a list of who gets these pre-notifcation
+ emails at *FIXME WHERE?*. This email should be signed by the key you'll use
+ for the release, and should include patches for each issue being fixed.
+
+#. As the release aproaches, watch Trac to make sure no release blockers
+ are left for the upcoming release.
+
+#. Check with the other committers to make sure they don't have any
+ un-committed changes for the release.
+
+#. Proofread the release notes, including looking at the online
+ version to catch any broken links or reST errors, and make sure the
+ release notes contain the correct date.
+
+#. Double-check that the release notes mention deprecation timelines
+ for any APIs noted as deprecated, and that they mention any changes
+ in Python version support.
+
+#. Double-check that the release notes index has a link to the notes
+ for the new release; this will be in ``docs/releases/index.txt``.
+
+Preparing for release
+=====================
+
+Next, everything needs to be made ready for actually rolling the
+release. The following things should be done a few days to a few hours
+before release:
+
+#. Update the djangoproject home page and download page templates to
+ reflect the new release. There are two templates to change:
+ ``flatpages/download.html`` and ``homepage.html``; here's
+ `one example commit for the 1.4.5 / 1.3.7 releases`__
+
+ __ https://github.com/django/djangoproject.com/commit/772edbc6ac5a2b8e718606b3338f2bcc429fb9b6
+
+#. Write the announcement blog post for the release. You can enter it into
+ the admin at any time and mark it as inactive. Here's a few examples:
+ `example security release accouncement`__, `example regular release
+ announcement`__, `example pre-release announcement`__.
+
+ __ https://www.djangoproject.com/weblog/2013/feb/19/security/
+ __ https://www.djangoproject.com/weblog/2012/mar/23/14/
+ __ https://www.djangoproject.com/weblog/2012/nov/27/15-beta-1/
+
+#. Create redirects in the admin for the new downloads. For each release,
+ we create two redirects that look like::
+
+ /download/<version>/tarball/ -> /m/releases/<version>/Django-<version>.tar.gz
+ /download/<version>/checksum/ -> /m/pgp/Django-<version>.checksum.txt
+
+Actually rolling the release
+============================
+
+OK, this is the fun part, where we actually push out a release!
+
+#. Check Jenkins is green for the version(s) you're putting out. You probably
+ shouldn't issue a release until it's green.
+
+#. A release always begins from a release branch, so you
+ should ``git pull`` to make sure you're up-to-date and then
+ ``git checkout stable/<release>`` (e.g. checkout ``stable/1.5.x`` to issue
+ a release in the 1.5 series.)
+
+#. If this is a security release, merge the apropriate patches from
+ ``django-private``. *FIXME: actual commands here - make sure to --ff-
+ only right?*. Make sure the commit messages explain that the commit
+ is a security fix and that an announcement will follow (`example
+ security commit`__)
+
+ __ https://github.com/django/django/commit/3ef4bbf495cc6c061789132e3d50a8231a89406b
+
+#. Update version numbers for the release. This has to happen in three
+ places: ``django/__init__.py``, ``docs/conf.py``, and ``setup.py``.
+ Please see `notes on setting the VERSION tuple`_ below for details
+ on ``VERSION``. Here's `an example commit updating version numbers`__
+
+ __ https://github.com/django/django/commit/18d920ea4839fb54f9d2a5dcb555b6a5666ee469
+
+ Make sure the ``download_url`` in ``setup.py`` is the actual URL you'll
+ use for the new release package, not the redirect URL (some tools can't
+ properly follow redirects).
+
+#. If this is a pre-release package, update the "Development Status" trove
+ classifier in ``setup.py`` to reflect this. Otherwise, make sure the
+ classifier is set to ``Development Status :: 5 - Production/Stable``.
+
+#. Tag the release by running ``git tag`` *FIXME actual commands*.
+
+#. ``git push`` your work.
+
+#. Make sure you have an absolutely clean tree by running ``git clean -dfx``.
+
+#. Run ``python setup.py sdist`` to generate the release package.
+
+#. Generate the MD5 and SHA1 hashes of the release package. *FIXME
+ actual commands for doign this?*
+
+#. Create a "checksums" file containing the hashes and release information.
+ You can start with `a previous checksums file`__ and replace the
+ dates, keys, links, and checksums. *FIXME: make a template file.*
+
+ __ https://www.djangoproject.com/m/pgp/Django-1.5b1.checksum.txt
+
+#. Sign the checksum file using the release key (``gpg
+ --clearsign``), then verify the signature (``gpg --verify``). *FIXME:
+ full, actual commands here*.
+
+If you're issuing multiple releases, repeat these steps for each release.
+
+Making the release(s) available to the public
+=============================================
+
+Now you're ready to actually put the release out there. To do this:
+
+#. Upload the release package(s) to the djangoproject server; releases go
+ in ``/home/www/djangoproject.com/src/media/releases``, under a
+ directory for the appropriate version number (e.g.
+ ``/home/www/djangoproject.com/src/media/releases/1.5`` for a ``1.5.X``
+ release.).
+
+#. Upload the checksum file(s); these go in
+ ``/home/www/djangoproject.com/src/media/pgp``.
+
+#. Test that the release packages install correctly using ``easy_install``
+ and ``pip``. Here's how I do it (which requires `virtualenvwrapper`__):
+
+ $ mktmpenv
+ $ easy_install http://www.djangoproject.com/download/<version>/tarball/
+ $ deactivate
+ $ mktmpenv
+ $ pip install http://www.djangoproject.com/download/<version>/tarball/
+ $ deactivate
+
+ This just tests that the tarballs are available (i.e. redirects are up) and
+ that they install correctly, but it'll catch silly mistakes. *XXX FIXME:
+ buildout too?*
+
+ __ https://pypi.python.org/pypi/virtualenvwrapper
+
+#. Ask a few people on IRC to verify the checksums by visiting the chucksums
+ file (e.g. https://www.djangoproject.com/m/pgp/Django-1.5b1.checksum.txt)
+ and following the instructions in it.
+
+#. If this is a security or regular release, register the new package with
+ PyPI by uploading the ``PGK-INFO`` file generated in the release package
+ *FIXME: be more specific about where this is and how to upload it.*
+ Don't do this for pre-releases.
+
+#. Deploy the template changes you made a while back by running `fab deploy`
+ from the ``djangoproject.com`` repo.
+
+#. Update the ``/download/`` flat page in the djangoproject.com
+ admin. For alpha/beta/RC releases, we add a temporary third section
+ to that page listing the preview package; otherwise, just update
+ the "Get the latest official version" section.
+
+#. Make up the blog post announcing the release live.
+
+#. Post the release announcement to the django-announce,
+ django-developers and django-users mailing lists. This should
+ include links to both the announcement blog post and the release
+ notes. *FIXME: make some templates with example text*.
+
+Post-release
+============
+
+You're almost done! All that's left to do now is:
+
+#. Update the ``VERSION`` tuple in ``django/__init__.py`` again,
+ incrementing to whatever the next expected release will be. For
+ example, after releasing 1.2.1, update ``VERSION`` to report "1.2.2
+ pre-alpha".
+
+Notes on setting the VERSION tuple
+==================================
+
+Django's version reporting is controlled by the ``VERSION`` tuple in
+``django/__init__.py``. This is a five-element tuple, whose elements
+are:
+
+#. Major version.
+#. Minor version.
+#. Micro version.
+#. Status -- can be one of "alpha", "beta", "rc" or "final".
+#. Series number, for alpha/beta/RC packages which run in sequence
+ (allowing, for example, "beta 1", "beta 2", etc.).
+
+For a final release, the status is always "final" and the series
+number is always 0. A series number of 0 with an "alpha" status will
+be reported as "pre-alpha".
+
+Some examples:
+
+* ``(1, 2, 1, 'final', 0)`` --> "1.2.1"
+
+* ``(1, 3, 0, 'alpha', 0)`` --> "1.3 pre-alpha"
+
+* ``(1, 3, 0, 'beta', 2)`` --> "1.3 beta 2"
View
1  docs/internals/index.txt
@@ -22,3 +22,4 @@ the hood".
release-process
deprecation
git
+ howto-release-django
View
60 docs/topics/class-based-views/index.txt
@@ -17,6 +17,66 @@ reusable views which suits your use case. For full details, see the
generic-display
generic-editing
mixins
+ usage-patterns
+
+Why class-based views?
+======================
+
+At their core, class-based views are intended to solve the same problem as
+function-based views: process requests and return responses. What makes class
+views different from function views is the approach they provide to solving
+that problem.
+
+That approach, as you could guess by the name, is modularity and extensibility
+via Python classes. The cost is potentially lost clarity around a view's flow
+of control. However, there are situations where for a sufficiently complex view
+the flow of control can be unclear and the implied flow-control offered by
+a well documented base-class becomes appropriate. These are differences
+demonstrated with
+:doc:`usage patterns</topics/class-based-views/usage-patterns>`.
+
+Why so many methods with this approach?
+=======================================
+
+You might notice with class-based views that once you break from a convention
+prescribed by a base-class, there are a lot of potential hook methods you could
+extend to achieve your goal. In short, this is because the approach with many
+of the provided base-classes and mixins is to define workflow using the classic
+`template method <http://en.wikipedia.org/wiki/Template_method_pattern>`_
+design pattern. The benefit here is that there are some straightforward
+high-level methods that do easy-to-understand things, like
+:meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response()`.
+Then there are specialized methods that let you hook into that "template method"
+such as
+:meth:`~django.views.generic.detail.SingleObjectMixin.get_context_data()`
+(which is a more commonly extended method).
+
+When should I use class-based views?
+====================================
+
+The :doc:`usage patterns</topics/class-based-views/usage-patterns>` should help
+you draw a useful comparison, but here are some questions that may help guide
+you:
+
+#. Is your view supported by a generic view?
+#. Do you have a function-based view that is greater than 20-30 lines of code?
@carljm Owner
carljm added a note

I think this documentation needs to mention that in many cases if your view is complex you will be better served to figure out whether some parts of the view can or should be moved into supporting objects (models, forms, viewmodels), rather than choosing a more complex structure for your view itself.

Yeah, that's fair. Basically explain that the decision tree is not just types of views to write, but that there are other places you should push logic to.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+#. Do you have a generic view that needs to be extended or configured?
+#. Is it hard to test specific logic within your view function?
+#. Have decorators increased the minimum overhead required to test your view
+ function.
+
+So, when should you choose class-based views? Whenever the complexity of your
+view warrants modularity or re-usability similar to the generic views
+(:doc:`display</topics/class-based-views/generic-display>` and
+:doc:`editing</topics/class-based-views/generic-editing>`) or these
+:doc:`usage patterns</topics/class-based-views/usage-patterns>`. If you're
+having trouble determining what's right, please peruse those pages to see if
+your case looks similar and inform your decision.
+
+Finally, while the class-based generic views do provide good examples and
+anc comparisons for designing views, they are not required to be the basis of
+your views.
+
Basic examples
==============
View
248 docs/topics/class-based-views/usage-patterns.txt
@@ -0,0 +1,248 @@
+================================
+Class-based views usage patterns
+================================
+
+When is it useful to depart from function based views and trade simple
+flow-control for modularity? The following usage patterns attempt to
+demonstrate cases where this makes sense.
+
+Usage patterns documented elsewhere:
+ * :doc:`Generic display</topics/class-based-views/generic-display>`
+ * :doc:`Generic editing</topics/class-based-views/generic-editing>`
+
+Each of the usage examples below demonstrates specific benefits of class-based
+views. It is implied that all of the examples demonstrate composition of
+featuers into views.
+
+
+
+Requiring authenticated user or super-user
@carljm Owner
carljm added a note

I think this is a poor choice of usage pattern, as it can be handled so much more straightforwardly using a decorator on a function view. It may be useful to document how to do something similar with class-based views, but a cross-cutting concern that is easy to implement as a decorator should not be put forward as a reason to use CBVs.

yeah, I can pull these simpler ones. Hopefully my testing section gives people a better idea for their choices at that level anyway. I'm still gathering good examples of more complex views, but there's a fine line at the other end too where at a certain complexity, the code just needs to be not in the view.

I think that, to complete this page, I'm going to need to take feedback from the community somehow. Even if I can come up with my own great examples, they're simply my own patterns but I'd prefer to gather more and validate patterns with wider usage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+==========================================
+Demonstrates: cross-cutting concerns
+
+views.py::
+
+ from django.contrib.auth.decorators import login_required
+ from django.core.exceptions import PermissionDenied
+ from django.utils.decorators import method_decorator
+ from django.views.generic.base import TemplateView
+
+ class LoginRequiredMixin(object):
+
+ @method_decorator(login_required)
+ def dispatch(self, request, *args, **kwargs):
+ return super(LoginRequiredMixin, self).dispatch(request, *args,
+ **kwargs)
+
+ class SuperuserRequiredMixin(object):
+
+ def dispatch(self, request, *args, **kwargs):
+ if not request.user.is_superuser:
+ raise PermissionDenied
+ return super(SuperuserRequiredMixin, self).dispatch(request, *args,
+ **kwargs)
+
+ class SecretView(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView):
+ template_name = 'secrets/secret.html'
+
+
+Responding differently to AJAX requests
+=======================================
+Demonstrates: cross-cutting concerns, extensibility
+
+views.py::
+
+ import json
+
+ from django.views.generic.base import TemplateView
+
+ class AjaxResponseMixin(object):
+ "Assume XHR requests want JSON data..."
+
+ def render_to_response(self, context, **response_kwargs):
+ if self.request.is_ajax():
+ return self.render_to_json_response(context, **response_kwargs)
+ return super(AjaxResponseMixin, self).render_to_response(context,
+ **response_kwargs)
+
+ def render_to_json_response(self, context, **response_kwargs):
+ response_content = json.dumps(self.get_json_data(context))
+ response_kwargs['content_type'] = 'application/json; charset=utf-8'
+ return HttpResponse(response_content, **response_kwargs)
+
+ def get_json_data(self, context):
+ return context
+
+ class StatusView(AjaxResponseMixin, TemplateView):
+ template_name = 'site_status.html'
+
+ def get_json_data(self, context):
+ return {
+ 'status': 'OK'
+ }
+
+
+Drop-in audit-logging of user's viewing data
+============================================
+Demonstrates: cross-cutting concerns
+
+models.py::
+
+ from django.db import models
+
+ class ViewAudit(models.Model):
+ viewer = models.ForeignKey('auth.User')
+ app_label = models.CharField(max_length=128)
+ model_name = models.CharField(max_length=128)
+ model_instance_pk = models.CharField(max_length=256)
+ viewed_at_time = models.DateTimeField(auto_now=True)
+
+ class Book(models.Model):
+ title = models.CharField(max_length=128)
+
+views.py::
+
+ from django.views.generic import DetailView
+ from .models import Book
+ from .models import ViewAudit
+
+ class ViewAuditLoggingMixin(object):
+ "Add this to DetailViews where you want to see who viewed data"
+
+ def get(self, request, *args, **kwargs):
+ response = super(ViewAuditLoggingMixin, self).get(request, *args,
+ **kwargs)
+ # A user saw this object, so let's document that...
+ model_instance = self.get_object()
+ ViewAudit.objects.create(
+ viewer = request.user,
+ app_label = model_instance._meta.app_label,
+ model_name = model_instance._meta.model_name,
+ model_instance_pk = unicode(model_instance.pk)
+ )
+ return response
+
+ class BookDetailView(ViewAuditLoggingMixin, DetailView):
+ # Using the derived template name
+ model = Book
+
+In a real site, we would probably define the audit model and mixin in their own
+reusable Django app.
+
+
+Modularity for testing
+======================
+
+There are many who believe that the act of making code more testable is a
+natural path to improved modularity. One of the usage patterns is simply that:
+breaking down a view because it was hard to test, or so that it is as easy to
+test as possible even as more complexity is layered onto the view.
+
+For example, suppose we had the following view::
+
+ from django.shortcuts import render
+
+ def add(request):
+ a = int(request.GET.get('a', 0))
+ b = int(request.GET.get('b', 0))
+ result = a + b
+ return render(request, 'math/add_result.html', {'result': result})
+
+As it stands, testing this view is quite simple. We can simply mock requests
+with the query parameters that are directly relevant to the view and assert the
+results.
+
+Well, the results are buried in a rendered template, so we'll have to hope that
+the result is rendered in a predictable fashion in the response.
+
+Let's assume we've written several tests for this view now.
+
+Now let's protect our view::
+
+ from django.contrib.auth.decorators import login_required
+ from django.shortcuts import render
+
+ @login_required
+ def add(request):
+ a = int(request.GET.get('a', 0))
+ b = int(request.GET.get('b', 0))
+ result = a + b
+ return render(request, 'math/add_result.html', {'result': result})
+
+Now, for every single one of our tests we have to mock a user on the request.
+This may not sound like much, but the real problem is that it's an extra thing
+to maintain in these tests and it's not really relevant to testing the view's
+core logic. This is just the beginning of the problem. For every decorator we
+add, we have to ensure the tests still work and, worst-case, add more
+irrelevant cruft to the tests.
+
+So then how do we layer ancillary functionality onto a view without hiding away
+what it really does? Let's see how this looks as a class::
+
+ from django.contrib.auth.decorators import login_required
+ from django.utils.decorators import method_decorator
+ from django.views.generic.base import TemplateView
+
+ class LoginRequiredMixin(object):
+
+ @method_decorator(login_required)
+ def dispatch(self, request, *args, **kwargs):
+ return super(LoginRequiredMixin, self).dispatch(request, *args,
+ **kwargs)
+
+ class AddView(LoginRequiredMixin, TemplateView):
+ template_name = 'math/add_result.html'
+
+ def get_context_data(self, **kwargs):
+ context = super(AddView, self).get_context_data(**kwargs)
+ context['result'] = self.get_result()
+ return context
+
+ def get_result(self):
+ a = int(self.request.GET.get('a', 0))
+ b = int(self.request.GET.get('b', 0))
+ result = a + b
+
+What's different here is that, although we've already starting composing the
+extra functionality into our view, we still have a distinct place where the
+core view logic lives. We could require permissions or feature activations, we
+could add auto-logging or auto-transactions. Yet we still have a method that,
+if directly called, bypasses all of this extra layering. Let's take a look at a
+sample test.
+
+tests.py::
+
+ import unittest
+ from .views import AddView
+
+ class AddViewTests(unitttest.TestCase):
+
+ def test_1_plus_2(self):
+ class Request(object):
+ GET = {'a': '1': 'b': '2'}
+
+ view = AddView(request=Request())
+ result = view.get_result()
+ self.assertEqual(3, result)
+
+As you can see, we get to be pretty direct and to the point here. We would
+probably want to factor out our mock request class, perhaps simply use
+python `mock <http://www.voidspace.org.uk/python/mock>`_, or Django's own
+:class:`django.test.client.RequestFactory`, but otherwise this is a good,
+simple example of how easy testing class-based views can be.
+
+Depending on the type of class-based view, there will be different methods that
+you might go to when testing core view logic. For generic displays, it is often
+:meth:`~django.views.generic.detail.SingleObjectMixin.get_context_data()`.
+Though, as you can see above, we even broke another method out of that which
+allows the result to be tested apart from the rest of the context. For editing
+views, it may be
+:meth:`~django.views.generic.edit.FormMixin.form_valid` that contains the core
+logic to test.
+
+Got other ideas?
+================
+
+This page is only a start. If you think you've got more useful ideas for views
+that should be class-based and can help others decide, consider
+:doc:`contributing </intro/contributing>`
View
8 tests/modeltests/files/tests.py
@@ -2,7 +2,6 @@
import gzip
import shutil
-import StringIO
import tempfile
from django.core.cache import cache
@@ -11,6 +10,7 @@
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from django.utils import unittest
+from django.utils.six import StringIO
from .models import Storage, temp_storage, temp_storage_location
@@ -115,12 +115,12 @@ def test_file_object(self):
self.assertTrue(temp_storage.exists('tests/file_obj'))
self.assertEqual(
temp_storage.open('tests/file_obj').read(),
- 'some content')
+ b'some content')
def test_stringio(self):
# Test passing StringIO instance as content argument to save
- output = StringIO.StringIO()
+ output = StringIO()
output.write('content')
output.seek(0)
@@ -129,7 +129,7 @@ def test_stringio(self):
self.assertTrue(temp_storage.exists('tests/stringio'))
self.assertEqual(
temp_storage.open('tests/stringio').read(),
- 'content')
+ b'content')
View
18 tests/modeltests/validation/tests.py
@@ -116,54 +116,40 @@ class GenericIPAddressFieldTests(ValidationTestCase):
def test_correct_generic_ip_passes(self):
giptm = GenericIPAddressTestModel(generic_ip="1.2.3.4")
self.assertEqual(None, giptm.full_clean())
- giptm = GenericIPAddressTestModel(generic_ip=" 1.2.3.4 ")
- self.assertEqual(None, giptm.full_clean())
giptm = GenericIPAddressTestModel(generic_ip="2001::2")
self.assertEqual(None, giptm.full_clean())
- giptm = GenericIPAddressTestModel(generic_ip=" 2001::2 ")
- self.assertEqual(None, giptm.full_clean())
def test_invalid_generic_ip_raises_error(self):
giptm = GenericIPAddressTestModel(generic_ip="294.4.2.1")
self.assertFailsValidation(giptm.full_clean, ['generic_ip',])
- giptm = GenericIPAddressTestModel(generic_ip="1.2.3 .4")
- self.assertFailsValidation(giptm.full_clean, ['generic_ip',])
giptm = GenericIPAddressTestModel(generic_ip="1:2")
self.assertFailsValidation(giptm.full_clean, ['generic_ip',])
def test_correct_v4_ip_passes(self):
giptm = GenericIPAddressTestModel(v4_ip="1.2.3.4")
self.assertEqual(None, giptm.full_clean())
- giptm = GenericIPAddressTestModel(v4_ip=" 1.2.3.4 ")
- self.assertEqual(None, giptm.full_clean())
def test_invalid_v4_ip_raises_error(self):
giptm = GenericIPAddressTestModel(v4_ip="294.4.2.1")
self.assertFailsValidation(giptm.full_clean, ['v4_ip',])
- giptm = GenericIPAddressTestModel(v4_ip="294.4 .2.1")
- self.assertFailsValidation(giptm.full_clean, ['v4_ip',])
giptm = GenericIPAddressTestModel(v4_ip="2001::2")
self.assertFailsValidation(giptm.full_clean, ['v4_ip',])
def test_correct_v6_ip_passes(self):
giptm = GenericIPAddressTestModel(v6_ip="2001::2")
self.assertEqual(None, giptm.full_clean())
- giptm = GenericIPAddressTestModel(v6_ip=" 2001::2 ")
- self.assertEqual(None, giptm.full_clean())
def test_invalid_v6_ip_raises_error(self):
giptm = GenericIPAddressTestModel(v6_ip="1.2.3.4")
self.assertFailsValidation(giptm.full_clean, ['v6_ip',])
- giptm = GenericIPAddressTestModel(v6_ip="2001:: 2")
- self.assertFailsValidation(giptm.full_clean, ['v6_ip',])
giptm = GenericIPAddressTestModel(v6_ip="1:2")
self.assertFailsValidation(giptm.full_clean, ['v6_ip',])
def test_v6_uniqueness_detection(self):
# These two addresses are the same with different syntax
- giptm = GenericIPAddressTestModel(generic_ip=" 2001::1:0:0:0:0:2")
+ giptm = GenericIPAddressTestModel(generic_ip="2001::1:0:0:0:0:2")
giptm.save()
- giptm = GenericIPAddressTestModel(generic_ip="2001:0:1:2 ")
+ giptm = GenericIPAddressTestModel(generic_ip="2001:0:1:2")
self.assertFailsValidation(giptm.full_clean, ['generic_ip',])
def test_v4_unpack_uniqueness_detection(self):
View
6 tests/regressiontests/i18n/tests.py
@@ -957,8 +957,7 @@ def test_localized_language_info(self):
self.assertEqual(li['bidi'], False)
def test_unknown_language_code(self):
- with self.assertRaisesRegexp(KeyError, "Unknown language code xx."):
- get_language_info('xx')
+ six.assertRaisesRegex(self, KeyError, "Unknown language code '?xx'?.", get_language_info, 'xx-xx')
def test_unknown_only_country_code(self):
li = get_language_info('de-xx')
@@ -968,8 +967,7 @@ def test_unknown_only_country_code(self):
self.assertEqual(li['bidi'], False)
def test_unknown_language_code_and_country_code(self):
- with self.assertRaisesRegexp(KeyError, "Unknown language code xx-xx and xx."):
- get_language_info('xx-xx')
+ six.assertRaisesRegex(self, KeyError, "Unknown language code '?xx-xx'? and '?xx'?.", get_language_info, 'xx-xx')
class MultipleLocaleActivationTests(TestCase):
Something went wrong with that request. Please try again.