From 3dd1d28c7fd172f8aefeca1831c89ba2d85ee48f Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Thu, 29 Sep 2005 19:56:17 +0000 Subject: [PATCH 001/117] Clarified potentially confusing sentence in docs/modpython.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@728 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/modpython.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modpython.txt b/docs/modpython.txt index 72c2d9a073..acac50a6d5 100644 --- a/docs/modpython.txt +++ b/docs/modpython.txt @@ -29,8 +29,8 @@ Then edit your ``httpd.conf`` file and add the following:: PythonDebug On -...and replace ``myproject.settings.main`` with the path to your settings file, -in dotted-package syntax. +...and replace ``myproject.settings.main`` with the Python path to your +settings file. This tells Apache: "Use mod_python for any URL at or under '/mysite/', using the Django mod_python handler." It passes the value of ``DJANGO_SETTINGS_MODULE`` From 2425907eacc8e315703404fd34faf242f5cc405b Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Thu, 29 Sep 2005 22:34:17 +0000 Subject: [PATCH 002/117] Fixed #576 - popups no longer show "save & continue" buttons. Thanks, Hein-Pieter git-svn-id: http://code.djangoproject.com/svn/django/trunk@731 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/views/admin/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/views/admin/main.py b/django/views/admin/main.py index a6109a46cd..9283230b26 100644 --- a/django/views/admin/main.py +++ b/django/views/admin/main.py @@ -523,7 +523,7 @@ def _get_submit_row_template(opts, app_label, add, change, show_delete, ordered_ if not opts.admin.save_as or add: t.append('{%% if not is_popup %%}{%% endif %%}' % \ (ordered_objects and change and 'onclick="submitOrderForm();"' or '')) - t.append('' % \ + t.append('{%% if not is_popup %%}{%% endif %%}' % \ (ordered_objects and change and 'onclick="submitOrderForm();"' or '')) t.append('' % \ (ordered_objects and change and 'onclick="submitOrderForm();"' or '')) From bab70003fe61846c83cf49ed9112ffeeac0da106 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Thu, 29 Sep 2005 22:36:48 +0000 Subject: [PATCH 003/117] Fixed #574 - small CSS issue in admin tables. Thanks, Hein-Pieter git-svn-id: http://code.djangoproject.com/svn/django/trunk@732 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/admin_media/css/changelists.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/django/conf/admin_media/css/changelists.css b/django/conf/admin_media/css/changelists.css index 3c6e125e7d..966ca4a486 100644 --- a/django/conf/admin_media/css/changelists.css +++ b/django/conf/admin_media/css/changelists.css @@ -18,10 +18,10 @@ #changelist {position:relative; width:100%;} #changelist table {width:100%;} -.change-list .filtered table { border-right:1px solid #ddd; } +.change-list .filtered table { border-right:1px solid #ddd, width:100%; } .change-list .filtered {min-height:400px; _height:400px;} .change-list .filtered {background:white url(../img/admin/changelist-bg.gif) top right repeat-y !important;} -.change-list .filtered table, .filtered .paginator, .filtered #toolbar, .filtered div.xfull {margin-right:160px !important; width:auto !important; } +.change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull {margin-right:160px !important; width:auto !important; } .change-list .filtered table tbody th {padding-right:10px;} #changelist .toplinks {border-bottom:1px solid #ccc !important;} #changelist .paginator { color:#666; border-top:1px solid #eee; border-bottom:1px solid #eee; background:white url(../img/admin/nav-bg.gif) 0 180% repeat-x; overflow:hidden;} @@ -57,4 +57,4 @@ .change-list ul.toplinks {display:block; background:white url(../img/admin/nav-bg-reverse.gif) 0 -10px repeat-x; border-top:1px solid white; float:left; padding:0 !important; margin:0 !important; width:100%;} .change-list ul.toplinks li {float: left; width: 9em; padding:3px 6px; font-weight: bold; list-style-type:none;} .change-list ul.toplinks .date-back a {color:#999;} -.change-list ul.toplinks .date-back a:hover {color:#036;} \ No newline at end of file +.change-list ul.toplinks .date-back a:hover {color:#036;} From 022a03ab32e85407109f2eda68b8725679c2f040 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Thu, 29 Sep 2005 23:16:29 +0000 Subject: [PATCH 004/117] Fixed #541 - generic views now may take a {{{template_loader}}} argument so they can use a different template loader than Django's own. Thanks, Joao. git-svn-id: http://code.djangoproject.com/svn/django/trunk@734 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/views/generic/create_update.py | 11 +++++++---- django/views/generic/date_based.py | 16 ++++++++++------ django/views/generic/list_detail.py | 6 ++++-- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/django/views/generic/create_update.py b/django/views/generic/create_update.py index d1775d61e9..f06e1d57e5 100644 --- a/django/views/generic/create_update.py +++ b/django/views/generic/create_update.py @@ -8,7 +8,8 @@ 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={}, post_save_redirect=None, login_required=False): + template_loader=template_loader, extra_context={}, + post_save_redirect=None, login_required=False): """ Generic object-creation function. @@ -65,8 +66,9 @@ def create_object(request, app_label, module_name, template_name=None, 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={}, post_save_redirect=None, login_required=False): + slug_field=None, template_name=None, template_loader=template_loader, + extra_lookup_kwargs={}, extra_context={}, post_save_redirect=None, + login_required=False): """ Generic object-update function. @@ -139,7 +141,8 @@ def update_object(request, app_label, module_name, object_id=None, slug=None, 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={}, login_required=False): + template_loader=template_loader, extra_lookup_kwargs={}, + extra_context={}, login_required=False): """ Generic object-delete function. diff --git a/django/views/generic/date_based.py b/django/views/generic/date_based.py index 0e726f2a78..5dc9892894 100644 --- a/django/views/generic/date_based.py +++ b/django/views/generic/date_based.py @@ -7,7 +7,8 @@ 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={}): + template_name=None, template_loader=template_loader, + extra_lookup_kwargs={}, extra_context={}): """ Generic top-level archive of date-based objects. @@ -49,7 +50,8 @@ def archive_index(request, app_label, module_name, date_field, num_latest=15, return HttpResponse(t.render(c)) def archive_year(request, year, app_label, module_name, date_field, - template_name=None, extra_lookup_kwargs={}, extra_context={}): + template_name=None, template_loader=template_loader, + extra_lookup_kwargs={}, extra_context={}): """ Generic yearly archive view. @@ -85,8 +87,8 @@ def archive_year(request, year, app_label, module_name, date_field, return HttpResponse(t.render(c)) def archive_month(request, year, month, app_label, module_name, date_field, - month_format='%b', template_name=None, extra_lookup_kwargs={}, - extra_context={}): + month_format='%b', template_name=None, template_loader=template_loader, + extra_lookup_kwargs={}, extra_context={}): """ Generic monthly archive view. @@ -138,7 +140,8 @@ def archive_month(request, year, month, app_label, module_name, date_field, def archive_day(request, year, month, day, app_label, module_name, date_field, month_format='%b', day_format='%d', template_name=None, - extra_lookup_kwargs={}, extra_context={}, allow_empty=False): + template_loader=template_loader, extra_lookup_kwargs={}, + extra_context={}, allow_empty=False): """ Generic daily archive view. @@ -201,7 +204,8 @@ def archive_today(request, **kwargs): def object_detail(request, year, month, day, app_label, module_name, date_field, month_format='%b', day_format='%d', object_id=None, slug=None, slug_field=None, template_name=None, template_name_field=None, - extra_lookup_kwargs={}, extra_context={}): + template_loader=template_loader, extra_lookup_kwargs={}, + extra_context={}): """ Generic detail view from year/month/day/slug or year/month/day/id structure. diff --git a/django/views/generic/list_detail.py b/django/views/generic/list_detail.py index 373aef3e18..6328b7097a 100644 --- a/django/views/generic/list_detail.py +++ b/django/views/generic/list_detail.py @@ -7,7 +7,8 @@ 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={}): + template_name=None, template_loader=template_loader, + extra_lookup_kwargs={}, extra_context={}): """ Generic list of objects. @@ -76,7 +77,8 @@ def object_list(request, app_label, module_name, paginate_by=None, allow_empty=F 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={}): + template_loader=template_loader, extra_lookup_kwargs={}, + extra_context={}): """ Generic list of objects. From 151b44c89a6c53bc5e312fd84afeedffb23a0e94 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Thu, 29 Sep 2005 23:23:11 +0000 Subject: [PATCH 005/117] Fixed #363 - django-admin sqlall now uses database-specific initial data files if they exist. git-svn-id: http://code.djangoproject.com/svn/django/trunk@735 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/management.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/django/core/management.py b/django/core/management.py index d494564d6b..89ffb80b59 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -163,7 +163,18 @@ def get_sql_initial_data(mod): for klass in mod._MODELS: opts = klass._meta # Add custom SQL, if it's available. - sql_file_name = os.path.join(app_dir, opts.module_name + '.sql') + from django.core import db + + # Get the sql file name for the init data for the current database engine + db_engine_sql_file_name = os.path.join(app_dir, opts.module_name + '.' + db.DATABASE_ENGINE.lower() + '.sql') + + # Check if the data specific file exists + if os.path.exists(db_engine_sql_file_name): + sql_file_name = db_engine_sql_file_name + # if the database specific file doesn't exist, use the database agnostic version + else: + sql_file_name = os.path.join(app_dir, opts.module_name + '.sql') + if os.path.exists(sql_file_name): fp = open(sql_file_name, 'r') output.append(fp.read()) From 27b1f69d79787af2c2db35b4d2a96784a59d39a7 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Thu, 29 Sep 2005 23:34:29 +0000 Subject: [PATCH 006/117] Fixed #295 - added {{{forloop.revcounter}}} and {{{forloop.revcounter0}}} variables to for loops. Also updated the docs and added unit tests to verify correct behavior. Thanks, Clint. git-svn-id: http://code.djangoproject.com/svn/django/trunk@736 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/defaulttags.py | 7 +++++++ docs/templates.txt | 4 ++++ tests/othertests/templates.py | 4 ++++ 3 files changed, 15 insertions(+) diff --git a/django/core/defaulttags.py b/django/core/defaulttags.py index 103eb4c9f4..e86b385f9c 100644 --- a/django/core/defaulttags.py +++ b/django/core/defaulttags.py @@ -97,6 +97,9 @@ class ForNode(template.Node): # shortcuts for current loop iteration number 'counter0': i, 'counter': i+1, + # reverse counter iteration numbers + 'revcounter': len_values - i, + 'revcounter0': len_values - i - 1, # boolean values designating first and last times through loop 'first': (i == 0), 'last': (i == len_values - 1), @@ -431,6 +434,10 @@ def do_for(parser, token): ========================== ================================================ ``forloop.counter`` The current iteration of the loop (1-indexed) ``forloop.counter0`` The current iteration of the loop (0-indexed) + ``forloop.revcounter`` The number of iterations from the end of the + loop (1-indexed) + ``forloop.revcounter0`` The number of iterations from the end of the + loop (0-indexed) ``forloop.first`` True if this is the first time through the loop ``forloop.last`` True if this is the last time through the loop ``forloop.parentloop`` For nested loops, this is the loop "above" the diff --git a/docs/templates.txt b/docs/templates.txt index 09431c1dda..a6848a9638 100644 --- a/docs/templates.txt +++ b/docs/templates.txt @@ -376,6 +376,10 @@ Built-in tag reference ========================== ================================================ ``forloop.counter`` The current iteration of the loop (1-indexed) ``forloop.counter0`` The current iteration of the loop (0-indexed) + ``forloop.revcounter`` The number of iterations from the end of the + loop (1-indexed) + ``forloop.revcounter0`` The number of iterations from the end of the + loop (0-indexed) ``forloop.first`` True if this is the first time through the loop ``forloop.last`` True if this is the last time through the loop ``forloop.parentloop`` For nested loops, this is the loop "above" the diff --git a/tests/othertests/templates.py b/tests/othertests/templates.py index 31fea0e1ba..fb96cfeadd 100644 --- a/tests/othertests/templates.py +++ b/tests/othertests/templates.py @@ -107,6 +107,10 @@ TEMPLATE_TESTS = { ### FOR TAG ############################################################### 'for-tag01': ("{% for val in values %}{{ val }}{% endfor %}", {"values": [1, 2, 3]}, "123"), 'for-tag02': ("{% for val in values reversed %}{{ val }}{% endfor %}", {"values": [1, 2, 3]}, "321"), + 'for-tag-vars01': ("{% for val in values %}{{ forloop.counter }}{% endfor %}", {"values": [6, 6, 6]}, "123"), + 'for-tag-vars02': ("{% for val in values %}{{ forloop.counter0 }}{% endfor %}", {"values": [6, 6, 6]}, "012"), + 'for-tag-vars03': ("{% for val in values %}{{ forloop.revcounter }}{% endfor %}", {"values": [6, 6, 6]}, "321"), + 'for-tag-vars04': ("{% for val in values %}{{ forloop.revcounter0 }}{% endfor %}", {"values": [6, 6, 6]}, "210"), ### IFEQUAL TAG ########################################################### 'ifequal01': ("{% ifequal a b %}yes{% endifequal %}", {"a": 1, "b": 2}, ""), From 998fc72c0d10afb6df3c58aa965540a4e23af431 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Thu, 29 Sep 2005 23:43:03 +0000 Subject: [PATCH 007/117] Changed [735] so that database-agnostic SQL always gets executed, even if database-specific SQL doesn't exist. git-svn-id: http://code.djangoproject.com/svn/django/trunk@737 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/management.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/django/core/management.py b/django/core/management.py index 89ffb80b59..f3accd84cc 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -156,29 +156,23 @@ get_sql_reset.args = APP_ARGS def get_sql_initial_data(mod): "Returns a list of the initial INSERT SQL statements for the given module." + from django.core import db output = [] app_label = mod._MODELS[0]._meta.app_label output.append(_get_packages_insert(app_label)) app_dir = os.path.normpath(os.path.join(os.path.dirname(mod.__file__), '../sql')) for klass in mod._MODELS: opts = klass._meta + # Add custom SQL, if it's available. - from django.core import db - - # Get the sql file name for the init data for the current database engine - db_engine_sql_file_name = os.path.join(app_dir, opts.module_name + '.' + db.DATABASE_ENGINE.lower() + '.sql') + sql_files = [os.path.join(app_dir, opts.module_name + '.' + db.DATABASE_ENGINE + '.sql'), + os.path.join(app_dir, opts.module_name + '.sql')] + for sql_file in sql_files: + if os.path.exists(sql_file): + fp = open(sql_file) + output.append(fp.read()) + fp.close() - # Check if the data specific file exists - if os.path.exists(db_engine_sql_file_name): - sql_file_name = db_engine_sql_file_name - # if the database specific file doesn't exist, use the database agnostic version - else: - sql_file_name = os.path.join(app_dir, opts.module_name + '.sql') - - if os.path.exists(sql_file_name): - fp = open(sql_file_name, 'r') - output.append(fp.read()) - fp.close() # Content types. output.append(_get_contenttype_insert(opts)) # Permissions. @@ -664,4 +658,4 @@ def createcachetable(tablename): for statement in index_output: curs.execute(statement) db.db.commit() -createcachetable.args = "[tablename]" \ No newline at end of file +createcachetable.args = "[tablename]" From 1838c763070b5e060787d71a8b4d3819fd4871e6 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Fri, 30 Sep 2005 13:49:43 +0000 Subject: [PATCH 008/117] Fixed #472 - added notes about File/ImageFields from the FAQ to the model API doc git-svn-id: http://code.djangoproject.com/svn/django/trunk@742 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/model-api.txt | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/model-api.txt b/docs/model-api.txt index 918f4eb5db..4af193ca48 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -248,7 +248,28 @@ Here are all available field types: uploaded files don't fill up the given directory). The admin represents this as an ```` (a file-upload widget). + + Using a `FieldField` or an ``ImageField`` (see below) in a model takes a few + steps: + + 1. In your settings file, you'll need to define ``MEDIA_ROOT``as the + full path to a directory where you'd like Django to store uploaded + files. (For performance, these files are not stored in the database.) + Define ``MEDIA_URL`` as the base public URL of that directory. Make + sure that this directory is writable by the Web server's user + account. + + 2. Add the ``FileField`` or ``ImageField`` to your model, making sure + to define the ``upload_to`` option to tell Django to which + subdirectory of ``MEDIA_ROOT`` it should upload files. + 3. All that will be stored in your database is a path to the file + (relative to ``MEDIA_ROOT``). You'll must likely want to use the + convenience ``get__url`` function provided by Django. For + example, if your ``ImageField`` is called ``mug_shot``, you can get + the absolute URL to your image in a template with ``{{ + object.get_mug_shot_url }}``. + .. _`strftime formatting`: http://docs.python.org/lib/module-time.html#l2h-1941 ``FloatField`` @@ -281,7 +302,7 @@ Here are all available field types: width of the image each time a model instance is saved. Requires the `Python Imaging Library`_. - + .. _Python Imaging Library: http://www.pythonware.com/products/pil/ ``IntegerField`` From 6b2226bab8688b520dba56c778d20644e778f3d7 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Fri, 30 Sep 2005 13:58:30 +0000 Subject: [PATCH 009/117] Fixed #447 - the RSS framework can now output pub dates git-svn-id: http://code.djangoproject.com/svn/django/trunk@743 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/rss.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/django/core/rss.py b/django/core/rss.py index a381a9cd78..cd2bcf4ad1 100644 --- a/django/core/rss.py +++ b/django/core/rss.py @@ -7,7 +7,7 @@ from django.conf.settings import LANGUAGE_CODE, SETTINGS_MODULE class FeedConfiguration: def __init__(self, slug, title_cb, link_cb, description_cb, get_list_func_cb, get_list_kwargs, - param_func=None, param_kwargs_cb=None, get_list_kwargs_cb=None, + param_func=None, param_kwargs_cb=None, get_list_kwargs_cb=None, get_pubdate_cb=None, enc_url=None, enc_length=None, enc_mime_type=None): """ slug -- Normal Python string. Used to register the feed. @@ -28,6 +28,9 @@ class FeedConfiguration: get_list_kwargs_cb -- Function that takes the param and returns a dictionary to use in addition to get_list_kwargs (if applicable). + + get_pubdate_cb -- Function that takes the object and returns a datetime + to use as the publication date in the feed. The three enc_* parameters are strings representing methods or attributes to call on a particular item to get its enclosure @@ -41,6 +44,7 @@ class FeedConfiguration: self.get_list_kwargs = get_list_kwargs self.param_func, self.param_kwargs_cb = param_func, param_kwargs_cb self.get_list_kwargs_cb = get_list_kwargs_cb + self.get_pubdate_cb = get_pubdate_cb assert (None == enc_url == enc_length == enc_mime_type) or (enc_url is not None and enc_length is not None and enc_mime_type is not None) self.enc_url = enc_url self.enc_length = enc_length @@ -95,6 +99,7 @@ class FeedConfiguration: description = description_template.render(Context({'obj': obj, 'site': current_site})).decode('utf-8'), unique_id=link, enclosure=enc, + pubdate = self.get_pubdate_cb and self.get_pubdate_cb(obj) or None, ) return f From 5595fe2aa6863df838f246d7aea8f15c8160f76d Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 30 Sep 2005 16:38:03 +0000 Subject: [PATCH 010/117] Fixed typo in docs/tutorial03.txt. Thanks, Aggelos Orfanakos git-svn-id: http://code.djangoproject.com/svn/django/trunk@745 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/tutorial03.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial03.txt b/docs/tutorial03.txt index 2bbd8d7782..84fb64abfe 100644 --- a/docs/tutorial03.txt +++ b/docs/tutorial03.txt @@ -91,8 +91,8 @@ Finally, it calls that ``detail()`` function like so:: detail(request=, poll_id=23) The ``poll_id=23`` part comes from ``(?P\d+)``. Using -``(?pattern)`` "captures" the text matched by ``pattern`` and sends it as -a keyword argument to the view function. +``(?Ppattern)`` "captures" the text matched by ``pattern`` and sends it +as a keyword argument to the view function. Because the URL patterns are regular expressions, there really is no limit on what you can do with them. And there's no need to add URL cruft such as From acde573821ebd99a5b7df05e16f6a4d906d4870e Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 30 Sep 2005 16:39:05 +0000 Subject: [PATCH 011/117] Fixed typo in docs/db-api.txt. Thanks, Aggelos Orfanakos git-svn-id: http://code.djangoproject.com/svn/django/trunk@746 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/db-api.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/db-api.txt b/docs/db-api.txt index 4c49a2760f..e0885da8f0 100644 --- a/docs/db-api.txt +++ b/docs/db-api.txt @@ -524,7 +524,7 @@ model with an ``ImageField`` will also get this method. get_FOO_url() ------------- -For every ``FileField``, the object will have a ``get_FOO_filename()`` method, +For every ``FileField``, the object will have a ``get_FOO_url()`` method, where ``FOO`` is the name of the field. This returns the full URL to the file, according to your ``MEDIA_URL`` setting. If the value is blank, this method returns an empty string. From 7cc9526b2ea1ae17f54dbd0731a33ed2b7608dbc Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 30 Sep 2005 16:40:15 +0000 Subject: [PATCH 012/117] Fixed typo in docs/tutorial01.txt. Thanks, Aggelos Orfanakos git-svn-id: http://code.djangoproject.com/svn/django/trunk@747 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/tutorial01.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/tutorial01.txt b/docs/tutorial01.txt index 629a2ab017..d69afa7392 100644 --- a/docs/tutorial01.txt +++ b/docs/tutorial01.txt @@ -385,23 +385,23 @@ Let's jump back into the Python interactive shell:: # Django provides a rich database lookup API that's entirely driven by # keyword arguments. >>> polls.get_object(id__exact=1) - What's up + What's up? >>> polls.get_object(question__startswith='What') - What's up + What's up? >>> polls.get_object(pub_date__year=2005) - What's up + What's up? >>> polls.get_object(id__exact=2) Traceback (most recent call last): ... PollDoesNotExist: Poll does not exist for {'id__exact': 2} >>> polls.get_list(question__startswith='What') - [What's up] + [What's up?] # Lookup by a primary key is the most common case, so Django provides a # shortcut for primary-key exact lookups. # The following is identical to polls.get_object(id__exact=1). >>> polls.get_object(pk=1) - What's up + What's up? # Make sure our custom method worked. >>> p = polls.get_object(pk=1) @@ -419,7 +419,7 @@ Let's jump back into the Python interactive shell:: # Choice objects have API access to their related Poll objects. >>> c.get_poll() - What's up + What's up? # And vice versa: Poll objects get access to Choice objects. >>> p.get_choice_list() From a0595851b66b23b3e0c3252227d2babeeb708f8e Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 30 Sep 2005 22:02:51 +0000 Subject: [PATCH 013/117] Added docs/outputting_pdf.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@752 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/outputting_pdf.txt | 90 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 docs/outputting_pdf.txt diff --git a/docs/outputting_pdf.txt b/docs/outputting_pdf.txt new file mode 100644 index 0000000000..a448e227ba --- /dev/null +++ b/docs/outputting_pdf.txt @@ -0,0 +1,90 @@ +=========================== +Outputting PDFs with Django +=========================== + +This document explains how to output PDF files dynamically using Django views. +This is made possible by the excellent, open-source ReportLab_ Python PDF +library. + +The advantage of generating PDF files dynamically is that you can create +customzed PDFs for different purposes -- say, for different users or different +pieces of content. + +For example, Django was used at kusports.com to generate customized, +printer-friendly NCAA tournament brackets, as PDF files, for people +participating in a March Madness contest. + +.. _ReportLab: http://www.reportlab.org/rl_toolkit.html + +Install ReportLab +================= + +Download and install the ReportLab library from http://www.reportlab.org/downloads.html +The `user guide`_ (not coincidentally, a PDF file) explains how to install it. + +Test your installation by typing this in the Python interactive interpreter:: + + import reportlab + +If that command doesn't raise any errors, the installation worked. + +.. _user guide: http://www.reportlab.org/rsrc/userguide.pdf + +Write your view +=============== + +The key to generating PDFs dynamically with Django is that the ReportLab API +acts on file-like objects, and Django's ``HttpResponse`` objects are file-like +objects. + +.. admonition:: Note + + For more information on ``HttpResponse`` objects, see + `Request and response objects`_. + + .. _Request and response objects: http://www.djangoproject.com/documentation/request_response/ + +Here's a "Hello World" example:: + + from reportlab.pdfgen import canvas + from django.utils.httpwrappers import HttpResponse + + def some_view(request): + # Create the HttpResponse object with the appropriate PDF headers. + response = HttpResponse(mimetype='application/pdf') + response['Content-Disposition'] = 'attachment; filename=somefilename.pdf' + + # Create the PDF object, using the response object as its "file." + p = canvas.Canvas(response) + + # Draw things on the PDF. Here's where the PDF generation happens. + # See the ReportLab documentation for the full list of functionality. + p.drawString(100, 100, "Hello world.") + + # Close the PDF object cleanly, and we're done. + p.showPage() + p.save() + return response + +The code and comments should be self-explanatory, but a few things deserve a +mention: + + * The response gets a special mimetype, ``application/pdf``. This tells + browsers that the document is a PDF file, rather than an HTML file. If + you leave this off, browsers will probably interpret the output as HTML, + which would result in ugly, scary gobbledygook in the browser window. + + * The response gets an additional ``Content-Disposition`` header, which + contains the name of the PDF file. This filename is arbitrary: Call it + whatever you want. It'll be used by browsers in the "Save as..." + dialogue, etc. + + * Hooking into the ReportLab API is easy: Just pass ``response`` as the + first argument to ``canvas.Canvas``. The ``Canvas`` class expects a + file-like object, and ``HttpResponse`` objects fit the bill. + + * Note that all subsequent PDF-generation methods are called on the PDF + object (in this case, ``p``) -- not on ``response``. + + * Finally, it's important to call ``showPage()`` and ``save()`` on the PDF + file. From 53581d6d8da64a6a898d575af5af2182fea7c594 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 30 Sep 2005 22:05:44 +0000 Subject: [PATCH 014/117] Small tweak to docs/outputting_pdf.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@753 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/outputting_pdf.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/outputting_pdf.txt b/docs/outputting_pdf.txt index a448e227ba..b641f08a75 100644 --- a/docs/outputting_pdf.txt +++ b/docs/outputting_pdf.txt @@ -22,9 +22,9 @@ Install ReportLab Download and install the ReportLab library from http://www.reportlab.org/downloads.html The `user guide`_ (not coincidentally, a PDF file) explains how to install it. -Test your installation by typing this in the Python interactive interpreter:: +Test your installation by importing it in the Python interactive interpreter:: - import reportlab + >>> import reportlab If that command doesn't raise any errors, the installation worked. From da715287171bcbfd3068f0711e7b4eb95f827b6d Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 30 Sep 2005 22:07:19 +0000 Subject: [PATCH 015/117] Added missing period to docs/outputting_pdf.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@754 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/outputting_pdf.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/outputting_pdf.txt b/docs/outputting_pdf.txt index b641f08a75..68504599bd 100644 --- a/docs/outputting_pdf.txt +++ b/docs/outputting_pdf.txt @@ -19,7 +19,7 @@ participating in a March Madness contest. Install ReportLab ================= -Download and install the ReportLab library from http://www.reportlab.org/downloads.html +Download and install the ReportLab library from http://www.reportlab.org/downloads.html. The `user guide`_ (not coincidentally, a PDF file) explains how to install it. Test your installation by importing it in the Python interactive interpreter:: From 6f07f717c0b3cb2a849fd3533abbdc943d41e76b Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Tue, 4 Oct 2005 13:34:59 +0000 Subject: [PATCH 016/117] Fixed #591 -- fixed typo in docs/templates_python.txt. Thanks, Boffbowsh git-svn-id: http://code.djangoproject.com/svn/django/trunk@771 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/templates_python.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/templates_python.txt b/docs/templates_python.txt index 63b734dc44..39b768429b 100644 --- a/docs/templates_python.txt +++ b/docs/templates_python.txt @@ -267,7 +267,7 @@ every template automatic access to the current time, use something like this:: from django.core.template import Context import datetime - class TimeContext(template.Context): + class TimeContext(Context): def __init__(self, *args, **kwargs): Context.__init__(self, *args, **kwargs) self['current_time'] = datetime.datetime.now() From 78b8fcc235f4c6f89a5baa7811665e9dbcc95820 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Tue, 4 Oct 2005 15:17:22 +0000 Subject: [PATCH 017/117] Fixed typo in docs/outputting_pdf.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@772 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/outputting_pdf.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/outputting_pdf.txt b/docs/outputting_pdf.txt index 68504599bd..bd209b2e90 100644 --- a/docs/outputting_pdf.txt +++ b/docs/outputting_pdf.txt @@ -7,7 +7,7 @@ This is made possible by the excellent, open-source ReportLab_ Python PDF library. The advantage of generating PDF files dynamically is that you can create -customzed PDFs for different purposes -- say, for different users or different +customized PDFs for different purposes -- say, for different users or different pieces of content. For example, Django was used at kusports.com to generate customized, From 837afc5a29d506b363d782f0fb306acf34ee2bf3 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Tue, 4 Oct 2005 18:05:37 +0000 Subject: [PATCH 018/117] Changed default JING_PATH setting to be something that might actually be useful. git-svn-id: http://code.djangoproject.com/svn/django/trunk@774 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/global_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 449d25d01e..c5e560c9e1 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -110,7 +110,7 @@ IGNORABLE_404_ENDS = ('mail.pl', 'mailform.pl', 'mail.cgi', 'mailform.cgi', 'fav SECRET_KEY = '' # Path to the "jing" executable -- needed to validate XMLFields -JING_PATH = "/usr/bin/jng" +JING_PATH = "/usr/bin/jing" ############## # MIDDLEWARE # From 16f9b08611ddc5512e16d9fcb6ce405f2962d319 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Wed, 5 Oct 2005 22:47:36 +0000 Subject: [PATCH 019/117] Clarified get_FOO_list part of docs/db-api.txt to specify keyword arguments are also accepted git-svn-id: http://code.djangoproject.com/svn/django/trunk@781 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/db-api.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/db-api.txt b/docs/db-api.txt index e0885da8f0..8a02437aaa 100644 --- a/docs/db-api.txt +++ b/docs/db-api.txt @@ -572,6 +572,9 @@ object in the result list is "truncated" to the given ``type``. * ``"month"`` returns a list of all distinct year/month values for the field. * ``"day"`` returns a list of all distinct year/month/day values for the field. +Additional, optional keyword arguments, in the format described in +"Field lookups" above, are also accepted. + Here's an example, using the ``Poll`` model defined above:: >>> from datetime import datetime @@ -587,6 +590,8 @@ Here's an example, using the ``Poll`` model defined above:: [datetime.datetime(2005, 2, 1), datetime.datetime(2005, 3, 1)] >>> polls.get_pub_date_list('day') [datetime.datetime(2005, 2, 20), datetime.datetime(2005, 3, 20)] + >>> polls.get_pub_date_list('day', question__contains='name') + [datetime.datetime(2005, 3, 20)] ``get_FOO_list()`` also accepts an optional keyword argument ``order``, which should be either ``"ASC"`` or ``"DESC"``. This specifies how to order the From c3fa47edb85fa0b3d77a3ca864990d6b5dba3ff0 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Wed, 5 Oct 2005 23:36:17 +0000 Subject: [PATCH 020/117] Added USE_FLAT_PAGES setting, which defaults to True. git-svn-id: http://code.djangoproject.com/svn/django/trunk@782 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/global_settings.py | 3 +++ django/middleware/common.py | 34 ++++++++++++++++------------------ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index c5e560c9e1..0d9a50148d 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -100,6 +100,9 @@ ALLOWED_INCLUDE_ROOTS = () # is an admin. ADMIN_FOR = [] +# Whether to check the flat-pages table as a last resort for all 404 errors. +USE_FLAT_PAGES = True + # 404s that may be ignored. IGNORABLE_404_STARTS = ('/cgi-bin/', '/_vti_bin', '/_vti_inf') IGNORABLE_404_ENDS = ('mail.pl', 'mailform.pl', 'mail.cgi', 'mailform.cgi', 'favicon.ico', '.php') diff --git a/django/middleware/common.py b/django/middleware/common.py index ee6b68be7e..4abe4ee236 100644 --- a/django/middleware/common.py +++ b/django/middleware/common.py @@ -54,29 +54,27 @@ class CommonMiddleware: return None def process_response(self, request, response): - """ - Check for a flatfile (for 404s) and calculate the Etag, if needed. - """ - - # If this was a 404, check for a flat file + "Check for a flat page (for 404s) and calculate the Etag, if needed." if response.status_code == 404: - try: - response = flat_file(request, request.path) - except exceptions.Http404: + if settings.USE_FLAT_PAGES: + try: + return flat_file(request, request.path) + except exceptions.Http404: + pass + + if settings.SEND_BROKEN_LINK_EMAILS: # If the referrer was from an internal link or a non-search-engine site, # send a note to the managers. - if settings.SEND_BROKEN_LINK_EMAILS: - domain = request.META['HTTP_HOST'] - referer = request.META.get('HTTP_REFERER', None) - is_internal = referer and (domain in referer) - path = request.get_full_path() - if referer and not _is_ignorable_404(path) and (is_internal or '?' not in referer): - mail_managers("Broken %slink on %s" % ((is_internal and 'INTERNAL ' or ''), domain), - "Referrer: %s\nRequested URL: %s\n" % (referer, request.get_full_path())) - # If there's no flatfile we want to return the original 404 response + domain = request.META['HTTP_HOST'] + referer = request.META.get('HTTP_REFERER', None) + is_internal = referer and (domain in referer) + path = request.get_full_path() + if referer and not _is_ignorable_404(path) and (is_internal or '?' not in referer): + mail_managers("Broken %slink on %s" % ((is_internal and 'INTERNAL ' or ''), domain), + "Referrer: %s\nRequested URL: %s\n" % (referer, request.get_full_path())) return response - # Use ETags, if requested + # Use ETags, if requested. if settings.USE_ETAGS: etag = md5.new(response.get_content_as_string('utf-8')).hexdigest() if request.META.get('HTTP_IF_NONE_MATCH') == etag: From 8dda2aeaa39667de4dd97d50469ad6dd1075e917 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Thu, 6 Oct 2005 01:41:54 +0000 Subject: [PATCH 021/117] Improved model validator to check admin.list_filter and type-check admin.list_display git-svn-id: http://code.djangoproject.com/svn/django/trunk@784 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/management.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/django/core/management.py b/django/core/management.py index f3accd84cc..fe7fca6b17 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -549,16 +549,29 @@ def get_validation_errors(outfile): if not isinstance(opts.admin, meta.Admin): e.add(opts, '"admin" attribute, if given, must be set to a meta.Admin() instance.') else: - for fn in opts.admin.list_display: - try: - f = opts.get_field(fn) - except meta.FieldDoesNotExist: - klass = opts.get_model_module().Klass - if not hasattr(klass, fn) or not callable(getattr(klass, fn)): - e.add(opts, '"admin.list_display" refers to %r, which isn\'t a field or method.' % fn) - else: - if isinstance(f, meta.ManyToManyField): - e.add(opts, '"admin.list_display" doesn\'t support ManyToManyFields (%r).' % fn) + # list_display + if not isinstance(opts.admin.list_display, (list, tuple)): + e.add(opts, '"admin.list_display", if given, must be set to a list or tuple.') + else: + for fn in opts.admin.list_display: + try: + f = opts.get_field(fn) + except meta.FieldDoesNotExist: + klass = opts.get_model_module().Klass + if not hasattr(klass, fn) or not callable(getattr(klass, fn)): + e.add(opts, '"admin.list_display" refers to %r, which isn\'t a field or method.' % fn) + else: + if isinstance(f, meta.ManyToManyField): + e.add(opts, '"admin.list_display" doesn\'t support ManyToManyFields (%r).' % fn) + # list_filter + if not isinstance(opts.admin.list_filter, (list, tuple)): + e.add(opts, '"admin.list_filter", if given, must be set to a list or tuple.') + else: + for fn in opts.admin.list_filter: + try: + f = opts.get_field(fn) + except meta.FieldDoesNotExist: + e.add(opts, '"admin.list_filter" refers to %r, which isn\'t a field.' % fn) # Check ordering attribute. if opts.ordering: From 261ab166cef09497ab2204442e74246758341135 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Thu, 6 Oct 2005 01:51:30 +0000 Subject: [PATCH 022/117] Fixed #586 -- raw_id_admin now works with non-integer primary keys git-svn-id: http://code.djangoproject.com/svn/django/trunk@785 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/views/admin/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/views/admin/main.py b/django/views/admin/main.py index 9283230b26..c58da1fbca 100644 --- a/django/views/admin/main.py +++ b/django/views/admin/main.py @@ -431,7 +431,7 @@ def change_list(request, app_label, module_name): if j == 0: # First column is a special case result_id = getattr(result, pk) raw_template.append('%s' % \ - (row_class, result_id, (is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %s); return false;"' % result_id or ''), result_repr)) + (row_class, result_id, (is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %r); return false;"' % result_id or ''), result_repr)) else: raw_template.append('%s' % (row_class, result_repr)) raw_template.append('\n') From ab9aacd4db5d1e69ff2c78bae69fcabe5252d395 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Thu, 6 Oct 2005 02:27:08 +0000 Subject: [PATCH 023/117] Fixed #333 and #440 -- Split DEFAULT_MIME_TYPE setting into DEFAULT_CONTENT_TYPE and DEFAULT_CHARSET. Thanks, Maniac. git-svn-id: http://code.djangoproject.com/svn/django/trunk@786 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/global_settings.py | 8 +++++--- django/core/formfields.py | 7 ++++--- django/core/handlers/modpython.py | 5 +++-- django/core/handlers/wsgi.py | 2 +- django/core/template.py | 3 ++- django/middleware/cache.py | 2 +- django/middleware/common.py | 2 +- django/utils/httpwrappers.py | 6 +++--- django/views/decorators/cache.py | 3 ++- 9 files changed, 22 insertions(+), 16 deletions(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 0d9a50148d..3d81f580fb 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -32,9 +32,11 @@ LANGUAGE_CODE = 'en-us' # notifications and other various e-mails. MANAGERS = ADMINS -# Default MIME type to use for all HttpResponse objects, if a MIME type -# isn't manually specified. This is directly used as the Content-Type header. -DEFAULT_MIME_TYPE = 'text/html; charset=utf-8' +# Default content type and charset to use for all HttpResponse objects, if a +# MIME type isn't manually specified. These are used to construct the +# Content-Type header. +DEFAULT_CONTENT_TYPE = 'text/html' +DEFAULT_CHARSET = 'utf-8' # E-mail address that error messages come from. SERVER_EMAIL = 'root@localhost' diff --git a/django/core/formfields.py b/django/core/formfields.py index 7587b67170..76721ba5c6 100644 --- a/django/core/formfields.py +++ b/django/core/formfields.py @@ -1,6 +1,7 @@ from django.core import validators from django.core.exceptions import PermissionDenied from django.utils.html import escape +from django.conf.settings import DEFAULT_CHARSET FORM_FIELD_ID_PREFIX = 'id_' @@ -221,7 +222,7 @@ class TextField(FormField): self.validator_list = [self.isValidLength, self.hasNoNewlines] + validator_list def isValidLength(self, data, form): - if data and self.maxlength and len(data) > self.maxlength: + if data and self.maxlength and len(data.decode(DEFAULT_CHARSET)) > self.maxlength: raise validators.ValidationError, "Ensure your text is less than %s characters." % self.maxlength def hasNoNewlines(self, data, form): @@ -235,7 +236,7 @@ class TextField(FormField): if self.maxlength: maxlength = 'maxlength="%s" ' % self.maxlength if isinstance(data, unicode): - data = data.encode('utf-8') + data = data.encode(DEFAULT_CHARSET) return '' % \ (FORM_FIELD_ID_PREFIX + self.field_name, self.__class__.__name__, self.is_required and ' required' or '', self.field_name, self.length, escape(data), maxlength) @@ -264,7 +265,7 @@ class LargeTextField(TextField): if data is None: data = '' if isinstance(data, unicode): - data = data.encode('utf-8') + data = data.encode(DEFAULT_CHARSET) return '' % \ (FORM_FIELD_ID_PREFIX + self.field_name, self.__class__.__name__, self.is_required and ' required' or '', self.field_name, self.rows, self.cols, escape(data)) diff --git a/django/core/handlers/modpython.py b/django/core/handlers/modpython.py index 623db959b2..e52879065f 100644 --- a/django/core/handlers/modpython.py +++ b/django/core/handlers/modpython.py @@ -150,14 +150,15 @@ class ModPythonHandler(BaseHandler): def populate_apache_request(http_response, mod_python_req): "Populates the mod_python request object with an HttpResponse" - mod_python_req.content_type = http_response['Content-Type'] or httpwrappers.DEFAULT_MIME_TYPE + from django.conf import settings + mod_python_req.content_type = http_response['Content-Type'] for key, value in http_response.headers.items(): if key != 'Content-Type': mod_python_req.headers_out[key] = value for c in http_response.cookies.values(): mod_python_req.headers_out.add('Set-Cookie', c.output(header='')) mod_python_req.status = http_response.status_code - mod_python_req.write(http_response.get_content_as_string('utf-8')) + mod_python_req.write(http_response.get_content_as_string(settings.DEFAULT_CHARSET)) def handler(req): # mod_python hooks into this function. diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index 40ea9fb902..2d34c64821 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -167,6 +167,6 @@ class WSGIHandler(BaseHandler): response_headers = response.headers.items() for c in response.cookies.values(): response_headers.append(('Set-Cookie', c.output(header=''))) - output = [response.get_content_as_string('utf-8')] + output = [response.get_content_as_string(settings.DEFAULT_CHARSET)] start_response(status, response_headers) return output diff --git a/django/core/template.py b/django/core/template.py index 35b557bbfb..71a8e621c8 100644 --- a/django/core/template.py +++ b/django/core/template.py @@ -55,6 +55,7 @@ times with multiple contexts) '\n\n\n\n' """ import re +from django.conf.settings import DEFAULT_CHARSET __all__ = ('Template','Context','compile_string') @@ -474,7 +475,7 @@ class VariableNode(Node): if not isinstance(output, basestring): output = str(output) elif isinstance(output, unicode): - output = output.encode('utf-8') + output = output.encode(DEFAULT_CHARSET) return output def register_tag(token_command, callback_function): diff --git a/django/middleware/cache.py b/django/middleware/cache.py index f3d03e657a..7f4057eec7 100644 --- a/django/middleware/cache.py +++ b/django/middleware/cache.py @@ -76,7 +76,7 @@ class CacheMiddleware: Sets the cache, if needed. """ if request._cache_middleware_set_cache: - content = response.get_content_as_string('utf-8') + content = response.get_content_as_string(settings.DEFAULT_CHARSET) if request._cache_middleware_accepts_gzip: content = compress_string(content) response.content = content diff --git a/django/middleware/common.py b/django/middleware/common.py index 4abe4ee236..e794477b62 100644 --- a/django/middleware/common.py +++ b/django/middleware/common.py @@ -76,7 +76,7 @@ class CommonMiddleware: # Use ETags, if requested. if settings.USE_ETAGS: - etag = md5.new(response.get_content_as_string('utf-8')).hexdigest() + etag = md5.new(response.get_content_as_string(settings.DEFAULT_CHARSET)).hexdigest() if request.META.get('HTTP_IF_NONE_MATCH') == etag: response = httpwrappers.HttpResponseNotModified() else: diff --git a/django/utils/httpwrappers.py b/django/utils/httpwrappers.py index eeebda565d..5f9362bd24 100644 --- a/django/utils/httpwrappers.py +++ b/django/utils/httpwrappers.py @@ -1,7 +1,7 @@ from Cookie import SimpleCookie from pprint import pformat from urllib import urlencode -import datastructures +from django.utils import datastructures class HttpRequest(object): # needs to be new-style class because subclasses define "property"s "A basic HTTP request" @@ -139,8 +139,8 @@ class HttpResponse: "A basic HTTP response, with content and dictionary-accessed headers" def __init__(self, content='', mimetype=None): if not mimetype: - from django.conf.settings import DEFAULT_MIME_TYPE - mimetype = DEFAULT_MIME_TYPE + from django.conf.settings import DEFAULT_CONTENT_TYPE, DEFAULT_CHARSET + mimetype = "%s; charset=%s" % (DEFAULT_CONTENT_TYPE, DEFAULT_CHARSET) self.content = content self.headers = {'Content-Type':mimetype} self.cookies = SimpleCookie() diff --git a/django/views/decorators/cache.py b/django/views/decorators/cache.py index 7c76ef272d..de80851363 100644 --- a/django/views/decorators/cache.py +++ b/django/views/decorators/cache.py @@ -1,6 +1,7 @@ from django.core.cache import cache from django.utils.httpwrappers import HttpResponseNotModified from django.utils.text import compress_string +from django.conf.settings import DEFAULT_CHARSET import datetime, md5 def cache_page(view_func, cache_timeout, key_prefix=''): @@ -25,7 +26,7 @@ def cache_page(view_func, cache_timeout, key_prefix=''): response = cache.get(cache_key, None) if response is None: response = view_func(request, *args, **kwargs) - content = response.get_content_as_string('utf-8') + content = response.get_content_as_string(DEFAULT_CHARSET) if accepts_gzip: content = compress_string(content) response.content = content From cb628c0cb7c03f94ab2aee5b60128132ca79d742 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Thu, 6 Oct 2005 14:30:35 +0000 Subject: [PATCH 024/117] Changed django.core.management to remove a couple of hard-coded slashes from os.path.join calls. Thanks, Stuart Langridge git-svn-id: http://code.djangoproject.com/svn/django/trunk@789 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/management.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/django/core/management.py b/django/core/management.py index fe7fca6b17..afb498ae63 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -16,8 +16,8 @@ APP_ARGS = '[modelmodule ...]' # Use django.__path__[0] because we don't know which directory django into # which has been installed. -PROJECT_TEMPLATE_DIR = os.path.join(django.__path__[0], 'conf/%s_template') -ADMIN_TEMPLATE_DIR = os.path.join(django.__path__[0], 'conf/admin_templates') +PROJECT_TEMPLATE_DIR = os.path.join(django.__path__[0], 'conf', '%s_template') +ADMIN_TEMPLATE_DIR = os.path.join(django.__path__[0], 'conf', 'admin_templates') def _get_packages_insert(app_label): return "INSERT INTO packages (label, name) VALUES ('%s', '%s');" % (app_label, app_label) @@ -160,7 +160,7 @@ def get_sql_initial_data(mod): output = [] app_label = mod._MODELS[0]._meta.app_label output.append(_get_packages_insert(app_label)) - app_dir = os.path.normpath(os.path.join(os.path.dirname(mod.__file__), '../sql')) + app_dir = os.path.normpath(os.path.join(os.path.dirname(mod.__file__), '..', 'sql')) for klass in mod._MODELS: opts = klass._meta @@ -376,14 +376,14 @@ def startproject(project_name, directory): _start_helper('project', project_name, directory) # Populate TEMPLATE_DIRS for the admin templates, based on where Django is # installed. - admin_settings_file = os.path.join(directory, project_name, 'settings/admin.py') + admin_settings_file = os.path.join(directory, project_name, 'settings', 'admin.py') settings_contents = open(admin_settings_file, 'r').read() fp = open(admin_settings_file, 'w') settings_contents = re.sub(r'(?s)\b(TEMPLATE_DIRS\s*=\s*\()(.*?)\)', "\\1\n r%r,\\2)" % ADMIN_TEMPLATE_DIR, settings_contents) fp.write(settings_contents) fp.close() # Create a random SECRET_KEY hash, and put it in the main settings. - main_settings_file = os.path.join(directory, project_name, 'settings/main.py') + main_settings_file = os.path.join(directory, project_name, 'settings', 'main.py') settings_contents = open(main_settings_file, 'r').read() fp = open(main_settings_file, 'w') secret_key = ''.join([choice('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') for i in range(50)]) @@ -397,7 +397,7 @@ def startapp(app_name, directory): "Creates a Django app for the given app_name in the given directory." # Determine the project_name a bit naively -- by looking at the name of # the parent directory. - project_dir = os.path.normpath(os.path.join(directory, '../')) + project_dir = os.path.normpath(os.path.join(directory, '..')) project_name = os.path.basename(project_dir) _start_helper('app', app_name, directory, project_name) startapp.help_doc = "Creates a Django app directory structure for the given app name in the current directory." From 9fe02e6b65bf6adb9f2bcb07e19e3bfbb5f918af Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Thu, 6 Oct 2005 14:43:07 +0000 Subject: [PATCH 025/117] Made raw_id_admin work with non-integer primary keys git-svn-id: http://code.djangoproject.com/svn/django/trunk@790 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/meta/fields.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/django/core/meta/fields.py b/django/core/meta/fields.py index 3e5cca169d..b13518c503 100644 --- a/django/core/meta/fields.py +++ b/django/core/meta/fields.py @@ -596,7 +596,10 @@ class ForeignKey(Field): Field.__init__(self, **kwargs) def get_manipulator_field_objs(self): - return [formfields.IntegerField] + if self.rel.raw_id_admin: + return self.rel.get_related_field().get_manipulator_field_objs() + else: + return [formfields.IntegerField] class ManyToManyField(Field): def __init__(self, to, **kwargs): From 485042b74d1ee3abb2f2ab4b4d814f2b9bcdad2c Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Thu, 6 Oct 2005 15:52:30 +0000 Subject: [PATCH 026/117] Fixed #595 -- Fixed error when sorting API results descending with custom 'select' parameters. Thanks for the patch, Robert Wittams git-svn-id: http://code.djangoproject.com/svn/django/trunk@792 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/meta/__init__.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/django/core/meta/__init__.py b/django/core/meta/__init__.py index 3ca42f14e6..0c6078705a 100644 --- a/django/core/meta/__init__.py +++ b/django/core/meta/__init__.py @@ -1332,16 +1332,19 @@ def function_get_sql_clause(opts, **kwargs): if f == '?': # Special case. order_by.append(db.get_random_function_sql()) else: + if f.startswith('-'): + col_name = f[1:] + order = "DESC" + else: + col_name = f + order = "ASC" # Use the database table as a column prefix if it wasn't given, # and if the requested column isn't a custom SELECT. - if "." not in f and f not in [k[0] for k in kwargs.get('select', [])]: + if "." not in col_name and col_name not in [k[0] for k in kwargs.get('select', [])]: table_prefix = opts.db_table + '.' else: table_prefix = '' - if f.startswith('-'): - order_by.append('%s%s DESC' % (table_prefix, orderfield2column(f[1:], opts))) - else: - order_by.append('%s%s ASC' % (table_prefix, orderfield2column(f, opts))) + order_by.append('%s%s %s' % (table_prefix, orderfield2column(col_name, opts), order)) order_by = ", ".join(order_by) # LIMIT and OFFSET clauses From c8930e3af441c68973e96b0afc0361357365bb0d Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Thu, 6 Oct 2005 17:22:23 +0000 Subject: [PATCH 027/117] Fixed #357 -- Added a '--pythonpath' option to django-admin. Thanks for the patch, Hugo git-svn-id: http://code.djangoproject.com/svn/django/trunk@793 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/bin/django-admin.py | 4 ++++ docs/django-admin.txt | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/django/bin/django-admin.py b/django/bin/django-admin.py index 31af89dae5..0d021ba172 100755 --- a/django/bin/django-admin.py +++ b/django/bin/django-admin.py @@ -53,11 +53,15 @@ def main(): parser = DjangoOptionParser(get_usage()) parser.add_option('--settings', help='Python path to settings module, e.g. "myproject.settings.main". If this isn\'t provided, the DJANGO_SETTINGS_MODULE environment variable will be used.') + parser.add_option('--pythonpath', + help='Lets you manually add a directory the Python path, e.g. "/home/djangoprojects/myproject".') options, args = parser.parse_args() # Take care of options. if options.settings: os.environ['DJANGO_SETTINGS_MODULE'] = options.settings + if options.pythonpath: + sys.path.insert(0, options.pythonpath) # Run the appropriate action. Unfortunately, optparse can't handle # positional arguments, so this has to parse/validate them. diff --git a/docs/django-admin.txt b/docs/django-admin.txt index b4b07f4f12..188dd4295c 100644 --- a/docs/django-admin.txt +++ b/docs/django-admin.txt @@ -193,6 +193,16 @@ Explicitly specifies the settings module to use. The settings module should be in Python path syntax, e.g. "myproject.settings.main". If this isn't provided, ``django-admin.py`` will use the DJANGO_SETTINGS_MODULE environment variable. +--pythonpath +============ + +Example usage:: + + django-admin.py init --pythonpath='/home/djangoprojects/myproject' + +Adds the given filesystem path to the Python path. If this isn't provided, +``django-admin.py`` will use the ``PYTHONPATH`` environment variable. + --help ====== From eaa8db131ddc06b0ed0d8c618979ad9467a0e37e Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Thu, 6 Oct 2005 17:27:13 +0000 Subject: [PATCH 028/117] Changed docs/django-admin.txt to add a link to diveintopython's explanation of Python import search path git-svn-id: http://code.djangoproject.com/svn/django/trunk@794 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/django-admin.txt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/django-admin.txt b/docs/django-admin.txt index 188dd4295c..130a734567 100644 --- a/docs/django-admin.txt +++ b/docs/django-admin.txt @@ -200,8 +200,11 @@ Example usage:: django-admin.py init --pythonpath='/home/djangoprojects/myproject' -Adds the given filesystem path to the Python path. If this isn't provided, -``django-admin.py`` will use the ``PYTHONPATH`` environment variable. +Adds the given filesystem path to the Python `import search path`_. If this +isn't provided, ``django-admin.py`` will use the ``PYTHONPATH`` environment +variable. + +.. _import search path: http://diveintopython.org/getting_to_know_python/everything_is_an_object.html --help ====== From 9f7e2f38dd1c4f2d0b453fc40f65545cbe90de63 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Thu, 6 Oct 2005 17:29:59 +0000 Subject: [PATCH 029/117] Fixed ReST formatting for docs/django-admin.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@795 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/django-admin.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/django-admin.txt b/docs/django-admin.txt index 130a734567..ba9bb1403f 100644 --- a/docs/django-admin.txt +++ b/docs/django-admin.txt @@ -183,7 +183,7 @@ Available options ================= --settings -========== +---------- Example usage:: @@ -194,7 +194,7 @@ in Python path syntax, e.g. "myproject.settings.main". If this isn't provided, ``django-admin.py`` will use the DJANGO_SETTINGS_MODULE environment variable. --pythonpath -============ +------------ Example usage:: @@ -207,7 +207,7 @@ variable. .. _import search path: http://diveintopython.org/getting_to_know_python/everything_is_an_object.html --help -====== +------ Displays a help message that includes a terse list of all available actions and options. From d93c68ab5fbd7a426845c4f51ae1588f7d928b84 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 7 Oct 2005 23:39:06 +0000 Subject: [PATCH 030/117] Fixed #586 -- Fixed bug in raw_id_admin caused by [785]. Thanks for the heads-up, slashzero git-svn-id: http://code.djangoproject.com/svn/django/trunk@800 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/meta/fields.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/django/core/meta/fields.py b/django/core/meta/fields.py index b13518c503..376595230c 100644 --- a/django/core/meta/fields.py +++ b/django/core/meta/fields.py @@ -596,8 +596,9 @@ class ForeignKey(Field): Field.__init__(self, **kwargs) def get_manipulator_field_objs(self): - if self.rel.raw_id_admin: - return self.rel.get_related_field().get_manipulator_field_objs() + rel_field = self.rel.get_related_field() + if self.rel.raw_id_admin and not isinstance(rel_field, AutoField): + return rel_field.get_manipulator_field_objs() else: return [formfields.IntegerField] From 6beab20722c5c4171541ebf10530f8f9949aa04e Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Sat, 8 Oct 2005 20:23:11 +0000 Subject: [PATCH 031/117] Fixed #374 -- Filtering by BooleanField now works in admin with SQLite. Thanks, davidschein git-svn-id: http://code.djangoproject.com/svn/django/trunk@804 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/views/admin/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/views/admin/main.py b/django/views/admin/main.py index c58da1fbca..5379c8efb5 100644 --- a/django/views/admin/main.py +++ b/django/views/admin/main.py @@ -251,7 +251,7 @@ def change_list(request, app_label, module_name): lookup_val = request.GET.get(lookup_kwarg, None) lookup_val2 = request.GET.get(lookup_kwarg2, None) filter_template.append('

