From f189280eb39b20a8cfdb6b9416df1176e107550d Mon Sep 17 00:00:00 2001 From: Malcolm Tredinnick Date: Sat, 3 Nov 2007 02:15:27 +0000 Subject: [PATCH] queryset-refactor: Merged from trunk up to [6623]. git-svn-id: http://code.djangoproject.com/svn/django/branches/queryset-refactor@6637 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/locale/es/LC_MESSAGES/django.mo | Bin 61344 -> 61344 bytes django/conf/locale/es/LC_MESSAGES/django.po | 2 +- django/contrib/auth/forms.py | 2 +- django/core/management/__init__.py | 2 + django/core/management/commands/startapp.py | 21 ++-- django/db/backends/__init__.py | 21 ++++ .../db/backends/postgresql_psycopg2/base.py | 9 +- django/db/backends/util.py | 18 +-- django/newforms/fields.py | 28 ++--- django/utils/translation/trans_real.py | 106 +++++++++++------- docs/django-admin.txt | 17 +-- tests/regressiontests/forms/regressions.py | 1 - tests/regressiontests/i18n/misc.py | 57 ++++++++++ tests/regressiontests/i18n/tests.py | 8 +- tests/regressiontests/templates/urls.py | 2 +- 15 files changed, 203 insertions(+), 91 deletions(-) create mode 100644 tests/regressiontests/i18n/misc.py diff --git a/django/conf/locale/es/LC_MESSAGES/django.mo b/django/conf/locale/es/LC_MESSAGES/django.mo index 29b411467e4bb30a442b596b9129e233f487e92c..728c53a15bad6a3e60fd75780780ae902fec51dc 100644 GIT binary patch delta 17 ZcmZ2*pLxN3<_+A7nTql^^Ddt30{};q2Z#Uw delta 17 ZcmZ2*pLxN3<_+A7nevM^^Ddt30{};n2Z#Uw diff --git a/django/conf/locale/es/LC_MESSAGES/django.po b/django/conf/locale/es/LC_MESSAGES/django.po index 786889106e..509e01f7c0 100644 --- a/django/conf/locale/es/LC_MESSAGES/django.po +++ b/django/conf/locale/es/LC_MESSAGES/django.po @@ -1016,7 +1016,7 @@ msgstr "Escoja %s para modificar" #: contrib/admin/views/main.py:780 msgid "Database error" -msgstr "Erorr en la base de datos" +msgstr "Error en la base de datos" #: contrib/auth/forms.py:17 #: contrib/auth/forms.py:138 diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 16ee0289a2..47a974cacd 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -104,7 +104,7 @@ class PasswordResetForm(oldforms.Manipulator): 'site_name': site_name, 'user': user, } - send_mail('Password reset on %s' % site_name, t.render(Context(c)), None, [user.email]) + send_mail(_('Password reset on %s') % site_name, t.render(Context(c)), None, [user.email]) class PasswordChangeForm(oldforms.Manipulator): "A form that lets a user change his password." diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index f706fa3c7e..dce2fd493d 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -242,6 +242,8 @@ def setup_environ(settings_mod): """ Configures the runtime environment. This can also be used by external scripts wanting to set up a similar environment to manage.py. + Returns the project directory (assuming the passed settings module is + directly in the project directory). """ # Add this project to sys.path so that it's importable in the conventional # way. For example, if this file (manage.py) lives in a directory diff --git a/django/core/management/commands/startapp.py b/django/core/management/commands/startapp.py index c238e74a08..a3d517dd7a 100644 --- a/django/core/management/commands/startapp.py +++ b/django/core/management/commands/startapp.py @@ -1,8 +1,10 @@ -from django.core.management.base import copy_helper, CommandError, LabelCommand import os +from django.core.management.base import copy_helper, CommandError, LabelCommand + class Command(LabelCommand): - help = "Creates a Django app directory structure for the given app name in the current directory." + help = ("Creates a Django app directory structure for the given app name" + " in the current directory.") args = "[appname]" label = 'application name' @@ -14,17 +16,18 @@ class Command(LabelCommand): def handle_label(self, app_name, directory=None, **options): if directory is None: directory = os.getcwd() - # 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, os.pardir)) - parent_dir = os.path.basename(project_dir) + # Determine the project_name by using the basename of directory, + # which should be the full path of the project directory (or the + # current directory if no directory was passed). project_name = os.path.basename(directory) if app_name == project_name: - raise CommandError("You cannot create an app with the same name (%r) as your project." % app_name) - copy_helper(self.style, 'app', app_name, directory, parent_dir) + raise CommandError("You cannot create an app with the same name" + " (%r) as your project." % app_name) + copy_helper(self.style, 'app', app_name, directory, project_name) class ProjectCommand(Command): - help = "Creates a Django app directory structure for the given app name in this project's directory." + help = ("Creates a Django app directory structure for the given app name" + " in this project's directory.") def __init__(self, project_directory): super(ProjectCommand, self).__init__() diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 1b6ba07f24..dd50229461 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -127,6 +127,27 @@ class BaseDatabaseOperations(object): """ raise NotImplementedError('Full-text search is not implemented for this database backend') + def last_executed_query(self, cursor, sql, params): + """ + Returns a string of the query last executed by the given cursor, with + placeholders replaced with actual values. + + `sql` is the raw query containing placeholders, and `params` is the + sequence of parameters. These are used by default, but this method + exists for database backends to provide a better implementation + according to their own quoting schemes. + """ + from django.utils.encoding import smart_unicode, force_unicode + + # Convert params to contain Unicode values. + to_unicode = lambda s: force_unicode(s, strings_only=True) + if isinstance(params, (list, tuple)): + u_params = tuple([to_unicode(val) for val in params]) + else: + u_params = dict([(to_unicode(k), to_unicode(v)) for k, v in params.items()]) + + return smart_unicode(sql) % u_params + def last_insert_id(self, cursor, table_name, pk_name): """ Given a cursor object that has just performed an INSERT statement into diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index a7b080d505..d7b3558344 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -5,7 +5,7 @@ Requires psycopg 2: http://initd.org/projects/psycopg2 """ from django.db.backends import BaseDatabaseWrapper, BaseDatabaseFeatures -from django.db.backends.postgresql.operations import DatabaseOperations +from django.db.backends.postgresql.operations import DatabaseOperations as PostgresqlDatabaseOperations try: import psycopg2 as Database import psycopg2.extensions @@ -21,6 +21,13 @@ psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) class DatabaseFeatures(BaseDatabaseFeatures): needs_datetime_string_cast = False +class DatabaseOperations(PostgresqlDatabaseOperations): + def last_executed_query(self, cursor, sql, params): + # With psycopg2, cursor objects have a "query" attribute that is the + # exact query sent to the database. See docs here: + # http://www.initd.org/tracker/psycopg/wiki/psycopg2_documentation#postgresql-status-message-and-executed-query + return cursor.query + class DatabaseWrapper(BaseDatabaseWrapper): features = DatabaseFeatures() ops = DatabaseOperations() diff --git a/django/db/backends/util.py b/django/db/backends/util.py index fa2df22927..ca4e90d6c2 100644 --- a/django/db/backends/util.py +++ b/django/db/backends/util.py @@ -1,7 +1,6 @@ import datetime import md5 from time import time -from django.utils.encoding import smart_unicode, force_unicode try: import decimal @@ -11,7 +10,7 @@ except ImportError: class CursorDebugWrapper(object): def __init__(self, cursor, db): self.cursor = cursor - self.db = db + self.db = db # Instance of a BaseDatabaseWrapper subclass def execute(self, sql, params=()): start = time() @@ -19,8 +18,9 @@ class CursorDebugWrapper(object): return self.cursor.execute(sql, params) finally: stop = time() + sql = self.db.ops.last_executed_query(self.cursor, sql, params) self.db.queries.append({ - 'sql': smart_unicode(sql) % convert_args(params), + 'sql': sql, 'time': "%.3f" % (stop - start), }) @@ -31,7 +31,7 @@ class CursorDebugWrapper(object): finally: stop = time() self.db.queries.append({ - 'sql': 'MANY: ' + sql + ' ' + smart_unicode(tuple(param_list)), + 'sql': '%s times: %s' % (len(param_list), sql), 'time': "%.3f" % (stop - start), }) @@ -41,16 +41,6 @@ class CursorDebugWrapper(object): else: return getattr(self.cursor, attr) -def convert_args(args): - """ - Convert sequence or dictionary to contain unicode values. - """ - to_unicode = lambda s: force_unicode(s, strings_only=True) - if isinstance(args, (list, tuple)): - return tuple([to_unicode(val) for val in args]) - else: - return dict([(to_unicode(k), to_unicode(v)) for k, v in args.items()]) - ############################################### # Converters from database (string) to Python # ############################################### diff --git a/django/newforms/fields.py b/django/newforms/fields.py index 11e6f51779..950307b15f 100644 --- a/django/newforms/fields.py +++ b/django/newforms/fields.py @@ -1,11 +1,20 @@ """ -Field classes +Field classes. """ import copy import datetime import re import time +# Python 2.3 fallbacks +try: + from decimal import Decimal, DecimalException +except ImportError: + from django.utils._decimal import Decimal, DecimalException +try: + set +except NameError: + from sets import Set as set from django.utils.translation import ugettext from django.utils.encoding import StrAndUnicode, smart_unicode @@ -13,18 +22,14 @@ from django.utils.encoding import StrAndUnicode, smart_unicode from util import ErrorList, ValidationError from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateTimeInput -try: - from decimal import Decimal, DecimalException -except ImportError: - from django.utils._decimal import Decimal, DecimalException __all__ = ( 'Field', 'CharField', 'IntegerField', 'DEFAULT_DATE_INPUT_FORMATS', 'DateField', 'DEFAULT_TIME_INPUT_FORMATS', 'TimeField', 'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField', - 'RegexField', 'EmailField', 'FileField', 'ImageField', 'URLField', 'BooleanField', - 'ChoiceField', 'NullBooleanField', 'MultipleChoiceField', + 'RegexField', 'EmailField', 'FileField', 'ImageField', 'URLField', + 'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField', 'ComboField', 'MultiValueField', 'FloatField', 'DecimalField', 'SplitDateTimeField', 'IPAddressField', ) @@ -32,15 +37,6 @@ __all__ = ( # These values, if given to to_python(), will trigger the self.required check. EMPTY_VALUES = (None, '') -try: - set -except NameError: - from sets import Set as set # Python 2.3 fallback - -try: - from decimal import Decimal -except ImportError: - from django.utils._decimal import Decimal # Python 2.3 fallback class Field(object): widget = TextInput # Default widget to use when rendering this type of Field. diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index 3427d721bf..98b4769031 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -1,8 +1,12 @@ "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.encoding import force_unicode try: @@ -25,15 +29,25 @@ _active = {} # 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() @@ -334,46 +348,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: + accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '') + for lang, unused in parse_accept_lang_header(accept): + if lang == '*': + break - t = _accepted.get(accept, None) - if t is not None: - return t + # 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. - 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) + normalized = locale.locale_alias.get(to_locale(lang, True)) + if not normalized: + continue - langs = [_parsed(el) for el in accept.split(',')] - langs.sort(lambda a,b: -1*cmp(a[2], b[2])) + # Remove the default encoding from locale_alias + normalized = normalized.split('.')[0] - 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 + 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 @@ -505,3 +513,23 @@ def templatize(src): 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/docs/django-admin.txt b/docs/django-admin.txt index f098dfa988..e751a7b3d9 100644 --- a/docs/django-admin.txt +++ b/docs/django-admin.txt @@ -741,22 +741,25 @@ Customized actions **New in Django development version** -If you want to add an action of your own to ``manage.py``, you can. -Simply add a ``management/commands`` directory to your application. -Each python module in that directory will be discovered and registered as +Applications can register their own actions with ``manage.py``. For example, +you might want to add a ``manage.py`` action for a Django app that you're +distributing. + +To do this, just add a ``management/commands`` directory to your application. +Each Python module in that directory will be auto-discovered and registered as a command that can be executed as an action when you run ``manage.py``:: - /fancy_blog + blog/ __init__.py models.py - /management + management/ __init__.py - /commands + commands/ __init__.py explode.py views.py -In this example, ``explode`` command will be made available to any project +In this example, the ``explode`` command will be made available to any project that includes the ``fancy_blog`` application in ``settings.INSTALLED_APPS``. The ``explode.py`` module has only one requirement -- it must define a class diff --git a/tests/regressiontests/forms/regressions.py b/tests/regressiontests/forms/regressions.py index 1bfb425188..1bb6f6e7e5 100644 --- a/tests/regressiontests/forms/regressions.py +++ b/tests/regressiontests/forms/regressions.py @@ -26,7 +26,6 @@ There were some problems with form translations in #3600 Translations are done at rendering time, so multi-lingual apps can define forms early and still send back the right translation. -# XFAIL >>> activate('de') >>> print f.as_p()

