From 371144f13401d8742876b77f5407011eb50f93d8 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Sun, 24 Jul 2005 22:21:09 +0000 Subject: [PATCH] A bunch of generics: documentation of generic views; cleaned up existing generic views, and added create/update generic views. git-svn-id: http://code.djangoproject.com/svn/django/trunk@304 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/views/generic/create_update.py | 185 ++++++++++++++++ django/views/generic/date_based.py | 23 +- django/views/generic/list_detail.py | 7 +- docs/generic_views.txt | 301 ++++++++++++++++++++++++++ 4 files changed, 508 insertions(+), 8 deletions(-) create mode 100644 django/views/generic/create_update.py create mode 100644 docs/generic_views.txt diff --git a/django/views/generic/create_update.py b/django/views/generic/create_update.py new file mode 100644 index 0000000000..35ce31713d --- /dev/null +++ b/django/views/generic/create_update.py @@ -0,0 +1,185 @@ +from django import models +from django.core.xheaders import populate_xheaders +from django.core import template_loader, formfields +from django.views.auth.login import redirect_to_login +from django.core.extensions import DjangoContext as Context +from django.core.paginator import ObjectPaginator, InvalidPage +from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect +from django.core.exceptions import Http404, ObjectDoesNotExist, ImproperlyConfigured + +def create_object(request, app_label, module_name, template_name=None, + extra_context=None, post_save_redirect=None, login_required=False): + """ + Generic object-creation function. + + Templates: ``/_form`` + Context: + form + the form wrapper for the object + """ + if login_required and request.user.is_anonymous(): + return redirect_to_login(request) + + mod = models.get_module(app_label, module_name) + manipulator = mod.AddManipulator() + if request.POST: + # If data was POSTed, we're trying to create a new object + new_data = request.POST.copy() + + # Check for errors + errors = manipulator.get_validation_errors(new_data) + + if not errors: + # No errors -- this means we can save the data! + manipulator.do_html2python(new_data) + new_object = manipulator.save(new_data) + + if not request.user.is_anonymous(): + request.user.add_message("The %s was created sucessfully." % mod.Klass._meta.verbose_name) + + # Redirect to the new object: first by trying post_save_redirect, + # then by obj.get_absolute_url; fail if neither works. + if post_save_redirect: + return HttpResponseRedirect(post_save_redirect % new_object.__dict__) + elif hasattr(new_object, 'get_absolute_url'): + return HttpResponseRedirect(new_object.get_absolute_url()) + else: + raise ImproperlyConfigured("No URL to redirect to from generic create view.") + else: + # No POST, so we want a brand new form without any data or errors + errors = new_data = {} + + # Create the FormWrapper, template, context, response + form = formfields.FormWrapper(manipulator, new_data, errors) + if not template_name: + template_name = "%s/%s_form" % (app_label, module_name) + t = template_loader.get_template(template_name) + c = Context(request, { + 'form' : form, + }) + if extra_context: + c.update(extra_context) + return HttpResponse(t.render(c)) + +def update_object(request, app_label, module_name, object_id=None, slug=None, + slug_field=None, template_name=None, extra_lookup_kwargs={}, + extra_context=None, post_save_redirect=None, login_required=False): + """ + Generic object-update function. + + Templates: ``/_form`` + Context: + form + the form wrapper for the object + object + the original object being edited + """ + if login_required and request.user.is_anonymous(): + return redirect_to_login(request) + + mod = models.get_module(app_label, module_name) + + # Look up the object to be edited + lookup_kwargs = {} + if object_id: + lookup_kwargs['%s__exact' % mod.Klass._meta.pk.name] = object_id + elif slug and slug_field: + lookup_kwargs['%s__exact' % slug_field] = slug + else: + raise AttributeError("Generic edit view must be called with either an object_id or a slug/slug_field") + lookup_kwargs.update(extra_lookup_kwargs) + try: + object = mod.get_object(**lookup_kwargs) + except ObjectDoesNotExist: + raise Http404("%s.%s does not exist for %s" % (app_label, module_name, lookup_kwargs)) + + manipulator = mod.ChangeManipulator(object.id) + + if request.POST: + new_data = request.POST.copy() + errors = manipulator.get_validation_errors(new_data) + if not errors: + manipulator.do_html2python(new_data) + manipulator.save(new_data) + + if not request.user.is_anonymous(): + request.user.add_message("The %s was updated sucessfully." % mod.Klass._meta.verbose_name) + + # Do a post-after-redirect so that reload works, etc. + if post_save_redirect: + return HttpResponseRedirect(post_save_redirect % object.__dict__) + elif hasattr(object, 'get_absolute_url'): + return HttpResponseRedirect(object.get_absolute_url()) + else: + raise ImproperlyConfigured("No URL to redirect to from generic create view.") + else: + errors = {} + # This makes sure the form acurate represents the fields of the place. + new_data = object.__dict__ + + form = formfields.FormWrapper(manipulator, new_data, errors) + if not template_name: + template_name = "%s/%s_form" % (app_label, module_name) + t = template_loader.get_template(template_name) + c = Context(request, { + 'form' : form, + 'object' : object, + }) + if extra_context: + c.update(extra_context) + response = HttpResponse(t.render(c)) + populate_xheaders(request, response, app_label, module_name, getattr(object, object._meta.pk.name)) + return response + +def delete_object(request, app_label, module_name, post_delete_redirect, + object_id=None, slug=None, slug_field=None, template_name=None, + extra_lookup_kwargs={}, extra_context=None, login_required=False): + """ + Generic object-delete function. + + The given template will be used to confirm deletetion if this view is + fetched using GET; for safty, deletion will only be performed if this + view is POSTed. + + Templates: ``/_confirm_delete`` + Context: + object + the original object being deleted + """ + if login_required and request.user.is_anonymous(): + return redirect_to_login(request) + + mod = models.get_module(app_label, module_name) + + # Look up the object to be edited + lookup_kwargs = {} + if object_id: + lookup_kwargs['%s__exact' % mod.Klass._meta.pk.name] = object_id + elif slug and slug_field: + lookup_kwargs['%s__exact' % slug_field] = slug + else: + raise AttributeError("Generic delete view must be called with either an object_id or a slug/slug_field") + lookup_kwargs.update(extra_lookup_kwargs) + try: + object = mod.get_object(**lookup_kwargs) + except ObjectDoesNotExist: + raise Http404("%s.%s does not exist for %s" % (app_label, module_name, lookup_kwargs)) + + if request.META['REQUEST_METHOD'] == 'POST': + object.delete() + if not request.user.is_anonymous(): + request.user.add_message("The %s was deleted." % mod.Klass._meta.verbose_name) + return HttpResponseRedirect(post_delete_redirect) + else: + if not template_name: + template_name = "%s/%s_confirm_delete" % (app_label, module_name) + t = template_loader.get_template(template_name) + c = Context(request, { + 'object' : object, + }) + if extra_context: + c.update(extra_context) + response = HttpResponse(t.render(c)) + populate_xheaders(request, response, app_label, module_name, getattr(object, object._meta.pk.name)) + return response + diff --git a/django/views/generic/date_based.py b/django/views/generic/date_based.py index c204ee035a..80ac2647a1 100644 --- a/django/views/generic/date_based.py +++ b/django/views/generic/date_based.py @@ -6,7 +6,8 @@ from django.models import get_module from django.utils.httpwrappers import HttpResponse import datetime, time -def archive_index(request, app_label, module_name, date_field, num_latest=15, template_name=None, extra_lookup_kwargs={}, extra_context=None): +def archive_index(request, app_label, module_name, date_field, num_latest=15, + template_name=None, extra_lookup_kwargs={}, extra_context=None): """ Generic top-level archive of date-based objects. @@ -44,7 +45,8 @@ def archive_index(request, app_label, module_name, date_field, num_latest=15, te c.update(extra_context) return HttpResponse(t.render(c)) -def archive_year(request, year, app_label, module_name, date_field, template_name=None, extra_lookup_kwargs={}, extra_context=None): +def archive_year(request, year, app_label, module_name, date_field, + template_name=None, extra_lookup_kwargs={}, extra_context=None): """ Generic yearly archive view. @@ -76,7 +78,8 @@ def archive_year(request, year, app_label, module_name, date_field, template_nam c.update(extra_context) return HttpResponse(t.render(c)) -def archive_month(request, year, month, app_label, module_name, date_field, template_name=None, extra_lookup_kwargs={}, extra_context=None): +def archive_month(request, year, month, app_label, module_name, date_field, + template_name=None, extra_lookup_kwargs={}, extra_context=None): """ Generic monthly archive view. @@ -122,7 +125,9 @@ def archive_month(request, year, month, app_label, module_name, date_field, temp c.update(extra_context) return HttpResponse(t.render(c)) -def archive_day(request, year, month, day, app_label, module_name, date_field, template_name=None, extra_lookup_kwargs={}, extra_context=None, allow_empty=False): +def archive_day(request, year, month, day, app_label, module_name, date_field, + template_name=None, extra_lookup_kwargs={}, extra_context=None, + allow_empty=False): """ Generic daily archive view. @@ -178,7 +183,9 @@ def archive_today(request, **kwargs): }) return archive_day(request, **kwargs) -def object_detail(request, year, month, day, app_label, module_name, date_field, object_id=None, slug=None, slug_field=None, template_name=None, extra_lookup_kwargs={}, extra_context=None): +def object_detail(request, year, month, day, app_label, module_name, date_field, + object_id=None, slug=None, slug_field=None, template_name=None, + template_name_field=None, extra_lookup_kwargs={}, extra_context=None): """ Generic detail view from year/month/day/slug or year/month/day/id structure. @@ -212,7 +219,11 @@ def object_detail(request, year, month, day, app_label, module_name, date_field, raise Http404("%s.%s does not exist for %s" % (app_label, module_name, lookup_kwargs)) if not template_name: template_name = "%s/%s_detail" % (app_label, module_name) - t = template_loader.get_template(template_name) + if template_name_field: + template_name_list = [getattr(object, template_name_field), template_name] + t = template_loader.select_template(template_name_list) + else: + t = template_loader.get_template(template_name) c = Context(request, { 'object': object, }) diff --git a/django/views/generic/list_detail.py b/django/views/generic/list_detail.py index c5b2b9bbb3..4723f254ab 100644 --- a/django/views/generic/list_detail.py +++ b/django/views/generic/list_detail.py @@ -6,7 +6,8 @@ from django.core.extensions import DjangoContext as Context from django.core.paginator import ObjectPaginator, InvalidPage from django.core.exceptions import Http404, ObjectDoesNotExist -def object_list(request, app_label, module_name, paginate_by=None, allow_empty=False, template_name=None, extra_lookup_kwargs={}, extra_context=None): +def object_list(request, app_label, module_name, paginate_by=None, allow_empty=False, + template_name=None, extra_lookup_kwargs={}, extra_context=None): """ Generic list of objects. @@ -67,7 +68,9 @@ def object_list(request, app_label, module_name, paginate_by=None, allow_empty=F t = template_loader.get_template(template_name) return HttpResponse(t.render(c)) -def object_detail(request, app_label, module_name, object_id=None, slug=None, slug_field=None, template_name=None, template_name_field=None, extra_lookup_kwargs={}, extra_context=None): +def object_detail(request, app_label, module_name, object_id=None, slug=None, + slug_field=None, template_name=None, template_name_field=None, + extra_lookup_kwargs={}, extra_context=None): """ Generic list of objects. diff --git a/docs/generic_views.txt b/docs/generic_views.txt new file mode 100644 index 0000000000..28e96404e1 --- /dev/null +++ b/docs/generic_views.txt @@ -0,0 +1,301 @@ +=================== +Using generic views +=================== + +Writing web applications can often be monotonous as we repeat certain patterns +again and again. In Django, the most common of these patterns have been abstracted into +"generic views" that let you quickly provide common views of object without actually +needing to write any views. + +Django's generic views contain the following: + + * A set of views for doing list/detail interfaces (for example, + Django's `documentation index`_ and `detail pages`_). + + * A set of views for year/month/day archive pages and associated + detail and "latest" pages (for example, the Django weblog's year_, + month_, day_, detail_, and latest_ pages). + + * A set of views for creating, editing, and deleting objects. + +.. _`documentation index`: http://www.djangoproject.com/documentation/ +.. _`detail pages`: http://www.djangoproject.com/documentation/faq/ +.. _year: http://www.djangoproject.com/weblog/2005/ +.. _month: http://www.djangoproject.com/weblog/2005/jul/ +.. _day: http://www.djangoproject.com/weblog/2005/jul/20/ +.. _detail: http://www.djangoproject.com/weblog/2005/jul/20/autoreload/ +.. _latest: http://www.djangoproject.com/weblog/ + +All of these views are used by creating configuration dictionaries in +your urlconfig files and passing those dicts as the third member of the +urlconf tuple. For example, here's the urlconf for the simple weblog +app that drives the blog on djangoproject.com:: + + from django.conf.urls.defaults import * + + info_dict = { + 'app_label': 'blog', + 'module_name': 'entries', + 'date_field': 'pub_date', + } + + urlpatterns = patterns('django.views.generic.date_based', + (r'^(?P\d{4})/(?P[a-z]{3})/(?P\w{1,2})/(?P\w+)/$', 'object_detail', dict(info_dict, slug_field='slug')), + (r'^(?P\d{4})/(?P[a-z]{3})/(?P\w{1,2})/$', 'archive_day', info_dict), + (r'^(?P\d{4})/(?P[a-z]{3})/$', 'archive_month', info_dict), + (r'^(?P\d{4})/$', 'archive_year', info_dict), + (r'^/?$', 'archive_index', info_dict), + ) + +As you can see, this urlconf defines a few options in ``info_dict`` that tell +the generic view which model to use (``blog.entries`` in this case), as well as +some extra information. + +Documentation of each generic view follows along with a list of all keyword arguments +that a generic view expects. Remember that as in the example above, arguments may +either come from the URL pattern (as ``month``, ``day``, ``year``, etc. do above) or +from the additional information dict (as for ``app_label``, ``module_name``, etc.). + +All the generic views that follow require the ``app_label`` and ``module_name`` keys. +These values are easiest to explain through example:: + + >>> from django.models.blog import entries + +In the above line, ``blog`` is the ``app_label`` (this is the name of the file that +holds all your model definitions) and ``entries`` is the ``module_name`` (this is +either a pluralized, lowercased version of the model class name or the value of +the ``module_name`` option of your model). In the docs below, these keys will not +be repeated, but each generic view requires them. + +Using date-based generic views +============================== + +Date-based generic views (in the module ``django.views.generic.date_based``) +export six functions for dealing with date-based data. Besides ``app_label`` +and ``module_name``, all date-based generic views require that the ``date_field`` +argument to passed to them; this is the name of the field that stores the date +the objects should key off of. + +Additional, all date-based generic views have the following optional arguments: + + ======================= ================================================== + Argument Description + ======================= ================================================== + ``template_name`` Override the default template name used for the + view. + + ``extra_lookup_kwargs`` A dictionary of extra lookup parameters (see + the `database API docs`_). + + ``extra_context`` A dict of extra data to put into the template's + context. + ======================= ================================================== + +.. _`database API docs`: http://www.djangoproject.com/documentation/db_api/ + +The date-based generic functions are: + +``archive_index`` + A top-level index page showing the "latest" objects. Has an optional argument, + ``num_latest`` which is the number of items to display on the page (defaults + to 15). + + Uses the template ``app_label/module_name_archive`` by default. + + Has the following template context: + + ``date_list`` + List of years with objects + ``latest`` + Latest objects by date + +``archive_year`` + Yearly archive. Requires that the ``year`` argument be present in the URL + pattern. + + Uses the template ``app_label/module_name__archive_year`` by default. + + Has the following template context: + + ``date_list`` + List of months in this year with objects + ``year`` + This year + +``archive_month`` + Monthly archive; requires that ``year`` and ``month`` arguments be given. + + Uses the template ``app_label/module_name__archive_month`` by default. + + Has the following template context: + + ``month`` + (datetime object) this month + ``object_list`` + list of objects published in the given month + +``archive_day`` + Daily archive; requires that ``year``, ``month``, and ``day`` arguments + be given. + + Uses the template ``app_label/module_name__archive_day`` by default. + + Has the following template context: + + ``object_list`` + list of objects published this day + ``day`` + (datetime) the day + ``previous_day`` + (datetime) the previous day + ``next_day`` + (datetime) the next day, or None if the current day is today + + +``archive_today`` + List of objects for today; exactly the same as ``archive_day``, except + that the year/month/day arguments are not given and today's date is + used instead. + +``object_detail`` + Individual object page; requires ``year``/``month``/``day`` arguments like + ``archive_day``. This function can be used with two types of URLs: either + ``/year/month/day/slug/`` or ``/year/month/day/object_id/``. + + If you're using the slug-style URLs, you'll need to have a ``slug`` item in + your urlconf, and you'll need to pass a ``slug_field`` key in your info + dict to indicate the name of the slug field. + + If your using the object_id-style URLs, you'll just need to have the URL + pattern have an ``object_id`` field. + + You can also pass the ``template_name_field`` argument to indicate that the + the object stores the name of its template in a field on the object itself. + +Using list/detail generic views +=============================== + +The list-detail generic views (in the ``django.views.generic.list_detail`` module) +are similar to the data-based ones, except the list-detail views simply have two +views: a list of objects, and an individual object page. + +All these views take the same three optional arguments as the date-based ones do +(and they obviously do not accept or require the date field argument). + +Individual views are: + +``object_list`` + List of objects. + + Takes the following optional arguments: + + ======================= ================================================= + Argument Description + ======================= ================================================= + ``paginate_by`` If set to an integer, the view will paginate + objects with ``paginate_by`` objects per page. + The view will expect a ``page`` GET param with + the (zero-indexed) page number. + + ``allow_empty`` If ``False`` and there are no objects to display + the view will raise a 404 instead of displaying + an empty index page. + ======================= ================================================= + + Uses the template ``app_label/module_name__list`` by default. + + Has the following template context: + + ``object_list`` + list of objects + ``is_paginated`` + are the results paginated? + + If the results are paginated, the context will have some extra variables: + + ``results_per_page`` + number of objects per page + ``has_next`` + is there a next page? + ``has_previous`` + is there a prev page? + ``page`` + the current page + ``next`` + the next page + ``previous`` + the previous page + ``pages`` + number of pages, total + +``object_detail`` + Object detail page. This works like and takes the same arguments as + the date-based ``object_detail`` above, except this one obviously + does not take the year/month/day arguments. + +Using create/update/delete generic views +======================================== + +The ``django.views.generic.create_update`` module contains a set of functions +for creating, editing, and deleting objects. These views take the same global +arguments as the above sets of generic views; they also have a +``login_required`` argument which, if ``True``, requires the user to be logged +in to have access to the page (``login_required`` defaults to ``False``). + +The create/update/delete views are: + +``create_object`` + Create a new object. Has an extra optional argument, ``post_save_redirect``, + which is a URL that the view will redirect to after saving the object + (defaults to ``object.get_absolute_url()``). + + ``post_save_redirect`` may contain dictionary string formatting which will + be interpolated against the object's dict (so you could use + ``post_save_redirect="/polls/%(slug)s/"``, for example). + + Uses the template ``app_label/module_name__form`` by default (this is the + same template as the ``update_object`` view below; your template can tell + the different by the presence or absence of ``{{ object }}`` in the context. + + Has the following template context: + + form + the form wrapper for the object + + .. admonition:: Note + + See the `manipulator and formfield documentation`_ for more information + about using form wrappers in templates. + +.. _`manipulator and formfield documentation`: http://www.djangoproject.com/documentation/forms/ + +``update_object`` + Edit an existing object. Has the same extra slug/ID parameters as + ``list_detail.object_detail`` does (see above), and the same ``post_save_redirect`` + as ``create_object`` does. + + Uses the template ``app_label/module_name__form`` by default. + + Has the following template context: + + form + the form wrapper for the object + object + the original object being edited + +``delete_object`` + Delete an existing object. The given object will only actually be deleted if + the request method is POST; if this view is fetched with GET it will display + a confirmation page that should contain a form that POSTs to the same URL. + + You must provide the ``post_delete_redirect`` argument to this function so + that the view knows where to go after the object is deleted. + + If fetched with GET, uses the template + ``app_label/module_name_s_confirm_delete`` by default (uses no template if + POSTed; simply deletes the object). + + Has the following template context: + + object + the object about to be deleted \ No newline at end of file