diff --git a/README b/README index d52451d3bade..830a873a83d3 100644 --- a/README +++ b/README @@ -1,37 +1,49 @@ -Django is a high-level Python Web framework that encourages rapid development -and clean, pragmatic design. +Django is a high-level Python Web framework that encourages rapid +development and clean, pragmatic design. -All documentation is in the "docs" directory and online at -http://www.djangoproject.com/documentation/. If you're just getting started, -here's how we recommend you read the docs: - * First, read docs/install.txt for instructions on installing Django. +About this version +================== - * Next, work through the tutorials in order (docs/tutorial01.txt, - docs/tutorial02.txt, etc.). +This is the Django 0.91 "bugfixes" branch, which is intended to +provide bugfix and patch support for users of Django 0.91 who have not +been able to migrate to a more recent version. No new features will be +added in this branch, and it is maintained solely as a means of +providing support to legacy Django installations. - * If you want to set up an actual deployment server, read docs/modpython.txt - for instructions on running Django under mod_python. +If you're completely new to Django we highly recommend that you use +either the latest stable release or a Subversion checkout from +Django's trunk; Django is always evolving, and the latest and greatest +features are only available to users of newer versions of the +framework. - * The rest of the documentation is of the reference-manual variety. - Read it -- and the FAQ -- as you run into problems. -Docs are updated rigorously. If you find any problems in the docs, or think they -should be clarified in any way, please take 30 seconds to fill out a ticket -here: +More information +================ -http://code.djangoproject.com/newticket +The complete history of bugs fixed in this branch can be viewed online +at http://code.djangoproject.com/log/django/branches/0.91-bugfixes. -To get more help: +We also recommend that users of this branch subscribe to the +"django-announce" mailing list, a low-traffic, announcements-only list +which will send messages whenever an important (i.e., +security-related) bug is fixed. You can subscribe to the list via +Google Groups at http://groups.google.com/group/django-announce. - * Join the #django channel on irc.freenode.net. Lots of helpful people - hang out there. Read the archives at http://loglibrary.com/179 . +The documentation for this version of Django has been frozen, and is +available online at http://www.djangoproject.com/documentation/0_91/. - * Join the django-users mailing list, or read the archives, at - http://groups-beta.google.com/group/django-users. -To contribute to Django: +Submitting bugs +=============== - * Check out http://www.djangoproject.com/community/ for information - about getting involved. +If you run into a bug in Django 0.91, please search the Django ticket +database to see if the issue has already been reported; if not, please +head over to http://code.djangoproject.com/newticket and file a new +ticket with as much information about the bug as you can provide. + +If you're running into a bug which has been reported but not fixed, +feel free to update the ticket with any additional information you +have, and to assign it to 'ubernostrum' (AKA James Bennett, the +maintainer of this branch). diff --git a/django/__init__.py b/django/__init__.py index 593e2f46e474..27f57db7975d 100644 --- a/django/__init__.py +++ b/django/__init__.py @@ -1 +1 @@ -VERSION = (0, 9, 1, 'SVN') +VERSION = (0, 91, 3, 'SVN') diff --git a/django/bin/compile-messages.py b/django/bin/compile-messages.py index e33fdd780b99..579f74364c91 100755 --- a/django/bin/compile-messages.py +++ b/django/bin/compile-messages.py @@ -20,7 +20,14 @@ def compile_messages(): if f.endswith('.po'): sys.stderr.write('processing file %s in %s\n' % (f, dirpath)) pf = os.path.splitext(os.path.join(dirpath, f))[0] - cmd = 'msgfmt -o "%s.mo" "%s.po"' % (pf, pf) + # Store the names of the .mo and .po files in an environment + # variable, rather than doing a string replacement into the + # command, so that we can take advantage of shell quoting, to + # quote any malicious characters/escaping. + # See http://cyberelk.net/tim/articles/cmdline/ar01s02.html + os.environ['djangocompilemo'] = pf + '.mo' + os.environ['djangocompilepo'] = pf + '.po' + cmd = 'msgfmt -o "$djangocompilemo" "$djangocompilepo"' os.system(cmd) if __name__ == "__main__": diff --git a/django/contrib/admin/media/js/admin/RelatedObjectLookups.js b/django/contrib/admin/media/js/admin/RelatedObjectLookups.js index 72c4e95ecab2..4ede09b54d2d 100644 --- a/django/contrib/admin/media/js/admin/RelatedObjectLookups.js +++ b/django/contrib/admin/media/js/admin/RelatedObjectLookups.js @@ -1,8 +1,35 @@ // Handles related-objects functionality: lookup link for raw_id_admin=True // and Add Another links. +function html_unescape(text) { + // Unescape a string that was escaped using django.utils.html.escape. + text = text.replace(/</g, '<'); + text = text.replace(/>/g, '>'); + text = text.replace(/"/g, '"'); + text = text.replace(/'/g, "'"); + text = text.replace(/&/g, '&'); + return text; +} + +// IE doesn't accept periods or dashes in the window name, but the element IDs +// we use to generate popup window names may contain them, therefore we map them +// to allowed characters in a reversible way so that we can locate the correct +// element when the popup window is dismissed. +function id_to_windowname(text) { + text = text.replace(/\./g, '__dot__'); + text = text.replace(/\-/g, '__dash__'); + return text; +} + +function windowname_to_id(text) { + text = text.replace(/__dot__/g, '.'); + text = text.replace(/__dash__/g, '-'); + return text; +} + function showRelatedObjectLookupPopup(triggeringLink) { var name = triggeringLink.id.replace(/^lookup_/, ''); + name = id_to_windowname(name); var href; if (triggeringLink.href.search(/\?/) >= 0) { href = triggeringLink.href + '&pop=1'; @@ -15,25 +42,36 @@ function showRelatedObjectLookupPopup(triggeringLink) { } function dismissRelatedLookupPopup(win, chosenId) { - var elem = document.getElementById(win.name); + var name = windowname_to_id(win.name); + var elem = document.getElementById(name); if (elem.className.indexOf('vRawIdAdminField') != -1 && elem.value) { elem.value += ',' + chosenId; } else { - document.getElementById(win.name).value = chosenId; + document.getElementById(name).value = chosenId; } win.close(); } function showAddAnotherPopup(triggeringLink) { var name = triggeringLink.id.replace(/^add_/, ''); - name = name.replace(/\./g, '___'); - var win = window.open(triggeringLink.href + '?_popup=1', name, 'height=500,width=800,resizable=yes,scrollbars=yes'); + name = id_to_windowname(name); + href = triggeringLink.href + if (href.indexOf('?') == -1) { + href += '?_popup=1'; + } else { + href += '&_popup=1'; + } + var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes'); win.focus(); return false; } function dismissAddAnotherPopup(win, newId, newRepr) { - var name = win.name.replace(/___/g, '.'); + // newId and newRepr are expected to have previously been escaped by + // django.utils.html.escape. + newId = html_unescape(newId); + newRepr = html_unescape(newRepr); + var name = windowname_to_id(win.name); var elem = document.getElementById(name); if (elem) { if (elem.nodeName == 'SELECT') { diff --git a/django/contrib/admin/templates/admin/login.html b/django/contrib/admin/templates/admin/login.html index ea823e10200a..2445b7957356 100644 --- a/django/contrib/admin/templates/admin/login.html +++ b/django/contrib/admin/templates/admin/login.html @@ -17,7 +17,6 @@

