From 3dd1d28c7fd172f8aefeca1831c89ba2d85ee48f Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Thu, 29 Sep 2005 19:56:17 +0000 Subject: [PATCH 01/55] 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 02/55] 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 03/55] 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 04/55] 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 05/55] 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 06/55] 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 07/55] 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 08/55] 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 09/55] 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 10/55] 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 11/55] 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 12/55] 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 13/55] 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 14/55] 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 15/55] 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 16/55] 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 17/55] 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 18/55] 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 19/55] 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 20/55] 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 21/55] 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 22/55] 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 23/55] 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 24/55] 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 25/55] 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 26/55] 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 27/55] 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 28/55] 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 29/55] 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 30/55] 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 31/55] 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 32/55] 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 33/55] 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 34/55] 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 35/55] 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 36/55] 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 37/55] 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 38/55] 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 39/55] 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 40/55] 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 41/55] 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 42/55] 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 43/55] 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 44/55] 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 45/55] 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 46/55] 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 47/55] 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 48/55] 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 49/55] 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 50/55] 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 51/55] 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 52/55] 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 53/55] 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 54/55] 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 55/55] 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.