diff options
| -rwxr-xr-x | django/bin/django-admin.py | 4 | ||||
| -rw-r--r-- | django/conf/global_settings.py | 3 | ||||
| -rw-r--r-- | django/core/db/backends/mysql.py | 28 | ||||
| -rw-r--r-- | django/core/management.py | 12 | ||||
| -rw-r--r-- | django/core/meta/__init__.py | 13 | ||||
| -rw-r--r-- | django/core/meta/fields.py | 6 | ||||
| -rw-r--r-- | django/middleware/cache.py | 116 | ||||
| -rw-r--r-- | django/middleware/gzip.py | 24 | ||||
| -rw-r--r-- | django/middleware/http.py | 37 | ||||
| -rw-r--r-- | django/middleware/sessions.py | 2 | ||||
| -rw-r--r-- | django/utils/cache.py | 121 | ||||
| -rw-r--r-- | django/utils/decorators.py | 22 | ||||
| -rw-r--r-- | django/views/admin/main.py | 2 | ||||
| -rw-r--r-- | django/views/decorators/cache.py | 70 | ||||
| -rw-r--r-- | django/views/decorators/gzip.py | 6 | ||||
| -rw-r--r-- | django/views/decorators/http.py | 9 | ||||
| -rw-r--r-- | django/views/decorators/vary.py | 35 | ||||
| -rw-r--r-- | docs/cache.txt | 174 | ||||
| -rw-r--r-- | docs/django-admin.txt | 17 | ||||
| -rw-r--r-- | docs/middleware.txt | 98 | ||||
| -rwxr-xr-x | tests/runtests.py | 4 |
21 files changed, 596 insertions, 207 deletions
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/django/conf/global_settings.py b/django/conf/global_settings.py index 6278ae6656..43869e0f1e 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -136,6 +136,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", ) @@ -155,6 +157,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 # 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): 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." 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 diff --git a/django/core/meta/fields.py b/django/core/meta/fields.py index 3e5cca169d..376595230c 100644 --- a/django/core/meta/fields.py +++ b/django/core/meta/fields.py @@ -596,7 +596,11 @@ class ForeignKey(Field): Field.__init__(self, **kwargs) def get_manipulator_field_objs(self): - return [formfields.IntegerField] + 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] class ManyToManyField(Field): def __init__(self, to, **kwargs): 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/utils/cache.py b/django/utils/cache.py new file mode 100644 index 0000000000..fcd0825a22 --- /dev/null +++ b/django/utils/cache.py @@ -0,0 +1,121 @@ +""" +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: + + 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 _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) 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 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('<h3>By %s:</h3><ul>\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('<li%s><a href="%s">%s</a></li>\n' % \ (((lookup_val == v and not lookup_val2) and ' class="selected"' or ''), get_query_string(params, {lookup_kwarg: v}, [lookup_kwarg2]), k)) 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/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 diff --git a/docs/cache.txt b/docs/cache.txt index 0a7ee1c25a..f15da2660b 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. + +See the `middleware documentation`_ for more on middleware. -It doesn't matter where in the middleware stack you put the cache 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/django-admin.txt b/docs/django-admin.txt index b4b07f4f12..ba9bb1403f 100644 --- a/docs/django-admin.txt +++ b/docs/django-admin.txt @@ -183,7 +183,7 @@ Available options ================= --settings -========== +---------- Example usage:: @@ -193,8 +193,21 @@ 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 `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 -====== +------ Displays a help message that includes a terse list of all available actions and options. diff --git a/docs/middleware.txt b/docs/middleware.txt index f3901bb693..dfa1947bbd 100644 --- a/docs/middleware.txt +++ b/docs/middleware.txt @@ -45,53 +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.sessions.SessionMiddleware`` - Enables session support. See the `session documentation`_. +* 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. - .. _`session documentation`: http://www.djangoproject.com/documentation/sessions/ +* 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. + +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. + +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 =========================== 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: |