-{% comment %} {% trans 'Have you forgotten your password?' %}{% endcomment %}

diff --git a/django/contrib/admin/views/decorators.py b/django/contrib/admin/views/decorators.py index 5ddc17fa85f0..9b865400fc0c 100644 --- a/django/contrib/admin/views/decorators.py +++ b/django/contrib/admin/views/decorators.py @@ -2,43 +2,21 @@ from django.conf.settings import SECRET_KEY from django.models.auth import users from django.utils import httpwrappers +from django.utils.html import escape from django.utils.translation import gettext_lazy -import base64, datetime, md5 -import cPickle as pickle +import base64, datetime ERROR_MESSAGE = gettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.") LOGIN_FORM_KEY = 'this_is_the_login_form' def _display_login_form(request, error_message=''): request.session.set_test_cookie() - if request.POST and request.POST.has_key('post_data'): - # User has failed login BUT has previously saved post data. - post_data = request.POST['post_data'] - elif request.POST: - # User's session must have expired; save their post data. - post_data = _encode_post_data(request.POST) - else: - post_data = _encode_post_data({}) return render_to_response('admin/login', { 'title': _('Log in'), - 'app_path': request.path, - 'post_data': post_data, + 'app_path': escape(request.path), 'error_message': error_message }, context_instance=DjangoContext(request)) -def _encode_post_data(post_data): - pickled = pickle.dumps(post_data) - pickled_md5 = md5.new(pickled + SECRET_KEY).hexdigest() - return base64.encodestring(pickled + pickled_md5) - -def _decode_post_data(encoded_data): - encoded_data = base64.decodestring(encoded_data) - pickled, tamper_check = encoded_data[:-32], encoded_data[-32:] - if md5.new(pickled + SECRET_KEY).hexdigest() != tamper_check: - from django.core.exceptions import SuspiciousOperation - raise SuspiciousOperation, "User may have tampered with session cookie." - return pickle.loads(pickled) - def staff_member_required(view_func): """ Decorator for views that checks that the user is logged in and is a staff @@ -47,10 +25,6 @@ def staff_member_required(view_func): def _checklogin(request, *args, **kwargs): if not request.user.is_anonymous() and request.user.is_staff: # The user is valid. Continue to the admin page. - if request.POST.has_key('post_data'): - # User must have re-authenticated through a different window - # or tab. - request.POST = _decode_post_data(request.POST['post_data']) return view_func(request, *args, **kwargs) assert hasattr(request, 'session'), "The Django admin requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.middleware.sessions.SessionMiddleware'." @@ -58,7 +32,7 @@ def _checklogin(request, *args, **kwargs): # If this isn't already the login page, display it. if not request.POST.has_key(LOGIN_FORM_KEY): if request.POST: - message = _("Please log in again, because your session has expired. Don't worry: Your submission has been saved.") + message = _("Please log in again, because your session has expired.") else: message = "" return _display_login_form(request, message) @@ -90,16 +64,7 @@ def _checklogin(request, *args, **kwargs): request.session[users.SESSION_KEY] = user.id user.last_login = datetime.datetime.now() user.save() - if request.POST.has_key('post_data'): - post_data = _decode_post_data(request.POST['post_data']) - if post_data and not post_data.has_key(LOGIN_FORM_KEY): - # overwrite request.POST with the saved post_data, and continue - request.POST = post_data - request.user = user - return view_func(request, *args, **kwargs) - else: - request.session.delete_test_cookie() - return httpwrappers.HttpResponseRedirect(request.path) + return httpwrappers.HttpResponseRedirect(request.path) else: return _display_login_form(request, ERROR_MESSAGE) diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 3d92a7d94992..3974f817c3f8 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -97,8 +97,16 @@ def get_modules_and_options(self, app_label, module_name, request): self.mod, self.opts = _get_mod_opts(app_label, module_name) if not request.user.has_perm(app_label + '.' + self.opts.get_change_permission()): raise PermissionDenied - - self.lookup_mod, self.lookup_opts = self.mod, self.opts + + lookup_mod, lookup_opts = self.mod, self.opts + if self.opts.one_to_one_field: + lookup_mod = self.opts.one_to_one_field.rel.to.get_model_module() + lookup_opts = lookup_mod.Klass._meta + # If lookup_opts doesn't have admin set, give it the default meta.Admin(). + if not lookup_opts.admin: + lookup_opts.admin = meta.Admin() + + self.lookup_mod, self.lookup_opts = lookup_mod, lookup_opts def get_search_parameters(self, request): # Get search parameters from the query string. diff --git a/django/contrib/comments/feeds.py b/django/contrib/comments/feeds.py index dd6c6ecf15af..cd38bd456712 100644 --- a/django/contrib/comments/feeds.py +++ b/django/contrib/comments/feeds.py @@ -43,6 +43,6 @@ def _get_lookup_kwargs(self): kwargs = LatestFreeCommentsFeed._get_lookup_kwargs(self) kwargs['is_removed__exact'] = False if settings.COMMENTS_BANNED_USERS_GROUP: - kwargs['where'] = ['user_id NOT IN (SELECT user_id FROM auth_users_group WHERE group_id = %s)'] - kwargs['params'] = [COMMENTS_BANNED_USERS_GROUP] - return kwargs \ No newline at end of file + kwargs['where'] = ['user_id NOT IN (SELECT user_id FROM auth_users_groups WHERE group_id = %s)'] + kwargs['params'] = [settings.COMMENTS_BANNED_USERS_GROUP] + return kwargs diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py index 5918db7dc83f..d6055a282ce6 100644 --- a/django/contrib/comments/views/comments.py +++ b/django/contrib/comments/views/comments.py @@ -107,7 +107,7 @@ def save(self, new_data): # send the comment to the managers. if self.user_cache.get_comments_comment_count() <= COMMENTS_FIRST_FEW: message = ngettext('This comment was posted by a user who has posted fewer than %(count)s comment:\n\n%(text)s', - 'This comment was posted by a user who has posted fewer than %(count)s comments:\n\n%(text)s') % \ + 'This comment was posted by a user who has posted fewer than %(count)s comments:\n\n%(text)s', COMMENTS_FIRST_FEW) % \ {'count': COMMENTS_FIRST_FEW, 'text': c.get_as_text()} mail_managers("Comment posted by rookie user", message) if COMMENTS_SKETCHY_USERS_GROUP and COMMENTS_SKETCHY_USERS_GROUP in [g.id for g in self.user_cache.get_group_list()]: diff --git a/django/core/db/backends/postgresql.py b/django/core/db/backends/postgresql.py index 0bc799c24796..243ab8eb58ff 100644 --- a/django/core/db/backends/postgresql.py +++ b/django/core/db/backends/postgresql.py @@ -16,13 +16,54 @@ # Import copy of _thread_local.py from python 2.4 from django.utils._threading_local import local +def smart_basestring(s, charset): + if isinstance(s, unicode): + return s.encode(charset) + return s + +class UnicodeCursorWrapper(object): + """ + A thin wrapper around psycopg cursors that allows them to accept Unicode + strings as params. + + This is necessary because psycopg doesn't apply any DB quoting to + parameters that are Unicode strings. If a param is Unicode, this will + convert it to a bytestring using DEFAULT_CHARSET before passing it to + psycopg. + """ + def __init__(self, cursor, charset): + self.cursor = cursor + self.charset = charset + + def execute(self, sql, params=()): + try: + params = dict([(k, smart_basestring(v, self.charset)) for (k, v) in params.items()]) + except AttributeError: + params = [smart_basestring(p, self.charset) for p in params] + return self.cursor.execute(sql, params) + + def executemany(self, sql, param_list): + try: + new_param_list = [dict([(k, smart_basestring(v, self.charset)) for (k, v) in params.items()]) + for params in param_list] + except AttributeError: + new_param_list = [tuple([smart_basestring(p, self.charset) for p in params]) + for params in param_list] + return self.cursor.executemany(sql, new_param_list) + + def __getattr__(self, attr): + if self.__dict__.has_key(attr): + return self.__dict__[attr] + else: + return getattr(self.cursor, attr) + class DatabaseWrapper(local): def __init__(self): self.connection = None self.queries = [] def cursor(self): - from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PORT, DATABASE_PASSWORD, DEBUG, TIME_ZONE + from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PORT, DATABASE_PASSWORD, DEBUG, DEFAULT_CHARSET, TIME_ZONE if self.connection is None: if DATABASE_NAME == '': from django.core.exceptions import ImproperlyConfigured @@ -40,6 +81,7 @@ def cursor(self): self.connection.set_isolation_level(1) # make transactions transparent to all cursors cursor = self.connection.cursor() cursor.execute("SET TIME ZONE %s", [TIME_ZONE]) + cursor = UnicodeCursorWrapper(cursor, DEFAULT_CHARSET) if DEBUG: return base.CursorDebugWrapper(cursor, self) return cursor diff --git a/django/core/formfields.py b/django/core/formfields.py index 167439cc0718..a25bdc16b207 100644 --- a/django/core/formfields.py +++ b/django/core/formfields.py @@ -325,7 +325,8 @@ def get_id(self): class TextField(FormField): input_type = "text" - def __init__(self, field_name, length=30, maxlength=None, is_required=False, validator_list=[], member_name=None): + def __init__(self, field_name, length=30, maxlength=None, is_required=False, validator_list=None, member_name=None): + if validator_list is None: validator_list = [] self.field_name = field_name self.length, self.maxlength = length, maxlength self.is_required = is_required @@ -362,7 +363,8 @@ class PasswordField(TextField): input_type = "password" class LargeTextField(TextField): - def __init__(self, field_name, rows=10, cols=40, is_required=False, validator_list=[], maxlength=None): + def __init__(self, field_name, rows=10, cols=40, is_required=False, validator_list=None, maxlength=None): + if validator_list is None: validator_list = [] self.field_name = field_name self.rows, self.cols, self.is_required = rows, cols, is_required self.validator_list = validator_list[:] @@ -380,7 +382,8 @@ def render(self, data): self.field_name, self.rows, self.cols, escape(data)) class HiddenField(FormField): - def __init__(self, field_name, is_required=False, validator_list=[]): + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] self.field_name, self.is_required = field_name, is_required self.validator_list = validator_list[:] @@ -410,7 +413,8 @@ def html2python(data): html2python = staticmethod(html2python) class SelectField(FormField): - def __init__(self, field_name, choices=[], size=1, is_required=False, validator_list=[], member_name=None): + def __init__(self, field_name, choices=[], size=1, is_required=False, validator_list=None, member_name=None): + if validator_list is None: validator_list = [] self.field_name = field_name # choices is a list of (value, human-readable key) tuples because order matters self.choices, self.size, self.is_required = choices, size, is_required @@ -446,7 +450,8 @@ def html2python(data): html2python = staticmethod(html2python) class RadioSelectField(FormField): - def __init__(self, field_name, choices=[], ul_class='', is_required=False, validator_list=[], member_name=None): + def __init__(self, field_name, choices=[], ul_class='', is_required=False, validator_list=None, member_name=None): + if validator_list is None: validator_list = [] self.field_name = field_name # choices is a list of (value, human-readable key) tuples because order matters self.choices, self.is_required = choices, is_required @@ -510,7 +515,8 @@ def isValidChoice(self, data, form): class NullBooleanField(SelectField): "This SelectField provides 'Yes', 'No' and 'Unknown', mapping results to True, False or None" - def __init__(self, field_name, is_required=False, validator_list=[]): + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] SelectField.__init__(self, field_name, choices=[('1', 'Unknown'), ('2', 'Yes'), ('3', 'No')], is_required=is_required, validator_list=validator_list) @@ -563,7 +569,8 @@ class CheckboxSelectMultipleField(SelectMultipleField): back into the single list that validators, renderers and save() expect. """ requires_data_list = True - def __init__(self, field_name, choices=[], validator_list=[]): + def __init__(self, field_name, choices=[], validator_list=None): + if validator_list is None: validator_list = [] SelectMultipleField.__init__(self, field_name, choices, size=1, is_required=False, validator_list=validator_list) def prepare(self, new_data): @@ -594,7 +601,8 @@ def render(self, data): #################### class FileUploadField(FormField): - def __init__(self, field_name, is_required=False, validator_list=[]): + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] self.field_name, self.is_required = field_name, is_required self.validator_list = [self.isNonEmptyFile] + validator_list @@ -629,7 +637,8 @@ def isValidImage(self, field_data, all_data): #################### class IntegerField(TextField): - def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=[], member_name=None): + def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=None, member_name=None): + if validator_list is None: validator_list = [] validator_list = [self.isInteger] + validator_list if member_name is not None: self.member_name = member_name @@ -648,7 +657,8 @@ def html2python(data): html2python = staticmethod(html2python) class SmallIntegerField(IntegerField): - def __init__(self, field_name, length=5, maxlength=5, is_required=False, validator_list=[]): + def __init__(self, field_name, length=5, maxlength=5, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] validator_list = [self.isSmallInteger] + validator_list IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list) @@ -657,7 +667,8 @@ def isSmallInteger(self, field_data, all_data): raise validators.CriticalValidationError, _("Enter a whole number between -32,768 and 32,767.") class PositiveIntegerField(IntegerField): - def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=[]): + def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] validator_list = [self.isPositive] + validator_list IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list) @@ -666,7 +677,8 @@ def isPositive(self, field_data, all_data): raise validators.CriticalValidationError, _("Enter a positive number.") class PositiveSmallIntegerField(IntegerField): - def __init__(self, field_name, length=5, maxlength=None, is_required=False, validator_list=[]): + def __init__(self, field_name, length=5, maxlength=None, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] validator_list = [self.isPositiveSmall] + validator_list IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list) @@ -675,7 +687,8 @@ def isPositiveSmall(self, field_data, all_data): raise validators.CriticalValidationError, _("Enter a whole number between 0 and 32,767.") class FloatField(TextField): - def __init__(self, field_name, max_digits, decimal_places, is_required=False, validator_list=[]): + def __init__(self, field_name, max_digits, decimal_places, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] self.max_digits, self.decimal_places = max_digits, decimal_places validator_list = [self.isValidFloat] + validator_list TextField.__init__(self, field_name, max_digits+1, max_digits+1, is_required, validator_list) @@ -700,7 +713,8 @@ def html2python(data): class DatetimeField(TextField): """A FormField that automatically converts its data to a datetime.datetime object. The data should be in the format YYYY-MM-DD HH:MM:SS.""" - def __init__(self, field_name, length=30, maxlength=None, is_required=False, validator_list=[]): + def __init__(self, field_name, length=30, maxlength=None, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] self.field_name = field_name self.length, self.maxlength = length, maxlength self.is_required = is_required @@ -723,7 +737,8 @@ def html2python(data): class DateField(TextField): """A FormField that automatically converts its data to a datetime.date object. The data should be in the format YYYY-MM-DD.""" - def __init__(self, field_name, is_required=False, validator_list=[]): + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] validator_list = [self.isValidDate] + validator_list TextField.__init__(self, field_name, length=10, maxlength=10, is_required=is_required, validator_list=validator_list) @@ -747,7 +762,8 @@ def html2python(data): class TimeField(TextField): """A FormField that automatically converts its data to a datetime.time object. The data should be in the format HH:MM:SS or HH:MM:SS.mmmmmm.""" - def __init__(self, field_name, is_required=False, validator_list=[]): + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] validator_list = [self.isValidTime] + validator_list TextField.__init__(self, field_name, length=8, maxlength=8, is_required=is_required, validator_list=validator_list) @@ -781,7 +797,8 @@ def html2python(data): class EmailField(TextField): "A convenience FormField for validating e-mail addresses" - def __init__(self, field_name, length=50, maxlength=75, is_required=False, validator_list=[]): + def __init__(self, field_name, length=50, maxlength=75, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] validator_list = [self.isValidEmail] + validator_list TextField.__init__(self, field_name, length, maxlength=maxlength, is_required=is_required, validator_list=validator_list) @@ -794,7 +811,8 @@ def isValidEmail(self, field_data, all_data): class URLField(TextField): "A convenience FormField for validating URLs" - def __init__(self, field_name, length=50, is_required=False, validator_list=[]): + def __init__(self, field_name, length=50, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] validator_list = [self.isValidURL] + validator_list TextField.__init__(self, field_name, length=length, maxlength=200, is_required=is_required, validator_list=validator_list) @@ -806,7 +824,8 @@ def isValidURL(self, field_data, all_data): raise validators.CriticalValidationError, e.messages class IPAddressField(TextField): - def __init__(self, field_name, length=15, maxlength=15, is_required=False, validator_list=[]): + def __init__(self, field_name, length=15, maxlength=15, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] validator_list = [self.isValidIPAddress] + validator_list TextField.__init__(self, field_name, length=length, maxlength=maxlength, is_required=is_required, validator_list=validator_list) @@ -827,7 +846,8 @@ def html2python(data): 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=[]): + def __init__(self, field_name, path, match=None, recursive=False, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] import os if match is not None: import re @@ -850,7 +870,8 @@ def __init__(self, field_name, path, match=None, recursive=False, is_required=Fa 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=[]): + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] validator_list = [self.isValidPhone] + validator_list TextField.__init__(self, field_name, length=12, maxlength=12, is_required=is_required, validator_list=validator_list) @@ -863,7 +884,8 @@ def isValidPhone(self, field_data, all_data): class USStateField(TextField): "A convenience FormField for validating U.S. states (e.g. 'IL')" - def __init__(self, field_name, is_required=False, validator_list=[]): + def __init__(self, field_name, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] validator_list = [self.isValidUSState] + validator_list TextField.__init__(self, field_name, length=2, maxlength=2, is_required=is_required, validator_list=validator_list) @@ -875,15 +897,13 @@ def isValidUSState(self, field_data, all_data): raise validators.CriticalValidationError, e.messages def html2python(data): - if data: - return data.upper() # Should always be stored in upper case - else: - return None + return data.upper() # Should always be stored in upper case html2python = staticmethod(html2python) class CommaSeparatedIntegerField(TextField): "A convenience FormField for validating comma-separated integer fields" - def __init__(self, field_name, maxlength=None, is_required=False, validator_list=[]): + def __init__(self, field_name, maxlength=None, is_required=False, validator_list=None): + if validator_list is None: validator_list = [] validator_list = [self.isCommaSeparatedIntegerList] + validator_list TextField.__init__(self, field_name, length=20, maxlength=maxlength, is_required=is_required, validator_list=validator_list) diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index fd3a3ccae124..9a4bb826a436 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -55,14 +55,14 @@ def get_response(self, path, request): # Reset query list per request. db.db.queries = [] - # Apply request middleware - for middleware_method in self._request_middleware: - response = middleware_method(request) - if response: - return response - resolver = urlresolvers.RegexURLResolver(r'^/', ROOT_URLCONF) try: + # Apply request middleware + for middleware_method in self._request_middleware: + response = middleware_method(request) + if response: + return response + callback, callback_args, callback_kwargs = resolver.resolve(path) # Apply view middleware diff --git a/django/core/handlers/modpython.py b/django/core/handlers/modpython.py index 37499c6f0651..34c21c52df59 100644 --- a/django/core/handlers/modpython.py +++ b/django/core/handlers/modpython.py @@ -13,9 +13,30 @@ def __init__(self, req): self.path = req.uri def __repr__(self): + # Since this is called as part of error handling, we need to be very + # robust against potentially malformed input. + try: + get = pformat(self.GET) + except: + get = '' + try: + post = pformat(self.POST) + except: + post = '' + try: + cookies = pformat(self.COOKIES) + except: + cookies = '' + try: + meta = pformat(self.META) + except: + meta = '' + try: + user = self.user + except: + user = '' return '' % \ - (self.path, pformat(self.GET), pformat(self.POST), pformat(self.COOKIES), - pformat(self.META), pformat(self.user)) + (self.path, get, post, cookies, meta, user) def get_full_path(self): return '%s%s' % (self.path, self._req.args and ('?' + self._req.args) or '') @@ -141,13 +162,12 @@ def __call__(self, req): try: request = ModPythonRequest(req) response = self.get_response(req.uri, request) + # Apply response middleware + for middleware_method in self._response_middleware: + response = middleware_method(request, response) finally: db.db.close() - # Apply response middleware - for middleware_method in self._response_middleware: - response = middleware_method(request, response) - # Convert our custom HttpResponse object back into the mod_python req. populate_apache_request(response, req) return 0 # mod_python.apache.OK diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index 7541a5fd9dd8..362e66b0ced0 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -55,9 +55,30 @@ def __init__(self, environ): def __repr__(self): from pprint import pformat - return '' % \ - (pformat(self.GET), pformat(self.POST), pformat(self.COOKIES), - pformat(self.META)) + # Since this is called as part of error handling, we need to be very + # robust against potentially malformed input. + try: + get = pformat(self.GET) + except: + get = '' + try: + post = pformat(self.POST) + except: + post = '' + try: + cookies = pformat(self.COOKIES) + except: + cookies = '' + try: + meta = pformat(self.META) + except: + meta = '' + try: + user = self.user + except: + user = '' + return '' % \ + (self.path, get, post, cookies, meta, user) def get_full_path(self): return '%s%s' % (self.path, self.environ['QUERY_STRING'] and ('?' + self.environ['QUERY_STRING']) or '') @@ -157,13 +178,12 @@ def __call__(self, environ, start_response): try: request = WSGIRequest(environ) response = self.get_response(request.path, request) + # Apply response middleware + for middleware_method in self._response_middleware: + response = middleware_method(request, response) finally: db.db.close() - # Apply response middleware - for middleware_method in self._response_middleware: - response = middleware_method(request, response) - try: status_text = STATUS_CODE_TEXT[response.status_code] except KeyError: diff --git a/django/core/meta/__init__.py b/django/core/meta/__init__.py index 812173609663..eaa69b960bf3 100644 --- a/django/core/meta/__init__.py +++ b/django/core/meta/__init__.py @@ -151,7 +151,7 @@ class BadKeywordArguments(Exception): class BoundRelatedObject(object): def __init__(self, related_object, field_mapping, original): self.relation = related_object - self.field_mappings = field_mapping[related_object.opts.module_name] + self.field_mappings = field_mapping[related_object.name] def template_name(self): raise NotImplementedError @@ -165,7 +165,7 @@ def __init__(self, parent_opts, opts, field): self.opts = opts self.field = field self.edit_inline = field.rel.edit_inline - self.name = opts.module_name + self.name = '%s_%s' % (opts.app_label, opts.module_name) self.var_name = opts.object_name.lower() def flatten_data(self, follow, obj=None): @@ -1734,7 +1734,7 @@ def manipulator_init(opts, add, change, self, obj_key=None, follow=None): # Sanity check -- Make sure the "parent" object exists. # For example, make sure the Place exists for the Restaurant. # Let the ObjectDoesNotExist exception propagate up. - lookup_kwargs = opts.one_to_one_field.rel.limit_choices_to + lookup_kwargs = opts.one_to_one_field.rel.limit_choices_to.copy() lookup_kwargs['%s__exact' % opts.one_to_one_field.rel.field_name] = obj_key _ = opts.one_to_one_field.rel.to.get_model_module().get_object(**lookup_kwargs) params = dict([(f.attname, f.get_default()) for f in opts.fields]) diff --git a/django/core/template/defaultfilters.py b/django/core/template/defaultfilters.py index b82d54a31c35..69362fa2fad0 100644 --- a/django/core/template/defaultfilters.py +++ b/django/core/template/defaultfilters.py @@ -327,18 +327,26 @@ def get_digit(value, arg): # DATES # ################### +EMPTY_DATE_VALUES = (None, '') + def date(value, arg=DATE_FORMAT): "Formats a date according to the given format" + if value in EMPTY_DATE_VALUES: + return '' from django.utils.dateformat import format return format(value, arg) def time(value, arg=TIME_FORMAT): "Formats a time according to the given format" + if value in EMPTY_DATE_VALUES: + return '' from django.utils.dateformat import time_format return time_format(value, arg) def timesince(value): 'Formats a date as the time since that date (i.e. "4 days, 6 hours")' + if value in EMPTY_DATE_VALUES: + return '' from django.utils.timesince import timesince return timesince(value) diff --git a/django/utils/translation.py b/django/utils/translation.py index 56cd5426f00b..feb7824f4351 100644 --- a/django/utils/translation.py +++ b/django/utils/translation.py @@ -1,6 +1,9 @@ "translation helper functions" -import os, re, sys +import locale +import os +import re +import sys import gettext as gettext_module from cStringIO import StringIO from django.utils.functional import lazy @@ -25,15 +28,25 @@ def currentThread(): # The default translation is based on the settings file. _default = None -# This is a cache for accept-header to translation object mappings to prevent -# the accept parser to run multiple times for one user. +# This is a cache for normalised accept-header languages to prevent multiple +# file lookups when checking the same locale on repeated requests. _accepted = {} -def to_locale(language): +# Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9. +accept_language_re = re.compile(r''' + ([A-Za-z]{1,8}(?:-[A-Za-z]{1,8})*|\*) # "en", "en-au", "x-y-z", "*" + (?:;q=(0(?:\.\d{,3})?|1(?:.0{,3})?))? # Optional "q=1.00", "q=0.8" + (?:\s*,\s*|$) # Multiple accepts per header. + ''', re.VERBOSE) + +def to_locale(language, to_lower=False): "Turns a language name (en-us) into a locale name (en_US)." p = language.find('-') if p >= 0: - return language[:p].lower()+'_'+language[p+1:].upper() + if to_lower: + return language[:p].lower()+'_'+language[p+1:].lower() + else: + return language[:p].lower()+'_'+language[p+1:].upper() else: return language.lower() @@ -297,46 +310,40 @@ def get_language_from_request(request): if lang_code in supported and lang_code is not None and check_for_language(lang_code): return lang_code - lang_code = request.COOKIES.get('django_language', None) - if lang_code in supported and lang_code is not None and check_for_language(lang_code): + lang_code = request.COOKIES.get('django_language') + if lang_code and lang_code in supported and check_for_language(lang_code): return lang_code - accept = request.META.get('HTTP_ACCEPT_LANGUAGE', None) - if accept is not None: - - t = _accepted.get(accept, None) - if t is not None: - return t - - def _parsed(el): - p = el.find(';q=') - if p >= 0: - lang = el[:p].strip() - order = int(float(el[p+3:].strip())*100) - else: - lang = el - order = 100 - p = lang.find('-') - if p >= 0: - mainlang = lang[:p] - else: - mainlang = lang - return (lang, mainlang, order) - - langs = [_parsed(el) for el in accept.split(',')] - langs.sort(lambda a,b: -1*cmp(a[2], b[2])) - - for lang, mainlang, order in langs: - if lang in supported or mainlang in supported: - langfile = gettext_module.find('django', globalpath, [to_locale(lang)]) - if langfile: - # reconstruct the actual language from the language - # filename, because otherwise we might incorrectly - # report de_DE if we only have de available, but - # did find de_DE because of language normalization - lang = langfile[len(globalpath):].split(os.path.sep)[1] - _accepted[accept] = lang - return lang + accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '') + for lang, unused in parse_accept_lang_header(accept): + if lang == '*': + break + + # We have a very restricted form for our language files (no encoding + # specifier, since they all must be UTF-8 and only one possible + # language each time. So we avoid the overhead of gettext.find() and + # look up the MO file manually. + + normalized = locale.locale_alias.get(to_locale(lang, True)) + if not normalized: + continue + + # Remove the default encoding from locale_alias + normalized = normalized.split('.')[0] + + if normalized in _accepted: + # We've seen this locale before and have an MO file for it, so no + # need to check again. + return _accepted[normalized] + + for lang in (normalized, normalized.split('_')[0]): + if lang not in supported: + continue + langfile = os.path.join(globalpath, lang, 'LC_MESSAGES', + 'django.mo') + if os.path.exists(langfile): + _accepted[normalized] = lang + return lang return settings.LANGUAGE_CODE @@ -457,3 +464,23 @@ def templatize(src): else: out.write(blankout(t.contents, 'X')) return out.getvalue() + +def parse_accept_lang_header(lang_string): + """ + Parses the lang_string, which is the body of an HTTP Accept-Language + header, and returns a list of (lang, q-value), ordered by 'q' values. + + Any format errors in lang_string results in an empty list being returned. + """ + result = [] + pieces = accept_language_re.split(lang_string) + if pieces[-1]: + return [] + for i in range(0, len(pieces) - 1, 3): + first, lang, priority = pieces[i : i + 3] + if first: + return [] + priority = priority and float(priority) or 1.0 + result.append((lang, priority)) + result.sort(lambda x, y: -cmp(x[1], y[1])) + return result diff --git a/setup.py b/setup.py index 2ff9b700fd0b..be0ef946e7a6 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name = "Django", - version = "0.91", + version = "0.91.3", url = 'http://www.djangoproject.com/', author = 'Lawrence Journal-World', author_email = 'holovaty@gmail.com',