By %s:

    \n' % f.verbose_name) - for k, v in (('All', None), ('Yes', 'True'), ('No', 'False')): + for k, v in (('All', None), ('Yes', '1'), ('No', '0')): filter_template.append('%s\n' % \ (((lookup_val == v and not lookup_val2) and ' class="selected"' or ''), get_query_string(params, {lookup_kwarg: v}, [lookup_kwarg2]), k)) From b63abf037943741db7c1958537ebe3233ed6d6ee Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Sat, 8 Oct 2005 20:56:34 +0000 Subject: [PATCH 032/117] Fixed bug in tests/runtests.py -- some versions of MySQLdb require an argument to connection.autocommit() git-svn-id: http://code.djangoproject.com/svn/django/trunk@805 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- tests/runtests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/runtests.py b/tests/runtests.py index 33ef7e0455..fbe20807ce 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -94,7 +94,7 @@ class TestRunner: # within transactions. cursor = db.cursor() try: - db.connection.autocommit() + db.connection.autocommit(1) except AttributeError: pass self.output(1, "Creating test database") @@ -180,7 +180,7 @@ class TestRunner: cursor = db.cursor() self.output(1, "Deleting test database") try: - db.connection.autocommit() + db.connection.autocommit(1) except AttributeError: pass else: From a933432a708e77437706051f9e80b05fdf602aee Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Sat, 8 Oct 2005 21:00:25 +0000 Subject: [PATCH 033/117] Fixed #473 -- Added a MysqlDebugWrapper to use for MySQL with DEBUG=True. It displays more informative error messages for MySQL warnings. Thanks for the patch, mlambert@gmail.com git-svn-id: http://code.djangoproject.com/svn/django/trunk@806 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/db/backends/mysql.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/django/core/db/backends/mysql.py b/django/core/db/backends/mysql.py index e678740b33..2e77adbc43 100644 --- a/django/core/db/backends/mysql.py +++ b/django/core/db/backends/mysql.py @@ -21,6 +21,32 @@ django_conversions.update({ FIELD_TYPE.TIME: typecasts.typecast_time, }) +# This is an extra debug layer over MySQL queries, to display warnings. +# It's only used when DEBUG=True. +class MysqlDebugWrapper: + def __init__(self, cursor): + self.cursor = cursor + + def execute(self, sql, params=()): + try: + return self.cursor.execute(sql, params) + except Database.Warning, w: + self.cursor.execute("SHOW WARNINGS") + raise Database.Warning, "%s: %s" % (w, self.cursor.fetchall()) + + def executemany(self, sql, param_list): + try: + return self.cursor.executemany(sql, param_list) + except Database.Warning: + self.cursor.execute("SHOW WARNINGS") + raise Database.Warning, "%s: %s" % (w, self.cursor.fetchall()) + + def __getattr__(self, attr): + if self.__dict__.has_key(attr): + return self.__dict__[attr] + else: + return getattr(self.cursor, attr) + class DatabaseWrapper: def __init__(self): self.connection = None @@ -32,7 +58,7 @@ class DatabaseWrapper: self.connection = Database.connect(user=DATABASE_USER, db=DATABASE_NAME, passwd=DATABASE_PASSWORD, host=DATABASE_HOST, conv=django_conversions) if DEBUG: - return base.CursorDebugWrapper(self.connection.cursor(), self) + return base.CursorDebugWrapper(MysqlDebugWrapper(self.connection.cursor()), self) return self.connection.cursor() def commit(self): From 71564b4349422d70af41cd3ea89d4667c9bf5086 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Sat, 8 Oct 2005 21:44:37 +0000 Subject: [PATCH 034/117] Added django.utils.decorators, from Hugo's #580 patch. Refs #580. git-svn-id: http://code.djangoproject.com/svn/django/trunk@807 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/utils/decorators.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 django/utils/decorators.py diff --git a/django/utils/decorators.py b/django/utils/decorators.py new file mode 100644 index 0000000000..b21a4e4248 --- /dev/null +++ b/django/utils/decorators.py @@ -0,0 +1,22 @@ +"Functions that help with dynamically creating decorators for views." + +def decorator_from_middleware(middleware_class): + """ + Given a middleware class (not an instance), returns a view decorator. This + lets you use middleware functionality on a per-view basis. + """ + def _decorator_from_middleware(view_func, *args, **kwargs): + middleware = middleware_class(*args, **kwargs) + def _wrapped_view(request, *args, **kwargs): + if hasattr(middleware, 'process_request'): + result = middleware.process_request(request) + if result is not None: + return result + response = view_func(request, *args, **kwargs) + if hasattr(middleware, 'process_response'): + result = middleware.process_response(request, response) + if result is not None: + return result + return response + return _wrapped_view + return _decorator_from_middleware From 8aa98af6bbebae4a6df2953716f07c6601f44259 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Sat, 8 Oct 2005 23:19:21 +0000 Subject: [PATCH 035/117] Added django.utils.cache, from Hugo's #580 patch. Refs #580. git-svn-id: http://code.djangoproject.com/svn/django/trunk@808 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/utils/cache.py | 155 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 django/utils/cache.py diff --git a/django/utils/cache.py b/django/utils/cache.py new file mode 100644 index 0000000000..26b60c4040 --- /dev/null +++ b/django/utils/cache.py @@ -0,0 +1,155 @@ +""" +This module contains helper functions and decorators for controlling caching. +It does so by managing the "Vary" header of responses. It includes functions +to patch the header of response objects directly and decorators that change +functions to do that header-patching themselves. + +For information on the Vary header, see: + + http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44 + +Essentially, the "Vary" HTTP header defines which headers a cache should take +into account when building its cache key. Requests with the same path but +different header content for headers named in "Vary" need to get different +cache keys to prevent delivery of wrong content. + +A example: i18n middleware would need to distinguish caches by the +"Accept-language" header. +""" + +import datetime, md5, re +from django.conf import settings +from django.core.cache import cache + +vary_delim_re = re.compile(r',\s*') + +def patch_response_headers(response, cache_timeout=None): + """ + Adds some useful headers to the given HttpResponse object: + ETag, Last-Modified, Expires and Cache-Control + + Each header is only added if it isn't already set. + + cache_timeout is in seconds. The CACHE_MIDDLEWARE_SECONDS setting is used + by default. + """ + if cache_timeout is None: + cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS + now = datetime.datetime.utcnow() + expires = now + datetime.timedelta(0, cache_timeout) + if not response.has_header('ETag'): + response['ETag'] = md5.new(response.content).hexdigest() + if not response.has_header('Last-Modified'): + response['Last-Modified'] = now.strftime('%a, %d %b %Y %H:%M:%S GMT') + if not response.has_header('Expires'): + response['Expires'] = expires.strftime('%a, %d %b %Y %H:%M:%S GMT') + if not response.has_header('Cache-Control'): + response['Cache-Control'] = 'max-age=%d' % cache_timeout + +def patch_vary_headers(response, newheaders): + """ + Adds (or updates) the "Vary" header in the given HttpResponse object. + newheaders is a list of header names that should be in "Vary". Existing + headers in "Vary" aren't removed. + """ + # Note that we need to keep the original order intact, because cache + # implementations may rely on the order of the Vary contents in, say, + # computing an MD5 hash. + vary = [] + if response.has_header('Vary'): + vary = vary_delim_re.split(response['Vary']) + oldheaders = dict([(el.lower(), 1) for el in vary]) + for newheader in newheaders: + if not newheader.lower() in oldheaders: + vary.append(newheader) + response['Vary'] = ', '.join(vary) + +def vary_on_headers(*headers): + """ + A view decorator that adds the specified headers to the Vary header of the + response. Usage: + + @vary_on_headers('Cookie', 'Accept-language') + def index(request): + ... + + Note that the header names are not case-sensitive. + """ + def decorator(func): + def inner_func(*args, **kwargs): + response = func(*args, **kwargs) + patch_vary_headers(response, headers) + return response + return inner_func + return decorator + +def vary_on_cookie(func): + """ + A view decorator that adds "Cookie" to the Vary header of a response. This + indicates that a page's contents depends on cookies. Usage: + + @vary_on_cookie + def index(request): + ... + """ + def inner_func(*args, **kwargs): + response = func(*args, **kwargs) + patch_vary_headers(response, ('Cookie',)) + return response + return inner_func + +def _generate_cache_key(request, headerlist, key_prefix): + "Returns a cache key from the headers given in the header list." + ctx = md5.new() + for header in headerlist: + value = request.META.get(header, None) + if value is not None: + ctx.update(value) + return 'views.decorators.cache.cache_page.%s.%s.%s' % (key_prefix, request.path, ctx.hexdigest()) + +def get_cache_key(request, key_prefix=None): + """ + Returns a cache key based on the request path. It can be used in the + request phase because it pulls the list of headers to take into account + from the global path registry and uses those to build a cache key to check + against. + + If there is no headerlist stored, the page needs to be rebuilt, so this + function returns None. + """ + if key_prefix is None: + key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX + cache_key = 'views.decorators.cache.cache_header.%s.%s' % (key_prefix, request.path) + headerlist = cache.get(cache_key, None) + if headerlist is not None: + return _generate_cache_key(request, headerlist, key_prefix) + else: + return None + +def learn_cache_key(request, response, cache_timeout=None, key_prefix=None): + """ + Learns what headers to take into account for some request path from the + response object. It stores those headers in a global path registry so that + later access to that path will know what headers to take into account + without building the response object itself. The headers are named in the + Vary header of the response, but we want to prevent response generation. + + The list of headers to use for cache key generation is stored in the same + cache as the pages themselves. If the cache ages some data out of the + cache, this just means that we have to build the response once to get at + the Vary header and so at the list of headers to use for the cache key. + """ + if key_prefix is None: + key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX + if cache_timeout is None: + cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS + cache_key = 'views.decorators.cache.cache_header.%s.%s' % (key_prefix, request.path) + if response.has_header('Vary'): + headerlist = ['HTTP_'+header.upper().replace('-', '_') for header in vary_delim_re.split(response['Vary'])] + cache.set(cache_key, headerlist, cache_timeout) + return _generate_cache_key(request, headerlist, key_prefix) + else: + # if there is no Vary header, we still need a cache key + # for the request.path + cache.set(cache_key, [], cache_timeout) + return _generate_cache_key(request, [], key_prefix) From a5a89b5a432df1f8c9003dd3b3b8b93675746da3 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Sun, 9 Oct 2005 00:37:56 +0000 Subject: [PATCH 036/117] Moved vary decorators from django.utils.cache to django.views.decorators.vary git-svn-id: http://code.djangoproject.com/svn/django/trunk@809 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/utils/cache.py | 42 ++++----------------------------- django/views/decorators/vary.py | 35 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 38 deletions(-) create mode 100644 django/views/decorators/vary.py diff --git a/django/utils/cache.py b/django/utils/cache.py index 26b60c4040..fcd0825a22 100644 --- a/django/utils/cache.py +++ b/django/utils/cache.py @@ -1,8 +1,8 @@ """ -This module contains helper functions and decorators for controlling caching. -It does so by managing the "Vary" header of responses. It includes functions -to patch the header of response objects directly and decorators that change -functions to do that header-patching themselves. +This module contains helper functions for controlling caching. It does so by +managing the "Vary" header of responses. It includes functions to patch the +header of response objects directly and decorators that change functions to do +that header-patching themselves. For information on the Vary header, see: @@ -64,40 +64,6 @@ def patch_vary_headers(response, newheaders): vary.append(newheader) response['Vary'] = ', '.join(vary) -def vary_on_headers(*headers): - """ - A view decorator that adds the specified headers to the Vary header of the - response. Usage: - - @vary_on_headers('Cookie', 'Accept-language') - def index(request): - ... - - Note that the header names are not case-sensitive. - """ - def decorator(func): - def inner_func(*args, **kwargs): - response = func(*args, **kwargs) - patch_vary_headers(response, headers) - return response - return inner_func - return decorator - -def vary_on_cookie(func): - """ - A view decorator that adds "Cookie" to the Vary header of a response. This - indicates that a page's contents depends on cookies. Usage: - - @vary_on_cookie - def index(request): - ... - """ - def inner_func(*args, **kwargs): - response = func(*args, **kwargs) - patch_vary_headers(response, ('Cookie',)) - return response - return inner_func - def _generate_cache_key(request, headerlist, key_prefix): "Returns a cache key from the headers given in the header list." ctx = md5.new() diff --git a/django/views/decorators/vary.py b/django/views/decorators/vary.py new file mode 100644 index 0000000000..9b49c45cf2 --- /dev/null +++ b/django/views/decorators/vary.py @@ -0,0 +1,35 @@ +from django.utils.cache import patch_vary_headers + +def vary_on_headers(*headers): + """ + A view decorator that adds the specified headers to the Vary header of the + response. Usage: + + @vary_on_headers('Cookie', 'Accept-language') + def index(request): + ... + + Note that the header names are not case-sensitive. + """ + def decorator(func): + def inner_func(*args, **kwargs): + response = func(*args, **kwargs) + patch_vary_headers(response, headers) + return response + return inner_func + return decorator + +def vary_on_cookie(func): + """ + A view decorator that adds "Cookie" to the Vary header of a response. This + indicates that a page's contents depends on cookies. Usage: + + @vary_on_cookie + def index(request): + ... + """ + def inner_func(*args, **kwargs): + response = func(*args, **kwargs) + patch_vary_headers(response, ('Cookie',)) + return response + return inner_func From d65526d6886067a8ef368e5b02fce80e1e4c4903 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Sun, 9 Oct 2005 00:55:08 +0000 Subject: [PATCH 037/117] Fixed #580 -- Added mega support for generating Vary headers, including some view decorators, and changed the CacheMiddleware to account for the Vary header. Also added GZipMiddleware and ConditionalGetMiddleware, which are no longer handled by CacheMiddleware itself. Also updated the cache.txt and middleware.txt docs. Thanks to Hugo and Sune for the excellent patches git-svn-id: http://code.djangoproject.com/svn/django/trunk@810 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/middleware/cache.py | 116 +++++++++------------ django/middleware/gzip.py | 24 +++++ django/middleware/http.py | 37 +++++++ django/middleware/sessions.py | 2 + django/views/decorators/cache.py | 70 +++---------- django/views/decorators/gzip.py | 6 ++ django/views/decorators/http.py | 9 ++ docs/cache.txt | 174 +++++++++++++++++++++++++------ docs/middleware.txt | 12 +++ 9 files changed, 297 insertions(+), 153 deletions(-) create mode 100644 django/middleware/gzip.py create mode 100644 django/middleware/http.py create mode 100644 django/views/decorators/gzip.py create mode 100644 django/views/decorators/http.py diff --git a/django/middleware/cache.py b/django/middleware/cache.py index 7f4057eec7..8216c40ae1 100644 --- a/django/middleware/cache.py +++ b/django/middleware/cache.py @@ -1,88 +1,70 @@ +import copy from django.conf import settings from django.core.cache import cache +from django.utils.cache import get_cache_key, learn_cache_key, patch_response_headers from django.utils.httpwrappers import HttpResponseNotModified -from django.utils.text import compress_string -import datetime, md5 class CacheMiddleware: """ Cache middleware. If this is enabled, each Django-powered page will be - cached for CACHE_MIDDLEWARE_SECONDS seconds. Cache is based on URLs. Pages - with GET or POST parameters are not cached. + cached for CACHE_MIDDLEWARE_SECONDS seconds. Cache is based on URLs. - If the cache is shared across multiple sites using the same Django - installation, set the CACHE_MIDDLEWARE_KEY_PREFIX to the name of the site, - or some other string that is unique to this Django instance, to prevent key - collisions. + Only parameter-less GET or HEAD-requests with status code 200 are cached. - This middleware will also make the following optimizations: + This middleware expects that a HEAD request is answered with a response + exactly like the corresponding GET request. - * If the CACHE_MIDDLEWARE_GZIP setting is True, the content will be - gzipped. + When a hit occurs, a shallow copy of the original response object is + returned from process_request. - * ETags will be added, using a simple MD5 hash of the page's content. + Pages will be cached based on the contents of the request headers + listed in the response's "Vary" header. This means that pages shouldn't + change their "Vary" header. + + This middleware also sets ETag, Last-Modified, Expires and Cache-Control + headers on the response object. """ + def __init__(self, cache_timeout=None, key_prefix=None): + self.cache_timeout = cache_timeout + if cache_timeout is None: + self.cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS + self.key_prefix = key_prefix + if key_prefix is None: + self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX + def process_request(self, request): - """ - Checks whether the page is already cached. If it is, returns the cached - version. Also handles ETag stuff. - """ - if request.GET or request.POST: - request._cache_middleware_set_cache = False + "Checks whether the page is already cached and returns the cached version if available." + if not request.META['REQUEST_METHOD'] in ('GET', 'HEAD') or request.GET: + request._cache_update_cache = False return None # Don't bother checking the cache. - accept_encoding = '' - if settings.CACHE_MIDDLEWARE_GZIP: - try: - accept_encoding = request.META['HTTP_ACCEPT_ENCODING'] - except KeyError: - pass - accepts_gzip = 'gzip' in accept_encoding - request._cache_middleware_accepts_gzip = accepts_gzip - - # This uses the same cache_key as views.decorators.cache.cache_page, - # so the cache can be shared. - cache_key = 'views.decorators.cache.cache_page.%s.%s.%s' % \ - (settings.CACHE_MIDDLEWARE_KEY_PREFIX, request.path, accepts_gzip) - request._cache_middleware_key = cache_key + cache_key = get_cache_key(request, self.key_prefix) + if cache_key is None: + request._cache_update_cache = True + return None # No cache information available, need to rebuild. response = cache.get(cache_key, None) if response is None: - request._cache_middleware_set_cache = True - return None - else: - request._cache_middleware_set_cache = False - # Logic is from http://simon.incutio.com/archive/2003/04/23/conditionalGet - try: - if_none_match = request.META['HTTP_IF_NONE_MATCH'] - except KeyError: - if_none_match = None - try: - if_modified_since = request.META['HTTP_IF_MODIFIED_SINCE'] - except KeyError: - if_modified_since = None - if if_none_match is None and if_modified_since is None: - pass - elif if_none_match is not None and response['ETag'] != if_none_match: - pass - elif if_modified_since is not None and response['Last-Modified'] != if_modified_since: - pass - else: - return HttpResponseNotModified() - return response + request._cache_update_cache = True + return None # No cache information available, need to rebuild. + + request._cache_update_cache = False + return copy.copy(response) def process_response(self, request, response): - """ - Sets the cache, if needed. - """ - if request._cache_middleware_set_cache: - content = response.get_content_as_string(settings.DEFAULT_CHARSET) - if request._cache_middleware_accepts_gzip: - content = compress_string(content) - response.content = content - response['Content-Encoding'] = 'gzip' - response['ETag'] = md5.new(content).hexdigest() - response['Content-Length'] = '%d' % len(content) - response['Last-Modified'] = datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT') - cache.set(request._cache_middleware_key, response, settings.CACHE_MIDDLEWARE_SECONDS) + "Sets the cache, if needed." + if not request._cache_update_cache: + # We don't need to update the cache, just return. + return response + if not request.META['REQUEST_METHOD'] == 'GET': + # This is a stronger requirement than above. It is needed + # because of interactions between this middleware and the + # HTTPMiddleware, which throws the body of a HEAD-request + # away before this middleware gets a chance to cache it. + return response + if not response.status_code == 200: + return response + patch_response_headers(response, self.cache_timeout) + cache_key = learn_cache_key(request, response, self.cache_timeout, self.key_prefix) + cache.set(cache_key, response, self.cache_timeout) return response diff --git a/django/middleware/gzip.py b/django/middleware/gzip.py new file mode 100644 index 0000000000..201bec2000 --- /dev/null +++ b/django/middleware/gzip.py @@ -0,0 +1,24 @@ +import re +from django.utils.text import compress_string +from django.utils.cache import patch_vary_headers + +re_accepts_gzip = re.compile(r'\bgzip\b') + +class GZipMiddleware: + """ + This middleware compresses content if the browser allows gzip compression. + It sets the Vary header accordingly, so that caches will base their storage + on the Accept-Encoding header. + """ + def process_response(self, request, response): + patch_vary_headers(response, ('Accept-Encoding',)) + if response.has_header('Content-Encoding'): + return response + + ae = request.META.get('HTTP_ACCEPT_ENCODING', '') + if not re_accepts_gzip.search(ae): + return response + + response.content = compress_string(response.content) + response['Content-Encoding'] = 'gzip' + return response diff --git a/django/middleware/http.py b/django/middleware/http.py new file mode 100644 index 0000000000..2bccd60903 --- /dev/null +++ b/django/middleware/http.py @@ -0,0 +1,37 @@ +import datetime + +class ConditionalGetMiddleware: + """ + Handles conditional GET operations. If the response has a ETag or + Last-Modified header, and the request has If-None-Match or + If-Modified-Since, the response is replaced by an HttpNotModified. + + Removes the content from any response to a HEAD request. + + Also sets the Date and Content-Length response-headers. + """ + def process_response(self, request, response): + now = datetime.datetime.utcnow() + response['Date'] = now.strftime('%a, %d %b %Y %H:%M:%S GMT') + if not response.has_header('Content-Length'): + response['Content-Length'] = str(len(response.content)) + + if response.has_header('ETag'): + if_none_match = request.META.get('HTTP_IF_NONE_MATCH', None) + if if_none_match == response['ETag']: + response.status_code = 304 + response.content = '' + response['Content-Length'] = '0' + + if response.has_header('Last-Modified'): + last_mod = response['Last-Modified'] + if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE', None) + if if_modified_since == response['Last-Modified']: + response.status_code = 304 + response.content = '' + response['Content-Length'] = '0' + + if request.META['REQUEST_METHOD'] == 'HEAD': + response.content = '' + + return response diff --git a/django/middleware/sessions.py b/django/middleware/sessions.py index a588e3e95b..42b2118410 100644 --- a/django/middleware/sessions.py +++ b/django/middleware/sessions.py @@ -1,5 +1,6 @@ from django.conf.settings import SESSION_COOKIE_NAME, SESSION_COOKIE_AGE, SESSION_COOKIE_DOMAIN from django.models.core import sessions +from django.utils.cache import patch_vary_headers import datetime TEST_COOKIE_NAME = 'testcookie' @@ -61,6 +62,7 @@ class SessionMiddleware: def process_response(self, request, response): # If request.session was modified, or if response.session was set, save # those changes and set a session cookie. + patch_vary_headers(response, ('Cookie',)) try: modified = request.session.modified except AttributeError: diff --git a/django/views/decorators/cache.py b/django/views/decorators/cache.py index de80851363..09f9a0139f 100644 --- a/django/views/decorators/cache.py +++ b/django/views/decorators/cache.py @@ -1,57 +1,17 @@ -from django.core.cache import cache -from django.utils.httpwrappers import HttpResponseNotModified -from django.utils.text import compress_string -from django.conf.settings import DEFAULT_CHARSET -import datetime, md5 +""" +Decorator for views that tries getting the page from the cache and +populates the cache if the page isn't in the cache yet. -def cache_page(view_func, cache_timeout, key_prefix=''): - """ - Decorator for views that tries getting the page from the cache and - populates the cache if the page isn't in the cache yet. Also takes care - of ETags and gzips the page if the client supports it. +The cache is keyed by the URL and some data from the headers. Additionally +there is the key prefix that is used to distinguish different cache areas +in a multi-site setup. You could use the sites.get_current().domain, for +example, as that is unique across a Django project. - The cache is keyed off of the page's URL plus the optional key_prefix - variable. Use key_prefix if your Django setup has multiple sites that - use cache; otherwise the cache for one site would affect the other. A good - example of key_prefix is to use sites.get_current().domain, because that's - unique across all Django instances on a particular server. - """ - def _check_cache(request, *args, **kwargs): - try: - accept_encoding = request.META['HTTP_ACCEPT_ENCODING'] - except KeyError: - accept_encoding = '' - accepts_gzip = 'gzip' in accept_encoding - cache_key = 'views.decorators.cache.cache_page.%s.%s.%s' % (key_prefix, request.path, accepts_gzip) - response = cache.get(cache_key, None) - if response is None: - response = view_func(request, *args, **kwargs) - content = response.get_content_as_string(DEFAULT_CHARSET) - if accepts_gzip: - content = compress_string(content) - response.content = content - response['Content-Encoding'] = 'gzip' - response['ETag'] = md5.new(content).hexdigest() - response['Content-Length'] = '%d' % len(content) - response['Last-Modified'] = datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT') - cache.set(cache_key, response, cache_timeout) - else: - # Logic is from http://simon.incutio.com/archive/2003/04/23/conditionalGet - try: - if_none_match = request.META['HTTP_IF_NONE_MATCH'] - except KeyError: - if_none_match = None - try: - if_modified_since = request.META['HTTP_IF_MODIFIED_SINCE'] - except KeyError: - if_modified_since = None - if if_none_match is None and if_modified_since is None: - pass - elif if_none_match is not None and response['ETag'] != if_none_match: - pass - elif if_modified_since is not None and response['Last-Modified'] != if_modified_since: - pass - else: - return HttpResponseNotModified() - return response - return _check_cache +Additionally, all headers from the response's Vary header will be taken into +account on caching -- just like the middleware does. +""" + +from django.utils.decorators import decorator_from_middleware +from django.middleware.cache import CacheMiddleware + +cache_page = decorator_from_middleware(CacheMiddleware) diff --git a/django/views/decorators/gzip.py b/django/views/decorators/gzip.py new file mode 100644 index 0000000000..dc6edad049 --- /dev/null +++ b/django/views/decorators/gzip.py @@ -0,0 +1,6 @@ +"Decorator for views that gzips pages if the client supports it." + +from django.utils.decorators import decorator_from_middleware +from django.middleware.gzip import GZipMiddleware + +gzip_page = decorator_from_middleware(GZipMiddleware) diff --git a/django/views/decorators/http.py b/django/views/decorators/http.py new file mode 100644 index 0000000000..13062b630f --- /dev/null +++ b/django/views/decorators/http.py @@ -0,0 +1,9 @@ +""" +Decorator for views that supports conditional get on ETag and Last-Modified +headers. +""" + +from django.utils.decorators import decorator_from_middleware +from django.middleware.http import ConditionalGetMiddleware + +conditional_page = decorator_from_middleware(ConditionalGetMiddleware) diff --git a/docs/cache.txt b/docs/cache.txt index 0a7ee1c25a..f690e5f904 100644 --- a/docs/cache.txt +++ b/docs/cache.txt @@ -2,25 +2,27 @@ Django's cache framework ======================== -So, you got slashdotted. Now what? +So, you got slashdotted_. Now what? Django's cache framework gives you three methods of caching dynamic pages in memory or in a database. You can cache the output of entire pages, you can cache only the pieces that are difficult to produce, or you can cache your entire site. +.. _slashdotted: http://en.wikipedia.org/wiki/Slashdot_effect + Setting up the cache ==================== -The cache framework is split into a set of "backends" that provide different -methods of caching data. There's a simple single-process memory cache (mostly -useful as a fallback) and a memcached_ backend (the fastest option, by far, if -you've got the RAM). +The cache framework allows for different "backends" -- different methods of +caching data. There's a simple single-process memory cache (mostly useful as a +fallback) and a memcached_ backend (the fastest option, by far, if you've got +the RAM). Before using the cache, you'll need to tell Django which cache backend you'd like to use. Do this by setting the ``CACHE_BACKEND`` in your settings file. -The CACHE_BACKEND setting is a "fake" URI (really an unregistered scheme). +The ``CACHE_BACKEND`` setting is a "fake" URI (really an unregistered scheme). Examples: ============================== =========================================== @@ -39,7 +41,7 @@ Examples: simple:/// A simple single-process memory cache; you probably don't want to use this except for testing. Note that this cache backend is - NOT threadsafe! + NOT thread-safe! locmem:/// A more sophisticated local memory cache; this is multi-process- and thread-safe. @@ -72,22 +74,24 @@ For example:: Invalid arguments are silently ignored, as are invalid values of known arguments. +.. _memcached: http://www.danga.com/memcached/ + The per-site cache ================== -Once the cache is set up, the simplest way to use the cache is to simply -cache your entire site. Just add ``django.middleware.cache.CacheMiddleware`` -to your ``MIDDLEWARE_CLASSES`` setting, as in this example:: +Once the cache is set up, the simplest way to use the cache is to cache your +entire site. Just add ``django.middleware.cache.CacheMiddleware`` to your +``MIDDLEWARE_CLASSES`` setting, as in this example:: MIDDLEWARE_CLASSES = ( "django.middleware.cache.CacheMiddleware", "django.middleware.common.CommonMiddleware", ) -Make sure it's the first entry in ``MIDDLEWARE_CLASSES``. (The order of -``MIDDLEWARE_CLASSES`` matters.) +(The order of ``MIDDLEWARE_CLASSES`` matters. See "Order of MIDDLEWARE_CLASSES" +below.) -Then, add the following three required settings: +Then, add the following three required settings to your Django settings file: * ``CACHE_MIDDLEWARE_SECONDS`` -- The number of seconds each page should be cached. @@ -102,16 +106,20 @@ Then, add the following three required settings: in the cache. That means subsequent requests won't have the overhead of zipping, and the cache will hold more pages because each one is smaller. -Pages with GET or POST parameters won't be cached. +The cache middleware caches every page that doesn't have GET or POST +parameters. Additionally, ``CacheMiddleware`` automatically sets a few headers +in each ``HttpResponse``: -The cache middleware also makes a few more optimizations: - -* Sets and deals with ``ETag`` headers. -* Sets the ``Content-Length`` header. * Sets the ``Last-Modified`` header to the current date/time when a fresh (uncached) version of the page is requested. +* Sets the ``Expires`` header to the current date/time plus the defined + ``CACHE_MIDDLEWARE_SECONDS``. +* Sets the ``Cache-Control`` header to give a max age for the page -- again, + from the ``CACHE_MIDDLEWARE_SECONDS`` setting. -It doesn't matter where in the middleware stack you put the cache middleware. +See the `middleware documentation`_ for more on middleware. + +.. _`middleware documentation`: http://www.djangoproject.com/documentation/middleware/ The per-page cache ================== @@ -134,25 +142,25 @@ Or, using Python 2.4's decorator syntax:: def slashdot_this(request): ... -This will cache the result of that view for 15 minutes. (The cache timeout is -in seconds.) +``cache_page`` takes a single argument: the cache timeout, in seconds. In the +above example, the result of the ``slashdot_this()`` view will be cached for 15 +minutes. The low-level cache API ======================= -There are times, however, that caching an entire rendered page doesn't gain -you very much. The Django developers have found it's only necessary to cache a -list of object IDs from an intensive database query, for example. In cases like -these, you can use the cache API to store objects in the cache with any level -of granularity you like. +Sometimes, however, caching an entire rendered page doesn't gain you very much. +For example, you may find it's only necessary to cache the result of an +intensive database. In cases like this, you can use the low-level cache API to +store objects in the cache with any level of granularity you like. The cache API is simple:: - # the cache module exports a cache object that's automatically - # created from the CACHE_BACKEND setting + # The cache module exports a cache object that's automatically + # created from the CACHE_BACKEND setting. >>> from django.core.cache import cache - # The basic interface is set(key, value, timeout_seconds) and get(key) + # The basic interface is set(key, value, timeout_seconds) and get(key). >>> cache.set('my_key', 'hello, world!', 30) >>> cache.get('my_key') 'hello, world!' @@ -161,7 +169,7 @@ The cache API is simple:: >>> cache.get('my_key') None - # get() can take a default argument + # get() can take a default argument. >>> cache.get('my_key', 'has_expired') 'has_expired' @@ -183,4 +191,108 @@ The cache API is simple:: That's it. The cache has very few restrictions: You can cache any object that can be pickled safely, although keys must be strings. -.. _memcached: http://www.danga.com/memcached/ +Controlling cache: Using Vary headers +===================================== + +The Django cache framework works with `HTTP Vary headers`_ to allow developers +to instruct caching mechanisms to differ their cache contents depending on +request HTTP headers. + +Essentially, the ``Vary`` response HTTP header defines which request headers a +cache mechanism should take into account when building its cache key. + +By default, Django's cache system creates its cache keys using the requested +path -- e.g., ``"/stories/2005/jun/23/bank_robbed/"``. This means every request +to that URL will use the same cached version, regardless of user-agent +differences such as cookies or language preferences. + +That's where ``Vary`` comes in. + +If your Django-powered page outputs different content based on some difference +in request headers -- such as a cookie, or language, or user-agent -- you'll +need to use the ``Vary`` header to tell caching mechanisms that the page output +depends on those things. + +To do this in Django, use the convenient ``vary_on_headers`` view decorator, +like so:: + + from django.views.decorators.vary import vary_on_headers + + # Python 2.3 syntax. + def my_view(request): + ... + my_view = vary_on_headers(my_view, 'User-Agent') + + # Python 2.4 decorator syntax. + @vary_on_headers('User-Agent') + def my_view(request): + ... + +In this case, a caching mechanism (such as Django's own cache middleware) will +cache a separate version of the page for each unique user-agent. + +The advantage to using the ``vary_on_headers`` decorator rather than manually +setting the ``Vary`` header (using something like +``response['Vary'] = 'user-agent'``) is that the decorator adds to the ``Vary`` +header (which may already exist) rather than setting it from scratch. + +Note that you can pass multiple headers to ``vary_on_headers()``: + + @vary_on_headers('User-Agent', 'Cookie') + def my_view(request): + ... + +Because varying on cookie is such a common case, there's a ``vary_on_cookie`` +decorator. These two views are equivalent:: + + @vary_on_cookie + def my_view(request): + ... + + @vary_on_headers('Cookie') + def my_view(request): + ... + +Also note that the headers you pass to ``vary_on_headers`` are not case +sensitive. ``"User-Agent"`` is the same thing as ``"user-agent"``. + +You can also use a helper function, ``patch_vary_headers()``, directly:: + + from django.utils.cache import patch_vary_headers + def my_view(request): + ... + response = render_to_response('template_name', context) + patch_vary_headers(response, ['Cookie']) + return response + +``patch_vary_headers`` takes an ``HttpResponse`` instance as its first argument +and a list/tuple of header names as its second argument. + +.. _`HTTP Vary headers`: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44 + +Other optimizations +=================== + +Django comes with a few other pieces of middleware that can help optimize your +apps' performance: + + * ``django.middleware.http.ConditionalGetMiddleware`` adds support for + conditional GET. This makes use of ``ETag`` and ``Last-Modified`` + headers. + + * ``django.middleware.gzip.GZipMiddleware`` compresses content for browsers + that understand gzip compression (all modern browsers). + +Order of MIDDLEWARE_CLASSES +=========================== + +If you use ``CacheMiddleware``, it's important to put it in the right place +within the ``MIDDLEWARE_CLASSES`` setting, because the cache middleware needs +to know which headers by which to vary the cache storage. Middleware always +adds something the ``Vary`` response header when it can. + +Put the ``CacheMiddleware`` after any middlewares that might add something to +the ``Vary`` header. The following middlewares do so: + + * ``SessionMiddleware`` adds ``Cookie`` + * ``GzipMiddleware`` adds ``Accept-Encoding`` diff --git a/docs/middleware.txt b/docs/middleware.txt index f3901bb693..21e62fa18c 100644 --- a/docs/middleware.txt +++ b/docs/middleware.txt @@ -88,6 +88,18 @@ Available middleware addresses defined in the ``INTERNAL_IPS`` setting. This is used by Django's automatic documentation system. +``django.middleware.gzip.GZipMiddleware`` + Compresses content for browsers that understand gzip compression (all + modern browsers). + +``django.middleware.http.ConditionalGetMiddleware`` + Handles conditional GET operations. If the response has a ``ETag`` or + ``Last-Modified`` header, and the request has ``If-None-Match`` or + ``If-Modified-Since``, the response is replaced by an HttpNotModified. + + Also removes the content from any response to a HEAD request and sets the + ``Date`` and ``Content-Length`` response-headers. + ``django.middleware.sessions.SessionMiddleware`` Enables session support. See the `session documentation`_. From b6fd05e4452a709b5c7dc2fc961041d8d6a461d3 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Sun, 9 Oct 2005 01:02:19 +0000 Subject: [PATCH 038/117] Added CACHE_MIDDLEWARE_KEY_PREFIX to global_settings git-svn-id: http://code.djangoproject.com/svn/django/trunk@811 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/global_settings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 3d81f580fb..c651543ff9 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -126,6 +126,8 @@ JING_PATH = "/usr/bin/jing" # response phase the middleware will be applied in reverse order. MIDDLEWARE_CLASSES = ( "django.middleware.sessions.SessionMiddleware", +# "django.middleware.http.ConditionalGetMiddleware", +# "django.middleware.gzip.GZipMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.doc.XViewMiddleware", ) @@ -145,6 +147,7 @@ SESSION_COOKIE_DOMAIN = None # A string like ".lawrence.com", or No # The cache backend to use. See the docstring in django.core.cache for the # possible values. CACHE_BACKEND = 'simple://' +CACHE_MIDDLEWARE_KEY_PREFIX = '' #################### # COMMENTS # From df794701d07206132c12b21d685793d1ec261cc5 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Sun, 9 Oct 2005 01:02:30 +0000 Subject: [PATCH 039/117] Fixed small typo in docs/cache.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@812 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/cache.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cache.txt b/docs/cache.txt index f690e5f904..7bb8ab6398 100644 --- a/docs/cache.txt +++ b/docs/cache.txt @@ -295,4 +295,4 @@ Put the ``CacheMiddleware`` after any middlewares that might add something to the ``Vary`` header. The following middlewares do so: * ``SessionMiddleware`` adds ``Cookie`` - * ``GzipMiddleware`` adds ``Accept-Encoding`` + * ``GZipMiddleware`` adds ``Accept-Encoding`` From fd4ddb179f4188cc7d2f4d855d4a52301f6e1acf Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Sun, 9 Oct 2005 01:03:04 +0000 Subject: [PATCH 040/117] Fixed ReST error in docs/cache.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@813 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/cache.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cache.txt b/docs/cache.txt index 7bb8ab6398..f15da2660b 100644 --- a/docs/cache.txt +++ b/docs/cache.txt @@ -236,7 +236,7 @@ setting the ``Vary`` header (using something like ``response['Vary'] = 'user-agent'``) is that the decorator adds to the ``Vary`` header (which may already exist) rather than setting it from scratch. -Note that you can pass multiple headers to ``vary_on_headers()``: +Note that you can pass multiple headers to ``vary_on_headers()``:: @vary_on_headers('User-Agent', 'Cookie') def my_view(request): From 22bbdc633cc987a4875ec86088489b3e0b0a4cea Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Sun, 9 Oct 2005 01:08:58 +0000 Subject: [PATCH 041/117] Changed some formatting in docs/middleware.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@814 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/middleware.txt | 104 +++++++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 45 deletions(-) diff --git a/docs/middleware.txt b/docs/middleware.txt index 21e62fa18c..dfa1947bbd 100644 --- a/docs/middleware.txt +++ b/docs/middleware.txt @@ -45,65 +45,79 @@ required. Available middleware ==================== -``django.middleware.admin.AdminUserRequired`` - Limits site access to valid users with the ``is_staff`` flag set. This is - required by Django's admin, and this middleware requires ``SessionMiddleware``. +django.middleware.admin.AdminUserRequired +----------------------------------------- -``django.middleware.cache.CacheMiddleware`` - Enables site-wide cache. If this is enabled, each Django-powered page will be - cached for as long as the ``CACHE_MIDDLEWARE_SECONDS`` setting defines. See - the `cache documentation`_. +Limits site access to valid users with the ``is_staff`` flag set. This is +required by Django's admin, and this middleware requires ``SessionMiddleware``. - .. _`cache documentation`: http://www.djangoproject.com/documentation/cache/#the-per-site-cache +django.middleware.cache.CacheMiddleware +--------------------------------------- -``django.middleware.common.CommonMiddleware`` - Adds a few conveniences for perfectionists: +Enables site-wide cache. If this is enabled, each Django-powered page will be +cached for as long as the ``CACHE_MIDDLEWARE_SECONDS`` setting defines. See +the `cache documentation`_. - * Forbids access to user agents in the ``DISALLOWED_USER_AGENTS`` setting, - which should be a list of strings. +.. _`cache documentation`: http://www.djangoproject.com/documentation/cache/#the-per-site-cache - * Performs URL rewriting based on the ``APPEND_SLASH`` and ``PREPEND_WWW`` - settings. If ``APPEND_SLASH`` is ``True``, URLs that lack a trailing - slash will be redirected to the same URL with a trailing slash. If - ``PREPEND_WWW`` is ``True``, URLs that lack a leading "www." will be - redirected to the same URL with a leading "www." +django.middleware.common.CommonMiddleware +----------------------------------------- - Both of these options are meant to normalize URLs. The philosophy is that - each URL should exist in one, and only one, place. Technically a URL - ``foo.com/bar`` is distinct from ``foo.com/bar/`` -- a search-engine - indexer would treat them as separate URLs -- so it's best practice to - normalize URLs. +Adds a few conveniences for perfectionists: - * Handles ETags based on the ``USE_ETAGS`` setting. If ``USE_ETAGS`` is set - to ``True``, Django will calculate an ETag for each request by - MD5-hashing the page content, and it'll take care of sending - ``Not Modified`` responses, if appropriate. +* Forbids access to user agents in the ``DISALLOWED_USER_AGENTS`` setting, + which should be a list of strings. - * Handles flat pages. Every time Django encounters a 404 -- either within - a view or as a result of no URLconfs matching -- it will check the - database of flat pages based on the current URL. +* Performs URL rewriting based on the ``APPEND_SLASH`` and ``PREPEND_WWW`` + settings. If ``APPEND_SLASH`` is ``True``, URLs that lack a trailing + slash will be redirected to the same URL with a trailing slash. If + ``PREPEND_WWW`` is ``True``, URLs that lack a leading "www." will be + redirected to the same URL with a leading "www." -``django.middleware.doc.XViewMiddleware`` - Sends custom ``X-View`` HTTP headers to HEAD requests that come from IP - addresses defined in the ``INTERNAL_IPS`` setting. This is used by Django's - automatic documentation system. + Both of these options are meant to normalize URLs. The philosophy is that + each URL should exist in one, and only one, place. Technically a URL + ``foo.com/bar`` is distinct from ``foo.com/bar/`` -- a search-engine + indexer would treat them as separate URLs -- so it's best practice to + normalize URLs. -``django.middleware.gzip.GZipMiddleware`` - Compresses content for browsers that understand gzip compression (all - modern browsers). +* Handles ETags based on the ``USE_ETAGS`` setting. If ``USE_ETAGS`` is set + to ``True``, Django will calculate an ETag for each request by + MD5-hashing the page content, and it'll take care of sending + ``Not Modified`` responses, if appropriate. -``django.middleware.http.ConditionalGetMiddleware`` - Handles conditional GET operations. If the response has a ``ETag`` or - ``Last-Modified`` header, and the request has ``If-None-Match`` or - ``If-Modified-Since``, the response is replaced by an HttpNotModified. +* Handles flat pages. Every time Django encounters a 404 -- either within + a view or as a result of no URLconfs matching -- it will check the + database of flat pages based on the current URL. - Also removes the content from any response to a HEAD request and sets the - ``Date`` and ``Content-Length`` response-headers. +django.middleware.doc.XViewMiddleware +------------------------------------- -``django.middleware.sessions.SessionMiddleware`` - Enables session support. See the `session documentation`_. +Sends custom ``X-View`` HTTP headers to HEAD requests that come from IP +addresses defined in the ``INTERNAL_IPS`` setting. This is used by Django's +automatic documentation system. - .. _`session documentation`: http://www.djangoproject.com/documentation/sessions/ +django.middleware.gzip.GZipMiddleware +------------------------------------- + +Compresses content for browsers that understand gzip compression (all modern +browsers). + +django.middleware.http.ConditionalGetMiddleware +----------------------------------------------- + +Handles conditional GET operations. If the response has a ``ETag`` or +``Last-Modified`` header, and the request has ``If-None-Match`` or +``If-Modified-Since``, the response is replaced by an HttpNotModified. + +Also removes the content from any response to a HEAD request and sets the +``Date`` and ``Content-Length`` response-headers. + +django.middleware.sessions.SessionMiddleware +-------------------------------------------- + +Enables session support. See the `session documentation`_. + +.. _`session documentation`: http://www.djangoproject.com/documentation/sessions/ Writing your own middleware =========================== From f258a8fce28ca65f2cfb3798b36f262c65639190 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 10 Oct 2005 13:51:58 +0000 Subject: [PATCH 042/117] Fixed #600 -- decorator_from_middleware now handles process_view. Thanks, Hugo git-svn-id: http://code.djangoproject.com/svn/django/trunk@820 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/utils/decorators.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/django/utils/decorators.py b/django/utils/decorators.py index b21a4e4248..1333f9da88 100644 --- a/django/utils/decorators.py +++ b/django/utils/decorators.py @@ -12,6 +12,10 @@ def decorator_from_middleware(middleware_class): result = middleware.process_request(request) if result is not None: return result + if hasattr(middleware, 'process_view'): + result = middleware.process_view(request, view_func, **kwargs) + if result is not None: + return result response = view_func(request, *args, **kwargs) if hasattr(middleware, 'process_response'): result = middleware.process_response(request, response) From b4e2d12b1f3f7da728c78de4b790040a0080a2d7 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 10 Oct 2005 13:56:39 +0000 Subject: [PATCH 043/117] Fixed #599 -- locmem cache now uses deepcopy() to prevent aliasing. Thanks, Hugo git-svn-id: http://code.djangoproject.com/svn/django/trunk@821 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/cache.py | 71 ++++++++++++++++++-------------------- django/middleware/cache.py | 3 +- 2 files changed, 34 insertions(+), 40 deletions(-) diff --git a/django/core/cache.py b/django/core/cache.py index cbf02f2f3b..6391304158 100644 --- a/django/core/cache.py +++ b/django/core/cache.py @@ -15,7 +15,7 @@ The CACHE_BACKEND setting is a quasi-URI; examples are: memcached://127.0.0.1:11211/ A memcached backend; the server is running on localhost port 11211. - db://tablename/ A database backend in a table named + db://tablename/ A database backend in a table named "tablename". This table should be created with "django-admin createcachetable". @@ -26,7 +26,7 @@ The CACHE_BACKEND setting is a quasi-URI; examples are: probably don't want to use this except for testing. Note that this cache backend is NOT threadsafe! - + locmem:/// A more sophisticaed local memory cache; this is multi-process- and thread-safe. @@ -72,7 +72,6 @@ class InvalidCacheBackendError(Exception): ################################ class _Cache: - def __init__(self, params): timeout = params.get('timeout', 300) try: @@ -132,8 +131,7 @@ except ImportError: _MemcachedCache = None else: class _MemcachedCache(_Cache): - """Memcached cache backend.""" - + "Memcached cache backend." def __init__(self, server, params): _Cache.__init__(self, params) self._cache = memcache.Client([server]) @@ -161,8 +159,7 @@ else: import time class _SimpleCache(_Cache): - """Simple single-process in-memory cache""" - + "Simple single-process in-memory cache." def __init__(self, host, params): _Cache.__init__(self, params) self._cache = {} @@ -230,11 +227,11 @@ try: import cPickle as pickle except ImportError: import pickle +import copy from django.utils.synch import RWLock class _LocMemCache(_SimpleCache): - """Thread-safe in-memory cache""" - + "Thread-safe in-memory cache." def __init__(self, host, params): _SimpleCache.__init__(self, host, params) self._lock = RWLock() @@ -250,7 +247,7 @@ class _LocMemCache(_SimpleCache): elif exp < now: should_delete = True else: - return self._cache[key] + return copy.deepcopy(self._cache[key]) finally: self._lock.reader_leaves() if should_delete: @@ -261,14 +258,14 @@ class _LocMemCache(_SimpleCache): return default finally: self._lock.writer_leaves() - + def set(self, key, value, timeout=None): self._lock.writer_enters() try: _SimpleCache.set(self, key, value, timeout) finally: self._lock.writer_leaves() - + def delete(self, key): self._lock.writer_enters() try: @@ -284,8 +281,7 @@ import os import urllib class _FileCache(_SimpleCache): - """File-based cache""" - + "File-based cache." def __init__(self, dir, params): self._dir = dir if not os.path.exists(self._dir): @@ -293,7 +289,7 @@ class _FileCache(_SimpleCache): _SimpleCache.__init__(self, dir, params) del self._cache del self._expire_info - + def get(self, key, default=None): fname = self._key_to_file(key) try: @@ -308,7 +304,7 @@ class _FileCache(_SimpleCache): except (IOError, pickle.PickleError): pass return default - + def set(self, key, value, timeout=None): fname = self._key_to_file(key) if timeout is None: @@ -327,16 +323,16 @@ class _FileCache(_SimpleCache): pickle.dump(value, f, 2) except (IOError, OSError): raise - + def delete(self, key): try: os.remove(self._key_to_file(key)) except (IOError, OSError): pass - + def has_key(self, key): return os.path.exists(self._key_to_file(key)) - + def _cull(self, filelist): if self.cull_frequency == 0: doomed = filelist @@ -348,7 +344,7 @@ class _FileCache(_SimpleCache): except (IOError, OSError): pass - def _createdir(self): + def _createdir(self): try: os.makedirs(self._dir) except OSError: @@ -366,22 +362,21 @@ from django.core.db import db, DatabaseError from datetime import datetime class _DBCache(_Cache): - """SQL cache backend""" - + "SQL cache backend." def __init__(self, table, params): _Cache.__init__(self, params) self._table = table - max_entries = params.get('max_entries', 300) - try: - self._max_entries = int(max_entries) - except (ValueError, TypeError): - self._max_entries = 300 - cull_frequency = params.get('cull_frequency', 3) - try: - self._cull_frequency = int(cull_frequency) - except (ValueError, TypeError): - self._cull_frequency = 3 - + max_entries = params.get('max_entries', 300) + try: + self._max_entries = int(max_entries) + except (ValueError, TypeError): + self._max_entries = 300 + cull_frequency = params.get('cull_frequency', 3) + try: + self._cull_frequency = int(cull_frequency) + except (ValueError, TypeError): + self._cull_frequency = 3 + def get(self, key, default=None): cursor = db.cursor() cursor.execute("SELECT cache_key, value, expires FROM %s WHERE cache_key = %%s" % self._table, [key]) @@ -394,7 +389,7 @@ class _DBCache(_Cache): db.commit() return default return pickle.loads(base64.decodestring(row[1])) - + def set(self, key, value, timeout=None): if timeout is None: timeout = self.default_timeout @@ -417,17 +412,17 @@ class _DBCache(_Cache): pass else: db.commit() - + def delete(self, key): cursor = db.cursor() cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % self._table, [key]) db.commit() - + def has_key(self, key): cursor = db.cursor() cursor.execute("SELECT cache_key FROM %s WHERE cache_key = %%s" % self._table, [key]) return cursor.fetchone() is not None - + def _cull(self, cursor, now): if self._cull_frequency == 0: cursor.execute("DELETE FROM %s" % self._table) @@ -438,7 +433,7 @@ class _DBCache(_Cache): if num > self._max_entries: cursor.execute("SELECT cache_key FROM %s ORDER BY cache_key LIMIT 1 OFFSET %%s" % self._table, [num / self._cull_frequency]) cursor.execute("DELETE FROM %s WHERE cache_key < %%s" % self._table, [cursor.fetchone()[0]]) - + ########################################## # Read settings and load a cache backend # ########################################## diff --git a/django/middleware/cache.py b/django/middleware/cache.py index 8216c40ae1..04f98122f7 100644 --- a/django/middleware/cache.py +++ b/django/middleware/cache.py @@ -1,4 +1,3 @@ -import copy from django.conf import settings from django.core.cache import cache from django.utils.cache import get_cache_key, learn_cache_key, patch_response_headers @@ -49,7 +48,7 @@ class CacheMiddleware: return None # No cache information available, need to rebuild. request._cache_update_cache = False - return copy.copy(response) + return response def process_response(self, request, response): "Sets the cache, if needed." From 474cfe56d4a9563f5acf5846f718e480752d4ff3 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 10 Oct 2005 14:00:20 +0000 Subject: [PATCH 044/117] Fixed #601 -- Updated docs/model-api.txt unique_together section to say it's enforced at the database level. git-svn-id: http://code.djangoproject.com/svn/django/trunk@822 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/model-api.txt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/model-api.txt b/docs/model-api.txt index 4af193ca48..fd70409213 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -248,18 +248,18 @@ Here are all available field types: uploaded files don't fill up the given directory). The admin represents this as an ```` (a file-upload widget). - - Using a `FieldField` or an ``ImageField`` (see below) in a model takes a few + + Using a `FieldField` or an ``ImageField`` (see below) in a model takes a few steps: - + 1. In your settings file, you'll need to define ``MEDIA_ROOT``as the full path to a directory where you'd like Django to store uploaded files. (For performance, these files are not stored in the database.) Define ``MEDIA_URL`` as the base public URL of that directory. Make sure that this directory is writable by the Web server's user account. - - 2. Add the ``FileField`` or ``ImageField`` to your model, making sure + + 2. Add the ``FileField`` or ``ImageField`` to your model, making sure to define the ``upload_to`` option to tell Django to which subdirectory of ``MEDIA_ROOT`` it should upload files. @@ -269,7 +269,7 @@ Here are all available field types: example, if your ``ImageField`` is called ``mug_shot``, you can get the absolute URL to your image in a template with ``{{ object.get_mug_shot_url }}``. - + .. _`strftime formatting`: http://docs.python.org/lib/module-time.html#l2h-1941 ``FloatField`` @@ -302,7 +302,7 @@ Here are all available field types: width of the image each time a model instance is saved. Requires the `Python Imaging Library`_. - + .. _Python Imaging Library: http://www.pythonware.com/products/pil/ ``IntegerField`` @@ -721,7 +721,9 @@ Here's a list of all possible ``META`` options. No options are required. Adding unique_together = (("driver", "restaurant"),) This is a list of lists of fields that must be unique when considered - together. It's used in the Django admin. + together. It's used in the Django admin and is enforced at the database + level (i.e., the appropriate ``UNIQUE`` statements are included in the + ``CREATE TABLE`` statement). ``verbose_name`` A human-readable name for the object, singular:: From 29ff2bb4cbc7ffa3193ba0cec48308cf0f4c1295 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 10 Oct 2005 20:14:56 +0000 Subject: [PATCH 045/117] Improved django.core.management.get_sql_delete to close database connection explicitly when it's done, to avoid locking issues git-svn-id: http://code.djangoproject.com/svn/django/trunk@823 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/management.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/django/core/management.py b/django/core/management.py index afb498ae63..046031e311 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -144,6 +144,10 @@ def get_sql_delete(mod): for row in cursor.fetchall(): output.append("DELETE FROM auth_admin_log WHERE content_type_id = %s;" % row[0]) + # Close database connection explicitly, in case this output is being piped + # directly into a database client, to avoid locking issues. + db.db.close() + return output[::-1] # Reverse it, to deal with table dependencies. get_sql_delete.help_doc = "Prints the DROP TABLE SQL statements for the given model module name(s)." get_sql_delete.args = APP_ARGS From 705a2c31c1989f4ecdb086efed0e60a1448ac70d Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Mon, 10 Oct 2005 20:18:47 +0000 Subject: [PATCH 046/117] Fixed #604 - total number of objects in generic object_list view is now available as {{ hits }} git-svn-id: http://code.djangoproject.com/svn/django/trunk@824 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/views/generic/list_detail.py | 1 + 1 file changed, 1 insertion(+) diff --git a/django/views/generic/list_detail.py b/django/views/generic/list_detail.py index 6328b7097a..87b1879059 100644 --- a/django/views/generic/list_detail.py +++ b/django/views/generic/list_detail.py @@ -56,6 +56,7 @@ def object_list(request, app_label, module_name, paginate_by=None, allow_empty=F 'next': page + 1, 'previous': page - 1, 'pages': paginator.pages, + 'hits' : paginator.hits, }) else: object_list = mod.get_list(**lookup_kwargs) From eb4f16e666ae3d7c29b5bcae737ce755d5003a26 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 10 Oct 2005 20:18:56 +0000 Subject: [PATCH 047/117] Improved docs/db-api.txt to say add_FOO() methods always return the newly-created object. git-svn-id: http://code.djangoproject.com/svn/django/trunk@825 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/db-api.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/db-api.txt b/docs/db-api.txt index 8a02437aaa..b80d4e8647 100644 --- a/docs/db-api.txt +++ b/docs/db-api.txt @@ -449,8 +449,7 @@ Related objects (e.g. ``Choices``) are created using convenience functions:: >>> p.get_choice_count() 4 -Each of those ``add_choice`` methods is equivalent to (except obviously much -simpler than):: +Each of those ``add_choice`` methods is equivalent to (but much simpler than):: >>> c = polls.Choice(poll_id=p.id, choice="Over easy", votes=0) >>> c.save() @@ -459,6 +458,8 @@ Note that when using the `add_foo()`` methods, you do not give any value for the ``id`` field, nor do you give a value for the field that stores the relation (``poll_id`` in this case). +The ``add_FOO()`` method always returns the newly created object. + Deleting objects ================ From b8f70f8c94b5e7915f7a6b87b5327375d1d2b790 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Mon, 10 Oct 2005 20:23:53 +0000 Subject: [PATCH 048/117] Updated docs to reflect changes in [824] git-svn-id: http://code.djangoproject.com/svn/django/trunk@826 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/views/generic/list_detail.py | 2 ++ docs/generic_views.txt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/django/views/generic/list_detail.py b/django/views/generic/list_detail.py index 87b1879059..f4cdf90c56 100644 --- a/django/views/generic/list_detail.py +++ b/django/views/generic/list_detail.py @@ -32,6 +32,8 @@ def object_list(request, app_label, module_name, paginate_by=None, allow_empty=F the previous page pages number of pages, total + hits + number of objects, total """ mod = models.get_module(app_label, module_name) lookup_kwargs = extra_lookup_kwargs.copy() diff --git a/docs/generic_views.txt b/docs/generic_views.txt index 62e7c14e2b..4201423c28 100644 --- a/docs/generic_views.txt +++ b/docs/generic_views.txt @@ -246,6 +246,8 @@ Individual views are: The previous page ``pages`` Number of pages total + ``hits`` + Total number of objects ``object_detail`` Object detail page. This works like and takes the same arguments as From 5f5db2c236a2ba8cad85e58c8b0686ddafe70b0a Mon Sep 17 00:00:00 2001 From: Wilson Miner Date: Mon, 10 Oct 2005 21:49:31 +0000 Subject: [PATCH 049/117] Added .hidden class to admin styles for future use. git-svn-id: http://code.djangoproject.com/svn/django/trunk@827 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/admin_media/css/global.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/django/conf/admin_media/css/global.css b/django/conf/admin_media/css/global.css index 31156589e0..7c60bf9755 100644 --- a/django/conf/admin_media/css/global.css +++ b/django/conf/admin_media/css/global.css @@ -230,6 +230,7 @@ fieldset.collapsed h2, fieldset.collapsed { display:block !important; } fieldset.collapsed .collapse-toggle { display: inline !important; } fieldset.collapse h2 a.collapse-toggle { color:#ffc; } fieldset.collapse h2 a.collapse-toggle:hover { text-decoration:underline; } +.hidden { display:none; } /* MESSAGES & ERRORS */ @@ -348,7 +349,7 @@ p.file-upload { line-height:20px; margin:0; padding:0; color:#666; font-size:11p ul.timelist, .timelist li { list-style-type:none; margin:0; padding:0; } .timelist a { padding:2px; } -/* OLD ORDERING WIDGET */ +/* ORDERING WIDGET */ ul#orderthese { padding:0; margin:0; list-style-type:none; } ul#orderthese li { list-style-type:none; display:block; padding:0; margin:6px 0; width:214px; background:#f6f6f6; white-space:nowrap; overflow:hidden; } From fef8adefe173e809c582a6611b6eb58577e0c9f0 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 10 Oct 2005 22:23:44 +0000 Subject: [PATCH 050/117] Fixed #605 -- Fixed template-name errors in docs/generic_views.txt. Thanks, cygnus@cprogrammer.org git-svn-id: http://code.djangoproject.com/svn/django/trunk@828 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/generic_views.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/generic_views.txt b/docs/generic_views.txt index 4201423c28..1c0de07a7a 100644 --- a/docs/generic_views.txt +++ b/docs/generic_views.txt @@ -115,7 +115,7 @@ The date-based generic functions are: Yearly archive. Requires that the ``year`` argument be present in the URL pattern. - Uses the template ``app_label/module_name__archive_year`` by default. + Uses the template ``app_label/module_name_archive_year`` by default. Has the following template context: @@ -134,7 +134,7 @@ The date-based generic functions are: default, which is a three-letter month abbreviation. To change it to use numbers, use ``"%m"``. - Uses the template ``app_label/module_name__archive_month`` by default. + Uses the template ``app_label/module_name_archive_month`` by default. Has the following template context: @@ -151,7 +151,7 @@ The date-based generic functions are: also pass ``day_format``, which defaults to ``"%d"`` (day of the month as a decimal number, 1-31). - Uses the template ``app_label/module_name__archive_day`` by default. + Uses the template ``app_label/module_name_archive_day`` by default. Has the following template context: @@ -274,7 +274,7 @@ The create/update/delete views are: be interpolated against the object's field attributes. For example, you could use ``post_save_redirect="/polls/%(slug)s/"``. - Uses the template ``app_label/module_name__form`` by default. This is the + 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. @@ -296,7 +296,7 @@ The create/update/delete views are: ``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. + Uses the template ``app_label/module_name_form`` by default. Has the following template context: From 179017afb57a1b0432ef769819f61a4b98997a25 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Tue, 11 Oct 2005 18:32:48 +0000 Subject: [PATCH 051/117] Fixed formatting bug in docs/model-api.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@842 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/model-api.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/model-api.txt b/docs/model-api.txt index fd70409213..2ad2b3594d 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -95,8 +95,8 @@ The following arguments are available to all field types. All are optional. ('GR', 'Graduate'), ) - The first element in each tuple is the actual value to be stored. The - second element is the human-readable name for the option. + The first element in each tuple is the actual value to be stored. The + second element is the human-readable name for the option. ``core`` For objects that are edited inline to a related object. From 9fdacc7a66e7db8ba7db351540c16f0a2fd631e7 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Wed, 12 Oct 2005 03:52:05 +0000 Subject: [PATCH 052/117] Added note to docs/modpython.txt about non-dev arrangements not serving admin media files git-svn-id: http://code.djangoproject.com/svn/django/trunk@845 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/modpython.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/modpython.txt b/docs/modpython.txt index acac50a6d5..d24ea29018 100644 --- a/docs/modpython.txt +++ b/docs/modpython.txt @@ -143,6 +143,9 @@ particular part of the site:: Just change ``Location`` to the root URL of your media files. +Note that the Django development server automagically serves admin media files, +but this is not the case when you use any other server arrangement. + .. _lighttpd: http://www.lighttpd.net/ .. _TUX: http://en.wikipedia.org/wiki/TUX_web_server .. _Apache: http://httpd.apache.org/ From 5f9fe6d403dcee757778fb2c800e7866cca4b185 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Wed, 12 Oct 2005 04:14:21 +0000 Subject: [PATCH 053/117] Fixed #589 -- Added FilePathField. It's available as an ORM field and as a standalone field in django.core.formfields. Thanks, jay@skabber.com git-svn-id: http://code.djangoproject.com/svn/django/trunk@846 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/db/backends/mysql.py | 1 + django/core/db/backends/postgresql.py | 1 + django/core/db/backends/sqlite3.py | 1 + django/core/formfields.py | 23 ++++++++++++++++++ django/core/meta/fields.py | 8 +++++++ docs/model-api.txt | 34 +++++++++++++++++++++++++++ 6 files changed, 68 insertions(+) diff --git a/django/core/db/backends/mysql.py b/django/core/db/backends/mysql.py index 2e77adbc43..af0dbca6c0 100644 --- a/django/core/db/backends/mysql.py +++ b/django/core/db/backends/mysql.py @@ -143,6 +143,7 @@ DATA_TYPES = { 'DateTimeField': 'datetime', 'EmailField': 'varchar(75)', 'FileField': 'varchar(100)', + 'FilePathField': 'varchar(100)', 'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)', 'ImageField': 'varchar(100)', 'IntegerField': 'integer', diff --git a/django/core/db/backends/postgresql.py b/django/core/db/backends/postgresql.py index 683ae3c9ee..6ec7bfbfcb 100644 --- a/django/core/db/backends/postgresql.py +++ b/django/core/db/backends/postgresql.py @@ -154,6 +154,7 @@ DATA_TYPES = { 'DateTimeField': 'timestamp with time zone', 'EmailField': 'varchar(75)', 'FileField': 'varchar(100)', + 'FilePathField': 'varchar(100)', 'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)', 'ImageField': 'varchar(100)', 'IntegerField': 'integer', diff --git a/django/core/db/backends/sqlite3.py b/django/core/db/backends/sqlite3.py index d4b936f82e..ea05302a61 100644 --- a/django/core/db/backends/sqlite3.py +++ b/django/core/db/backends/sqlite3.py @@ -154,6 +154,7 @@ DATA_TYPES = { 'DateTimeField': 'datetime', 'EmailField': 'varchar(75)', 'FileField': 'varchar(100)', + 'FilePathField': 'varchar(100)', 'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)', 'ImageField': 'varchar(100)', 'IntegerField': 'integer', diff --git a/django/core/formfields.py b/django/core/formfields.py index 76721ba5c6..9b9f0af1d1 100644 --- a/django/core/formfields.py +++ b/django/core/formfields.py @@ -707,6 +707,29 @@ class IPAddressField(TextField): # MISCELLANEOUS # #################### +class FilePathField(SelectField): + "A SelectField whose choices are the files in a given directory." + def __init__(self, field_name, path, match=None, recursive=False, is_required=False, validator_list=[]): + import os + if match is not None: + import re + match_re = re.compile(match) + choices = [] + if recursive: + for root, dirs, files in os.walk(path): + for f in files: + if match is None or match_re.search(f): + choices.append((os.path.join(path, f), f)) + else: + try: + for f in os.listdir(path): + full_file = os.path.join(path, f) + if os.path.isfile(full_file) and (match is None or match_re.search(f)): + choices.append((full_file, f)) + except OSError: + pass + SelectField.__init__(self, field_name, choices, 1, is_required, validator_list) + class PhoneNumberField(TextField): "A convenience FormField for validating phone numbers (e.g. '630-555-1234')" def __init__(self, field_name, is_required=False, validator_list=[]): diff --git a/django/core/meta/fields.py b/django/core/meta/fields.py index 376595230c..0740010579 100644 --- a/django/core/meta/fields.py +++ b/django/core/meta/fields.py @@ -427,6 +427,14 @@ class FileField(Field): f = os.path.join(self.get_directory_name(), get_valid_filename(os.path.basename(filename))) return os.path.normpath(f) +class FilePathField(Field): + def __init__(self, verbose_name=None, name=None, path='', match=None, recursive=False, **kwargs): + self.path, self.match, self.recursive = path, match, recursive + Field.__init__(self, verbose_name, name, **kwargs) + + def get_manipulator_field_objs(self): + return [curry(formfields.FilePathField, path=self.path, match=self.match, recursive=self.recursive)] + class FloatField(Field): empty_strings_allowed = False def __init__(self, verbose_name=None, name=None, max_digits=None, decimal_places=None, **kwargs): diff --git a/docs/model-api.txt b/docs/model-api.txt index 2ad2b3594d..19b69b2d66 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -272,6 +272,40 @@ Here are all available field types: .. _`strftime formatting`: http://docs.python.org/lib/module-time.html#l2h-1941 +``FilePathField`` + A field whose choices are limited to the filenames in a certain directory + on the filesystem. Has three special arguments, of which the first is + required: + + ====================== =================================================== + Argument Description + ====================== =================================================== + ``path`` Required. The absolute filesystem path to a + directory from which this ``FilePathField`` should + get its choices. Example: ``"/home/images"``. + + ``match`` Optional. A regular expression, as a string, that + ``FilePathField`` will use to filter filenames. + Note that the regex will be applied to the + base filename, not the full path. Example: + ``"foo.*\.txt^"``, which will match a file called + ``foo23.txt`` but not ``bar.txt`` or ``foo23.gif``. + + ``recursive`` Optional. Either ``True`` or ``False``. Default is + ``False``. Specifies whether all subdirectories of + ``path`` should be included. + + Of course, these arguments can be used together. + + The one potential gotcha is that ``match`` applies to the base filename, + not the full path. So, this example:: + + FilePathField(path="/home/images", match="foo.*", recursive=True) + + ...will match ``/home/images/foo.gif`` but not ``/home/images/foo/bar.gif`` + because the ``match`` applies to the base filename (``foo.gif`` and + ``bar.gif``). + ``FloatField`` A floating-point number. Has two **required** arguments: From eb5d802cc2e48f7c035e0f33a84d9fef8e58bad9 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Wed, 12 Oct 2005 04:17:06 +0000 Subject: [PATCH 054/117] Fixed ReST bug in docs/model-api.txt from [846] git-svn-id: http://code.djangoproject.com/svn/django/trunk@847 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/model-api.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/model-api.txt b/docs/model-api.txt index 19b69b2d66..140518e80e 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -294,6 +294,7 @@ Here are all available field types: ``recursive`` Optional. Either ``True`` or ``False``. Default is ``False``. Specifies whether all subdirectories of ``path`` should be included. + ====================== =================================================== Of course, these arguments can be used together. From 84a30e709677deea2f4b2ce6661329d76b73e9b0 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Wed, 12 Oct 2005 13:25:24 +0000 Subject: [PATCH 055/117] Added link to server-arrangements page from docs/install.txt. Thanks, Alice git-svn-id: http://code.djangoproject.com/svn/django/trunk@849 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/install.txt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/install.txt b/docs/install.txt index b347006cbb..b18d26d5c8 100644 --- a/docs/install.txt +++ b/docs/install.txt @@ -21,14 +21,15 @@ See `How to use Django with mod_python`_ for information on how to configure mod_python once you have it installed. If you can't use mod_python for some reason, fear not: Django follows the WSGI_ -spec, which allows it to run on a variety of server platforms. As people -experiment with different server platforms, we'll update this document to -give specific installation instructions for each platform. +spec, which allows it to run on a variety of server platforms. See the +`server-arrangements wiki page`_ for specific installation instructions for +each platform. .. _Apache: http://httpd.apache.org/ .. _mod_python: http://www.modpython.org/ .. _WSGI: http://www.python.org/peps/pep-0333.html .. _How to use Django with mod_python: http://www.djangoproject.com/documentation/modpython/ +.. _server-arrangements wiki page: http://code.djangoproject.com/wiki/ServerArrangements Get your database running ========================= @@ -37,11 +38,6 @@ If you plan to use Django's database API functionality, you'll need to make sure a database server is running. Django works with PostgreSQL_ (recommended), MySQL_ and SQLite_. -Note that support for MySQL and SQLite is a recent development, and Django -hasn't been comprehensively tested in those environments. If you find any bugs -in Django's MySQL or SQLite bindings, please file them in -`Django's ticket system`_ so we can fix them immediately. - Additionally, you'll need to make sure your Python database bindings are installed. From 5e5fe5b5a2abe9fe680085ae1aa89a765ae276cd Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 03:28:03 +0000 Subject: [PATCH 056/117] Added docs/design_philosophies.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@855 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/design_philosophies.txt | 243 +++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 docs/design_philosophies.txt diff --git a/docs/design_philosophies.txt b/docs/design_philosophies.txt new file mode 100644 index 0000000000..9bfe81ddc1 --- /dev/null +++ b/docs/design_philosophies.txt @@ -0,0 +1,243 @@ +=================== +Design philosophies +=================== + +This document explains some of the fundamental philosophies Django's developers +have used in creating the framework. Its goal is to explain the past and guide +the future. + +Overall +======= + +Loose coupling +-------------- + +A fundamental goal of Django's stack is `loose coupling and tight cohesion`_. +The various layers of the framework shouldn't "know" about each other unless +absolutely necessary. + +For example, the template system knows nothing about Web requests, the database +layer knows nothing about data display and the view system doesn't care which +template system a programmer uses. + +.. _`loose coupling and tight cohesion`: http://c2.com/cgi/wiki?CouplingAndCohesion + +Less code +--------- + +Django apps should use as little code as possible; they should lack boilerplate. +Django should take full advantage of Python's dynamic capabilities, such as +introspection. + +Quick development +----------------- + +The point of a Web framework in the 21st century is to make the tedious aspects +of Web development fast. Django should allow for incredibly quick Web +development. + +Don't repeat yourself (DRY) +--------------------------- + +Every distinct concept and/or piece of data should live in one, and only one, +place. Redundancy is bad. Normalization is good. + +The framework, within reason, should deduce as much as possible from as little +as possible. + +Explicit is better than implicit +-------------------------------- + +This, a `core Python principle`_, means Django shouldn't do too much "magic." +Magic shouldn't happen unless there's a really good reason for it. + +.. _`core Python principle`: http://www.python.org/doc/Humor.html#zen + +Consistency +----------- + +The framework should be consistent at all levels. Consistency applies to +everything from low-level (the Python coding style used) to high-level (the +"experience" of using Django). + +Models +====== + +Explicit is better than implicit +-------------------------------- + +Fields shouldn't assume certain behaviors based solely on the name of the +field. This requires too much knowledge of the system and is prone to errors. +Instead, behaviors should be based on keyword arguments and, in some cases, on +the type of the field. + +Include all relevant domain logic +--------------------------------- + +Models should encapsulate every aspect of an "object," following Martin +Fowler's `Active Record`_ design pattern. + +This is why model-specific admin options are included in the model itself; data +related to a model should be stored *in* the model. + +.. _`Active Record`: http://www.martinfowler.com/eaaCatalog/activeRecord.html + +Database API +============ + +The core goals of the database API are: + +SQL efficiency +-------------- + +It should execute SQL statements as few times as possible, and it should +optimize statements internally. + +This is why developers need to call ``save()`` explicitly, rather than the +framework saving things behind the scenes silently. + +This is also why the ``select_related`` argument exists. It's an optional +performance booster for the common case of selecting "every related object." + +Terse, powerful syntax +---------------------- + +The database API should allow rich, expressive statements in as little syntax +as possible. It should not rely on importing other modules or helper objects. + +Joins should be performed automatically, behind the scenes, when necessary. + +Every object should be able to access every related object, systemwide. This +access should work both ways. + +Option to drop into raw SQL easily, when needed +----------------------------------------------- + +The database API should realize it's a shortcut but not necessarily an +end-all-be-all. The framework should make it easy to write custom SQL -- entire +statements, or just custom ``WHERE`` clauses as custom parameters to API calls. + +URL design +========== + +Loose coupling +-------------- + +URLs in a Django app should not be coupled to the underlying Python code. Tying +URLs to Python function names is a Bad And Ugly Thing. + +Along these lines, the Django URL system should allow URLs for the same app to +be different in different contexts. For example, one site may put stories at +``/stories/``, while another may use ``/news/``. + +Infinite flexibility +-------------------- + +URLs should be as flexible as possible. Any conceivable URL design should be +allowed. + +Encourage best practices +------------------------ + +The framework should make it just as easy (or even easier) for a developer to +design pretty URLs than ugly ones. + +File extensions in Web-page URLs should be avoided. + +Definitive URLs +--------------- + +Technically, ``foo.com/bar`` and ``foo.com/bar/`` are two different URLs, and +search-engine robots (and some Web traffic-analyzing tools) would treat them as +separate pages. Django should make an effort to "normalize" URLs so that +search-engine robots don't get confused. + +This is the reasoning behind the ``APPEND_SLASH`` setting. + +Template system +=============== + +Separate logic from presentation +-------------------------------- + +We see a template system as a tool that controls presentation and +presentation-related logic -- and that's it. The template system shouldn't +support functionality that goes beyond this basic goal. + +If we wanted to put everything in templates, we'd be using PHP. Been there, +done that, wised up. + +Discourage redundancy +--------------------- + +The majority of dynamic Web sites use some sort of common sitewide design -- +a common header, footer, navigation bar, etc. The Django template system should +make it easy to store those elements in a single place, eliminating duplicate +code. + +This is the philosophy behind template inheritance. + +Be decoupled from HTML +---------------------- + +The template system shouldn't be designed so that it only outputs HTML. It +should be equally good at generating other text-based formats, or just plain +text. + +Assume designer competence +-------------------------- + +The template system shouldn't be designed so that templates are displayed +nicely in WYSIWYG editors such as Dreamweaver. That is too severe of a +limitation and wouldn't allow the syntax to be as nice as it is. Django expects +some level of competence in template authors. + +Treat whitespace obviously +-------------------------- + +The template system do magic things with whitespace. If a template includes +whitespace, the system should treat it as it treats text -- just display it. + +Don't invent a programming language +----------------------------------- + +The template system intentionally doesn't allow the following: + + * Assignment to variables + * Advanced logic + +The goal is not to invent a programming language. The goal is to offer just +enough programming-esque functionality, such as branching and looping, that are +essential for making presentation-related decisions. + +Extensibility +------------- + +The template system should recognize that advanced template authors may want +to extend its technology. + +This is the philosophy behind custom template tags and filters. + +Views +===== + +Simplicity +---------- + +Writing a view should be as simple as writing a Python function. Developers +shouldn't have to instantiate a class when a function will do. + +Use request objects +------------------- + +Views should have access to a request object -- an object that stores metadata +about the current request. The object should be passed directly to a view +function, rather than the view function having to access the request data from +a global variable. This makes it light, clean and easy to test views by passing +in "fake" request objects. + +Loose coupling +-------------- + +A view shouldn't care about which template system the developer uses -- or even +whether a template system is used at all. From cdfbdfb1b23be17ac2aac00a10db8874b54fd7e2 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 03:32:34 +0000 Subject: [PATCH 057/117] Cleaned up 'Assume designer competence' section in docs/design_philosophies.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@856 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/design_philosophies.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/design_philosophies.txt b/docs/design_philosophies.txt index 9bfe81ddc1..9553e4fb44 100644 --- a/docs/design_philosophies.txt +++ b/docs/design_philosophies.txt @@ -187,10 +187,10 @@ text. Assume designer competence -------------------------- -The template system shouldn't be designed so that templates are displayed -nicely in WYSIWYG editors such as Dreamweaver. That is too severe of a -limitation and wouldn't allow the syntax to be as nice as it is. Django expects -some level of competence in template authors. +The template system shouldn't be designed so that templates necessarily are +displayed nicely in WYSIWYG editors such as Dreamweaver. That is too severe of +a limitation and wouldn't allow the syntax to be as nice as it is. Django +expects some level of code competence in template authors. Treat whitespace obviously -------------------------- From d5bbe395c216f3b71ea1ab4a0d820df9a229d5b1 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 03:33:54 +0000 Subject: [PATCH 058/117] Small grammar cleanup to design_philosophies.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@857 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/design_philosophies.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/design_philosophies.txt b/docs/design_philosophies.txt index 9553e4fb44..257c3b0c2e 100644 --- a/docs/design_philosophies.txt +++ b/docs/design_philosophies.txt @@ -196,7 +196,8 @@ Treat whitespace obviously -------------------------- The template system do magic things with whitespace. If a template includes -whitespace, the system should treat it as it treats text -- just display it. +whitespace, the system should treat the whitespace as it treats text -- just +display it. Don't invent a programming language ----------------------------------- From f2088d456c4ca394f59d179e990c6e611e92ff5f Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 03:43:01 +0000 Subject: [PATCH 059/117] Fixed #618 -- Added DATABASE_PORT setting. Thanks, Esaj git-svn-id: http://code.djangoproject.com/svn/django/trunk@858 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/global_settings.py | 3 ++- django/conf/project_template/settings/main.py | 1 + django/core/db/backends/mysql.py | 14 +++++++++++--- django/core/db/backends/postgresql.py | 4 +++- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index c651543ff9..ea2fc440de 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -49,7 +49,8 @@ DATABASE_ENGINE = 'postgresql' # 'postgresql', 'mysql', or 'sqlite3'. DATABASE_NAME = '' DATABASE_USER = '' DATABASE_PASSWORD = '' -DATABASE_HOST = '' # Set to empty string for localhost +DATABASE_HOST = '' # Set to empty string for localhost. +DATABASE_PORT = '' # Set to empty string for default. # Host for sending e-mail. EMAIL_HOST = 'localhost' diff --git a/django/conf/project_template/settings/main.py b/django/conf/project_template/settings/main.py index 56b1f023ce..38df2ad01d 100644 --- a/django/conf/project_template/settings/main.py +++ b/django/conf/project_template/settings/main.py @@ -15,6 +15,7 @@ DATABASE_NAME = '' # Or path to database file if using sqlite3. DATABASE_USER = '' # Not used with sqlite3. DATABASE_PASSWORD = '' # Not used with sqlite3. DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. +DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. SITE_ID = 1 diff --git a/django/core/db/backends/mysql.py b/django/core/db/backends/mysql.py index af0dbca6c0..4399b6a6a0 100644 --- a/django/core/db/backends/mysql.py +++ b/django/core/db/backends/mysql.py @@ -53,10 +53,18 @@ class DatabaseWrapper: self.queries = [] def cursor(self): - from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PASSWORD, DEBUG + from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PORT, DATABASE_PASSWORD, DEBUG if self.connection is None: - self.connection = Database.connect(user=DATABASE_USER, db=DATABASE_NAME, - passwd=DATABASE_PASSWORD, host=DATABASE_HOST, conv=django_conversions) + kwargs = { + 'user': DATABASE_USER, + 'db': DATABASE_NAME, + 'passwd': DATABASE_PASSWORD, + 'host': DATABASE_HOST, + 'conv': django_conversions, + } + if DATABASE_PORT: + kwargs['port'] = DATABASE_PORT + self.connection = Database.connect(**kwargs) if DEBUG: return base.CursorDebugWrapper(MysqlDebugWrapper(self.connection.cursor()), self) return self.connection.cursor() diff --git a/django/core/db/backends/postgresql.py b/django/core/db/backends/postgresql.py index 6ec7bfbfcb..c922fd42f6 100644 --- a/django/core/db/backends/postgresql.py +++ b/django/core/db/backends/postgresql.py @@ -15,7 +15,7 @@ class DatabaseWrapper: self.queries = [] def cursor(self): - from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PASSWORD, DEBUG, TIME_ZONE + from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PORT, DATABASE_PASSWORD, DEBUG, TIME_ZONE if self.connection is None: if DATABASE_NAME == '': from django.core.exceptions import ImproperlyConfigured @@ -27,6 +27,8 @@ class DatabaseWrapper: conn_string += " password=%s" % DATABASE_PASSWORD if DATABASE_HOST: conn_string += " host=%s" % DATABASE_HOST + if DATABASE_PORT: + conn_string += " port=%s" % DATABASE_PORT self.connection = Database.connect(conn_string) self.connection.set_isolation_level(1) # make transactions transparent to all cursors cursor = self.connection.cursor() From ed1c9b20466324b085fb055b28c515ea3443e2a9 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 03:48:27 +0000 Subject: [PATCH 060/117] Fixed #622 -- Added default_if_none filter. Thanks, Eric git-svn-id: http://code.djangoproject.com/svn/django/trunk@859 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/defaultfilters.py | 6 +++ docs/templates.txt | 84 ++++++++++++++++++----------------- 2 files changed, 50 insertions(+), 40 deletions(-) diff --git a/django/core/defaultfilters.py b/django/core/defaultfilters.py index eb3ec11c95..42b46d03ea 100644 --- a/django/core/defaultfilters.py +++ b/django/core/defaultfilters.py @@ -333,6 +333,12 @@ def default(value, arg): "If value is unavailable, use given default" return value or arg +def default_if_none(value, arg): + "If value is None, use given default" + if value is None: + return arg + return value + def divisibleby(value, arg): "Returns true if the value is devisible by the argument" return int(value) % int(arg) == 0 diff --git a/docs/templates.txt b/docs/templates.txt index a6848a9638..15abaaedaa 100644 --- a/docs/templates.txt +++ b/docs/templates.txt @@ -376,9 +376,9 @@ Built-in tag reference ========================== ================================================ ``forloop.counter`` The current iteration of the loop (1-indexed) ``forloop.counter0`` The current iteration of the loop (0-indexed) - ``forloop.revcounter`` The number of iterations from the end of the + ``forloop.revcounter`` The number of iterations from the end of the loop (1-indexed) - ``forloop.revcounter0`` The number of iterations from the end of the + ``forloop.revcounter0`` The number of iterations from the end of the loop (0-indexed) ``forloop.first`` True if this is the first time through the loop ``forloop.last`` True if this is the last time through the loop @@ -569,25 +569,28 @@ Built-in filter reference ------------------------- ``add`` - Adds the arg to the value + Adds the arg to the value. ``addslashes`` - Adds slashes - useful for passing strings to JavaScript, for example. + Adds slashes. Useful for passing strings to JavaScript, for example. ``capfirst`` - Capitalizes the first character of the value + Capitalizes the first character of the value. ``center`` - Centers the value in a field of a given width + Centers the value in a field of a given width. ``cut`` - Removes all values of arg from the given string + Removes all values of arg from the given string. ``date`` - Formats a date according to the given format (same as the ``now`` tag) + Formats a date according to the given format (same as the ``now`` tag). ``default`` - If value is unavailable, use given default + If value is unavailable, use given default. + +``default_if_none`` + If value is ``None``, use given default. ``dictsort`` Takes a list of dicts, returns that list sorted by the property given in the @@ -598,24 +601,24 @@ Built-in filter reference given in the argument. ``divisibleby`` - Returns true if the value is divisible by the argument + Returns true if the value is divisible by the argument. ``escape`` - Escapes a string's HTML + Escapes a string's HTML. ``filesizeformat`` Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 102 bytes, etc). ``first`` - Returns the first item in a list + Returns the first item in a list. ``fix_ampersands`` - Replaces ampersands with ``&`` entities + Replaces ampersands with ``&`` entities. ``floatformat`` Displays a floating point number as 34.2 (with one decimal places) - but - only if there's a point to be displayed + only if there's a point to be displayed. ``get_digit`` Given a whole number, returns the requested digit of it, where 1 is the @@ -624,52 +627,52 @@ Built-in filter reference or if argument is less than 1). Otherwise, output is always an integer. ``join`` - Joins a list with a string, like Python's ``str.join(list)`` + Joins a list with a string, like Python's ``str.join(list)``. ``length`` - Returns the length of the value - useful for lists + Returns the length of the value. Useful for lists. ``length_is`` - Returns a boolean of whether the value's length is the argument + Returns a boolean of whether the value's length is the argument. ``linebreaks`` - Converts newlines into

    and
    s + Converts newlines into ``

    `` and ``
    ``s. ``linebreaksbr`` - Converts newlines into
    s + Converts newlines into ``
    ``s. ``linenumbers`` - Displays text with line numbers + Displays text with line numbers. ``ljust`` - Left-aligns the value in a field of a given width + Left-aligns the value in a field of a given width. **Argument:** field size ``lower`` - Converts a string into all lowercase + Converts a string into all lowercase. ``make_list`` Returns the value turned into a list. For an integer, it's a list of digits. For a string, it's a list of characters. ``phone2numeric`` - Takes a phone number and converts it in to its numerical equivalent + Takes a phone number and converts it in to its numerical equivalent. ``pluralize`` - Returns 's' if the value is not 1, for '1 vote' vs. '2 votes' + Returns 's' if the value is not 1, for '1 vote' vs. '2 votes'. ``pprint`` - A wrapper around pprint.pprint -- for debugging, really + A wrapper around pprint.pprint -- for debugging, really. ``random`` - Returns a random item from the list + Returns a random item from the list. ``removetags`` - Removes a space separated list of [X]HTML tags from the output + Removes a space separated list of [X]HTML tags from the output. ``rjust`` - Right-aligns the value in a field of a given width + Right-aligns the value in a field of a given width. **Argument:** field size @@ -696,19 +699,19 @@ Built-in filter reference of Python string formatting ``striptags`` - Strips all [X]HTML tags + Strips all [X]HTML tags. ``time`` Formats a time according to the given format (same as the ``now`` tag). ``timesince`` - Formats a date as the time since that date (i.e. "4 days, 6 hours") + Formats a date as the time since that date (i.e. "4 days, 6 hours"). ``title`` - Converts a string into titlecase + Converts a string into titlecase. ``truncatewords`` - Truncates a string after a certain number of words + Truncates a string after a certain number of words. **Argument:** Number of words to truncate after @@ -733,26 +736,27 @@ Built-in filter reference ``upper`` - Converts a string into all uppercase + Converts a string into all uppercase. ``urlencode`` - Escapes a value for use in a URL + Escapes a value for use in a URL. ``urlize`` - Converts URLs in plain text into clickable links + Converts URLs in plain text into clickable links. ``urlizetrunc`` - Converts URLs into clickable links, truncating URLs to the given character limit + Converts URLs into clickable links, truncating URLs to the given character + limit. - **Argument:** Length to truncate URLs to. + **Argument:** Length to truncate URLs to ``wordcount`` - Returns the number of words + Returns the number of words. ``wordwrap`` - Wraps words at specified line length + Wraps words at specified line length. - **Argument:** number of words to wrap the text at. + **Argument:** number of words at which to wrap the text ``yesno`` Given a string mapping values for true, false and (optionally) None, From fdcc9da59c92dae4d17b0c0e71d4cc666ba759f5 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 14:05:46 +0000 Subject: [PATCH 061/117] Fixed typo in docs/design_philosophies.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@860 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/design_philosophies.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/design_philosophies.txt b/docs/design_philosophies.txt index 257c3b0c2e..b718942a09 100644 --- a/docs/design_philosophies.txt +++ b/docs/design_philosophies.txt @@ -195,9 +195,9 @@ expects some level of code competence in template authors. Treat whitespace obviously -------------------------- -The template system do magic things with whitespace. If a template includes -whitespace, the system should treat the whitespace as it treats text -- just -display it. +The template system shouldn't do magic things with whitespace. If a template +includes whitespace, the system should treat the whitespace as it treats text +-- just display it. Don't invent a programming language ----------------------------------- From 42f8a069aa55a6edce316a1164800965c200ad65 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 14:06:49 +0000 Subject: [PATCH 062/117] Grammar fix to docs/design_philosophies.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@861 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/design_philosophies.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design_philosophies.txt b/docs/design_philosophies.txt index b718942a09..720694e7a3 100644 --- a/docs/design_philosophies.txt +++ b/docs/design_philosophies.txt @@ -208,7 +208,7 @@ The template system intentionally doesn't allow the following: * Advanced logic The goal is not to invent a programming language. The goal is to offer just -enough programming-esque functionality, such as branching and looping, that are +enough programming-esque functionality, such as branching and looping, that is essential for making presentation-related decisions. Extensibility From f2b8e85360ddc115ee2ed6ed781be260c0110150 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 15:02:44 +0000 Subject: [PATCH 063/117] Fixed ReST bugs in docs/templates.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@862 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/templates.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/templates.txt b/docs/templates.txt index 15abaaedaa..843ed0cbaa 100644 --- a/docs/templates.txt +++ b/docs/templates.txt @@ -636,10 +636,10 @@ Built-in filter reference Returns a boolean of whether the value's length is the argument. ``linebreaks`` - Converts newlines into ``

    `` and ``
    ``s. + Converts newlines into

    and
    s. ``linebreaksbr`` - Converts newlines into ``
    ``s. + Converts newlines into
    s. ``linenumbers`` Displays text with line numbers. From 2db3d9c7ccf5e43713592cec4832fdbe4bc4b40f Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 15:04:21 +0000 Subject: [PATCH 064/117] Reworded part of design_philosophies.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@863 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/design_philosophies.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design_philosophies.txt b/docs/design_philosophies.txt index 720694e7a3..2084c992a5 100644 --- a/docs/design_philosophies.txt +++ b/docs/design_philosophies.txt @@ -190,7 +190,7 @@ Assume designer competence The template system shouldn't be designed so that templates necessarily are displayed nicely in WYSIWYG editors such as Dreamweaver. That is too severe of a limitation and wouldn't allow the syntax to be as nice as it is. Django -expects some level of code competence in template authors. +expects template authors are comfortable editing HTML directly. Treat whitespace obviously -------------------------- From b773bf45c3011dc95617dcbec9584c8d139c27cc Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Fri, 14 Oct 2005 18:37:16 +0000 Subject: [PATCH 065/117] Registered default_if_none filter from [859] git-svn-id: http://code.djangoproject.com/svn/django/trunk@865 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/defaultfilters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/django/core/defaultfilters.py b/django/core/defaultfilters.py index 42b46d03ea..b9e0e67745 100644 --- a/django/core/defaultfilters.py +++ b/django/core/defaultfilters.py @@ -422,6 +422,7 @@ template.register_filter('center', center, True) template.register_filter('cut', cut, True) template.register_filter('date', date, True) template.register_filter('default', default, True) +template.register_filter('default_if_none', default_if_none, True) template.register_filter('dictsort', dictsort, True) template.register_filter('dictsortreversed', dictsortreversed, True) template.register_filter('divisibleby', divisibleby, True) From f71f8546283dbdf698c7578f8f9154045c84f9e7 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 20:10:13 +0000 Subject: [PATCH 066/117] Fixed #626 -- Moved template modules to django.core.template package. django.core.template_loader is deprecated, in favor of django.core.template.loader. git-svn-id: http://code.djangoproject.com/svn/django/trunk@867 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/extensions.py | 5 +- django/core/rss.py | 13 +- .../{template.py => template/__init__.py} | 0 django/core/{ => template}/defaultfilters.py | 104 +++++------ django/core/{ => template}/defaulttags.py | 165 ++++++++--------- django/core/template/loader.py | 163 +++++++++++++++++ django/core/template/loaders/__init__.py | 0 .../loaders/filesystem.py} | 2 +- django/core/template_loader.py | 167 +----------------- django/middleware/admin.py | 10 +- django/views/admin/doc.py | 3 +- django/views/admin/template.py | 9 +- django/views/auth/login.py | 2 +- django/views/defaults.py | 7 +- django/views/registration/passwords.py | 5 +- docs/forms.txt | 36 +--- docs/sessions.txt | 4 +- docs/templates_python.txt | 10 +- docs/tutorial03.txt | 9 +- docs/tutorial04.txt | 4 +- tests/othertests/templates.py | 11 +- 21 files changed, 355 insertions(+), 374 deletions(-) rename django/core/{template.py => template/__init__.py} (100%) rename django/core/{ => template}/defaultfilters.py (80%) rename django/core/{ => template}/defaulttags.py (81%) create mode 100644 django/core/template/loader.py create mode 100644 django/core/template/loaders/__init__.py rename django/core/{template_file.py => template/loaders/filesystem.py} (93%) diff --git a/django/core/extensions.py b/django/core/extensions.py index 6909ace33d..a7eaf3b06f 100644 --- a/django/core/extensions.py +++ b/django/core/extensions.py @@ -2,14 +2,13 @@ # of MVC. In other words, these functions/classes introduce controlled coupling # for convenience's sake. -from django.core import template_loader from django.core.exceptions import Http404, ObjectDoesNotExist -from django.core.template import Context +from django.core.template import Context, loader from django.conf.settings import DEBUG, INTERNAL_IPS from django.utils.httpwrappers import HttpResponse def render_to_response(*args, **kwargs): - return HttpResponse(template_loader.render_to_string(*args, **kwargs)) + return HttpResponse(loader.render_to_string(*args, **kwargs)) load_and_render = render_to_response # For backwards compatibility. def get_object_or_404(mod, **kwargs): diff --git a/django/core/rss.py b/django/core/rss.py index cd2bcf4ad1..7563d2ea82 100644 --- a/django/core/rss.py +++ b/django/core/rss.py @@ -1,6 +1,5 @@ -from django.core import template_loader from django.core.exceptions import ObjectDoesNotExist -from django.core.template import Context +from django.core.template import Context, loader from django.models.core import sites from django.utils import feedgenerator from django.conf.settings import LANGUAGE_CODE, SETTINGS_MODULE @@ -28,7 +27,7 @@ class FeedConfiguration: get_list_kwargs_cb -- Function that takes the param and returns a dictionary to use in addition to get_list_kwargs (if applicable). - + get_pubdate_cb -- Function that takes the object and returns a datetime to use as the publication date in the feed. @@ -49,7 +48,7 @@ class FeedConfiguration: self.enc_url = enc_url self.enc_length = enc_length self.enc_mime_type = enc_mime_type - + def get_feed(self, param_slug=None): """ Returns a utils.feedgenerator.DefaultRssFeed object, fully populated, @@ -64,8 +63,8 @@ class FeedConfiguration: param = None current_site = sites.get_current() f = self._get_feed_generator_object(param) - title_template = template_loader.get_template('rss/%s_title' % self.slug) - description_template = template_loader.get_template('rss/%s_description' % self.slug) + title_template = loader.get_template('rss/%s_title' % self.slug) + description_template = loader.get_template('rss/%s_description' % self.slug) kwargs = self.get_list_kwargs.copy() if param and self.get_list_kwargs_cb: kwargs.update(self.get_list_kwargs_cb(param)) @@ -102,7 +101,7 @@ class FeedConfiguration: pubdate = self.get_pubdate_cb and self.get_pubdate_cb(obj) or None, ) return f - + def _get_feed_generator_object(self, param): current_site = sites.get_current() link = self.link_cb(param).decode() diff --git a/django/core/template.py b/django/core/template/__init__.py similarity index 100% rename from django/core/template.py rename to django/core/template/__init__.py diff --git a/django/core/defaultfilters.py b/django/core/template/defaultfilters.py similarity index 80% rename from django/core/defaultfilters.py rename to django/core/template/defaultfilters.py index b9e0e67745..9ef367ee52 100644 --- a/django/core/defaultfilters.py +++ b/django/core/template/defaultfilters.py @@ -1,6 +1,7 @@ "Default variable filters" -import template, re +from django.core.template import register_filter, resolve_variable +import re import random as random_module ################### @@ -196,7 +197,7 @@ def dictsort(value, arg): Takes a list of dicts, returns that list sorted by the property given in the argument. """ - decorated = [(template.resolve_variable('var.' + arg, {'var' : item}), item) for item in value] + decorated = [(resolve_variable('var.' + arg, {'var' : item}), item) for item in value] decorated.sort() return [item[1] for item in decorated] @@ -205,7 +206,7 @@ def dictsortreversed(value, arg): Takes a list of dicts, returns that list sorted in reverse order by the property given in the argument. """ - decorated = [(template.resolve_variable('var.' + arg, {'var' : item}), item) for item in value] + decorated = [(resolve_variable('var.' + arg, {'var' : item}), item) for item in value] decorated.sort() decorated.reverse() return [item[1] for item in decorated] @@ -414,52 +415,51 @@ def pprint(value, _): from pprint import pformat return pformat(value) -# Syntax: template.register_filter(name of filter, callback, has_argument) -template.register_filter('add', add, True) -template.register_filter('addslashes', addslashes, False) -template.register_filter('capfirst', capfirst, False) -template.register_filter('center', center, True) -template.register_filter('cut', cut, True) -template.register_filter('date', date, True) -template.register_filter('default', default, True) -template.register_filter('default_if_none', default_if_none, True) -template.register_filter('dictsort', dictsort, True) -template.register_filter('dictsortreversed', dictsortreversed, True) -template.register_filter('divisibleby', divisibleby, True) -template.register_filter('escape', escape, False) -template.register_filter('filesizeformat', filesizeformat, False) -template.register_filter('first', first, False) -template.register_filter('fix_ampersands', fix_ampersands, False) -template.register_filter('floatformat', floatformat, False) -template.register_filter('get_digit', get_digit, True) -template.register_filter('join', join, True) -template.register_filter('length', length, False) -template.register_filter('length_is', length_is, True) -template.register_filter('linebreaks', linebreaks, False) -template.register_filter('linebreaksbr', linebreaksbr, False) -template.register_filter('linenumbers', linenumbers, False) -template.register_filter('ljust', ljust, True) -template.register_filter('lower', lower, False) -template.register_filter('make_list', make_list, False) -template.register_filter('phone2numeric', phone2numeric, False) -template.register_filter('pluralize', pluralize, False) -template.register_filter('pprint', pprint, False) -template.register_filter('removetags', removetags, True) -template.register_filter('random', random, False) -template.register_filter('rjust', rjust, True) -template.register_filter('slice', slice_, True) -template.register_filter('slugify', slugify, False) -template.register_filter('stringformat', stringformat, True) -template.register_filter('striptags', striptags, False) -template.register_filter('time', time, True) -template.register_filter('timesince', timesince, False) -template.register_filter('title', title, False) -template.register_filter('truncatewords', truncatewords, True) -template.register_filter('unordered_list', unordered_list, False) -template.register_filter('upper', upper, False) -template.register_filter('urlencode', urlencode, False) -template.register_filter('urlize', urlize, False) -template.register_filter('urlizetrunc', urlizetrunc, True) -template.register_filter('wordcount', wordcount, False) -template.register_filter('wordwrap', wordwrap, True) -template.register_filter('yesno', yesno, True) +# Syntax: register_filter(name of filter, callback, has_argument) +register_filter('add', add, True) +register_filter('addslashes', addslashes, False) +register_filter('capfirst', capfirst, False) +register_filter('center', center, True) +register_filter('cut', cut, True) +register_filter('date', date, True) +register_filter('default', default, True) +register_filter('dictsort', dictsort, True) +register_filter('dictsortreversed', dictsortreversed, True) +register_filter('divisibleby', divisibleby, True) +register_filter('escape', escape, False) +register_filter('filesizeformat', filesizeformat, False) +register_filter('first', first, False) +register_filter('fix_ampersands', fix_ampersands, False) +register_filter('floatformat', floatformat, False) +register_filter('get_digit', get_digit, True) +register_filter('join', join, True) +register_filter('length', length, False) +register_filter('length_is', length_is, True) +register_filter('linebreaks', linebreaks, False) +register_filter('linebreaksbr', linebreaksbr, False) +register_filter('linenumbers', linenumbers, False) +register_filter('ljust', ljust, True) +register_filter('lower', lower, False) +register_filter('make_list', make_list, False) +register_filter('phone2numeric', phone2numeric, False) +register_filter('pluralize', pluralize, False) +register_filter('pprint', pprint, False) +register_filter('removetags', removetags, True) +register_filter('random', random, False) +register_filter('rjust', rjust, True) +register_filter('slice', slice_, True) +register_filter('slugify', slugify, False) +register_filter('stringformat', stringformat, True) +register_filter('striptags', striptags, False) +register_filter('time', time, True) +register_filter('timesince', timesince, False) +register_filter('title', title, False) +register_filter('truncatewords', truncatewords, True) +register_filter('unordered_list', unordered_list, False) +register_filter('upper', upper, False) +register_filter('urlencode', urlencode, False) +register_filter('urlize', urlize, False) +register_filter('urlizetrunc', urlizetrunc, True) +register_filter('wordcount', wordcount, False) +register_filter('wordwrap', wordwrap, True) +register_filter('yesno', yesno, True) diff --git a/django/core/defaulttags.py b/django/core/template/defaulttags.py similarity index 81% rename from django/core/defaulttags.py rename to django/core/template/defaulttags.py index e86b385f9c..ea21ebafce 100644 --- a/django/core/defaulttags.py +++ b/django/core/template/defaulttags.py @@ -1,13 +1,14 @@ "Default tags used by the template system, available to all templates." +from django.core.template import Node, NodeList, Template, Context, resolve_variable, resolve_variable_with_filters, get_filters_from_token, registered_filters +from django.core.template import TemplateSyntaxError, VariableDoesNotExist, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END, register_tag import sys -import template -class CommentNode(template.Node): +class CommentNode(Node): def render(self, context): return '' -class CycleNode(template.Node): +class CycleNode(Node): def __init__(self, cyclevars): self.cyclevars = cyclevars self.cyclevars_len = len(cyclevars) @@ -17,7 +18,7 @@ class CycleNode(template.Node): self.counter += 1 return self.cyclevars[self.counter % self.cyclevars_len] -class DebugNode(template.Node): +class DebugNode(Node): def render(self, context): from pprint import pformat output = [pformat(val) for val in context] @@ -25,7 +26,7 @@ class DebugNode(template.Node): output.append(pformat(sys.modules)) return ''.join(output) -class FilterNode(template.Node): +class FilterNode(Node): def __init__(self, filters, nodelist): self.filters, self.nodelist = filters, nodelist @@ -33,21 +34,21 @@ class FilterNode(template.Node): output = self.nodelist.render(context) # apply filters for f in self.filters: - output = template.registered_filters[f[0]][0](output, f[1]) + output = registered_filters[f[0]][0](output, f[1]) return output -class FirstOfNode(template.Node): +class FirstOfNode(Node): def __init__(self, vars): self.vars = vars def render(self, context): for var in self.vars: - value = template.resolve_variable(var, context) + value = resolve_variable(var, context) if value: return str(value) return '' -class ForNode(template.Node): +class ForNode(Node): def __init__(self, loopvar, sequence, reversed, nodelist_loop): self.loopvar, self.sequence = loopvar, sequence self.reversed = reversed @@ -73,15 +74,15 @@ class ForNode(template.Node): return nodes def render(self, context): - nodelist = template.NodeList() + nodelist = NodeList() if context.has_key('forloop'): parentloop = context['forloop'] else: parentloop = {} context.push() try: - values = template.resolve_variable_with_filters(self.sequence, context) - except template.VariableDoesNotExist: + values = resolve_variable_with_filters(self.sequence, context) + except VariableDoesNotExist: values = [] if values is None: values = [] @@ -111,7 +112,7 @@ class ForNode(template.Node): context.pop() return nodelist.render(context) -class IfChangedNode(template.Node): +class IfChangedNode(Node): def __init__(self, nodelist): self.nodelist = nodelist self._last_seen = None @@ -129,7 +130,7 @@ class IfChangedNode(template.Node): else: return '' -class IfEqualNode(template.Node): +class IfEqualNode(Node): def __init__(self, var1, var2, nodelist_true, nodelist_false, negate): self.var1, self.var2 = var1, var2 self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false @@ -139,13 +140,13 @@ class IfEqualNode(template.Node): return "" def render(self, context): - val1 = template.resolve_variable(self.var1, context) - val2 = template.resolve_variable(self.var2, context) + val1 = resolve_variable(self.var1, context) + val2 = resolve_variable(self.var2, context) if (self.negate and val1 != val2) or (not self.negate and val1 == val2): return self.nodelist_true.render(context) return self.nodelist_false.render(context) -class IfNode(template.Node): +class IfNode(Node): def __init__(self, boolvars, nodelist_true, nodelist_false): self.boolvars = boolvars self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false @@ -170,27 +171,27 @@ class IfNode(template.Node): def render(self, context): for ifnot, boolvar in self.boolvars: try: - value = template.resolve_variable_with_filters(boolvar, context) - except template.VariableDoesNotExist: + value = resolve_variable_with_filters(boolvar, context) + except VariableDoesNotExist: value = None if (value and not ifnot) or (ifnot and not value): return self.nodelist_true.render(context) return self.nodelist_false.render(context) -class RegroupNode(template.Node): +class RegroupNode(Node): def __init__(self, target_var, expression, var_name): self.target_var, self.expression = target_var, expression self.var_name = var_name def render(self, context): - obj_list = template.resolve_variable_with_filters(self.target_var, context) + obj_list = resolve_variable_with_filters(self.target_var, context) if obj_list == '': # target_var wasn't found in context; fail silently context[self.var_name] = [] return '' output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]} for obj in obj_list: - grouper = template.resolve_variable_with_filters('var.%s' % self.expression, \ - template.Context({'var': obj})) + grouper = resolve_variable_with_filters('var.%s' % self.expression, \ + Context({'var': obj})) if output and repr(output[-1]['grouper']) == repr(grouper): output[-1]['list'].append(obj) else: @@ -205,7 +206,7 @@ def include_is_allowed(filepath): return True return False -class SsiNode(template.Node): +class SsiNode(Node): def __init__(self, filepath, parsed): self.filepath, self.parsed = filepath, parsed @@ -220,13 +221,13 @@ class SsiNode(template.Node): output = '' if self.parsed: try: - t = template.Template(output) + t = Template(output) return t.render(context) - except template.TemplateSyntaxError: + except TemplateSyntaxError: return '' # Fail silently for invalid included templates. return output -class LoadNode(template.Node): +class LoadNode(Node): def __init__(self, taglib): self.taglib = taglib @@ -244,7 +245,7 @@ class LoadNode(template.Node): pass # Fail silently for invalid loads. return '' -class NowNode(template.Node): +class NowNode(Node): def __init__(self, format_string): self.format_string = format_string @@ -254,11 +255,11 @@ class NowNode(template.Node): df = DateFormat(datetime.now()) return df.format(self.format_string) -class TemplateTagNode(template.Node): - mapping = {'openblock': template.BLOCK_TAG_START, - 'closeblock': template.BLOCK_TAG_END, - 'openvariable': template.VARIABLE_TAG_START, - 'closevariable': template.VARIABLE_TAG_END} +class TemplateTagNode(Node): + mapping = {'openblock': BLOCK_TAG_START, + 'closeblock': BLOCK_TAG_END, + 'openvariable': VARIABLE_TAG_START, + 'closevariable': VARIABLE_TAG_END} def __init__(self, tagtype): self.tagtype = tagtype @@ -266,7 +267,7 @@ class TemplateTagNode(template.Node): def render(self, context): return self.mapping.get(self.tagtype, '') -class WidthRatioNode(template.Node): +class WidthRatioNode(Node): def __init__(self, val_var, max_var, max_width): self.val_var = val_var self.max_var = max_var @@ -274,9 +275,9 @@ class WidthRatioNode(template.Node): def render(self, context): try: - value = template.resolve_variable_with_filters(self.val_var, context) - maxvalue = template.resolve_variable_with_filters(self.max_var, context) - except template.VariableDoesNotExist: + value = resolve_variable_with_filters(self.val_var, context) + maxvalue = resolve_variable_with_filters(self.max_var, context) + except VariableDoesNotExist: return '' try: value = float(value) @@ -330,7 +331,7 @@ def do_cycle(parser, token): args = token.contents.split() if len(args) < 2: - raise template.TemplateSyntaxError("'Cycle' statement requires at least two arguments") + raise TemplateSyntaxError("'Cycle' statement requires at least two arguments") elif len(args) == 2 and "," in args[1]: # {% cycle a,b,c %} @@ -341,13 +342,13 @@ def do_cycle(parser, token): elif len(args) == 2: name = args[1] if not parser._namedCycleNodes.has_key(name): - raise template.TemplateSyntaxError("Named cycle '%s' does not exist" % name) + raise TemplateSyntaxError("Named cycle '%s' does not exist" % name) return parser._namedCycleNodes[name] elif len(args) == 4: # {% cycle a,b,c as name %} if args[2] != 'as': - raise template.TemplateSyntaxError("Second 'cycle' argument must be 'as'") + raise TemplateSyntaxError("Second 'cycle' argument must be 'as'") cyclevars = [v for v in args[1].split(",") if v] # split and kill blanks name = args[3] node = CycleNode(cyclevars) @@ -359,7 +360,7 @@ def do_cycle(parser, token): return node else: - raise template.TemplateSyntaxError("Invalid arguments to 'cycle': %s" % args) + raise TemplateSyntaxError("Invalid arguments to 'cycle': %s" % args) def do_debug(parser, token): "Print a whole load of debugging information, including the context and imported modules" @@ -379,7 +380,7 @@ def do_filter(parser, token): {% endfilter %} """ _, rest = token.contents.split(None, 1) - _, filters = template.get_filters_from_token('var|%s' % rest) + _, filters = get_filters_from_token('var|%s' % rest) nodelist = parser.parse(('endfilter',)) parser.delete_first_token() return FilterNode(filters, nodelist) @@ -408,7 +409,7 @@ def do_firstof(parser, token): """ bits = token.contents.split()[1:] if len(bits) < 1: - raise template.TemplateSyntaxError, "'firstof' statement requires at least one argument" + raise TemplateSyntaxError, "'firstof' statement requires at least one argument" return FirstOfNode(bits) @@ -434,9 +435,9 @@ def do_for(parser, token): ========================== ================================================ ``forloop.counter`` The current iteration of the loop (1-indexed) ``forloop.counter0`` The current iteration of the loop (0-indexed) - ``forloop.revcounter`` The number of iterations from the end of the + ``forloop.revcounter`` The number of iterations from the end of the loop (1-indexed) - ``forloop.revcounter0`` The number of iterations from the end of the + ``forloop.revcounter0`` The number of iterations from the end of the loop (0-indexed) ``forloop.first`` True if this is the first time through the loop ``forloop.last`` True if this is the last time through the loop @@ -447,11 +448,11 @@ def do_for(parser, token): """ bits = token.contents.split() if len(bits) == 5 and bits[4] != 'reversed': - raise template.TemplateSyntaxError, "'for' statements with five words should end in 'reversed': %s" % token.contents + raise TemplateSyntaxError, "'for' statements with five words should end in 'reversed': %s" % token.contents if len(bits) not in (4, 5): - raise template.TemplateSyntaxError, "'for' statements should have either four or five words: %s" % token.contents + raise TemplateSyntaxError, "'for' statements should have either four or five words: %s" % token.contents if bits[2] != 'in': - raise template.TemplateSyntaxError, "'for' statement must contain 'in' as the second word: %s" % token.contents + raise TemplateSyntaxError, "'for' statement must contain 'in' as the second word: %s" % token.contents loopvar = bits[1] sequence = bits[3] reversed = (len(bits) == 5) @@ -477,7 +478,7 @@ def do_ifequal(parser, token, negate): """ bits = token.contents.split() if len(bits) != 3: - raise template.TemplateSyntaxError, "%r takes two arguments" % bits[0] + raise TemplateSyntaxError, "%r takes two arguments" % bits[0] end_tag = 'end' + bits[0] nodelist_true = parser.parse(('else', end_tag)) token = parser.next_token() @@ -485,7 +486,7 @@ def do_ifequal(parser, token, negate): nodelist_false = parser.parse((end_tag,)) parser.delete_first_token() else: - nodelist_false = template.NodeList() + nodelist_false = NodeList() return IfEqualNode(bits[1], bits[2], nodelist_true, nodelist_false, negate) def do_if(parser, token): @@ -538,7 +539,7 @@ def do_if(parser, token): bits = token.contents.split() del bits[0] if not bits: - raise template.TemplateSyntaxError, "'if' statement requires at least one argument" + raise TemplateSyntaxError, "'if' statement requires at least one argument" # bits now looks something like this: ['a', 'or', 'not', 'b', 'or', 'c.d'] boolpairs = ' '.join(bits).split(' or ') boolvars = [] @@ -546,7 +547,7 @@ def do_if(parser, token): if ' ' in boolpair: not_, boolvar = boolpair.split() if not_ != 'not': - raise template.TemplateSyntaxError, "Expected 'not' in if statement" + raise TemplateSyntaxError, "Expected 'not' in if statement" boolvars.append((True, boolvar)) else: boolvars.append((False, boolpair)) @@ -556,7 +557,7 @@ def do_if(parser, token): nodelist_false = parser.parse(('endif',)) parser.delete_first_token() else: - nodelist_false = template.NodeList() + nodelist_false = NodeList() return IfNode(boolvars, nodelist_true, nodelist_false) def do_ifchanged(parser, token): @@ -576,7 +577,7 @@ def do_ifchanged(parser, token): """ bits = token.contents.split() if len(bits) != 1: - raise template.TemplateSyntaxError, "'ifchanged' tag takes no arguments" + raise TemplateSyntaxError, "'ifchanged' tag takes no arguments" nodelist = parser.parse(('endifchanged',)) parser.delete_first_token() return IfChangedNode(nodelist) @@ -599,12 +600,12 @@ def do_ssi(parser, token): bits = token.contents.split() parsed = False if len(bits) not in (2, 3): - raise template.TemplateSyntaxError, "'ssi' tag takes one argument: the path to the file to be included" + raise TemplateSyntaxError, "'ssi' tag takes one argument: the path to the file to be included" if len(bits) == 3: if bits[2] == 'parsed': parsed = True else: - raise template.TemplateSyntaxError, "Second (optional) argument to %s tag must be 'parsed'" % bits[0] + raise TemplateSyntaxError, "Second (optional) argument to %s tag must be 'parsed'" % bits[0] return SsiNode(bits[1], parsed) def do_load(parser, token): @@ -617,13 +618,13 @@ def do_load(parser, token): """ bits = token.contents.split() if len(bits) != 2: - raise template.TemplateSyntaxError, "'load' statement takes one argument" + raise TemplateSyntaxError, "'load' statement takes one argument" taglib = bits[1] # check at compile time that the module can be imported try: LoadNode.load_taglib(taglib) except ImportError: - raise template.TemplateSyntaxError, "'%s' is not a valid tag library" % taglib + raise TemplateSyntaxError, "'%s' is not a valid tag library" % taglib return LoadNode(taglib) def do_now(parser, token): @@ -639,7 +640,7 @@ def do_now(parser, token): """ bits = token.contents.split('"') if len(bits) != 3: - raise template.TemplateSyntaxError, "'now' statement takes one argument" + raise TemplateSyntaxError, "'now' statement takes one argument" format_string = bits[1] return NowNode(format_string) @@ -691,13 +692,13 @@ def do_regroup(parser, token): """ firstbits = token.contents.split(None, 3) if len(firstbits) != 4: - raise template.TemplateSyntaxError, "'regroup' tag takes five arguments" + raise TemplateSyntaxError, "'regroup' tag takes five arguments" target_var = firstbits[1] if firstbits[2] != 'by': - raise template.TemplateSyntaxError, "second argument to 'regroup' tag must be 'by'" + raise TemplateSyntaxError, "second argument to 'regroup' tag must be 'by'" lastbits_reversed = firstbits[3][::-1].split(None, 2) if lastbits_reversed[1][::-1] != 'as': - raise template.TemplateSyntaxError, "next-to-last argument to 'regroup' tag must be 'as'" + raise TemplateSyntaxError, "next-to-last argument to 'regroup' tag must be 'as'" expression = lastbits_reversed[2][::-1] var_name = lastbits_reversed[0][::-1] return RegroupNode(target_var, expression, var_name) @@ -722,10 +723,10 @@ def do_templatetag(parser, token): """ bits = token.contents.split() if len(bits) != 2: - raise template.TemplateSyntaxError, "'templatetag' statement takes one argument" + raise TemplateSyntaxError, "'templatetag' statement takes one argument" tag = bits[1] if not TemplateTagNode.mapping.has_key(tag): - raise template.TemplateSyntaxError, "Invalid templatetag argument: '%s'. Must be one of: %s" % \ + raise TemplateSyntaxError, "Invalid templatetag argument: '%s'. Must be one of: %s" % \ (tag, TemplateTagNode.mapping.keys()) return TemplateTagNode(tag) @@ -744,27 +745,27 @@ def do_widthratio(parser, token): """ bits = token.contents.split() if len(bits) != 4: - raise template.TemplateSyntaxError("widthratio takes three arguments") + raise TemplateSyntaxError("widthratio takes three arguments") tag, this_value_var, max_value_var, max_width = bits try: max_width = int(max_width) except ValueError: - raise template.TemplateSyntaxError("widthratio final argument must be an integer") + raise TemplateSyntaxError("widthratio final argument must be an integer") return WidthRatioNode(this_value_var, max_value_var, max_width) -template.register_tag('comment', do_comment) -template.register_tag('cycle', do_cycle) -template.register_tag('debug', do_debug) -template.register_tag('filter', do_filter) -template.register_tag('firstof', do_firstof) -template.register_tag('for', do_for) -template.register_tag('ifequal', lambda parser, token: do_ifequal(parser, token, False)) -template.register_tag('ifnotequal', lambda parser, token: do_ifequal(parser, token, True)) -template.register_tag('if', do_if) -template.register_tag('ifchanged', do_ifchanged) -template.register_tag('regroup', do_regroup) -template.register_tag('ssi', do_ssi) -template.register_tag('load', do_load) -template.register_tag('now', do_now) -template.register_tag('templatetag', do_templatetag) -template.register_tag('widthratio', do_widthratio) +register_tag('comment', do_comment) +register_tag('cycle', do_cycle) +register_tag('debug', do_debug) +register_tag('filter', do_filter) +register_tag('firstof', do_firstof) +register_tag('for', do_for) +register_tag('ifequal', lambda parser, token: do_ifequal(parser, token, False)) +register_tag('ifnotequal', lambda parser, token: do_ifequal(parser, token, True)) +register_tag('if', do_if) +register_tag('ifchanged', do_ifchanged) +register_tag('regroup', do_regroup) +register_tag('ssi', do_ssi) +register_tag('load', do_load) +register_tag('now', do_now) +register_tag('templatetag', do_templatetag) +register_tag('widthratio', do_widthratio) diff --git a/django/core/template/loader.py b/django/core/template/loader.py new file mode 100644 index 0000000000..65a6c735b3 --- /dev/null +++ b/django/core/template/loader.py @@ -0,0 +1,163 @@ +"Wrapper for loading templates from storage of some sort (e.g. files or db)" + +from django.core.template import Template, Context, Node, TemplateDoesNotExist, TemplateSyntaxError, resolve_variable_with_filters, register_tag +from django.core.template.loaders.filesystem import load_template_source + +class ExtendsError(Exception): + pass + +def get_template(template_name): + """ + Returns a compiled Template object for the given template name, + handling template inheritance recursively. + """ + return get_template_from_string(load_template_source(template_name)) + +def get_template_from_string(source): + """ + Returns a compiled Template object for the given template code, + handling template inheritance recursively. + """ + return Template(source) + +def render_to_string(template_name, dictionary=None, context_instance=None): + """ + Loads the given template_name and renders it with the given dictionary as + context. The template_name may be a string to load a single template using + get_template, or it may be a tuple to use select_template to find one of + the templates in the list. Returns a string. + """ + dictionary = dictionary or {} + if isinstance(template_name, (list, tuple)): + t = select_template(template_name) + else: + t = get_template(template_name) + if context_instance: + context_instance.update(dictionary) + else: + context_instance = Context(dictionary) + return t.render(context_instance) + +def select_template(template_name_list): + "Given a list of template names, returns the first that can be loaded." + for template_name in template_name_list: + try: + return get_template(template_name) + except TemplateDoesNotExist: + continue + # If we get here, none of the templates could be loaded + raise TemplateDoesNotExist, ', '.join(template_name_list) + +class BlockNode(Node): + def __init__(self, name, nodelist, parent=None): + self.name, self.nodelist, self.parent = name, nodelist, parent + + def __repr__(self): + return "" % (self.name, self.nodelist) + + def render(self, context): + context.push() + # Save context in case of block.super(). + self.context = context + context['block'] = self + result = self.nodelist.render(context) + context.pop() + return result + + def super(self): + if self.parent: + return self.parent.render(self.context) + return '' + + def add_parent(self, nodelist): + if self.parent: + self.parent.add_parent(nodelist) + else: + self.parent = BlockNode(self.name, nodelist) + +class ExtendsNode(Node): + def __init__(self, nodelist, parent_name, parent_name_var, template_dirs=None): + self.nodelist = nodelist + self.parent_name, self.parent_name_var = parent_name, parent_name_var + self.template_dirs = template_dirs + + def get_parent(self, context): + if self.parent_name_var: + self.parent_name = resolve_variable_with_filters(self.parent_name_var, context) + parent = self.parent_name + if not parent: + error_msg = "Invalid template name in 'extends' tag: %r." % parent + if self.parent_name_var: + error_msg += " Got this from the %r variable." % self.parent_name_var + raise TemplateSyntaxError, error_msg + try: + return get_template_from_string(load_template_source(parent, self.template_dirs)) + except TemplateDoesNotExist: + raise TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent + + def render(self, context): + compiled_parent = self.get_parent(context) + parent_is_child = isinstance(compiled_parent.nodelist[0], ExtendsNode) + parent_blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)]) + for block_node in self.nodelist.get_nodes_by_type(BlockNode): + # Check for a BlockNode with this node's name, and replace it if found. + try: + parent_block = parent_blocks[block_node.name] + except KeyError: + # This BlockNode wasn't found in the parent template, but the + # parent block might be defined in the parent's *parent*, so we + # add this BlockNode to the parent's ExtendsNode nodelist, so + # it'll be checked when the parent node's render() is called. + if parent_is_child: + compiled_parent.nodelist[0].nodelist.append(block_node) + else: + # Keep any existing parents and add a new one. Used by BlockNode. + parent_block.parent = block_node.parent + parent_block.add_parent(parent_block.nodelist) + parent_block.nodelist = block_node.nodelist + return compiled_parent.render(context) + +def do_block(parser, token): + """ + Define a block that can be overridden by child templates. + """ + bits = token.contents.split() + if len(bits) != 2: + raise TemplateSyntaxError, "'%s' tag takes only one argument" % bits[0] + block_name = bits[1] + # Keep track of the names of BlockNodes found in this template, so we can + # check for duplication. + try: + if block_name in parser.__loaded_blocks: + raise TemplateSyntaxError, "'%s' tag with name '%s' appears more than once" % (bits[0], block_name) + parser.__loaded_blocks.append(block_name) + except AttributeError: # parser._loaded_blocks isn't a list yet + parser.__loaded_blocks = [block_name] + nodelist = parser.parse(('endblock',)) + parser.delete_first_token() + return BlockNode(block_name, nodelist) + +def do_extends(parser, token): + """ + Signal that this template extends a parent template. + + This tag may be used in two ways: ``{% extends "base" %}`` (with quotes) + uses the literal value "base" as the name of the parent template to extend, + or ``{% entends variable %}`` uses the value of ``variable`` as the name + of the parent template to extend. + """ + bits = token.contents.split() + if len(bits) != 2: + raise TemplateSyntaxError, "'%s' takes one argument" % bits[0] + parent_name, parent_name_var = None, None + if (bits[1].startswith('"') and bits[1].endswith('"')) or (bits[1].startswith("'") and bits[1].endswith("'")): + parent_name = bits[1][1:-1] + else: + parent_name_var = bits[1] + nodelist = parser.parse() + if nodelist.get_nodes_by_type(ExtendsNode): + raise TemplateSyntaxError, "'%s' cannot appear more than once in the same template" % bits[0] + return ExtendsNode(nodelist, parent_name, parent_name_var) + +register_tag('block', do_block) +register_tag('extends', do_extends) diff --git a/django/core/template/loaders/__init__.py b/django/core/template/loaders/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/core/template_file.py b/django/core/template/loaders/filesystem.py similarity index 93% rename from django/core/template_file.py rename to django/core/template/loaders/filesystem.py index 6f5a324d61..849f62b94e 100644 --- a/django/core/template_file.py +++ b/django/core/template/loaders/filesystem.py @@ -1,4 +1,4 @@ -# Wrapper for loading templates from files +# Wrapper for loading templates from the filesystem. from django.conf.settings import TEMPLATE_DIRS, TEMPLATE_FILE_EXTENSION from django.core.template import TemplateDoesNotExist diff --git a/django/core/template_loader.py b/django/core/template_loader.py index 7c26cf0faa..e268c390e1 100644 --- a/django/core/template_loader.py +++ b/django/core/template_loader.py @@ -1,162 +1,7 @@ -"Wrapper for loading templates from storage of some sort (e.g. files or db)" -import template -from template_file import load_template_source +# This module is DEPRECATED! +# +# You should no longer be using django.core.template_loader. +# +# Use django.core.template.loader instead. -class ExtendsError(Exception): - pass - -def get_template(template_name): - """ - Returns a compiled template.Template object for the given template name, - handling template inheritance recursively. - """ - return get_template_from_string(load_template_source(template_name)) - -def get_template_from_string(source): - """ - Returns a compiled template.Template object for the given template code, - handling template inheritance recursively. - """ - return template.Template(source) - -def render_to_string(template_name, dictionary=None, context_instance=None): - """ - Loads the given template_name and renders it with the given dictionary as - context. The template_name may be a string to load a single template using - get_template, or it may be a tuple to use select_template to find one of - the templates in the list. Returns a string. - """ - dictionary = dictionary or {} - if isinstance(template_name, (list, tuple)): - t = select_template(template_name) - else: - t = get_template(template_name) - if context_instance: - context_instance.update(dictionary) - else: - context_instance = template.Context(dictionary) - return t.render(context_instance) - -def select_template(template_name_list): - "Given a list of template names, returns the first that can be loaded." - for template_name in template_name_list: - try: - return get_template(template_name) - except template.TemplateDoesNotExist: - continue - # If we get here, none of the templates could be loaded - raise template.TemplateDoesNotExist, ', '.join(template_name_list) - -class BlockNode(template.Node): - def __init__(self, name, nodelist, parent=None): - self.name, self.nodelist, self.parent = name, nodelist, parent - - def __repr__(self): - return "" % (self.name, self.nodelist) - - def render(self, context): - context.push() - # Save context in case of block.super(). - self.context = context - context['block'] = self - result = self.nodelist.render(context) - context.pop() - return result - - def super(self): - if self.parent: - return self.parent.render(self.context) - return '' - - def add_parent(self, nodelist): - if self.parent: - self.parent.add_parent(nodelist) - else: - self.parent = BlockNode(self.name, nodelist) - -class ExtendsNode(template.Node): - def __init__(self, nodelist, parent_name, parent_name_var, template_dirs=None): - self.nodelist = nodelist - self.parent_name, self.parent_name_var = parent_name, parent_name_var - self.template_dirs = template_dirs - - def get_parent(self, context): - if self.parent_name_var: - self.parent_name = template.resolve_variable_with_filters(self.parent_name_var, context) - parent = self.parent_name - if not parent: - error_msg = "Invalid template name in 'extends' tag: %r." % parent - if self.parent_name_var: - error_msg += " Got this from the %r variable." % self.parent_name_var - raise template.TemplateSyntaxError, error_msg - try: - return get_template_from_string(load_template_source(parent, self.template_dirs)) - except template.TemplateDoesNotExist: - raise template.TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent - - def render(self, context): - compiled_parent = self.get_parent(context) - parent_is_child = isinstance(compiled_parent.nodelist[0], ExtendsNode) - parent_blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)]) - for block_node in self.nodelist.get_nodes_by_type(BlockNode): - # Check for a BlockNode with this node's name, and replace it if found. - try: - parent_block = parent_blocks[block_node.name] - except KeyError: - # This BlockNode wasn't found in the parent template, but the - # parent block might be defined in the parent's *parent*, so we - # add this BlockNode to the parent's ExtendsNode nodelist, so - # it'll be checked when the parent node's render() is called. - if parent_is_child: - compiled_parent.nodelist[0].nodelist.append(block_node) - else: - # Keep any existing parents and add a new one. Used by BlockNode. - parent_block.parent = block_node.parent - parent_block.add_parent(parent_block.nodelist) - parent_block.nodelist = block_node.nodelist - return compiled_parent.render(context) - -def do_block(parser, token): - """ - Define a block that can be overridden by child templates. - """ - bits = token.contents.split() - if len(bits) != 2: - raise template.TemplateSyntaxError, "'%s' tag takes only one argument" % bits[0] - block_name = bits[1] - # Keep track of the names of BlockNodes found in this template, so we can - # check for duplication. - try: - if block_name in parser.__loaded_blocks: - raise template.TemplateSyntaxError, "'%s' tag with name '%s' appears more than once" % (bits[0], block_name) - parser.__loaded_blocks.append(block_name) - except AttributeError: # parser._loaded_blocks isn't a list yet - parser.__loaded_blocks = [block_name] - nodelist = parser.parse(('endblock',)) - parser.delete_first_token() - return BlockNode(block_name, nodelist) - -def do_extends(parser, token): - """ - Signal that this template extends a parent template. - - This tag may be used in two ways: ``{% extends "base" %}`` (with quotes) - uses the literal value "base" as the name of the parent template to extend, - or ``{% entends variable %}`` uses the value of ``variable`` as the name - of the parent template to extend. - """ - bits = token.contents.split() - if len(bits) != 2: - raise template.TemplateSyntaxError, "'%s' takes one argument" % bits[0] - parent_name, parent_name_var = None, None - if (bits[1].startswith('"') and bits[1].endswith('"')) or (bits[1].startswith("'") and bits[1].endswith("'")): - parent_name = bits[1][1:-1] - else: - parent_name_var = bits[1] - nodelist = parser.parse() - if nodelist.get_nodes_by_type(ExtendsNode): - raise template.TemplateSyntaxError, "'%s' cannot appear more than once in the same template" % bits[0] - return ExtendsNode(nodelist, parent_name, parent_name_var) - -template.register_tag('block', do_block) -template.register_tag('extends', do_extends) +from django.core.template.loader import * diff --git a/django/middleware/admin.py b/django/middleware/admin.py index ff21689646..9c20cfc31c 100644 --- a/django/middleware/admin.py +++ b/django/middleware/admin.py @@ -1,6 +1,6 @@ from django.utils import httpwrappers -from django.core import template_loader -from django.core.extensions import DjangoContext as Context +from django.core.extensions import DjangoContext +from django.core.extensions import render_to_response from django.models.auth import users from django.views.registration import passwords from django.views.auth.login import logout @@ -96,14 +96,12 @@ class AdminUserRequired: post_data = encode_post_data(request.POST) else: post_data = encode_post_data({}) - t = template_loader.get_template(self.get_login_template_name()) - c = Context(request, { + return render_to_response(self.get_login_template_name(), { 'title': 'Log in', 'app_path': request.path, 'post_data': post_data, 'error_message': error_message - }) - return httpwrappers.HttpResponse(t.render(c)) + }, context_instance=DjangoContext(request)) def authenticate_user(self, user, password): return user.check_password(password) and user.is_staff diff --git a/django/views/admin/doc.py b/django/views/admin/doc.py index 38d7fdc572..64785385c4 100644 --- a/django/views/admin/doc.py +++ b/django/views/admin/doc.py @@ -4,7 +4,8 @@ from django.conf import settings from django.models.core import sites from django.core.extensions import DjangoContext, render_to_response from django.core.exceptions import Http404, ViewDoesNotExist -from django.core import template, template_loader, defaulttags, defaultfilters, urlresolvers +from django.core import template, template_loader, urlresolvers +from django.core.template import defaulttags, defaultfilters try: from django.parts.admin import doc except ImportError: diff --git a/django/views/admin/template.py b/django/views/admin/template.py index 2bf4f1a1d8..c35770178e 100644 --- a/django/views/admin/template.py +++ b/django/views/admin/template.py @@ -1,5 +1,6 @@ -from django.core import formfields, template_loader, validators +from django.core import formfields, validators from django.core import template +from django.core.template import loader from django.core.extensions import DjangoContext, render_to_response from django.models.core import sites from django.conf import settings @@ -49,7 +50,7 @@ class TemplateValidator(formfields.Manipulator): # so that inheritance works in the site's context, register a new function # for "extends" that uses the site's TEMPLATE_DIR instead def new_do_extends(parser, token): - node = template_loader.do_extends(parser, token) + node = loader.do_extends(parser, token) node.template_dirs = settings_module.TEMPLATE_DIRS return node template.register_tag('extends', new_do_extends) @@ -58,10 +59,10 @@ class TemplateValidator(formfields.Manipulator): # making sure to reset the extends function in any case error = None try: - tmpl = template_loader.get_template_from_string(field_data) + tmpl = loader.get_template_from_string(field_data) tmpl.render(template.Context({})) except template.TemplateSyntaxError, e: error = e - template.register_tag('extends', template_loader.do_extends) + template.register_tag('extends', loader.do_extends) if error: raise validators.ValidationError, e.args diff --git a/django/views/auth/login.py b/django/views/auth/login.py index 75a80c7907..6950ad7efe 100644 --- a/django/views/auth/login.py +++ b/django/views/auth/login.py @@ -1,5 +1,5 @@ from django.parts.auth.formfields import AuthenticationForm -from django.core import formfields, template_loader +from django.core import formfields from django.core.extensions import DjangoContext, render_to_response from django.models.auth import users from django.models.core import sites diff --git a/django/views/defaults.py b/django/views/defaults.py index c75bc6880e..d283e54c1b 100644 --- a/django/views/defaults.py +++ b/django/views/defaults.py @@ -1,6 +1,5 @@ -from django.core import template_loader from django.core.exceptions import Http404, ObjectDoesNotExist -from django.core.template import Context +from django.core.template import Context, loader from django.models.core import sites from django.utils import httpwrappers @@ -56,7 +55,7 @@ def page_not_found(request): if r == '': return httpwrappers.HttpResponseGone() return httpwrappers.HttpResponseRedirect(r.new_path) - t = template_loader.get_template('404') + t = loader.get_template('404') c = Context() return httpwrappers.HttpResponseNotFound(t.render(c)) @@ -67,6 +66,6 @@ def server_error(request): Templates: `500` Context: None """ - t = template_loader.get_template('500') + t = loader.get_template('500') c = Context() return httpwrappers.HttpResponseServerError(t.render(c)) diff --git a/django/views/registration/passwords.py b/django/views/registration/passwords.py index 662490eec7..09d3037560 100644 --- a/django/views/registration/passwords.py +++ b/django/views/registration/passwords.py @@ -1,5 +1,6 @@ -from django.core import formfields, template_loader, validators +from django.core import formfields, validators from django.core.extensions import DjangoContext, render_to_response +from django.core.template import loader from django.models.auth import users from django.views.decorators.auth import login_required from django.utils.httpwrappers import HttpResponseRedirect @@ -32,7 +33,7 @@ class PasswordResetForm(formfields.Manipulator): domain = current_site.domain else: site_name = domain = domain_override - t = template_loader.get_template('registration/password_reset_email') + t = loader.get_template('registration/password_reset_email') c = { 'new_password': new_pass, 'email': self.user_cache.email, diff --git a/docs/forms.txt b/docs/forms.txt index c93db95bcd..969ebd428d 100644 --- a/docs/forms.txt +++ b/docs/forms.txt @@ -65,9 +65,8 @@ Using the ``AddManipulator`` We'll start with the ``AddManipulator``. Here's a very simple view that takes POSTed data from the browser and creates a new ``Place`` object:: - from django.core import template_loader from django.core.exceptions import Http404 - from django.core.extensions import DjangoContext as Context + from django.core.extensions import render_to_response from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect from django.models.places import places from django.core import formfields @@ -112,13 +111,7 @@ view with a form that submits to this flawed creation view:: # Create a FormWrapper object that the template can use. Ignore # the last two arguments to FormWrapper for now. form = formfields.FormWrapper(places.AddManipulator(), {}, {}) - - # Create a template, context and response. - t = template_loader.get_template('places/naive_create_form') - c = Context(request, { - 'form': form - }) - return HttpResponse(t.render(c)) + return render_to_response('places/naive_create_form', {'form': form}) (This view, as well as all the following ones, has the same imports as in the first example above.) @@ -169,11 +162,7 @@ creation view that takes validation into account:: # Check for validation errors errors = manipulator.get_validation_errors(new_data) if errors: - t = template_loader.get_template('places/errors') - c = Context(request, { - 'errors': errors - } - return HttpResponse(t.render(c)) + return render_to_response('places/errors', {'errors': errors}) else: manipulator.do_html2python(request.POST) new_place = manipulator.save(request.POST) @@ -245,11 +234,7 @@ Below is the finished view:: # Create the FormWrapper, template, context, response. form = formfields.FormWrapper(manipulator, new_data, errors) - t = template_loader.get_template("places/create_form") - c = Context(request, { - 'form': form, - }) - return HttpResponse(t.render(c)) + return render_to_response('places/create_form', {'form': form}) and here's the ``create_form`` template:: @@ -338,12 +323,7 @@ about editing an existing one? It's shockingly similar to creating a new one:: new_data = place.__dict__ form = formfields.FormWrapper(manipulator, new_data, errors) - t = template_loader.get_template("places/edit_form") - c = Context(request, { - 'form': form, - 'place': place, - }) - return HttpResponse(t.render(c)) + return render_to_response('places/edit_form', {'form': form, 'place': place}) The only real differences are: @@ -422,11 +402,7 @@ Here's a simple function that might drive the above form:: else: errors = new_data = {} form = formfields.FormWrapper(manipulator, new_data, errors) - t = template_loader.get_template("contact_form") - c = Context(request, { - 'form': form, - }) - return HttpResponse(t.render(c)) + return render_to_response('contact_form', {'form': form}) Validators ========== diff --git a/docs/sessions.txt b/docs/sessions.txt index 22d06fdedd..b18ca25a2c 100644 --- a/docs/sessions.txt +++ b/docs/sessions.txt @@ -136,9 +136,7 @@ Here's a typical usage example:: else: return HttpResponse("Please enable cookies and try again.") request.session.set_test_cookie() - t = template_loader.get_template("foo/login_form") - c = Context(request) - return HttpResponse(t.render(c)) + return render_to_response('foo/login_form') Using sessions out of views =========================== diff --git a/docs/templates_python.txt b/docs/templates_python.txt index 39b768429b..bd8aea62c7 100644 --- a/docs/templates_python.txt +++ b/docs/templates_python.txt @@ -307,12 +307,12 @@ The Python API Django has two ways to load templates from files: -``django.core.template_loader.get_template(template_name)`` +``django.core.template.loader.get_template(template_name)`` ``get_template`` returns the compiled template (a ``Template`` object) for the template with the given name. If the template doesn't exist, it raises ``django.core.template.TemplateDoesNotExist``. -``django.core.template_loader.select_template(template_name_list)`` +``django.core.template.loader.select_template(template_name_list)`` ``select_template`` is just like ``get_template``, except it takes a list of template names. Of the list, it returns the first template that exists. @@ -398,8 +398,8 @@ Python code, depending on whether you're writing filters or tags. .. admonition:: Behind the scenes For a ton of examples, read the source code for Django's default filters - and tags. They're in ``django/core/defaultfilters.py`` and - ``django/core/defaulttags.py``, respectively. + and tags. They're in ``django/core/template/defaultfilters.py`` and + ``django/core/template/defaulttags.py``, respectively. Writing custom template filters ------------------------------- @@ -710,4 +710,4 @@ The only new concept here is the ``self.nodelist.render(context)`` in For more examples of complex rendering, see the source code for ``{% if %}``, ``{% for %}``, ``{% ifequal %}`` and ``{% ifchanged %}``. They live in -``django/core/defaulttags.py``. +``django/core/template/defaulttags.py``. diff --git a/docs/tutorial03.txt b/docs/tutorial03.txt index 84fb64abfe..f602bf2ebd 100644 --- a/docs/tutorial03.txt +++ b/docs/tutorial03.txt @@ -191,14 +191,13 @@ There's a problem here, though: The page's design is hard-coded in the view. If you want to change the way the page looks, you'll have to edit this Python code. So let's use Django's template system to separate the design from Python:: - from django.core import template_loader - from django.core.template import Context + from django.core.template import Context, loader from django.models.polls import polls from django.utils.httpwrappers import HttpResponse def index(request): latest_poll_list = polls.get_list(order_by=['-pub_date'], limit=5) - t = template_loader.get_template('polls/index') + t = loader.get_template('polls/index') c = Context({ 'latest_poll_list': latest_poll_list, }) @@ -224,7 +223,7 @@ and feel" section of Tutorial 2. When you've done that, create a directory ``polls`` in your template directory. Within that, create a file called ``index.html``. Django requires that templates have ".html" extension. Note that our -``template_loader.get_template('polls/index')`` code from above maps to +``loader.get_template('polls/index')`` code from above maps to "[template_directory]/polls/index.html" on the filesystem. Put the following code in that template:: @@ -256,7 +255,7 @@ provides a shortcut. Here's the full ``index()`` view, rewritten:: latest_poll_list = polls.get_list(order_by=['-pub_date'], limit=5) return render_to_response('polls/index', {'latest_poll_list': latest_poll_list}) -Note that we no longer need to import ``template_loader``, ``Context`` or +Note that we no longer need to import ``loader``, ``Context`` or ``HttpResponse``. The ``render_to_response()`` function takes a template name as its first diff --git a/docs/tutorial04.txt b/docs/tutorial04.txt index 3e6d6205bb..737d8deb1f 100644 --- a/docs/tutorial04.txt +++ b/docs/tutorial04.txt @@ -197,8 +197,8 @@ objects" and "display a detail page for a particular type of object." By default, the ``object_detail`` generic view uses a template called ``/_detail``. In our case, it'll use the template ``"polls/polls_detail"``. Thus, rename your ``polls/detail.html`` template to -``polls/polls_detail.html``, and change the ``template_loader.get_template()`` -line in ``vote()``. +``polls/polls_detail.html``, and change the ``render_to_response()`` line in +``vote()``. Similarly, the ``object_list`` generic view uses a template called ``/_list``. Thus, rename ``polls/index.html`` to diff --git a/tests/othertests/templates.py b/tests/othertests/templates.py index fb96cfeadd..f2da069ea2 100644 --- a/tests/othertests/templates.py +++ b/tests/othertests/templates.py @@ -1,4 +1,5 @@ -from django.core import template, template_loader +from django.core import template +from django.core.template import loader # Helper objects for template tests class SomeClass: @@ -216,7 +217,7 @@ TEMPLATE_TESTS = { 'exception04': ("{% extends 'inheritance17' %}{% block first %}{% echo 400 %}5678{% endblock %}", {}, template.TemplateSyntaxError), } -# This replaces the standard template_loader. +# This replaces the standard template loader. def test_template_loader(template_name, template_dirs=None): try: return TEMPLATE_TESTS[template_name][0] @@ -224,13 +225,13 @@ def test_template_loader(template_name, template_dirs=None): raise template.TemplateDoesNotExist, template_name def run_tests(verbosity=0, standalone=False): - template_loader.load_template_source, old_template_loader = test_template_loader, template_loader.load_template_source + loader.load_template_source, old_template_loader = test_template_loader, loader.load_template_source failed_tests = [] tests = TEMPLATE_TESTS.items() tests.sort() for name, vals in tests: try: - output = template_loader.get_template(name).render(template.Context(vals[1])) + output = loader.get_template(name).render(template.Context(vals[1])) except Exception, e: if e.__class__ == vals[2]: if verbosity: @@ -247,7 +248,7 @@ def run_tests(verbosity=0, standalone=False): if verbosity: print "Template test: %s -- FAILED. Expected %r, got %r" % (name, vals[2], output) failed_tests.append(name) - template_loader.load_template_source = old_template_loader + loader.load_template_source = old_template_loader if failed_tests and not standalone: msg = "Template tests %s failed." % failed_tests From 083b4f9001f3968138c4d0c34ab741cf7be97f43 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 20:12:16 +0000 Subject: [PATCH 067/117] Redid [865], which got lost in [867] git-svn-id: http://code.djangoproject.com/svn/django/trunk@868 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/template/defaultfilters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/django/core/template/defaultfilters.py b/django/core/template/defaultfilters.py index 9ef367ee52..746fa9b873 100644 --- a/django/core/template/defaultfilters.py +++ b/django/core/template/defaultfilters.py @@ -423,6 +423,7 @@ register_filter('center', center, True) register_filter('cut', cut, True) register_filter('date', date, True) register_filter('default', default, True) +register_filter('default_if_none', default_if_none, True) register_filter('dictsort', dictsort, True) register_filter('dictsortreversed', dictsortreversed, True) register_filter('divisibleby', divisibleby, True) From 7aefff78335c45edb8dd66aeebadad934fd062ca Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 22:22:12 +0000 Subject: [PATCH 068/117] Fixed #582 -- Added support for loading templates from Python eggs, and a TEMPLATE_LOADERS setting, which defines which loaders to use. Thanks, Sune git-svn-id: http://code.djangoproject.com/svn/django/trunk@870 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/global_settings.py | 8 ++++ django/core/template/loader.py | 47 +++++++++++++++++++++- django/core/template/loaders/eggs.py | 25 ++++++++++++ django/core/template/loaders/filesystem.py | 1 + 4 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 django/core/template/loaders/eggs.py diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index ea2fc440de..dde5612038 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -61,6 +61,14 @@ TEMPLATE_DIRS = () # Extension on all templates. TEMPLATE_FILE_EXTENSION = '.html' +# List of callables that know how to import templates from various sources. +# See the comments in django/core/template/loader.py for interface +# documentation. +TEMPLATE_LOADERS = ( + 'django.core.template.loaders.filesystem.load_template_source', +# 'django.core.template.loaders.eggs.load_template_source', +) + # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. # Examples: "http://foo.com/media/", "/media/". diff --git a/django/core/template/loader.py b/django/core/template/loader.py index 65a6c735b3..067b3fcfb8 100644 --- a/django/core/template/loader.py +++ b/django/core/template/loader.py @@ -1,7 +1,50 @@ -"Wrapper for loading templates from storage of some sort (e.g. files or db)" +# Wrapper for loading templates from storage of some sort (e.g. filesystem, database). +# +# This uses the TEMPLATE_LOADERS setting, which is a list of loaders to use. +# Each loader is expected to have this interface: +# +# callable(name, dirs=[]) +# +# name is the template name. +# dirs is an optional list of directories to search instead of TEMPLATE_DIRS. +# +# Each loader should have an "is_usable" attribute set. This is a boolean that +# specifies whether the loader can be used in this Python installation. Each +# loader is responsible for setting this when it's initialized. +# +# For example, the eggs loader (which is capable of loading templates from +# Python eggs) sets is_usable to False if the "pkg_resources" module isn't +# installed, because pkg_resources is necessary to read eggs. +from django.core.exceptions import ImproperlyConfigured from django.core.template import Template, Context, Node, TemplateDoesNotExist, TemplateSyntaxError, resolve_variable_with_filters, register_tag -from django.core.template.loaders.filesystem import load_template_source +from django.conf.settings import TEMPLATE_LOADERS + +template_source_loaders = [] +for path in TEMPLATE_LOADERS: + i = path.rfind('.') + module, attr = path[:i], path[i+1:] + try: + mod = __import__(module, globals(), locals(), [attr]) + except ImportError, e: + raise ImproperlyConfigured, 'Error importing template source loader %s: "%s"' % (module, e) + try: + func = getattr(mod, attr) + except AttributeError: + raise ImproperlyConfigured, 'Module "%s" does not define a "%s" callable template source loader' % (module, attr) + if not func.is_usable: + import warnings + warnings.warn("Your TEMPLATE_LOADERS setting includes %r, but your Python installation doesn't support that type of template loading. Consider removing that line from TEMPLATE_LOADERS." % path) + else: + template_source_loaders.append(func) + +def load_template_source(name, dirs=None): + for loader in template_source_loaders: + try: + return loader(name, dirs) + except template.TemplateDoesNotExist: + pass + raise template.TemplateDoesNotExist, name class ExtendsError(Exception): pass diff --git a/django/core/template/loaders/eggs.py b/django/core/template/loaders/eggs.py new file mode 100644 index 0000000000..b1e221ee1c --- /dev/null +++ b/django/core/template/loaders/eggs.py @@ -0,0 +1,25 @@ +# Wrapper for loading templates from eggs via pkg_resources.resource_string. + +try: + from pkg_resources import resource_string +except ImportError: + resource_string = None + +from django.core.template import TemplateDoesNotExist +from django.conf.settings import INSTALLED_APPS, TEMPLATE_FILE_EXTENSION + +def load_template_source(name, dirs=None): + """ + Loads templates from Python eggs via pkg_resource.resource_string. + + For every installed app, it tries to get the resource (app, name). + """ + if resource_string is not None: + pkg_name = 'templates/' + name + TEMPLATE_FILE_EXTENSION + for app in INSTALLED_APPS: + try: + return resource_string(app, pkg_name) + except: + pass + raise TemplateDoesNotExist, name +load_template_source.is_usable = resource_string is not None diff --git a/django/core/template/loaders/filesystem.py b/django/core/template/loaders/filesystem.py index 849f62b94e..1d42c6d841 100644 --- a/django/core/template/loaders/filesystem.py +++ b/django/core/template/loaders/filesystem.py @@ -19,3 +19,4 @@ def load_template_source(template_name, template_dirs=None): else: error_msg = "Your TEMPLATE_DIRS settings is empty. Change it to point to at least one template directory." raise TemplateDoesNotExist, error_msg +load_template_source.is_usable = True From 6ee014725e18bd28af019fc5d8aca0a4e9a47935 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 22:28:38 +0000 Subject: [PATCH 069/117] Changed template unit test runner to use new template-loader framework from [870] git-svn-id: http://code.djangoproject.com/svn/django/trunk@871 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- tests/othertests/templates.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/othertests/templates.py b/tests/othertests/templates.py index f2da069ea2..d451612713 100644 --- a/tests/othertests/templates.py +++ b/tests/othertests/templates.py @@ -217,15 +217,18 @@ TEMPLATE_TESTS = { 'exception04': ("{% extends 'inheritance17' %}{% block first %}{% echo 400 %}5678{% endblock %}", {}, template.TemplateSyntaxError), } -# This replaces the standard template loader. def test_template_loader(template_name, template_dirs=None): + "A custom template loader that loads the unit-test templates." try: return TEMPLATE_TESTS[template_name][0] except KeyError: raise template.TemplateDoesNotExist, template_name def run_tests(verbosity=0, standalone=False): - loader.load_template_source, old_template_loader = test_template_loader, loader.load_template_source + # Register our custom template loader. + old_template_loaders = loader.template_source_loaders + loader.template_source_loaders = [test_template_loader] + failed_tests = [] tests = TEMPLATE_TESTS.items() tests.sort() @@ -248,7 +251,7 @@ def run_tests(verbosity=0, standalone=False): if verbosity: print "Template test: %s -- FAILED. Expected %r, got %r" % (name, vals[2], output) failed_tests.append(name) - loader.load_template_source = old_template_loader + loader.template_source_loaders = old_template_loaders if failed_tests and not standalone: msg = "Template tests %s failed." % failed_tests From f575a4edaadcf3941d44ed502d8a13337e922e9a Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 22:29:13 +0000 Subject: [PATCH 070/117] Fixed small namespace bug in [867] git-svn-id: http://code.djangoproject.com/svn/django/trunk@872 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/template/loader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django/core/template/loader.py b/django/core/template/loader.py index 067b3fcfb8..20df897d71 100644 --- a/django/core/template/loader.py +++ b/django/core/template/loader.py @@ -42,9 +42,9 @@ def load_template_source(name, dirs=None): for loader in template_source_loaders: try: return loader(name, dirs) - except template.TemplateDoesNotExist: + except TemplateDoesNotExist: pass - raise template.TemplateDoesNotExist, name + raise TemplateDoesNotExist, name class ExtendsError(Exception): pass From 67e6252a1ecd6587693b2756bf4bf27c0b89c3e6 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 22:38:59 +0000 Subject: [PATCH 071/117] Upgraded ez_setup.py from 0.5a12 to 0.6a5 git-svn-id: http://code.djangoproject.com/svn/django/trunk@873 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- ez_setup.py | 377 +++++++++++++++++++++++++++++----------------------- 1 file changed, 213 insertions(+), 164 deletions(-) diff --git a/ez_setup.py b/ez_setup.py index 08ee09ce93..248f4078a4 100644 --- a/ez_setup.py +++ b/ez_setup.py @@ -1,164 +1,213 @@ -#!python -"""Bootstrap setuptools installation - -If you want to use setuptools in your package's setup.py, just include this -file in the same directory with it, and add this to the top of your setup.py:: - - from ez_setup import use_setuptools - use_setuptools() - -If you want to require a specific version of setuptools, set a download -mirror, or use an alternate download directory, you can do so by supplying -the appropriate options to ``use_setuptools()``. - -This file can also be run as a script to install or upgrade setuptools. -""" - -DEFAULT_VERSION = "0.5a12" -DEFAULT_URL = "http://www.python.org/packages/source/s/setuptools/" - -import sys, os - - - - - - - - - - - - - - - - - - - - - -def use_setuptools( - version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir -): - """Automatically find/download setuptools and make it available on sys.path - - `version` should be a valid setuptools version number that is available - as an egg for download under the `download_base` URL (which should end with - a '/'). `to_dir` is the directory where setuptools will be downloaded, if - it is not already available. - - If an older version of setuptools is installed, this will print a message - to ``sys.stderr`` and raise SystemExit in an attempt to abort the calling - script. - """ - try: - import setuptools - if setuptools.__version__ == '0.0.1': - print >>sys.stderr, ( - "You have an obsolete version of setuptools installed. Please\n" - "remove it from your system entirely before rerunning this script." - ) - sys.exit(2) - - except ImportError: - egg = download_setuptools(version, download_base, to_dir) - sys.path.insert(0, egg) - import setuptools; setuptools.bootstrap_install_from = egg - - import pkg_resources - try: - pkg_resources.require("setuptools>="+version) - - except pkg_resources.VersionConflict: - # XXX could we install in a subprocess here? - print >>sys.stderr, ( - "The required version of setuptools (>=%s) is not available, and\n" - "can't be installed while this script is running. Please install\n" - " a more recent version first." - ) % version - sys.exit(2) - -def download_setuptools( - version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir -): - """Download setuptools from a specified location and return its filename - - `version` should be a valid setuptools version number that is available - as an egg for download under the `download_base` URL (which should end - with a '/'). `to_dir` is the directory where the egg will be downloaded. - """ - import urllib2, shutil - egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) - url = download_base + egg_name + '.zip' # XXX - saveto = os.path.join(to_dir, egg_name) - src = dst = None - - if not os.path.exists(saveto): # Avoid repeated downloads - try: - from distutils import log - log.warn("Downloading %s", url) - src = urllib2.urlopen(url) - # Read/write all in one block, so we don't create a corrupt file - # if the download is interrupted. - data = src.read() - dst = open(saveto,"wb") - dst.write(data) - finally: - if src: src.close() - if dst: dst.close() - - return os.path.realpath(saveto) - - - - - - - - - - - -def main(argv, version=DEFAULT_VERSION): - """Install or upgrade setuptools and EasyInstall""" - - try: - import setuptools - except ImportError: - import tempfile, shutil - tmpdir = tempfile.mkdtemp(prefix="easy_install-") - try: - egg = download_setuptools(version, to_dir=tmpdir) - sys.path.insert(0,egg) - from setuptools.command.easy_install import main - main(list(argv)+[egg]) - finally: - shutil.rmtree(tmpdir) - else: - if setuptools.__version__ == '0.0.1': - # tell the user to uninstall obsolete version - use_setuptools(version) - - req = "setuptools>="+version - import pkg_resources - try: - pkg_resources.require(req) - except pkg_resources.VersionConflict: - try: - from setuptools.command.easy_install import main - except ImportError: - from easy_install import main - main(list(argv)+[download_setuptools()]) - sys.exit(0) # try to force an exit - else: - if argv: - from setuptools.command.easy_install import main - main(argv) - else: - print "Setuptools version",version,"or greater has been installed." - print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' -if __name__=='__main__': - main(sys.argv[1:]) - +#!python +"""Bootstrap setuptools installation + +If you want to use setuptools in your package's setup.py, just include this +file in the same directory with it, and add this to the top of your setup.py:: + + from ez_setup import use_setuptools + use_setuptools() + +If you want to require a specific version of setuptools, set a download +mirror, or use an alternate download directory, you can do so by supplying +the appropriate options to ``use_setuptools()``. + +This file can also be run as a script to install or upgrade setuptools. +""" +import sys +DEFAULT_VERSION = "0.6a5" +DEFAULT_URL = "http://cheeseshop.python.org/packages/%s/s/setuptools/" % sys.version[:3] + +md5_data = { + 'setuptools-0.5a13-py2.3.egg': '85edcf0ef39bab66e130d3f38f578c86', + 'setuptools-0.5a13-py2.4.egg': 'ede4be600e3890e06d4ee5e0148e092a', + 'setuptools-0.6a1-py2.3.egg': 'ee819a13b924d9696b0d6ca6d1c5833d', + 'setuptools-0.6a1-py2.4.egg': '8256b5f1cd9e348ea6877b5ddd56257d', + 'setuptools-0.6a2-py2.3.egg': 'b98da449da411267c37a738f0ab625ba', + 'setuptools-0.6a2-py2.4.egg': 'be5b88bc30aed63fdefd2683be135c3b', + 'setuptools-0.6a3-py2.3.egg': 'ee0e325de78f23aab79d33106dc2a8c8', + 'setuptools-0.6a3-py2.4.egg': 'd95453d525a456d6c23e7a5eea89a063', + 'setuptools-0.6a4-py2.3.egg': 'e958cbed4623bbf47dd1f268b99d7784', + 'setuptools-0.6a4-py2.4.egg': '7f33c3ac2ef1296f0ab4fac1de4767d8', + 'setuptools-0.6a5-py2.3.egg': '748408389c49bcd2d84f6ae0b01695b1', + 'setuptools-0.6a5-py2.4.egg': '999bacde623f4284bfb3ea77941d2627', +} + +import sys, os + +def _validate_md5(egg_name, data): + if egg_name in md5_data: + from md5 import md5 + digest = md5(data).hexdigest() + if digest != md5_data[egg_name]: + print >>sys.stderr, ( + "md5 validation of %s failed! (Possible download problem?)" + % egg_name + ) + sys.exit(2) + return data + + +def use_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + download_delay=15 +): + """Automatically find/download setuptools and make it available on sys.path + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end with + a '/'). `to_dir` is the directory where setuptools will be downloaded, if + it is not already available. If `download_delay` is specified, it should + be the number of seconds that will be paused before initiating a download, + should one be required. If an older version of setuptools is installed, + this routine will print a message to ``sys.stderr`` and raise SystemExit in + an attempt to abort the calling script. + """ + try: + import setuptools + if setuptools.__version__ == '0.0.1': + print >>sys.stderr, ( + "You have an obsolete version of setuptools installed. Please\n" + "remove it from your system entirely before rerunning this script." + ) + sys.exit(2) + except ImportError: + egg = download_setuptools(version, download_base, to_dir, download_delay) + sys.path.insert(0, egg) + import setuptools; setuptools.bootstrap_install_from = egg + + import pkg_resources + try: + pkg_resources.require("setuptools>="+version) + + except pkg_resources.VersionConflict: + # XXX could we install in a subprocess here? + print >>sys.stderr, ( + "The required version of setuptools (>=%s) is not available, and\n" + "can't be installed while this script is running. Please install\n" + " a more recent version first." + ) % version + sys.exit(2) + +def download_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + delay = 15 +): + """Download setuptools from a specified location and return its filename + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download attempt. + """ + import urllib2, shutil + egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) + url = download_base + egg_name + saveto = os.path.join(to_dir, egg_name) + src = dst = None + if not os.path.exists(saveto): # Avoid repeated downloads + try: + from distutils import log + if delay: + log.warn(""" +--------------------------------------------------------------------------- +This script requires setuptools version %s to run (even to display +help). I will attempt to download it for you (from +%s), but +you may need to enable firewall access for this script first. +I will start the download in %d seconds. +---------------------------------------------------------------------------""", + version, download_base, delay + ); from time import sleep; sleep(delay) + log.warn("Downloading %s", url) + src = urllib2.urlopen(url) + # Read/write all in one block, so we don't create a corrupt file + # if the download is interrupted. + data = _validate_md5(egg_name, src.read()) + dst = open(saveto,"wb"); dst.write(data) + finally: + if src: src.close() + if dst: dst.close() + return os.path.realpath(saveto) + +def main(argv, version=DEFAULT_VERSION): + """Install or upgrade setuptools and EasyInstall""" + + try: + import setuptools + except ImportError: + import tempfile, shutil + tmpdir = tempfile.mkdtemp(prefix="easy_install-") + try: + egg = download_setuptools(version, to_dir=tmpdir, delay=0) + sys.path.insert(0,egg) + from setuptools.command.easy_install import main + main(list(argv)+[egg]) + finally: + shutil.rmtree(tmpdir) + else: + if setuptools.__version__ == '0.0.1': + # tell the user to uninstall obsolete version + use_setuptools(version) + + req = "setuptools>="+version + import pkg_resources + try: + pkg_resources.require(req) + except pkg_resources.VersionConflict: + try: + from setuptools.command.easy_install import main + except ImportError: + from easy_install import main + main(list(argv)+[download_setuptools(delay=0)]) + sys.exit(0) # try to force an exit + else: + if argv: + from setuptools.command.easy_install import main + main(argv) + else: + print "Setuptools version",version,"or greater has been installed." + print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' + + + +def update_md5(filenames): + """Update our built-in md5 registry""" + + import re + from md5 import md5 + + for name in filenames: + base = os.path.basename(name) + f = open(name,'rb') + md5_data[base] = md5(f.read()).hexdigest() + f.close() + + data = [" %r: %r,\n" % it for it in md5_data.items()] + data.sort() + repl = "".join(data) + + import inspect + srcfile = inspect.getsourcefile(sys.modules[__name__]) + f = open(srcfile, 'rb'); src = f.read(); f.close() + + match = re.search("\nmd5_data = {\n([^}]+)}", src) + if not match: + print >>sys.stderr, "Internal error!" + sys.exit(2) + + src = src[:match.start(1)] + repl + src[match.end(1):] + f = open(srcfile,'w') + f.write(src) + f.close() + + +if __name__=='__main__': + if len(sys.argv)>2 and sys.argv[1]=='--md5update': + update_md5(sys.argv[2:]) + else: + main(sys.argv[1:]) + + + + + From 8254a7a7db483e3d252d7cc34f5d254e98cfa066 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Sat, 15 Oct 2005 00:54:42 +0000 Subject: [PATCH 072/117] Fixed #628 -- Django no longer overwrites model class docstrings if they're provided git-svn-id: http://code.djangoproject.com/svn/django/trunk@878 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/meta/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/django/core/meta/__init__.py b/django/core/meta/__init__.py index 0c6078705a..f934f9dd6c 100644 --- a/django/core/meta/__init__.py +++ b/django/core/meta/__init__.py @@ -563,7 +563,8 @@ class ModelBase(type): new_class = type.__new__(cls, name, bases, attrs) # Give the class a docstring -- its definition. - new_class.__doc__ = "%s.%s(%s)" % (opts.module_name, name, ", ".join([f.name for f in opts.fields])) + if new_class.__doc__ is None: + new_class.__doc__ = "%s.%s(%s)" % (opts.module_name, name, ", ".join([f.name for f in opts.fields])) # Create the standard, module-level API helper functions such # as get_object() and get_list(). From 24154b216682dfef7ff647a5c2f698b89ba429a6 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Sat, 15 Oct 2005 01:37:16 +0000 Subject: [PATCH 073/117] Fixed #225 -- Added first stab at MS SQL Server support (via ADO). Thanks to gmilas@gmail.com for the patch git-svn-id: http://code.djangoproject.com/svn/django/trunk@879 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/global_settings.py | 12 +- django/conf/project_template/settings/main.py | 2 +- django/core/db/backends/ado_mssql.py | 153 ++++++++++++++++++ 3 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 django/core/db/backends/ado_mssql.py diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index dde5612038..41fe147374 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -45,12 +45,12 @@ SERVER_EMAIL = 'root@localhost' SEND_BROKEN_LINK_EMAILS = False # Database connection info. -DATABASE_ENGINE = 'postgresql' # 'postgresql', 'mysql', or 'sqlite3'. -DATABASE_NAME = '' -DATABASE_USER = '' -DATABASE_PASSWORD = '' -DATABASE_HOST = '' # Set to empty string for localhost. -DATABASE_PORT = '' # Set to empty string for default. +DATABASE_ENGINE = 'postgresql' # 'postgresql', 'mysql', 'sqlite3' or 'ado_mssql'. +DATABASE_NAME = '' # Or path to database file if using sqlite3. +DATABASE_USER = '' # Not used with sqlite3. +DATABASE_PASSWORD = '' # Not used with sqlite3. +DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. +DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. # Host for sending e-mail. EMAIL_HOST = 'localhost' diff --git a/django/conf/project_template/settings/main.py b/django/conf/project_template/settings/main.py index 38df2ad01d..cbb32b7920 100644 --- a/django/conf/project_template/settings/main.py +++ b/django/conf/project_template/settings/main.py @@ -10,7 +10,7 @@ MANAGERS = ADMINS LANGUAGE_CODE = 'en-us' -DATABASE_ENGINE = 'postgresql' # 'postgresql', 'mysql', or 'sqlite3'. +DATABASE_ENGINE = 'postgresql' # 'postgresql', 'mysql', 'sqlite3' or 'ado_mssql'. DATABASE_NAME = '' # Or path to database file if using sqlite3. DATABASE_USER = '' # Not used with sqlite3. DATABASE_PASSWORD = '' # Not used with sqlite3. diff --git a/django/core/db/backends/ado_mssql.py b/django/core/db/backends/ado_mssql.py new file mode 100644 index 0000000000..46116ccdad --- /dev/null +++ b/django/core/db/backends/ado_mssql.py @@ -0,0 +1,153 @@ +""" +ADO MSSQL database backend for Django. + +Requires adodbapi 2.0.1: http://adodbapi.sourceforge.net/ +""" + +from django.core.db import base +from django.core.db.dicthelpers import * +import adodbapi as Database +import datetime +try: + import mx +except ImportError: + mx = None + +DatabaseError = Database.DatabaseError + +# We need to use a special Cursor class because adodbapi expects question-mark +# param style, but Django expects "%s". This cursor converts question marks to +# format-string style. +class Cursor(Database.Cursor): + def executeHelper(self, operation, isStoredProcedureCall, parameters=None): + if parameters is not None and "%s" in operation: + operation = operation.replace("%s", "?") + Database.Cursor.executeHelper(self, operation, isStoredProcedureCall, parameters) + +class Connection(Database.Connection): + def cursor(self): + return Cursor(self) +Database.Connection = Connection + +origCVtoP = Database.convertVariantToPython +def variantToPython(variant, adType): + if type(variant) == bool and adType == 11: + return variant # bool not 1/0 + res = origCVtoP(variant, adType) + if mx is not None and type(res) == mx.DateTime.mxDateTime.DateTimeType: + # Convert ms.DateTime objects to Python datetime.datetime objects. + tv = list(res.tuple()[:7]) + tv[-2] = int(tv[-2]) + return datetime.datetime(*tuple(tv)) + if type(res) == float and str(res)[-2:] == ".0": + return int(res) # If float but int, then int. + return res +Database.convertVariantToPython = variantToPython + +class DatabaseWrapper: + def __init__(self): + self.connection = None + self.queries = [] + + def cursor(self): + from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PORT, DATABASE_PASSWORD, DEBUG + if self.connection is None: + if DATABASE_NAME == '' or DATABASE_USER == '': + from django.core.exceptions import ImproperlyConfigured + raise ImproperlyConfigured, "You need to specify both DATABASE_NAME and DATABASE_USER in your Django settings file." + if not DATABASE_HOST: + DATABASE_HOST = "127.0.0.1" + # TODO: Handle DATABASE_PORT. + conn_string = "PROVIDER=SQLOLEDB;DATA SOURCE=%s;UID=%s;PWD=%s;DATABASE=%s" % (DATABASE_HOST, DATABASE_USER, DATABASE_PASSWORD, DATABASE_NAME) + self.connection = Database.connect(conn_string) + cursor = self.connection.cursor() + if DEBUG: + return base.CursorDebugWrapper(cursor, self) + return cursor + + def commit(self): + return self.connection.commit() + + def rollback(self): + if self.connection: + return self.connection.rollback() + + def close(self): + if self.connection is not None: + self.connection.close() + self.connection = None + +def get_last_insert_id(cursor, table_name, pk_name): + cursor.execute("SELECT %s FROM %s WHERE %s = @@IDENTITY" % (pk_name, table_name, pk_name)) + return cursor.fetchone()[0] + +def get_date_extract_sql(lookup_type, table_name): + # lookup_type is 'year', 'month', 'day' + return "DATEPART(%s, %s)" % (lookup_type, table_name) + +def get_date_trunc_sql(lookup_type, field_name): + # lookup_type is 'year', 'month', 'day' + if lookup_type=='year': + return "Convert(datetime, Convert(varchar, DATEPART(year, %s)) + '/01/01')" % field_name + if lookup_type=='month': + return "Convert(datetime, Convert(varchar, DATEPART(year, %s)) + '/' + Convert(varchar, DATEPART(month, %s)) + '/01')" % (field_name, field_name) + if lookup_type=='day': + return "Convert(datetime, Convert(varchar(12), %s))" % field_name + +def get_limit_offset_sql(limit, offset=None): + # TODO: This is a guess. Make sure this is correct. + sql = "LIMIT %s" % limit + if offset and offset != 0: + sql += " OFFSET %s" % offset + return sql + +def get_random_function_sql(): + # TODO: This is a guess. Make sure this is correct. + return "RANDOM()" + +def get_relations(cursor, table_name): + raise NotImplementedError + +OPERATOR_MAPPING = { + 'exact': '=', + 'iexact': 'LIKE', + 'contains': 'LIKE', + 'icontains': 'LIKE', + 'ne': '!=', + 'gt': '>', + 'gte': '>=', + 'lt': '<', + 'lte': '<=', + 'startswith': 'LIKE', + 'endswith': 'LIKE', + 'istartswith': 'LIKE', + 'iendswith': 'LIKE', +} + +DATA_TYPES = { + 'AutoField': 'int IDENTITY (1, 1)', + 'BooleanField': 'bit', + 'CharField': 'varchar(%(maxlength)s)', + 'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)', + 'DateField': 'smalldatetime', + 'DateTimeField': 'smalldatetime', + 'EmailField': 'varchar(75)', + 'FileField': 'varchar(100)', + 'FilePathField': 'varchar(100)', + 'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)', + 'ImageField': 'varchar(100)', + 'IntegerField': 'int', + 'IPAddressField': 'char(15)', + 'ManyToManyField': None, + 'NullBooleanField': 'bit', + 'OneToOneField': 'int', + 'PhoneNumberField': 'varchar(20)', + 'PositiveIntegerField': 'int CONSTRAINT [CK_int_pos_%(name)s] CHECK ([%(name)s] > 0)', + 'PositiveSmallIntegerField': 'smallint CONSTRAINT [CK_smallint_pos_%(name)s] CHECK ([%(name)s] > 0)', + 'SlugField': 'varchar(50)', + 'SmallIntegerField': 'smallint', + 'TextField': 'text', + 'TimeField': 'time', + 'URLField': 'varchar(200)', + 'USStateField': 'varchar(2)', +} From a2e26150b77cd2cdad4cc9de120a87a6370c6dd5 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Sat, 15 Oct 2005 02:20:35 +0000 Subject: [PATCH 074/117] Fixed #616 -- Added a process_exception() hook to middleware framework. Thanks, Hugo git-svn-id: http://code.djangoproject.com/svn/django/trunk@880 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/handlers/base.py | 17 +++++++++++++++-- django/utils/decorators.py | 9 ++++++++- docs/middleware.txt | 13 +++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index 00149ff791..ecec674d3e 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -2,7 +2,7 @@ from django.utils import httpwrappers class BaseHandler: def __init__(self): - self._request_middleware = self._view_middleware = self._response_middleware = None + self._request_middleware = self._view_middleware = self._response_middleware = self._exception_middleware = None def load_middleware(self): """ @@ -15,6 +15,7 @@ class BaseHandler: self._request_middleware = [] self._view_middleware = [] self._response_middleware = [] + self._exception_middleware = [] for middleware_path in settings.MIDDLEWARE_CLASSES: dot = middleware_path.rindex('.') mw_module, mw_classname = middleware_path[:dot], middleware_path[dot+1:] @@ -38,6 +39,8 @@ class BaseHandler: self._view_middleware.append(mw_instance.process_view) if hasattr(mw_instance, 'process_response'): self._response_middleware.insert(0, mw_instance.process_response) + if hasattr(mw_instance, 'process_exception'): + self._exception_middleware.insert(0, mw_instance.process_exception) def get_response(self, path, request): "Returns an HttpResponse object for the given HttpRequest" @@ -61,7 +64,17 @@ class BaseHandler: if response: return response - response = callback(request, **param_dict) + try: + response = callback(request, **param_dict) + except Exception, e: + # If the view raised an exception, run it through exception + # middleware, and if the exception middleware returns a + # response, use that. Otherwise, reraise the exception. + for middleware_method in self._exception_middleware: + response = middleware_method(request, e) + if response: + return response + raise e # Complain if the view returned None (a common error). if response is None: diff --git a/django/utils/decorators.py b/django/utils/decorators.py index 1333f9da88..074532e741 100644 --- a/django/utils/decorators.py +++ b/django/utils/decorators.py @@ -16,7 +16,14 @@ def decorator_from_middleware(middleware_class): result = middleware.process_view(request, view_func, **kwargs) if result is not None: return result - response = view_func(request, *args, **kwargs) + try: + response = view_func(request, *args, **kwargs) + except Exception, e: + if hasattr(middleware, 'process_exception'): + result = middleware.process_exception(request, e) + if result is not None: + return result + raise e if hasattr(middleware, 'process_response'): result = middleware.process_response(request, response) if result is not None: diff --git a/docs/middleware.txt b/docs/middleware.txt index dfa1947bbd..33cb1a38e4 100644 --- a/docs/middleware.txt +++ b/docs/middleware.txt @@ -168,6 +168,19 @@ object returned by a Django view. the given ``response``, or it could create and return a brand-new ``HttpResponse``. +process_exception +----------------- + +Interface: ``process_exception(self, request, exception)`` + +``request`` is an ``HttpRequest`` object. ``exception`` is an ``Exception`` +object raised by the view function. + +Django calls ``process_exception()`` when a view raises an exception. +``process_exception()`` should return either ``None`` or an ``HttpResponse`` +object. If it returns an ``HttpResponse`` object, the response will be returned +to the browser. Otherwise, default exception handling kicks in. + Guidelines ---------- From f1ecfe991d32f42202a6ed3ce61a234982286c5b Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Sun, 16 Oct 2005 19:42:16 +0000 Subject: [PATCH 075/117] Fixed #630 -- Fixed formatting error in docs/model-api.txt. Thanks, ken@kenkinder.com git-svn-id: http://code.djangoproject.com/svn/django/trunk@887 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/model-api.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/model-api.txt b/docs/model-api.txt index 140518e80e..ee5f9ee723 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -252,7 +252,7 @@ Here are all available field types: Using a `FieldField` or an ``ImageField`` (see below) in a model takes a few steps: - 1. In your settings file, you'll need to define ``MEDIA_ROOT``as the + 1. In your settings file, you'll need to define ``MEDIA_ROOT`` as the full path to a directory where you'd like Django to store uploaded files. (For performance, these files are not stored in the database.) Define ``MEDIA_URL`` as the base public URL of that directory. Make From f808d1bb8c75f7272373992f4da72126a2c685f7 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Sun, 16 Oct 2005 20:52:11 +0000 Subject: [PATCH 076/117] Removed django/conf/admin_templates/changelist_generic.html -- a leftover, legacy template that's no longer used git-svn-id: http://code.djangoproject.com/svn/django/trunk@888 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- .../admin_templates/changelist_generic.html | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 django/conf/admin_templates/changelist_generic.html diff --git a/django/conf/admin_templates/changelist_generic.html b/django/conf/admin_templates/changelist_generic.html deleted file mode 100644 index d8f51c537b..0000000000 --- a/django/conf/admin_templates/changelist_generic.html +++ /dev/null @@ -1,34 +0,0 @@ -{% extends "base_site" %} - -{% block bodyclass %}change-list{% endblock %} - -{% block content %} - -{% if not hide_add_link %} -

    -{% endif %} - -
    -
    - - {% if toplinks %} - -
    - {% endif %} - - {% if changelist %} - - {% endif %} - -
    -
    - -{% endblock %} From dc3f6e41137a6b138d777b1449790753e825d5d2 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 17 Oct 2005 01:53:22 +0000 Subject: [PATCH 077/117] Changed internal variable names in django.core.template.loaders.eggs to be consistent with filesystem git-svn-id: http://code.djangoproject.com/svn/django/trunk@889 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/template/loaders/eggs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/django/core/template/loaders/eggs.py b/django/core/template/loaders/eggs.py index b1e221ee1c..33ba043220 100644 --- a/django/core/template/loaders/eggs.py +++ b/django/core/template/loaders/eggs.py @@ -8,18 +8,18 @@ except ImportError: from django.core.template import TemplateDoesNotExist from django.conf.settings import INSTALLED_APPS, TEMPLATE_FILE_EXTENSION -def load_template_source(name, dirs=None): +def load_template_source(template_name, template_dirs=None): """ Loads templates from Python eggs via pkg_resource.resource_string. - For every installed app, it tries to get the resource (app, name). + For every installed app, it tries to get the resource (app, template_name). """ if resource_string is not None: - pkg_name = 'templates/' + name + TEMPLATE_FILE_EXTENSION + pkg_name = 'templates/' + template_name + TEMPLATE_FILE_EXTENSION for app in INSTALLED_APPS: try: return resource_string(app, pkg_name) except: pass - raise TemplateDoesNotExist, name + raise TemplateDoesNotExist, template_name load_template_source.is_usable = resource_string is not None From e8fda9091af809259cc9a63cc51717bf513ca199 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 17 Oct 2005 02:18:28 +0000 Subject: [PATCH 078/117] Fixed typo in filesystem template-loader error message git-svn-id: http://code.djangoproject.com/svn/django/trunk@890 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/template/loaders/filesystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/core/template/loaders/filesystem.py b/django/core/template/loaders/filesystem.py index 1d42c6d841..e5bb1bab1c 100644 --- a/django/core/template/loaders/filesystem.py +++ b/django/core/template/loaders/filesystem.py @@ -17,6 +17,6 @@ def load_template_source(template_name, template_dirs=None): if template_dirs: error_msg = "Tried %s" % tried else: - error_msg = "Your TEMPLATE_DIRS settings is empty. Change it to point to at least one template directory." + error_msg = "Your TEMPLATE_DIRS setting is empty. Change it to point to at least one template directory." raise TemplateDoesNotExist, error_msg load_template_source.is_usable = True From b9736c5c633fe0bd2e1aeda2b9e0a99257175e33 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 17 Oct 2005 02:31:35 +0000 Subject: [PATCH 079/117] Added 'Designate between GET and POST' section to design_philosophies.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@891 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/design_philosophies.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/design_philosophies.txt b/docs/design_philosophies.txt index 2084c992a5..2988672f02 100644 --- a/docs/design_philosophies.txt +++ b/docs/design_philosophies.txt @@ -242,3 +242,9 @@ Loose coupling A view shouldn't care about which template system the developer uses -- or even whether a template system is used at all. + +Designate between GET and POST +------------------------------ + +GET and POST are distinct; developers should explicitly use one or the other. +The framework should make it easy to distinguish between GET and POST data. From 57b4f231fdc24dcb41a503c3314442ff5250a830 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 17 Oct 2005 02:37:50 +0000 Subject: [PATCH 080/117] Fixed #583 -- Added app_directories template loader, which searches for templates in 'templates' directory in each INSTALLED_APPS package. It's turned off by default. git-svn-id: http://code.djangoproject.com/svn/django/trunk@892 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/global_settings.py | 1 + django/conf/project_template/settings/main.py | 7 +++++ .../core/template/loaders/app_directories.py | 28 +++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 django/core/template/loaders/app_directories.py diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 41fe147374..f0f66540db 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -65,6 +65,7 @@ TEMPLATE_FILE_EXTENSION = '.html' # See the comments in django/core/template/loader.py for interface # documentation. TEMPLATE_LOADERS = ( +# 'django.core.template.loaders.app_directories.load_template_source', 'django.core.template.loaders.filesystem.load_template_source', # 'django.core.template.loaders.eggs.load_template_source', ) diff --git a/django/conf/project_template/settings/main.py b/django/conf/project_template/settings/main.py index cbb32b7920..1bde7df10a 100644 --- a/django/conf/project_template/settings/main.py +++ b/django/conf/project_template/settings/main.py @@ -30,6 +30,13 @@ MEDIA_URL = '' # Make this unique, and don't share it with anybody. SECRET_KEY = '' +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( +# 'django.core.template.loaders.app_directories.load_template_source', + 'django.core.template.loaders.filesystem.load_template_source', +# 'django.core.template.loaders.eggs.load_template_source', +) + MIDDLEWARE_CLASSES = ( "django.middleware.common.CommonMiddleware", "django.middleware.doc.XViewMiddleware", diff --git a/django/core/template/loaders/app_directories.py b/django/core/template/loaders/app_directories.py new file mode 100644 index 0000000000..5afb18e2f5 --- /dev/null +++ b/django/core/template/loaders/app_directories.py @@ -0,0 +1,28 @@ +# Wrapper for loading templates from "template" directories in installed app packages. + +from django.conf.settings import INSTALLED_APPS, TEMPLATE_FILE_EXTENSION +from django.core.template import TemplateDoesNotExist +import os + +# At compile time, cache the directories to search. +app_template_dirs = [] +for app in INSTALLED_APPS: + i = app.rfind('.') + m, a = app[:i], app[i+1:] + mod = getattr(__import__(m, '', '', [a]), a) + template_dir = os.path.join(os.path.dirname(mod.__file__), 'templates') + if os.path.isdir(template_dir): + app_template_dirs.append(template_dir) + +# It won't change, so convert it to a tuple to save memory. +app_template_dirs = tuple(app_template_dirs) + +def load_template_source(template_name, template_dirs=None): + for template_dir in app_template_dirs: + filepath = os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION + try: + return open(filepath).read() + except IOError: + pass + raise TemplateDoesNotExist, template_name +load_template_source.is_usable = True From 3df39deede8eba5a005ae8c487cd6320b04dc83c Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 17 Oct 2005 03:00:18 +0000 Subject: [PATCH 081/117] Added 'Loader types' section to docs/templates_python.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@893 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/templates_python.txt | 49 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/docs/templates_python.txt b/docs/templates_python.txt index bd8aea62c7..b4e09252bd 100644 --- a/docs/templates_python.txt +++ b/docs/templates_python.txt @@ -287,8 +287,12 @@ Generally, you'll store templates in files on your filesystem rather than using the low-level ``Template`` API yourself. Save templates in a file with an ".html" extension in a directory specified as a **template directory**. -(The ".html" extension is just a required convention. It doesn't mean templates -can only contain HTML. They can contain whatever textual content you want.) +If you don't like the requirement that templates have an ".html" extension, +change your ``TEMPLATE_FILE_EXTENSION`` setting. It's set to ``".html"`` by +default. + +Also, the .html extension doesn't mean templates can contain only HTML. They +can contain whatever textual content you want. The TEMPLATE_DIRS setting ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -355,6 +359,47 @@ To load a template that's within a subdirectory, just use a slash, like so:: get_template("news/story_detail") +Loader types +~~~~~~~~~~~~ + +By default, Django uses a filesystem-based template loader, but Django comes +with a few other template loaders. They're disabled by default, but you can +activate them by editing your ``TEMPLATE_LOADERS`` setting. +``TEMPLATE_LOADERS`` should be a tuple of strings, where each string represents +a template loader. Here are the built-in template loaders: + +``django.core.template.loaders.filesystem.load_template_source`` + Loads templates from the filesystem, according to ``TEMPLATE_DIRS``. + +``django.core.template.loaders.app_directories.load_template_source`` + Loads templates from Django apps on the filesystem. For each app in + ``INSTALLED_APPS``, the loader looks for a ``templates`` subdirectory. If + the directory exists, Django looks for templates in there. + + This means you can store templates with your individual apps. This also + makes it easy to distribute Django apps with default templates. + + For example, for this setting:: + + INSTALLED_APPS = ('myproject.polls', 'myproject.music') + + ...then ``get_template("foo")`` will look for templates in these + directories, in this order: + + * ``/path/to/myproject/polls/templates/foo.html`` + * ``/path/to/myproject/music/templates/music.html`` + + Note that the loader performs an optimization when it is first imported: + It caches a list of which ``INSTALLED_APPS`` packages have a ``templates`` + subdirectory. + +``django.core.template.loaders.eggs.load_template_source`` + Just like ``app_directories`` above, but it loads templates from Python + eggs rather than from the filesystem. + +Django uses the template loaders in order according to the ``TEMPLATE_LOADERS`` +setting. It uses each loader until a loader finds a match. + Extending the template system ============================= From 8b7ebca68a04c7f88dbb61e5cc43835b761e8d76 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 17 Oct 2005 04:36:51 +0000 Subject: [PATCH 082/117] Changed global_settings.ADMIN_FOR from [] to () git-svn-id: http://code.djangoproject.com/svn/django/trunk@894 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/global_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index f0f66540db..52acb51de4 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -110,7 +110,7 @@ ALLOWED_INCLUDE_ROOTS = () # If this is a admin settings module, this should be a list of # settings modules (in the format 'foo.bar.baz') for which this admin # is an admin. -ADMIN_FOR = [] +ADMIN_FOR = () # Whether to check the flat-pages table as a last resort for all 404 errors. USE_FLAT_PAGES = True From 6bec753867485da2b11f5b36cee0b96f795c91c3 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 17 Oct 2005 04:53:03 +0000 Subject: [PATCH 083/117] Added docs/settings.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@895 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/settings.txt | 517 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 517 insertions(+) create mode 100644 docs/settings.txt diff --git a/docs/settings.txt b/docs/settings.txt new file mode 100644 index 0000000000..5238022ebb --- /dev/null +++ b/docs/settings.txt @@ -0,0 +1,517 @@ +=============== +Django settings +=============== + +A Django settings file contains all the configuration of your Django +installation. This document explains how settings work and which settings are +available. + +The basics +========== + +A settings file is just a Python module with module-level variables. + +Here are a couple of example settings:: + + DEBUG = False + DEFAULT_FROM_EMAIL = 'webmaster@example.com' + TEMPLATE_DIRS = ('/home/templates/mike', '/home/templates/john') + +Because a settings file is a Python module, the following apply: + + * It shouldn't have Python syntax errors. + * It can assign settings dynamically using normal Python syntax. + For example:: + + MY_SETTING = [str(i) for i in range(30)] + + * It can import values from other settings files. + +Designating the settings +======================== + +When you use Django, you have to tell it which settings you're using. Do this +by using an environment variable, DJANGO_SETTINGS_MODULE. + +The value of DJANGO_SETTINGS_MODULE should be in Python path syntax, e.g. +``"myproject.settings.main"``. Note that the settings module should be on the +Python `import search path`_. + +.. _import search path: http://diveintopython.org/getting_to_know_python/everything_is_an_object.html + +The django-admin.py utility +--------------------------- + +When using `django-admin.py`_, you can either set the environment variable +once, or explicitly pass in the settings module each time you run the utility. + +Example (Unix Bash shell):: + + export DJANGO_SETTINGS_MODULE=myproject.settings.main + django-admin.py runserver + +Example (Windows shell):: + + set DJANGO_SETTINGS_MODULE=myproject.settings.main + django-admin.py runserver + +Use the ``--settings`` command-line argument to specify the settings manually:: + + django-admin.py runserver --settings=myproject.settings.main + +.. _django-admin.py: http://www.djangoproject.com/documentation/django_admin/ + +On the server (mod_python) +-------------------------- + +In your live server environment, you'll need to tell Apache/mod_python which +settings file to use. Do that with ``SetEnv``:: + + + SetHandler python-program + PythonHandler django.core.handlers.modpython + SetEnv DJANGO_SETTINGS_MODULE myproject.settings.main + + +Read the `Django mod_python documentation`_ for more information. + +.. _Django mod_python documentation: http://www.djangoproject.com/documentation/mod_python/ + +Default settings +================ + +A Django settings file doesn't have to define any settings if it doesn't need +to. Each setting has a sensible default value. These defaults live in the file +``django/conf/global_settings.py``. + +Here's the algorithm Django uses in compiling settings: + + * Load settings from ``default_settings.py``. + * Load settings from the specified settings file, overriding the global + settings as necessary. + +Using settings in Python code +============================= + +In your Django apps, use settings by importing them from +``django.conf.settings``. Example:: + + from django.conf.settings import DEBUG + + if DEBUG: + # Do something + +Note that your code should *not* import from either ``default_settings`` or +your own settings file. ``django.conf.settings`` abstracts the concepts of +default settings and site-specific settings; it presents a single interface. + +Altering settings at runtime +============================ + +You shouldn't alter settings in your applications at runtime. For example, +don't do this in a view:: + + from django.conf.settings import DEBUG + + DEBUG = True # Don't do this! + +The only place you should assign to settings is in a settings file. + +Security +======== + +Because a settings file contains sensitive information, such as the database +password, you should make every attempt to limit access to it. For example, +change its file permissions so that only you and your Web server's user can +read it. This is especially important in a shared-hosting environment. + +Available settings +================== + +Here's a full list of all available settings, in alphabetical order, and their +default values. + +ABSOLUTE_URL_OVERRIDES +---------------------- + +Default: ``{}`` (Empty dictionary) + +A dictionary mapping ``"app_label.module_name"`` strings to functions that take +a model object and return its URL. This is a way of overriding +``get_absolute_url()`` methods on a per-installation basis. Example:: + + ABSOLUTE_URL_OVERRIDES = { + 'blogs.blogs': lambda o: "/blogs/%s/" % o.slug, + 'news.stories': lambda o: "/stories/%s/%s/" % (o.pub_year, o.slug), + } + +ADMIN_FOR +--------- + +Default: ``()`` (Empty list) + +Used for admin-site settings modules, this should be a tuple of settings +modules (in the format ``'foo.bar.baz'``) for which this site is an admin. + +ADMIN_MEDIA_PREFIX +------------------ + +Default: ``'/media/'`` + +The URL prefix for admin media -- CSS, JavaScript and images. Make sure to use +a trailing slash. + +ADMINS +------ + +Default: ``()`` (Empty tuple) + +A tuple that lists people who get code error notifications. When +``DEBUG=False`` and a view raises an exception, Django will e-mail these people +with the full exception information. Each member of the tuple should be a tuple +of (Full name, e-mail address). Example:: + + (('John', 'john@example.com'), ('Mary', 'mary@example.com')) + +ALLOWED_INCLUDE_ROOTS +--------------------- + +Default: ``()`` (Empty tuple) + +A tuple of strings representing allowed prefixes for the ``{% ssi %}`` template +tag. This is a security measure, so that template authors can't access files +that they shouldn't be accessing. + +For example, if ``ALLOWED_INCLUDE_ROOTS`` is ``('/home/html', '/var/www')``, +then ``{% ssi /home/html/foo.txt %}`` would work, but ``{% ssi /etc/passwd %}`` +wouldn't. + +APPEND_SLASH +------------ + +Default: ``True`` + +Whether to append trailing slashes to URLs. This is only used if +``CommonMiddleware`` is installed (see the `middleware docs`_). See also +``PREPEND_WWW``. + +CACHE_BACKEND +------------- + +Default: ``'simple://'`` + +The cache backend to use. See the `cache docs`_. + +CACHE_MIDDLEWARE_KEY_PREFIX + +Default: ``''`` (Empty string) + +The cache key prefix that the cache middleware should use. See the +`cache docs`_. + +DATABASE_ENGINE +--------------- + +Default: ``'postgresql'`` + +Which database backend to use. Either ``'postgresql'``, ``'mysql'``, +``'sqlite3'`` or ``'ado_mssql'``. + +DATABASE_HOST +------------- + +Default: ``''`` (Empty string) + +Which host to use when connecting to the database. An empty string means +localhost. Not used with SQLite. + +DATABASE_NAME +------------- + +Default: ``''`` (Empty string) + +The name of the database to use. For SQLite, it's the full path to the database +file. + +DATABASE_PASSWORD +----------------- + +Default: ``''`` (Empty string) + +The password to use when connecting to the database. Not used with SQLite. + +DATABASE_PORT +------------- + +Default: ``''`` (Empty string) + +The port to use when connecting to the database. An empty string means the +default port. Not used with SQLite. + +DATABASE_USER +------------- + +Default: ``''`` (Empty string) + +The username to use when connecting to the database. Not used with SQLite. + +DEBUG +----- + +Default: ``False`` + +A boolean that turns on/off debug mode. + +DEFAULT_CHARSET +--------------- + +Default: ``'utf-8'`` + +Default charset to use for all ``HttpResponse`` objects, if a MIME type isn't +manually specified. Used with ``DEFAULT_CONTENT_TYPE`` to construct the +``Content-Type`` header. + +DEFAULT_CONTENT_TYPE +-------------------- + +Default: ``'text/html'`` + +Default content type to use for all ``HttpResponse`` objects, if a MIME type +isn't manually specified. Used with ``DEFAULT_CHARSET`` to construct the +``Content-Type`` header. + +DEFAULT_FROM_EMAIL +------------------ + +Default: ``'webmaster@localhost'`` + +Default e-mail address to use for various automated correspondence from the +site manager(s). + +DISALLOWED_USER_AGENTS +---------------------- + +Default: ``()`` (Empty tuple) + +List of compiled regular expression objects representing User-Agent strings +that are not allowed to visit any page, systemwide. Use this for bad +robots/crawlers. This is only used if ``CommonMiddleware`` is installed (see +the `middleware docs`_). + +EMAIL_HOST +---------- + +Default: ``'localhost'`` + +The host to use for sending e-mail. + +EMAIL_SUBJECT_PREFIX +-------------------- + +Default: ``'[Django] '`` + +Subject-line prefix for e-mail messages sent with ``django.core.mail.mail_admins`` +or ``django.core.mail.mail_managers``. You'll probably want to include the +trailing space. + +IGNORABLE_404_ENDS +------------------ + +Default: ``('mail.pl', 'mailform.pl', 'mail.cgi', 'mailform.cgi', 'favicon.ico', '.php')`` + +See also ``IGNORABLE_404_STARTS``. + +IGNORABLE_404_STARTS +-------------------- + +Default: ``('/cgi-bin/', '/_vti_bin', '/_vti_inf')`` + +A tuple of strings that specify beginnings of URLs that should be ignored by +the 404 e-mailer. See ``SEND_BROKEN_LINK_EMAILS`` and ``IGNORABLE_404_ENDS``. + +INTERNAL_IPS +------------ + +Default: ``()`` (Empty tuple) + +A tuple of IP addresses, as strings, that: + + * See debug comments, when ``DEBUG`` is ``True`` + * Receive X headers if the ``XViewMiddleware`` is installed (see the + `middleware docs`_) + +JING_PATH +--------- + +Default: ``'/usr/bin/jing'`` + +Path to the "Jing" executable. Jing is a RELAX NG validator, and Django uses it +to validate ``XMLField``s. See http://www.thaiopensource.com/relaxng/jing.html . + +LANGUAGE_CODE +------------- + +Default: ``'en-us'`` + +A string representing the language code for this installation. + +MANAGERS +-------- + +Default: ``ADMINS`` (Whatever ``ADMINS`` is set to) + +A tuple in the same format as ``ADMINS`` that specifies who should get +broken-link notifications when ``SEND_BROKEN_LINK_EMAILS=True``. + +MEDIA_ROOT +---------- + +Default: ``''`` (Empty string) + +Absolute path to the directory that holds media for this installation. +Example: ``"/home/media/media.lawrence.com/"`` See also ``MEDIA_URL``. + +MEDIA_URL +--------- + +Default: ``''`` (Empty string) + +URL that handles the media served from ``MEDIA_ROOT``. +Example: ``"http://media.lawrence.com"`` + +MIDDLEWARE_CLASSES +------------------ + +Default: ``("django.middleware.sessions.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.doc.XViewMiddleware")`` + +A tuple of middleware classes to use. See the `middleware docs`_. + +PREPEND_WWW +----------- + +Default: ``False`` + +Whether to prepend the "www." subdomain to URLs that don't have it. This is +only used if ``CommonMiddleware`` is installed (see the `middleware docs`_). +See also ``PREPEND_WWW``. + +SECRET_KEY +---------- + +Default: ``''`` (Empty string) + +A secret key for this particular Django installation. Used to provide a seed in +secret-key hashing algorithms. Set this to a random string -- the longer, the +better. ``django-admin.py startproject`` creates one automatically. + +SEND_BROKEN_LINK_EMAILS +----------------------- + +Default: ``False`` + +Whether to send an e-mail to the ``MANAGERS`` each time somebody visits a +Django-powered page that is 404ed with a non-empty referer (i.e., a broken +link). This is only used if ``CommonMiddleware`` is installed (see the +`middleware docs`_). See also ``IGNORABLE_404_STARTS`` and +``IGNORABLE_404_ENDS``. + +SERVER_EMAIL +------------ + +Default: ``'root@localhost'`` + +The e-mail address that error messages come from, such as those sent to +``ADMINS`` and ``MANAGERS``. + +SESSION_COOKIE_AGE +------------------ + +Default: ``1209600`` (2 weeks, in seconds) + +The age of session cookies, in seconds. See the `session docs`_. + +SESSION_COOKIE_DOMAIN +--------------------- + +Default: ``None`` + +The domain to use for session cookies. Set this to a string such as +``".lawrence.com"`` for cross-domain cookies, or use ``None`` for a standard +domain cookie. See the `session docs`_. + +SESSION_COOKIE_NAME +------------------- + +Default: ``'hotclub'`` + +The name of the cookie to use for sessions. This can be whatever you want. +See the `session docs`_. + +``'hotclub'`` is a reference to the Hot Club of France, the band Django +Reinhardt played in. + +TEMPLATE_DIRS +------------- + +Default: ``()`` (Empty tuple) + +List of locations of the template source files, in search order. See the +`template documentation`_. + +TEMPLATE_FILE_EXTENSION +----------------------- + +Default: ``'.html'`` + +The file extension to append to all template names when searching for +templates. See the `template documentation`_. + +TEMPLATE_LOADERS +---------------- + +Default: ``('django.core.template.loaders.filesystem.load_template_source',)`` + +A tuple of callables (as strings) that know how to import templates from +various sources. See the `template documentation`_. + +TIME_ZONE +--------- + +Default: ``'America/Chicago'`` + +A string representing the time zone for this installation. +`See available choices`_. + +USE_ETAGS +--------- + +Default: ``False`` + +A boolean that specifies whether to output the "Etag" header. This saves +bandwidth but slows down performance. This is only used if ``CommonMiddleware`` +is installed (see the `middleware docs`_). + +USE_FLAT_PAGES +-------------- + +Default: ``True`` + +Whether to check the flat-pages table as a last resort for all 404 errors. This +is only used if ``CommonMiddleware`` is installed (see the `middleware docs`_). + +.. _cache docs: http://www.djangoproject.com/documentation/cache/ +.. _middleware docs: http://www.djangoproject.com/documentation/middleware/ +.. _session docs: http://www.djangoproject.com/documentation/sessions/ +.. _See available choices: http://www.postgresql.org/docs/current/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE +.. _template documentation: http://www.djangoproject.com/documentation/templates_python/ + +Creating your own settings +========================== + +There's nothing stopping you from creating your own settings, for your own +Django apps. Just follow these conventions: + + * Setting names are in all uppercase. + * For settings that are sequences, use tuples instead of lists. This is + purely for performance. + * Don't reinvent an already-existing setting. From c686424ed9ba71ddfc6655741c8781b071f08881 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 17 Oct 2005 04:59:06 +0000 Subject: [PATCH 084/117] Fixed some formatting issues in docs/settings.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@896 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/settings.txt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/settings.txt b/docs/settings.txt index 5238022ebb..5a2cf46827 100644 --- a/docs/settings.txt +++ b/docs/settings.txt @@ -31,9 +31,9 @@ Designating the settings ======================== When you use Django, you have to tell it which settings you're using. Do this -by using an environment variable, DJANGO_SETTINGS_MODULE. +by using an environment variable, ``DJANGO_SETTINGS_MODULE``. -The value of DJANGO_SETTINGS_MODULE should be in Python path syntax, e.g. +The value of ``DJANGO_SETTINGS_MODULE`` should be in Python path syntax, e.g. ``"myproject.settings.main"``. Note that the settings module should be on the Python `import search path`_. @@ -86,10 +86,13 @@ to. Each setting has a sensible default value. These defaults live in the file Here's the algorithm Django uses in compiling settings: - * Load settings from ``default_settings.py``. + * Load settings from ``global_settings.py``. * Load settings from the specified settings file, overriding the global settings as necessary. +Note that a settings file should *not* import from ``global_settings``, because +that's redundant. + Using settings in Python code ============================= @@ -101,7 +104,7 @@ In your Django apps, use settings by importing them from if DEBUG: # Do something -Note that your code should *not* import from either ``default_settings`` or +Note that your code should *not* import from either ``global_settings`` or your own settings file. ``django.conf.settings`` abstracts the concepts of default settings and site-specific settings; it presents a single interface. @@ -346,7 +349,8 @@ JING_PATH Default: ``'/usr/bin/jing'`` Path to the "Jing" executable. Jing is a RELAX NG validator, and Django uses it -to validate ``XMLField``s. See http://www.thaiopensource.com/relaxng/jing.html . +to validate each ``XMLField`` in your models. +See http://www.thaiopensource.com/relaxng/jing.html . LANGUAGE_CODE ------------- From b223d0fd87b5580709f11c86c8eb3d5fb2142183 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 17 Oct 2005 13:05:55 +0000 Subject: [PATCH 085/117] Added note to docs/django-admin.txt about 127.0.0.1 not being accessible from other machines on the network git-svn-id: http://code.djangoproject.com/svn/django/trunk@900 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/django-admin.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/django-admin.txt b/docs/django-admin.txt index ba9bb1403f..0faabbfcca 100644 --- a/docs/django-admin.txt +++ b/docs/django-admin.txt @@ -112,6 +112,11 @@ them to standard output, but it won't stop the server. You can run as many servers as you want, as long as they're on separate ports. Just execute ``django-admin.py runserver`` more than once. +Note that the default IP address, 127.0.0.1, is not accessible from other +machines on your network. To make your development server viewable to other +machines on the network, use its own IP address (e.g. ``192.168.2.1``) or +``0.0.0.0``. + Examples: ~~~~~~~~~ From 0bb68cd0725988bd7302da293ffdb2fa9e1b2750 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 17 Oct 2005 13:20:59 +0000 Subject: [PATCH 086/117] Fixed #635 -- Fixed typo in docs/settings.txt. Thanks, anonymous git-svn-id: http://code.djangoproject.com/svn/django/trunk@902 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/settings.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/settings.txt b/docs/settings.txt index 5a2cf46827..382a661be9 100644 --- a/docs/settings.txt +++ b/docs/settings.txt @@ -397,7 +397,7 @@ Default: ``False`` Whether to prepend the "www." subdomain to URLs that don't have it. This is only used if ``CommonMiddleware`` is installed (see the `middleware docs`_). -See also ``PREPEND_WWW``. +See also ``APPEND_SLASH``. SECRET_KEY ---------- From 383704ac844323375e743b555f6a64d6daf6d14d Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 17 Oct 2005 13:24:29 +0000 Subject: [PATCH 087/117] Fixed #634 -- Changed shortcut view to accept get_absolute_url()s that return URLs starting with http. Thanks, Hugo git-svn-id: http://code.djangoproject.com/svn/django/trunk@903 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/views/defaults.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/django/views/defaults.py b/django/views/defaults.py index d283e54c1b..decc220cf7 100644 --- a/django/views/defaults.py +++ b/django/views/defaults.py @@ -10,8 +10,12 @@ def shortcut(request, content_type_id, object_id): obj = content_type.get_object_for_this_type(pk=object_id) except ObjectDoesNotExist: raise Http404, "Content type %s object %s doesn't exist" % (content_type_id, object_id) - if not hasattr(obj, 'get_absolute_url'): + try: + absurl = obj.get_absolute_url() + except AttributeError: raise Http404, "%s objects don't have get_absolute_url() methods" % content_type.name + if absurl.startswith('http://'): + return httpwrappers.HttpResponseRedirect(absurl) object_domain = None if hasattr(obj, 'get_site_list'): site_list = obj.get_site_list() @@ -27,8 +31,8 @@ def shortcut(request, content_type_id, object_id): except sites.SiteDoesNotExist: pass if not object_domain: - return httpwrappers.HttpResponseRedirect(obj.get_absolute_url()) - return httpwrappers.HttpResponseRedirect('http://%s%s' % (object_domain, obj.get_absolute_url())) + return httpwrappers.HttpResponseRedirect(absurl) + return httpwrappers.HttpResponseRedirect('http://%s%s' % (object_domain, absurl)) def page_not_found(request): """ From 63a3c72e19758995c7933be0644b33ff6a5d9837 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 17 Oct 2005 14:26:20 +0000 Subject: [PATCH 088/117] Added note about multiple block tags to docs/templates.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@905 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/templates.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/templates.txt b/docs/templates.txt index 843ed0cbaa..215b663634 100644 --- a/docs/templates.txt +++ b/docs/templates.txt @@ -224,6 +224,13 @@ Here are some tips for working with inheritance: if you want to add to the contents of a parent block instead of completely overriding it. +Finally, note that you can't define multiple ``{% block %}`` tags with the same +name in the same template. This limitation exists because a block tag works in +"both" directions. That is, a block tag doesn't just provide a hole to fill -- +it also defines the content that fills the hole in the *parent*. If there were +two similarly-named ``{% block %}`` tags in a template, that template's parent +wouldn't know which one of the blocks' content to use. + Using the built-in reference ============================ From 91bd6eed062774cff93e4ffca0bdfcda2b4b0899 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Mon, 17 Oct 2005 18:08:55 +0000 Subject: [PATCH 089/117] Fixed typo in docs/templates.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@911 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/templates.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/templates.txt b/docs/templates.txt index 215b663634..5c52371c8a 100644 --- a/docs/templates.txt +++ b/docs/templates.txt @@ -399,13 +399,13 @@ Built-in tag reference block are output:: {% if athlete_list %} - Number of athletes: {{ athlete_list|count }} + Number of athletes: {{ athlete_list|length }} {% else %} No athletes. {% endif %} In the above, if ``athlete_list`` is not empty, the number of athletes will be - displayed by the ``{{ athlete_list|count }}`` variable. + displayed by the ``{{ athlete_list|length }}`` variable. As you can see, the ``if`` tag can take an option ``{% else %}`` clause that will be displayed if the test fails. @@ -432,8 +432,8 @@ Built-in tag reference {% if athlete_list %} {% if coach_list %} - Number of athletes: {{ athlete_list|count }}. - Number of coaches: {{ coach_list|count }}. + Number of athletes: {{ athlete_list|length }}. + Number of coaches: {{ coach_list|length }}. {% endif %} {% endif %} From 72442152d231ec5cb0ce07b3a6a7aa6d2a350525 Mon Sep 17 00:00:00 2001 From: Wilson Miner Date: Mon, 17 Oct 2005 20:22:12 +0000 Subject: [PATCH 090/117] Fixed #638 and cleaned up some CSS git-svn-id: http://code.djangoproject.com/svn/django/trunk@917 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/admin_media/css/changelists.css | 82 +++++++++------------ django/conf/admin_media/css/global.css | 69 +---------------- 2 files changed, 36 insertions(+), 115 deletions(-) diff --git a/django/conf/admin_media/css/changelists.css b/django/conf/admin_media/css/changelists.css index 966ca4a486..7ff59c5e6b 100644 --- a/django/conf/admin_media/css/changelists.css +++ b/django/conf/admin_media/css/changelists.css @@ -1,60 +1,44 @@ -/* - ______________________________ - DJANGO - Admin Changelist Styles - - Extends base.css - - by Wilson Miner - wilson@lawrence.com - - Copyright (c) 2005 - Lawrence Journal-World - - 645 New Hampshire - Lawrence, KS 66044 - +/* + DJANGO Admin Changelist Styles + by Wilson Miner wilson@lawrence.com + Copyright (c) 2005 Lawrence Journal-World */ -#changelist {position:relative; width:100%;} -#changelist table {width:100%;} -.change-list .filtered table { border-right:1px solid #ddd, width:100%; } -.change-list .filtered {min-height:400px; _height:400px;} -.change-list .filtered {background:white url(../img/admin/changelist-bg.gif) top right repeat-y !important;} -.change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull {margin-right:160px !important; width:auto !important; } -.change-list .filtered table tbody th {padding-right:10px;} -#changelist .toplinks {border-bottom:1px solid #ccc !important;} -#changelist .paginator { color:#666; border-top:1px solid #eee; border-bottom:1px solid #eee; background:white url(../img/admin/nav-bg.gif) 0 180% repeat-x; overflow:hidden;} +#changelist { position:relative; width:100%; } +#changelist table { width:100%; } +.change-list .filtered table { border-right:1px solid #ddd; } +.change-list .filtered { min-height:400px; _height:400px; } +.change-list .filtered { background:white url(../img/admin/changelist-bg.gif) top right repeat-y !important; } +.change-list .filtered table, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull { margin-right:160px !important; width:auto !important; } +.change-list .filtered table tbody th { padding-right:10px; } +#changelist .toplinks { border-bottom:1px solid #ccc !important; } +#changelist .paginator { color:#666; border-top:1px solid #eee; border-bottom:1px solid #eee; background:white url(../img/admin/nav-bg.gif) 0 180% repeat-x; overflow:hidden; } .change-list .filtered .paginator { border-right:1px solid #ddd; } /* CHANGELIST TABLES */ - -#changelist table thead th {white-space:nowrap;} -#changelist table tbody td {border-left: 1px solid #ddd;} -#changelist table tfoot {color: #666;} +#changelist table thead th { white-space:nowrap; } +#changelist table tbody td { border-left: 1px solid #ddd; } +#changelist table tfoot { color: #666; } /* TOOLBAR */ - -#changelist #toolbar {padding:3px; border-bottom:1px solid #ddd; background:#e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x; color:#666;} -#changelist #toolbar form input {font-size:11px; padding:1px 2px;} -#changelist #toolbar form #searchbar {padding:2px;} -#changelist #changelist-search img {vertical-align:middle;} +#changelist #toolbar { padding:3px; border-bottom:1px solid #ddd; background:#e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x; color:#666; } +#changelist #toolbar form input { font-size:11px; padding:1px 2px; } +#changelist #toolbar form #searchbar { padding:2px; } +#changelist #changelist-search img { vertical-align:middle; } /* FILTER COLUMN */ - -#changelist-filter {position:absolute; top:0; right:0; z-index:1000; width:160px; border-left:1px solid #ddd; background:#efefef; margin:0;} -#changelist-filter h2 {font-size:11px; padding:2px 5px; border-bottom:1px solid #ddd;} -#changelist-filter h3 {font-size:12px; margin-bottom:0;} -#changelist-filter ul {padding-left:0;margin-left:10px;_margin-right:-10px;} -#changelist-filter li {list-style-type:none; margin-left:0; padding-left:0;} -#changelist-filter a {color:#999;} -#changelist-filter a:hover {color:#036;} -#changelist-filter li.selected {border-left:5px solid #ccc; padding-left:5px;margin-left:-10px;} -#changelist-filter li.selected a {color:#5b80b2 !important;} +#changelist-filter { position:absolute; top:0; right:0; z-index:1000; width:160px; border-left:1px solid #ddd; background:#efefef; margin:0; } +#changelist-filter h2 { font-size:11px; padding:2px 5px; border-bottom:1px solid #ddd; } +#changelist-filter h3 { font-size:12px; margin-bottom:0; } +#changelist-filter ul { padding-left:0;margin-left:10px;_margin-right:-10px; } +#changelist-filter li { list-style-type:none; margin-left:0; padding-left:0; } +#changelist-filter a { color:#999; } +#changelist-filter a:hover { color:#036; } +#changelist-filter li.selected { border-left:5px solid #ccc; padding-left:5px;margin-left:-10px; } +#changelist-filter li.selected a { color:#5b80b2 !important; } /* DATE DRILLDOWN */ - -.change-list ul.toplinks {display:block; background:white url(../img/admin/nav-bg-reverse.gif) 0 -10px repeat-x; border-top:1px solid white; float:left; padding:0 !important; margin:0 !important; width:100%;} -.change-list ul.toplinks li {float: left; width: 9em; padding:3px 6px; font-weight: bold; list-style-type:none;} -.change-list ul.toplinks .date-back a {color:#999;} -.change-list ul.toplinks .date-back a:hover {color:#036;} +.change-list ul.toplinks { display:block; background:white url(../img/admin/nav-bg-reverse.gif) 0 -10px repeat-x; border-top:1px solid white; float:left; padding:0 !important; margin:0 !important; width:100%; } +.change-list ul.toplinks li { float: left; width: 9em; padding:3px 6px; font-weight: bold; list-style-type:none; } +.change-list ul.toplinks .date-back a { color:#999; } +.change-list ul.toplinks .date-back a:hover { color:#036; } diff --git a/django/conf/admin_media/css/global.css b/django/conf/admin_media/css/global.css index 7c60bf9755..453998c363 100644 --- a/django/conf/admin_media/css/global.css +++ b/django/conf/admin_media/css/global.css @@ -1,45 +1,17 @@ /* - ______________________________ - DJANGO - Admin Global Styles - - Extends base.css - - by Wilson Miner - wilson@lawrence.com - - Copyright (c) 2005 - Lawrence Journal-World - - 645 New Hampshire - Lawrence, KS 66044 - - ______________________________ - SITE DIMENSIONS - - Site Width: 768px - Content Width: 750px - Main Column: 580px - Sidebar: 220px - - ______________________________ - COLORS - - Blue #5b80b2 - Dark Blue #036 - + DJANGO Admin Global Styles + by Wilson Miner wilson@lawrence.com + Copyright (c) 2005 Lawrence Journal-World */ body { margin:0; padding:0; font-family:"Lucida Grande","Bitstream Vera Sans",Verdana,Arial,sans-serif; color:#333; } /* LINKS */ - a:link, a:visited { color: #5b80b2; text-decoration:none; } a:hover { color: #036; } a img { border:none; } /* GLOBAL DEFAULTS */ - p, ol, ul, dl { margin:.2em 0 .8em 0; font-size:12px; } p { padding:0; line-height:140%; } h1,h2,h3,h4,h5 { font-weight:bold; } @@ -67,7 +39,6 @@ div.system-message { background: #ffc; margin: 10px; padding: 6px 8px; font-size div.system-message p.system-message-title { padding:4px 5px 4px 25px; margin:0; color:red;background:#ffc url(../img/admin/icon_error.gif) 5px .3em no-repeat; } /* PAGE STRUCTURE */ - #container { position:relative; width:100%; min-width:720px; } #header { text-align:left; min-height:55px; _height:55px; } #content { margin:10px 15px; } @@ -76,13 +47,6 @@ div.system-message p.system-message-title { padding:4px 5px 4px 25px; margin:0; #footer { clear:both; padding:10px; } /* COLUMN TYPES */ -/* - colM = Main | M | - colMS = Main, Sidebar | M |S| - colSM = Sidebar, Main |S| M | - flex = single-column, liquid width - superwide = single-column, extra-wide fixed width -*/ .colMS, .colM, .colSM, .colM #content-main, .colM #content-main .xfull { width:758px; } /* master site width for fixed-width pages */ .colMS #content-main, .colSM #content-main, .colMS #content-main .xfull, .colSM #content-main .xfull { width:519px; } /* main column width for 2-column pages */ .colMS #content-related, .colSM #content-related, .colSMS #content-related { width:220px; } /* sidebar column width */ @@ -94,7 +58,6 @@ div.system-message p.system-message-title { padding:4px 5px 4px 25px; margin:0; .subcol { float:left; width:46%; margin-right:15px; } /* WIDTHS */ - .x50 { width:50px; } .x75 { width:75px; } .x100 { width:100px; } @@ -106,7 +69,6 @@ div.system-message p.system-message-title { padding:4px 5px 4px 25px; margin:0; .x500 { width:500px; } /* HEADER */ - #header { background:#417690; color:#ffc; } #header a:link, #header a:visited { color:white; } #header a:hover { text-decoration:underline; } @@ -117,12 +79,10 @@ div.system-message p.system-message-title { padding:4px 5px 4px 25px; margin:0; /* SIDEBAR */ - #content-related h3 { font-size:12px; color:#666; margin-bottom:3px; } #content-related h4 { font-size:11px; } /* TABLES */ - table { border-collapse:collapse; border-color:#ccc; } td, th { font-size:11px; line-height:13px; border-bottom:1px solid #eee; vertical-align:top; padding:5px; font-family:"Lucida Grande", Verdana, Arial, sans-serif; } th { text-align:left; font-size:12px; } @@ -141,7 +101,6 @@ table#change-history { width:100%; } table#change-history tbody th { width:16em; } /* TABLE SORTING */ - thead th a:link, thead th a:visited { color:#666; display:block; } table thead th.sorted { background-position:bottom left !important; } table thead th.sorted a { padding-right:13px; } @@ -149,7 +108,6 @@ table thead th.ascending a { background:url(../img/admin/arrow-down.gif) right . table thead th.descending a { background:url(../img/admin/arrow-up.gif) right .4em no-repeat; } /* MODULES */ - .module { border:1px solid #ccc; margin-bottom:5px; background:white; } .module p, .module ul, .module h3, .module h4, .module dl, .module pre { padding-left:10px; padding-right:10px; } .module blockquote { margin-left:12px; } @@ -173,7 +131,6 @@ textarea { vertical-align:top !important; } input[type=checkbox], input[type=radio] { border:none; } /* FORM BUTTONS */ - input[type=submit], input[type=button], .submit-row input { background:white url(../img/admin/nav-bg.gif) bottom repeat-x; padding:3px; } input[type=submit]:active, input[type=button]:active { background-image:url(../img/admin/nav-bg-reverse.gif); background-position:top; } input[type=submit].default, .submit-row input.default { border:2px solid #5b80b2; background:#7CA0C7 url(../img/admin/default-bg.gif) bottom repeat-x; font-weight:bold; color:white; } @@ -183,7 +140,6 @@ input[type=submit].default:active { background-image:url(../img/admin/default-bg .submit-row .float-left { padding-top:.1em; } /* FORM ROWS */ - .form-row { clear:both; padding:8px 12px; font-size:11px; } html>body .form-row { border-bottom:1px solid #eee; } .form-row:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } @@ -191,21 +147,18 @@ html>body .form-row { border-bottom:1px solid #eee; } form .form-row p { padding-left:0; font-size:11px; } /* FORM LABELS */ - form h4 { margin:0 !important; padding:0 !important; border:none !important; } label { font-weight:normal !important; color:#666; font-size:12px; } label.inline { margin-left:20px; } .required label, label.required { font-weight:bold !important; color:#333 !important; } /* RADIO BUTTONS */ - form ul.radiolist li { list-style-type:none; } form ul.radiolist label { float:none; display:inline; } form ul.inline { margin-left:0; padding:0; } form ul.inline li { float:left; padding-right:7px; } /* ALIGNED FIELDSETS */ - .aligned label { display:block; padding:0 1em 3px 0; float:left; text-align:left; width:8em; } .aligned label.inline { display:inline; float:none; } .colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField { width:350px; } @@ -217,14 +170,12 @@ form .aligned p.help { padding-left:38px; } .checkbox-row p.help { margin-left:0; padding-left:0 !important; } /* WIDE FIELDSETS */ - .wide label { width:15em !important; } form .wide p { margin-left:15em; } form .wide p.help { padding-left:38px; } .colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField { width:450px; } /* COLLAPSED FIELDSETS */ - fieldset.collapsed * { display:none; } fieldset.collapsed h2, fieldset.collapsed { display:block !important; } fieldset.collapsed .collapse-toggle { display: inline !important; } @@ -233,7 +184,6 @@ fieldset.collapse h2 a.collapse-toggle:hover { text-decoration:underline; } .hidden { display:none; } /* MESSAGES & ERRORS */ - ul.messagelist { padding:0 0 5px 0; margin:0; } ul.messagelist li { font-size:12px; display:block; padding:4px 5px 4px 25px; margin:0 0 3px 0; border-bottom:1px solid #ddd; color:#666; background:#ffc url(../img/admin/icon_success.gif) 5px .3em no-repeat; } .errornote { font-size:12px !important; display:block; padding:4px 5px 4px 25px; margin:0 0 3px 0; border:1px solid red; color:red;background:#ffc url(../img/admin/icon_error.gif) 5px .3em no-repeat; } @@ -245,7 +195,6 @@ td ul.errorlist li { margin:0 !important; } .error input, .error select { border:1px solid red; } /* ACTION ICONS */ - .addlink { padding-left:12px; background:url(../img/admin/icon_addlink.gif) 0 .2em no-repeat; } .changelink { padding-left:12px; background:url(../img/admin/icon_changelink.gif) 0 .2em no-repeat; } .deletelink { padding-left:12px; background:url(../img/admin/icon_deletelink.gif) 0 50% no-repeat; } @@ -253,7 +202,6 @@ a.deletelink:link, a.deletelink:visited { color:#CC3434; } a.deletelink:hover { color:#993333; } /* OBJECT TOOLS */ - .object-tools { font-size:10px; font-weight:bold; font-family:Arial,Helvetica,sans-serif; padding-left:0; margin-bottom:5px; float:right; position:relative; margin-top:-2.4em; margin-bottom:-2em; } .form-row .object-tools { margin-top:0; margin-bottom:0; } .object-tools li { display:block; float:left; background:url(../img/admin/tool-left.gif) 0 0 no-repeat; padding:0 0 0 8px; margin-left:2px; height:16px; } @@ -266,17 +214,14 @@ a.deletelink:hover { color:#993333; } .object-tools a.addlink:hover { background:#5b80b2 url(../img/admin/tooltag-add_over.gif) top right no-repeat; } /* INLINE CONTROLS */ - #inline-controls { font-weight:bold; font-size:12px; } #inline-specific-controls { margin-left:6px; padding:0 8px; border-left:6px solid #ccc; } /* BREADCRUMBS */ - p.breadcrumbs { font-size:11px; color:#ccc;text-align:left; } /* old breadcrumbs style */ div.breadcrumbs { background:white url(../img/admin/nav-bg-reverse.gif) 0 -10px repeat-x; padding:2px 8px 3px 8px; font-size:11px; color:#999; border-top:1px solid white; border-bottom:1px solid #ccc; text-align:left; } /* SELECTOR (FILTER INTERFACE) */ - .selector { width:580px; float:left; } .selector select { width:270px; height:170px; } .selector-available, .selector-chosen { float:left; width:270px; text-align:center; margin-bottom:5px; } @@ -297,7 +242,6 @@ a.selector-chooseall { width:7em; background:url(../img/admin/selector-addall.gi a.selector-clearall { background:url(../img/admin/selector-removeall.gif) left center no-repeat; } /* Stacked selectors for long items */ - .stacked { float:left; width:500px; } .stacked select { width:480px; height:100px; } .stacked .selector-available, .stacked .selector-chosen { width:480px; } @@ -310,20 +254,17 @@ a.selector-clearall { background:url(../img/admin/selector-removeall.gif) left c .stacked .selector-remove { background-image:url(../img/admin/selector_stacked-remove.gif); } /* DATE AND TIME */ - p.datetime { line-height:20px; margin:0; padding:0; color:#666; font-size:11px; font-weight:bold; } .datetime span { font-size:11px; font-weight:normal; white-space:nowrap; } .vDateField { margin-left:4px; } table p.datetime { font-size:10px; margin-left:0; padding-left:0; } /* FILE UPLOADS */ - p.file-upload { line-height:20px; margin:0; padding:0; color:#666; font-size:11px; font-weight:bold; } .file-upload a { font-weight:normal; } .file-upload .deletelink { margin-left:5px; } /* CALENDARS & CLOCKS */ - .calendarbox, .clockbox { margin:5px auto; width: 10em; text-align: center; background:white; position:relative; } .clockbox { width:6em; } .calendar { margin:0; padding: 0; } @@ -350,14 +291,12 @@ ul.timelist, .timelist li { list-style-type:none; margin:0; padding:0; } .timelist a { padding:2px; } /* ORDERING WIDGET */ - ul#orderthese { padding:0; margin:0; list-style-type:none; } ul#orderthese li { list-style-type:none; display:block; padding:0; margin:6px 0; width:214px; background:#f6f6f6; white-space:nowrap; overflow:hidden; } ul#orderthese li span { display:block; border:1px solid #e7e7e7; background:transparent url(../img/admin/nav-bg-grabber.gif) top left repeat-y; font-size:10px !important; padding:4px 6px 4px 12px; } ul#orderthese span:hover { background-color:#efefef; } /* PAGINATOR */ - .paginator { font-size:11px; padding-top:10px; padding-bottom:10px; line-height:22px; margin:0; border-top:1px solid #ddd; } .paginator a:link, .paginator a:visited { padding:2px 6px; border:solid 1px #ccc; background:white; text-decoration:none; } .paginator a.showall { padding:0 !important; border:none !important; } @@ -367,7 +306,6 @@ ul#orderthese span:hover { background-color:#efefef; } .paginator a:hover { color:white; background:#5b80b2; border-color:#036; } /* TEXT STYLES & MODIFIERS */ - .small { font-size:11px; } .tiny { font-size:10px; } p.tiny { margin-top:-2px; } @@ -386,7 +324,6 @@ p img, h1 img, h2 img, h3 img, h4 img, td img { vertical-align:middle; } .nowrap { white-space:nowrap; } /* CUSTOM FORM FIELDS */ - .vSelectMultipleField { vertical-align:top !important; } .vCheckboxField { border:none; } .vDateField, .vTimeField { margin-right:2px; } From 91c67bcd45facfc55abc8e7afcfc4af2732bac7f Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Tue, 18 Oct 2005 04:21:07 +0000 Subject: [PATCH 091/117] Added django.contrib.admin, with a staff_member_required decorator and code from AdminUserRequired middleware. Refs #627 git-svn-id: http://code.djangoproject.com/svn/django/trunk@921 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/admin/__init__.py | 0 django/contrib/admin/views/__init__.py | 0 django/contrib/admin/views/decorators.py | 100 +++++++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 django/contrib/admin/__init__.py create mode 100644 django/contrib/admin/views/__init__.py create mode 100644 django/contrib/admin/views/decorators.py diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/admin/views/__init__.py b/django/contrib/admin/views/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/admin/views/decorators.py b/django/contrib/admin/views/decorators.py new file mode 100644 index 0000000000..44e1234ce4 --- /dev/null +++ b/django/contrib/admin/views/decorators.py @@ -0,0 +1,100 @@ +from django.core.extensions import DjangoContext, render_to_response +from django.conf.settings import SECRET_KEY +from django.models.auth import users +from django.utils import httpwrappers +import base64, md5 +import cPickle as pickle + +ERROR_MESSAGE = "Please enter a correct username and password. Note that both fields are case-sensitive." +LOGIN_FORM_KEY = 'this_is_the_login_form' + +def _display_login_form(request, error_message=''): + request.session.set_test_cookie() + if request.POST and request.POST.has_key('post_data'): + # User has failed login BUT has previously saved post data. + post_data = request.POST['post_data'] + elif request.POST: + # User's session must have expired; save their post data. + post_data = _encode_post_data(request.POST) + else: + post_data = _encode_post_data({}) + return render_to_response('admin/login', { + 'title': 'Log in', + 'app_path': request.path, + 'post_data': post_data, + 'error_message': error_message + }, context_instance=DjangoContext(request)) + +def _encode_post_data(post_data): + pickled = pickle.dumps(post_data) + pickled_md5 = md5.new(pickled + SECRET_KEY).hexdigest() + return base64.encodestring(pickled + pickled_md5) + +def _decode_post_data(encoded_data): + encoded_data = base64.decodestring(encoded_data) + pickled, tamper_check = encoded_data[:-32], encoded_data[-32:] + if md5.new(pickled + SECRET_KEY).hexdigest() != tamper_check: + from django.core.exceptions import SuspiciousOperation + raise SuspiciousOperation, "User may have tampered with session cookie." + return pickle.loads(pickled) + +def staff_member_required(view_func): + """ + Decorator for views that checks that the user is logged in and is a staff + member, displaying the login page if necessary. + """ + def _checklogin(request, *args, **kwargs): + if not request.user.is_anonymous() and request.user.is_staff: + # The user is valid. Continue to the admin page. + return view_func(request, *args, **kwargs) + + assert hasattr(request, 'session'), "The Django admin requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.middleware.sessions.SessionMiddleware'." + + # If this isn't already the login page, display it. + if not request.POST.has_key(LOGIN_FORM_KEY): + if request.POST: + message = "Please log in again, because your session has expired. "\ + "Don't worry: Your submission has been saved." + else: + message = "" + return _display_login_form(request, message) + + # Check that the user accepts cookies. + if not request.session.test_cookie_worked(): + message = "Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again." + return _display_login_form(request, message) + + # Check the password. + username = request.POST.get('username', '') + try: + user = users.get_object(username__exact=username, is_staff__exact=True) + except users.UserDoesNotExist: + message = ERROR_MESSAGE + if '@' in username: + # Mistakenly entered e-mail address instead of username? Look it up. + try: + user = users.get_object(email__exact=username) + except users.UserDoesNotExist: + message = "Usernames cannot contain the '@' character." + else: + message = "Your e-mail address is not your username. Try '%s' instead." % user.username + return _display_login_form(request, message) + + # The user data is correct; log in the user in and continue. + else: + if user.check_password(request.POST.get('password', '')): + request.session[users.SESSION_KEY] = user.id + if request.POST.has_key('post_data'): + post_data = _decode_post_data(request.POST['post_data']) + if post_data and not post_data.has_key(LOGIN_FORM_KEY): + # overwrite request.POST with the saved post_data, and continue + request.POST = post_data + request.user = user + return view_func(request, *args, **kwargs) + else: + request.session.delete_test_cookie() + return httpwrappers.HttpResponseRedirect(request.path) + else: + return _display_login_form(request, ERROR_MESSAGE) + + return _checklogin From 1dc6d4b26506fe3d8c5e72aa958b8e82ea9975be Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Tue, 18 Oct 2005 04:36:28 +0000 Subject: [PATCH 092/117] Changed django.views.admin.doc to use template.loader instead of template_loader git-svn-id: http://code.djangoproject.com/svn/django/trunk@922 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/views/admin/doc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/django/views/admin/doc.py b/django/views/admin/doc.py index 64785385c4..e7e791b2e4 100644 --- a/django/views/admin/doc.py +++ b/django/views/admin/doc.py @@ -4,8 +4,8 @@ from django.conf import settings from django.models.core import sites from django.core.extensions import DjangoContext, render_to_response from django.core.exceptions import Http404, ViewDoesNotExist -from django.core import template, template_loader, urlresolvers -from django.core.template import defaulttags, defaultfilters +from django.core import template, urlresolvers +from django.core.template import defaulttags, defaultfilters, loader try: from django.parts.admin import doc except ImportError: @@ -223,7 +223,7 @@ def load_all_installed_template_libraries(): # Clear out and reload default tags template.registered_tags.clear() reload(defaulttags) - reload(template_loader) # template_loader defines the block/extends tags + reload(loader) # loader defines the block/extends tags # Load any template tag libraries from installed apps for e in templatetags.__path__: From aed1930133ad4e7970c8a7909283a59fb2e3fef2 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Tue, 18 Oct 2005 04:40:47 +0000 Subject: [PATCH 093/117] Moved django.views.admin.template and django.views.admin.doc to django.contrib.admin.views. Refs #627 git-svn-id: http://code.djangoproject.com/svn/django/trunk@923 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/urls/admin.py | 22 +++++++++---------- .../admin => contrib/admin/views}/doc.py | 0 .../admin => contrib/admin/views}/template.py | 0 3 files changed, 11 insertions(+), 11 deletions(-) rename django/{views/admin => contrib/admin/views}/doc.py (100%) rename django/{views/admin => contrib/admin/views}/template.py (100%) diff --git a/django/conf/urls/admin.py b/django/conf/urls/admin.py index 841d50523e..b31f4de2c9 100644 --- a/django/conf/urls/admin.py +++ b/django/conf/urls/admin.py @@ -6,20 +6,20 @@ urlpatterns = ( ('^logout/$', 'django.views.auth.login.logout'), ('^password_change/$', 'django.views.registration.passwords.password_change'), ('^password_change/done/$', 'django.views.registration.passwords.password_change_done'), - ('^template_validator/$', 'django.views.admin.template.template_validator'), + ('^template_validator/$', 'django.contrib.admin.views.template.template_validator'), # Documentation - ('^doc/$', 'django.views.admin.doc.doc_index'), - ('^doc/bookmarklets/$', 'django.views.admin.doc.bookmarklets'), - ('^doc/tags/$', 'django.views.admin.doc.template_tag_index'), - ('^doc/filters/$', 'django.views.admin.doc.template_filter_index'), - ('^doc/views/$', 'django.views.admin.doc.view_index'), - ('^doc/views/jump/$', 'django.views.admin.doc.jump_to_view'), - ('^doc/views/(?P[^/]+)/$', 'django.views.admin.doc.view_detail'), - ('^doc/models/$', 'django.views.admin.doc.model_index'), - ('^doc/models/(?P[^/]+)/$', 'django.views.admin.doc.model_detail'), + ('^doc/$', 'django.contrib.admin.views.doc.doc_index'), + ('^doc/bookmarklets/$', 'django.contrib.admin.views.doc.bookmarklets'), + ('^doc/tags/$', 'django.contrib.admin.views.doc.template_tag_index'), + ('^doc/filters/$', 'django.contrib.admin.views.doc.template_filter_index'), + ('^doc/views/$', 'django.contrib.admin.views.doc.view_index'), + ('^doc/views/jump/$', 'django.contrib.admin.views.doc.jump_to_view'), + ('^doc/views/(?P[^/]+)/$', 'django.contrib.admin.views.doc.view_detail'), + ('^doc/models/$', 'django.contrib.admin.views.doc.model_index'), + ('^doc/models/(?P[^/]+)/$', 'django.contrib.admin.views.doc.model_detail'), # ('^doc/templates/$', 'django.views.admin.doc.template_index'), - ('^doc/templates/(?P