diff --git a/tests/regressiontests/i18n/misc.py b/tests/regressiontests/i18n/misc.py new file mode 100644 index 0000000000..fa22fd05d3 --- /dev/null +++ b/tests/regressiontests/i18n/misc.py @@ -0,0 +1,57 @@ +tests = """ +>>> from django.utils.translation.trans_real import parse_accept_lang_header +>>> p = parse_accept_lang_header + +Good headers. +>>> p('de') +[('de', 1.0)] +>>> p('en-AU') +[('en-AU', 1.0)] +>>> p('*;q=1.00') +[('*', 1.0)] +>>> p('en-AU;q=0.123') +[('en-AU', 0.123)] +>>> p('en-au;q=0.1') +[('en-au', 0.10000000000000001)] +>>> p('en-au;q=1.0') +[('en-au', 1.0)] +>>> p('da, en-gb;q=0.25, en;q=0.5') +[('da', 1.0), ('en', 0.5), ('en-gb', 0.25)] +>>> p('en-au-xx') +[('en-au-xx', 1.0)] +>>> p('de,en-au;q=0.75,en-us;q=0.5,en;q=0.25,es;q=0.125,fa;q=0.125') +[('de', 1.0), ('en-au', 0.75), ('en-us', 0.5), ('en', 0.25), ('es', 0.125), ('fa', 0.125)] +>>> p('*') +[('*', 1.0)] +>>> p('de;q=0.') +[('de', 1.0)] +>>> p('') +[] + +Bad headers; should always return []. +>>> p('en-gb;q=1.0000') +[] +>>> p('en;q=0.1234') +[] +>>> p('en;q=.2') +[] +>>> p('abcdefghi-au') +[] +>>> p('**') +[] +>>> p('en,,gb') +[] +>>> p('en-au;q=0.1.0') +[] +>>> p('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZ,en') +[] +>>> p('da, en-gb;q=0.8, en;q=0.7,#') +[] +>>> p('de;q=2.0') +[] +>>> p('de;q=0.a') +[] +>>> p('') +[] + +""" diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py index 99204451e4..0a18e8bea5 100644 --- a/tests/regressiontests/i18n/tests.py +++ b/tests/regressiontests/i18n/tests.py @@ -1,6 +1,7 @@ # coding: utf-8 +import misc -ur""" +regressions = ur""" Format string interpolation should work with *_lazy objects. >>> from django.utils.translation import ugettext_lazy, activate, deactivate, gettext_lazy @@ -39,3 +40,8 @@ unicode(string_concat(...)) should not raise a TypeError - #4796 >>> unicode(django.utils.translation.string_concat("dja", "ngo")) u'django' """ + +__test__ = { + 'regressions': regressions, + 'misc': misc.tests, +} diff --git a/tests/regressiontests/templates/urls.py b/tests/regressiontests/templates/urls.py index d79f38e0a7..3b28e70d0b 100644 --- a/tests/regressiontests/templates/urls.py +++ b/tests/regressiontests/templates/urls.py @@ -7,7 +7,7 @@ urlpatterns = patterns('', # Test urls for testing reverse lookups (r'^$', views.index), (r'^client/(\d+)/$', views.client), - (r'^client/(\d+)/(?P[^/]+)/$', views.client_action), + (r'^client/(?P\d+)/(?P[^/]+)/$', views.client_action), url(r'^named-client/(\d+)/$', views.client, name="named.client"), # Unicode strings are permitted everywhere.