diff --git a/.gitignore b/.gitignore index a34f512130..2d028c7287 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ *.egg-info *.pot *.py[co] +MANIFEST +dist/ docs/_build/ +tests/coverage_html/ +tests/.coverage \ No newline at end of file diff --git a/.hgignore b/.hgignore index 765a29d091..3dc253a3c1 100644 --- a/.hgignore +++ b/.hgignore @@ -4,3 +4,5 @@ syntax:glob *.pot *.py[co] docs/_build/ +tests/coverage_html/ +tests/.coverage \ No newline at end of file diff --git a/.tx/config b/.tx/config index 771fb3f875..d3ded2878a 100644 --- a/.tx/config +++ b/.tx/config @@ -1,5 +1,5 @@ [main] -host = https://www.transifex.net +host = https://www.transifex.com lang_map = sr@latin:sr_Latn [django.core] diff --git a/AUTHORS b/AUTHORS index 6a7f22ada4..5799b941ff 100644 --- a/AUTHORS +++ b/AUTHORS @@ -33,6 +33,7 @@ The PRIMARY AUTHORS are (and/or have been): * Florian Apolloner * Jeremy Dunck * Bryan Veloso + * Preston Holmes More information on the main contributors to Django can be found in docs/internals/committers.txt. @@ -424,6 +425,7 @@ answer newbie questions, and generally made Django that much better: phil@produxion.net phil.h.smith@gmail.com Gustavo Picon + Travis Pinney Michael Placentra II plisk Daniel Poelzleithner @@ -499,6 +501,7 @@ answer newbie questions, and generally made Django that much better: Wiliam Alves de Souza Don Spaulding Calvin Spealman + Dane Springmeyer Bjørn Stabell Georgi Stanojevski starrynight diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000000..3b1734157d --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,16 @@ +====================== +Contributing to Django +====================== + +As an open source project, Django welcomes contributions of many forms. + +Examples of contributions include: + +* Code patches +* Documentation improvements +* Bug reports and patch reviews + +Extensive contribution guidelines are available in the repository at +``docs/internals/contributing/``, or online at: + +https://docs.djangoproject.com/en/dev/internals/contributing/ diff --git a/MANIFEST.in b/MANIFEST.in index 185e57646a..fbda541d22 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -32,3 +32,5 @@ recursive-include django/contrib/gis/tests/geogapp/fixtures * recursive-include django/contrib/gis/tests/relatedapp/fixtures * recursive-include django/contrib/sitemaps/templates * recursive-include django/contrib/sitemaps/tests/templates * +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] diff --git a/django/conf/__init__.py b/django/conf/__init__.py index 6272f4ed5d..1804c851bf 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -25,7 +25,7 @@ class LazySettings(LazyObject): The user can manually configure settings prior to using them. Otherwise, Django uses the settings module pointed to by DJANGO_SETTINGS_MODULE. """ - def _setup(self, name): + def _setup(self, name=None): """ Load the settings module pointed to by the environment variable. This is used the first time we need any settings at all, if the user has not @@ -36,20 +36,40 @@ class LazySettings(LazyObject): if not settings_module: # If it's set but is an empty string. raise KeyError except KeyError: + desc = ("setting %s" % name) if name else "settings" raise ImproperlyConfigured( - "Requested setting %s, but settings are not configured. " + "Requested %s, but settings are not configured. " "You must either define the environment variable %s " "or call settings.configure() before accessing settings." - % (name, ENVIRONMENT_VARIABLE)) + % (desc, ENVIRONMENT_VARIABLE)) self._wrapped = Settings(settings_module) - + self._configure_logging() def __getattr__(self, name): if self._wrapped is empty: self._setup(name) return getattr(self._wrapped, name) + def _configure_logging(self): + """ + Setup logging from LOGGING_CONFIG and LOGGING settings. + """ + if self.LOGGING_CONFIG: + from django.utils.log import DEFAULT_LOGGING + # First find the logging configuration function ... + logging_config_path, logging_config_func_name = self.LOGGING_CONFIG.rsplit('.', 1) + logging_config_module = importlib.import_module(logging_config_path) + logging_config_func = getattr(logging_config_module, logging_config_func_name) + + logging_config_func(DEFAULT_LOGGING) + + if self.LOGGING: + # Backwards-compatibility shim for #16288 fix + compat_patch_logging_config(self.LOGGING) + + # ... then invoke it with the logging settings + logging_config_func(self.LOGGING) def configure(self, default_settings=global_settings, **options): """ @@ -133,19 +153,6 @@ class Settings(BaseSettings): os.environ['TZ'] = self.TIME_ZONE time.tzset() - # Settings are configured, so we can set up the logger if required - if self.LOGGING_CONFIG: - # First find the logging configuration function ... - logging_config_path, logging_config_func_name = self.LOGGING_CONFIG.rsplit('.', 1) - logging_config_module = importlib.import_module(logging_config_path) - logging_config_func = getattr(logging_config_module, logging_config_func_name) - - # Backwards-compatibility shim for #16288 fix - compat_patch_logging_config(self.LOGGING) - - # ... then invoke it with the logging settings - logging_config_func(self.LOGGING) - class UserSettingsHolder(BaseSettings): """ diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 13f7991b57..f1cbb22880 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -144,7 +144,7 @@ DEFAULT_CHARSET = 'utf-8' # Encoding of files read from disk (template and initial SQL files). FILE_CHARSET = 'utf-8' -# E-mail address that error messages come from. +# Email address that error messages come from. SERVER_EMAIL = 'root@localhost' # Whether to send broken-link emails. @@ -488,6 +488,8 @@ PROFANITIES_LIST = () # AUTHENTICATION # ################## +AUTH_USER_MODEL = 'auth.User' + AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',) LOGIN_URL = '/accounts/login/' @@ -549,33 +551,8 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.fallback.FallbackStorage' # The callable to use to configure logging LOGGING_CONFIG = 'django.utils.log.dictConfig' -# The default logging configuration. This sends an email to -# the site admins on every HTTP 500 error. All other log -# records are sent to the bit bucket. - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse', - } - }, - 'handlers': { - 'mail_admins': { - 'level': 'ERROR', - 'filters': ['require_debug_false'], - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True, - }, - } -} +# Custom logging configuration. +LOGGING = {} # Default exception reporter filter class used in case none has been # specifically assigned to the HttpRequest instance. diff --git a/tests/regressiontests/localflavor/ar/__init__.py b/django/conf/locale/de_CH/__init__.py similarity index 100% rename from tests/regressiontests/localflavor/ar/__init__.py rename to django/conf/locale/de_CH/__init__.py diff --git a/django/contrib/localflavor/de_CH/formats.py b/django/conf/locale/de_CH/formats.py similarity index 99% rename from django/contrib/localflavor/de_CH/formats.py rename to django/conf/locale/de_CH/formats.py index 9d56f9f298..7cbf76e7db 100644 --- a/django/contrib/localflavor/de_CH/formats.py +++ b/django/conf/locale/de_CH/formats.py @@ -35,7 +35,7 @@ DATETIME_INPUT_FORMATS = ( '%Y-%m-%d', # '2006-10-25' ) -# these are the separators for non-monetary numbers. For monetary numbers, +# these are the separators for non-monetary numbers. For monetary numbers, # the DECIMAL_SEPARATOR is a . (decimal point) and the THOUSAND_SEPARATOR is a # ' (single quote). # For details, please refer to http://www.bk.admin.ch/dokumentation/sprachen/04915/05016/index.html?lang=de diff --git a/django/conf/locale/en/LC_MESSAGES/django.po b/django/conf/locale/en/LC_MESSAGES/django.po index 7c815ae17d..1e301665c6 100644 --- a/django/conf/locale/en/LC_MESSAGES/django.po +++ b/django/conf/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-03-23 02:29+0100\n" +"POT-Creation-Date: 2012-10-15 10:55+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -297,385 +297,386 @@ msgstr "" msgid "Traditional Chinese" msgstr "" -#: core/validators.py:24 forms/fields.py:51 +#: core/validators.py:21 forms/fields.py:52 msgid "Enter a valid value." msgstr "" -#: core/validators.py:99 forms/fields.py:601 -msgid "This URL appears to be a broken link." +#: core/validators.py:104 forms/fields.py:464 +msgid "Enter a valid email address." msgstr "" -#: core/validators.py:131 forms/fields.py:600 -msgid "Enter a valid URL." -msgstr "" - -#: core/validators.py:165 forms/fields.py:474 -msgid "Enter a valid e-mail address." -msgstr "" - -#: core/validators.py:168 forms/fields.py:1023 +#: core/validators.py:107 forms/fields.py:1013 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "" -#: core/validators.py:171 core/validators.py:188 forms/fields.py:997 +#: core/validators.py:110 core/validators.py:129 forms/fields.py:987 msgid "Enter a valid IPv4 address." msgstr "" -#: core/validators.py:175 core/validators.py:189 +#: core/validators.py:115 core/validators.py:130 msgid "Enter a valid IPv6 address." msgstr "" -#: core/validators.py:184 core/validators.py:187 +#: core/validators.py:125 core/validators.py:128 msgid "Enter a valid IPv4 or IPv6 address." msgstr "" -#: core/validators.py:209 db/models/fields/__init__.py:638 +#: core/validators.py:151 db/models/fields/__init__.py:655 msgid "Enter only digits separated by commas." msgstr "" -#: core/validators.py:215 +#: core/validators.py:157 #, python-format msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." msgstr "" -#: core/validators.py:233 forms/fields.py:209 forms/fields.py:262 +#: core/validators.py:176 forms/fields.py:210 forms/fields.py:263 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "" -#: core/validators.py:238 forms/fields.py:210 forms/fields.py:263 +#: core/validators.py:182 forms/fields.py:211 forms/fields.py:264 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "" -#: core/validators.py:244 +#: core/validators.py:189 #, python-format msgid "" "Ensure this value has at least %(limit_value)d characters (it has " "%(show_value)d)." msgstr "" -#: core/validators.py:250 +#: core/validators.py:196 #, python-format msgid "" "Ensure this value has at most %(limit_value)d characters (it has " "%(show_value)d)." msgstr "" -#: db/models/base.py:764 +#: db/models/base.py:843 #, python-format msgid "%(field_name)s must be unique for %(date_field)s %(lookup)s." msgstr "" -#: db/models/base.py:787 forms/models.py:577 +#: db/models/base.py:866 forms/models.py:573 msgid "and" msgstr "" -#: db/models/base.py:788 db/models/fields/__init__.py:65 +#: db/models/base.py:867 db/models/fields/__init__.py:70 #, python-format msgid "%(model_name)s with this %(field_label)s already exists." msgstr "" -#: db/models/fields/__init__.py:62 +#: db/models/fields/__init__.py:67 #, python-format msgid "Value %r is not a valid choice." msgstr "" -#: db/models/fields/__init__.py:63 +#: db/models/fields/__init__.py:68 msgid "This field cannot be null." msgstr "" -#: db/models/fields/__init__.py:64 +#: db/models/fields/__init__.py:69 msgid "This field cannot be blank." msgstr "" -#: db/models/fields/__init__.py:71 +#: db/models/fields/__init__.py:76 #, python-format msgid "Field of type: %(field_type)s" msgstr "" -#: db/models/fields/__init__.py:506 db/models/fields/__init__.py:961 +#: db/models/fields/__init__.py:517 db/models/fields/__init__.py:985 msgid "Integer" msgstr "" -#: db/models/fields/__init__.py:510 db/models/fields/__init__.py:959 +#: db/models/fields/__init__.py:521 db/models/fields/__init__.py:983 #, python-format msgid "'%s' value must be an integer." msgstr "" -#: db/models/fields/__init__.py:552 +#: db/models/fields/__init__.py:569 #, python-format msgid "'%s' value must be either True or False." msgstr "" -#: db/models/fields/__init__.py:554 +#: db/models/fields/__init__.py:571 msgid "Boolean (Either True or False)" msgstr "" -#: db/models/fields/__init__.py:605 +#: db/models/fields/__init__.py:622 #, python-format msgid "String (up to %(max_length)s)" msgstr "" -#: db/models/fields/__init__.py:633 +#: db/models/fields/__init__.py:650 msgid "Comma-separated integers" msgstr "" -#: db/models/fields/__init__.py:647 +#: db/models/fields/__init__.py:664 #, python-format msgid "'%s' value has an invalid date format. It must be in YYYY-MM-DD format." msgstr "" -#: db/models/fields/__init__.py:649 db/models/fields/__init__.py:734 +#: db/models/fields/__init__.py:666 db/models/fields/__init__.py:754 #, python-format msgid "" "'%s' value has the correct format (YYYY-MM-DD) but it is an invalid date." msgstr "" -#: db/models/fields/__init__.py:652 +#: db/models/fields/__init__.py:669 msgid "Date (without time)" msgstr "" -#: db/models/fields/__init__.py:732 +#: db/models/fields/__init__.py:752 #, python-format msgid "" "'%s' value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[." "uuuuuu]][TZ] format." msgstr "" -#: db/models/fields/__init__.py:736 +#: db/models/fields/__init__.py:756 #, python-format msgid "" "'%s' value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) but " "it is an invalid date/time." msgstr "" -#: db/models/fields/__init__.py:740 +#: db/models/fields/__init__.py:760 msgid "Date (with time)" msgstr "" -#: db/models/fields/__init__.py:831 +#: db/models/fields/__init__.py:849 #, python-format msgid "'%s' value must be a decimal number." msgstr "" -#: db/models/fields/__init__.py:833 +#: db/models/fields/__init__.py:851 msgid "Decimal number" msgstr "" -#: db/models/fields/__init__.py:890 -msgid "E-mail address" +#: db/models/fields/__init__.py:908 +msgid "Email address" msgstr "" -#: db/models/fields/__init__.py:906 +#: db/models/fields/__init__.py:927 msgid "File path" msgstr "" -#: db/models/fields/__init__.py:930 +#: db/models/fields/__init__.py:954 #, python-format msgid "'%s' value must be a float." msgstr "" -#: db/models/fields/__init__.py:932 +#: db/models/fields/__init__.py:956 msgid "Floating point number" msgstr "" -#: db/models/fields/__init__.py:993 +#: db/models/fields/__init__.py:1017 msgid "Big (8 byte) integer" msgstr "" -#: db/models/fields/__init__.py:1007 +#: db/models/fields/__init__.py:1031 msgid "IPv4 address" msgstr "" -#: db/models/fields/__init__.py:1023 +#: db/models/fields/__init__.py:1047 msgid "IP address" msgstr "" -#: db/models/fields/__init__.py:1065 +#: db/models/fields/__init__.py:1090 #, python-format msgid "'%s' value must be either None, True or False." msgstr "" -#: db/models/fields/__init__.py:1067 +#: db/models/fields/__init__.py:1092 msgid "Boolean (Either True, False or None)" msgstr "" -#: db/models/fields/__init__.py:1116 +#: db/models/fields/__init__.py:1141 msgid "Positive integer" msgstr "" -#: db/models/fields/__init__.py:1127 +#: db/models/fields/__init__.py:1152 msgid "Positive small integer" msgstr "" -#: db/models/fields/__init__.py:1138 +#: db/models/fields/__init__.py:1163 #, python-format msgid "Slug (up to %(max_length)s)" msgstr "" -#: db/models/fields/__init__.py:1156 +#: db/models/fields/__init__.py:1181 msgid "Small integer" msgstr "" -#: db/models/fields/__init__.py:1162 +#: db/models/fields/__init__.py:1187 msgid "Text" msgstr "" -#: db/models/fields/__init__.py:1180 +#: db/models/fields/__init__.py:1205 #, python-format msgid "" "'%s' value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] format." msgstr "" -#: db/models/fields/__init__.py:1182 +#: db/models/fields/__init__.py:1207 #, python-format msgid "" "'%s' value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an invalid " "time." msgstr "" -#: db/models/fields/__init__.py:1185 +#: db/models/fields/__init__.py:1210 msgid "Time" msgstr "" -#: db/models/fields/__init__.py:1249 +#: db/models/fields/__init__.py:1272 msgid "URL" msgstr "" -#: db/models/fields/files.py:214 +#: db/models/fields/files.py:211 +#, python-format +msgid "Filename is %(extra)d characters too long." +msgstr "" + +#: db/models/fields/files.py:221 msgid "File" msgstr "" -#: db/models/fields/files.py:321 +#: db/models/fields/files.py:347 msgid "Image" msgstr "" -#: db/models/fields/related.py:903 +#: db/models/fields/related.py:950 #, python-format msgid "Model %(model)s with pk %(pk)r does not exist." msgstr "" -#: db/models/fields/related.py:905 +#: db/models/fields/related.py:952 msgid "Foreign Key (type determined by related field)" msgstr "" -#: db/models/fields/related.py:1033 +#: db/models/fields/related.py:1082 msgid "One-to-one relationship" msgstr "" -#: db/models/fields/related.py:1096 +#: db/models/fields/related.py:1149 msgid "Many-to-many relationship" msgstr "" -#: db/models/fields/related.py:1120 +#: db/models/fields/related.py:1174 msgid "" "Hold down \"Control\", or \"Command\" on a Mac, to select more than one." msgstr "" -#: forms/fields.py:50 +#: forms/fields.py:51 msgid "This field is required." msgstr "" -#: forms/fields.py:208 +#: forms/fields.py:209 msgid "Enter a whole number." msgstr "" -#: forms/fields.py:240 forms/fields.py:261 +#: forms/fields.py:241 forms/fields.py:262 msgid "Enter a number." msgstr "" -#: forms/fields.py:264 -#, python-format -msgid "Ensure that there are no more than %s digits in total." -msgstr "" - #: forms/fields.py:265 #, python-format -msgid "Ensure that there are no more than %s decimal places." +msgid "Ensure that there are no more than %s digits in total." msgstr "" #: forms/fields.py:266 #, python-format +msgid "Ensure that there are no more than %s decimal places." +msgstr "" + +#: forms/fields.py:267 +#, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "" -#: forms/fields.py:365 forms/fields.py:963 +#: forms/fields.py:355 forms/fields.py:953 msgid "Enter a valid date." msgstr "" -#: forms/fields.py:388 forms/fields.py:964 +#: forms/fields.py:378 forms/fields.py:954 msgid "Enter a valid time." msgstr "" -#: forms/fields.py:409 +#: forms/fields.py:399 msgid "Enter a valid date/time." msgstr "" -#: forms/fields.py:485 +#: forms/fields.py:475 msgid "No file was submitted. Check the encoding type on the form." msgstr "" -#: forms/fields.py:486 +#: forms/fields.py:476 msgid "No file was submitted." msgstr "" -#: forms/fields.py:487 +#: forms/fields.py:477 msgid "The submitted file is empty." msgstr "" -#: forms/fields.py:488 +#: forms/fields.py:478 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." msgstr "" -#: forms/fields.py:489 +#: forms/fields.py:479 msgid "Please either submit a file or check the clear checkbox, not both." msgstr "" -#: forms/fields.py:544 +#: forms/fields.py:534 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: forms/fields.py:689 forms/fields.py:769 +#: forms/fields.py:580 +msgid "Enter a valid URL." +msgstr "" + +#: forms/fields.py:666 forms/fields.py:746 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" -#: forms/fields.py:770 forms/fields.py:858 forms/models.py:999 +#: forms/fields.py:747 forms/fields.py:835 forms/models.py:999 msgid "Enter a list of values." msgstr "" -#: forms/formsets.py:317 forms/formsets.py:319 +#: forms/formsets.py:323 forms/formsets.py:325 msgid "Order" msgstr "" -#: forms/formsets.py:321 +#: forms/formsets.py:327 msgid "Delete" msgstr "" -#: forms/models.py:571 +#: forms/models.py:567 #, python-format msgid "Please correct the duplicate data for %(field)s." msgstr "" -#: forms/models.py:575 +#: forms/models.py:571 #, python-format msgid "Please correct the duplicate data for %(field)s, which must be unique." msgstr "" -#: forms/models.py:581 +#: forms/models.py:577 #, python-format msgid "" "Please correct the duplicate data for %(field_name)s which must be unique " "for the %(lookup)s in %(date_field)s." msgstr "" -#: forms/models.py:589 +#: forms/models.py:585 msgid "Please correct the duplicate values below." msgstr "" @@ -697,94 +698,94 @@ msgstr "" msgid "\"%s\" is not a valid value for a primary key." msgstr "" -#: forms/util.py:70 +#: forms/util.py:81 #, python-format msgid "" "%(datetime)s couldn't be interpreted in time zone %(current_timezone)s; it " "may be ambiguous or it may not exist." msgstr "" -#: forms/widgets.py:325 +#: forms/widgets.py:336 msgid "Currently" msgstr "" -#: forms/widgets.py:326 +#: forms/widgets.py:337 msgid "Change" msgstr "" -#: forms/widgets.py:327 +#: forms/widgets.py:338 msgid "Clear" msgstr "" -#: forms/widgets.py:582 +#: forms/widgets.py:591 msgid "Unknown" msgstr "" -#: forms/widgets.py:583 +#: forms/widgets.py:592 msgid "Yes" msgstr "" -#: forms/widgets.py:584 +#: forms/widgets.py:593 msgid "No" msgstr "" -#: template/defaultfilters.py:797 +#: template/defaultfilters.py:794 msgid "yes,no,maybe" msgstr "" -#: template/defaultfilters.py:825 template/defaultfilters.py:830 +#: template/defaultfilters.py:822 template/defaultfilters.py:833 #, python-format msgid "%(size)d byte" msgid_plural "%(size)d bytes" msgstr[0] "" msgstr[1] "" -#: template/defaultfilters.py:832 +#: template/defaultfilters.py:835 #, python-format msgid "%s KB" msgstr "" -#: template/defaultfilters.py:834 +#: template/defaultfilters.py:837 #, python-format msgid "%s MB" msgstr "" -#: template/defaultfilters.py:836 +#: template/defaultfilters.py:839 #, python-format msgid "%s GB" msgstr "" -#: template/defaultfilters.py:838 +#: template/defaultfilters.py:841 #, python-format msgid "%s TB" msgstr "" -#: template/defaultfilters.py:839 +#: template/defaultfilters.py:842 #, python-format msgid "%s PB" msgstr "" -#: utils/dateformat.py:45 +#: utils/dateformat.py:47 msgid "p.m." msgstr "" -#: utils/dateformat.py:46 +#: utils/dateformat.py:48 msgid "a.m." msgstr "" -#: utils/dateformat.py:51 +#: utils/dateformat.py:53 msgid "PM" msgstr "" -#: utils/dateformat.py:52 +#: utils/dateformat.py:54 msgid "AM" msgstr "" -#: utils/dateformat.py:101 +#: utils/dateformat.py:103 msgid "midnight" msgstr "" -#: utils/dateformat.py:103 +#: utils/dateformat.py:105 msgid "noon" msgstr "" @@ -1060,148 +1061,133 @@ msgctxt "alt. month" msgid "December" msgstr "" -#: utils/text.py:65 +#: utils/text.py:70 #, python-format msgctxt "String to return when truncating text" msgid "%(truncated_text)s..." msgstr "" -#: utils/text.py:234 +#: utils/text.py:239 msgid "or" msgstr "" #. Translators: This string is used as a separator between list elements -#: utils/text.py:251 +#: utils/text.py:256 msgid ", " msgstr "" -#: utils/timesince.py:20 +#: utils/timesince.py:22 msgid "year" msgid_plural "years" msgstr[0] "" msgstr[1] "" -#: utils/timesince.py:21 +#: utils/timesince.py:23 msgid "month" msgid_plural "months" msgstr[0] "" msgstr[1] "" -#: utils/timesince.py:22 +#: utils/timesince.py:24 msgid "week" msgid_plural "weeks" msgstr[0] "" msgstr[1] "" -#: utils/timesince.py:23 +#: utils/timesince.py:25 msgid "day" msgid_plural "days" msgstr[0] "" msgstr[1] "" -#: utils/timesince.py:24 +#: utils/timesince.py:26 msgid "hour" msgid_plural "hours" msgstr[0] "" msgstr[1] "" -#: utils/timesince.py:25 +#: utils/timesince.py:27 msgid "minute" msgid_plural "minutes" msgstr[0] "" msgstr[1] "" -#: utils/timesince.py:41 +#: utils/timesince.py:43 msgid "minutes" msgstr "" -#: utils/timesince.py:46 +#: utils/timesince.py:48 #, python-format msgid "%(number)d %(type)s" msgstr "" -#: utils/timesince.py:52 +#: utils/timesince.py:54 #, python-format msgid ", %(number)d %(type)s" msgstr "" -#: views/static.py:52 +#: views/static.py:55 msgid "Directory indexes are not allowed here." msgstr "" -#: views/static.py:54 +#: views/static.py:57 #, python-format msgid "\"%(path)s\" does not exist" msgstr "" -#: views/static.py:95 +#: views/static.py:98 #, python-format msgid "Index of %(directory)s" msgstr "" -#: views/generic/create_update.py:121 -#, python-format -msgid "The %(verbose_name)s was created successfully." -msgstr "" - -#: views/generic/create_update.py:164 -#, python-format -msgid "The %(verbose_name)s was updated successfully." -msgstr "" - -#: views/generic/create_update.py:207 -#, python-format -msgid "The %(verbose_name)s was deleted." -msgstr "" - -#: views/generic/dates.py:33 +#: views/generic/dates.py:42 msgid "No year specified" msgstr "" -#: views/generic/dates.py:58 +#: views/generic/dates.py:98 msgid "No month specified" msgstr "" -#: views/generic/dates.py:99 +#: views/generic/dates.py:157 msgid "No day specified" msgstr "" -#: views/generic/dates.py:138 +#: views/generic/dates.py:213 msgid "No week specified" msgstr "" -#: views/generic/dates.py:198 views/generic/dates.py:215 +#: views/generic/dates.py:368 views/generic/dates.py:393 #, python-format msgid "No %(verbose_name_plural)s available" msgstr "" -#: views/generic/dates.py:467 +#: views/generic/dates.py:646 #, python-format msgid "" "Future %(verbose_name_plural)s not available because %(class_name)s." "allow_future is False." msgstr "" -#: views/generic/dates.py:501 +#: views/generic/dates.py:678 #, python-format msgid "Invalid date string '%(datestr)s' given format '%(format)s'" msgstr "" -#: views/generic/detail.py:51 +#: views/generic/detail.py:54 #, python-format msgid "No %(verbose_name)s found matching the query" msgstr "" -#: views/generic/list.py:45 +#: views/generic/list.py:49 msgid "Page is not 'last', nor can it be converted to an int." msgstr "" -#: views/generic/list.py:50 +#: views/generic/list.py:54 #, python-format msgid "Invalid page (%(page_number)s)" msgstr "" -#: views/generic/list.py:117 +#: views/generic/list.py:134 #, python-format msgid "Empty list and '%(class_name)s.allow_empty' is False." msgstr "" diff --git a/django/conf/project_template/project_name/wsgi.py b/django/conf/project_template/project_name/wsgi.py index b083a0e699..f768265b23 100644 --- a/django/conf/project_template/project_name/wsgi.py +++ b/django/conf/project_template/project_name/wsgi.py @@ -15,6 +15,10 @@ framework. """ import os +# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks +# if running multiple sites in the same mod_wsgi process. To fix this, use +# mod_wsgi daemon mode with each site in its own daemon process, or use +# os.environ["DJANGO_SETTINGS_MODULE"] = "{{ project_name }}.settings" os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings") # This application object is used by any WSGI server configured to use this diff --git a/django/contrib/admin/forms.py b/django/contrib/admin/forms.py index 398af075b1..f1e7076ece 100644 --- a/django/contrib/admin/forms.py +++ b/django/contrib/admin/forms.py @@ -4,12 +4,12 @@ from django import forms from django.contrib.auth import authenticate from django.contrib.auth.forms import AuthenticationForm -from django.contrib.auth.models import User -from django.utils.translation import ugettext_lazy, ugettext as _ +from django.utils.translation import ugettext_lazy ERROR_MESSAGE = ugettext_lazy("Please enter the correct username and password " "for a staff account. Note that both fields are case-sensitive.") + class AdminAuthenticationForm(AuthenticationForm): """ A custom authentication form used in the admin app. @@ -26,17 +26,6 @@ class AdminAuthenticationForm(AuthenticationForm): if username and password: self.user_cache = authenticate(username=username, password=password) if self.user_cache is None: - if '@' in username: - # Mistakenly entered e-mail address instead of username? Look it up. - try: - user = User.objects.get(email=username) - except (User.DoesNotExist, User.MultipleObjectsReturned): - # Nothing to do here, moving along. - pass - else: - if user.check_password(password): - message = _("Your e-mail address is not your username." - " Try '%s' instead.") % user.username raise forms.ValidationError(message) elif not self.user_cache.is_active or not self.user_cache.is_staff: raise forms.ValidationError(message) diff --git a/django/contrib/admin/locale/en/LC_MESSAGES/django.po b/django/contrib/admin/locale/en/LC_MESSAGES/django.po index 6e3bb60e7d..497c7463c2 100644 --- a/django/contrib/admin/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/admin/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-03-23 02:34+0100\n" +"POT-Creation-Date: 2012-10-15 10:56+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -32,39 +32,39 @@ msgstr "" msgid "Delete selected %(verbose_name_plural)s" msgstr "" -#: filters.py:101 filters.py:191 filters.py:231 filters.py:268 filters.py:378 +#: filters.py:101 filters.py:197 filters.py:237 filters.py:274 filters.py:380 msgid "All" msgstr "" -#: filters.py:232 +#: filters.py:238 msgid "Yes" msgstr "" -#: filters.py:233 +#: filters.py:239 msgid "No" msgstr "" -#: filters.py:247 +#: filters.py:253 msgid "Unknown" msgstr "" -#: filters.py:306 +#: filters.py:308 msgid "Any date" msgstr "" -#: filters.py:307 +#: filters.py:309 msgid "Today" msgstr "" -#: filters.py:311 +#: filters.py:313 msgid "Past 7 days" msgstr "" -#: filters.py:315 +#: filters.py:317 msgid "This month" msgstr "" -#: filters.py:319 +#: filters.py:321 msgid "This year" msgstr "" @@ -74,134 +74,129 @@ msgid "" "that both fields are case-sensitive." msgstr "" -#: forms.py:18 +#: forms.py:19 msgid "Please log in again, because your session has expired." msgstr "" -#: forms.py:37 -#, python-format -msgid "Your e-mail address is not your username. Try '%s' instead." -msgstr "" - -#: helpers.py:20 +#: helpers.py:23 msgid "Action:" msgstr "" -#: models.py:19 +#: models.py:24 msgid "action time" msgstr "" -#: models.py:22 +#: models.py:27 msgid "object id" msgstr "" -#: models.py:23 +#: models.py:28 msgid "object repr" msgstr "" -#: models.py:24 +#: models.py:29 msgid "action flag" msgstr "" -#: models.py:25 +#: models.py:30 msgid "change message" msgstr "" -#: models.py:30 +#: models.py:35 msgid "log entry" msgstr "" -#: models.py:31 +#: models.py:36 msgid "log entries" msgstr "" -#: models.py:40 +#: models.py:45 #, python-format msgid "Added \"%(object)s\"." msgstr "" -#: models.py:42 +#: models.py:47 #, python-format msgid "Changed \"%(object)s\" - %(changes)s" msgstr "" -#: models.py:44 +#: models.py:49 #, python-format msgid "Deleted \"%(object)s.\"" msgstr "" -#: models.py:46 +#: models.py:51 msgid "LogEntry Object" msgstr "" -#: options.py:150 options.py:166 +#: options.py:151 options.py:167 msgid "None" msgstr "" -#: options.py:671 +#: options.py:672 #, python-format msgid "Changed %s." msgstr "" -#: options.py:671 options.py:681 +#: options.py:672 options.py:682 msgid "and" msgstr "" -#: options.py:676 +#: options.py:677 #, python-format msgid "Added %(name)s \"%(object)s\"." msgstr "" -#: options.py:680 +#: options.py:681 #, python-format msgid "Changed %(list)s for %(name)s \"%(object)s\"." msgstr "" -#: options.py:685 +#: options.py:686 #, python-format msgid "Deleted %(name)s \"%(object)s\"." msgstr "" -#: options.py:689 +#: options.py:690 msgid "No fields changed." msgstr "" -#: options.py:772 +#: options.py:773 #, python-format msgid "The %(name)s \"%(obj)s\" was added successfully." msgstr "" -#: options.py:776 options.py:824 +#: options.py:777 options.py:825 msgid "You may edit it again below." msgstr "" -#: options.py:788 options.py:837 +#: options.py:789 options.py:838 #, python-format msgid "You may add another %s below." msgstr "" -#: options.py:822 +#: options.py:823 #, python-format msgid "The %(name)s \"%(obj)s\" was changed successfully." msgstr "" -#: options.py:830 +#: options.py:831 #, python-format msgid "" "The %(name)s \"%(obj)s\" was added successfully. You may edit it again below." msgstr "" -#: options.py:899 options.py:1159 +#: options.py:900 options.py:1159 msgid "" "Items must be selected in order to perform actions on them. No items have " "been changed." msgstr "" -#: options.py:918 +#: options.py:919 msgid "No action selected." msgstr "" -#: options.py:998 +#: options.py:999 #, python-format msgid "Add %s" msgstr "" @@ -249,34 +244,34 @@ msgstr "" msgid "Change history: %s" msgstr "" -#: sites.py:315 tests.py:61 templates/admin/login.html:49 -#: templates/registration/password_reset_complete.html:20 -#: views/decorators.py:23 +#: sites.py:322 tests.py:57 templates/admin/login.html:48 +#: templates/registration/password_reset_complete.html:19 +#: views/decorators.py:24 msgid "Log in" msgstr "" -#: sites.py:380 +#: sites.py:388 msgid "Site administration" msgstr "" -#: sites.py:432 +#: sites.py:440 #, python-format msgid "%s administration" msgstr "" -#: widgets.py:87 +#: widgets.py:90 msgid "Date:" msgstr "" -#: widgets.py:87 +#: widgets.py:91 msgid "Time:" msgstr "" -#: widgets.py:161 +#: widgets.py:165 msgid "Lookup" msgstr "" -#: widgets.py:267 +#: widgets.py:271 msgid "Add Another" msgstr "" @@ -288,39 +283,39 @@ msgstr "" msgid "We're sorry, but the requested page could not be found." msgstr "" -#: templates/admin/500.html:7 templates/admin/app_index.html:8 -#: templates/admin/base.html:45 templates/admin/change_form.html:21 -#: templates/admin/change_list.html:43 -#: templates/admin/delete_confirmation.html:8 -#: templates/admin/delete_selected_confirmation.html:8 -#: templates/admin/invalid_setup.html:7 templates/admin/object_history.html:8 -#: templates/admin/auth/user/change_password.html:15 -#: templates/registration/logged_out.html:5 -#: templates/registration/password_change_done.html:7 -#: templates/registration/password_change_form.html:8 -#: templates/registration/password_reset_complete.html:7 -#: templates/registration/password_reset_confirm.html:7 -#: templates/registration/password_reset_done.html:7 -#: templates/registration/password_reset_form.html:7 +#: templates/admin/500.html:6 templates/admin/app_index.html:7 +#: templates/admin/base.html:47 templates/admin/change_form.html:19 +#: templates/admin/change_list.html:41 +#: templates/admin/delete_confirmation.html:7 +#: templates/admin/delete_selected_confirmation.html:7 +#: templates/admin/invalid_setup.html:6 templates/admin/object_history.html:7 +#: templates/admin/auth/user/change_password.html:13 +#: templates/registration/logged_out.html:4 +#: templates/registration/password_change_done.html:6 +#: templates/registration/password_change_form.html:7 +#: templates/registration/password_reset_complete.html:6 +#: templates/registration/password_reset_confirm.html:6 +#: templates/registration/password_reset_done.html:6 +#: templates/registration/password_reset_form.html:6 msgid "Home" msgstr "" -#: templates/admin/500.html:8 +#: templates/admin/500.html:7 msgid "Server error" msgstr "" -#: templates/admin/500.html:12 +#: templates/admin/500.html:11 msgid "Server error (500)" msgstr "" -#: templates/admin/500.html:15 +#: templates/admin/500.html:14 msgid "Server Error (500)" msgstr "" -#: templates/admin/500.html:16 +#: templates/admin/500.html:15 msgid "" -"There's been an error. It's been reported to the site administrators via e-" -"mail and should be fixed shortly. Thanks for your patience." +"There's been an error. It's been reported to the site administrators via " +"email and should be fixed shortly. Thanks for your patience." msgstr "" #: templates/admin/actions.html:4 @@ -344,7 +339,7 @@ msgstr "" msgid "Clear selection" msgstr "" -#: templates/admin/app_index.html:11 templates/admin/index.html:19 +#: templates/admin/app_index.html:10 templates/admin/index.html:21 #, python-format msgid "%(name)s" msgstr "" @@ -354,22 +349,22 @@ msgid "Welcome," msgstr "" #: templates/admin/base.html:33 -#: templates/registration/password_change_done.html:4 -#: templates/registration/password_change_form.html:5 +#: templates/registration/password_change_done.html:3 +#: templates/registration/password_change_form.html:4 msgid "Documentation" msgstr "" -#: templates/admin/base.html:35 -#: templates/admin/auth/user/change_password.html:19 -#: templates/admin/auth/user/change_password.html:53 -#: templates/registration/password_change_done.html:4 -#: templates/registration/password_change_form.html:5 +#: templates/admin/base.html:36 +#: templates/admin/auth/user/change_password.html:17 +#: templates/admin/auth/user/change_password.html:51 +#: templates/registration/password_change_done.html:3 +#: templates/registration/password_change_form.html:4 msgid "Change password" msgstr "" -#: templates/admin/base.html:36 -#: templates/registration/password_change_done.html:4 -#: templates/registration/password_change_form.html:5 +#: templates/admin/base.html:38 +#: templates/registration/password_change_done.html:3 +#: templates/registration/password_change_form.html:4 msgid "Log out" msgstr "" @@ -381,35 +376,35 @@ msgstr "" msgid "Django administration" msgstr "" -#: templates/admin/change_form.html:24 templates/admin/index.html:29 +#: templates/admin/change_form.html:22 templates/admin/index.html:33 msgid "Add" msgstr "" -#: templates/admin/change_form.html:34 templates/admin/object_history.html:12 +#: templates/admin/change_form.html:32 templates/admin/object_history.html:11 msgid "History" msgstr "" -#: templates/admin/change_form.html:35 +#: templates/admin/change_form.html:33 #: templates/admin/edit_inline/stacked.html:9 #: templates/admin/edit_inline/tabular.html:30 msgid "View on site" msgstr "" -#: templates/admin/change_form.html:46 templates/admin/change_list.html:69 -#: templates/admin/login.html:18 -#: templates/admin/auth/user/change_password.html:29 -#: templates/registration/password_change_form.html:21 +#: templates/admin/change_form.html:44 templates/admin/change_list.html:67 +#: templates/admin/login.html:17 +#: templates/admin/auth/user/change_password.html:27 +#: templates/registration/password_change_form.html:20 msgid "Please correct the error below." msgid_plural "Please correct the errors below." msgstr[0] "" msgstr[1] "" -#: templates/admin/change_list.html:60 +#: templates/admin/change_list.html:58 #, python-format msgid "Add %(name)s" msgstr "" -#: templates/admin/change_list.html:80 +#: templates/admin/change_list.html:78 msgid "Filter" msgstr "" @@ -426,12 +421,12 @@ msgstr "" msgid "Toggle sorting" msgstr "" -#: templates/admin/delete_confirmation.html:12 +#: templates/admin/delete_confirmation.html:11 #: templates/admin/submit_line.html:4 msgid "Delete" msgstr "" -#: templates/admin/delete_confirmation.html:19 +#: templates/admin/delete_confirmation.html:18 #, python-format msgid "" "Deleting the %(object_name)s '%(escaped_object)s' would result in deleting " @@ -439,30 +434,30 @@ msgid "" "following types of objects:" msgstr "" -#: templates/admin/delete_confirmation.html:27 +#: templates/admin/delete_confirmation.html:26 #, python-format msgid "" "Deleting the %(object_name)s '%(escaped_object)s' would require deleting the " "following protected related objects:" msgstr "" -#: templates/admin/delete_confirmation.html:35 +#: templates/admin/delete_confirmation.html:34 #, python-format msgid "" "Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"? " "All of the following related items will be deleted:" msgstr "" -#: templates/admin/delete_confirmation.html:40 -#: templates/admin/delete_selected_confirmation.html:45 +#: templates/admin/delete_confirmation.html:39 +#: templates/admin/delete_selected_confirmation.html:44 msgid "Yes, I'm sure" msgstr "" -#: templates/admin/delete_selected_confirmation.html:11 +#: templates/admin/delete_selected_confirmation.html:10 msgid "Delete multiple objects" msgstr "" -#: templates/admin/delete_selected_confirmation.html:18 +#: templates/admin/delete_selected_confirmation.html:17 #, python-format msgid "" "Deleting the selected %(objects_name)s would result in deleting related " @@ -470,14 +465,14 @@ msgid "" "types of objects:" msgstr "" -#: templates/admin/delete_selected_confirmation.html:26 +#: templates/admin/delete_selected_confirmation.html:25 #, python-format msgid "" "Deleting the selected %(objects_name)s would require deleting the following " "protected related objects:" msgstr "" -#: templates/admin/delete_selected_confirmation.html:34 +#: templates/admin/delete_selected_confirmation.html:33 #, python-format msgid "" "Are you sure you want to delete the selected %(objects_name)s? All of the " @@ -489,67 +484,63 @@ msgstr "" msgid " By %(filter_title)s " msgstr "" -#: templates/admin/index.html:18 +#: templates/admin/index.html:20 #, python-format -msgid "Models available in the %(name)s application." +msgid "Models in the %(name)s application" msgstr "" -#: templates/admin/index.html:35 +#: templates/admin/index.html:39 msgid "Change" msgstr "" -#: templates/admin/index.html:45 +#: templates/admin/index.html:49 msgid "You don't have permission to edit anything." msgstr "" -#: templates/admin/index.html:53 +#: templates/admin/index.html:57 msgid "Recent Actions" msgstr "" -#: templates/admin/index.html:54 +#: templates/admin/index.html:58 msgid "My Actions" msgstr "" -#: templates/admin/index.html:58 +#: templates/admin/index.html:62 msgid "None available" msgstr "" -#: templates/admin/index.html:72 +#: templates/admin/index.html:76 msgid "Unknown content" msgstr "" -#: templates/admin/invalid_setup.html:13 +#: templates/admin/invalid_setup.html:12 msgid "" "Something's wrong with your database installation. Make sure the appropriate " "database tables have been created, and make sure the database is readable by " "the appropriate user." msgstr "" -#: templates/admin/login.html:34 -msgid "Username:" -msgstr "" - -#: templates/admin/login.html:38 +#: templates/admin/login.html:37 msgid "Password:" msgstr "" -#: templates/admin/login.html:45 +#: templates/admin/login.html:44 msgid "Forgotten your password or username?" msgstr "" -#: templates/admin/object_history.html:24 +#: templates/admin/object_history.html:23 msgid "Date/time" msgstr "" -#: templates/admin/object_history.html:25 +#: templates/admin/object_history.html:24 msgid "User" msgstr "" -#: templates/admin/object_history.html:26 +#: templates/admin/object_history.html:25 msgid "Action" msgstr "" -#: templates/admin/object_history.html:40 +#: templates/admin/object_history.html:39 msgid "" "This object doesn't have a change history. It probably wasn't added via this " "admin site." @@ -601,147 +592,147 @@ msgstr "" msgid "Enter a username and password." msgstr "" -#: templates/admin/auth/user/change_password.html:33 +#: templates/admin/auth/user/change_password.html:31 #, python-format msgid "Enter a new password for the user %(username)s." msgstr "" -#: templates/admin/auth/user/change_password.html:40 +#: templates/admin/auth/user/change_password.html:38 msgid "Password" msgstr "" -#: templates/admin/auth/user/change_password.html:46 -#: templates/registration/password_change_form.html:43 +#: templates/admin/auth/user/change_password.html:44 +#: templates/registration/password_change_form.html:42 msgid "Password (again)" msgstr "" -#: templates/admin/auth/user/change_password.html:47 +#: templates/admin/auth/user/change_password.html:45 msgid "Enter the same password as above, for verification." msgstr "" -#: templates/admin/edit_inline/stacked.html:67 -#: templates/admin/edit_inline/tabular.html:115 -#, python-format -msgid "Add another %(verbose_name)s" +#: templates/admin/edit_inline/stacked.html:26 +#: templates/admin/edit_inline/tabular.html:76 +msgid "Remove" msgstr "" -#: templates/admin/edit_inline/stacked.html:70 -#: templates/admin/edit_inline/tabular.html:118 -msgid "Remove" +#: templates/admin/edit_inline/stacked.html:27 +#: templates/admin/edit_inline/tabular.html:75 +#, python-format +msgid "Add another %(verbose_name)s" msgstr "" #: templates/admin/edit_inline/tabular.html:17 msgid "Delete?" msgstr "" -#: templates/registration/logged_out.html:9 +#: templates/registration/logged_out.html:8 msgid "Thanks for spending some quality time with the Web site today." msgstr "" -#: templates/registration/logged_out.html:11 +#: templates/registration/logged_out.html:10 msgid "Log in again" msgstr "" -#: templates/registration/password_change_done.html:8 -#: templates/registration/password_change_form.html:9 -#: templates/registration/password_change_form.html:13 -#: templates/registration/password_change_form.html:25 +#: templates/registration/password_change_done.html:7 +#: templates/registration/password_change_form.html:8 +#: templates/registration/password_change_form.html:12 +#: templates/registration/password_change_form.html:24 msgid "Password change" msgstr "" -#: templates/registration/password_change_done.html:12 -#: templates/registration/password_change_done.html:16 +#: templates/registration/password_change_done.html:11 +#: templates/registration/password_change_done.html:15 msgid "Password change successful" msgstr "" -#: templates/registration/password_change_done.html:18 +#: templates/registration/password_change_done.html:17 msgid "Your password was changed." msgstr "" -#: templates/registration/password_change_form.html:27 +#: templates/registration/password_change_form.html:26 msgid "" "Please enter your old password, for security's sake, and then enter your new " "password twice so we can verify you typed it in correctly." msgstr "" -#: templates/registration/password_change_form.html:33 +#: templates/registration/password_change_form.html:32 msgid "Old password" msgstr "" -#: templates/registration/password_change_form.html:38 +#: templates/registration/password_change_form.html:37 msgid "New password" msgstr "" -#: templates/registration/password_change_form.html:49 -#: templates/registration/password_reset_confirm.html:27 +#: templates/registration/password_change_form.html:48 +#: templates/registration/password_reset_confirm.html:26 msgid "Change my password" msgstr "" -#: templates/registration/password_reset_complete.html:8 -#: templates/registration/password_reset_confirm.html:12 -#: templates/registration/password_reset_done.html:8 -#: templates/registration/password_reset_form.html:8 -#: templates/registration/password_reset_form.html:12 -#: templates/registration/password_reset_form.html:16 +#: templates/registration/password_reset_complete.html:7 +#: templates/registration/password_reset_confirm.html:11 +#: templates/registration/password_reset_done.html:7 +#: templates/registration/password_reset_form.html:7 +#: templates/registration/password_reset_form.html:11 +#: templates/registration/password_reset_form.html:15 msgid "Password reset" msgstr "" -#: templates/registration/password_reset_complete.html:12 -#: templates/registration/password_reset_complete.html:16 +#: templates/registration/password_reset_complete.html:11 +#: templates/registration/password_reset_complete.html:15 msgid "Password reset complete" msgstr "" -#: templates/registration/password_reset_complete.html:18 +#: templates/registration/password_reset_complete.html:17 msgid "Your password has been set. You may go ahead and log in now." msgstr "" -#: templates/registration/password_reset_confirm.html:8 +#: templates/registration/password_reset_confirm.html:7 msgid "Password reset confirmation" msgstr "" -#: templates/registration/password_reset_confirm.html:18 +#: templates/registration/password_reset_confirm.html:17 msgid "Enter new password" msgstr "" -#: templates/registration/password_reset_confirm.html:20 +#: templates/registration/password_reset_confirm.html:19 msgid "" "Please enter your new password twice so we can verify you typed it in " "correctly." msgstr "" -#: templates/registration/password_reset_confirm.html:24 +#: templates/registration/password_reset_confirm.html:23 msgid "New password:" msgstr "" -#: templates/registration/password_reset_confirm.html:26 +#: templates/registration/password_reset_confirm.html:25 msgid "Confirm password:" msgstr "" -#: templates/registration/password_reset_confirm.html:32 +#: templates/registration/password_reset_confirm.html:31 msgid "Password reset unsuccessful" msgstr "" -#: templates/registration/password_reset_confirm.html:34 +#: templates/registration/password_reset_confirm.html:33 msgid "" "The password reset link was invalid, possibly because it has already been " "used. Please request a new password reset." msgstr "" -#: templates/registration/password_reset_done.html:12 -#: templates/registration/password_reset_done.html:16 +#: templates/registration/password_reset_done.html:11 +#: templates/registration/password_reset_done.html:15 msgid "Password reset successful" msgstr "" -#: templates/registration/password_reset_done.html:18 +#: templates/registration/password_reset_done.html:17 msgid "" -"We've e-mailed you instructions for setting your password to the e-mail " +"We've emailed you instructions for setting your password to the email " "address you submitted. You should be receiving it shortly." msgstr "" #: templates/registration/password_reset_email.html:2 #, python-format msgid "" -"You're receiving this e-mail because you requested a password reset for your " +"You're receiving this email because you requested a password reset for your " "user account at %(site_name)s." msgstr "" @@ -762,34 +753,34 @@ msgstr "" msgid "The %(site_name)s team" msgstr "" -#: templates/registration/password_reset_form.html:18 +#: templates/registration/password_reset_form.html:17 msgid "" -"Forgotten your password? Enter your e-mail address below, and we'll e-mail " +"Forgotten your password? Enter your email address below, and we'll email " "instructions for setting a new one." msgstr "" -#: templates/registration/password_reset_form.html:22 -msgid "E-mail address:" +#: templates/registration/password_reset_form.html:21 +msgid "Email address:" msgstr "" -#: templates/registration/password_reset_form.html:22 +#: templates/registration/password_reset_form.html:21 msgid "Reset my password" msgstr "" -#: templatetags/admin_list.py:336 +#: templatetags/admin_list.py:344 msgid "All dates" msgstr "" -#: views/main.py:31 +#: views/main.py:33 msgid "(None)" msgstr "" -#: views/main.py:74 -#, python-format -msgid "Select %s" -msgstr "" - #: views/main.py:76 #, python-format +msgid "Select %s" +msgstr "" + +#: views/main.py:78 +#, python-format msgid "Select %s to change" msgstr "" diff --git a/django/contrib/admin/models.py b/django/contrib/admin/models.py index 2b12edd4e2..e1d3b40d01 100644 --- a/django/contrib/admin/models.py +++ b/django/contrib/admin/models.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals from django.db import models +from django.conf import settings from django.contrib.contenttypes.models import ContentType -from django.contrib.auth.models import User from django.contrib.admin.util import quote from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import smart_text @@ -12,15 +12,17 @@ ADDITION = 1 CHANGE = 2 DELETION = 3 + class LogEntryManager(models.Manager): def log_action(self, user_id, content_type_id, object_id, object_repr, action_flag, change_message=''): e = self.model(None, None, user_id, content_type_id, smart_text(object_id), object_repr[:200], action_flag, change_message) e.save() + @python_2_unicode_compatible class LogEntry(models.Model): action_time = models.DateTimeField(_('action time'), auto_now=True) - user = models.ForeignKey(User) + user = models.ForeignKey(settings.AUTH_USER_MODEL) content_type = models.ForeignKey(ContentType, blank=True, null=True) object_id = models.TextField(_('object id'), blank=True, null=True) object_repr = models.CharField(_('object repr'), max_length=200) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index f4205f2ce7..19c212db9a 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -1,4 +1,6 @@ from functools import update_wrapper, partial +import warnings + from django import forms from django.conf import settings from django.forms.formsets import all_valid @@ -6,7 +8,7 @@ from django.forms.models import (modelform_factory, modelformset_factory, inlineformset_factory, BaseInlineFormSet) from django.contrib.contenttypes.models import ContentType from django.contrib.admin import widgets, helpers -from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_format_dict +from django.contrib.admin.util import quote, unquote, flatten_fieldsets, get_deleted_objects, model_format_dict from django.contrib.admin.templatetags.admin_static import static from django.contrib import messages from django.views.decorators.csrf import csrf_protect @@ -344,14 +346,14 @@ class ModelAdmin(BaseModelAdmin): self.admin_site = admin_site super(ModelAdmin, self).__init__() - def get_inline_instances(self, request): + def get_inline_instances(self, request, obj=None): inline_instances = [] for inline_class in self.inlines: inline = inline_class(self.model, self.admin_site) if request: if not (inline.has_add_permission(request) or - inline.has_change_permission(request) or - inline.has_delete_permission(request)): + inline.has_change_permission(request, obj) or + inline.has_delete_permission(request, obj)): continue if not inline.has_add_permission(request): inline.max_num = 0 @@ -504,7 +506,7 @@ class ModelAdmin(BaseModelAdmin): fields=self.list_editable, **defaults) def get_formsets(self, request, obj=None): - for inline in self.get_inline_instances(request): + for inline in self.get_inline_instances(request, obj): yield inline.get_formset(request, obj) def get_paginator(self, request, queryset, per_page, orphans=0, allow_empty_first_page=True): @@ -763,21 +765,49 @@ class ModelAdmin(BaseModelAdmin): "admin/change_form.html" ], context, current_app=self.admin_site.name) - def response_add(self, request, obj, post_url_continue='../%s/'): + def response_add(self, request, obj, post_url_continue='../%s/', + continue_editing_url=None, add_another_url=None, + hasperm_url=None, noperm_url=None): """ Determines the HttpResponse for the add_view stage. - """ - opts = obj._meta - pk_value = obj._get_pk_val() - msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': force_text(opts.verbose_name), 'obj': force_text(obj)} + :param request: HttpRequest instance. + :param obj: Object just added. + :param post_url_continue: Deprecated/undocumented. + :param continue_editing_url: URL where user will be redirected after + pressing 'Save and continue editing'. + :param add_another_url: URL where user will be redirected after + pressing 'Save and add another'. + :param hasperm_url: URL to redirect after a successful object creation + when the user has change permissions. + :param noperm_url: URL to redirect after a successful object creation + when the user has no change permissions. + """ + if post_url_continue != '../%s/': + warnings.warn("The undocumented 'post_url_continue' argument to " + "ModelAdmin.response_add() is deprecated, use the new " + "*_url arguments instead.", DeprecationWarning, + stacklevel=2) + opts = obj._meta + pk_value = obj.pk + app_label = opts.app_label + model_name = opts.module_name + site_name = self.admin_site.name + + msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)} + # Here, we distinguish between different save types by checking for # the presence of keys in request.POST. if "_continue" in request.POST: - self.message_user(request, msg + ' ' + _("You may edit it again below.")) + msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict + self.message_user(request, msg) + if continue_editing_url is None: + continue_editing_url = 'admin:%s_%s_change' % (app_label, model_name) + url = reverse(continue_editing_url, args=(quote(pk_value),), + current_app=site_name) if "_popup" in request.POST: - post_url_continue += "?_popup=1" - return HttpResponseRedirect(post_url_continue % pk_value) + url += "?_popup=1" + return HttpResponseRedirect(url) if "_popup" in request.POST: return HttpResponse( @@ -786,72 +816,104 @@ class ModelAdmin(BaseModelAdmin): # escape() calls force_text. (escape(pk_value), escapejs(obj))) elif "_addanother" in request.POST: - self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_text(opts.verbose_name))) - return HttpResponseRedirect(request.path) + msg = _('The %(name)s "%(obj)s" was added successfully. You may add another %(name)s below.') % msg_dict + self.message_user(request, msg) + if add_another_url is None: + add_another_url = 'admin:%s_%s_add' % (app_label, model_name) + url = reverse(add_another_url, current_app=site_name) + return HttpResponseRedirect(url) else: + msg = _('The %(name)s "%(obj)s" was added successfully.') % msg_dict self.message_user(request, msg) # Figure out where to redirect. If the user has change permission, # redirect to the change-list page for this object. Otherwise, # redirect to the admin index. if self.has_change_permission(request, None): - post_url = reverse('admin:%s_%s_changelist' % - (opts.app_label, opts.module_name), - current_app=self.admin_site.name) + if hasperm_url is None: + hasperm_url = 'admin:%s_%s_changelist' % (app_label, model_name) + url = reverse(hasperm_url, current_app=site_name) else: - post_url = reverse('admin:index', - current_app=self.admin_site.name) - return HttpResponseRedirect(post_url) + if noperm_url is None: + noperm_url = 'admin:index' + url = reverse(noperm_url, current_app=site_name) + return HttpResponseRedirect(url) - def response_change(self, request, obj): + def response_change(self, request, obj, continue_editing_url=None, + save_as_new_url=None, add_another_url=None, + hasperm_url=None, noperm_url=None): """ Determines the HttpResponse for the change_view stage. + + :param request: HttpRequest instance. + :param obj: Object just modified. + :param continue_editing_url: URL where user will be redirected after + pressing 'Save and continue editing'. + :param save_as_new_url: URL where user will be redirected after pressing + 'Save as new' (when applicable). + :param add_another_url: URL where user will be redirected after pressing + 'Save and add another'. + :param hasperm_url: URL to redirect after a successful object edition when + the user has change permissions. + :param noperm_url: URL to redirect after a successful object edition when + the user has no change permissions. """ opts = obj._meta + app_label = opts.app_label + model_name = opts.module_name + site_name = self.admin_site.name + verbose_name = opts.verbose_name # Handle proxy models automatically created by .only() or .defer(). # Refs #14529 - verbose_name = opts.verbose_name - module_name = opts.module_name if obj._deferred: opts_ = opts.proxy_for_model._meta verbose_name = opts_.verbose_name - module_name = opts_.module_name + model_name = opts_.module_name - pk_value = obj._get_pk_val() + msg_dict = {'name': force_text(verbose_name), 'obj': force_text(obj)} - msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': force_text(verbose_name), 'obj': force_text(obj)} if "_continue" in request.POST: - self.message_user(request, msg + ' ' + _("You may edit it again below.")) - if "_popup" in request.REQUEST: - return HttpResponseRedirect(request.path + "?_popup=1") - else: - return HttpResponseRedirect(request.path) - elif "_saveasnew" in request.POST: - msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_text(verbose_name), 'obj': obj} + msg = _('The %(name)s "%(obj)s" was changed successfully. You may edit it again below.') % msg_dict self.message_user(request, msg) - return HttpResponseRedirect(reverse('admin:%s_%s_change' % - (opts.app_label, module_name), - args=(pk_value,), - current_app=self.admin_site.name)) + if continue_editing_url is None: + continue_editing_url = 'admin:%s_%s_change' % (app_label, model_name) + url = reverse(continue_editing_url, args=(quote(obj.pk),), + current_app=site_name) + if "_popup" in request.POST: + url += "?_popup=1" + return HttpResponseRedirect(url) + elif "_saveasnew" in request.POST: + msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict + self.message_user(request, msg) + if save_as_new_url is None: + save_as_new_url = 'admin:%s_%s_change' % (app_label, model_name) + url = reverse(save_as_new_url, args=(quote(obj.pk),), + current_app=site_name) + return HttpResponseRedirect(url) elif "_addanother" in request.POST: - self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_text(verbose_name))) - return HttpResponseRedirect(reverse('admin:%s_%s_add' % - (opts.app_label, module_name), - current_app=self.admin_site.name)) + msg = _('The %(name)s "%(obj)s" was changed successfully. You may add another %(name)s below.') % msg_dict + self.message_user(request, msg) + if add_another_url is None: + add_another_url = 'admin:%s_%s_add' % (app_label, model_name) + url = reverse(add_another_url, current_app=site_name) + return HttpResponseRedirect(url) else: + msg = _('The %(name)s "%(obj)s" was changed successfully.') % msg_dict self.message_user(request, msg) # Figure out where to redirect. If the user has change permission, # redirect to the change-list page for this object. Otherwise, # redirect to the admin index. if self.has_change_permission(request, None): - post_url = reverse('admin:%s_%s_changelist' % - (opts.app_label, module_name), - current_app=self.admin_site.name) + if hasperm_url is None: + hasperm_url = 'admin:%s_%s_changelist' % (app_label, + model_name) + url = reverse(hasperm_url, current_app=site_name) else: - post_url = reverse('admin:index', - current_app=self.admin_site.name) - return HttpResponseRedirect(post_url) + if noperm_url is None: + noperm_url = 'admin:index' + url = reverse(noperm_url, current_app=site_name) + return HttpResponseRedirect(url) def response_action(self, request, queryset): """ @@ -932,7 +994,7 @@ class ModelAdmin(BaseModelAdmin): ModelForm = self.get_form(request) formsets = [] - inline_instances = self.get_inline_instances(request) + inline_instances = self.get_inline_instances(request, None) if request.method == 'POST': form = ModelForm(request.POST, request.FILES) if form.is_valid(): @@ -1029,7 +1091,7 @@ class ModelAdmin(BaseModelAdmin): ModelForm = self.get_form(request, obj) formsets = [] - inline_instances = self.get_inline_instances(request) + inline_instances = self.get_inline_instances(request, obj) if request.method == 'POST': form = ModelForm(request.POST, request.FILES, instance=obj) if form.is_valid(): diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 05773ceac0..e375bc608f 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -9,7 +9,6 @@ from django.db.models.base import ModelBase from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse, NoReverseMatch from django.template.response import TemplateResponse -from django.utils.safestring import mark_safe from django.utils import six from django.utils.text import capfirst from django.utils.translation import ugettext as _ @@ -18,12 +17,15 @@ from django.conf import settings LOGIN_FORM_KEY = 'this_is_the_login_form' + class AlreadyRegistered(Exception): pass + class NotRegistered(Exception): pass + class AdminSite(object): """ An AdminSite object encapsulates an instance of the Django admin application, ready @@ -41,7 +43,7 @@ class AdminSite(object): password_change_done_template = None def __init__(self, name='admin', app_name='admin'): - self._registry = {} # model_class class -> admin_class instance + self._registry = {} # model_class class -> admin_class instance self.name = name self.app_name = app_name self._actions = {'delete_selected': actions.delete_selected} @@ -80,20 +82,23 @@ class AdminSite(object): if model in self._registry: raise AlreadyRegistered('The model %s is already registered' % model.__name__) - # If we got **options then dynamically construct a subclass of - # admin_class with those **options. - if options: - # For reasons I don't quite understand, without a __module__ - # the created class appears to "live" in the wrong place, - # which causes issues later on. - options['__module__'] = __name__ - admin_class = type("%sAdmin" % model.__name__, (admin_class,), options) + # Ignore the registration if the model has been + # swapped out. + if not model._meta.swapped: + # If we got **options then dynamically construct a subclass of + # admin_class with those **options. + if options: + # For reasons I don't quite understand, without a __module__ + # the created class appears to "live" in the wrong place, + # which causes issues later on. + options['__module__'] = __name__ + admin_class = type("%sAdmin" % model.__name__, (admin_class,), options) - # Validate (which might be a no-op) - validate(admin_class, model) + # Validate (which might be a no-op) + validate(admin_class, model) - # Instantiate the admin class to save in the registry - self._registry[model] = admin_class(model, self) + # Instantiate the admin class to save in the registry + self._registry[model] = admin_class(model, self) def unregister(self, model_or_iterable): """ @@ -319,6 +324,7 @@ class AdminSite(object): REDIRECT_FIELD_NAME: request.get_full_path(), } context.update(extra_context or {}) + defaults = { 'extra_context': context, 'current_app': self.name, diff --git a/django/contrib/admin/templates/admin/500.html b/django/contrib/admin/templates/admin/500.html index 9a3b636346..4842faa656 100644 --- a/django/contrib/admin/templates/admin/500.html +++ b/django/contrib/admin/templates/admin/500.html @@ -12,6 +12,6 @@ {% block content %}

{% trans 'Server Error (500)' %}

-

{% trans "There's been an error. It's been reported to the site administrators via e-mail and should be fixed shortly. Thanks for your patience." %}

+

{% trans "There's been an error. It's been reported to the site administrators via email and should be fixed shortly. Thanks for your patience." %}

{% endblock %} diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html index f367223bc5..b5a7715844 100644 --- a/django/contrib/admin/templates/admin/auth/user/change_password.html +++ b/django/contrib/admin/templates/admin/auth/user/change_password.html @@ -28,7 +28,7 @@

{% endif %} -

{% blocktrans with username=original.username %}Enter a new password for the user {{ username }}.{% endblocktrans %}

+

{% blocktrans with username=original %}Enter a new password for the user {{ username }}.{% endblocktrans %}

diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html index caa26744d4..7bbd73a464 100644 --- a/django/contrib/admin/templates/admin/base.html +++ b/django/contrib/admin/templates/admin/base.html @@ -26,7 +26,7 @@ {% if user.is_active and user.is_staff %}
{% trans 'Welcome,' %} - {% filter force_escape %}{% firstof user.first_name user.username %}{% endfilter %}. + {% filter force_escape %}{% firstof user.get_short_name user.get_username %}{% endfilter %}. {% block userlinks %} {% url 'django-admindocs-docroot' as docsroot %} {% if docsroot %} diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index e27875cdad..4962e732a2 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -29,7 +29,7 @@ {% if change %}{% if not is_popup %} diff --git a/django/contrib/admin/templates/admin/login.html b/django/contrib/admin/templates/admin/login.html index 06fe4c8160..4690363891 100644 --- a/django/contrib/admin/templates/admin/login.html +++ b/django/contrib/admin/templates/admin/login.html @@ -30,7 +30,7 @@
{% csrf_token %}
{% if not form.this_is_the_login_form.errors %}{{ form.username.errors }}{% endif %} - {{ form.username }} + {{ form.username }}
{% if not form.this_is_the_login_form.errors %}{{ form.password.errors }}{% endif %} diff --git a/django/contrib/admin/templates/admin/object_history.html b/django/contrib/admin/templates/admin/object_history.html index 55dd4a3b4c..870c4648a6 100644 --- a/django/contrib/admin/templates/admin/object_history.html +++ b/django/contrib/admin/templates/admin/object_history.html @@ -29,7 +29,7 @@ {% for action in action_list %} {{ action.action_time|date:"DATETIME_FORMAT" }} - {{ action.user.username }}{% if action.user.get_full_name %} ({{ action.user.get_full_name }}){% endif %} + {{ action.user.get_username }}{% if action.user.get_full_name %} ({{ action.user.get_full_name }}){% endif %} {{ action.change_message }} {% endfor %} diff --git a/django/contrib/admin/templates/admin/submit_line.html b/django/contrib/admin/templates/admin/submit_line.html index d6f854a233..8c9d22752d 100644 --- a/django/contrib/admin/templates/admin/submit_line.html +++ b/django/contrib/admin/templates/admin/submit_line.html @@ -1,8 +1,8 @@ -{% load i18n %} +{% load i18n admin_urls %}
{% if show_save %}{% endif %} -{% if show_delete_link %}{% endif %} +{% if show_delete_link %}{% endif %} {% if show_save_as_new %}{%endif%} -{% if show_save_and_add_another %}{% endif %} +{% if show_save_and_add_another %}{% endif %} {% if show_save_and_continue %}{% endif %}
diff --git a/django/contrib/admin/templates/registration/password_reset_done.html b/django/contrib/admin/templates/registration/password_reset_done.html index 3c9796e63c..7584c8393a 100644 --- a/django/contrib/admin/templates/registration/password_reset_done.html +++ b/django/contrib/admin/templates/registration/password_reset_done.html @@ -14,6 +14,6 @@

{% trans 'Password reset successful' %}

-

{% trans "We've e-mailed you instructions for setting your password to the e-mail address you submitted. You should be receiving it shortly." %}

+

{% trans "We've emailed you instructions for setting your password to the email address you submitted. You should be receiving it shortly." %}

{% endblock %} diff --git a/django/contrib/admin/templates/registration/password_reset_email.html b/django/contrib/admin/templates/registration/password_reset_email.html index 4f002fe5bb..a220f12033 100644 --- a/django/contrib/admin/templates/registration/password_reset_email.html +++ b/django/contrib/admin/templates/registration/password_reset_email.html @@ -1,11 +1,11 @@ {% load i18n %}{% autoescape off %} -{% blocktrans %}You're receiving this e-mail because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} +{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} {% trans "Please go to the following page and choose a new password:" %} {% block reset_link %} {{ protocol }}://{{ domain }}{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %} {% endblock %} -{% trans "Your username, in case you've forgotten:" %} {{ user.username }} +{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }} {% trans "Thanks for using our site!" %} diff --git a/django/contrib/admin/templates/registration/password_reset_form.html b/django/contrib/admin/templates/registration/password_reset_form.html index ca9ff115bc..c9998a1a3b 100644 --- a/django/contrib/admin/templates/registration/password_reset_form.html +++ b/django/contrib/admin/templates/registration/password_reset_form.html @@ -14,11 +14,11 @@

{% trans "Password reset" %}

-

{% trans "Forgotten your password? Enter your e-mail address below, and we'll e-mail instructions for setting a new one." %}

+

{% trans "Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one." %}

{% csrf_token %} {{ form.email.errors }} -

{{ form.email }}

+

{{ form.email }}

{% endblock %} diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py index c190533f95..f6ac59635a 100644 --- a/django/contrib/admin/templatetags/admin_modify.py +++ b/django/contrib/admin/templatetags/admin_modify.py @@ -28,7 +28,8 @@ def submit_row(context): change = context['change'] is_popup = context['is_popup'] save_as = context['save_as'] - return { + ctx = { + 'opts': opts, 'onclick_attrib': (opts.get_ordered_objects() and change and 'onclick="submitOrderForm();"' or ''), 'show_delete_link': (not is_popup and context['has_delete_permission'] @@ -40,6 +41,9 @@ def submit_row(context): 'is_popup': is_popup, 'show_save': True } + if context.get('original') is not None: + ctx['original'] = context['original'] + return ctx @register.filter def cell_count(inline_admin_form): diff --git a/django/contrib/admin/tests.py b/django/contrib/admin/tests.py index eaf1c8600c..7c62c1a22f 100644 --- a/django/contrib/admin/tests.py +++ b/django/contrib/admin/tests.py @@ -21,9 +21,9 @@ class AdminSeleniumWebDriverTestCase(LiveServerTestCase): @classmethod def tearDownClass(cls): - super(AdminSeleniumWebDriverTestCase, cls).tearDownClass() if hasattr(cls, 'selenium'): cls.selenium.quit() + super(AdminSeleniumWebDriverTestCase, cls).tearDownClass() def wait_until(self, callback, timeout=10): """ @@ -98,4 +98,4 @@ class AdminSeleniumWebDriverTestCase(LiveServerTestCase): `klass`. """ return (self.selenium.find_element_by_css_selector(selector) - .get_attribute('class').find(klass) != -1) \ No newline at end of file + .get_attribute('class').find(klass) != -1) diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py index f95fe53de1..a85045c515 100644 --- a/django/contrib/admin/util.py +++ b/django/contrib/admin/util.py @@ -48,9 +48,9 @@ def prepare_lookup_value(key, value): def quote(s): """ Ensure that primary key values do not confuse the admin URLs by escaping - any '/', '_' and ':' characters. Similar to urllib.quote, except that the - quoting is slightly different so that it doesn't get automatically - unquoted by the Web browser. + any '/', '_' and ':' and similarly problematic characters. + Similar to urllib.quote, except that the quoting is slightly different so + that it doesn't get automatically unquoted by the Web browser. """ if not isinstance(s, six.string_types): return s @@ -191,6 +191,13 @@ class NestedObjects(Collector): roots.extend(self._nested(root, seen, format_callback)) return roots + def can_fast_delete(self, *args, **kwargs): + """ + We always want to load the objects into memory so that we can display + them to the user in confirm page. + """ + return False + def model_format_dict(obj): """ diff --git a/django/contrib/admin/views/decorators.py b/django/contrib/admin/views/decorators.py index b5313a162e..e19265fc83 100644 --- a/django/contrib/admin/views/decorators.py +++ b/django/contrib/admin/views/decorators.py @@ -4,6 +4,7 @@ from django.contrib.admin.forms import AdminAuthenticationForm from django.contrib.auth.views import login from django.contrib.auth import REDIRECT_FIELD_NAME + def staff_member_required(view_func): """ Decorator for views that checks that the user is logged in and is a staff diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 74ef095b4b..5033ba98bc 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -3,6 +3,7 @@ from functools import reduce from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured from django.core.paginator import InvalidPage +from django.core.urlresolvers import reverse from django.db import models from django.db.models.fields import FieldDoesNotExist from django.utils.datastructures import SortedDict @@ -376,4 +377,8 @@ class ChangeList(object): return qs def url_for_result(self, result): - return "%s/" % quote(getattr(result, self.pk_attname)) + pk = getattr(result, self.pk_attname) + return reverse('admin:%s_%s_change' % (self.opts.app_label, + self.opts.module_name), + args=(quote(pk),), + current_app=self.model_admin.admin_site.name) diff --git a/django/contrib/admindocs/locale/en/LC_MESSAGES/django.po b/django/contrib/admindocs/locale/en/LC_MESSAGES/django.po index 8f55140ff2..f58586f414 100644 --- a/django/contrib/admindocs/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/admindocs/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-03-23 02:36+0100\n" +"POT-Creation-Date: 2012-10-22 09:28+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -13,63 +13,75 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: views.py:57 views.py:59 views.py:61 +#: views.py:58 views.py:60 views.py:62 msgid "tag:" msgstr "" -#: views.py:92 views.py:94 views.py:96 +#: views.py:93 views.py:95 views.py:97 msgid "filter:" msgstr "" -#: views.py:155 views.py:157 views.py:159 +#: views.py:156 views.py:158 views.py:160 msgid "view:" msgstr "" -#: views.py:187 +#: views.py:188 #, python-format msgid "App %r not found" msgstr "" -#: views.py:194 +#: views.py:195 #, python-format msgid "Model %(model_name)r not found in app %(app_label)r" msgstr "" -#: views.py:206 +#: views.py:207 #, python-format msgid "the related `%(app_label)s.%(data_type)s` object" msgstr "" -#: views.py:206 views.py:225 views.py:230 views.py:244 views.py:258 -#: views.py:263 +#: views.py:207 views.py:226 views.py:231 views.py:245 views.py:259 +#: views.py:264 msgid "model:" msgstr "" -#: views.py:221 views.py:253 +#: views.py:222 views.py:254 #, python-format msgid "related `%(app_label)s.%(object_name)s` objects" msgstr "" -#: views.py:225 views.py:258 +#: views.py:226 views.py:259 #, python-format msgid "all %s" msgstr "" -#: views.py:230 views.py:263 +#: views.py:231 views.py:264 #, python-format msgid "number of %s" msgstr "" -#: views.py:268 +#: views.py:269 #, python-format msgid "Fields on %s objects" msgstr "" -#: views.py:360 +#: views.py:361 #, python-format msgid "%s does not appear to be a urlpattern object" msgstr "" +#: templates/admin_doc/bookmarklets.html:6 templates/admin_doc/index.html:6 +#: templates/admin_doc/missing_docutils.html:6 +#: templates/admin_doc/model_detail.html:14 +#: templates/admin_doc/model_index.html:8 +#: templates/admin_doc/template_detail.html:6 +#: templates/admin_doc/template_filter_index.html:7 +#: templates/admin_doc/template_tag_index.html:7 +#: templates/admin_doc/view_detail.html:6 +#: templates/admin_doc/view_index.html:7 +msgid "Home" +msgstr "" + #: templates/admin_doc/bookmarklets.html:7 templates/admin_doc/index.html:7 #: templates/admin_doc/missing_docutils.html:7 #: templates/admin_doc/model_detail.html:15 @@ -79,30 +91,18 @@ msgstr "" #: templates/admin_doc/template_tag_index.html:8 #: templates/admin_doc/view_detail.html:7 #: templates/admin_doc/view_index.html:8 -msgid "Home" -msgstr "" - -#: templates/admin_doc/bookmarklets.html:8 templates/admin_doc/index.html:8 -#: templates/admin_doc/missing_docutils.html:8 -#: templates/admin_doc/model_detail.html:16 -#: templates/admin_doc/model_index.html:10 -#: templates/admin_doc/template_detail.html:8 -#: templates/admin_doc/template_filter_index.html:9 -#: templates/admin_doc/template_tag_index.html:9 -#: templates/admin_doc/view_detail.html:8 -#: templates/admin_doc/view_index.html:9 msgid "Documentation" msgstr "" -#: templates/admin_doc/bookmarklets.html:9 +#: templates/admin_doc/bookmarklets.html:8 msgid "Bookmarklets" msgstr "" -#: templates/admin_doc/bookmarklets.html:12 +#: templates/admin_doc/bookmarklets.html:11 msgid "Documentation bookmarklets" msgstr "" -#: templates/admin_doc/bookmarklets.html:16 +#: templates/admin_doc/bookmarklets.html:15 msgid "" "\n" "

To install bookmarklets, drag the link to your bookmarks\n" @@ -113,60 +113,69 @@ msgid "" "your computer is \"internal\").

\n" msgstr "" -#: templates/admin_doc/bookmarklets.html:26 +#: templates/admin_doc/bookmarklets.html:25 msgid "Documentation for this page" msgstr "" -#: templates/admin_doc/bookmarklets.html:27 +#: templates/admin_doc/bookmarklets.html:26 msgid "" "Jumps you from any page to the documentation for the view that generates " "that page." msgstr "" -#: templates/admin_doc/bookmarklets.html:29 +#: templates/admin_doc/bookmarklets.html:28 msgid "Show object ID" msgstr "" -#: templates/admin_doc/bookmarklets.html:30 +#: templates/admin_doc/bookmarklets.html:29 msgid "" "Shows the content-type and unique ID for pages that represent a single " "object." msgstr "" -#: templates/admin_doc/bookmarklets.html:32 +#: templates/admin_doc/bookmarklets.html:31 msgid "Edit this object (current window)" msgstr "" -#: templates/admin_doc/bookmarklets.html:33 +#: templates/admin_doc/bookmarklets.html:32 msgid "Jumps to the admin page for pages that represent a single object." msgstr "" -#: templates/admin_doc/bookmarklets.html:35 +#: templates/admin_doc/bookmarklets.html:34 msgid "Edit this object (new window)" msgstr "" -#: templates/admin_doc/bookmarklets.html:36 +#: templates/admin_doc/bookmarklets.html:35 msgid "As above, but opens the admin page in a new window." msgstr "" -#: templates/admin_doc/model_detail.html:17 -#: templates/admin_doc/model_index.html:11 +#: templates/admin_doc/model_detail.html:16 +#: templates/admin_doc/model_index.html:10 msgid "Models" msgstr "" -#: templates/admin_doc/template_detail.html:9 +#: templates/admin_doc/template_detail.html:8 msgid "Templates" msgstr "" -#: templates/admin_doc/template_filter_index.html:10 +#: templates/admin_doc/template_filter_index.html:9 msgid "Filters" msgstr "" -#: templates/admin_doc/template_tag_index.html:10 +#: templates/admin_doc/template_tag_index.html:9 msgid "Tags" msgstr "" -#: templates/admin_doc/view_detail.html:9 -#: templates/admin_doc/view_index.html:10 +#: templates/admin_doc/view_detail.html:8 +#: templates/admin_doc/view_index.html:9 msgid "Views" msgstr "" + +#: tests/__init__.py:23 +msgid "Boolean (Either True or False)" +msgstr "" + +#: tests/__init__.py:33 +#, python-format +msgid "Field of type: %(field_type)s" +msgstr "" diff --git a/django/contrib/admindocs/tests/__init__.py b/django/contrib/admindocs/tests/__init__.py index 306475beb1..aa5bd6a8dc 100644 --- a/django/contrib/admindocs/tests/__init__.py +++ b/django/contrib/admindocs/tests/__init__.py @@ -26,7 +26,7 @@ class TestFieldType(unittest.TestCase): def test_custom_fields(self): self.assertEqual( views.get_readable_field_data_type(fields.CustomField()), - _('A custom field type') + 'A custom field type' ) self.assertEqual( views.get_readable_field_data_type(fields.DescriptionLackingField()), diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index 0b3ccf7d8c..dd4a8484f5 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -1,14 +1,17 @@ +import re + from django.core.exceptions import ImproperlyConfigured from django.utils.importlib import import_module -from django.contrib.auth.signals import user_logged_in, user_logged_out +from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed SESSION_KEY = '_auth_user_id' BACKEND_SESSION_KEY = '_auth_user_backend' REDIRECT_FIELD_NAME = 'next' + def load_backend(path): i = path.rfind('.') - module, attr = path[:i], path[i+1:] + module, attr = path[:i], path[i + 1:] try: mod = import_module(module) except ImportError as e: @@ -21,6 +24,7 @@ def load_backend(path): raise ImproperlyConfigured('Module "%s" does not define a "%s" authentication backend' % (module, attr)) return cls() + def get_backends(): from django.conf import settings backends = [] @@ -30,6 +34,22 @@ def get_backends(): raise ImproperlyConfigured('No authentication backends have been defined. Does AUTHENTICATION_BACKENDS contain anything?') return backends + +def _clean_credentials(credentials): + """ + Cleans a dictionary of credentials of potentially sensitive info before + sending to less secure functions. + + Not comprehensive - intended for user_login_failed signal + """ + SENSITIVE_CREDENTIALS = re.compile('api|token|key|secret|password|signature', re.I) + CLEANSED_SUBSTITUTE = '********************' + for key in credentials: + if SENSITIVE_CREDENTIALS.search(key): + credentials[key] = CLEANSED_SUBSTITUTE + return credentials + + def authenticate(**credentials): """ If the given credentials are valid, return a User object. @@ -46,6 +66,11 @@ def authenticate(**credentials): user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) return user + # The credentials supplied are invalid to all backends, fire signal + user_login_failed.send(sender=__name__, + credentials=_clean_credentials(credentials)) + + def login(request, user): """ Persist a user id and a backend in the request. This way a user doesn't @@ -69,6 +94,7 @@ def login(request, user): request.user = user user_logged_in.send(sender=user.__class__, request=request, user=user) + def logout(request): """ Removes the authenticated user's ID from the request and flushes their @@ -86,6 +112,22 @@ def logout(request): from django.contrib.auth.models import AnonymousUser request.user = AnonymousUser() + +def get_user_model(): + "Return the User model that is active in this project" + from django.conf import settings + from django.db.models import get_model + + try: + app_label, model_name = settings.AUTH_USER_MODEL.split('.') + except ValueError: + raise ImproperlyConfigured("AUTH_USER_MODEL must be of the form 'app_label.model_name'") + user_model = get_model(app_label, model_name) + if user_model is None: + raise ImproperlyConfigured("AUTH_USER_MODEL refers to model '%s' that has not been installed" % settings.AUTH_USER_MODEL) + return user_model + + def get_user(request): from django.contrib.auth.models import AnonymousUser try: diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py index ccf940d16d..5f476f91c2 100644 --- a/django/contrib/auth/admin.py +++ b/django/contrib/auth/admin.py @@ -11,14 +11,13 @@ from django.shortcuts import get_object_or_404 from django.template.response import TemplateResponse from django.utils.html import escape from django.utils.decorators import method_decorator -from django.utils.safestring import mark_safe -from django.utils import six from django.utils.translation import ugettext, ugettext_lazy as _ from django.views.decorators.csrf import csrf_protect from django.views.decorators.debug import sensitive_post_parameters csrf_protect_m = method_decorator(csrf_protect) + class GroupAdmin(admin.ModelAdmin): search_fields = ('name',) ordering = ('name',) @@ -54,10 +53,10 @@ class UserAdmin(admin.ModelAdmin): add_form = UserCreationForm change_password_form = AdminPasswordChangeForm list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') - list_filter = ('is_staff', 'is_superuser', 'is_active') + list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups') search_fields = ('username', 'first_name', 'last_name', 'email') ordering = ('username',) - filter_horizontal = ('user_permissions',) + filter_horizontal = ('groups', 'user_permissions',) def get_fieldsets(self, request, obj=None): if not obj: @@ -106,9 +105,10 @@ class UserAdmin(admin.ModelAdmin): raise PermissionDenied if extra_context is None: extra_context = {} + username_field = self.model._meta.get_field(self.model.USERNAME_FIELD) defaults = { 'auto_populated_fields': (), - 'username_help_text': self.model._meta.get_field('username').help_text, + 'username_help_text': username_field.help_text, } extra_context.update(defaults) return super(UserAdmin, self).add_view(request, form_url, @@ -153,7 +153,7 @@ class UserAdmin(admin.ModelAdmin): 'admin/auth/user/change_password.html' ], context, current_app=self.admin_site.name) - def response_add(self, request, obj, post_url_continue='../%s/'): + def response_add(self, request, obj, **kwargs): """ Determines the HttpResponse for the add_view stage. It mostly defers to its superclass implementation but is customized because the User model @@ -166,9 +166,7 @@ class UserAdmin(admin.ModelAdmin): # * We are adding a user in a popup if '_addanother' not in request.POST and '_popup' not in request.POST: request.POST['_continue'] = 1 - return super(UserAdmin, self).response_add(request, obj, - post_url_continue) + return super(UserAdmin, self).response_add(request, obj, **kwargs) admin.site.register(Group, GroupAdmin) admin.site.register(User, UserAdmin) - diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py index 9088e2fbf6..db99c94838 100644 --- a/django/contrib/auth/backends.py +++ b/django/contrib/auth/backends.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals - -from django.contrib.auth.models import User, Permission +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission class ModelBackend(object): @@ -12,10 +12,11 @@ class ModelBackend(object): # configurable. def authenticate(self, username=None, password=None): try: - user = User.objects.get(username=username) + UserModel = get_user_model() + user = UserModel.objects.get_by_natural_key(username) if user.check_password(password): return user - except User.DoesNotExist: + except UserModel.DoesNotExist: return None def get_group_permissions(self, user_obj, obj=None): @@ -29,7 +30,9 @@ class ModelBackend(object): if user_obj.is_superuser: perms = Permission.objects.all() else: - perms = Permission.objects.filter(group__user=user_obj) + user_groups_field = get_user_model()._meta.get_field('groups') + user_groups_query = 'group__%s' % user_groups_field.related_query_name() + perms = Permission.objects.filter(**{user_groups_query: user_obj}) perms = perms.values_list('content_type__app_label', 'codename').order_by() user_obj._group_perm_cache = set(["%s.%s" % (ct, name) for ct, name in perms]) return user_obj._group_perm_cache @@ -60,8 +63,9 @@ class ModelBackend(object): def get_user(self, user_id): try: - return User.objects.get(pk=user_id) - except User.DoesNotExist: + UserModel = get_user_model() + return UserModel.objects.get(pk=user_id) + except UserModel.DoesNotExist: return None @@ -94,17 +98,21 @@ class RemoteUserBackend(ModelBackend): user = None username = self.clean_username(remote_user) + UserModel = get_user_model() + # Note that this could be accomplished in one try-except clause, but # instead we use get_or_create when creating unknown users since it has # built-in safeguards for multiple threads. if self.create_unknown_user: - user, created = User.objects.get_or_create(username=username) + user, created = UserModel.objects.get_or_create(**{ + UserModel.USERNAME_FIELD: username + }) if created: user = self.configure_user(user) else: try: - user = User.objects.get(username=username) - except User.DoesNotExist: + user = UserModel.objects.get_by_natural_key(username) + except UserModel.DoesNotExist: pass return user diff --git a/django/contrib/auth/context_processors.py b/django/contrib/auth/context_processors.py index 1b6c2eedd0..5929505359 100644 --- a/django/contrib/auth/context_processors.py +++ b/django/contrib/auth/context_processors.py @@ -11,6 +11,11 @@ class PermLookupDict(object): def __getitem__(self, perm_name): return self.user.has_perm("%s.%s" % (self.module_name, perm_name)) + def __iter__(self): + # To fix 'item in perms.someapp' and __getitem__ iteraction we need to + # define __iter__. See #18979 for details. + raise TypeError("PermLookupDict is not iterable.") + def __bool__(self): return self.user.has_module_perms(self.module_name) __nonzero__ = __bool__ # Python 2 @@ -27,6 +32,17 @@ class PermWrapper(object): # I am large, I contain multitudes. raise TypeError("PermWrapper is not iterable.") + def __contains__(self, perm_name): + """ + Lookup by "someapp" or "someapp.someperm" in perms. + """ + if '.' not in perm_name: + # The name refers to module. + return bool(self[perm_name]) + module_name, perm_name = perm_name.split('.', 1) + return self[module_name][perm_name] + + def auth(request): """ Returns context variables required by apps that use Django's authentication diff --git a/django/contrib/auth/fixtures/custom_user.json b/django/contrib/auth/fixtures/custom_user.json new file mode 100644 index 0000000000..770bea6541 --- /dev/null +++ b/django/contrib/auth/fixtures/custom_user.json @@ -0,0 +1,14 @@ +[ + { + "pk": "1", + "model": "auth.customuser", + "fields": { + "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", + "last_login": "2006-12-17 07:03:31", + "email": "staffmember@example.com", + "is_active": true, + "is_admin": false, + "date_of_birth": "1976-11-08" + } + } +] \ No newline at end of file diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 08488237c7..423e3429e6 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -7,9 +7,10 @@ from django.utils.datastructures import SortedDict from django.utils.html import format_html, format_html_join from django.utils.http import int_to_base36 from django.utils.safestring import mark_safe +from django.utils.text import capfirst from django.utils.translation import ugettext, ugettext_lazy as _ -from django.contrib.auth import authenticate +from django.contrib.auth import authenticate, get_user_model from django.contrib.auth.models import User from django.contrib.auth.hashers import UNUSABLE_PASSWORD, identify_hasher from django.contrib.auth.tokens import default_token_generator @@ -117,9 +118,6 @@ class UserChangeForm(forms.ModelForm): "this user's password, but you can change the password " "using this form.")) - def clean_password(self): - return self.initial["password"] - class Meta: model = User @@ -129,13 +127,19 @@ class UserChangeForm(forms.ModelForm): if f is not None: f.queryset = f.queryset.select_related('content_type') + def clean_password(self): + # Regardless of what the user provides, return the initial value. + # This is done here, rather than on the field, because the + # field does not have access to the initial value + return self.initial["password"] + class AuthenticationForm(forms.Form): """ Base class for authenticating users. Extend this to get a form that accepts username/password logins. """ - username = forms.CharField(label=_("Username"), max_length=30) + username = forms.CharField(max_length=254) password = forms.CharField(label=_("Password"), widget=forms.PasswordInput) error_messages = { @@ -157,6 +161,11 @@ class AuthenticationForm(forms.Form): self.user_cache = None super(AuthenticationForm, self).__init__(*args, **kwargs) + # Set the label for the "username" field. + UserModel = get_user_model() + username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD) + self.fields['username'].label = capfirst(username_field.verbose_name) + def clean(self): username = self.cleaned_data.get('username') password = self.cleaned_data.get('password') @@ -187,20 +196,21 @@ class AuthenticationForm(forms.Form): class PasswordResetForm(forms.Form): error_messages = { - 'unknown': _("That e-mail address doesn't have an associated " + 'unknown': _("That email address doesn't have an associated " "user account. Are you sure you've registered?"), - 'unusable': _("The user account associated with this e-mail " + 'unusable': _("The user account associated with this email " "address cannot reset the password."), } - email = forms.EmailField(label=_("E-mail"), max_length=75) + email = forms.EmailField(label=_("Email"), max_length=254) def clean_email(self): """ Validates that an active user exists with the given email address. """ + UserModel = get_user_model() email = self.cleaned_data["email"] - self.users_cache = User.objects.filter(email__iexact=email, - is_active=True) + self.users_cache = UserModel.objects.filter(email__iexact=email, + is_active=True) if not len(self.users_cache): raise forms.ValidationError(self.error_messages['unknown']) if any((user.password == UNUSABLE_PASSWORD) diff --git a/django/contrib/auth/handlers/modwsgi.py b/django/contrib/auth/handlers/modwsgi.py new file mode 100644 index 0000000000..3229c6714b --- /dev/null +++ b/django/contrib/auth/handlers/modwsgi.py @@ -0,0 +1,56 @@ +from django.contrib import auth +from django import db +from django.utils.encoding import force_bytes + + +def check_password(environ, username, password): + """ + Authenticates against Django's auth database + + mod_wsgi docs specify None, True, False as return value depending + on whether the user exists and authenticates. + """ + + UserModel = auth.get_user_model() + # db connection state is managed similarly to the wsgi handler + # as mod_wsgi may call these functions outside of a request/response cycle + db.reset_queries() + + try: + try: + user = UserModel.objects.get_by_natural_key(username) + except UserModel.DoesNotExist: + return None + try: + if not user.is_active: + return None + except AttributeError as e: + # a custom user may not support is_active + return None + return user.check_password(password) + finally: + db.close_connection() + + +def groups_for_user(environ, username): + """ + Authorizes a user based on groups + """ + + UserModel = auth.get_user_model() + db.reset_queries() + + try: + try: + user = UserModel.objects.get_by_natural_key(username) + except UserModel.DoesNotExist: + return [] + try: + if not user.is_active: + return [] + except AttributeError as e: + # a custom user may not support is_active + return [] + return [force_bytes(group.name) for group in user.groups.all()] + finally: + db.close_connection() diff --git a/django/contrib/auth/locale/en/LC_MESSAGES/django.po b/django/contrib/auth/locale/en/LC_MESSAGES/django.po index c70a660c5c..9b0cab8372 100644 --- a/django/contrib/auth/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/auth/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-03-23 02:36+0100\n" +"POT-Creation-Date: 2012-10-15 10:56+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -25,48 +25,56 @@ msgstr "" msgid "Important dates" msgstr "" -#: admin.py:125 +#: admin.py:126 msgid "Password changed successfully." msgstr "" -#: admin.py:135 +#: admin.py:136 #, python-format msgid "Change password: %s" msgstr "" -#: forms.py:62 +#: forms.py:31 tests/forms.py:249 tests/forms.py:254 +msgid "No password set." +msgstr "" + +#: forms.py:37 tests/forms.py:259 tests/forms.py:265 +msgid "Invalid password format or unknown hashing algorithm." +msgstr "" + +#: forms.py:65 msgid "A user with that username already exists." msgstr "" -#: forms.py:63 forms.py:251 forms.py:308 +#: forms.py:66 forms.py:257 forms.py:317 msgid "The two password fields didn't match." msgstr "" -#: forms.py:65 forms.py:110 forms.py:139 +#: forms.py:68 forms.py:113 msgid "Username" msgstr "" -#: forms.py:67 forms.py:111 +#: forms.py:70 forms.py:114 msgid "Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only." msgstr "" -#: forms.py:70 forms.py:114 +#: forms.py:73 forms.py:117 msgid "This value may contain only letters, numbers and @/./+/-/_ characters." msgstr "" -#: forms.py:72 forms.py:116 forms.py:140 forms.py:310 +#: forms.py:75 forms.py:119 forms.py:140 forms.py:319 msgid "Password" msgstr "" -#: forms.py:74 +#: forms.py:77 msgid "Password confirmation" msgstr "" -#: forms.py:76 +#: forms.py:79 msgid "Enter the same password as above, for verification." msgstr "" -#: forms.py:117 +#: forms.py:120 msgid "" "Raw passwords are not stored, so there is no way to see this user's " "password, but you can change the password using this " @@ -89,178 +97,178 @@ msgstr "" msgid "This account is inactive." msgstr "" -#: forms.py:191 +#: forms.py:196 msgid "" -"That e-mail address doesn't have an associated user account. Are you sure " +"That email address doesn't have an associated user account. Are you sure " "you've registered?" msgstr "" -#: forms.py:193 +#: forms.py:198 tests/forms.py:347 msgid "" -"The user account associated with this e-mail address cannot reset the " +"The user account associated with this email address cannot reset the " "password." msgstr "" -#: forms.py:196 -msgid "E-mail" +#: forms.py:201 +msgid "Email" msgstr "" -#: forms.py:253 +#: forms.py:259 msgid "New password" msgstr "" -#: forms.py:255 +#: forms.py:261 msgid "New password confirmation" msgstr "" -#: forms.py:284 +#: forms.py:290 msgid "Your old password was entered incorrectly. Please enter it again." msgstr "" -#: forms.py:287 +#: forms.py:293 msgid "Old password" msgstr "" -#: forms.py:312 +#: forms.py:321 msgid "Password (again)" msgstr "" -#: hashers.py:218 hashers.py:269 hashers.py:298 hashers.py:326 hashers.py:355 -#: hashers.py:389 +#: hashers.py:241 hashers.py:292 hashers.py:321 hashers.py:349 hashers.py:378 +#: hashers.py:412 msgid "algorithm" msgstr "" -#: hashers.py:219 +#: hashers.py:242 msgid "iterations" msgstr "" -#: hashers.py:220 hashers.py:271 hashers.py:299 hashers.py:327 hashers.py:390 +#: hashers.py:243 hashers.py:294 hashers.py:322 hashers.py:350 hashers.py:413 msgid "salt" msgstr "" -#: hashers.py:221 hashers.py:300 hashers.py:328 hashers.py:356 hashers.py:391 +#: hashers.py:244 hashers.py:323 hashers.py:351 hashers.py:379 hashers.py:414 msgid "hash" msgstr "" -#: hashers.py:270 +#: hashers.py:293 msgid "work factor" msgstr "" -#: hashers.py:272 +#: hashers.py:295 msgid "checksum" msgstr "" -#: models.py:66 models.py:113 +#: models.py:72 models.py:121 msgid "name" msgstr "" -#: models.py:68 +#: models.py:74 msgid "codename" msgstr "" -#: models.py:72 +#: models.py:78 msgid "permission" msgstr "" -#: models.py:73 models.py:115 +#: models.py:79 models.py:123 msgid "permissions" msgstr "" -#: models.py:120 +#: models.py:128 msgid "group" msgstr "" -#: models.py:121 models.py:250 +#: models.py:129 models.py:317 msgid "groups" msgstr "" #: models.py:232 -msgid "username" +msgid "password" msgstr "" #: models.py:233 +msgid "last login" +msgstr "" + +#: models.py:298 +msgid "username" +msgstr "" + +#: models.py:299 msgid "" "Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters" msgstr "" -#: models.py:235 +#: models.py:302 +msgid "Enter a valid username." +msgstr "" + +#: models.py:304 msgid "first name" msgstr "" -#: models.py:236 +#: models.py:305 msgid "last name" msgstr "" -#: models.py:237 -msgid "e-mail address" +#: models.py:306 +msgid "email address" msgstr "" -#: models.py:238 -msgid "password" -msgstr "" - -#: models.py:239 +#: models.py:307 msgid "staff status" msgstr "" -#: models.py:240 +#: models.py:308 msgid "Designates whether the user can log into this admin site." msgstr "" -#: models.py:242 +#: models.py:310 msgid "active" msgstr "" -#: models.py:243 +#: models.py:311 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." msgstr "" -#: models.py:245 +#: models.py:313 msgid "superuser status" msgstr "" -#: models.py:246 +#: models.py:314 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." msgstr "" -#: models.py:248 -msgid "last login" -msgstr "" - -#: models.py:249 +#: models.py:316 msgid "date joined" msgstr "" -#: models.py:251 +#: models.py:318 msgid "" "The groups this user belongs to. A user will get all permissions granted to " "each of his/her group." msgstr "" -#: models.py:255 +#: models.py:322 msgid "user permissions" msgstr "" -#: models.py:260 +#: models.py:331 msgid "user" msgstr "" -#: models.py:261 +#: models.py:332 msgid "users" msgstr "" -#: views.py:93 +#: views.py:97 msgid "Logged out" msgstr "" -#: management/commands/createsuperuser.py:27 -msgid "Enter a valid e-mail address." -msgstr "" - #: templates/registration/password_reset_subject.txt:2 #, python-format msgid "Password reset on %(site_name)s" diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py index 23a053d985..b5fd29a1c2 100644 --- a/django/contrib/auth/management/__init__.py +++ b/django/contrib/auth/management/__init__.py @@ -6,9 +6,11 @@ from __future__ import unicode_literals import getpass import locale import unicodedata -from django.contrib.auth import models as auth_app + +from django.contrib.auth import models as auth_app, get_user_model +from django.core import exceptions +from django.core.management.base import CommandError from django.db.models import get_models, signals -from django.contrib.auth.models import User from django.utils import six from django.utils.six.moves import input @@ -17,13 +19,43 @@ def _get_permission_codename(action, opts): return '%s_%s' % (action, opts.object_name.lower()) -def _get_all_permissions(opts): - "Returns (codename, name) for all permissions in the given opts." +def _get_all_permissions(opts, ctype): + """ + Returns (codename, name) for all permissions in the given opts. + """ + builtin = _get_builtin_permissions(opts) + custom = list(opts.permissions) + _check_permission_clashing(custom, builtin, ctype) + return builtin + custom + +def _get_builtin_permissions(opts): + """ + Returns (codename, name) for all autogenerated permissions. + """ perms = [] for action in ('add', 'change', 'delete'): - perms.append((_get_permission_codename(action, opts), 'Can %s %s' % (action, opts.verbose_name_raw))) - return perms + list(opts.permissions) + perms.append((_get_permission_codename(action, opts), + 'Can %s %s' % (action, opts.verbose_name_raw))) + return perms +def _check_permission_clashing(custom, builtin, ctype): + """ + Check that permissions for a model do not clash. Raises CommandError if + there are duplicate permissions. + """ + pool = set() + builtin_codenames = set(p[0] for p in builtin) + for codename, _name in custom: + if codename in pool: + raise CommandError( + "The permission codename '%s' is duplicated for model '%s.%s'." % + (codename, ctype.app_label, ctype.model_class().__name__)) + elif codename in builtin_codenames: + raise CommandError( + "The permission codename '%s' clashes with a builtin permission " + "for model '%s.%s'." % + (codename, ctype.app_label, ctype.model_class().__name__)) + pool.add(codename) def create_permissions(app, created_models, verbosity, **kwargs): from django.contrib.contenttypes.models import ContentType @@ -38,7 +70,7 @@ def create_permissions(app, created_models, verbosity, **kwargs): for klass in app_models: ctype = ContentType.objects.get_for_model(klass) ctypes.add(ctype) - for perm in _get_all_permissions(klass._meta): + for perm in _get_all_permissions(klass._meta, ctype): searched_perms.append((ctype, perm)) # Find all the Permissions that have a context_type for a model we're @@ -64,7 +96,9 @@ def create_permissions(app, created_models, verbosity, **kwargs): def create_superuser(app, created_models, verbosity, db, **kwargs): from django.core.management import call_command - if auth_app.User in created_models and kwargs.get('interactive', True): + UserModel = get_user_model() + + if UserModel in created_models and kwargs.get('interactive', True): msg = ("\nYou just installed Django's auth system, which means you " "don't have any superusers defined.\nWould you like to create one " "now? (yes/no): ") @@ -113,28 +147,35 @@ def get_default_username(check_db=True): :returns: The username, or an empty string if no username can be determined. """ - from django.contrib.auth.management.commands.createsuperuser import ( - RE_VALID_USERNAME) + # If the User model has been swapped out, we can't make any assumptions + # about the default user name. + if auth_app.User._meta.swapped: + return '' + default_username = get_system_username() try: default_username = unicodedata.normalize('NFKD', default_username)\ .encode('ascii', 'ignore').decode('ascii').replace(' ', '').lower() except UnicodeDecodeError: return '' - if not RE_VALID_USERNAME.match(default_username): + + # Run the username validator + try: + auth_app.User._meta.get_field('username').run_validators(default_username) + except exceptions.ValidationError: return '' + # Don't return the default username if it is already taken. if check_db and default_username: try: - User.objects.get(username=default_username) - except User.DoesNotExist: + auth_app.User.objects.get(username=default_username) + except auth_app.User.DoesNotExist: pass else: return '' return default_username - signals.post_syncdb.connect(create_permissions, - dispatch_uid = "django.contrib.auth.management.create_permissions") + dispatch_uid="django.contrib.auth.management.create_permissions") signals.post_syncdb.connect(create_superuser, - sender=auth_app, dispatch_uid = "django.contrib.auth.management.create_superuser") + sender=auth_app, dispatch_uid="django.contrib.auth.management.create_superuser") diff --git a/django/contrib/auth/management/commands/changepassword.py b/django/contrib/auth/management/commands/changepassword.py index d125dfe5b6..ff38836a95 100644 --- a/django/contrib/auth/management/commands/changepassword.py +++ b/django/contrib/auth/management/commands/changepassword.py @@ -1,8 +1,8 @@ import getpass from optparse import make_option +from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand, CommandError -from django.contrib.auth.models import User from django.db import DEFAULT_DB_ALIAS @@ -30,12 +30,16 @@ class Command(BaseCommand): else: username = getpass.getuser() + UserModel = get_user_model() + try: - u = User.objects.using(options.get('database')).get(username=username) - except User.DoesNotExist: + u = UserModel.objects.using(options.get('database')).get(**{ + UserModel.USERNAME_FIELD: username + }) + except UserModel.DoesNotExist: raise CommandError("user '%s' does not exist" % username) - self.stdout.write("Changing password for user '%s'\n" % u.username) + self.stdout.write("Changing password for user '%s'\n" % u) MAX_TRIES = 3 count = 0 @@ -48,9 +52,9 @@ class Command(BaseCommand): count = count + 1 if count == MAX_TRIES: - raise CommandError("Aborting password change for user '%s' after %s attempts" % (username, count)) + raise CommandError("Aborting password change for user '%s' after %s attempts" % (u, count)) u.set_password(p1) u.save() - return "Password changed successfully for user '%s'" % u.username + return "Password changed successfully for user '%s'" % u diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py index 6e0d0bc754..216d56d730 100644 --- a/django/contrib/auth/management/commands/createsuperuser.py +++ b/django/contrib/auth/management/commands/createsuperuser.py @@ -3,109 +3,119 @@ Management utility to create superusers. """ import getpass -import re import sys from optparse import make_option -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.contrib.auth.management import get_default_username from django.core import exceptions from django.core.management.base import BaseCommand, CommandError from django.db import DEFAULT_DB_ALIAS from django.utils.six.moves import input -from django.utils.translation import ugettext as _ - -RE_VALID_USERNAME = re.compile('[\w.@+-]+$') - -EMAIL_RE = re.compile( - r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom - r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"' # quoted-string - r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain - - -def is_valid_email(value): - if not EMAIL_RE.search(value): - raise exceptions.ValidationError(_('Enter a valid e-mail address.')) +from django.utils.text import capfirst class Command(BaseCommand): - option_list = BaseCommand.option_list + ( - make_option('--username', dest='username', default=None, - help='Specifies the username for the superuser.'), - make_option('--email', dest='email', default=None, - help='Specifies the email address for the superuser.'), - make_option('--noinput', action='store_false', dest='interactive', default=True, - help=('Tells Django to NOT prompt the user for input of any kind. ' - 'You must use --username and --email with --noinput, and ' - 'superusers created with --noinput will not be able to log ' - 'in until they\'re given a valid password.')), - make_option('--database', action='store', dest='database', - default=DEFAULT_DB_ALIAS, help='Specifies the database to use. Default is "default".'), - ) + + def __init__(self, *args, **kwargs): + # Options are defined in an __init__ method to support swapping out + # custom user models in tests. + super(Command, self).__init__(*args, **kwargs) + self.UserModel = get_user_model() + self.username_field = self.UserModel._meta.get_field(self.UserModel.USERNAME_FIELD) + + self.option_list = BaseCommand.option_list + ( + make_option('--%s' % self.UserModel.USERNAME_FIELD, dest=self.UserModel.USERNAME_FIELD, default=None, + help='Specifies the login for the superuser.'), + make_option('--noinput', action='store_false', dest='interactive', default=True, + help=('Tells Django to NOT prompt the user for input of any kind. ' + 'You must use --%s with --noinput, along with an option for ' + 'any other required field. Superusers created with --noinput will ' + ' not be able to log in until they\'re given a valid password.' % + self.UserModel.USERNAME_FIELD)), + make_option('--database', action='store', dest='database', + default=DEFAULT_DB_ALIAS, help='Specifies the database to use. Default is "default".'), + ) + tuple( + make_option('--%s' % field, dest=field, default=None, + help='Specifies the %s for the superuser.' % field) + for field in self.UserModel.REQUIRED_FIELDS + ) + + option_list = BaseCommand.option_list help = 'Used to create a superuser.' def handle(self, *args, **options): - username = options.get('username', None) - email = options.get('email', None) + username = options.get(self.UserModel.USERNAME_FIELD, None) interactive = options.get('interactive') verbosity = int(options.get('verbosity', 1)) database = options.get('database') - # Do quick and dirty validation if --noinput - if not interactive: - if not username or not email: - raise CommandError("You must use --username and --email with --noinput.") - if not RE_VALID_USERNAME.match(username): - raise CommandError("Invalid username. Use only letters, digits, and underscores") - try: - is_valid_email(email) - except exceptions.ValidationError: - raise CommandError("Invalid email address.") - # If not provided, create the user with an unusable password password = None + user_data = {} - # Prompt for username/email/password. Enclose this whole thing in a - # try/except to trap for a keyboard interrupt and exit gracefully. - if interactive: + # Do quick and dirty validation if --noinput + if not interactive: + try: + if not username: + raise CommandError("You must use --%s with --noinput." % + self.UserModel.USERNAME_FIELD) + username = self.username_field.clean(username, None) + + for field_name in self.UserModel.REQUIRED_FIELDS: + if options.get(field_name): + field = self.UserModel._meta.get_field(field_name) + user_data[field_name] = field.clean(options[field_name], None) + else: + raise CommandError("You must use --%s with --noinput." % field_name) + except exceptions.ValidationError as e: + raise CommandError('; '.join(e.messages)) + + else: + # Prompt for username/password, and any other required fields. + # Enclose this whole thing in a try/except to trap for a + # keyboard interrupt and exit gracefully. default_username = get_default_username() try: # Get a username - while 1: + while username is None: if not username: - input_msg = 'Username' + input_msg = capfirst(self.username_field.verbose_name) if default_username: - input_msg += ' (leave blank to use %r)' % default_username - username = input(input_msg + ': ') - if default_username and username == '': - username = default_username - if not RE_VALID_USERNAME.match(username): - self.stderr.write("Error: That username is invalid. Use only letters, digits and underscores.") + input_msg += " (leave blank to use '%s')" % default_username + raw_value = input(input_msg + ': ') + + if default_username and raw_value == '': + raw_value = default_username + try: + username = self.username_field.clean(raw_value, None) + except exceptions.ValidationError as e: + self.stderr.write("Error: %s" % '; '.join(e.messages)) username = None continue try: - User.objects.using(database).get(username=username) - except User.DoesNotExist: - break + self.UserModel.objects.db_manager(database).get_by_natural_key(username) + except self.UserModel.DoesNotExist: + pass else: - self.stderr.write("Error: That username is already taken.") + self.stderr.write("Error: That %s is already taken." % + self.username_field.verbose_name) username = None - # Get an email - while 1: - if not email: - email = input('E-mail address: ') - try: - is_valid_email(email) - except exceptions.ValidationError: - self.stderr.write("Error: That e-mail address is invalid.") - email = None - else: - break + for field_name in self.UserModel.REQUIRED_FIELDS: + field = self.UserModel._meta.get_field(field_name) + user_data[field_name] = options.get(field_name) + while user_data[field_name] is None: + raw_value = input(capfirst(field.verbose_name + ': ')) + try: + user_data[field_name] = field.clean(raw_value, None) + except exceptions.ValidationError as e: + self.stderr.write("Error: %s" % '; '.join(e.messages)) + user_data[field_name] = None # Get a password - while 1: + while password is None: if not password: password = getpass.getpass() password2 = getpass.getpass('Password (again): ') @@ -117,12 +127,13 @@ class Command(BaseCommand): self.stderr.write("Error: Blank passwords aren't allowed.") password = None continue - break + except KeyboardInterrupt: self.stderr.write("\nOperation cancelled.") sys.exit(1) - User.objects.db_manager(database).create_superuser(username, email, password) + user_data[self.UserModel.USERNAME_FIELD] = username + user_data['password'] = password + self.UserModel.objects.db_manager(database).create_superuser(**user_data) if verbosity >= 1: - self.stdout.write("Superuser created successfully.") - + self.stdout.write("Superuser created successfully.") diff --git a/django/contrib/auth/middleware.py b/django/contrib/auth/middleware.py index df616a9243..0398cfaf1e 100644 --- a/django/contrib/auth/middleware.py +++ b/django/contrib/auth/middleware.py @@ -55,7 +55,7 @@ class RemoteUserMiddleware(object): # getting passed in the headers, then the correct user is already # persisted in the session and we don't need to continue. if request.user.is_authenticated(): - if request.user.username == self.clean_username(username, request): + if request.user.get_username() == self.clean_username(username, request): return # We are seeing this user for the first time in this session, attempt # to authenticate the user. @@ -75,6 +75,6 @@ class RemoteUserMiddleware(object): backend = auth.load_backend(backend_str) try: username = backend.clean_username(username) - except AttributeError: # Backend has no clean_username method. + except AttributeError: # Backend has no clean_username method. pass return username diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 98eb44ea05..bd7bf4a162 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -1,7 +1,10 @@ from __future__ import unicode_literals +import re +import warnings from django.core.exceptions import ImproperlyConfigured from django.core.mail import send_mail +from django.core import validators from django.db import models from django.db.models.manager import EmptyManager from django.utils.crypto import get_random_string @@ -96,6 +99,7 @@ class GroupManager(models.Manager): def get_by_natural_key(self, name): return self.get(name=name) + @python_2_unicode_compatible class Group(models.Model): """ @@ -131,7 +135,7 @@ class Group(models.Model): return (self.name,) -class UserManager(models.Manager): +class BaseUserManager(models.Manager): @classmethod def normalize_email(cls, email): @@ -148,30 +152,6 @@ class UserManager(models.Manager): email = '@'.join([email_name, domain_part.lower()]) return email - def create_user(self, username, email=None, password=None): - """ - Creates and saves a User with the given username, email and password. - """ - now = timezone.now() - if not username: - raise ValueError('The given username must be set') - email = UserManager.normalize_email(email) - user = self.model(username=username, email=email, - is_staff=False, is_active=True, is_superuser=False, - last_login=now, date_joined=now) - - user.set_password(password) - user.save(using=self._db) - return user - - def create_superuser(self, username, email, password): - u = self.create_user(username, email, password) - u.is_staff = True - u.is_active = True - u.is_superuser = True - u.save(using=self._db) - return u - def make_random_password(self, length=10, allowed_chars='abcdefghjkmnpqrstuvwxyz' 'ABCDEFGHJKLMNPQRSTUVWXYZ' @@ -185,7 +165,34 @@ class UserManager(models.Manager): return get_random_string(length, allowed_chars) def get_by_natural_key(self, username): - return self.get(username=username) + return self.get(**{self.model.USERNAME_FIELD: username}) + + +class UserManager(BaseUserManager): + + def create_user(self, username, email=None, password=None, **extra_fields): + """ + Creates and saves a User with the given username, email and password. + """ + now = timezone.now() + if not username: + raise ValueError('The given username must be set') + email = UserManager.normalize_email(email) + user = self.model(username=username, email=email, + is_staff=False, is_active=True, is_superuser=False, + last_login=now, date_joined=now, **extra_fields) + + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, username, email, password, **extra_fields): + u = self.create_user(username, email, password, **extra_fields) + u.is_staff = True + u.is_active = True + u.is_superuser = True + u.save(using=self._db) + return u # A few helper functions for common logic between User and AnonymousUser. @@ -201,8 +208,6 @@ def _user_get_all_permissions(user, obj): def _user_has_perm(user, perm, obj): - anon = user.is_anonymous() - active = user.is_active for backend in auth.get_backends(): if hasattr(backend, "has_perm"): if obj is not None: @@ -215,8 +220,6 @@ def _user_has_perm(user, perm, obj): def _user_has_module_perms(user, app_label): - anon = user.is_anonymous() - active = user.is_active for backend in auth.get_backends(): if hasattr(backend, "has_module_perms"): if backend.has_module_perms(user, app_label): @@ -225,52 +228,24 @@ def _user_has_module_perms(user, app_label): @python_2_unicode_compatible -class User(models.Model): - """ - Users within the Django authentication system are represented by this - model. - - Username and password are required. Other fields are optional. - """ - username = models.CharField(_('username'), max_length=30, unique=True, - help_text=_('Required. 30 characters or fewer. Letters, numbers and ' - '@/./+/-/_ characters')) - first_name = models.CharField(_('first name'), max_length=30, blank=True) - last_name = models.CharField(_('last name'), max_length=30, blank=True) - email = models.EmailField(_('e-mail address'), blank=True) +class AbstractBaseUser(models.Model): password = models.CharField(_('password'), max_length=128) - is_staff = models.BooleanField(_('staff status'), default=False, - help_text=_('Designates whether the user can log into this admin ' - 'site.')) - is_active = models.BooleanField(_('active'), default=True, - help_text=_('Designates whether this user should be treated as ' - 'active. Unselect this instead of deleting accounts.')) - is_superuser = models.BooleanField(_('superuser status'), default=False, - help_text=_('Designates that this user has all permissions without ' - 'explicitly assigning them.')) last_login = models.DateTimeField(_('last login'), default=timezone.now) - date_joined = models.DateTimeField(_('date joined'), default=timezone.now) - groups = models.ManyToManyField(Group, verbose_name=_('groups'), - blank=True, help_text=_('The groups this user belongs to. A user will ' - 'get all permissions granted to each of ' - 'his/her group.')) - user_permissions = models.ManyToManyField(Permission, - verbose_name=_('user permissions'), blank=True, - help_text='Specific permissions for this user.') - objects = UserManager() + + REQUIRED_FIELDS = [] class Meta: - verbose_name = _('user') - verbose_name_plural = _('users') + abstract = True + + def get_username(self): + "Return the identifying username for this User" + return getattr(self, self.USERNAME_FIELD) def __str__(self): - return self.username + return self.get_username() def natural_key(self): - return (self.username,) - - def get_absolute_url(self): - return "/users/%s/" % urlquote(self.username) + return (self.get_username(),) def is_anonymous(self): """ @@ -286,13 +261,6 @@ class User(models.Model): """ return True - def get_full_name(self): - """ - Returns the first_name plus the last_name, with a space in between. - """ - full_name = '%s %s' % (self.first_name, self.last_name) - return full_name.strip() - def set_password(self, raw_password): self.password = make_password(raw_password) @@ -313,6 +281,71 @@ class User(models.Model): def has_usable_password(self): return is_password_usable(self.password) + def get_full_name(self): + raise NotImplementedError() + + def get_short_name(self): + raise NotImplementedError() + + +class AbstractUser(AbstractBaseUser): + """ + An abstract base class implementing a fully featured User model with + admin-compliant permissions. + + Username, password and email are required. Other fields are optional. + """ + username = models.CharField(_('username'), max_length=30, unique=True, + help_text=_('Required. 30 characters or fewer. Letters, numbers and ' + '@/./+/-/_ characters'), + validators=[ + validators.RegexValidator(re.compile('^[\w.@+-]+$'), _('Enter a valid username.'), 'invalid') + ]) + first_name = models.CharField(_('first name'), max_length=30, blank=True) + last_name = models.CharField(_('last name'), max_length=30, blank=True) + email = models.EmailField(_('email address'), blank=True) + is_staff = models.BooleanField(_('staff status'), default=False, + help_text=_('Designates whether the user can log into this admin ' + 'site.')) + is_active = models.BooleanField(_('active'), default=True, + help_text=_('Designates whether this user should be treated as ' + 'active. Unselect this instead of deleting accounts.')) + is_superuser = models.BooleanField(_('superuser status'), default=False, + help_text=_('Designates that this user has all permissions without ' + 'explicitly assigning them.')) + date_joined = models.DateTimeField(_('date joined'), default=timezone.now) + groups = models.ManyToManyField(Group, verbose_name=_('groups'), + blank=True, help_text=_('The groups this user belongs to. A user will ' + 'get all permissions granted to each of ' + 'his/her group.')) + user_permissions = models.ManyToManyField(Permission, + verbose_name=_('user permissions'), blank=True, + help_text='Specific permissions for this user.') + + objects = UserManager() + + USERNAME_FIELD = 'username' + REQUIRED_FIELDS = ['email'] + + class Meta: + verbose_name = _('user') + verbose_name_plural = _('users') + abstract = True + + def get_absolute_url(self): + return "/users/%s/" % urlquote(self.username) + + def get_full_name(self): + """ + Returns the first_name plus the last_name, with a space in between. + """ + full_name = '%s %s' % (self.first_name, self.last_name) + return full_name.strip() + + def get_short_name(self): + "Returns the short name for the user." + return self.first_name + def get_group_permissions(self, obj=None): """ Returns a list of permission strings that this user has through his/her @@ -381,6 +414,8 @@ class User(models.Model): Returns site-specific profile for this user. Raises SiteProfileNotAvailable if this site does not allow profiles. """ + warnings.warn("The use of AUTH_PROFILE_MODULE to define user profiles has been deprecated.", + PendingDeprecationWarning) if not hasattr(self, '_profile_cache'): from django.conf import settings if not getattr(settings, 'AUTH_PROFILE_MODULE', False): @@ -407,6 +442,17 @@ class User(models.Model): return self._profile_cache +class User(AbstractUser): + """ + Users within the Django authentication system are represented by this + model. + + Username, password and email are required. Other fields are optional. + """ + class Meta: + swappable = 'AUTH_USER_MODEL' + + @python_2_unicode_compatible class AnonymousUser(object): id = None @@ -431,7 +477,7 @@ class AnonymousUser(object): return not self.__eq__(other) def __hash__(self): - return 1 # instances always return the same hash value + return 1 # instances always return the same hash value def save(self): raise NotImplementedError diff --git a/django/contrib/auth/signals.py b/django/contrib/auth/signals.py index 4f0b2c235c..71ab6a11d1 100644 --- a/django/contrib/auth/signals.py +++ b/django/contrib/auth/signals.py @@ -1,4 +1,5 @@ from django.dispatch import Signal user_logged_in = Signal(providing_args=['request', 'user']) +user_login_failed = Signal(providing_args=['credentials']) user_logged_out = Signal(providing_args=['request', 'user']) diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py index 16eaa5c5b4..b3007ea484 100644 --- a/django/contrib/auth/tests/__init__.py +++ b/django/contrib/auth/tests/__init__.py @@ -1,26 +1,16 @@ -from django.contrib.auth.tests.auth_backends import (BackendTest, - RowlevelBackendTest, AnonymousUserBackendTest, NoBackendsTest, - InActiveUserBackendTest) -from django.contrib.auth.tests.basic import BasicTestCase -from django.contrib.auth.tests.context_processors import AuthContextProcessorTests -from django.contrib.auth.tests.decorators import LoginRequiredTestCase -from django.contrib.auth.tests.forms import (UserCreationFormTest, - AuthenticationFormTest, SetPasswordFormTest, PasswordChangeFormTest, - UserChangeFormTest, PasswordResetFormTest) -from django.contrib.auth.tests.remote_user import (RemoteUserTest, - RemoteUserNoCreateTest, RemoteUserCustomTest) -from django.contrib.auth.tests.management import ( - GetDefaultUsernameTestCase, - ChangepasswordManagementCommandTestCase, -) -from django.contrib.auth.tests.models import (ProfileTestCase, NaturalKeysTestCase, - LoadDataWithoutNaturalKeysTestCase, LoadDataWithNaturalKeysTestCase, - UserManagerTestCase) -from django.contrib.auth.tests.hashers import TestUtilsHashPass -from django.contrib.auth.tests.signals import SignalTestCase -from django.contrib.auth.tests.tokens import TokenGeneratorTest -from django.contrib.auth.tests.views import (AuthViewNamedURLTests, - PasswordResetTest, ChangePasswordTest, LoginTest, LogoutTest, - LoginURLSettings) +from django.contrib.auth.tests.custom_user import * +from django.contrib.auth.tests.auth_backends import * +from django.contrib.auth.tests.basic import * +from django.contrib.auth.tests.context_processors import * +from django.contrib.auth.tests.decorators import * +from django.contrib.auth.tests.forms import * +from django.contrib.auth.tests.remote_user import * +from django.contrib.auth.tests.management import * +from django.contrib.auth.tests.models import * +from django.contrib.auth.tests.handlers import * +from django.contrib.auth.tests.hashers import * +from django.contrib.auth.tests.signals import * +from django.contrib.auth.tests.tokens import * +from django.contrib.auth.tests.views import * # The password for the fixture data users is 'password' diff --git a/django/contrib/auth/tests/auth_backends.py b/django/contrib/auth/tests/auth_backends.py index 9a4d8f9b3a..e92f159ff9 100644 --- a/django/contrib/auth/tests/auth_backends.py +++ b/django/contrib/auth/tests/auth_backends.py @@ -1,22 +1,29 @@ from __future__ import unicode_literals +from datetime import date from django.conf import settings from django.contrib.auth.models import User, Group, Permission, AnonymousUser +from django.contrib.auth.tests.utils import skipIfCustomUser +from django.contrib.auth.tests.custom_user import ExtensionUser from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured from django.test import TestCase from django.test.utils import override_settings -class BackendTest(TestCase): - +class BaseModelBackendTest(object): + """ + A base class for tests that need to validate the ModelBackend + with different User models. Subclasses should define a class + level UserModel attribute, and a create_users() method to + construct two users for test purposes. + """ backend = 'django.contrib.auth.backends.ModelBackend' def setUp(self): self.curr_auth = settings.AUTHENTICATION_BACKENDS settings.AUTHENTICATION_BACKENDS = (self.backend,) - User.objects.create_user('test', 'test@example.com', 'test') - User.objects.create_superuser('test2', 'test2@example.com', 'test') + self.create_users() def tearDown(self): settings.AUTHENTICATION_BACKENDS = self.curr_auth @@ -26,7 +33,7 @@ class BackendTest(TestCase): ContentType.objects.clear_cache() def test_has_perm(self): - user = User.objects.get(username='test') + user = self.UserModel.objects.get(username='test') self.assertEqual(user.has_perm('auth.test'), False) user.is_staff = True user.save() @@ -45,14 +52,14 @@ class BackendTest(TestCase): self.assertEqual(user.has_perm('auth.test'), False) def test_custom_perms(self): - user = User.objects.get(username='test') - content_type=ContentType.objects.get_for_model(Group) + user = self.UserModel.objects.get(username='test') + content_type = ContentType.objects.get_for_model(Group) perm = Permission.objects.create(name='test', content_type=content_type, codename='test') user.user_permissions.add(perm) user.save() # reloading user to purge the _perm_cache - user = User.objects.get(username='test') + user = self.UserModel.objects.get(username='test') self.assertEqual(user.get_all_permissions() == set(['auth.test']), True) self.assertEqual(user.get_group_permissions(), set([])) self.assertEqual(user.has_module_perms('Group'), False) @@ -63,7 +70,7 @@ class BackendTest(TestCase): perm = Permission.objects.create(name='test3', content_type=content_type, codename='test3') user.user_permissions.add(perm) user.save() - user = User.objects.get(username='test') + user = self.UserModel.objects.get(username='test') self.assertEqual(user.get_all_permissions(), set(['auth.test2', 'auth.test', 'auth.test3'])) self.assertEqual(user.has_perm('test'), False) self.assertEqual(user.has_perm('auth.test'), True) @@ -73,7 +80,7 @@ class BackendTest(TestCase): group.permissions.add(perm) group.save() user.groups.add(group) - user = User.objects.get(username='test') + user = self.UserModel.objects.get(username='test') exp = set(['auth.test2', 'auth.test', 'auth.test3', 'auth.test_group']) self.assertEqual(user.get_all_permissions(), exp) self.assertEqual(user.get_group_permissions(), set(['auth.test_group'])) @@ -85,8 +92,8 @@ class BackendTest(TestCase): def test_has_no_object_perm(self): """Regressiontest for #12462""" - user = User.objects.get(username='test') - content_type=ContentType.objects.get_for_model(Group) + user = self.UserModel.objects.get(username='test') + content_type = ContentType.objects.get_for_model(Group) perm = Permission.objects.create(name='test', content_type=content_type, codename='test') user.user_permissions.add(perm) user.save() @@ -98,9 +105,65 @@ class BackendTest(TestCase): def test_get_all_superuser_permissions(self): "A superuser has all permissions. Refs #14795" - user = User.objects.get(username='test2') + user = self.UserModel.objects.get(username='test2') self.assertEqual(len(user.get_all_permissions()), len(Permission.objects.all())) + +@skipIfCustomUser +class ModelBackendTest(BaseModelBackendTest, TestCase): + """ + Tests for the ModelBackend using the default User model. + """ + UserModel = User + + def create_users(self): + User.objects.create_user( + username='test', + email='test@example.com', + password='test', + ) + User.objects.create_superuser( + username='test2', + email='test2@example.com', + password='test', + ) + + +@override_settings(AUTH_USER_MODEL='auth.ExtensionUser') +class ExtensionUserModelBackendTest(BaseModelBackendTest, TestCase): + """ + Tests for the ModelBackend using the custom ExtensionUser model. + + This isn't a perfect test, because both the User and ExtensionUser are + synchronized to the database, which wouldn't ordinary happen in + production. As a result, it doesn't catch errors caused by the non- + existence of the User table. + + The specific problem is queries on .filter(groups__user) et al, which + makes an implicit assumption that the user model is called 'User'. In + production, the auth.User table won't exist, so the requested join + won't exist either; in testing, the auth.User *does* exist, and + so does the join. However, the join table won't contain any useful + data; for testing, we check that the data we expect actually does exist. + """ + + UserModel = ExtensionUser + + def create_users(self): + ExtensionUser.objects.create_user( + username='test', + email='test@example.com', + password='test', + date_of_birth=date(2006, 4, 25) + ) + ExtensionUser.objects.create_superuser( + username='test2', + email='test2@example.com', + password='test', + date_of_birth=date(1976, 11, 8) + ) + + class TestObj(object): pass @@ -108,7 +171,7 @@ class TestObj(object): class SimpleRowlevelBackend(object): def has_perm(self, user, perm, obj=None): if not obj: - return # We only support row level perms + return # We only support row level perms if isinstance(obj, TestObj): if user.username == 'test2': @@ -126,7 +189,7 @@ class SimpleRowlevelBackend(object): def get_all_permissions(self, user, obj=None): if not obj: - return [] # We only support row level perms + return [] # We only support row level perms if not isinstance(obj, TestObj): return ['none'] @@ -140,7 +203,7 @@ class SimpleRowlevelBackend(object): def get_group_permissions(self, user, obj=None): if not obj: - return # We only support row level perms + return # We only support row level perms if not isinstance(obj, TestObj): return ['none'] @@ -151,6 +214,7 @@ class SimpleRowlevelBackend(object): return ['none'] +@skipIfCustomUser class RowlevelBackendTest(TestCase): """ Tests for auth backend that supports object level permissions @@ -186,7 +250,6 @@ class RowlevelBackendTest(TestCase): self.assertEqual(self.user2.get_all_permissions(), set([])) def test_get_group_permissions(self): - content_type=ContentType.objects.get_for_model(Group) group = Group.objects.create(name='test_group') self.user3.groups.add(group) self.assertEqual(self.user3.get_group_permissions(TestObj()), set(['group_perm'])) @@ -223,6 +286,7 @@ class AnonymousUserBackendTest(TestCase): self.assertEqual(self.user1.get_all_permissions(TestObj()), set(['anon'])) +@skipIfCustomUser @override_settings(AUTHENTICATION_BACKENDS=[]) class NoBackendsTest(TestCase): """ @@ -235,6 +299,7 @@ class NoBackendsTest(TestCase): self.assertRaises(ImproperlyConfigured, self.user.has_perm, ('perm', TestObj(),)) +@skipIfCustomUser class InActiveUserBackendTest(TestCase): """ Tests for a inactive user diff --git a/django/contrib/auth/tests/basic.py b/django/contrib/auth/tests/basic.py index 710754b8f1..bc7344f753 100644 --- a/django/contrib/auth/tests/basic.py +++ b/django/contrib/auth/tests/basic.py @@ -1,13 +1,18 @@ import locale -import traceback +from django.contrib.auth import get_user_model from django.contrib.auth.management.commands import createsuperuser from django.contrib.auth.models import User, AnonymousUser +from django.contrib.auth.tests.custom_user import CustomUser +from django.contrib.auth.tests.utils import skipIfCustomUser +from django.core.exceptions import ImproperlyConfigured from django.core.management import call_command from django.test import TestCase +from django.test.utils import override_settings from django.utils.six import StringIO +@skipIfCustomUser class BasicTestCase(TestCase): def test_user(self): "Check that users can be created and can set their password" @@ -34,7 +39,7 @@ class BasicTestCase(TestCase): # Check API-based user creation with no password u2 = User.objects.create_user('testuser2', 'test2@example.com') - self.assertFalse(u.has_usable_password()) + self.assertFalse(u2.has_usable_password()) def test_user_no_email(self): "Check that users can be created without an email" @@ -98,7 +103,6 @@ class BasicTestCase(TestCase): self.assertEqual(u.email, 'joe2@somewhere.org') self.assertFalse(u.has_usable_password()) - new_io = StringIO() call_command("createsuperuser", interactive=False, @@ -124,15 +128,21 @@ class BasicTestCase(TestCase): # Temporarily replace getpass to allow interactive code to be used # non-interactively - class mock_getpass: pass + class mock_getpass: + pass mock_getpass.getpass = staticmethod(lambda p=None: "nopasswd") createsuperuser.getpass = mock_getpass # Call the command in this new environment new_io = StringIO() - call_command("createsuperuser", interactive=True, username="nolocale@somewhere.org", email="nolocale@somewhere.org", stdout=new_io) + call_command("createsuperuser", + interactive=True, + username="nolocale@somewhere.org", + email="nolocale@somewhere.org", + stdout=new_io + ) - except TypeError as e: + except TypeError: self.fail("createsuperuser fails if the OS provides no information about the current locale") finally: @@ -143,3 +153,24 @@ class BasicTestCase(TestCase): # If we were successful, a user should have been created u = User.objects.get(username="nolocale@somewhere.org") self.assertEqual(u.email, 'nolocale@somewhere.org') + + def test_get_user_model(self): + "The current user model can be retrieved" + self.assertEqual(get_user_model(), User) + + @override_settings(AUTH_USER_MODEL='auth.CustomUser') + def test_swappable_user(self): + "The current user model can be swapped out for another" + self.assertEqual(get_user_model(), CustomUser) + + @override_settings(AUTH_USER_MODEL='badsetting') + def test_swappable_user_bad_setting(self): + "The alternate user setting must point to something in the format app.model" + with self.assertRaises(ImproperlyConfigured): + get_user_model() + + @override_settings(AUTH_USER_MODEL='thismodel.doesntexist') + def test_swappable_user_nonexistent_model(self): + "The current user model must point to an installed model" + with self.assertRaises(ImproperlyConfigured): + get_user_model() diff --git a/django/contrib/auth/tests/context_processors.py b/django/contrib/auth/tests/context_processors.py index 6c824e831b..32fea8ac80 100644 --- a/django/contrib/auth/tests/context_processors.py +++ b/django/contrib/auth/tests/context_processors.py @@ -2,12 +2,65 @@ import os from django.conf import global_settings from django.contrib.auth import authenticate +from django.contrib.auth.tests.utils import skipIfCustomUser +from django.contrib.auth.models import User, Permission +from django.contrib.contenttypes.models import ContentType +from django.contrib.auth.context_processors import PermWrapper, PermLookupDict from django.db.models import Q -from django.template import context from django.test import TestCase from django.test.utils import override_settings +class MockUser(object): + def has_module_perms(self, perm): + if perm == 'mockapp': + return True + return False + + def has_perm(self, perm): + if perm == 'mockapp.someperm': + return True + return False + + +class PermWrapperTests(TestCase): + """ + Test some details of the PermWrapper implementation. + """ + class EQLimiterObject(object): + """ + This object makes sure __eq__ will not be called endlessly. + """ + def __init__(self): + self.eq_calls = 0 + + def __eq__(self, other): + if self.eq_calls > 0: + return True + self.eq_calls += 1 + return False + + def test_permwrapper_in(self): + """ + Test that 'something' in PermWrapper works as expected. + """ + perms = PermWrapper(MockUser()) + # Works for modules and full permissions. + self.assertTrue('mockapp' in perms) + self.assertFalse('nonexisting' in perms) + self.assertTrue('mockapp.someperm' in perms) + self.assertFalse('mockapp.nonexisting' in perms) + + def test_permlookupdict_in(self): + """ + No endless loops if accessed with 'in' - refs #18979. + """ + pldict = PermLookupDict(MockUser(), 'mockapp') + with self.assertRaises(TypeError): + self.EQLimiterObject() in pldict + + +@skipIfCustomUser @override_settings( TEMPLATE_DIRS=( os.path.join(os.path.dirname(__file__), 'templates'), @@ -47,9 +100,28 @@ class AuthContextProcessorTests(TestCase): self.assertContains(response, "Session accessed") def test_perms_attrs(self): - self.client.login(username='super', password='secret') + u = User.objects.create_user(username='normal', password='secret') + u.user_permissions.add( + Permission.objects.get( + content_type=ContentType.objects.get_for_model(Permission), + codename='add_permission')) + self.client.login(username='normal', password='secret') response = self.client.get('/auth_processor_perms/') self.assertContains(response, "Has auth permissions") + self.assertContains(response, "Has auth.add_permission permissions") + self.assertNotContains(response, "nonexisting") + + def test_perm_in_perms_attrs(self): + u = User.objects.create_user(username='normal', password='secret') + u.user_permissions.add( + Permission.objects.get( + content_type=ContentType.objects.get_for_model(Permission), + codename='add_permission')) + self.client.login(username='normal', password='secret') + response = self.client.get('/auth_processor_perm_in_perms/') + self.assertContains(response, "Has auth permissions") + self.assertContains(response, "Has auth.add_permission permissions") + self.assertNotContains(response, "nonexisting") def test_message_attrs(self): self.client.login(username='super', password='secret') diff --git a/django/contrib/auth/tests/custom_user.py b/django/contrib/auth/tests/custom_user.py new file mode 100644 index 0000000000..a29ed6a104 --- /dev/null +++ b/django/contrib/auth/tests/custom_user.py @@ -0,0 +1,90 @@ +from django.db import models +from django.contrib.auth.models import BaseUserManager, AbstractBaseUser, AbstractUser, UserManager + + +# The custom User uses email as the unique identifier, and requires +# that every user provide a date of birth. This lets us test +# changes in username datatype, and non-text required fields. + +class CustomUserManager(BaseUserManager): + def create_user(self, email, date_of_birth, password=None): + """ + Creates and saves a User with the given email and password. + """ + if not email: + raise ValueError('Users must have an email address') + + user = self.model( + email=CustomUserManager.normalize_email(email), + date_of_birth=date_of_birth, + ) + + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, email, password, date_of_birth): + u = self.create_user(email, password=password, date_of_birth=date_of_birth) + u.is_admin = True + u.save(using=self._db) + return u + + +class CustomUser(AbstractBaseUser): + email = models.EmailField(verbose_name='email address', max_length=255, unique=True) + is_active = models.BooleanField(default=True) + is_admin = models.BooleanField(default=False) + date_of_birth = models.DateField() + + objects = CustomUserManager() + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['date_of_birth'] + + class Meta: + app_label = 'auth' + + def get_full_name(self): + return self.email + + def get_short_name(self): + return self.email + + def __unicode__(self): + return self.email + + # Maybe required? + def get_group_permissions(self, obj=None): + return set() + + def get_all_permissions(self, obj=None): + return set() + + def has_perm(self, perm, obj=None): + return True + + def has_perms(self, perm_list, obj=None): + return True + + def has_module_perms(self, app_label): + return True + + # Admin required fields + @property + def is_staff(self): + return self.is_admin + + +# The extension user is a simple extension of the built-in user class, +# adding a required date_of_birth field. This allows us to check for +# any hard references to the name "User" in forms/handlers etc. + +class ExtensionUser(AbstractUser): + date_of_birth = models.DateField() + + objects = UserManager() + + REQUIRED_FIELDS = AbstractUser.REQUIRED_FIELDS + ['date_of_birth'] + + class Meta: + app_label = 'auth' diff --git a/django/contrib/auth/tests/decorators.py b/django/contrib/auth/tests/decorators.py index cefc310e40..be99e7abb6 100644 --- a/django/contrib/auth/tests/decorators.py +++ b/django/contrib/auth/tests/decorators.py @@ -1,7 +1,9 @@ -from django.conf import settings from django.contrib.auth.decorators import login_required from django.contrib.auth.tests.views import AuthViewsTestCase +from django.contrib.auth.tests.utils import skipIfCustomUser + +@skipIfCustomUser class LoginRequiredTestCase(AuthViewsTestCase): """ Tests the login_required decorators diff --git a/django/contrib/auth/tests/forms.py b/django/contrib/auth/tests/forms.py index 74aa47e199..f3eb24287e 100644 --- a/django/contrib/auth/tests/forms.py +++ b/django/contrib/auth/tests/forms.py @@ -4,16 +4,17 @@ import os from django.contrib.auth.models import User from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm, PasswordChangeForm, SetPasswordForm, UserChangeForm, PasswordResetForm) +from django.contrib.auth.tests.utils import skipIfCustomUser from django.core import mail from django.forms.fields import Field, EmailField from django.test import TestCase from django.test.utils import override_settings from django.utils.encoding import force_text -from django.utils import six from django.utils import translation from django.utils.translation import ugettext as _ +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class UserCreationFormTest(TestCase): @@ -81,6 +82,7 @@ class UserCreationFormTest(TestCase): self.assertEqual(repr(u), '') +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AuthenticationFormTest(TestCase): @@ -133,6 +135,7 @@ class AuthenticationFormTest(TestCase): self.assertEqual(form.non_field_errors(), []) +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class SetPasswordFormTest(TestCase): @@ -160,6 +163,7 @@ class SetPasswordFormTest(TestCase): self.assertTrue(form.is_valid()) +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class PasswordChangeFormTest(TestCase): @@ -208,6 +212,7 @@ class PasswordChangeFormTest(TestCase): ['old_password', 'new_password1', 'new_password2']) +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class UserChangeFormTest(TestCase): @@ -260,7 +265,25 @@ class UserChangeFormTest(TestCase): self.assertIn(_("Invalid password format or unknown hashing algorithm."), form.as_table()) + def test_bug_19133(self): + "The change form does not return the password value" + # Use the form to construct the POST data + user = User.objects.get(username='testclient') + form_for_data = UserChangeForm(instance=user) + post_data = form_for_data.initial + # The password field should be readonly, so anything + # posted here should be ignored; the form will be + # valid, and give back the 'initial' value for the + # password field. + post_data['password'] = 'new password' + form = UserChangeForm(instance=user, data=post_data) + + self.assertTrue(form.is_valid()) + self.assertEqual(form.cleaned_data['password'], 'sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161') + + +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class PasswordResetFormTest(TestCase): @@ -338,4 +361,4 @@ class PasswordResetFormTest(TestCase): form = PasswordResetForm(data) self.assertFalse(form.is_valid()) self.assertEqual(form["email"].errors, - [_("The user account associated with this e-mail address cannot reset the password.")]) + [_("The user account associated with this email address cannot reset the password.")]) diff --git a/django/contrib/auth/tests/handlers.py b/django/contrib/auth/tests/handlers.py new file mode 100644 index 0000000000..a867aae47a --- /dev/null +++ b/django/contrib/auth/tests/handlers.py @@ -0,0 +1,50 @@ +from __future__ import unicode_literals + +from django.contrib.auth.handlers.modwsgi import check_password, groups_for_user +from django.contrib.auth.models import User, Group +from django.contrib.auth.tests.utils import skipIfCustomUser +from django.test import TransactionTestCase + + +class ModWsgiHandlerTestCase(TransactionTestCase): + """ + Tests for the mod_wsgi authentication handler + """ + + def setUp(self): + user1 = User.objects.create_user('test', 'test@example.com', 'test') + User.objects.create_user('test1', 'test1@example.com', 'test1') + group = Group.objects.create(name='test_group') + user1.groups.add(group) + + def test_check_password(self): + """ + Verify that check_password returns the correct values as per + http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms#Apache_Authentication_Provider + + because the custom user available in the test framework does not + support the is_active attribute, we can't test this with a custom + user. + """ + + # User not in database + self.assertTrue(check_password({}, 'unknown', '') is None) + + # Valid user with correct password + self.assertTrue(check_password({}, 'test', 'test')) + + # Valid user with incorrect password + self.assertFalse(check_password({}, 'test', 'incorrect')) + + @skipIfCustomUser + def test_groups_for_user(self): + """ + Check that groups_for_user returns correct values as per + http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms#Apache_Group_Authorisation + """ + + # User not in database + self.assertEqual(groups_for_user({}, 'unknown'), []) + + self.assertEqual(groups_for_user({}, 'test'), [b'test_group']) + self.assertEqual(groups_for_user({}, 'test1'), []) diff --git a/django/contrib/auth/tests/management.py b/django/contrib/auth/tests/management.py index ac83086dc3..976c0c4972 100644 --- a/django/contrib/auth/tests/management.py +++ b/django/contrib/auth/tests/management.py @@ -1,13 +1,21 @@ from __future__ import unicode_literals +from datetime import date from django.contrib.auth import models, management +from django.contrib.auth.management import create_permissions from django.contrib.auth.management.commands import changepassword +from django.contrib.auth.models import User +from django.contrib.auth.tests import CustomUser +from django.contrib.auth.tests.utils import skipIfCustomUser +from django.core.management import call_command from django.core.management.base import CommandError from django.test import TestCase +from django.test.utils import override_settings from django.utils import six from django.utils.six import StringIO +@skipIfCustomUser class GetDefaultUsernameTestCase(TestCase): def setUp(self): @@ -36,6 +44,7 @@ class GetDefaultUsernameTestCase(TestCase): self.assertEqual(management.get_default_username(), 'julia') +@skipIfCustomUser class ChangepasswordManagementCommandTestCase(TestCase): def setUp(self): @@ -48,7 +57,7 @@ class ChangepasswordManagementCommandTestCase(TestCase): self.stderr.close() def test_that_changepassword_command_changes_joes_password(self): - " Executing the changepassword management command should change joe's password " + "Executing the changepassword management command should change joe's password" self.assertTrue(self.user.check_password('qwerty')) command = changepassword.Command() command._get_pass = lambda *args: 'not qwerty' @@ -69,3 +78,133 @@ class ChangepasswordManagementCommandTestCase(TestCase): with self.assertRaises(CommandError): command.execute("joe", stdout=self.stdout, stderr=self.stderr) + + +@skipIfCustomUser +class CreatesuperuserManagementCommandTestCase(TestCase): + + def test_createsuperuser(self): + "Check the operation of the createsuperuser management command" + # We can use the management command to create a superuser + new_io = StringIO() + call_command("createsuperuser", + interactive=False, + username="joe", + email="joe@somewhere.org", + stdout=new_io + ) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, 'Superuser created successfully.') + u = User.objects.get(username="joe") + self.assertEqual(u.email, 'joe@somewhere.org') + + # created password should be unusable + self.assertFalse(u.has_usable_password()) + + def test_verbosity_zero(self): + # We can supress output on the management command + new_io = StringIO() + call_command("createsuperuser", + interactive=False, + username="joe2", + email="joe2@somewhere.org", + verbosity=0, + stdout=new_io + ) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, '') + u = User.objects.get(username="joe2") + self.assertEqual(u.email, 'joe2@somewhere.org') + self.assertFalse(u.has_usable_password()) + + def test_email_in_username(self): + new_io = StringIO() + call_command("createsuperuser", + interactive=False, + username="joe+admin@somewhere.org", + email="joe@somewhere.org", + stdout=new_io + ) + u = User.objects.get(username="joe+admin@somewhere.org") + self.assertEqual(u.email, 'joe@somewhere.org') + self.assertFalse(u.has_usable_password()) + + @override_settings(AUTH_USER_MODEL='auth.CustomUser') + def test_swappable_user(self): + "A superuser can be created when a custom User model is in use" + # We can use the management command to create a superuser + # We skip validation because the temporary substitution of the + # swappable User model messes with validation. + new_io = StringIO() + call_command("createsuperuser", + interactive=False, + email="joe@somewhere.org", + date_of_birth="1976-04-01", + stdout=new_io, + skip_validation=True + ) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, 'Superuser created successfully.') + u = CustomUser.objects.get(email="joe@somewhere.org") + self.assertEqual(u.date_of_birth, date(1976, 4, 1)) + + # created password should be unusable + self.assertFalse(u.has_usable_password()) + + @override_settings(AUTH_USER_MODEL='auth.CustomUser') + def test_swappable_user_missing_required_field(self): + "A Custom superuser won't be created when a required field isn't provided" + # We can use the management command to create a superuser + # We skip validation because the temporary substitution of the + # swappable User model messes with validation. + new_io = StringIO() + with self.assertRaises(CommandError): + call_command("createsuperuser", + interactive=False, + username="joe@somewhere.org", + stdout=new_io, + stderr=new_io, + skip_validation=True + ) + + self.assertEqual(CustomUser.objects.count(), 0) + + +class PermissionDuplicationTestCase(TestCase): + + def setUp(self): + self._original_permissions = models.Permission._meta.permissions[:] + + def tearDown(self): + models.Permission._meta.permissions = self._original_permissions + + def test_duplicated_permissions(self): + """ + Test that we show proper error message if we are trying to create + duplicate permissions. + """ + # check duplicated default permission + models.Permission._meta.permissions = [ + ('change_permission', 'Can edit permission (duplicate)')] + self.assertRaisesRegexp(CommandError, + "The permission codename 'change_permission' clashes with a " + "builtin permission for model 'auth.Permission'.", + create_permissions, models, [], verbosity=0) + + # check duplicated custom permissions + models.Permission._meta.permissions = [ + ('my_custom_permission', 'Some permission'), + ('other_one', 'Some other permission'), + ('my_custom_permission', 'Some permission with duplicate permission code'), + ] + self.assertRaisesRegexp(CommandError, + "The permission codename 'my_custom_permission' is duplicated for model " + "'auth.Permission'.", + create_permissions, models, [], verbosity=0) + + # should not raise anything + models.Permission._meta.permissions = [ + ('my_custom_permission', 'Some permission'), + ('other_one', 'Some other permission'), + ] + create_permissions(models, [], verbosity=0) diff --git a/django/contrib/auth/tests/models.py b/django/contrib/auth/tests/models.py index e4efee4339..252a0887c8 100644 --- a/django/contrib/auth/tests/models.py +++ b/django/contrib/auth/tests/models.py @@ -1,11 +1,13 @@ from django.conf import settings from django.contrib.auth.models import (Group, User, SiteProfileNotAvailable, UserManager) +from django.contrib.auth.tests.utils import skipIfCustomUser from django.test import TestCase from django.test.utils import override_settings from django.utils import six +@skipIfCustomUser @override_settings(USE_TZ=False, AUTH_PROFILE_MODULE='') class ProfileTestCase(TestCase): @@ -31,6 +33,7 @@ class ProfileTestCase(TestCase): user.get_profile() +@skipIfCustomUser @override_settings(USE_TZ=False) class NaturalKeysTestCase(TestCase): fixtures = ['authtestdata.json'] @@ -45,6 +48,7 @@ class NaturalKeysTestCase(TestCase): self.assertEqual(Group.objects.get_by_natural_key('users'), users_group) +@skipIfCustomUser @override_settings(USE_TZ=False) class LoadDataWithoutNaturalKeysTestCase(TestCase): fixtures = ['regular.json'] @@ -55,6 +59,7 @@ class LoadDataWithoutNaturalKeysTestCase(TestCase): self.assertEqual(group, user.groups.get()) +@skipIfCustomUser @override_settings(USE_TZ=False) class LoadDataWithNaturalKeysTestCase(TestCase): fixtures = ['natural.json'] @@ -65,6 +70,7 @@ class LoadDataWithNaturalKeysTestCase(TestCase): self.assertEqual(group, user.groups.get()) +@skipIfCustomUser class UserManagerTestCase(TestCase): def test_create_user(self): diff --git a/django/contrib/auth/tests/remote_user.py b/django/contrib/auth/tests/remote_user.py index fa324781d2..9b0f6f8be3 100644 --- a/django/contrib/auth/tests/remote_user.py +++ b/django/contrib/auth/tests/remote_user.py @@ -3,10 +3,12 @@ from datetime import datetime from django.conf import settings from django.contrib.auth.backends import RemoteUserBackend from django.contrib.auth.models import User +from django.contrib.auth.tests.utils import skipIfCustomUser from django.test import TestCase from django.utils import timezone +@skipIfCustomUser class RemoteUserTest(TestCase): urls = 'django.contrib.auth.tests.urls' @@ -106,6 +108,7 @@ class RemoteUserNoCreateBackend(RemoteUserBackend): create_unknown_user = False +@skipIfCustomUser class RemoteUserNoCreateTest(RemoteUserTest): """ Contains the same tests as RemoteUserTest, but using a custom auth backend @@ -142,6 +145,7 @@ class CustomRemoteUserBackend(RemoteUserBackend): return user +@skipIfCustomUser class RemoteUserCustomTest(RemoteUserTest): """ Tests a custom RemoteUserBackend subclass that overrides the clean_username diff --git a/django/contrib/auth/tests/signals.py b/django/contrib/auth/tests/signals.py index 51f14d35f0..024f44f547 100644 --- a/django/contrib/auth/tests/signals.py +++ b/django/contrib/auth/tests/signals.py @@ -1,8 +1,12 @@ -from django.test import TestCase -from django.test.utils import override_settings from django.contrib.auth import signals +from django.contrib.auth.models import User +from django.contrib.auth.tests.utils import skipIfCustomUser +from django.test import TestCase +from django.test.client import RequestFactory +from django.test.utils import override_settings +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class SignalTestCase(TestCase): urls = 'django.contrib.auth.tests.urls' @@ -14,27 +18,41 @@ class SignalTestCase(TestCase): def listener_logout(self, user, **kwargs): self.logged_out.append(user) + def listener_login_failed(self, sender, credentials, **kwargs): + self.login_failed.append(credentials) + def setUp(self): """Set up the listeners and reset the logged in/logged out counters""" self.logged_in = [] self.logged_out = [] + self.login_failed = [] signals.user_logged_in.connect(self.listener_login) signals.user_logged_out.connect(self.listener_logout) + signals.user_login_failed.connect(self.listener_login_failed) def tearDown(self): """Disconnect the listeners""" signals.user_logged_in.disconnect(self.listener_login) signals.user_logged_out.disconnect(self.listener_logout) + signals.user_login_failed.disconnect(self.listener_login_failed) def test_login(self): - # Only a successful login will trigger the signal. + # Only a successful login will trigger the success signal. self.client.login(username='testclient', password='bad') self.assertEqual(len(self.logged_in), 0) + self.assertEqual(len(self.login_failed), 1) + self.assertEqual(self.login_failed[0]['username'], 'testclient') + # verify the password is cleansed + self.assertTrue('***' in self.login_failed[0]['password']) + # Like this: self.client.login(username='testclient', password='password') self.assertEqual(len(self.logged_in), 1) self.assertEqual(self.logged_in[0].username, 'testclient') + # Ensure there were no more failures. + self.assertEqual(len(self.login_failed), 1) + def test_logout_anonymous(self): # The log_out function will still trigger the signal for anonymous # users. @@ -47,3 +65,16 @@ class SignalTestCase(TestCase): self.client.get('/logout/next_page/') self.assertEqual(len(self.logged_out), 1) self.assertEqual(self.logged_out[0].username, 'testclient') + + def test_update_last_login(self): + """Ensure that only `last_login` is updated in `update_last_login`""" + user = User.objects.get(pk=3) + old_last_login = user.last_login + + user.username = "This username shouldn't get saved" + request = RequestFactory().get('/login') + signals.user_logged_in.send(sender=user.__class__, request=request, + user=user) + user = User.objects.get(pk=3) + self.assertEqual(user.username, 'staff') + self.assertNotEqual(user.last_login, old_last_login) diff --git a/django/contrib/auth/tests/templates/context_processors/auth_attrs_perm_in_perms.html b/django/contrib/auth/tests/templates/context_processors/auth_attrs_perm_in_perms.html new file mode 100644 index 0000000000..3a18cd7405 --- /dev/null +++ b/django/contrib/auth/tests/templates/context_processors/auth_attrs_perm_in_perms.html @@ -0,0 +1,4 @@ +{% if 'auth' in perms %}Has auth permissions{% endif %} +{% if 'auth.add_permission' in perms %}Has auth.add_permission permissions{% endif %} +{% if 'nonexisting' in perms %}nonexisting perm found{% endif %} +{% if 'auth.nonexisting' in perms %}auth.nonexisting perm found{% endif %} diff --git a/django/contrib/auth/tests/templates/context_processors/auth_attrs_perms.html b/django/contrib/auth/tests/templates/context_processors/auth_attrs_perms.html index a5db868e9e..6f441afc10 100644 --- a/django/contrib/auth/tests/templates/context_processors/auth_attrs_perms.html +++ b/django/contrib/auth/tests/templates/context_processors/auth_attrs_perms.html @@ -1 +1,4 @@ {% if perms.auth %}Has auth permissions{% endif %} +{% if perms.auth.add_permission %}Has auth.add_permission permissions{% endif %} +{% if perms.nonexisting %}nonexisting perm found{% endif %} +{% if perms.auth.nonexisting in perms %}auth.nonexisting perm found{% endif %} diff --git a/django/contrib/auth/tests/templates/registration/password_reset_done.html b/django/contrib/auth/tests/templates/registration/password_reset_done.html index d56b10f0d5..c3d1d0c7b0 100644 --- a/django/contrib/auth/tests/templates/registration/password_reset_done.html +++ b/django/contrib/auth/tests/templates/registration/password_reset_done.html @@ -1 +1 @@ -E-mail sent \ No newline at end of file +Email sent \ No newline at end of file diff --git a/django/contrib/auth/tests/tokens.py b/django/contrib/auth/tests/tokens.py index 44117a4f84..e8aeb46326 100644 --- a/django/contrib/auth/tests/tokens.py +++ b/django/contrib/auth/tests/tokens.py @@ -4,10 +4,12 @@ from datetime import date, timedelta from django.conf import settings from django.contrib.auth.models import User from django.contrib.auth.tokens import PasswordResetTokenGenerator +from django.contrib.auth.tests.utils import skipIfCustomUser from django.test import TestCase from django.utils import unittest +@skipIfCustomUser class TokenGeneratorTest(TestCase): def test_make_token(self): diff --git a/django/contrib/auth/tests/urls.py b/django/contrib/auth/tests/urls.py index dbbd35ee88..4b498ceaf0 100644 --- a/django/contrib/auth/tests/urls.py +++ b/django/contrib/auth/tests/urls.py @@ -37,6 +37,10 @@ def auth_processor_perms(request): return render_to_response('context_processors/auth_attrs_perms.html', RequestContext(request, {}, processors=[context_processors.auth])) +def auth_processor_perm_in_perms(request): + return render_to_response('context_processors/auth_attrs_perm_in_perms.html', + RequestContext(request, {}, processors=[context_processors.auth])) + def auth_processor_messages(request): info(request, "Message 1") return render_to_response('context_processors/auth_attrs_messages.html', @@ -51,6 +55,7 @@ urlpatterns = urlpatterns + patterns('', (r'^logout/next_page/$', 'django.contrib.auth.views.logout', dict(next_page='/somewhere/')), (r'^remote_user/$', remote_user_auth_view), (r'^password_reset_from_email/$', 'django.contrib.auth.views.password_reset', dict(from_email='staffmember@example.com')), + (r'^admin_password_reset/$', 'django.contrib.auth.views.password_reset', dict(is_admin_site=True)), (r'^login_required/$', login_required(password_reset)), (r'^login_required_login_url/$', login_required(password_reset, login_url='/somewhere/')), @@ -58,6 +63,7 @@ urlpatterns = urlpatterns + patterns('', (r'^auth_processor_attr_access/$', auth_processor_attr_access), (r'^auth_processor_user/$', auth_processor_user), (r'^auth_processor_perms/$', auth_processor_perms), + (r'^auth_processor_perm_in_perms/$', auth_processor_perm_in_perms), (r'^auth_processor_messages/$', auth_processor_messages), url(r'^userpage/(.+)/$', userpage, name="userpage"), ) diff --git a/django/contrib/auth/tests/utils.py b/django/contrib/auth/tests/utils.py new file mode 100644 index 0000000000..6bb3d9994f --- /dev/null +++ b/django/contrib/auth/tests/utils.py @@ -0,0 +1,9 @@ +from django.conf import settings +from django.utils.unittest import skipIf + + +def skipIfCustomUser(test_func): + """ + Skip a test if a custom user model is in use. + """ + return skipIf(settings.AUTH_USER_MODEL != 'auth.User', 'Custom user model in use')(test_func) diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py index e3402b13b9..bb17576d31 100644 --- a/django/contrib/auth/tests/views.py +++ b/django/contrib/auth/tests/views.py @@ -5,6 +5,7 @@ from django.conf import global_settings, settings from django.contrib.sites.models import Site, RequestSite from django.contrib.auth.models import User from django.core import mail +from django.core.exceptions import SuspiciousOperation from django.core.urlresolvers import reverse, NoReverseMatch from django.http import QueryDict from django.utils.encoding import force_text @@ -16,6 +17,7 @@ from django.test.utils import override_settings from django.contrib.auth import SESSION_KEY, REDIRECT_FIELD_NAME from django.contrib.auth.forms import (AuthenticationForm, PasswordChangeForm, SetPasswordForm, PasswordResetForm) +from django.contrib.auth.tests.utils import skipIfCustomUser @override_settings( @@ -50,6 +52,7 @@ class AuthViewsTestCase(TestCase): return self.assertContains(response, escape(force_text(text)), **kwargs) +@skipIfCustomUser class AuthViewNamedURLTests(AuthViewsTestCase): urls = 'django.contrib.auth.urls' @@ -75,6 +78,7 @@ class AuthViewNamedURLTests(AuthViewsTestCase): self.fail("Reversal of url named '%s' failed with NoReverseMatch" % name) +@skipIfCustomUser class PasswordResetTest(AuthViewsTestCase): def test_email_not_found(self): @@ -100,6 +104,42 @@ class PasswordResetTest(AuthViewsTestCase): self.assertEqual(len(mail.outbox), 1) self.assertEqual("staffmember@example.com", mail.outbox[0].from_email) + def test_admin_reset(self): + "If the reset view is marked as being for admin, the HTTP_HOST header is used for a domain override." + response = self.client.post('/admin_password_reset/', + {'email': 'staffmember@example.com'}, + HTTP_HOST='adminsite.com' + ) + self.assertEqual(response.status_code, 302) + self.assertEqual(len(mail.outbox), 1) + self.assertTrue("http://adminsite.com" in mail.outbox[0].body) + self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email) + + def test_poisoned_http_host(self): + "Poisoned HTTP_HOST headers can't be used for reset emails" + # This attack is based on the way browsers handle URLs. The colon + # should be used to separate the port, but if the URL contains an @, + # the colon is interpreted as part of a username for login purposes, + # making 'evil.com' the request domain. Since HTTP_HOST is used to + # produce a meaningful reset URL, we need to be certain that the + # HTTP_HOST header isn't poisoned. This is done as a check when get_host() + # is invoked, but we check here as a practical consequence. + with self.assertRaises(SuspiciousOperation): + self.client.post('/password_reset/', + {'email': 'staffmember@example.com'}, + HTTP_HOST='www.example:dr.frankenstein@evil.tld' + ) + self.assertEqual(len(mail.outbox), 0) + + def test_poisoned_http_host_admin_site(self): + "Poisoned HTTP_HOST headers can't be used for reset emails on admin views" + with self.assertRaises(SuspiciousOperation): + self.client.post('/admin_password_reset/', + {'email': 'staffmember@example.com'}, + HTTP_HOST='www.example:dr.frankenstein@evil.tld' + ) + self.assertEqual(len(mail.outbox), 0) + def _test_confirm_start(self): # Start by creating the email response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'}) @@ -172,6 +212,30 @@ class PasswordResetTest(AuthViewsTestCase): self.assertContainsEscaped(response, SetPasswordForm.error_messages['password_mismatch']) +@override_settings(AUTH_USER_MODEL='auth.CustomUser') +class CustomUserPasswordResetTest(AuthViewsTestCase): + fixtures = ['custom_user.json'] + + def _test_confirm_start(self): + # Start by creating the email + response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'}) + self.assertEqual(response.status_code, 302) + self.assertEqual(len(mail.outbox), 1) + return self._read_signup_email(mail.outbox[0]) + + def _read_signup_email(self, email): + urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body) + self.assertTrue(urlmatch is not None, "No URL found in sent email") + return urlmatch.group(), urlmatch.groups()[0] + + def test_confirm_valid_custom_user(self): + url, path = self._test_confirm_start() + response = self.client.get(path) + # redirect to a 'complete' page: + self.assertContains(response, "Please enter your new password") + + +@skipIfCustomUser class ChangePasswordTest(AuthViewsTestCase): def fail_login(self, password='password'): @@ -231,6 +295,7 @@ class ChangePasswordTest(AuthViewsTestCase): self.assertTrue(response['Location'].endswith('/login/?next=/password_change/done/')) +@skipIfCustomUser class LoginTest(AuthViewsTestCase): def test_current_site_in_context_after_login(self): @@ -289,6 +354,7 @@ class LoginTest(AuthViewsTestCase): "%s should be allowed" % good_url) +@skipIfCustomUser class LoginURLSettings(AuthViewsTestCase): def setUp(self): @@ -347,6 +413,7 @@ class LoginURLSettings(AuthViewsTestCase): querystring.urlencode('/'))) +@skipIfCustomUser class LogoutTest(AuthViewsTestCase): def confirm_logged_out(self): diff --git a/django/contrib/auth/tokens.py b/django/contrib/auth/tokens.py index 9b2eda83d4..930c70012b 100644 --- a/django/contrib/auth/tokens.py +++ b/django/contrib/auth/tokens.py @@ -4,6 +4,7 @@ from django.utils.http import int_to_base36, base36_to_int from django.utils.crypto import constant_time_compare, salted_hmac from django.utils import six + class PasswordResetTokenGenerator(object): """ Strategy object used to generate and check tokens for the password diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index 024be5e46d..d27e2f5aba 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -15,10 +15,9 @@ from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_protect # Avoid shadowing the login() and logout() views below. -from django.contrib.auth import REDIRECT_FIELD_NAME, login as auth_login, logout as auth_logout +from django.contrib.auth import REDIRECT_FIELD_NAME, login as auth_login, logout as auth_logout, get_user_model from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm, SetPasswordForm, PasswordChangeForm -from django.contrib.auth.models import User from django.contrib.auth.tokens import default_token_generator from django.contrib.sites.models import get_current_site @@ -74,6 +73,7 @@ def login(request, template_name='registration/login.html', return TemplateResponse(request, template_name, context, current_app=current_app) + def logout(request, next_page=None, template_name='registration/logged_out.html', redirect_field_name=REDIRECT_FIELD_NAME, @@ -104,6 +104,7 @@ def logout(request, next_page=None, # Redirect to this page until the session has been cleared. return HttpResponseRedirect(next_page or request.path) + def logout_then_login(request, login_url=None, current_app=None, extra_context=None): """ Logs out the user if he is logged in. Then redirects to the log-in page. @@ -113,6 +114,7 @@ def logout_then_login(request, login_url=None, current_app=None, extra_context=N login_url = resolve_url(login_url) return logout(request, login_url, current_app=current_app, extra_context=extra_context) + def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME): """ @@ -128,6 +130,7 @@ def redirect_to_login(next, login_url=None, return HttpResponseRedirect(urlunparse(login_url_parts)) + # 4 views for password reset: # - password_reset sends the mail # - password_reset_done shows a success message for the above @@ -160,7 +163,7 @@ def password_reset(request, is_admin_site=False, 'request': request, } if is_admin_site: - opts = dict(opts, domain_override=request.META['HTTP_HOST']) + opts = dict(opts, domain_override=request.get_host()) form.save(**opts) return HttpResponseRedirect(post_reset_redirect) else: @@ -173,6 +176,7 @@ def password_reset(request, is_admin_site=False, return TemplateResponse(request, template_name, context, current_app=current_app) + def password_reset_done(request, template_name='registration/password_reset_done.html', current_app=None, extra_context=None): @@ -182,6 +186,7 @@ def password_reset_done(request, return TemplateResponse(request, template_name, context, current_app=current_app) + # Doesn't need csrf_protect since no-one can guess the URL @sensitive_post_parameters() @never_cache @@ -195,13 +200,14 @@ def password_reset_confirm(request, uidb36=None, token=None, View that checks the hash in a password reset link and presents a form for entering a new password. """ - assert uidb36 is not None and token is not None # checked by URLconf + UserModel = get_user_model() + assert uidb36 is not None and token is not None # checked by URLconf if post_reset_redirect is None: post_reset_redirect = reverse('django.contrib.auth.views.password_reset_complete') try: uid_int = base36_to_int(uidb36) - user = User.objects.get(id=uid_int) - except (ValueError, OverflowError, User.DoesNotExist): + user = UserModel.objects.get(id=uid_int) + except (ValueError, OverflowError, UserModel.DoesNotExist): user = None if user is not None and token_generator.check_token(user, token): @@ -225,6 +231,7 @@ def password_reset_confirm(request, uidb36=None, token=None, return TemplateResponse(request, template_name, context, current_app=current_app) + def password_reset_complete(request, template_name='registration/password_reset_complete.html', current_app=None, extra_context=None): @@ -236,6 +243,7 @@ def password_reset_complete(request, return TemplateResponse(request, template_name, context, current_app=current_app) + @sensitive_post_parameters() @csrf_protect @login_required @@ -261,6 +269,7 @@ def password_change(request, return TemplateResponse(request, template_name, context, current_app=current_app) + @login_required def password_change_done(request, template_name='registration/password_change_done.html', diff --git a/django/contrib/comments/admin.py b/django/contrib/comments/admin.py index 0024a1d1b5..a651baaadf 100644 --- a/django/contrib/comments/admin.py +++ b/django/contrib/comments/admin.py @@ -1,11 +1,22 @@ from __future__ import unicode_literals from django.contrib import admin +from django.contrib.auth import get_user_model from django.contrib.comments.models import Comment from django.utils.translation import ugettext_lazy as _, ungettext from django.contrib.comments import get_model from django.contrib.comments.views.moderation import perform_flag, perform_approve, perform_delete + +class UsernameSearch(object): + """The User object may not be auth.User, so we need to provide + a mechanism for issuing the equivalent of a .filter(user__username=...) + search in CommentAdmin. + """ + def __str__(self): + return 'user__%s' % get_user_model().USERNAME_FIELD + + class CommentsAdmin(admin.ModelAdmin): fieldsets = ( (None, @@ -24,7 +35,7 @@ class CommentsAdmin(admin.ModelAdmin): date_hierarchy = 'submit_date' ordering = ('-submit_date',) raw_id_fields = ('user',) - search_fields = ('comment', 'user__username', 'user_name', 'user_email', 'user_url', 'ip_address') + search_fields = ('comment', UsernameSearch(), 'user_name', 'user_email', 'user_url', 'ip_address') actions = ["flag_comments", "approve_comments", "remove_comments"] def get_actions(self, request): diff --git a/django/contrib/comments/feeds.py b/django/contrib/comments/feeds.py index db4b2f818e..2e0d4c3dc5 100644 --- a/django/contrib/comments/feeds.py +++ b/django/contrib/comments/feeds.py @@ -1,30 +1,27 @@ -from django.conf import settings from django.contrib.syndication.views import Feed -from django.contrib.sites.models import Site +from django.contrib.sites.models import get_current_site from django.contrib import comments from django.utils.translation import ugettext as _ class LatestCommentFeed(Feed): """Feed of latest comments on the current site.""" + def __call__(self, request, *args, **kwargs): + self.site = get_current_site(request) + return super(LatestCommentFeed, self).__call__(request, *args, **kwargs) + def title(self): - if not hasattr(self, '_site'): - self._site = Site.objects.get_current() - return _("%(site_name)s comments") % dict(site_name=self._site.name) + return _("%(site_name)s comments") % dict(site_name=self.site.name) def link(self): - if not hasattr(self, '_site'): - self._site = Site.objects.get_current() - return "http://%s/" % (self._site.domain) + return "http://%s/" % (self.site.domain) def description(self): - if not hasattr(self, '_site'): - self._site = Site.objects.get_current() - return _("Latest comments on %(site_name)s") % dict(site_name=self._site.name) + return _("Latest comments on %(site_name)s") % dict(site_name=self.site.name) def items(self): qs = comments.get_model().objects.filter( - site__pk = settings.SITE_ID, + site__pk = self.site.pk, is_public = True, is_removed = False, ) diff --git a/django/contrib/comments/locale/en/LC_MESSAGES/django.po b/django/contrib/comments/locale/en/LC_MESSAGES/django.po index 04466bdc41..b41b89011a 100644 --- a/django/contrib/comments/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/comments/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-03-23 02:37+0100\n" +"POT-Creation-Date: 2012-10-15 10:56+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -13,57 +13,57 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: admin.py:12 +#: admin.py:25 msgid "Content" msgstr "" -#: admin.py:15 +#: admin.py:28 msgid "Metadata" msgstr "" -#: admin.py:42 +#: admin.py:55 msgid "flagged" msgid_plural "flagged" msgstr[0] "" msgstr[1] "" -#: admin.py:43 +#: admin.py:56 msgid "Flag selected comments" msgstr "" -#: admin.py:47 +#: admin.py:60 msgid "approved" msgid_plural "approved" msgstr[0] "" msgstr[1] "" -#: admin.py:48 +#: admin.py:61 msgid "Approve selected comments" msgstr "" -#: admin.py:52 +#: admin.py:65 msgid "removed" msgid_plural "removed" msgstr[0] "" msgstr[1] "" -#: admin.py:53 +#: admin.py:66 msgid "Remove selected comments" msgstr "" -#: admin.py:65 +#: admin.py:78 #, python-format msgid "1 comment was successfully %(action)s." msgid_plural "%(count)s comments were successfully %(action)s." msgstr[0] "" msgstr[1] "" -#: feeds.py:13 +#: feeds.py:14 #, python-format msgid "%(site_name)s comments" msgstr "" -#: feeds.py:23 +#: feeds.py:20 #, python-format msgid "Latest comments on %(site_name)s" msgstr "" @@ -100,78 +100,78 @@ msgid "" "If you enter anything in this field your comment will be treated as spam" msgstr "" -#: models.py:22 +#: models.py:23 msgid "content type" msgstr "" -#: models.py:24 +#: models.py:25 msgid "object ID" msgstr "" -#: models.py:50 models.py:168 +#: models.py:53 models.py:177 msgid "user" msgstr "" -#: models.py:52 +#: models.py:55 msgid "user's name" msgstr "" -#: models.py:53 +#: models.py:56 msgid "user's email address" msgstr "" -#: models.py:54 +#: models.py:57 msgid "user's URL" msgstr "" -#: models.py:56 models.py:76 models.py:169 +#: models.py:59 models.py:79 models.py:178 msgid "comment" msgstr "" -#: models.py:59 +#: models.py:62 msgid "date/time submitted" msgstr "" -#: models.py:60 +#: models.py:63 msgid "IP address" msgstr "" -#: models.py:61 +#: models.py:64 msgid "is public" msgstr "" -#: models.py:62 +#: models.py:65 msgid "" "Uncheck this box to make the comment effectively disappear from the site." msgstr "" -#: models.py:64 +#: models.py:67 msgid "is removed" msgstr "" -#: models.py:65 +#: models.py:68 msgid "" "Check this box if the comment is inappropriate. A \"This comment has been " "removed\" message will be displayed instead." msgstr "" -#: models.py:77 +#: models.py:80 msgid "comments" msgstr "" -#: models.py:119 +#: models.py:124 msgid "" "This comment was posted by an authenticated user and thus the name is read-" "only." msgstr "" -#: models.py:128 +#: models.py:134 msgid "" "This comment was posted by an authenticated user and thus the email is read-" "only." msgstr "" -#: models.py:153 +#: models.py:160 #, python-format msgid "" "Posted by %(user)s at %(date)s\n" @@ -181,19 +181,19 @@ msgid "" "http://%(domain)s%(url)s" msgstr "" -#: models.py:170 +#: models.py:179 msgid "flag" msgstr "" -#: models.py:171 +#: models.py:180 msgid "date" msgstr "" -#: models.py:181 +#: models.py:190 msgid "comment flag" msgstr "" -#: models.py:182 +#: models.py:191 msgid "comment flags" msgstr "" diff --git a/django/contrib/comments/models.py b/django/contrib/comments/models.py index b043b4187a..c263ea7d10 100644 --- a/django/contrib/comments/models.py +++ b/django/contrib/comments/models.py @@ -1,16 +1,16 @@ -from django.contrib.auth.models import User +from django.conf import settings from django.contrib.comments.managers import CommentManager from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site -from django.db import models from django.core import urlresolvers +from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils import timezone -from django.conf import settings from django.utils.encoding import python_2_unicode_compatible -COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH',3000) +COMMENT_MAX_LENGTH = getattr(settings, 'COMMENT_MAX_LENGTH', 3000) + class BaseCommentAbstractModel(models.Model): """ @@ -19,14 +19,14 @@ class BaseCommentAbstractModel(models.Model): """ # Content-object field - content_type = models.ForeignKey(ContentType, + content_type = models.ForeignKey(ContentType, verbose_name=_('content type'), related_name="content_type_set_for_%(class)s") - object_pk = models.TextField(_('object ID')) + object_pk = models.TextField(_('object ID')) content_object = generic.GenericForeignKey(ct_field="content_type", fk_field="object_pk") # Metadata about the comment - site = models.ForeignKey(Site) + site = models.ForeignKey(Site) class Meta: abstract = True @@ -40,6 +40,7 @@ class BaseCommentAbstractModel(models.Model): args=(self.content_type_id, self.object_pk) ) + @python_2_unicode_compatible class Comment(BaseCommentAbstractModel): """ @@ -49,21 +50,21 @@ class Comment(BaseCommentAbstractModel): # Who posted this comment? If ``user`` is set then it was an authenticated # user; otherwise at least user_name should have been set and the comment # was posted by a non-authenticated user. - user = models.ForeignKey(User, verbose_name=_('user'), + user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'), blank=True, null=True, related_name="%(class)s_comments") - user_name = models.CharField(_("user's name"), max_length=50, blank=True) - user_email = models.EmailField(_("user's email address"), blank=True) - user_url = models.URLField(_("user's URL"), blank=True) + user_name = models.CharField(_("user's name"), max_length=50, blank=True) + user_email = models.EmailField(_("user's email address"), blank=True) + user_url = models.URLField(_("user's URL"), blank=True) comment = models.TextField(_('comment'), max_length=COMMENT_MAX_LENGTH) # Metadata about the comment submit_date = models.DateTimeField(_('date/time submitted'), default=None) - ip_address = models.IPAddressField(_('IP address'), blank=True, null=True) - is_public = models.BooleanField(_('is public'), default=True, + ip_address = models.IPAddressField(_('IP address'), blank=True, null=True) + is_public = models.BooleanField(_('is public'), default=True, help_text=_('Uncheck this box to make the comment effectively ' \ 'disappear from the site.')) - is_removed = models.BooleanField(_('is removed'), default=False, + is_removed = models.BooleanField(_('is removed'), default=False, help_text=_('Check this box if the comment is inappropriate. ' \ 'A "This comment has been removed" message will ' \ 'be displayed instead.')) @@ -95,9 +96,9 @@ class Comment(BaseCommentAbstractModel): """ if not hasattr(self, "_userinfo"): userinfo = { - "name" : self.user_name, - "email" : self.user_email, - "url" : self.user_url + "name": self.user_name, + "email": self.user_email, + "url": self.user_url } if self.user_id: u = self.user @@ -110,13 +111,14 @@ class Comment(BaseCommentAbstractModel): if u.get_full_name(): userinfo["name"] = self.user.get_full_name() elif not self.user_name: - userinfo["name"] = u.username + userinfo["name"] = u.get_username() self._userinfo = userinfo return self._userinfo userinfo = property(_get_userinfo, doc=_get_userinfo.__doc__) def _get_name(self): return self.userinfo["name"] + def _set_name(self, val): if self.user_id: raise AttributeError(_("This comment was posted by an authenticated "\ @@ -126,6 +128,7 @@ class Comment(BaseCommentAbstractModel): def _get_email(self): return self.userinfo["email"] + def _set_email(self, val): if self.user_id: raise AttributeError(_("This comment was posted by an authenticated "\ @@ -135,6 +138,7 @@ class Comment(BaseCommentAbstractModel): def _get_url(self): return self.userinfo["url"] + def _set_url(self, val): self.user_url = val url = property(_get_url, _set_url, doc="The URL given by the user who posted this comment") @@ -155,6 +159,7 @@ class Comment(BaseCommentAbstractModel): } return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % d + @python_2_unicode_compatible class CommentFlag(models.Model): """ @@ -169,9 +174,9 @@ class CommentFlag(models.Model): design users are only allowed to flag a comment with a given flag once; if you want rating look elsewhere. """ - user = models.ForeignKey(User, verbose_name=_('user'), related_name="comment_flags") - comment = models.ForeignKey(Comment, verbose_name=_('comment'), related_name="flags") - flag = models.CharField(_('flag'), max_length=30, db_index=True) + user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'), related_name="comment_flags") + comment = models.ForeignKey(Comment, verbose_name=_('comment'), related_name="flags") + flag = models.CharField(_('flag'), max_length=30, db_index=True) flag_date = models.DateTimeField(_('date'), default=None) # Constants for flag types @@ -187,7 +192,7 @@ class CommentFlag(models.Model): def __str__(self): return "%s flag of comment ID %s by %s" % \ - (self.flag, self.comment_id, self.user.username) + (self.flag, self.comment_id, self.user.get_username()) def save(self, *args, **kwargs): if self.flag_date is None: diff --git a/django/contrib/comments/moderation.py b/django/contrib/comments/moderation.py index 9b206a5bad..6c56d7a8a5 100644 --- a/django/contrib/comments/moderation.py +++ b/django/contrib/comments/moderation.py @@ -62,7 +62,7 @@ from django.contrib.comments import signals from django.db.models.base import ModelBase from django.template import Context, loader from django.contrib import comments -from django.contrib.sites.models import Site +from django.contrib.sites.models import get_current_site from django.utils import timezone class AlreadyModerated(Exception): @@ -240,7 +240,7 @@ class CommentModerator(object): t = loader.get_template('comments/comment_notification_email.txt') c = Context({ 'comment': comment, 'content_object': content_object }) - subject = '[%s] New comment posted on "%s"' % (Site.objects.get_current().name, + subject = '[%s] New comment posted on "%s"' % (get_current_site(request).name, content_object) message = t.render(c) send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipient_list, fail_silently=True) diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py index c9a11606b3..27d5a48ac6 100644 --- a/django/contrib/comments/views/comments.py +++ b/django/contrib/comments/views/comments.py @@ -15,7 +15,6 @@ from django.views.decorators.csrf import csrf_protect from django.views.decorators.http import require_POST - class CommentPostBadRequest(http.HttpResponseBadRequest): """ Response returned when a comment post is invalid. If ``DEBUG`` is on a @@ -27,6 +26,7 @@ class CommentPostBadRequest(http.HttpResponseBadRequest): if settings.DEBUG: self.content = render_to_string("comments/400-debug.html", {"why": why}) + @csrf_protect @require_POST def post_comment(request, next=None, using=None): @@ -40,7 +40,7 @@ def post_comment(request, next=None, using=None): data = request.POST.copy() if request.user.is_authenticated(): if not data.get('name', ''): - data["name"] = request.user.get_full_name() or request.user.username + data["name"] = request.user.get_full_name() or request.user.get_username() if not data.get('email', ''): data["email"] = request.user.email @@ -98,8 +98,8 @@ def post_comment(request, next=None, using=None): ] return render_to_response( template_list, { - "comment" : form.data.get("comment", ""), - "form" : form, + "comment": form.data.get("comment", ""), + "form": form, "next": next, }, RequestContext(request, {}) @@ -113,9 +113,9 @@ def post_comment(request, next=None, using=None): # Signal that the comment is about to be saved responses = signals.comment_will_be_posted.send( - sender = comment.__class__, - comment = comment, - request = request + sender=comment.__class__, + comment=comment, + request=request ) for (receiver, response) in responses: @@ -126,15 +126,14 @@ def post_comment(request, next=None, using=None): # Save the comment and signal that it was saved comment.save() signals.comment_was_posted.send( - sender = comment.__class__, - comment = comment, - request = request + sender=comment.__class__, + comment=comment, + request=request ) return next_redirect(data, next, comment_done, c=comment._get_pk_val()) comment_done = confirmation_view( - template = "comments/posted.html", - doc = """Display a "comment was posted" success page.""" + template="comments/posted.html", + doc="""Display a "comment was posted" success page.""" ) - diff --git a/django/contrib/contenttypes/locale/en/LC_MESSAGES/django.po b/django/contrib/contenttypes/locale/en/LC_MESSAGES/django.po index 8643f2eae1..c45c2aff9c 100644 --- a/django/contrib/contenttypes/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/contenttypes/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-03-23 02:37+0100\n" +"POT-Creation-Date: 2012-10-15 10:56+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -13,29 +13,29 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: models.py:123 +#: models.py:130 msgid "python model class name" msgstr "" -#: models.py:127 +#: models.py:134 msgid "content type" msgstr "" -#: models.py:128 +#: models.py:135 msgid "content types" msgstr "" -#: views.py:15 +#: views.py:17 #, python-format msgid "Content type %(ct_id)s object has no associated model" msgstr "" -#: views.py:19 +#: views.py:21 #, python-format msgid "Content type %(ct_id)s object %(obj_id)s doesn't exist" msgstr "" -#: views.py:25 +#: views.py:27 #, python-format msgid "%(ct_name)s objects don't have a get_absolute_url() method" msgstr "" diff --git a/django/contrib/contenttypes/tests.py b/django/contrib/contenttypes/tests.py index 2f92a34581..10311fae92 100644 --- a/django/contrib/contenttypes/tests.py +++ b/django/contrib/contenttypes/tests.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.views import shortcut -from django.contrib.sites.models import Site +from django.contrib.sites.models import Site, get_current_site from django.http import HttpRequest, Http404 from django.test import TestCase from django.utils.http import urlquote @@ -219,9 +219,8 @@ class ContentTypesTests(TestCase): obj = FooWithUrl.objects.create(name="john") if Site._meta.installed: - current_site = Site.objects.get_current() response = shortcut(request, user_ct.id, obj.id) - self.assertEqual("http://%s/users/john/" % current_site.domain, + self.assertEqual("http://%s/users/john/" % get_current_site(request).domain, response._headers.get("location")[1]) Site._meta.installed = False diff --git a/django/contrib/flatpages/locale/en/LC_MESSAGES/django.po b/django/contrib/flatpages/locale/en/LC_MESSAGES/django.po index 68840db86c..ded87e5420 100644 --- a/django/contrib/flatpages/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/flatpages/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-03-23 02:37+0100\n" +"POT-Creation-Date: 2012-10-15 10:56+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -17,7 +17,7 @@ msgstr "" msgid "Advanced options" msgstr "" -#: forms.py:7 models.py:7 +#: forms.py:7 models.py:11 msgid "URL" msgstr "" @@ -45,40 +45,44 @@ msgstr "" msgid "Flatpage with url %(url)s already exists for site %(site)s" msgstr "" -#: models.py:8 +#: models.py:12 msgid "title" msgstr "" -#: models.py:9 +#: models.py:13 msgid "content" msgstr "" -#: models.py:10 +#: models.py:14 msgid "enable comments" msgstr "" -#: models.py:11 +#: models.py:15 msgid "template name" msgstr "" -#: models.py:12 +#: models.py:16 msgid "" "Example: 'flatpages/contact_page.html'. If this isn't provided, the system " "will use 'flatpages/default.html'." msgstr "" -#: models.py:13 +#: models.py:17 msgid "registration required" msgstr "" -#: models.py:13 +#: models.py:17 msgid "If this is checked, only logged-in users will be able to view the page." msgstr "" -#: models.py:18 +#: models.py:22 msgid "flat page" msgstr "" -#: models.py:19 +#: models.py:23 msgid "flat pages" msgstr "" + +#: tests/forms.py:97 +msgid "This field is required." +msgstr "" diff --git a/django/contrib/flatpages/templatetags/flatpages.py b/django/contrib/flatpages/templatetags/flatpages.py index 702d968145..a32ac7f490 100644 --- a/django/contrib/flatpages/templatetags/flatpages.py +++ b/django/contrib/flatpages/templatetags/flatpages.py @@ -1,6 +1,7 @@ from django import template from django.conf import settings from django.contrib.flatpages.models import FlatPage +from django.contrib.sites.models import get_current_site register = template.Library() @@ -19,7 +20,11 @@ class FlatpageNode(template.Node): self.user = None def render(self, context): - flatpages = FlatPage.objects.filter(sites__id=settings.SITE_ID) + if 'request' in context: + site_pk = get_current_site(context['request']).pk + else: + site_pk = settings.SITE_ID + flatpages = FlatPage.objects.filter(sites__id=site_pk) # If a prefix was specified, add a filter if self.starts_with: flatpages = flatpages.filter( diff --git a/django/contrib/flatpages/views.py b/django/contrib/flatpages/views.py index 0b462ac5a4..497979e497 100644 --- a/django/contrib/flatpages/views.py +++ b/django/contrib/flatpages/views.py @@ -1,9 +1,10 @@ -from django.contrib.flatpages.models import FlatPage -from django.template import loader, RequestContext -from django.shortcuts import get_object_or_404 -from django.http import Http404, HttpResponse, HttpResponsePermanentRedirect from django.conf import settings +from django.contrib.flatpages.models import FlatPage +from django.contrib.sites.models import get_current_site from django.core.xheaders import populate_xheaders +from django.http import Http404, HttpResponse, HttpResponsePermanentRedirect +from django.shortcuts import get_object_or_404 +from django.template import loader, RequestContext from django.utils.safestring import mark_safe from django.views.decorators.csrf import csrf_protect @@ -30,14 +31,15 @@ def flatpage(request, url): """ if not url.startswith('/'): url = '/' + url + site_id = get_current_site(request).id try: f = get_object_or_404(FlatPage, - url__exact=url, sites__id__exact=settings.SITE_ID) + url__exact=url, sites__id__exact=site_id) except Http404: if not url.endswith('/') and settings.APPEND_SLASH: url += '/' f = get_object_or_404(FlatPage, - url__exact=url, sites__id__exact=settings.SITE_ID) + url__exact=url, sites__id__exact=site_id) return HttpResponsePermanentRedirect('%s/' % request.path) else: raise diff --git a/django/contrib/formtools/locale/en/LC_MESSAGES/django.po b/django/contrib/formtools/locale/en/LC_MESSAGES/django.po index 15d57af7e9..04fa380ffa 100644 --- a/django/contrib/formtools/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/formtools/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-03-23 02:38+0100\n" +"POT-Creation-Date: 2012-10-15 10:56+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" diff --git a/django/contrib/formtools/tests/__init__.py b/django/contrib/formtools/tests/__init__.py index 15941332ed..a21ffde533 100644 --- a/django/contrib/formtools/tests/__init__.py +++ b/django/contrib/formtools/tests/__init__.py @@ -12,6 +12,7 @@ from django.conf import settings from django.contrib.formtools import preview, utils from django.contrib.formtools.wizard import FormWizard from django.test import TestCase +from django.test.html import parse_html from django.test.utils import override_settings from django.utils import unittest @@ -218,7 +219,6 @@ class DummyRequest(http.HttpRequest): ) class WizardTests(TestCase): urls = 'django.contrib.formtools.tests.urls' - input_re = re.compile('name="([^"]+)" value="([^"]+)"') wizard_step_data = ( { '0-name': 'Pony', @@ -409,14 +409,13 @@ class WizardTests(TestCase): """ Pull the appropriate field data from the context to pass to the next wizard step """ - previous_fields = response.context['previous_fields'] + previous_fields = parse_html(response.context['previous_fields']) fields = {'wizard_step': response.context['step0']} - def grab(m): - fields[m.group(1)] = m.group(2) - return '' + for input_field in previous_fields: + input_attrs = dict(input_field.attributes) + fields[input_attrs["name"]] = input_attrs["value"] - self.input_re.sub(grab, previous_fields) return fields def check_wizard_step(self, response, step_no): @@ -428,7 +427,6 @@ class WizardTests(TestCase): """ step_count = len(self.wizard_step_data) - self.assertEqual(response.status_code, 200) self.assertContains(response, 'Step %d of %d' % (step_no, step_count)) data = self.grab_field_data(response) diff --git a/django/contrib/formtools/tests/wizard/cookiestorage.py b/django/contrib/formtools/tests/wizard/cookiestorage.py index 495d3afd03..d450f47861 100644 --- a/django/contrib/formtools/tests/wizard/cookiestorage.py +++ b/django/contrib/formtools/tests/wizard/cookiestorage.py @@ -1,3 +1,5 @@ +import json + from django.test import TestCase from django.core import signing from django.core.exceptions import SuspiciousOperation @@ -41,4 +43,5 @@ class TestCookieStorage(TestStorage, TestCase): storage.init_data() storage.update_response(response) unsigned_cookie_data = cookie_signer.unsign(response.cookies[storage.prefix].value) - self.assertEqual(unsigned_cookie_data, '{"step_files":{},"step":null,"extra_data":{},"step_data":{}}') + self.assertEqual(json.loads(unsigned_cookie_data), + {"step_files": {}, "step": None, "extra_data": {}, "step_data": {}}) diff --git a/django/contrib/gis/__init__.py b/django/contrib/gis/__init__.py index e69de29bb2..c996fdfdc2 100644 --- a/django/contrib/gis/__init__.py +++ b/django/contrib/gis/__init__.py @@ -0,0 +1,6 @@ +from django.utils import six + +if six.PY3: + memoryview = memoryview +else: + memoryview = buffer diff --git a/django/contrib/gis/admin/widgets.py b/django/contrib/gis/admin/widgets.py index 47570d3f9d..f4379be7f3 100644 --- a/django/contrib/gis/admin/widgets.py +++ b/django/contrib/gis/admin/widgets.py @@ -1,3 +1,5 @@ +import logging + from django.forms.widgets import Textarea from django.template import loader, Context from django.templatetags.static import static @@ -10,6 +12,8 @@ from django.contrib.gis.geos import GEOSGeometry, GEOSException, fromstr # Creating a template context that contains Django settings # values needed by admin map templates. geo_context = Context({'LANGUAGE_BIDI' : translation.get_language_bidi()}) +logger = logging.getLogger('django.contrib.gis') + class OpenLayersWidget(Textarea): """ @@ -29,7 +33,11 @@ class OpenLayersWidget(Textarea): if isinstance(value, six.string_types): try: value = GEOSGeometry(value) - except (GEOSException, ValueError): + except (GEOSException, ValueError) as err: + logger.error( + "Error creating geometry from value '%s' (%s)" % ( + value, err) + ) value = None if value and value.geom_type.upper() != self.geom_type: @@ -56,7 +64,11 @@ class OpenLayersWidget(Textarea): ogr = value.ogr ogr.transform(srid) wkt = ogr.wkt - except OGRException: + except OGRException as err: + logger.error( + "Error transforming geometry from srid '%s' to srid '%s' (%s)" % ( + value.srid, srid, err) + ) wkt = '' else: wkt = value.wkt diff --git a/django/contrib/gis/db/backends/base.py b/django/contrib/gis/db/backends/base.py index 2b8924d92e..171a304439 100644 --- a/django/contrib/gis/db/backends/base.py +++ b/django/contrib/gis/db/backends/base.py @@ -32,8 +32,9 @@ class BaseSpatialOperations(object): # How the geometry column should be selected. select = None - # Does the spatial database have a geography type? + # Does the spatial database have a geometry or geography type? geography = False + geometry = False area = False centroid = False @@ -116,6 +117,16 @@ class BaseSpatialOperations(object): """ raise NotImplementedError + def get_expression_column(self, evaluator): + """ + Helper method to return the quoted column string from the evaluator + for its expression. + """ + for expr, col_tup in evaluator.cols: + if expr is evaluator.expression: + return '%s.%s' % tuple(map(self.quote_name, col_tup)) + raise Exception("Could not find the column for the expression.") + # Spatial SQL Construction def spatial_aggregate_sql(self, agg): raise NotImplementedError('Aggregate support not implemented for this spatial backend.') diff --git a/django/contrib/gis/db/backends/mysql/operations.py b/django/contrib/gis/db/backends/mysql/operations.py index 7152f4682d..fa20ca07f4 100644 --- a/django/contrib/gis/db/backends/mysql/operations.py +++ b/django/contrib/gis/db/backends/mysql/operations.py @@ -44,7 +44,7 @@ class MySQLOperations(DatabaseOperations, BaseSpatialOperations): modify the placeholder based on the contents of the given value. """ if hasattr(value, 'expression'): - placeholder = '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression])) + placeholder = self.get_expression_column(value) else: placeholder = '%s(%%s)' % self.from_text return placeholder diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py index 392feb129b..35a4d9491d 100644 --- a/django/contrib/gis/db/backends/oracle/operations.py +++ b/django/contrib/gis/db/backends/oracle/operations.py @@ -213,7 +213,7 @@ class OracleOperations(DatabaseOperations, BaseSpatialOperations): placeholder = '%s' # No geometry value used for F expression, substitue in # the column name instead. - return placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression])) + return placeholder % self.get_expression_column(value) else: if transform_value(value, f.srid): return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (self.transform, value.srid, f.srid) diff --git a/django/contrib/gis/db/backends/postgis/adapter.py b/django/contrib/gis/db/backends/postgis/adapter.py index 863ee78acd..8bb514d760 100644 --- a/django/contrib/gis/db/backends/postgis/adapter.py +++ b/django/contrib/gis/db/backends/postgis/adapter.py @@ -1,6 +1,7 @@ """ This object provides quoting for GEOS geometries into PostgreSQL/PostGIS. """ +from __future__ import unicode_literals from psycopg2 import Binary from psycopg2.extensions import ISQLQuote @@ -10,7 +11,7 @@ class PostGISAdapter(object): "Initializes on the geometry." # Getting the WKB (in string form, to allow easy pickling of # the adaptor) and the SRID from the geometry. - self.ewkb = str(geom.ewkb) + self.ewkb = bytes(geom.ewkb) self.srid = geom.srid self._adapter = Binary(self.ewkb) @@ -39,7 +40,7 @@ class PostGISAdapter(object): def getquoted(self): "Returns a properly quoted string for use in PostgreSQL/PostGIS." # psycopg will figure out whether to use E'\\000' or '\000' - return 'ST_GeomFromEWKB(%s)' % self._adapter.getquoted() + return str('ST_GeomFromEWKB(%s)' % self._adapter.getquoted().decode()) def prepare_database_save(self, unused): return self diff --git a/django/contrib/gis/db/backends/postgis/creation.py b/django/contrib/gis/db/backends/postgis/creation.py index bad22bee70..406dc4e487 100644 --- a/django/contrib/gis/db/backends/postgis/creation.py +++ b/django/contrib/gis/db/backends/postgis/creation.py @@ -1,9 +1,11 @@ from django.conf import settings +from django.core.exceptions import ImproperlyConfigured from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation class PostGISCreation(DatabaseCreation): geom_index_type = 'GIST' - geom_index_opts = 'GIST_GEOMETRY_OPS' + geom_index_ops = 'GIST_GEOMETRY_OPS' + geom_index_ops_nd = 'GIST_GEOMETRY_OPS_ND' def sql_indexes_for_field(self, model, f, style): "Return any spatial index creation SQL for the field." @@ -16,8 +18,9 @@ class PostGISCreation(DatabaseCreation): qn = self.connection.ops.quote_name db_table = model._meta.db_table - if f.geography: - # Geogrophy columns are created normally. + if f.geography or self.connection.ops.geometry: + # Geography and Geometry (PostGIS 2.0+) columns are + # created normally. pass else: # Geometry columns are created by `AddGeometryColumn` @@ -38,23 +41,31 @@ class PostGISCreation(DatabaseCreation): style.SQL_FIELD(qn(f.column)) + style.SQL_KEYWORD(' SET NOT NULL') + ';') - if f.spatial_index: # Spatial indexes created the same way for both Geometry and - # Geography columns + # Geography columns. + # PostGIS 2.0 does not support GIST_GEOMETRY_OPS. So, on 1.5 + # we use GIST_GEOMETRY_OPS, on 2.0 we use either "nd" ops + # which are fast on multidimensional cases, or just plain + # gist index for the 2d case. if f.geography: - index_opts = '' + index_ops = '' + elif self.connection.ops.geometry: + if f.dim > 2: + index_ops = ' ' + style.SQL_KEYWORD(self.geom_index_ops_nd) + else: + index_ops = '' else: - index_opts = ' ' + style.SQL_KEYWORD(self.geom_index_opts) + index_ops = ' ' + style.SQL_KEYWORD(self.geom_index_ops) output.append(style.SQL_KEYWORD('CREATE INDEX ') + style.SQL_TABLE(qn('%s_%s_id' % (db_table, f.column))) + style.SQL_KEYWORD(' ON ') + style.SQL_TABLE(qn(db_table)) + style.SQL_KEYWORD(' USING ') + style.SQL_COLTYPE(self.geom_index_type) + ' ( ' + - style.SQL_FIELD(qn(f.column)) + index_opts + ' );') + style.SQL_FIELD(qn(f.column)) + index_ops + ' );') return output def sql_table_creation_suffix(self): - qn = self.connection.ops.quote_name - return ' TEMPLATE %s' % qn(getattr(settings, 'POSTGIS_TEMPLATE', 'template_postgis')) + postgis_template = getattr(settings, 'POSTGIS_TEMPLATE', 'template_postgis') + return ' TEMPLATE %s' % self.connection.ops.quote_name(postgis_template) diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py index 434d8719cc..aa23b974db 100644 --- a/django/contrib/gis/db/backends/postgis/operations.py +++ b/django/contrib/gis/db/backends/postgis/operations.py @@ -103,11 +103,12 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): self.geom_func_prefix = prefix self.spatial_version = version except DatabaseError: - raise ImproperlyConfigured('Cannot determine PostGIS version for database "%s". ' - 'GeoDjango requires at least PostGIS version 1.3. ' - 'Was the database created from a spatial database ' - 'template?' % self.connection.settings_dict['NAME'] - ) + raise ImproperlyConfigured( + 'Cannot determine PostGIS version for database "%s". ' + 'GeoDjango requires at least PostGIS version 1.3. ' + 'Was the database created from a spatial database ' + 'template?' % self.connection.settings_dict['NAME'] + ) # TODO: Raise helpful exceptions as they become known. # PostGIS-specific operators. The commented descriptions of these @@ -215,6 +216,10 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): 'bboverlaps' : PostGISOperator('&&'), } + # Native geometry type support added in PostGIS 2.0. + if version >= (2, 0, 0): + self.geometry = True + # Creating a dictionary lookup of all GIS terms for PostGIS. gis_terms = ['isnull'] gis_terms += list(self.geometry_operators) @@ -231,7 +236,6 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): self.distance_spheroid = prefix + 'distance_spheroid' self.envelope = prefix + 'Envelope' self.extent = prefix + 'Extent' - self.extent3d = prefix + 'Extent3D' self.force_rhr = prefix + 'ForceRHR' self.geohash = GEOHASH self.geojson = GEOJSON @@ -239,14 +243,12 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): self.intersection = prefix + 'Intersection' self.kml = prefix + 'AsKML' self.length = prefix + 'Length' - self.length3d = prefix + 'Length3D' self.length_spheroid = prefix + 'length_spheroid' self.makeline = prefix + 'MakeLine' self.mem_size = prefix + 'mem_size' self.num_geom = prefix + 'NumGeometries' self.num_points =prefix + 'npoints' self.perimeter = prefix + 'Perimeter' - self.perimeter3d = prefix + 'Perimeter3D' self.point_on_surface = prefix + 'PointOnSurface' self.polygonize = prefix + 'Polygonize' self.reverse = prefix + 'Reverse' @@ -259,6 +261,15 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): self.union = prefix + 'Union' self.unionagg = prefix + 'Union' + if version >= (2, 0, 0): + self.extent3d = prefix + '3DExtent' + self.length3d = prefix + '3DLength' + self.perimeter3d = prefix + '3DPerimeter' + else: + self.extent3d = prefix + 'Extent3D' + self.length3d = prefix + 'Length3D' + self.perimeter3d = prefix + 'Perimeter3D' + def check_aggregate_support(self, aggregate): """ Checks if the given aggregate name is supported (that is, if it's @@ -314,6 +325,14 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): 'only with an SRID of 4326.') return 'geography(%s,%d)'% (f.geom_type, f.srid) + elif self.geometry: + # Postgis 2.0 supports type-based geometries. + # TODO: Support 'M' extension. + if f.dim == 3: + geom_type = f.geom_type + 'Z' + else: + geom_type = f.geom_type + return 'geometry(%s,%d)' % (geom_type, f.srid) else: return None @@ -375,7 +394,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): # If this is an F expression, then we don't really want # a placeholder and instead substitute in the column # of the expression. - placeholder = placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression])) + placeholder = placeholder % self.get_expression_column(value) return placeholder diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py index 80f05ef076..5eaa77843c 100644 --- a/django/contrib/gis/db/backends/spatialite/operations.py +++ b/django/contrib/gis/db/backends/spatialite/operations.py @@ -146,6 +146,8 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): except DatabaseError: # we are using < 2.4.0-RC4 pass + if version >= (3, 0, 0): + self.geojson = 'AsGeoJSON' def check_aggregate_support(self, aggregate): """ @@ -208,7 +210,7 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): placeholder = '%s' # No geometry value used for F expression, substitue in # the column name instead. - return placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression])) + return placeholder % self.get_expression_column(value) else: if transform_value(value, f.srid): # Adding Transform() to the SQL placeholder. diff --git a/django/contrib/gis/db/models/fields.py b/django/contrib/gis/db/models/fields.py index 17630d0899..d90ce309d4 100644 --- a/django/contrib/gis/db/models/fields.py +++ b/django/contrib/gis/db/models/fields.py @@ -95,7 +95,7 @@ class GeometryField(Field): # Is this a geography rather than a geometry column? self.geography = geography - # Oracle-specific private attributes for creating the entrie in + # Oracle-specific private attributes for creating the entry in # `USER_SDO_GEOM_METADATA` self._extent = kwargs.pop('extent', (-180.0, -90.0, 180.0, 90.0)) self._tolerance = kwargs.pop('tolerance', 0.05) @@ -160,7 +160,7 @@ class GeometryField(Field): # from the given string input. if isinstance(geom, Geometry): pass - elif isinstance(geom, six.string_types) or hasattr(geom, '__geo_interface__'): + elif isinstance(geom, (bytes, six.string_types)) or hasattr(geom, '__geo_interface__'): try: geom = Geometry(geom) except GeometryException: diff --git a/django/contrib/gis/db/models/proxy.py b/django/contrib/gis/db/models/proxy.py index 413610fc5c..1fdc5036ba 100644 --- a/django/contrib/gis/db/models/proxy.py +++ b/django/contrib/gis/db/models/proxy.py @@ -5,6 +5,7 @@ corresponding to geographic model fields. Thanks to Robert Coup for providing this functionality (see #4322). """ +from django.contrib.gis import memoryview from django.utils import six class GeometryProxy(object): @@ -54,7 +55,7 @@ class GeometryProxy(object): if isinstance(value, self._klass) and (str(value.geom_type).upper() == gtype or gtype == 'GEOMETRY'): # Assigning the SRID to the geometry. if value.srid is None: value.srid = self._field.srid - elif value is None or isinstance(value, six.string_types + (buffer,)): + elif value is None or isinstance(value, six.string_types + (memoryview,)): # Set with None, WKT, HEX, or WKB pass else: diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index d87e151aea..2ffbd2021b 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -1,6 +1,7 @@ from django.db import connections from django.db.models.query import QuerySet, ValuesQuerySet, ValuesListQuerySet +from django.contrib.gis import memoryview from django.contrib.gis.db.models import aggregates from django.contrib.gis.db.models.fields import get_srid_info, PointField, LineStringField from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery @@ -145,13 +146,14 @@ class GeoQuerySet(QuerySet): """ backend = connections[self.db].ops if not backend.geojson: - raise NotImplementedError('Only PostGIS 1.3.4+ supports GeoJSON serialization.') + raise NotImplementedError('Only PostGIS 1.3.4+ and SpatiaLite 3.0+ ' + 'support GeoJSON serialization.') if not isinstance(precision, six.integer_types): raise TypeError('Precision keyword must be set with an integer.') # Setting the options flag -- which depends on which version of - # PostGIS we're using. + # PostGIS we're using. SpatiaLite only uses the first group of options. if backend.spatial_version >= (1, 4, 0): options = 0 if crs and bbox: options = 3 @@ -193,9 +195,9 @@ class GeoQuerySet(QuerySet): # PostGIS AsGML() aggregate function parameter order depends on the # version -- uggh. if backend.spatial_version > (1, 3, 1): - procedure_fmt = '%(version)s,%(geo_col)s,%(precision)s' + s['procedure_fmt'] = '%(version)s,%(geo_col)s,%(precision)s' else: - procedure_fmt = '%(geo_col)s,%(precision)s,%(version)s' + s['procedure_fmt'] = '%(geo_col)s,%(precision)s,%(version)s' s['procedure_args'] = {'precision' : precision, 'version' : version} return self._spatial_attribute('gml', s, **kwargs) @@ -676,7 +678,7 @@ class GeoQuerySet(QuerySet): if not backend.geography: if not isinstance(geo_field, PointField): raise ValueError('Spherical distance calculation only supported on PointFields.') - if not str(Geometry(buffer(params[0].ewkb)).geom_type) == 'Point': + if not str(Geometry(memoryview(params[0].ewkb)).geom_type) == 'Point': raise ValueError('Spherical distance calculation only supported with Point Geometry parameters') # The `function` procedure argument needs to be set differently for # geodetic distance calculations. diff --git a/django/contrib/gis/db/models/sql/compiler.py b/django/contrib/gis/db/models/sql/compiler.py index 5c8d2647f7..cf6a8ad047 100644 --- a/django/contrib/gis/db/models/sql/compiler.py +++ b/django/contrib/gis/db/models/sql/compiler.py @@ -1,3 +1,8 @@ +try: + from itertools import zip_longest +except ImportError: + from itertools import izip_longest as zip_longest + from django.utils.six.moves import zip from django.db.backends.util import truncate_name, typecast_timestamp @@ -114,10 +119,10 @@ class GeoSQLCompiler(compiler.SQLCompiler): result = [] if opts is None: opts = self.query.model._meta + # Skip all proxy to the root proxied model + opts = opts.concrete_model._meta aliases = set() only_load = self.deferred_to_columns() - # Skip all proxy to the root proxied model - proxied_model = opts.concrete_model if start_alias: seen = {None: start_alias} @@ -128,12 +133,9 @@ class GeoSQLCompiler(compiler.SQLCompiler): try: alias = seen[model] except KeyError: - if model is proxied_model: - alias = start_alias - else: - link_field = opts.get_ancestor_link(model) - alias = self.query.join((start_alias, model._meta.db_table, - link_field.column, model._meta.pk.column)) + link_field = opts.get_ancestor_link(model) + alias = self.query.join((start_alias, model._meta.db_table, + link_field.column, model._meta.pk.column)) seen[model] = alias else: # If we're starting from the base model of the queryset, the @@ -190,7 +192,7 @@ class GeoSQLCompiler(compiler.SQLCompiler): if self.connection.ops.oracle or getattr(self.query, 'geo_values', False): # We resolve the rest of the columns if we're on Oracle or if # the `geo_values` attribute is defined. - for value, field in map(None, row[index_start:], fields): + for value, field in zip_longest(row[index_start:], fields): values.append(self.query.convert_values(value, field, self.connection)) else: values.extend(row[index_start:]) diff --git a/django/contrib/gis/gdal/__init__.py b/django/contrib/gis/gdal/__init__.py index adff96b47a..c33fbcb97a 100644 --- a/django/contrib/gis/gdal/__init__.py +++ b/django/contrib/gis/gdal/__init__.py @@ -37,11 +37,11 @@ try: from django.contrib.gis.gdal.driver import Driver from django.contrib.gis.gdal.datasource import DataSource - from django.contrib.gis.gdal.libgdal import gdal_version, gdal_full_version, gdal_release_date, GDAL_VERSION + from django.contrib.gis.gdal.libgdal import gdal_version, gdal_full_version, GDAL_VERSION from django.contrib.gis.gdal.srs import SpatialReference, CoordTransform from django.contrib.gis.gdal.geometries import OGRGeometry HAS_GDAL = True -except: +except Exception: HAS_GDAL = False try: diff --git a/django/contrib/gis/gdal/datasource.py b/django/contrib/gis/gdal/datasource.py index 4ceddc6c72..c92b2e170b 100644 --- a/django/contrib/gis/gdal/datasource.py +++ b/django/contrib/gis/gdal/datasource.py @@ -45,6 +45,7 @@ from django.contrib.gis.gdal.layer import Layer # Getting the ctypes prototypes for the DataSource. from django.contrib.gis.gdal.prototypes import ds as capi +from django.utils.encoding import force_bytes, force_text from django.utils import six from django.utils.six.moves import xrange @@ -56,12 +57,14 @@ class DataSource(GDALBase): "Wraps an OGR Data Source object." #### Python 'magic' routines #### - def __init__(self, ds_input, ds_driver=False, write=False): + def __init__(self, ds_input, ds_driver=False, write=False, encoding='utf-8'): # The write flag. if write: self._write = 1 else: self._write = 0 + # See also http://trac.osgeo.org/gdal/wiki/rfc23_ogr_unicode + self.encoding = encoding # Registering all the drivers, this needs to be done # _before_ we try to open up a data source. @@ -73,7 +76,7 @@ class DataSource(GDALBase): ds_driver = Driver.ptr_type() try: # OGROpen will auto-detect the data source type. - ds = capi.open_ds(ds_input, self._write, byref(ds_driver)) + ds = capi.open_ds(force_bytes(ds_input), self._write, byref(ds_driver)) except OGRException: # Making the error message more clear rather than something # like "Invalid pointer returned from OGROpen". @@ -102,7 +105,7 @@ class DataSource(GDALBase): def __getitem__(self, index): "Allows use of the index [] operator to get a layer at the index." if isinstance(index, six.string_types): - l = capi.get_layer_by_name(self.ptr, index) + l = capi.get_layer_by_name(self.ptr, force_bytes(index)) if not l: raise OGRIndexError('invalid OGR Layer name given: "%s"' % index) elif isinstance(index, int): if index < 0 or index >= self.layer_count: @@ -128,4 +131,5 @@ class DataSource(GDALBase): @property def name(self): "Returns the name of the data source." - return capi.get_ds_name(self._ptr) + name = capi.get_ds_name(self._ptr) + return force_text(name, self.encoding, strings_only=True) diff --git a/django/contrib/gis/gdal/driver.py b/django/contrib/gis/gdal/driver.py index de4dc61c63..55a5d77d66 100644 --- a/django/contrib/gis/gdal/driver.py +++ b/django/contrib/gis/gdal/driver.py @@ -5,6 +5,7 @@ from django.contrib.gis.gdal.error import OGRException from django.contrib.gis.gdal.prototypes import ds as capi from django.utils import six +from django.utils.encoding import force_bytes # For more information, see the OGR C API source code: # http://www.gdal.org/ogr/ogr__api_8h.html @@ -36,7 +37,7 @@ class Driver(GDALBase): name = dr_input # Attempting to get the OGR driver by the string name. - dr = capi.get_driver_by_name(name) + dr = capi.get_driver_by_name(force_bytes(name)) elif isinstance(dr_input, int): self._register() dr = capi.get_driver(dr_input) diff --git a/django/contrib/gis/gdal/envelope.py b/django/contrib/gis/gdal/envelope.py index ab0940e476..f145526af0 100644 --- a/django/contrib/gis/gdal/envelope.py +++ b/django/contrib/gis/gdal/envelope.py @@ -52,7 +52,7 @@ class Envelope(object): elif len(args) == 4: # Individual parameters passed in. # Thanks to ww for the help - self._from_sequence(map(float, args)) + self._from_sequence([float(a) for a in args]) else: raise OGRException('Incorrect number (%d) of arguments.' % len(args)) diff --git a/django/contrib/gis/gdal/feature.py b/django/contrib/gis/gdal/feature.py index 292004873d..a11a6873c5 100644 --- a/django/contrib/gis/gdal/feature.py +++ b/django/contrib/gis/gdal/feature.py @@ -7,6 +7,7 @@ from django.contrib.gis.gdal.geometries import OGRGeometry, OGRGeomType # ctypes function prototypes from django.contrib.gis.gdal.prototypes import ds as capi, geom as geom_api +from django.utils.encoding import force_bytes, force_text from django.utils import six from django.utils.six.moves import xrange @@ -15,15 +16,20 @@ from django.utils.six.moves import xrange # # The OGR_F_* routines are relevant here. class Feature(GDALBase): - "A class that wraps an OGR Feature, needs to be instantiated from a Layer object." + """ + This class that wraps an OGR Feature, needs to be instantiated + from a Layer object. + """ #### Python 'magic' routines #### - def __init__(self, feat, fdefn): - "Initializes on the pointers for the feature and the layer definition." - if not feat or not fdefn: + def __init__(self, feat, layer): + """ + Initializes Feature from a pointer and its Layer object. + """ + if not feat: raise OGRException('Cannot create OGR Feature, invalid pointer given.') self.ptr = feat - self._fdefn = fdefn + self._layer = layer def __del__(self): "Releases a reference to this object." @@ -42,7 +48,7 @@ class Feature(GDALBase): if index < 0 or index > self.num_fields: raise OGRIndexError('index out of range') i = index - return Field(self.ptr, i) + return Field(self, i) def __iter__(self): "Iterates over each field in the Feature." @@ -62,6 +68,10 @@ class Feature(GDALBase): return bool(capi.feature_equal(self.ptr, other._ptr)) #### Feature Properties #### + @property + def encoding(self): + return self._layer._ds.encoding + @property def fid(self): "Returns the feature identifier." @@ -70,7 +80,8 @@ class Feature(GDALBase): @property def layer_name(self): "Returns the name of the layer for the feature." - return capi.get_feat_name(self._fdefn) + name = capi.get_feat_name(self._layer._ldefn) + return force_text(name, self.encoding, strings_only=True) @property def num_fields(self): @@ -80,7 +91,7 @@ class Feature(GDALBase): @property def fields(self): "Returns a list of fields in the Feature." - return [capi.get_field_name(capi.get_field_defn(self._fdefn, i)) + return [capi.get_field_name(capi.get_field_defn(self._layer._ldefn, i)) for i in xrange(self.num_fields)] @property @@ -93,7 +104,7 @@ class Feature(GDALBase): @property def geom_type(self): "Returns the OGR Geometry Type for this Feture." - return OGRGeomType(capi.get_fd_geom_type(self._fdefn)) + return OGRGeomType(capi.get_fd_geom_type(self._layer._ldefn)) #### Feature Methods #### def get(self, field): @@ -107,6 +118,7 @@ class Feature(GDALBase): def index(self, field_name): "Returns the index of the given field name." - i = capi.get_field_index(self.ptr, field_name) - if i < 0: raise OGRIndexError('invalid OFT field name given: "%s"' % field_name) + i = capi.get_field_index(self.ptr, force_bytes(field_name)) + if i < 0: + raise OGRIndexError('invalid OFT field name given: "%s"' % field_name) return i diff --git a/django/contrib/gis/gdal/field.py b/django/contrib/gis/gdal/field.py index 12dc8b921f..2415f32b26 100644 --- a/django/contrib/gis/gdal/field.py +++ b/django/contrib/gis/gdal/field.py @@ -3,18 +3,23 @@ from datetime import date, datetime, time from django.contrib.gis.gdal.base import GDALBase from django.contrib.gis.gdal.error import OGRException from django.contrib.gis.gdal.prototypes import ds as capi +from django.utils.encoding import force_text + # For more information, see the OGR C API source code: # http://www.gdal.org/ogr/ogr__api_8h.html # # The OGR_Fld_* routines are relevant here. class Field(GDALBase): - "A class that wraps an OGR Field, needs to be instantiated from a Feature object." + """ + This class wraps an OGR Field, and needs to be instantiated + from a Feature object. + """ #### Python 'magic' routines #### def __init__(self, feat, index): """ - Initializes on the feature pointer and the integer index of + Initializes on the feature object and the integer index of the field within the feature. """ # Setting the feature pointer and index. @@ -22,7 +27,7 @@ class Field(GDALBase): self._index = index # Getting the pointer for this field. - fld_ptr = capi.get_feat_field_defn(feat, index) + fld_ptr = capi.get_feat_field_defn(feat.ptr, index) if not fld_ptr: raise OGRException('Cannot create OGR Field, invalid pointer given.') self.ptr = fld_ptr @@ -42,21 +47,23 @@ class Field(GDALBase): #### Field Methods #### def as_double(self): "Retrieves the Field's value as a double (float)." - return capi.get_field_as_double(self._feat, self._index) + return capi.get_field_as_double(self._feat.ptr, self._index) def as_int(self): "Retrieves the Field's value as an integer." - return capi.get_field_as_integer(self._feat, self._index) + return capi.get_field_as_integer(self._feat.ptr, self._index) def as_string(self): "Retrieves the Field's value as a string." - return capi.get_field_as_string(self._feat, self._index) + string = capi.get_field_as_string(self._feat.ptr, self._index) + return force_text(string, encoding=self._feat.encoding, strings_only=True) def as_datetime(self): "Retrieves the Field's value as a tuple of date & time components." yy, mm, dd, hh, mn, ss, tz = [c_int() for i in range(7)] - status = capi.get_field_as_datetime(self._feat, self._index, byref(yy), byref(mm), byref(dd), - byref(hh), byref(mn), byref(ss), byref(tz)) + status = capi.get_field_as_datetime( + self._feat.ptr, self._index, byref(yy), byref(mm), byref(dd), + byref(hh), byref(mn), byref(ss), byref(tz)) if status: return (yy, mm, dd, hh, mn, ss, tz) else: @@ -66,7 +73,8 @@ class Field(GDALBase): @property def name(self): "Returns the name of this Field." - return capi.get_field_name(self.ptr) + name = capi.get_field_name(self.ptr) + return force_text(name, encoding=self._feat.encoding, strings_only=True) @property def precision(self): diff --git a/django/contrib/gis/gdal/geometries.py b/django/contrib/gis/gdal/geometries.py index 373ece777d..eb67059245 100644 --- a/django/contrib/gis/gdal/geometries.py +++ b/django/contrib/gis/gdal/geometries.py @@ -43,6 +43,8 @@ import sys from binascii import a2b_hex, b2a_hex from ctypes import byref, string_at, c_char_p, c_double, c_ubyte, c_void_p +from django.contrib.gis import memoryview + # Getting GDAL prerequisites from django.contrib.gis.gdal.base import GDALBase from django.contrib.gis.gdal.envelope import Envelope, OGREnvelope @@ -76,16 +78,11 @@ class OGRGeometry(GDALBase): # If HEX, unpack input to to a binary buffer. if str_instance and hex_regex.match(geom_input): - geom_input = buffer(a2b_hex(geom_input.upper())) + geom_input = memoryview(a2b_hex(geom_input.upper().encode())) str_instance = False # Constructing the geometry, if str_instance: - # Checking if unicode - if isinstance(geom_input, six.text_type): - # Encoding to ASCII, WKT or HEX doesn't need any more. - geom_input = geom_input.encode('ascii') - wkt_m = wkt_regex.match(geom_input) json_m = json_regex.match(geom_input) if wkt_m: @@ -96,19 +93,19 @@ class OGRGeometry(GDALBase): # OGR_G_CreateFromWkt doesn't work with LINEARRING WKT. # See http://trac.osgeo.org/gdal/ticket/1992. g = capi.create_geom(OGRGeomType(wkt_m.group('type')).num) - capi.import_wkt(g, byref(c_char_p(wkt_m.group('wkt')))) + capi.import_wkt(g, byref(c_char_p(wkt_m.group('wkt').encode()))) else: - g = capi.from_wkt(byref(c_char_p(wkt_m.group('wkt'))), None, byref(c_void_p())) + g = capi.from_wkt(byref(c_char_p(wkt_m.group('wkt').encode())), None, byref(c_void_p())) elif json_m: - g = capi.from_json(geom_input) + g = capi.from_json(geom_input.encode()) else: # Seeing if the input is a valid short-hand string # (e.g., 'Point', 'POLYGON'). ogr_t = OGRGeomType(geom_input) g = capi.create_geom(OGRGeomType(geom_input).num) - elif isinstance(geom_input, buffer): + elif isinstance(geom_input, memoryview): # WKB was passed in - g = capi.from_wkb(str(geom_input), None, byref(c_void_p()), len(geom_input)) + g = capi.from_wkb(bytes(geom_input), None, byref(c_void_p()), len(geom_input)) elif isinstance(geom_input, OGRGeomType): # OGRGeomType was passed in, an empty geometry will be created. g = capi.create_geom(geom_input.num) @@ -141,7 +138,7 @@ class OGRGeometry(GDALBase): srs = srs.wkt else: srs = None - return str(self.wkb), srs + return bytes(self.wkb), srs def __setstate__(self, state): wkb, srs = state @@ -354,7 +351,7 @@ class OGRGeometry(GDALBase): buf = (c_ubyte * sz)() wkb = capi.to_wkb(self.ptr, byteorder, byref(buf)) # Returning a buffer of the string at the pointer. - return buffer(string_at(buf, sz)) + return memoryview(string_at(buf, sz)) @property def wkt(self): diff --git a/django/contrib/gis/gdal/layer.py b/django/contrib/gis/gdal/layer.py index 2357fbb88a..7f935cd114 100644 --- a/django/contrib/gis/gdal/layer.py +++ b/django/contrib/gis/gdal/layer.py @@ -14,6 +14,7 @@ from django.contrib.gis.gdal.srs import SpatialReference # GDAL ctypes function prototypes. from django.contrib.gis.gdal.prototypes import ds as capi, geom as geom_api, srs as srs_api +from django.utils.encoding import force_bytes, force_text from django.utils import six from django.utils.six.moves import xrange @@ -38,7 +39,7 @@ class Layer(GDALBase): self._ds = ds self._ldefn = capi.get_layer_defn(self._ptr) # Does the Layer support random reading? - self._random_read = self.test_capability('RandomRead') + self._random_read = self.test_capability(b'RandomRead') def __getitem__(self, index): "Gets the Feature at the specified index." @@ -60,7 +61,7 @@ class Layer(GDALBase): # ResetReading() must be called before iteration is to begin. capi.reset_reading(self._ptr) for i in xrange(self.num_feat): - yield Feature(capi.get_next_feature(self._ptr), self._ldefn) + yield Feature(capi.get_next_feature(self._ptr), self) def __len__(self): "The length is the number of features." @@ -80,7 +81,7 @@ class Layer(GDALBase): if self._random_read: # If the Layer supports random reading, return. try: - return Feature(capi.get_feature(self.ptr, feat_id), self._ldefn) + return Feature(capi.get_feature(self.ptr, feat_id), self) except OGRException: pass else: @@ -102,7 +103,8 @@ class Layer(GDALBase): @property def name(self): "Returns the name of this layer in the Data Source." - return capi.get_fd_name(self._ldefn) + name = capi.get_fd_name(self._ldefn) + return force_text(name, self._ds.encoding, strings_only=True) @property def num_feat(self, force=1): @@ -134,8 +136,9 @@ class Layer(GDALBase): Returns a list of string names corresponding to each of the Fields available in this Layer. """ - return [capi.get_field_name(capi.get_field_defn(self._ldefn, i)) - for i in xrange(self.num_fields) ] + return [force_text(capi.get_field_name(capi.get_field_defn(self._ldefn, i)), + self._ds.encoding, strings_only=True) + for i in xrange(self.num_fields)] @property def field_types(self): @@ -212,4 +215,4 @@ class Layer(GDALBase): 'FastFeatureCount', 'FastGetExtent', 'CreateField', 'Transactions', 'DeleteFeature', and 'FastSetNextByIndex'. """ - return bool(capi.test_capability(self.ptr, capability)) + return bool(capi.test_capability(self.ptr, force_bytes(capability))) diff --git a/django/contrib/gis/gdal/libgdal.py b/django/contrib/gis/gdal/libgdal.py index 27a5b8172e..91f8d618fc 100644 --- a/django/contrib/gis/gdal/libgdal.py +++ b/django/contrib/gis/gdal/libgdal.py @@ -1,14 +1,22 @@ +from __future__ import unicode_literals + +import logging import os import re -from ctypes import c_char_p, CDLL +from ctypes import c_char_p, c_int, CDLL, CFUNCTYPE from ctypes.util import find_library + from django.contrib.gis.gdal.error import OGRException +from django.core.exceptions import ImproperlyConfigured + +logger = logging.getLogger('django.contrib.gis') # Custom library path set? try: from django.conf import settings lib_path = settings.GDAL_LIBRARY_PATH -except (AttributeError, EnvironmentError, ImportError): +except (AttributeError, EnvironmentError, + ImportError, ImproperlyConfigured): lib_path = None if lib_path: @@ -65,28 +73,15 @@ _version_info.restype = c_char_p def gdal_version(): "Returns only the GDAL version number information." - return _version_info('RELEASE_NAME') + return _version_info(b'RELEASE_NAME') def gdal_full_version(): "Returns the full GDAL version information." return _version_info('') -def gdal_release_date(date=False): - """ - Returns the release date in a string format, e.g, "2007/06/27". - If the date keyword argument is set to True, a Python datetime object - will be returned instead. - """ - from datetime import date as date_type - rel = _version_info('RELEASE_DATE') - yy, mm, dd = map(int, (rel[0:4], rel[4:6], rel[6:8])) - d = date_type(yy, mm, dd) - if date: return d - else: return d.strftime('%Y/%m/%d') - version_regex = re.compile(r'^(?P\d+)\.(?P\d+)(\.(?P\d+))?') def gdal_version_info(): - ver = gdal_version() + ver = gdal_version().decode() m = version_regex.match(ver) if not m: raise OGRException('Could not parse GDAL version string "%s"' % ver) return dict([(key, m.group(key)) for key in ('major', 'minor', 'subminor')]) @@ -97,3 +92,18 @@ GDAL_MINOR_VERSION = int(_verinfo['minor']) GDAL_SUBMINOR_VERSION = _verinfo['subminor'] and int(_verinfo['subminor']) GDAL_VERSION = (GDAL_MAJOR_VERSION, GDAL_MINOR_VERSION, GDAL_SUBMINOR_VERSION) del _verinfo + +# Set library error handling so as errors are logged +CPLErrorHandler = CFUNCTYPE(None, c_int, c_int, c_char_p) +def err_handler(error_class, error_number, message): + logger.error('GDAL_ERROR %d: %s' % (error_number, message)) +err_handler = CPLErrorHandler(err_handler) + +def function(name, args, restype): + func = std_call(name) + func.argtypes = args + func.restype = restype + return func + +set_error_handler = function('CPLSetErrorHandler', [CPLErrorHandler], CPLErrorHandler) +set_error_handler(err_handler) diff --git a/django/contrib/gis/gdal/prototypes/ds.py b/django/contrib/gis/gdal/prototypes/ds.py index d8537bcaa4..f798069fd0 100644 --- a/django/contrib/gis/gdal/prototypes/ds.py +++ b/django/contrib/gis/gdal/prototypes/ds.py @@ -17,7 +17,7 @@ cleanup_all = void_output(lgdal.OGRCleanupAll, [], errcheck=False) get_driver = voidptr_output(lgdal.OGRGetDriver, [c_int]) get_driver_by_name = voidptr_output(lgdal.OGRGetDriverByName, [c_char_p]) get_driver_count = int_output(lgdal.OGRGetDriverCount, []) -get_driver_name = const_string_output(lgdal.OGR_Dr_GetName, [c_void_p]) +get_driver_name = const_string_output(lgdal.OGR_Dr_GetName, [c_void_p], decoding='ascii') ### DataSource ### open_ds = voidptr_output(lgdal.OGROpen, [c_char_p, c_int, POINTER(c_void_p)]) diff --git a/django/contrib/gis/gdal/prototypes/errcheck.py b/django/contrib/gis/gdal/prototypes/errcheck.py index d8ff1c7dcf..2d2791124c 100644 --- a/django/contrib/gis/gdal/prototypes/errcheck.py +++ b/django/contrib/gis/gdal/prototypes/errcheck.py @@ -125,4 +125,4 @@ def check_str_arg(result, func, cargs): """ dbl = result ptr = cargs[-1]._obj - return dbl, ptr.value + return dbl, ptr.value.decode() diff --git a/django/contrib/gis/gdal/prototypes/generation.py b/django/contrib/gis/gdal/prototypes/generation.py index 45cffd645a..577d29bbaa 100644 --- a/django/contrib/gis/gdal/prototypes/generation.py +++ b/django/contrib/gis/gdal/prototypes/generation.py @@ -57,7 +57,7 @@ def srs_output(func, argtypes): func.errcheck = check_srs return func -def const_string_output(func, argtypes, offset=None): +def const_string_output(func, argtypes, offset=None, decoding=None): func.argtypes = argtypes if offset: func.restype = c_int @@ -65,12 +65,15 @@ def const_string_output(func, argtypes, offset=None): func.restype = c_char_p def _check_const(result, func, cargs): - return check_const_string(result, func, cargs, offset=offset) + res = check_const_string(result, func, cargs, offset=offset) + if res and decoding: + res = res.decode(decoding) + return res func.errcheck = _check_const return func -def string_output(func, argtypes, offset=-1, str_result=False): +def string_output(func, argtypes, offset=-1, str_result=False, decoding=None): """ Generates a ctypes prototype for the given function with the given argument types that returns a string from a GDAL pointer. @@ -90,8 +93,11 @@ def string_output(func, argtypes, offset=-1, str_result=False): # Dynamically defining our error-checking function with the # given offset. def _check_str(result, func, cargs): - return check_string(result, func, cargs, + res = check_string(result, func, cargs, offset=offset, str_result=str_result) + if res and decoding: + res = res.decode(decoding) + return res func.errcheck = _check_str return func diff --git a/django/contrib/gis/gdal/prototypes/geom.py b/django/contrib/gis/gdal/prototypes/geom.py index f2c833d576..fa0b503c65 100644 --- a/django/contrib/gis/gdal/prototypes/geom.py +++ b/django/contrib/gis/gdal/prototypes/geom.py @@ -27,8 +27,8 @@ def topology_func(f): # GeoJSON routines. from_json = geom_output(lgdal.OGR_G_CreateGeometryFromJson, [c_char_p]) -to_json = string_output(lgdal.OGR_G_ExportToJson, [c_void_p], str_result=True) -to_kml = string_output(lgdal.OGR_G_ExportToKML, [c_void_p, c_char_p], str_result=True) +to_json = string_output(lgdal.OGR_G_ExportToJson, [c_void_p], str_result=True, decoding='ascii') +to_kml = string_output(lgdal.OGR_G_ExportToKML, [c_void_p, c_char_p], str_result=True, decoding='ascii') # GetX, GetY, GetZ all return doubles. getx = pnt_func(lgdal.OGR_G_GetX) @@ -57,8 +57,8 @@ destroy_geom = void_output(lgdal.OGR_G_DestroyGeometry, [c_void_p], errcheck=Fal # Geometry export routines. to_wkb = void_output(lgdal.OGR_G_ExportToWkb, None, errcheck=True) # special handling for WKB. -to_wkt = string_output(lgdal.OGR_G_ExportToWkt, [c_void_p, POINTER(c_char_p)]) -to_gml = string_output(lgdal.OGR_G_ExportToGML, [c_void_p], str_result=True) +to_wkt = string_output(lgdal.OGR_G_ExportToWkt, [c_void_p, POINTER(c_char_p)], decoding='ascii') +to_gml = string_output(lgdal.OGR_G_ExportToGML, [c_void_p], str_result=True, decoding='ascii') get_wkbsize = int_output(lgdal.OGR_G_WkbSize, [c_void_p]) # Geometry spatial-reference related routines. @@ -73,7 +73,7 @@ get_coord_dim = int_output(lgdal.OGR_G_GetCoordinateDimension, [c_void_p]) set_coord_dim = void_output(lgdal.OGR_G_SetCoordinateDimension, [c_void_p, c_int], errcheck=False) get_geom_count = int_output(lgdal.OGR_G_GetGeometryCount, [c_void_p]) -get_geom_name = const_string_output(lgdal.OGR_G_GetGeometryName, [c_void_p]) +get_geom_name = const_string_output(lgdal.OGR_G_GetGeometryName, [c_void_p], decoding='ascii') get_geom_type = int_output(lgdal.OGR_G_GetGeometryType, [c_void_p]) get_point_count = int_output(lgdal.OGR_G_GetPointCount, [c_void_p]) get_point = void_output(lgdal.OGR_G_GetPoint, [c_void_p, c_int, POINTER(c_double), POINTER(c_double), POINTER(c_double)], errcheck=False) diff --git a/django/contrib/gis/gdal/prototypes/srs.py b/django/contrib/gis/gdal/prototypes/srs.py index 66cf84c34f..58ceb75456 100644 --- a/django/contrib/gis/gdal/prototypes/srs.py +++ b/django/contrib/gis/gdal/prototypes/srs.py @@ -49,17 +49,17 @@ linear_units = units_func(lgdal.OSRGetLinearUnits) angular_units = units_func(lgdal.OSRGetAngularUnits) # For exporting to WKT, PROJ.4, "Pretty" WKT, and XML. -to_wkt = string_output(std_call('OSRExportToWkt'), [c_void_p, POINTER(c_char_p)]) -to_proj = string_output(std_call('OSRExportToProj4'), [c_void_p, POINTER(c_char_p)]) -to_pretty_wkt = string_output(std_call('OSRExportToPrettyWkt'), [c_void_p, POINTER(c_char_p), c_int], offset=-2) +to_wkt = string_output(std_call('OSRExportToWkt'), [c_void_p, POINTER(c_char_p)], decoding='ascii') +to_proj = string_output(std_call('OSRExportToProj4'), [c_void_p, POINTER(c_char_p)], decoding='ascii') +to_pretty_wkt = string_output(std_call('OSRExportToPrettyWkt'), [c_void_p, POINTER(c_char_p), c_int], offset=-2, decoding='ascii') # Memory leak fixed in GDAL 1.5; still exists in 1.4. -to_xml = string_output(lgdal.OSRExportToXML, [c_void_p, POINTER(c_char_p), c_char_p], offset=-2) +to_xml = string_output(lgdal.OSRExportToXML, [c_void_p, POINTER(c_char_p), c_char_p], offset=-2, decoding='ascii') # String attribute retrival routines. -get_attr_value = const_string_output(std_call('OSRGetAttrValue'), [c_void_p, c_char_p, c_int]) -get_auth_name = const_string_output(lgdal.OSRGetAuthorityName, [c_void_p, c_char_p]) -get_auth_code = const_string_output(lgdal.OSRGetAuthorityCode, [c_void_p, c_char_p]) +get_attr_value = const_string_output(std_call('OSRGetAttrValue'), [c_void_p, c_char_p, c_int], decoding='ascii') +get_auth_name = const_string_output(lgdal.OSRGetAuthorityName, [c_void_p, c_char_p], decoding='ascii') +get_auth_code = const_string_output(lgdal.OSRGetAuthorityCode, [c_void_p, c_char_p], decoding='ascii') # SRS Properties isgeographic = int_output(lgdal.OSRIsGeographic, [c_void_p]) diff --git a/django/contrib/gis/gdal/srs.py b/django/contrib/gis/gdal/srs.py index cdeaaca690..66a8d4ec93 100644 --- a/django/contrib/gis/gdal/srs.py +++ b/django/contrib/gis/gdal/srs.py @@ -34,6 +34,8 @@ from django.contrib.gis.gdal.error import SRSException from django.contrib.gis.gdal.prototypes import srs as capi from django.utils import six +from django.utils.encoding import force_bytes + #### Spatial Reference class. #### class SpatialReference(GDALBase): @@ -51,7 +53,6 @@ class SpatialReference(GDALBase): EPSG code, a PROJ.4 string, and/or a projection "well known" shorthand string (one of 'WGS84', 'WGS72', 'NAD27', 'NAD83'). """ - buf = c_char_p('') srs_type = 'user' if isinstance(srs_input, six.string_types): @@ -79,6 +80,7 @@ class SpatialReference(GDALBase): srs = srs_input else: # Creating a new SRS pointer, using the string buffer. + buf = c_char_p(b'') srs = capi.new_srs(buf) # If the pointer is NULL, throw an exception. @@ -137,15 +139,15 @@ class SpatialReference(GDALBase): """ if not isinstance(target, six.string_types) or not isinstance(index, int): raise TypeError - return capi.get_attr_value(self.ptr, target, index) + return capi.get_attr_value(self.ptr, force_bytes(target), index) def auth_name(self, target): "Returns the authority name for the given string target node." - return capi.get_auth_name(self.ptr, target) + return capi.get_auth_name(self.ptr, force_bytes(target)) def auth_code(self, target): "Returns the authority code for the given string target node." - return capi.get_auth_code(self.ptr, target) + return capi.get_auth_code(self.ptr, force_bytes(target)) def clone(self): "Returns a clone of this SpatialReference object." @@ -219,12 +221,14 @@ class SpatialReference(GDALBase): and will automatically determines whether to return the linear or angular units. """ + units, name = None, None if self.projected or self.local: - return capi.linear_units(self.ptr, byref(c_char_p())) + units, name = capi.linear_units(self.ptr, byref(c_char_p())) elif self.geographic: - return capi.angular_units(self.ptr, byref(c_char_p())) - else: - return (None, None) + units, name = capi.angular_units(self.ptr, byref(c_char_p())) + if name is not None: + name.decode() + return (units, name) #### Spheroid/Ellipsoid Properties #### @property @@ -283,7 +287,7 @@ class SpatialReference(GDALBase): def import_user_input(self, user_input): "Imports the Spatial Reference from the given user input string." - capi.from_user_input(self.ptr, user_input) + capi.from_user_input(self.ptr, force_bytes(user_input)) def import_wkt(self, wkt): "Imports the Spatial Reference from OGC WKT (string)" diff --git a/django/contrib/gis/gdal/tests/test_ds.py b/django/contrib/gis/gdal/tests/test_ds.py index 22394a2888..a87a1c6c35 100644 --- a/django/contrib/gis/gdal/tests/test_ds.py +++ b/django/contrib/gis/gdal/tests/test_ds.py @@ -4,12 +4,13 @@ from django.contrib.gis.gdal import DataSource, Envelope, OGRGeometry, OGRExcept from django.contrib.gis.gdal.field import OFTReal, OFTInteger, OFTString from django.contrib.gis.geometry.test_data import get_ds_file, TestDS, TEST_DATA + # List of acceptable data sources. ds_list = (TestDS('test_point', nfeat=5, nfld=3, geom='POINT', gtype=1, driver='ESRI Shapefile', fields={'dbl' : OFTReal, 'int' : OFTInteger, 'str' : OFTString,}, extent=(-1.35011,0.166623,-0.524093,0.824508), # Got extent from QGIS srs_wkt='GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]', - field_values={'dbl' : [float(i) for i in range(1, 6)], 'int' : range(1, 6), 'str' : [str(i) for i in range(1, 6)]}, + field_values={'dbl' : [float(i) for i in range(1, 6)], 'int' : list(range(1, 6)), 'str' : [str(i) for i in range(1, 6)]}, fids=range(5)), TestDS('test_vrt', ext='vrt', nfeat=3, nfld=3, geom='POINT', gtype='Point25D', driver='VRT', fields={'POINT_X' : OFTString, 'POINT_Y' : OFTString, 'NUM' : OFTString}, # VRT uses CSV, which all types are OFTString. @@ -59,7 +60,6 @@ class DataSourceTest(unittest.TestCase): def test03a_layers(self): "Testing Data Source Layers." - print("\nBEGIN - expecting out of range feature id error; safe to ignore.\n") for source in ds_list: ds = DataSource(source.ds) @@ -108,7 +108,6 @@ class DataSourceTest(unittest.TestCase): # the feature values here while in this loop. for fld_name in fld_names: self.assertEqual(source.field_values[fld_name][i], feat.get(fld_name)) - print("\nEND - expecting out of range feature id error; safe to ignore.") def test03b_layer_slice(self): "Test indexing and slicing on Layers." @@ -126,7 +125,9 @@ class DataSourceTest(unittest.TestCase): self.assertEqual(control_vals, test_vals) def test03c_layer_references(self): - "Test to make sure Layer access is still available without the DataSource." + """ + Ensure OGR objects keep references to the objects they belong to. + """ source = ds_list[0] # See ticket #9448. @@ -142,6 +143,9 @@ class DataSourceTest(unittest.TestCase): self.assertEqual(source.nfeat, len(lyr)) self.assertEqual(source.gtype, lyr.geom_type.num) + # Same issue for Feature/Field objects, see #18640 + self.assertEqual(str(lyr[0]['str']), "1") + def test04_features(self): "Testing Data Source Features." for source in ds_list: @@ -163,7 +167,8 @@ class DataSourceTest(unittest.TestCase): self.assertEqual(True, isinstance(feat[k], v)) # Testing Feature.__iter__ - for fld in feat: self.assertEqual(True, fld.name in source.fields.keys()) + for fld in feat: + self.assertEqual(True, fld.name in source.fields.keys()) def test05_geometries(self): "Testing Geometries from Data Source Features." @@ -200,7 +205,7 @@ class DataSourceTest(unittest.TestCase): # Setting the spatial filter with a tuple/list with the extent of # a buffer centering around Pueblo. - self.assertRaises(ValueError, lyr._set_spatial_filter, range(5)) + self.assertRaises(ValueError, lyr._set_spatial_filter, list(range(5))) filter_extent = (-105.609252, 37.255001, -103.609252, 39.255001) lyr.spatial_filter = (-105.609252, 37.255001, -103.609252, 39.255001) self.assertEqual(OGRGeometry.from_bbox(filter_extent), lyr.spatial_filter) diff --git a/django/contrib/gis/gdal/tests/test_geom.py b/django/contrib/gis/gdal/tests/test_geom.py index dda22036e3..9b8ae6a26b 100644 --- a/django/contrib/gis/gdal/tests/test_geom.py +++ b/django/contrib/gis/gdal/tests/test_geom.py @@ -92,7 +92,7 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin): "Testing HEX input/output." for g in self.geometries.hex_wkt: geom1 = OGRGeometry(g.wkt) - self.assertEqual(g.hex, geom1.hex) + self.assertEqual(g.hex.encode(), geom1.hex) # Constructing w/HEX geom2 = OGRGeometry(g.hex) self.assertEqual(geom1, geom2) @@ -102,7 +102,7 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin): for g in self.geometries.hex_wkt: geom1 = OGRGeometry(g.wkt) wkb = geom1.wkb - self.assertEqual(b2a_hex(wkb).upper(), g.hex) + self.assertEqual(b2a_hex(wkb).upper(), g.hex.encode()) # Constructing w/WKB. geom2 = OGRGeometry(wkb) self.assertEqual(geom1, geom2) @@ -235,15 +235,8 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin): # Both rings in this geometry are not closed. poly = OGRGeometry('POLYGON((0 0, 5 0, 5 5, 0 5), (1 1, 2 1, 2 2, 2 1))') self.assertEqual(8, poly.point_count) - print("\nBEGIN - expecting IllegalArgumentException; safe to ignore.\n") - try: - c = poly.centroid - except OGRException: - # Should raise an OGR exception, rings are not closed - pass - else: - self.fail('Should have raised an OGRException!') - print("\nEND - expecting IllegalArgumentException; safe to ignore.\n") + with self.assertRaises(OGRException): + _ = poly.centroid poly.close_rings() self.assertEqual(10, poly.point_count) # Two closing points should've been added diff --git a/django/contrib/gis/geometry/test_data.py b/django/contrib/gis/geometry/test_data.py index 505f0e4f4b..b0f6e1ad57 100644 --- a/django/contrib/gis/geometry/test_data.py +++ b/django/contrib/gis/geometry/test_data.py @@ -2,7 +2,6 @@ This module has the mock object definitions used to hold reference geometry for the GEOS and GDAL tests. """ -import gzip import json import os @@ -100,7 +99,7 @@ class TestDataMixin(object): global GEOMETRIES if GEOMETRIES is None: # Load up the test geometry data from fixture into global. - gzf = gzip.GzipFile(os.path.join(TEST_DATA, 'geometries.json.gz')) - geometries = json.loads(gzf.read()) + with open(os.path.join(TEST_DATA, 'geometries.json')) as f: + geometries = json.load(f) GEOMETRIES = TestGeomSet(**strconvert(geometries)) return GEOMETRIES diff --git a/django/contrib/gis/geos/factory.py b/django/contrib/gis/geos/factory.py index fbd7d5a3e9..2e5fa4f331 100644 --- a/django/contrib/gis/geos/factory.py +++ b/django/contrib/gis/geos/factory.py @@ -1,7 +1,9 @@ +from django.contrib.gis import memoryview from django.contrib.gis.geos.geometry import GEOSGeometry, wkt_regex, hex_regex from django.utils import six + def fromfile(file_h): """ Given a string file name, returns a GEOSGeometry. The file may contain WKB, @@ -14,11 +16,19 @@ def fromfile(file_h): else: buf = file_h.read() - # If we get WKB need to wrap in buffer(), so run through regexes. - if wkt_regex.match(buf) or hex_regex.match(buf): - return GEOSGeometry(buf) + # If we get WKB need to wrap in memoryview(), so run through regexes. + if isinstance(buf, bytes): + try: + decoded = buf.decode() + if wkt_regex.match(decoded) or hex_regex.match(decoded): + return GEOSGeometry(decoded) + except UnicodeDecodeError: + pass else: - return GEOSGeometry(buffer(buf)) + return GEOSGeometry(buf) + + return GEOSGeometry(memoryview(buf)) + def fromstr(string, **kwargs): "Given a string value, returns a GEOSGeometry object." diff --git a/django/contrib/gis/geos/geometry.py b/django/contrib/gis/geos/geometry.py index 4e5409de1d..df396bdbd3 100644 --- a/django/contrib/gis/geos/geometry.py +++ b/django/contrib/gis/geos/geometry.py @@ -2,9 +2,12 @@ This module contains the 'base' GEOSGeometry object -- all GEOS Geometries inherit from this object. """ +from __future__ import unicode_literals + # Python, ctypes and types dependencies. from ctypes import addressof, byref, c_double +from django.contrib.gis import memoryview # super-class for mutable list behavior from django.contrib.gis.geos.mutable_list import ListMixin @@ -22,12 +25,14 @@ from django.contrib.gis.geos import prototypes as capi # These functions provide access to a thread-local instance # of their corresponding GEOS I/O class. -from django.contrib.gis.geos.prototypes.io import wkt_r, wkt_w, wkb_r, wkb_w, ewkb_w, ewkb_w3d +from django.contrib.gis.geos.prototypes.io import wkt_r, wkt_w, wkb_r, wkb_w, ewkb_w # For recognizing geometry input. from django.contrib.gis.geometry.regex import hex_regex, wkt_regex, json_regex from django.utils import six +from django.utils.encoding import force_bytes, force_text + class GEOSGeometry(GEOSBase, ListMixin): "A class that, generally, encapsulates a GEOS geometry." @@ -54,19 +59,17 @@ class GEOSGeometry(GEOSBase, ListMixin): The `srid` keyword is used to specify the Source Reference Identifier (SRID) number for this Geometry. If not set, the SRID will be None. """ + if isinstance(geo_input, bytes): + geo_input = force_text(geo_input) if isinstance(geo_input, six.string_types): - if isinstance(geo_input, six.text_type): - # Encoding to ASCII, WKT or HEXEWKB doesn't need any more. - geo_input = geo_input.encode('ascii') - wkt_m = wkt_regex.match(geo_input) if wkt_m: # Handling WKT input. if wkt_m.group('srid'): srid = int(wkt_m.group('srid')) - g = wkt_r().read(wkt_m.group('wkt')) + g = wkt_r().read(force_bytes(wkt_m.group('wkt'))) elif hex_regex.match(geo_input): # Handling HEXEWKB input. - g = wkb_r().read(geo_input) + g = wkb_r().read(force_bytes(geo_input)) elif gdal.HAS_GDAL and json_regex.match(geo_input): # Handling GeoJSON input. g = wkb_r().read(gdal.OGRGeometry(geo_input).wkb) @@ -75,7 +78,7 @@ class GEOSGeometry(GEOSBase, ListMixin): elif isinstance(geo_input, GEOM_PTR): # When the input is a pointer to a geomtry (GEOM_PTR). g = geo_input - elif isinstance(geo_input, buffer): + elif isinstance(geo_input, memoryview): # When the input is a buffer (WKB). g = wkb_r().read(geo_input) elif isinstance(geo_input, GEOSGeometry): @@ -139,12 +142,12 @@ class GEOSGeometry(GEOSBase, ListMixin): def __getstate__(self): # The pickled state is simply a tuple of the WKB (in string form) # and the SRID. - return str(self.wkb), self.srid + return bytes(self.wkb), self.srid def __setstate__(self, state): # Instantiating from the tuple state that was pickled. wkb, srid = state - ptr = wkb_r().read(buffer(wkb)) + ptr = wkb_r().read(memoryview(wkb)) if not ptr: raise GEOSException('Invalid Geometry loaded from pickled state.') self.ptr = ptr self._post_init(srid) @@ -216,7 +219,7 @@ class GEOSGeometry(GEOSBase, ListMixin): @property def geom_type(self): "Returns a string representing the Geometry type, e.g. 'Polygon'" - return capi.geos_type(self.ptr) + return capi.geos_type(self.ptr).decode() @property def geom_typeid(self): @@ -283,7 +286,7 @@ class GEOSGeometry(GEOSBase, ListMixin): """ if not GEOS_PREPARE: raise GEOSException('Upgrade GEOS to 3.1 to get validity reason.') - return capi.geos_isvalidreason(self.ptr) + return capi.geos_isvalidreason(self.ptr).decode() #### Binary predicates. #### def contains(self, other): @@ -337,7 +340,7 @@ class GEOSGeometry(GEOSBase, ListMixin): """ if not isinstance(pattern, six.string_types) or len(pattern) > 9: raise GEOSException('invalid intersection matrix pattern') - return capi.geos_relatepattern(self.ptr, other.ptr, pattern) + return capi.geos_relatepattern(self.ptr, other.ptr, force_bytes(pattern)) def touches(self, other): """ @@ -379,34 +382,30 @@ class GEOSGeometry(GEOSBase, ListMixin): @property def wkt(self): "Returns the WKT (Well-Known Text) representation of this Geometry." - return wkt_w().write(self) + return wkt_w().write(self).decode() @property def hex(self): """ Returns the WKB of this Geometry in hexadecimal form. Please note - that the SRID and Z values are not included in this representation - because it is not a part of the OGC specification (use the `hexewkb` - property instead). + that the SRID is not included in this representation because it is not + a part of the OGC specification (use the `hexewkb` property instead). """ # A possible faster, all-python, implementation: # str(self.wkb).encode('hex') - return wkb_w().write_hex(self) + return wkb_w(self.hasz and 3 or 2).write_hex(self) @property def hexewkb(self): """ Returns the EWKB of this Geometry in hexadecimal form. This is an - extension of the WKB specification that includes SRID and Z values - that are a part of this geometry. + extension of the WKB specification that includes SRID value that are + a part of this geometry. """ - if self.hasz: - if not GEOS_PREPARE: - # See: http://trac.osgeo.org/geos/ticket/216 - raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D HEXEWKB.') - return ewkb_w3d().write_hex(self) - else: - return ewkb_w().write_hex(self) + if self.hasz and not GEOS_PREPARE: + # See: http://trac.osgeo.org/geos/ticket/216 + raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D HEXEWKB.') + return ewkb_w(self.hasz and 3 or 2).write_hex(self) @property def json(self): @@ -426,22 +425,19 @@ class GEOSGeometry(GEOSBase, ListMixin): as a Python buffer. SRID and Z values are not included, use the `ewkb` property instead. """ - return wkb_w().write(self) + return wkb_w(self.hasz and 3 or 2).write(self) @property def ewkb(self): """ Return the EWKB representation of this Geometry as a Python buffer. This is an extension of the WKB specification that includes any SRID - and Z values that are a part of this geometry. + value that are a part of this geometry. """ - if self.hasz: - if not GEOS_PREPARE: - # See: http://trac.osgeo.org/geos/ticket/216 - raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D EWKB.') - return ewkb_w3d().write(self) - else: - return ewkb_w().write(self) + if self.hasz and not GEOS_PREPARE: + # See: http://trac.osgeo.org/geos/ticket/216 + raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D EWKB.') + return ewkb_w(self.hasz and 3 or 2).write(self) @property def kml(self): @@ -513,7 +509,7 @@ class GEOSGeometry(GEOSBase, ListMixin): raise GEOSException("GDAL library is not available to transform() geometry.") # Creating an OGR Geometry, which is then transformed. - g = gdal.OGRGeometry(self.wkb, srid) + g = self.ogr g.transform(ct) # Getting a new GEOS pointer ptr = wkb_r().read(g.wkb) @@ -578,6 +574,20 @@ class GEOSGeometry(GEOSBase, ListMixin): "Return the envelope for this geometry (a polygon)." return self._topology(capi.geos_envelope(self.ptr)) + def interpolate(self, distance): + if not isinstance(self, (LineString, MultiLineString)): + raise TypeError('interpolate only works on LineString and MultiLineString geometries') + if not hasattr(capi, 'geos_interpolate'): + raise NotImplementedError('interpolate requires GEOS 3.2+') + return self._topology(capi.geos_interpolate(self.ptr, distance)) + + def interpolate_normalized(self, distance): + if not isinstance(self, (LineString, MultiLineString)): + raise TypeError('interpolate only works on LineString and MultiLineString geometries') + if not hasattr(capi, 'geos_interpolate_normalized'): + raise NotImplementedError('interpolate_normalized requires GEOS 3.2+') + return self._topology(capi.geos_interpolate_normalized(self.ptr, distance)) + def intersection(self, other): "Returns a Geometry representing the points shared by this Geometry and other." return self._topology(capi.geos_intersection(self.ptr, other.ptr)) @@ -587,9 +597,27 @@ class GEOSGeometry(GEOSBase, ListMixin): "Computes an interior point of this Geometry." return self._topology(capi.geos_pointonsurface(self.ptr)) + def project(self, point): + if not isinstance(point, Point): + raise TypeError('locate_point argument must be a Point') + if not isinstance(self, (LineString, MultiLineString)): + raise TypeError('locate_point only works on LineString and MultiLineString geometries') + if not hasattr(capi, 'geos_project'): + raise NotImplementedError('geos_project requires GEOS 3.2+') + return capi.geos_project(self.ptr, point.ptr) + + def project_normalized(self, point): + if not isinstance(point, Point): + raise TypeError('locate_point argument must be a Point') + if not isinstance(self, (LineString, MultiLineString)): + raise TypeError('locate_point only works on LineString and MultiLineString geometries') + if not hasattr(capi, 'geos_project_normalized'): + raise NotImplementedError('project_normalized requires GEOS 3.2+') + return capi.geos_project_normalized(self.ptr, point.ptr) + def relate(self, other): "Returns the DE-9IM intersection matrix for this Geometry and the other." - return capi.geos_relate(self.ptr, other.ptr) + return capi.geos_relate(self.ptr, other.ptr).decode() def simplify(self, tolerance=0.0, preserve_topology=False): """ diff --git a/django/contrib/gis/geos/libgeos.py b/django/contrib/gis/geos/libgeos.py index aed6cf366c..f011208ea0 100644 --- a/django/contrib/gis/geos/libgeos.py +++ b/django/contrib/gis/geos/libgeos.py @@ -6,18 +6,23 @@ This module also houses GEOS Pointer utilities, including get_pointer_arr(), and GEOM_PTR. """ +import logging import os import re -import sys from ctypes import c_char_p, Structure, CDLL, CFUNCTYPE, POINTER from ctypes.util import find_library + from django.contrib.gis.geos.error import GEOSException +from django.core.exceptions import ImproperlyConfigured + +logger = logging.getLogger('django.contrib.gis') # Custom library path set? try: from django.conf import settings lib_path = settings.GEOS_LIBRARY_PATH -except (AttributeError, EnvironmentError, ImportError): +except (AttributeError, EnvironmentError, + ImportError, ImproperlyConfigured): lib_path = None # Setting the appropriate names for the GEOS-C library. @@ -56,21 +61,23 @@ lgeos = CDLL(lib_path) # Supposed to mimic the GEOS message handler (C below): # typedef void (*GEOSMessageHandler)(const char *fmt, ...); NOTICEFUNC = CFUNCTYPE(None, c_char_p, c_char_p) -def notice_h(fmt, lst, output_h=sys.stdout): +def notice_h(fmt, lst): + fmt, lst = fmt.decode(), lst.decode() try: warn_msg = fmt % lst except: warn_msg = fmt - output_h.write('GEOS_NOTICE: %s\n' % warn_msg) + logger.warn('GEOS_NOTICE: %s\n' % warn_msg) notice_h = NOTICEFUNC(notice_h) ERRORFUNC = CFUNCTYPE(None, c_char_p, c_char_p) -def error_h(fmt, lst, output_h=sys.stderr): +def error_h(fmt, lst): + fmt, lst = fmt.decode(), lst.decode() try: err_msg = fmt % lst except: err_msg = fmt - output_h.write('GEOS_ERROR: %s\n' % err_msg) + logger.error('GEOS_ERROR: %s\n' % err_msg) error_h = ERRORFUNC(error_h) #### GEOS Geometry C data structures, and utility functions. #### diff --git a/django/contrib/gis/geos/prototypes/io.py b/django/contrib/gis/geos/prototypes/io.py index 053b9948a2..1be7da8845 100644 --- a/django/contrib/gis/geos/prototypes/io.py +++ b/django/contrib/gis/geos/prototypes/io.py @@ -1,5 +1,6 @@ import threading from ctypes import byref, c_char_p, c_int, c_char, c_size_t, Structure, POINTER +from django.contrib.gis import memoryview from django.contrib.gis.geos.base import GEOSBase from django.contrib.gis.geos.libgeos import GEOM_PTR from django.contrib.gis.geos.prototypes.errcheck import check_geom, check_string, check_sized_string @@ -7,6 +8,7 @@ from django.contrib.gis.geos.prototypes.geom import c_uchar_p, geos_char_p from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc from django.utils import six +from django.utils.encoding import force_bytes ### The WKB/WKT Reader/Writer structures and pointers ### class WKTReader_st(Structure): pass @@ -120,8 +122,9 @@ class _WKTReader(IOBase): ptr_type = WKT_READ_PTR def read(self, wkt): - if not isinstance(wkt, six.string_types): raise TypeError - return wkt_reader_read(self.ptr, wkt) + if not isinstance(wkt, (bytes, six.string_types)): + raise TypeError + return wkt_reader_read(self.ptr, force_bytes(wkt)) class _WKBReader(IOBase): _constructor = wkb_reader_create @@ -130,10 +133,10 @@ class _WKBReader(IOBase): def read(self, wkb): "Returns a _pointer_ to C GEOS Geometry object from the given WKB." - if isinstance(wkb, buffer): - wkb_s = str(wkb) + if isinstance(wkb, memoryview): + wkb_s = bytes(wkb) return wkb_reader_read(self.ptr, wkb_s, len(wkb_s)) - elif isinstance(wkb, six.string_types): + elif isinstance(wkb, (bytes, six.string_types)): return wkb_reader_read_hex(self.ptr, wkb, len(wkb)) else: raise TypeError @@ -155,7 +158,7 @@ class WKBWriter(IOBase): def write(self, geom): "Returns the WKB representation of the given geometry." - return buffer(wkb_writer_write(self.ptr, geom.ptr, byref(c_size_t()))) + return memoryview(wkb_writer_write(self.ptr, geom.ptr, byref(c_size_t()))) def write_hex(self, geom): "Returns the HEXEWKB representation of the given geometry." @@ -188,8 +191,8 @@ class WKBWriter(IOBase): return bool(ord(wkb_writer_get_include_srid(self.ptr))) def _set_include_srid(self, include): - if bool(include): flag = chr(1) - else: flag = chr(0) + if bool(include): flag = b'\x01' + else: flag = b'\x00' wkb_writer_set_include_srid(self.ptr, flag) srid = property(_get_include_srid, _set_include_srid) @@ -204,7 +207,6 @@ class ThreadLocalIO(threading.local): wkb_r = None wkb_w = None ewkb_w = None - ewkb_w3d = None thread_context = ThreadLocalIO() @@ -225,20 +227,15 @@ def wkb_r(): thread_context.wkb_r = _WKBReader() return thread_context.wkb_r -def wkb_w(): +def wkb_w(dim=2): if not thread_context.wkb_w: thread_context.wkb_w = WKBWriter() + thread_context.wkb_w.outdim = dim return thread_context.wkb_w -def ewkb_w(): +def ewkb_w(dim=2): if not thread_context.ewkb_w: thread_context.ewkb_w = WKBWriter() thread_context.ewkb_w.srid = True + thread_context.ewkb_w.outdim = dim return thread_context.ewkb_w - -def ewkb_w3d(): - if not thread_context.ewkb_w3d: - thread_context.ewkb_w3d = WKBWriter() - thread_context.ewkb_w3d.srid = True - thread_context.ewkb_w3d.outdim = 3 - return thread_context.ewkb_w3d diff --git a/django/contrib/gis/geos/prototypes/topology.py b/django/contrib/gis/geos/prototypes/topology.py index cc5734b5e4..dfea3e98b6 100644 --- a/django/contrib/gis/geos/prototypes/topology.py +++ b/django/contrib/gis/geos/prototypes/topology.py @@ -8,18 +8,18 @@ __all__ = ['geos_boundary', 'geos_buffer', 'geos_centroid', 'geos_convexhull', 'geos_simplify', 'geos_symdifference', 'geos_union', 'geos_relate'] from ctypes import c_double, c_int -from django.contrib.gis.geos.libgeos import GEOM_PTR, GEOS_PREPARE -from django.contrib.gis.geos.prototypes.errcheck import check_geom, check_string +from django.contrib.gis.geos.libgeos import geos_version_info, GEOM_PTR, GEOS_PREPARE +from django.contrib.gis.geos.prototypes.errcheck import check_geom, check_minus_one, check_string from django.contrib.gis.geos.prototypes.geom import geos_char_p from django.contrib.gis.geos.prototypes.threadsafe import GEOSFunc -def topology(func, *args): +def topology(func, *args, **kwargs): "For GEOS unary topology functions." argtypes = [GEOM_PTR] if args: argtypes += args func.argtypes = argtypes - func.restype = GEOM_PTR - func.errcheck = check_geom + func.restype = kwargs.get('restype', GEOM_PTR) + func.errcheck = kwargs.get('errcheck', check_geom) return func ### Topology Routines ### @@ -49,3 +49,16 @@ if GEOS_PREPARE: geos_cascaded_union.argtypes = [GEOM_PTR] geos_cascaded_union.restype = GEOM_PTR __all__.append('geos_cascaded_union') + +# Linear referencing routines +info = geos_version_info() +if info['version'] >= '3.2.0': + geos_project = topology(GEOSFunc('GEOSProject'), GEOM_PTR, + restype=c_double, errcheck=check_minus_one) + geos_interpolate = topology(GEOSFunc('GEOSInterpolate'), c_double) + + geos_project_normalized = topology(GEOSFunc('GEOSProjectNormalized'), + GEOM_PTR, restype=c_double, errcheck=check_minus_one) + geos_interpolate_normalized = topology(GEOSFunc('GEOSInterpolateNormalized'), c_double) + __all__.extend(['geos_project', 'geos_interpolate', + 'geos_project_normalized', 'geos_interpolate_normalized']) diff --git a/django/contrib/gis/geos/tests/test_geos.py b/django/contrib/gis/geos/tests/test_geos.py index 7300ab9c63..283daa47c0 100644 --- a/django/contrib/gis/geos/tests/test_geos.py +++ b/django/contrib/gis/geos/tests/test_geos.py @@ -1,7 +1,12 @@ +from __future__ import unicode_literals + import ctypes import json import random +from binascii import a2b_hex, b2a_hex +from io import BytesIO +from django.contrib.gis import memoryview from django.contrib.gis.geos import (GEOSException, GEOSIndexError, GEOSGeometry, GeometryCollection, Point, MultiPoint, Polygon, MultiPolygon, LinearRing, LineString, MultiLineString, fromfile, fromstr, geos_version_info) @@ -9,6 +14,7 @@ from django.contrib.gis.geos.base import gdal, numpy, GEOSBase from django.contrib.gis.geos.libgeos import GEOS_PREPARE from django.contrib.gis.geometry.test_data import TestDataMixin +from django.utils.encoding import force_bytes from django.utils import six from django.utils.six.moves import xrange from django.utils import unittest @@ -64,7 +70,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): # result in a TypeError when trying to assign it to the `ptr` property. # Thus, memmory addresses (integers) and pointers of the incorrect type # (in `bad_ptrs`) will not be allowed. - bad_ptrs = (5, ctypes.c_char_p('foobar')) + bad_ptrs = (5, ctypes.c_char_p(b'foobar')) for bad_ptr in bad_ptrs: # Equivalent to `fg.ptr = bad_ptr` self.assertRaises(TypeError, fg1._set_ptr, bad_ptr) @@ -80,25 +86,24 @@ class GEOSTest(unittest.TestCase, TestDataMixin): "Testing HEX output." for g in self.geometries.hex_wkt: geom = fromstr(g.wkt) - self.assertEqual(g.hex, geom.hex) + self.assertEqual(g.hex, geom.hex.decode()) def test_hexewkb(self): "Testing (HEX)EWKB output." - from binascii import a2b_hex - # For testing HEX(EWKB). - ogc_hex = '01010000000000000000000000000000000000F03F' + ogc_hex = b'01010000000000000000000000000000000000F03F' + ogc_hex_3d = b'01010000800000000000000000000000000000F03F0000000000000040' # `SELECT ST_AsHEXEWKB(ST_GeomFromText('POINT(0 1)', 4326));` - hexewkb_2d = '0101000020E61000000000000000000000000000000000F03F' + hexewkb_2d = b'0101000020E61000000000000000000000000000000000F03F' # `SELECT ST_AsHEXEWKB(ST_GeomFromEWKT('SRID=4326;POINT(0 1 2)'));` - hexewkb_3d = '01010000A0E61000000000000000000000000000000000F03F0000000000000040' + hexewkb_3d = b'01010000A0E61000000000000000000000000000000000F03F0000000000000040' pnt_2d = Point(0, 1, srid=4326) pnt_3d = Point(0, 1, 2, srid=4326) - # OGC-compliant HEX will not have SRID nor Z value. + # OGC-compliant HEX will not have SRID value. self.assertEqual(ogc_hex, pnt_2d.hex) - self.assertEqual(ogc_hex, pnt_3d.hex) + self.assertEqual(ogc_hex_3d, pnt_3d.hex) # HEXEWKB should be appropriate for its dimension -- have to use an # a WKBWriter w/dimension set accordingly, else GEOS will insert @@ -118,9 +123,9 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.fail('Should have raised GEOSException.') # Same for EWKB. - self.assertEqual(buffer(a2b_hex(hexewkb_2d)), pnt_2d.ewkb) + self.assertEqual(memoryview(a2b_hex(hexewkb_2d)), pnt_2d.ewkb) if GEOS_PREPARE: - self.assertEqual(buffer(a2b_hex(hexewkb_3d)), pnt_3d.ewkb) + self.assertEqual(memoryview(a2b_hex(hexewkb_3d)), pnt_3d.ewkb) else: try: ewkb = pnt_3d.ewkb @@ -142,17 +147,12 @@ class GEOSTest(unittest.TestCase, TestDataMixin): def test_errors(self): "Testing the Error handlers." # string-based - print("\nBEGIN - expecting GEOS_ERROR; safe to ignore.\n") for err in self.geometries.errors: - try: - g = fromstr(err.wkt) - except (GEOSException, ValueError): - pass + with self.assertRaises((GEOSException, ValueError)): + _ = fromstr(err.wkt) # Bad WKB - self.assertRaises(GEOSException, GEOSGeometry, buffer('0')) - - print("\nEND - expecting GEOS_ERROR; safe to ignore.\n") + self.assertRaises(GEOSException, GEOSGeometry, memoryview(b'0')) class NotAGeometry(object): pass @@ -164,11 +164,10 @@ class GEOSTest(unittest.TestCase, TestDataMixin): def test_wkb(self): "Testing WKB output." - from binascii import b2a_hex for g in self.geometries.hex_wkt: geom = fromstr(g.wkt) wkb = geom.wkb - self.assertEqual(b2a_hex(wkb).upper(), g.hex) + self.assertEqual(b2a_hex(wkb).decode().upper(), g.hex) def test_create_hex(self): "Testing creation from HEX." @@ -180,9 +179,8 @@ class GEOSTest(unittest.TestCase, TestDataMixin): def test_create_wkb(self): "Testing creation from WKB." - from binascii import a2b_hex for g in self.geometries.hex_wkt: - wkb = buffer(a2b_hex(g.hex)) + wkb = memoryview(a2b_hex(g.hex.encode())) geom_h = GEOSGeometry(wkb) # we need to do this so decimal places get normalised geom_t = fromstr(g.wkt) @@ -212,13 +210,12 @@ class GEOSTest(unittest.TestCase, TestDataMixin): def test_fromfile(self): "Testing the fromfile() factory." - from io import BytesIO ref_pnt = GEOSGeometry('POINT(5 23)') wkt_f = BytesIO() - wkt_f.write(ref_pnt.wkt) + wkt_f.write(force_bytes(ref_pnt.wkt)) wkb_f = BytesIO() - wkb_f.write(str(ref_pnt.wkb)) + wkb_f.write(bytes(ref_pnt.wkb)) # Other tests use `fromfile()` on string filenames so those # aren't tested here. @@ -439,8 +436,8 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertEqual(r.geom_typeid, 2) # Testing polygon construction. - self.assertRaises(TypeError, Polygon.__init__, 0, [1, 2, 3]) - self.assertRaises(TypeError, Polygon.__init__, 'foo') + self.assertRaises(TypeError, Polygon, 0, [1, 2, 3]) + self.assertRaises(TypeError, Polygon, 'foo') # Polygon(shell, (hole1, ... holeN)) rings = tuple(r for r in poly) @@ -456,7 +453,6 @@ class GEOSTest(unittest.TestCase, TestDataMixin): def test_multipolygons(self): "Testing MultiPolygon objects." - print("\nBEGIN - expecting GEOS_NOTICE; safe to ignore.\n") prev = fromstr('POINT (0 0)') for mp in self.geometries.multipolygons: mpoly = fromstr(mp.wkt) @@ -475,8 +471,6 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertEqual(p.valid, True) self.assertEqual(mpoly.wkt, MultiPolygon(*tuple(poly.clone() for poly in mpoly)).wkt) - print("\nEND - expecting GEOS_NOTICE; safe to ignore.\n") - def test_memory_hijinks(self): "Testing Geometry __del__() on rings and polygons." #### Memory issues with rings and polygons @@ -829,12 +823,17 @@ class GEOSTest(unittest.TestCase, TestDataMixin): def test_gdal(self): "Testing `ogr` and `srs` properties." g1 = fromstr('POINT(5 23)') - self.assertEqual(True, isinstance(g1.ogr, gdal.OGRGeometry)) - self.assertEqual(g1.srs, None) + self.assertIsInstance(g1.ogr, gdal.OGRGeometry) + self.assertIsNone(g1.srs) + + if GEOS_PREPARE: + g1_3d = fromstr('POINT(5 23 8)') + self.assertIsInstance(g1_3d.ogr, gdal.OGRGeometry) + self.assertEqual(g1_3d.ogr.z, 8) g2 = fromstr('LINESTRING(0 0, 5 5, 23 23)', srid=4326) - self.assertEqual(True, isinstance(g2.ogr, gdal.OGRGeometry)) - self.assertEqual(True, isinstance(g2.srs, gdal.SpatialReference)) + self.assertIsInstance(g2.ogr, gdal.OGRGeometry) + self.assertIsInstance(g2.srs, gdal.SpatialReference) self.assertEqual(g2.hex, g2.ogr.hex) self.assertEqual('WGS 84', g2.srs.name) @@ -847,7 +846,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertNotEqual(poly._ptr, cpy1._ptr) self.assertNotEqual(poly._ptr, cpy2._ptr) - @unittest.skipUnless(gdal.HAS_GDAL, "gdal is required") + @unittest.skipUnless(gdal.HAS_GDAL, "gdal is required to transform geometries") def test_transform(self): "Testing `transform` method." orig = GEOSGeometry('POINT (-104.609 38.255)', 4326) @@ -872,6 +871,15 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertAlmostEqual(trans.x, p.x, prec) self.assertAlmostEqual(trans.y, p.y, prec) + @unittest.skipUnless(gdal.HAS_GDAL, "gdal is required to transform geometries") + def test_transform_3d(self): + p3d = GEOSGeometry('POINT (5 23 100)', 4326) + p3d.transform(2774) + if GEOS_PREPARE: + self.assertEqual(p3d.z, 100) + else: + self.assertIsNone(p3d.z) + def test_transform_noop(self): """ Testing `transform` method (SRID match) """ # transform() should no-op if source & dest SRIDs match, @@ -1009,18 +1017,35 @@ class GEOSTest(unittest.TestCase, TestDataMixin): g = GEOSGeometry("POINT(0 0)") self.assertTrue(g.valid) - self.assertTrue(isinstance(g.valid_reason, six.string_types)) + self.assertIsInstance(g.valid_reason, six.string_types) self.assertEqual(g.valid_reason, "Valid Geometry") - print("\nBEGIN - expecting GEOS_NOTICE; safe to ignore.\n") - g = GEOSGeometry("LINESTRING(0 0, 0 0)") - self.assertTrue(not g.valid) - self.assertTrue(isinstance(g.valid_reason, six.string_types)) + self.assertFalse(g.valid) + self.assertIsInstance(g.valid_reason, six.string_types) self.assertTrue(g.valid_reason.startswith("Too few points in geometry component")) - print("\nEND - expecting GEOS_NOTICE; safe to ignore.\n") + @unittest.skipUnless(geos_version_info()['version'] >= '3.2.0', "geos >= 3.2.0 is required") + def test_linearref(self): + "Testing linear referencing" + + ls = fromstr('LINESTRING(0 0, 0 10, 10 10, 10 0)') + mls = fromstr('MULTILINESTRING((0 0, 0 10), (10 0, 10 10))') + + self.assertEqual(ls.project(Point(0, 20)), 10.0) + self.assertEqual(ls.project(Point(7, 6)), 24) + self.assertEqual(ls.project_normalized(Point(0, 20)), 1.0/3) + + self.assertEqual(ls.interpolate(10), Point(0, 10)) + self.assertEqual(ls.interpolate(24), Point(10, 6)) + self.assertEqual(ls.interpolate_normalized(1.0/3), Point(0, 10)) + + self.assertEqual(mls.project(Point(0, 20)), 10) + self.assertEqual(mls.project(Point(7, 6)), 16) + + self.assertEqual(mls.interpolate(9), Point(0, 9)) + self.assertEqual(mls.interpolate(17), Point(10, 7)) def test_geos_version(self): "Testing the GEOS version regular expression." diff --git a/django/contrib/gis/geos/tests/test_io.py b/django/contrib/gis/geos/tests/test_io.py index ebf178a807..45a9a220b1 100644 --- a/django/contrib/gis/geos/tests/test_io.py +++ b/django/contrib/gis/geos/tests/test_io.py @@ -1,8 +1,13 @@ +from __future__ import unicode_literals + import binascii import unittest + +from django.contrib.gis import memoryview from django.contrib.gis.geos import GEOSGeometry, WKTReader, WKTWriter, WKBReader, WKBWriter, geos_version_info from django.utils import six + class GEOSIOTest(unittest.TestCase): def test01_wktreader(self): @@ -12,15 +17,15 @@ class GEOSIOTest(unittest.TestCase): # read() should return a GEOSGeometry ref = GEOSGeometry(wkt) - g1 = wkt_r.read(wkt) - g2 = wkt_r.read(six.text_type(wkt)) + g1 = wkt_r.read(wkt.encode()) + g2 = wkt_r.read(wkt) for geom in (g1, g2): self.assertEqual(ref, geom) # Should only accept six.string_types objects. self.assertRaises(TypeError, wkt_r.read, 1) - self.assertRaises(TypeError, wkt_r.read, buffer('foo')) + self.assertRaises(TypeError, wkt_r.read, memoryview(b'foo')) def test02_wktwriter(self): # Creating a WKTWriter instance, testing its ptr property. @@ -29,14 +34,14 @@ class GEOSIOTest(unittest.TestCase): ref = GEOSGeometry('POINT (5 23)') ref_wkt = 'POINT (5.0000000000000000 23.0000000000000000)' - self.assertEqual(ref_wkt, wkt_w.write(ref)) + self.assertEqual(ref_wkt, wkt_w.write(ref).decode()) def test03_wkbreader(self): # Creating a WKBReader instance wkb_r = WKBReader() - hex = '000000000140140000000000004037000000000000' - wkb = buffer(binascii.a2b_hex(hex)) + hex = b'000000000140140000000000004037000000000000' + wkb = memoryview(binascii.a2b_hex(hex)) ref = GEOSGeometry(hex) # read() should return a GEOSGeometry on either a hex string or @@ -56,10 +61,10 @@ class GEOSIOTest(unittest.TestCase): # Representations of 'POINT (5 23)' in hex -- one normal and # the other with the byte order changed. g = GEOSGeometry('POINT (5 23)') - hex1 = '010100000000000000000014400000000000003740' - wkb1 = buffer(binascii.a2b_hex(hex1)) - hex2 = '000000000140140000000000004037000000000000' - wkb2 = buffer(binascii.a2b_hex(hex2)) + hex1 = b'010100000000000000000014400000000000003740' + wkb1 = memoryview(binascii.a2b_hex(hex1)) + hex2 = b'000000000140140000000000004037000000000000' + wkb2 = memoryview(binascii.a2b_hex(hex2)) self.assertEqual(hex1, wkb_w.write_hex(g)) self.assertEqual(wkb1, wkb_w.write(g)) @@ -81,10 +86,10 @@ class GEOSIOTest(unittest.TestCase): g = GEOSGeometry('POINT (5 23 17)') g.srid = 4326 - hex3d = '0101000080000000000000144000000000000037400000000000003140' - wkb3d = buffer(binascii.a2b_hex(hex3d)) - hex3d_srid = '01010000A0E6100000000000000000144000000000000037400000000000003140' - wkb3d_srid = buffer(binascii.a2b_hex(hex3d_srid)) + hex3d = b'0101000080000000000000144000000000000037400000000000003140' + wkb3d = memoryview(binascii.a2b_hex(hex3d)) + hex3d_srid = b'01010000A0E6100000000000000000144000000000000037400000000000003140' + wkb3d_srid = memoryview(binascii.a2b_hex(hex3d_srid)) # Ensuring bad output dimensions are not accepted for bad_outdim in (-1, 0, 1, 4, 423, 'foo', None): @@ -100,7 +105,7 @@ class GEOSIOTest(unittest.TestCase): self.assertEqual(hex3d, wkb_w.write_hex(g)) self.assertEqual(wkb3d, wkb_w.write(g)) - # Telling the WKBWriter to inlcude the srid in the representation. + # Telling the WKBWriter to include the srid in the representation. wkb_w.srid = True self.assertEqual(hex3d_srid, wkb_w.write_hex(g)) self.assertEqual(wkb3d_srid, wkb_w.write(g)) diff --git a/django/contrib/gis/locale/en/LC_MESSAGES/django.po b/django/contrib/gis/locale/en/LC_MESSAGES/django.po index 2b2f16d851..6ce301287e 100644 --- a/django/contrib/gis/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/gis/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-03-23 02:38+0100\n" +"POT-Creation-Date: 2012-10-15 10:56+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -13,76 +13,76 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: views.py:7 +#: views.py:9 msgid "No feeds are registered." msgstr "" -#: views.py:17 +#: views.py:19 #, python-format msgid "Slug %r isn't registered." msgstr "" -#: db/models/fields.py:50 +#: db/models/fields.py:51 msgid "The base GIS field -- maps to the OpenGIS Specification Geometry type." msgstr "" -#: db/models/fields.py:270 +#: db/models/fields.py:271 msgid "Point" msgstr "" -#: db/models/fields.py:274 +#: db/models/fields.py:275 msgid "Line string" msgstr "" -#: db/models/fields.py:278 +#: db/models/fields.py:279 msgid "Polygon" msgstr "" -#: db/models/fields.py:282 +#: db/models/fields.py:283 msgid "Multi-point" msgstr "" -#: db/models/fields.py:286 +#: db/models/fields.py:287 msgid "Multi-line string" msgstr "" -#: db/models/fields.py:290 +#: db/models/fields.py:291 msgid "Multi polygon" msgstr "" -#: db/models/fields.py:294 +#: db/models/fields.py:295 msgid "Geometry collection" msgstr "" -#: forms/fields.py:17 +#: forms/fields.py:19 msgid "No geometry value provided." msgstr "" -#: forms/fields.py:18 +#: forms/fields.py:20 msgid "Invalid geometry value." msgstr "" -#: forms/fields.py:19 +#: forms/fields.py:21 msgid "Invalid geometry type." msgstr "" -#: forms/fields.py:20 +#: forms/fields.py:22 msgid "" "An error occurred when transforming the geometry to the SRID of the geometry " "form field." msgstr "" -#: sitemaps/views.py:44 +#: sitemaps/views.py:46 #, python-format msgid "No sitemap available for section: %r" msgstr "" -#: sitemaps/views.py:58 +#: sitemaps/views.py:60 #, python-format msgid "Page %s empty" msgstr "" -#: sitemaps/views.py:60 +#: sitemaps/views.py:62 #, python-format msgid "No page '%s'" msgstr "" diff --git a/django/contrib/gis/templates/gis/admin/openlayers.js b/django/contrib/gis/templates/gis/admin/openlayers.js index f54b75e258..eb40edae8f 100644 --- a/django/contrib/gis/templates/gis/admin/openlayers.js +++ b/django/contrib/gis/templates/gis/admin/openlayers.js @@ -1,4 +1,4 @@ -{% load l10n %}{# Author: Justin Bronn, Travis Pinney & Dane Springmeyer #} +{% load l10n %} OpenLayers.Projection.addTransform("EPSG:4326", "EPSG:3857", OpenLayers.Layer.SphericalMercator.projectForward); {% block vars %}var {{ module }} = {}; {{ module }}.map = null; {{ module }}.controls = null; {{ module }}.panel = null; {{ module }}.re = new RegExp("^SRID=\\d+;(.+)", "i"); {{ module }}.layers = {}; @@ -109,10 +109,12 @@ OpenLayers.Projection.addTransform("EPSG:4326", "EPSG:3857", OpenLayers.Layer.Sp {% autoescape off %}{% for item in map_options.items %} '{{ item.0 }}' : {{ item.1 }}{% if not forloop.last %},{% endif %} {% endfor %}{% endautoescape %} };{% endblock %} // The admin map for this geometry field. + {% block map_creation %} {{ module }}.map = new OpenLayers.Map('{{ id }}_map', options); // Base Layer {{ module }}.layers.base = {% block base_layer %}new OpenLayers.Layer.WMS("{{ wms_name }}", "{{ wms_url }}", {layers: '{{ wms_layer }}'{{ wms_options|safe }}});{% endblock %} {{ module }}.map.addLayer({{ module }}.layers.base); + {% endblock %} {% block extra_layers %}{% endblock %} {% if is_linestring %}OpenLayers.Feature.Vector.style["default"]["strokeWidth"] = 3; // Default too thin for linestrings. {% endif %} {{ module }}.layers.vector = new OpenLayers.Layer.Vector(" {{ field_name }}"); diff --git a/django/contrib/gis/tests/data/ch-city/ch-city.dbf b/django/contrib/gis/tests/data/ch-city/ch-city.dbf new file mode 100644 index 0000000000..6ba9d698f7 Binary files /dev/null and b/django/contrib/gis/tests/data/ch-city/ch-city.dbf differ diff --git a/django/contrib/gis/tests/data/ch-city/ch-city.prj b/django/contrib/gis/tests/data/ch-city/ch-city.prj new file mode 100644 index 0000000000..a30c00a55d --- /dev/null +++ b/django/contrib/gis/tests/data/ch-city/ch-city.prj @@ -0,0 +1 @@ +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] \ No newline at end of file diff --git a/django/contrib/gis/tests/data/ch-city/ch-city.shp b/django/contrib/gis/tests/data/ch-city/ch-city.shp new file mode 100644 index 0000000000..e430020de0 Binary files /dev/null and b/django/contrib/gis/tests/data/ch-city/ch-city.shp differ diff --git a/django/contrib/gis/tests/data/ch-city/ch-city.shx b/django/contrib/gis/tests/data/ch-city/ch-city.shx new file mode 100644 index 0000000000..a1487ad829 Binary files /dev/null and b/django/contrib/gis/tests/data/ch-city/ch-city.shx differ diff --git a/django/contrib/gis/tests/data/geometries.json b/django/contrib/gis/tests/data/geometries.json new file mode 100644 index 0000000000..46de4d6182 --- /dev/null +++ b/django/contrib/gis/tests/data/geometries.json @@ -0,0 +1,121 @@ +{ + "polygons": [ + {"wkt": "POLYGON ((0 0, 0 100, 100 100, 100 0, 0 0), (10 10, 10 90, 90 90, 90 10, 10 10))", "n_i": 1, "ext_ring_cs": [[0, 0], [0, 100], [100, 100], [100, 0], [0, 0]], "n_p": 10, "area": 3600.0, "centroid": [50.0, 50.0]}, + {"wkt": "POLYGON ((0 0, 0 100, 100 100, 100 0, 0 0), (10 10, 10 20, 20 20, 20 10, 10 10), (80 80, 80 90, 90 90, 90 80, 80 80))", "n_i": 2, "ext_ring_cs": [[0, 0], [0, 100], [100, 100], [100, 0], [0, 0]], "n_p": 15, "area": 9800.0, "centroid": [50.0, 50.0]}, + {"wkt": "POLYGON ((0 0, 0 100, 100 100, 100 0, 0 0))", "n_i": 0, "ext_ring_cs": [[0, 0], [0, 100], [100, 100], [100, 0], [0, 0]], "n_p": 5, "area": 10000.0, "centroid": [50.0, 50.0]}, + {"wkt": "POLYGON ((-95.3848703124799471 29.7056021479768511, -95.3851905195191847 29.7046588196500281, -95.3859356966379011 29.7025053545605502, -95.3860723000647539 29.7020963367038391, -95.3871517697222089 29.6989779021280995, -95.3865578518265522 29.6990856888057202, -95.3862634205175226 29.6999471753441782, -95.3861991779541967 29.6999591988978615, -95.3856773799358137 29.6998323107113578, -95.3856209915427229 29.6998005235473741, -95.3855833545501639 29.6996619391729801, -95.3855776331865002 29.6996232659570047, -95.3850162731712885 29.6997236706530536, -95.3831047357410284 29.7000847603095082, -95.3829800724914776 29.7000676365023502, -95.3828084594470909 29.6999969684031200, -95.3828131504821499 29.6999090511531065, -95.3828022942979601 29.6998152117366025, -95.3827893930918833 29.6997790953076759, -95.3825174668099862 29.6998267772748825, -95.3823521544804862 29.7000451723151606, -95.3820491918785223 29.6999682034582335, -95.3817932841505893 29.6999640407204772, -95.3815438924600443 29.7005983712500630, -95.3807812390843424 29.7007538492921590, -95.3778578936435935 29.7012966201172048, -95.3770817300034679 29.7010555145969093, -95.3772763716395957 29.7004995005932031, -95.3769891024414420 29.7005797730360186, -95.3759855007185990 29.7007754783987821, -95.3759516423090474 29.7007305400669388, -95.3765252155960042 29.6989549173240874, -95.3766842746727832 29.6985134987163164, -95.3768510987262914 29.6980530300744938, -95.3769198676258014 29.6977137204527573, -95.3769616670751930 29.6973351617272172, -95.3770309229297766 29.6969821084304186, -95.3772352596880637 29.6959751305871613, -95.3776232419333354 29.6945439060847463, -95.3776849628727064 29.6943364710766069, -95.3779699491714723 29.6926548349458947, -95.3781945479573494 29.6920088336742545, -95.3785807118394189 29.6908279316076005, -95.3787441368896651 29.6908846275832197, -95.3787903214163890 29.6907152912461640, -95.3791765069353659 29.6893335376821526, -95.3794935959513026 29.6884781789101595, -95.3796592071232112 29.6880066681407619, -95.3799788182090111 29.6873687353035081, -95.3801545516183893 29.6868782380716993, -95.3801258908302145 29.6867756621337762, -95.3801104284899566 29.6867229678809572, -95.3803803523746154 29.6863753372986459, -95.3821028558287622 29.6837392961470421, -95.3827289584682205 29.6828097375216160, -95.3827494698109035 29.6790739156259278, -95.3826022014838486 29.6776502228345507, -95.3825047356438063 29.6765773006280753, -95.3823473035336917 29.6750405250369127, -95.3824540163482055 29.6750076408228587, -95.3838984230304305 29.6745679207378679, -95.3916547074937426 29.6722459226508377, -95.3926154662749468 29.6719609085105489, -95.3967246645118081 29.6707316485589736, -95.3974588054406780 29.6705065336410989, -95.3978523748756828 29.6703795547846845, -95.3988598162279970 29.6700874981900853, -95.3995628600665952 29.6698505300412414, -95.4134721665944170 29.6656841279906232, -95.4143262068232616 29.6654291174019278, -95.4159685142480214 29.6649750989232288, -95.4180067396277565 29.6643253024318021, -95.4185886692196590 29.6641482768691063, -95.4234155309609662 29.6626925393704788, -95.4287785503196346 29.6611023620959706, -95.4310287312749352 29.6604222580752648, -95.4320295629628959 29.6603361318136720, -95.4332899683975739 29.6600560661713608, -95.4342675748811047 29.6598454934599900, -95.4343110414310871 29.6598411486215490, -95.4345576779282538 29.6598147020668499, -95.4348823041721630 29.6597875803673112, -95.4352827715209457 29.6597762346946681, -95.4355290431309982 29.6597827926562374, -95.4359197997999331 29.6598014511782715, -95.4361907884752156 29.6598444333523368, -95.4364608955807228 29.6598901433108217, -95.4367250147512323 29.6599494499910712, -95.4364898759758091 29.6601880616540186, -95.4354501111810691 29.6616378572201107, -95.4381459623171224 29.6631265631655126, -95.4367852490863129 29.6642266600024023, -95.4370040894557263 29.6643425389568769, -95.4367078350812648 29.6645492592343238, -95.4366081749871285 29.6646291473027297, -95.4358539359938192 29.6652308742342932, -95.4350327668927889 29.6658995989314462, -95.4350580905272921 29.6678812477895271, -95.4349710541447536 29.6680054925936965, -95.4349500440473548 29.6671410080890006, -95.4341492724148850 29.6678790545191688, -95.4340248868274728 29.6680353198492135, -95.4333227845797438 29.6689245624945990, -95.4331325652123326 29.6691616138940901, -95.4321314741096955 29.6704473333237253, -95.4320435792664341 29.6702578985411982, -95.4320147929883547 29.6701800936425109, -95.4319764538662980 29.6683246590817085, -95.4317490976340679 29.6684974372577166, -95.4305958185342718 29.6694049049170374, -95.4296600735653016 29.6701723430938493, -95.4284928989940937 29.6710931793380972, -95.4274630532378580 29.6719378813640091, -95.4273056811974811 29.6720684984625791, -95.4260554084574864 29.6730668861566969, -95.4253558063699643 29.6736342467365724, -95.4249278826026028 29.6739557343648919, -95.4248648873821423 29.6745400910786152, -95.4260016131471929 29.6750987014005858, -95.4258567183010911 29.6753452063069929, -95.4260238081486847 29.6754322077221353, -95.4258707374502393 29.6756647377294307, -95.4257951755816691 29.6756407098663360, -95.4257701599566985 29.6761077719536068, -95.4257726684792260 29.6761711204603955, -95.4257980187195614 29.6770219651929423, -95.4252712669032519 29.6770161558853758, -95.4249234392992065 29.6770068683962300, -95.4249574272905789 29.6779707498635759, -95.4244725881033702 29.6779825646764159, -95.4222269476429545 29.6780711474441716, -95.4223032371999267 29.6796029391538809, -95.4239133706588945 29.6795331493690355, -95.4224579084327331 29.6813706893847780, -95.4224290108823965 29.6821953228763924, -95.4230916478977349 29.6822130268724109, -95.4222928279595521 29.6832041816675343, -95.4228763710016352 29.6832087677714505, -95.4223401691637179 29.6838987872753748, -95.4211655906087088 29.6838784024852984, -95.4201984153205558 29.6851319258758082, -95.4206156387716362 29.6851623398125319, -95.4213438084897660 29.6851763011334739, -95.4212071118618752 29.6853679931624974, -95.4202651399651245 29.6865313962980508, -95.4172061157659783 29.6865816431043932, -95.4182217951255183 29.6872251197301544, -95.4178664826439160 29.6876750901471631, -95.4180678442928780 29.6877960336377207, -95.4188763472917572 29.6882826379510938, -95.4185374500596311 29.6887137897831934, -95.4182121713132290 29.6885097429738813, -95.4179857231741551 29.6888118367840086, -95.4183106010563620 29.6890048676118212, -95.4179489865331334 29.6894546700979056, -95.4175581746284820 29.6892323606815438, -95.4173439957341571 29.6894990139807007, -95.4177411199311081 29.6897435034738422, -95.4175789200209721 29.6899207529979208, -95.4170598559864800 29.6896042165807508, -95.4166733682539814 29.6900891174451367, -95.4165941362704331 29.6900347214235047, -95.4163537218065301 29.6903529467753238, -95.4126843270708775 29.6881086357212780, -95.4126604121378392 29.6880942378803496, -95.4126672298953338 29.6885951670109982, -95.4126680884821923 29.6887052446594275, -95.4158080137241882 29.6906382377959339, -95.4152061403821961 29.6910871045531586, -95.4155842583188161 29.6917382915894308, -95.4157426793520358 29.6920726941677096, -95.4154520563662203 29.6922052332446427, -95.4151389936167078 29.6923261661269571, -95.4148649784384872 29.6924343866430256, -95.4144051352401590 29.6925623927348106, -95.4146792019416665 29.6926770338507744, -95.4148824479948985 29.6928117893696388, -95.4149851734360226 29.6929823719519774, -95.4140436551925291 29.6929626643100946, -95.4140465993023241 29.6926545917254892, -95.4137269186733334 29.6927395764256090, -95.4137372859685513 29.6935432485666624, -95.4135702836218655 29.6933186678088283, -95.4133925235973237 29.6930415229852152, -95.4133017035615580 29.6928685062036166, -95.4129588921634593 29.6929391128977862, -95.4125107395559695 29.6930481664661485, -95.4102647423187307 29.6935850183258019, -95.4081931340840157 29.6940907430947760, -95.4078783596459772 29.6941703429951609, -95.4049213975000043 29.6948723732981961, -95.4045944244127071 29.6949626434239207, -95.4045865139788134 29.6954109019001358, -95.4045953345484037 29.6956972800496963, -95.4038879332535146 29.6958296089365490, -95.4040366394459340 29.6964389004769842, -95.4032774779020798 29.6965643341263892, -95.4026066501239853 29.6966646227683881, -95.4024991226393837 29.6961389766619703, -95.4011781398631911 29.6963566063186377, -95.4011524097636112 29.6962596176762190, -95.4018184046368276 29.6961399466727336, -95.4016995838361908 29.6956442609415099, -95.4007100753964608 29.6958900524002978, -95.4008032469935188 29.6962639900781404, -95.3995660267125487 29.6965636449370329, -95.3996140564775601 29.6967877962763644, -95.3996364430014410 29.6968901984825280, -95.3984003269631842 29.6968679634805746, -95.3981442026887265 29.6983660679730335, -95.3980178461957706 29.6990890276252415, -95.3977097967130163 29.7008526152273049, -95.3962347157626027 29.7009697553607630, -95.3951949050136250 29.7004740386619019, -95.3957564950617183 29.6990281830553187, -95.3965927101519924 29.6968771129030706, -95.3957496517238184 29.6970800358387095, -95.3957720559467361 29.6972264611230727, -95.3957391586571788 29.6973548894558732, -95.3956286413405365 29.6974949857280883, -95.3955111053256957 29.6975661086270186, -95.3953215342724121 29.6976022763384790, -95.3951795558443365 29.6975846977491038, -95.3950369632041060 29.6975175779330200, -95.3949401089966500 29.6974269267953304, -95.3948740281415581 29.6972903308506346, -95.3946650813866910 29.6973397326847923, -95.3947654059391112 29.6974882560192022, -95.3949627316619768 29.6980355864961858, -95.3933200807862249 29.6984590863712796, -95.3932606497523494 29.6984464798710839, -95.3932983699113350 29.6983154306484352, -95.3933058014696655 29.6982165816983610, -95.3932946347785133 29.6981089778195759, -95.3931780601756287 29.6977068906794841, -95.3929928222970602 29.6977541771878180, -95.3930873169846478 29.6980676264932946, -95.3932743746374570 29.6981249406449663, -95.3929512584706316 29.6989526513922222, -95.3919850280655197 29.7014358632108646, -95.3918950918929056 29.7014169320765724, -95.3916928317890296 29.7019232352846423, -95.3915424614970959 29.7022988712928289, -95.3901530441668939 29.7058519502930061, -95.3899656322116698 29.7059156823562418, -95.3897628748670883 29.7059900058266777, -95.3896062677805787 29.7060738276384946, -95.3893941800512266 29.7061891695242046, -95.3892150365492455 29.7062641292949436, -95.3890502563035199 29.7063339729630940, -95.3888717930715586 29.7063896908080736, -95.3886925428988945 29.7064453871994978, -95.3885376849411983 29.7064797304524149, -95.3883284158984139 29.7065153575050189, -95.3881046767627794 29.7065368368267357, -95.3878809284696132 29.7065363048447537, -95.3876046356120924 29.7065288525102424, -95.3873060894974714 29.7064822806001452, -95.3869851943158409 29.7063993367575350, -95.3865967896568065 29.7062870572919202, -95.3861785624983156 29.7061492099008184, -95.3857375009733488 29.7059887337478798, -95.3854573290902152 29.7058683664514618, -95.3848703124799471 29.7056021479768511))", "n_i": 0, "ext_ring_cs": false, "n_p": 264, "area": 0.00129917360654, "centroid": [-95.403569179437341, 29.681772571690402]} + ], + "multipolygons": [ + {"wkt": "MULTIPOLYGON (((100 20, 180 20, 180 100, 100 100, 100 20)), ((20 100, 100 100, 100 180, 20 180, 20 100)), ((100 180, 180 180, 180 260, 100 260, 100 180)), ((180 100, 260 100, 260 180, 180 180, 180 100)))","valid": true, "num_geom":4, "n_p":20}, + {"wkt": "MULTIPOLYGON (((60 300, 320 220, 260 60, 60 100, 60 300)), ((60 300, 320 220, 260 60, 60 100, 60 300)))", "valid": false}, + {"wkt": "MULTIPOLYGON (((180 60, 240 160, 300 60, 180 60)), ((80 80, 180 60, 160 140, 240 160, 360 140, 300 60, 420 100, 320 280, 120 260, 80 80)))", "valid": true, "num_geom": 2, "n_p": 14} + ], + "errors": [ + {"wkt": "GEOMETR##!@#%#............a32515", "bad": true, "hex": false}, + {"wkt": "Foo.Bar", "bad": true, "hex": false}, + {"wkt": "POINT (5, 23)", "bad": true, "hex": false}, + {"wkt": "AAABBBDDDAAD##@#1113511111-098111111111111111533333333333333", "bad": true, "hex": true}, + {"wkt": "FFFFFFFFFFFFFFFFF1355555555555555555565111", "bad": true, "hex": true}, + {"wkt": "", "bad": true, "hex": false} + ], + "wkt_out": [ + {"wkt": "POINT (110 130)", "ewkt": "POINT (110.0000000000000000 130.0000000000000000)", "kml": "110.0,130.0,0", "gml": "110,130"}, + {"wkt": "LINESTRING (40 40,50 130,130 130)", "ewkt": "LINESTRING (40.0000000000000000 40.0000000000000000, 50.0000000000000000 130.0000000000000000, 130.0000000000000000 130.0000000000000000)", "kml": "40.0,40.0,0 50.0,130.0,0 130.0,130.0,0", "gml": "40,40 50,130 130,130"}, + {"wkt": "POLYGON ((150 150,410 150,280 20,20 20,150 150),(170 120,330 120,260 50,100 50,170 120))", "ewkt": "POLYGON ((150.0000000000000000 150.0000000000000000, 410.0000000000000000 150.0000000000000000, 280.0000000000000000 20.0000000000000000, 20.0000000000000000 20.0000000000000000, 150.0000000000000000 150.0000000000000000), (170.0000000000000000 120.0000000000000000, 330.0000000000000000 120.0000000000000000, 260.0000000000000000 50.0000000000000000, 100.0000000000000000 50.0000000000000000, 170.0000000000000000 120.0000000000000000))", "kml": "150.0,150.0,0 410.0,150.0,0 280.0,20.0,0 20.0,20.0,0 150.0,150.0,0170.0,120.0,0 330.0,120.0,0 260.0,50.0,0 100.0,50.0,0 170.0,120.0,0", "gml": "150,150 410,150 280,20 20,20 150,150170,120 330,120 260,50 100,50 170,120"}, + {"wkt": "MULTIPOINT (10 80,110 170,110 120)", "ewkt": "MULTIPOINT (10.0000000000000000 80.0000000000000000, 110.0000000000000000 170.0000000000000000, 110.0000000000000000 120.0000000000000000)", "kml": "10.0,80.0,0110.0,170.0,0110.0,120.0,0", "gml": "10,80110,170110,120"}, + {"wkt": "MULTILINESTRING ((110 100,40 30,180 30),(170 30,110 90,50 30))", "ewkt": "MULTILINESTRING ((110.0000000000000000 100.0000000000000000, 40.0000000000000000 30.0000000000000000, 180.0000000000000000 30.0000000000000000), (170.0000000000000000 30.0000000000000000, 110.0000000000000000 90.0000000000000000, 50.0000000000000000 30.0000000000000000))", "kml": "110.0,100.0,0 40.0,30.0,0 180.0,30.0,0170.0,30.0,0 110.0,90.0,0 50.0,30.0,0", "gml": "110,100 40,30 180,30170,30 110,90 50,30"}, + {"wkt": "MULTIPOLYGON (((110 110,70 200,150 200,110 110),(110 110,100 180,120 180,110 110)),((110 110,150 20,70 20,110 110),(110 110,120 40,100 40,110 110)))", "ewkt": "MULTIPOLYGON (((110.0000000000000000 110.0000000000000000, 70.0000000000000000 200.0000000000000000, 150.0000000000000000 200.0000000000000000, 110.0000000000000000 110.0000000000000000), (110.0000000000000000 110.0000000000000000, 100.0000000000000000 180.0000000000000000, 120.0000000000000000 180.0000000000000000, 110.0000000000000000 110.0000000000000000)), ((110.0000000000000000 110.0000000000000000, 150.0000000000000000 20.0000000000000000, 70.0000000000000000 20.0000000000000000, 110.0000000000000000 110.0000000000000000), (110.0000000000000000 110.0000000000000000, 120.0000000000000000 40.0000000000000000, 100.0000000000000000 40.0000000000000000, 110.0000000000000000 110.0000000000000000)))", "kml": "110.0,110.0,0 70.0,200.0,0 150.0,200.0,0 110.0,110.0,0110.0,110.0,0 100.0,180.0,0 120.0,180.0,0 110.0,110.0,0110.0,110.0,0 150.0,20.0,0 70.0,20.0,0 110.0,110.0,0110.0,110.0,0 120.0,40.0,0 100.0,40.0,0 110.0,110.0,0", "gml": "110,110 70,200 150,200 110,110110,110 100,180 120,180 110,110110,110 150,20 70,20 110,110110,110 120,40 100,40 110,110"}, + {"wkt": "GEOMETRYCOLLECTION (POINT (110 260),LINESTRING (110 0,110 60))", "ewkt": "GEOMETRYCOLLECTION (POINT (110.0000000000000000 260.0000000000000000), LINESTRING (110.0000000000000000 0.0000000000000000, 110.0000000000000000 60.0000000000000000))", "kml": "110.0,260.0,0110.0,0.0,0 110.0,60.0,0", "gml": "110,260110,0 110,60"} + ], + "hex_wkt": [ + {"wkt": "POINT(0 1)", "hex": "01010000000000000000000000000000000000F03F"}, + {"wkt": "LINESTRING(0 1, 2 3, 4 5)", "hex": "0102000000030000000000000000000000000000000000F03F0000000000000040000000000000084000000000000010400000000000001440"}, + {"wkt": "POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))", "hex": "010300000001000000050000000000000000000000000000000000000000000000000024400000000000000000000000000000244000000000000024400000000000000000000000000000244000000000000000000000000000000000"}, + {"wkt": "MULTIPOINT(0 0, 10 0, 10 10, 0 10, 0 0)", "hex": "010400000005000000010100000000000000000000000000000000000000010100000000000000000024400000000000000000010100000000000000000024400000000000002440010100000000000000000000000000000000002440010100000000000000000000000000000000000000"}, + {"wkt": "MULTILINESTRING((0 0, 10 0, 10 10, 0 10),(20 20, 30 20))", "hex": "01050000000200000001020000000400000000000000000000000000000000000000000000000000244000000000000000000000000000002440000000000000244000000000000000000000000000002440010200000002000000000000000000344000000000000034400000000000003E400000000000003440"}, + {"wkt": "MULTIPOLYGON(((0 0, 10 0, 10 10, 0 10, 0 0)),((20 20, 20 30, 30 30, 30 20, 20 20),(25 25, 25 26, 26 26, 26 25, 25 25)))", "hex": "010600000002000000010300000001000000050000000000000000000000000000000000000000000000000024400000000000000000000000000000244000000000000024400000000000000000000000000000244000000000000000000000000000000000010300000002000000050000000000000000003440000000000000344000000000000034400000000000003E400000000000003E400000000000003E400000000000003E40000000000000344000000000000034400000000000003440050000000000000000003940000000000000394000000000000039400000000000003A400000000000003A400000000000003A400000000000003A40000000000000394000000000000039400000000000003940"}, + {"wkt": "GEOMETRYCOLLECTION(MULTIPOLYGON(((0 0, 10 0, 10 10, 0 10, 0 0)),((20 20, 20 30, 30 30, 30 20, 20 20),(25 25, 25 26, 26 26, 26 25, 25 25))),MULTILINESTRING((0 0, 10 0, 10 10, 0 10),(20 20, 30 20)),MULTIPOINT(0 0, 10 0, 10 10, 0 10, 0 0))", "hex": "010700000003000000010600000002000000010300000001000000050000000000000000000000000000000000000000000000000024400000000000000000000000000000244000000000000024400000000000000000000000000000244000000000000000000000000000000000010300000002000000050000000000000000003440000000000000344000000000000034400000000000003E400000000000003E400000000000003E400000000000003E40000000000000344000000000000034400000000000003440050000000000000000003940000000000000394000000000000039400000000000003A400000000000003A400000000000003A400000000000003A4000000000000039400000000000003940000000000000394001050000000200000001020000000400000000000000000000000000000000000000000000000000244000000000000000000000000000002440000000000000244000000000000000000000000000002440010200000002000000000000000000344000000000000034400000000000003E400000000000003440010400000005000000010100000000000000000000000000000000000000010100000000000000000024400000000000000000010100000000000000000024400000000000002440010100000000000000000000000000000000002440010100000000000000000000000000000000000000"} + ], + "json_geoms": [ + {"wkt": "POINT(100 0)", "json": "{ \"type\": \"Point\", \"coordinates\": [ 100.000000, 0.000000 ] }"}, + {"wkt": "POLYGON((0 0, -10 0, -10 -10, 0 -10, 0 0))", "json": "{ \"type\": \"Polygon\", \"coordinates\": [ [ [ 0.000000, 0.000000 ], [ -10.000000, 0.000000 ], [ -10.000000, -10.000000 ], [ 0.000000, -10.000000 ], [ 0.000000, 0.000000 ] ] ] }"}, + {"wkt": "MULTIPOLYGON(((102 2, 103 2, 103 3, 102 3, 102 2)), ((100.0 0.0, 101.0 0.0, 101.0 1.0, 100.0 1.0, 100.0 0.0), (100.2 0.2, 100.8 0.2, 100.8 0.8, 100.2 0.8, 100.2 0.2)))", "json": "{ \"type\": \"MultiPolygon\", \"coordinates\": [ [ [ [ 102.000000, 2.000000 ], [ 103.000000, 2.000000 ], [ 103.000000, 3.000000 ], [ 102.000000, 3.000000 ], [ 102.000000, 2.000000 ] ] ], [ [ [ 100.000000, 0.000000 ], [ 101.000000, 0.000000 ], [ 101.000000, 1.000000 ], [ 100.000000, 1.000000 ], [ 100.000000, 0.000000 ] ], [ [ 100.200000, 0.200000 ], [ 100.800000, 0.200000 ], [ 100.800000, 0.800000 ], [ 100.200000, 0.800000 ], [ 100.200000, 0.200000 ] ] ] ] }"}, + {"wkt": "GEOMETRYCOLLECTION(POINT(100 0),LINESTRING(101.0 0.0, 102.0 1.0))", "json": "{ \"type\": \"GeometryCollection\", \"geometries\": [ { \"type\": \"Point\", \"coordinates\": [ 100.000000, 0.000000 ] }, { \"type\": \"LineString\", \"coordinates\": [ [ 101.000000, 0.000000 ], [ 102.000000, 1.000000 ] ] } ] }"}, + {"wkt": "MULTILINESTRING((100.0 0.0, 101.0 1.0),(102.0 2.0, 103.0 3.0))", "json": "\\n\\n{ \"type\": \"MultiLineString\",\\n \"coordinates\": [\\n [ [100.0, 0.0], [101.0, 1.0] ],\\n [ [102.0, 2.0], [103.0, 3.0] ]\\n ]\\n }\\n\\n", "not_equal": true} + ], + "points": [ + {"wkt": "POINT (5 23)", "x": 5.0, "y": 23.0, "centroid": [5.0, 23.0]}, + {"wkt": "POINT (-95.338492 29.723893)", "x": -95.338492, "y": 29.723893, "centroid": [-95.338492, 29.723893]}, + {"wkt": "POINT(1.234 5.678)", "x": 1.234, "y": 5.678, "centroid": [1.234, 5.678]}, + {"wkt": "POINT(4.321 8.765)", "x": 4.321, "y": 8.765, "centroid": [4.321, 8.765]}, + {"wkt": "POINT(10 10)", "x": 10, "y": 10, "centroid": [10.0, 10.0]}, + {"wkt": "POINT (5 23 8)", "x": 5.0, "y": 23.0, "z": 8.0, "centroid": [5.0, 23.0]} + ], + "multipoints":[ + {"wkt": "MULTIPOINT(10 10, 20 20 )", "n_p": 2, "coords": [[10.0, 10.0], [20.0, 20.0]], "centroid": [15.0, 15.0]}, + {"wkt": "MULTIPOINT(10 10, 20 20, 10 20, 20 10)", "n_p": 4, "coords": [[10.0, 10.0], [20.0, 20.0], [10.0, 20.0], [20.0, 10.0]], "centroid": [15.0, 15.0]} + ], + "linestrings": [ + {"wkt": "LINESTRING (60 180, 120 100, 180 180)", "n_p": 3, "centroid": [120.0, 140.0], "coords": [[60.0, 180.0], [120.0, 100.0], [180.0, 180.0]]}, + {"wkt": "LINESTRING (0 0, 5 5, 10 5, 10 10)", "n_p": 4, "centroid": [6.1611652351681556, 4.6966991411008934], "coords": [[0.0, 0.0], [5.0, 5.0], [10.0, 5.0], [10.0, 10.0]]} + ], + "linearrings": [ + {"wkt": "LINEARRING (649899.3065171393100172 4176512.3807915160432458, 649902.7294133581453934 4176512.7834989596158266, 649906.5550170192727819 4176514.3942507002502680, 649910.5820134161040187 4176516.0050024418160319, 649914.4076170771149918 4176518.0184616246260703, 649917.2264131171396002 4176519.4278986593708396, 649920.0452871860470623 4176521.6427505780011415, 649922.0587463703704998 4176522.8507948759943247, 649924.2735982896992937 4176524.4616246484220028, 649926.2870574744883925 4176525.4683542405255139, 649927.8978092158213258 4176526.8777912775985897, 649929.3072462501004338 4176528.0858355751261115, 649930.1126611357321963 4176529.4952726080082357, 649927.4951798024121672 4176506.9444361114874482, 649899.3065171393100172 4176512.3807915160432458)", "n_p": 15} + ], + "multilinestrings": [ + {"wkt": "MULTILINESTRING ((0 0, 0 100), (100 0, 100 100))", "n_p": 4, "centroid": [50.0, 50.0], "coords": [[[0, 0], [0, 100]], [[100, 0], [100, 100]]]}, + {"wkt": "MULTILINESTRING ((20 20, 60 60), (20 -20, 60 -60), (-20 -20, -60 -60), (-20 20, -60 60), (-80 0, 0 80, 80 0, 0 -80, -80 0), (-40 20, -40 -20), (-20 40, 20 40), (40 20, 40 -20), (20 -40, -20 -40))", "n_p": 21, "centroid": [0.0, 0.0], "coords": [[[20.0, 20.0], [60.0, 60.0]], [[20.0, -20.0], [60.0, -60.0]], [[-20.0, -20.0], [-60.0, -60.0]], [[-20.0, 20.0], [-60.0, 60.0]], [[-80.0, 0.0], [0.0, 80.0], [80.0, 0.0], [0.0, -80.0], [-80.0, 0.0]], [[-40.0, 20.0], [-40.0, -20.0]], [[-20.0, 40.0], [20.0, 40.0]], [[40.0, 20.0], [40.0, -20.0]], [[20.0, -40.0], [-20.0, -40.0]]]} + ], + "buffer_geoms": [ + {"wkt": "POINT(0 0)", + "buffer_wkt": "POLYGON ((5 0,4.903926402016153 -0.97545161008064,4.619397662556435 -1.913417161825447,4.157348061512728 -2.777851165098009,3.53553390593274 -3.535533905932735,2.777851165098015 -4.157348061512724,1.913417161825454 -4.619397662556431,0.975451610080648 -4.903926402016151,0.000000000000008 -5.0,-0.975451610080632 -4.903926402016154,-1.913417161825439 -4.619397662556437,-2.777851165098002 -4.157348061512732,-3.53553390593273 -3.535533905932746,-4.157348061512719 -2.777851165098022,-4.619397662556429 -1.913417161825462,-4.903926402016149 -0.975451610080656,-5.0 -0.000000000000016,-4.903926402016156 0.975451610080624,-4.619397662556441 1.913417161825432,-4.157348061512737 2.777851165097995,-3.535533905932752 3.535533905932723,-2.777851165098029 4.157348061512714,-1.913417161825468 4.619397662556426,-0.975451610080661 4.903926402016149,-0.000000000000019 5.0,0.975451610080624 4.903926402016156,1.913417161825434 4.61939766255644,2.777851165097998 4.157348061512735,3.535533905932727 3.535533905932748,4.157348061512719 2.777851165098022,4.619397662556429 1.91341716182546,4.90392640201615 0.975451610080652,5 0))", + "width": 5.0, "quadsegs": 8 + }, + {"wkt": "POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))", + "buffer_wkt": "POLYGON ((-2 0,-2 10,-1.961570560806461 10.390180644032258,-1.847759065022573 10.765366864730179,-1.662939224605091 11.111140466039204,-1.414213562373095 11.414213562373096,-1.111140466039204 11.662939224605092,-0.765366864730179 11.847759065022574,-0.390180644032256 11.961570560806461,0 12,10 12,10.390180644032256 11.961570560806461,10.765366864730179 11.847759065022574,11.111140466039204 11.66293922460509,11.414213562373096 11.414213562373096,11.66293922460509 11.111140466039204,11.847759065022574 10.765366864730179,11.961570560806461 10.390180644032256,12 10,12 0,11.961570560806461 -0.390180644032256,11.847759065022574 -0.76536686473018,11.66293922460509 -1.111140466039204,11.414213562373096 -1.414213562373095,11.111140466039204 -1.66293922460509,10.765366864730179 -1.847759065022573,10.390180644032256 -1.961570560806461,10 -2,0.0 -2.0,-0.390180644032255 -1.961570560806461,-0.765366864730177 -1.847759065022575,-1.1111404660392 -1.662939224605093,-1.41421356237309 -1.4142135623731,-1.662939224605086 -1.111140466039211,-1.84775906502257 -0.765366864730189,-1.961570560806459 -0.390180644032268,-2 0))", + "width": 2.0, "quadsegs": 8 + } + ], + "relate_geoms": [ + {"wkt_a": "MULTIPOINT(80 70, 20 20, 200 170, 140 120)", + "wkt_b": "MULTIPOINT(80 170, 140 120, 200 80, 80 70)", + "pattern": "0F0FFF0F2", "result": true + }, + {"wkt_a": "POINT(20 20)", + "wkt_b": "POINT(40 60)", + "pattern": "FF0FFF0F2", "result": true + }, + {"wkt_a": "POINT(110 110)", + "wkt_b": "LINESTRING(200 200, 110 110, 200 20, 20 20, 110 110, 20 200, 200 200)", + "pattern": "0FFFFF1F2", "result": true + }, + {"wkt_a": "MULTILINESTRING((20 20, 90 20, 170 20), (90 20, 90 80, 90 140))", + "wkt_b": "MULTILINESTRING((90 20, 170 100, 170 140), (130 140, 130 60, 90 20, 20 90, 90 20))", + "pattern": "FF10F0102", "result": true + } + ], + "topology_geoms": [ + {"wkt_a": "POLYGON ((-5.0 0.0, -5.0 10.0, 5.0 10.0, 5.0 0.0, -5.0 0.0))", + "wkt_b": "POLYGON ((0.0 -5.0, 0.0 5.0, 10.0 5.0, 10.0 -5.0, 0.0 -5.0))" + }, + {"wkt_a": "POLYGON ((2 0, 18 0, 18 15, 2 15, 2 0))", + "wkt_b": "POLYGON ((10 1, 11 3, 13 4, 15 6, 16 8, 16 10, 15 12, 13 13, 11 12, 10 10, 9 12, 7 13, 5 12, 4 10, 4 8, 5 6, 7 4, 9 3, 10 1))" + } + ], + "diff_geoms": [ + {"wkt": "POLYGON ((-5 0,-5 10,5 10,5 5,0 5,0 0,-5 0))"}, + {"wkt": "POLYGON ((2 0, 2 15, 18 15, 18 0, 2 0), (10 1, 11 3, 13 4, 15 6, 16 8, 16 10, 15 12, 13 13, 11 12, 10 10, 9 12, 7 13, 5 12, 4 10, 4 8, 5 6, 7 4, 9 3, 10 1))"} + ], + "sdiff_geoms": [ + {"wkt": "MULTIPOLYGON (((-5 0,-5 10,5 10,5 5,0 5,0 0,-5 0)),((0 0,5 0,5 5,10 5,10 -5,0 -5,0 0)))"}, + {"wkt": "POLYGON ((2 0, 2 15, 18 15, 18 0, 2 0), (10 1, 11 3, 13 4, 15 6, 16 8, 16 10, 15 12, 13 13, 11 12, 10 10, 9 12, 7 13, 5 12, 4 10, 4 8, 5 6, 7 4, 9 3, 10 1))"} + ], + "intersect_geoms": [ + {"wkt": "POLYGON ((5 5,5 0,0 0,0 5,5 5))"}, + {"wkt": "POLYGON ((10 1, 9 3, 7 4, 5 6, 4 8, 4 10, 5 12, 7 13, 9 12, 10 10, 11 12, 13 13, 15 12, 16 10, 16 8, 15 6, 13 4, 11 3, 10 1))"} + ], + "union_geoms": [ + {"wkt": "POLYGON ((-5 0,-5 10,5 10,5 5,10 5,10 -5,0 -5,0 0,-5 0))"}, + {"wkt": "POLYGON ((2 0, 2 15, 18 15, 18 0, 2 0))"} + ] +} \ No newline at end of file diff --git a/django/contrib/gis/tests/data/geometries.json.gz b/django/contrib/gis/tests/data/geometries.json.gz deleted file mode 100644 index 683dc83e4d..0000000000 Binary files a/django/contrib/gis/tests/data/geometries.json.gz and /dev/null differ diff --git a/django/contrib/gis/tests/distapp/tests.py b/django/contrib/gis/tests/distapp/tests.py index bf075add6c..5574b42738 100644 --- a/django/contrib/gis/tests/distapp/tests.py +++ b/django/contrib/gis/tests/distapp/tests.py @@ -324,7 +324,7 @@ class DistanceTest(TestCase): else: qs = Interstate.objects.length() if oracle: tol = 2 - else: tol = 5 + else: tol = 3 self.assertAlmostEqual(len_m1, qs[0].length.m, tol) # Now doing length on a projected coordinate system. diff --git a/django/contrib/gis/tests/geo3d/tests.py b/django/contrib/gis/tests/geo3d/tests.py index dfd0e496ff..f7590fe84a 100644 --- a/django/contrib/gis/tests/geo3d/tests.py +++ b/django/contrib/gis/tests/geo3d/tests.py @@ -1,16 +1,17 @@ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals import os import re -from django.utils.unittest import TestCase from django.contrib.gis.db.models import Union, Extent3D -from django.contrib.gis.geos import GEOSGeometry, Point, Polygon +from django.contrib.gis.geos import GEOSGeometry, LineString, Point, Polygon from django.contrib.gis.utils import LayerMapping, LayerMapError +from django.test import TestCase from .models import (City3D, Interstate2D, Interstate3D, InterstateProj2D, InterstateProj3D, Point2D, Point3D, MultiPoint3D, Polygon2D, Polygon3D) + data_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'data')) city_file = os.path.join(data_path, 'cities', 'cities.shp') vrt_file = os.path.join(data_path, 'test_vrt', 'test_vrt.vrt') @@ -46,12 +47,11 @@ interstate_data = ( # Bounding box polygon for inner-loop of Houston (in projected coordinate # system 32140), with elevation values from the National Elevation Dataset # (see above). -bbox_wkt = 'POLYGON((941527.97 4225693.20,962596.48 4226349.75,963152.57 4209023.95,942051.75 4208366.38,941527.97 4225693.20))' -bbox_z = (21.71, 13.21, 9.12, 16.40, 21.71) -def gen_bbox(): - bbox_2d = GEOSGeometry(bbox_wkt, srid=32140) - bbox_3d = Polygon(tuple((x, y, z) for (x, y), z in zip(bbox_2d[0].coords, bbox_z)), srid=32140) - return bbox_2d, bbox_3d +bbox_data = ( + 'POLYGON((941527.97 4225693.20,962596.48 4226349.75,963152.57 4209023.95,942051.75 4208366.38,941527.97 4225693.20))', + (21.71, 13.21, 9.12, 16.40, 21.71) +) + class Geo3DTest(TestCase): """ @@ -63,25 +63,11 @@ class Geo3DTest(TestCase): http://postgis.refractions.net/documentation/manual-1.4/ch08.html#PostGIS_3D_Functions """ - def test01_3d(self): - "Test the creation of 3D models." - # 3D models for the rest of the tests will be populated in here. - # For each 3D data set create model (and 2D version if necessary), - # retrieve, and assert geometry is in 3D and contains the expected - # 3D values. - for name, pnt_data in city_data: - x, y, z = pnt_data - pnt = Point(x, y, z, srid=4326) - City3D.objects.create(name=name, point=pnt) - city = City3D.objects.get(name=name) - self.assertTrue(city.point.hasz) - self.assertEqual(z, city.point.z) - + def _load_interstate_data(self): # Interstate (2D / 3D and Geographic/Projected variants) for name, line, exp_z in interstate_data: line_3d = GEOSGeometry(line, srid=4269) - # Using `hex` attribute because it omits 3D. - line_2d = GEOSGeometry(line_3d.hex, srid=4269) + line_2d = LineString([l[:2] for l in line_3d.coords], srid=4269) # Creating a geographic and projected version of the # interstate in both 2D and 3D. @@ -90,26 +76,51 @@ class Geo3DTest(TestCase): Interstate2D.objects.create(name=name, line=line_2d) InterstateProj2D.objects.create(name=name, line=line_2d) - # Retrieving and making sure it's 3D and has expected - # Z values -- shouldn't change because of coordinate system. + def _load_city_data(self): + for name, pnt_data in city_data: + City3D.objects.create(name=name, point=Point(*pnt_data, srid=4326)) + + def _load_polygon_data(self): + bbox_wkt, bbox_z = bbox_data + bbox_2d = GEOSGeometry(bbox_wkt, srid=32140) + bbox_3d = Polygon(tuple((x, y, z) for (x, y), z in zip(bbox_2d[0].coords, bbox_z)), srid=32140) + Polygon2D.objects.create(name='2D BBox', poly=bbox_2d) + Polygon3D.objects.create(name='3D BBox', poly=bbox_3d) + + def test_3d_hasz(self): + """ + Make sure data is 3D and has expected Z values -- shouldn't change + because of coordinate system. + """ + self._load_interstate_data() + for name, line, exp_z in interstate_data: interstate = Interstate3D.objects.get(name=name) interstate_proj = InterstateProj3D.objects.get(name=name) for i in [interstate, interstate_proj]: self.assertTrue(i.line.hasz) self.assertEqual(exp_z, tuple(i.line.z)) - # Creating 3D Polygon. - bbox2d, bbox3d = gen_bbox() - Polygon2D.objects.create(name='2D BBox', poly=bbox2d) - Polygon3D.objects.create(name='3D BBox', poly=bbox3d) + self._load_city_data() + for name, pnt_data in city_data: + city = City3D.objects.get(name=name) + z = pnt_data[2] + self.assertTrue(city.point.hasz) + self.assertEqual(z, city.point.z) + + def test_3d_polygons(self): + """ + Test the creation of polygon 3D models. + """ + self._load_polygon_data() p3d = Polygon3D.objects.get(name='3D BBox') self.assertTrue(p3d.poly.hasz) - self.assertEqual(bbox3d, p3d.poly) - - def test01a_3d_layermapping(self): - "Testing LayerMapping on 3D models." - from .models import Point2D, Point3D + self.assertIsInstance(p3d.poly, Polygon) + self.assertEqual(p3d.poly.srid, 32140) + def test_3d_layermapping(self): + """ + Testing LayerMapping on 3D models. + """ point_mapping = {'point' : 'POINT'} mpoint_mapping = {'mpoint' : 'MULTIPOINT'} @@ -134,34 +145,46 @@ class Geo3DTest(TestCase): lm.save() self.assertEqual(3, MultiPoint3D.objects.count()) - def test02a_kml(self): - "Test GeoQuerySet.kml() with Z values." + def test_kml(self): + """ + Test GeoQuerySet.kml() with Z values. + """ + self._load_city_data() h = City3D.objects.kml(precision=6).get(name='Houston') # KML should be 3D. # `SELECT ST_AsKML(point, 6) FROM geo3d_city3d WHERE name = 'Houston';` ref_kml_regex = re.compile(r'^-95.363\d+,29.763\d+,18$') self.assertTrue(ref_kml_regex.match(h.kml)) - def test02b_geojson(self): - "Test GeoQuerySet.geojson() with Z values." + def test_geojson(self): + """ + Test GeoQuerySet.geojson() with Z values. + """ + self._load_city_data() h = City3D.objects.geojson(precision=6).get(name='Houston') # GeoJSON should be 3D # `SELECT ST_AsGeoJSON(point, 6) FROM geo3d_city3d WHERE name='Houston';` ref_json_regex = re.compile(r'^{"type":"Point","coordinates":\[-95.363151,29.763374,18(\.0+)?\]}$') self.assertTrue(ref_json_regex.match(h.geojson)) - def test03a_union(self): - "Testing the Union aggregate of 3D models." + def test_union(self): + """ + Testing the Union aggregate of 3D models. + """ # PostGIS query that returned the reference EWKT for this test: # `SELECT ST_AsText(ST_Union(point)) FROM geo3d_city3d;` + self._load_city_data() ref_ewkt = 'SRID=4326;MULTIPOINT(-123.305196 48.462611 15,-104.609252 38.255001 1433,-97.521157 34.464642 380,-96.801611 32.782057 147,-95.363151 29.763374 18,-95.23506 38.971823 251,-87.650175 41.850385 181,174.783117 -41.315268 14)' ref_union = GEOSGeometry(ref_ewkt) union = City3D.objects.aggregate(Union('point'))['point__union'] self.assertTrue(union.hasz) self.assertEqual(ref_union, union) - def test03b_extent(self): - "Testing the Extent3D aggregate for 3D models." + def test_extent(self): + """ + Testing the Extent3D aggregate for 3D models. + """ + self._load_city_data() # `SELECT ST_Extent3D(point) FROM geo3d_city3d;` ref_extent3d = (-123.305196, -41.315268, 14,174.783117, 48.462611, 1433) extent1 = City3D.objects.aggregate(Extent3D('point'))['point__extent3d'] @@ -174,8 +197,11 @@ class Geo3DTest(TestCase): for e3d in [extent1, extent2]: check_extent3d(e3d) - def test04_perimeter(self): - "Testing GeoQuerySet.perimeter() on 3D fields." + def test_perimeter(self): + """ + Testing GeoQuerySet.perimeter() on 3D fields. + """ + self._load_polygon_data() # Reference query for values below: # `SELECT ST_Perimeter3D(poly), ST_Perimeter2D(poly) FROM geo3d_polygon3d;` ref_perim_3d = 76859.2620451 @@ -188,12 +214,15 @@ class Geo3DTest(TestCase): Polygon3D.objects.perimeter().get(name='3D BBox').perimeter.m, tol) - def test05_length(self): - "Testing GeoQuerySet.length() on 3D fields." + def test_length(self): + """ + Testing GeoQuerySet.length() on 3D fields. + """ # ST_Length_Spheroid Z-aware, and thus does not need to use # a separate function internally. # `SELECT ST_Length_Spheroid(line, 'SPHEROID["GRS 1980",6378137,298.257222101]') # FROM geo3d_interstate[2d|3d];` + self._load_interstate_data() tol = 3 ref_length_2d = 4368.1721949481 ref_length_3d = 4368.62547052088 @@ -217,16 +246,22 @@ class Geo3DTest(TestCase): InterstateProj3D.objects.length().get(name='I-45').length.m, tol) - def test06_scale(self): - "Testing GeoQuerySet.scale() on Z values." + def test_scale(self): + """ + Testing GeoQuerySet.scale() on Z values. + """ + self._load_city_data() # Mapping of City name to reference Z values. zscales = (-3, 4, 23) for zscale in zscales: for city in City3D.objects.scale(1.0, 1.0, zscale): self.assertEqual(city_dict[city.name][2] * zscale, city.scale.z) - def test07_translate(self): - "Testing GeoQuerySet.translate() on Z values." + def test_translate(self): + """ + Testing GeoQuerySet.translate() on Z values. + """ + self._load_city_data() ztranslations = (5.23, 23, -17) for ztrans in ztranslations: for city in City3D.objects.translate(0, 0, ztrans): diff --git a/django/contrib/gis/tests/geoapp/test_regress.py b/django/contrib/gis/tests/geoapp/test_regress.py index fffd7d3cab..0e9c5c44a3 100644 --- a/django/contrib/gis/tests/geoapp/test_regress.py +++ b/django/contrib/gis/tests/geoapp/test_regress.py @@ -1,4 +1,5 @@ -from __future__ import absolute_import +# -*- encoding: utf-8 -*- +from __future__ import absolute_import, unicode_literals from datetime import datetime @@ -26,7 +27,7 @@ class GeoRegressionTests(TestCase): def test_kmz(self): "Testing `render_to_kmz` with non-ASCII data. See #11624." - name = '\xc3\x85land Islands'.decode('iso-8859-1') + name = "Åland Islands" places = [{'name' : name, 'description' : name, 'kml' : '5.0,23.0' diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index cd3cec3074..8f2c22e841 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -11,7 +11,7 @@ from django.contrib.gis.tests.utils import ( no_mysql, no_oracle, no_spatialite, mysql, oracle, postgis, spatialite) from django.test import TestCase -from django.utils import six +from django.utils import six, unittest from .models import Country, City, PennsylvaniaCity, State, Track @@ -295,6 +295,13 @@ class GeoLookupTest(TestCase): self.assertEqual(2, len(qs)) for c in qs: self.assertEqual(True, c.name in cities) + # The left/right lookup tests are known failures on PostGIS 2.0+ + # until the following bug is fixed: + # http://trac.osgeo.org/postgis/ticket/2035 + # TODO: Ensure fixed in 2.0.2, else modify upper bound for version here. + if (2, 0, 0) <= connection.ops.spatial_version <= (2, 0, 1): + test_left_right_lookups = unittest.expectedFailure(test_left_right_lookups) + def test_equals_lookups(self): "Testing the 'same_as' and 'equals' lookup types." pnt = fromstr('POINT (-95.363151 29.763374)', srid=4326) @@ -467,21 +474,21 @@ class GeoQuerySetTest(TestCase): def test_geojson(self): "Testing GeoJSON output from the database using GeoQuerySet.geojson()." - # Only PostGIS 1.3.4+ supports GeoJSON. + # Only PostGIS 1.3.4+ and SpatiaLite 3.0+ support GeoJSON. if not connection.ops.geojson: self.assertRaises(NotImplementedError, Country.objects.all().geojson, field_name='mpoly') return - if connection.ops.spatial_version >= (1, 4, 0): - pueblo_json = '{"type":"Point","coordinates":[-104.609252,38.255001]}' - houston_json = '{"type":"Point","crs":{"type":"name","properties":{"name":"EPSG:4326"}},"coordinates":[-95.363151,29.763374]}' - victoria_json = '{"type":"Point","bbox":[-123.30519600,48.46261100,-123.30519600,48.46261100],"coordinates":[-123.305196,48.462611]}' - chicago_json = '{"type":"Point","crs":{"type":"name","properties":{"name":"EPSG:4326"}},"bbox":[-87.65018,41.85039,-87.65018,41.85039],"coordinates":[-87.65018,41.85039]}' - else: + pueblo_json = '{"type":"Point","coordinates":[-104.609252,38.255001]}' + houston_json = '{"type":"Point","crs":{"type":"name","properties":{"name":"EPSG:4326"}},"coordinates":[-95.363151,29.763374]}' + victoria_json = '{"type":"Point","bbox":[-123.30519600,48.46261100,-123.30519600,48.46261100],"coordinates":[-123.305196,48.462611]}' + chicago_json = '{"type":"Point","crs":{"type":"name","properties":{"name":"EPSG:4326"}},"bbox":[-87.65018,41.85039,-87.65018,41.85039],"coordinates":[-87.65018,41.85039]}' + if postgis and connection.ops.spatial_version < (1, 4, 0): pueblo_json = '{"type":"Point","coordinates":[-104.60925200,38.25500100]}' houston_json = '{"type":"Point","crs":{"type":"EPSG","properties":{"EPSG":4326}},"coordinates":[-95.36315100,29.76337400]}' victoria_json = '{"type":"Point","bbox":[-123.30519600,48.46261100,-123.30519600,48.46261100],"coordinates":[-123.30519600,48.46261100]}' - chicago_json = '{"type":"Point","crs":{"type":"EPSG","properties":{"EPSG":4326}},"bbox":[-87.65018,41.85039,-87.65018,41.85039],"coordinates":[-87.65018,41.85039]}' + elif spatialite: + victoria_json = '{"type":"Point","bbox":[-123.305196,48.462611,-123.305196,48.462611],"coordinates":[-123.305196,48.462611]}' # Precision argument should only be an integer self.assertRaises(TypeError, City.objects.geojson, precision='foo') @@ -520,8 +527,8 @@ class GeoQuerySetTest(TestCase): if oracle: # No precision parameter for Oracle :-/ gml_regex = re.compile(r'^-104.60925\d+,38.25500\d+ ') - elif spatialite: - # Spatialite has extra colon in SrsName + elif spatialite and connection.ops.spatial_version < (3, 0, 0): + # Spatialite before 3.0 has extra colon in SrsName gml_regex = re.compile(r'^-104.609251\d+,38.255001') else: gml_regex = re.compile(r'^-104\.60925\d+,38\.255001') @@ -529,6 +536,11 @@ class GeoQuerySetTest(TestCase): for ptown in [ptown1, ptown2]: self.assertTrue(gml_regex.match(ptown.gml)) + # PostGIS < 1.5 doesn't include dimension im GMLv3 output. + if postgis and connection.ops.spatial_version >= (1, 5, 0): + self.assertIn('', + City.objects.gml(version=3).get(name='Pueblo').gml) + def test_kml(self): "Testing KML output from the database using GeoQuerySet.kml()." # Only PostGIS and Spatialite (>=2.4.0-RC4) support KML serialization @@ -572,12 +584,15 @@ class GeoQuerySetTest(TestCase): def test_num_geom(self): "Testing the `num_geom` GeoQuerySet method." # Both 'countries' only have two geometries. - for c in Country.objects.num_geom(): self.assertEqual(2, c.num_geom) + for c in Country.objects.num_geom(): + self.assertEqual(2, c.num_geom) + for c in City.objects.filter(point__isnull=False).num_geom(): - # Oracle will return 1 for the number of geometries on non-collections, - # whereas PostGIS will return None. - if postgis: - self.assertEqual(None, c.num_geom) + # Oracle and PostGIS 2.0+ will return 1 for the number of + # geometries on non-collections, whereas PostGIS < 2.0.0 + # will return None. + if postgis and connection.ops.spatial_version < (2, 0, 0): + self.assertIsNone(c.num_geom) else: self.assertEqual(1, c.num_geom) diff --git a/django/contrib/gis/tests/inspectapp/tests.py b/django/contrib/gis/tests/inspectapp/tests.py index a3d19784c2..8fc39db58d 100644 --- a/django/contrib/gis/tests/inspectapp/tests.py +++ b/django/contrib/gis/tests/inspectapp/tests.py @@ -4,7 +4,7 @@ import os from django.db import connections from django.test import TestCase -from django.contrib.gis.gdal import Driver +from django.contrib.gis.gdal import Driver, GDAL_VERSION from django.contrib.gis.geometry.test_data import TEST_DATA from django.contrib.gis.utils.ogrinspect import ogrinspect @@ -74,15 +74,33 @@ class OGRInspectTest(TestCase): '', 'class Measurement(models.Model):', ' f_decimal = models.DecimalField(max_digits=0, decimal_places=0)', + ] + + if GDAL_VERSION < (1, 9, 0): + # Prior to GDAL 1.9, the order of the model fields was not + # the same as the columns in the database. + expected.extend([ ' f_int = models.IntegerField()', ' f_datetime = models.DateTimeField()', ' f_time = models.TimeField()', ' f_float = models.FloatField()', ' f_char = models.CharField(max_length=10)', ' f_date = models.DateField()', + ]) + else: + expected.extend([ + ' f_float = models.FloatField()', + ' f_int = models.IntegerField()', + ' f_char = models.CharField(max_length=10)', + ' f_date = models.DateField()', + ' f_datetime = models.DateTimeField()', + ' f_time = models.TimeField()', + ]) + + expected.extend([ ' geom = models.PolygonField()', ' objects = models.GeoManager()', - ] + ]) self.assertEqual(model_def, '\n'.join(expected)) diff --git a/django/contrib/gis/tests/layermap/tests.py b/django/contrib/gis/tests/layermap/tests.py index 85b8d0c8b5..a976954d25 100644 --- a/django/contrib/gis/tests/layermap/tests.py +++ b/django/contrib/gis/tests/layermap/tests.py @@ -1,4 +1,5 @@ -from __future__ import absolute_import +# coding: utf-8 +from __future__ import absolute_import, unicode_literals import os from copy import copy @@ -8,7 +9,10 @@ from django.contrib.gis.gdal import DataSource from django.contrib.gis.tests.utils import mysql from django.contrib.gis.utils.layermapping import (LayerMapping, LayerMapError, InvalidDecimal, MissingForeignKey) +from django.db import router +from django.conf import settings from django.test import TestCase +from django.utils import unittest from .models import ( City, County, CountyFeat, Interstate, ICity1, ICity2, Invalid, State, @@ -26,6 +30,7 @@ NAMES = ['Bexar', 'Galveston', 'Harris', 'Honolulu', 'Pueblo'] NUMS = [1, 2, 1, 19, 1] # Number of polygons for each. STATES = ['Texas', 'Texas', 'Texas', 'Hawaii', 'Colorado'] + class LayerMapTest(TestCase): def test_init(self): @@ -281,3 +286,39 @@ class LayerMapTest(TestCase): lm.save(silent=True, strict=True) self.assertEqual(City.objects.count(), 3) self.assertEqual(City.objects.all().order_by('name_txt')[0].name_txt, "Houston") + + def test_encoded_name(self): + """ Test a layer containing utf-8-encoded name """ + city_shp = os.path.join(shp_path, 'ch-city', 'ch-city.shp') + lm = LayerMapping(City, city_shp, city_mapping) + lm.save(silent=True, strict=True) + self.assertEqual(City.objects.count(), 1) + self.assertEqual(City.objects.all()[0].name, "Zürich") + +class OtherRouter(object): + def db_for_read(self, model, **hints): + return 'other' + + def db_for_write(self, model, **hints): + return self.db_for_read(model, **hints) + + def allow_relation(self, obj1, obj2, **hints): + return None + + def allow_syncdb(self, db, model): + return True + + +class LayerMapRouterTest(TestCase): + + def setUp(self): + self.old_routers = router.routers + router.routers = [OtherRouter()] + + def tearDown(self): + router.routers = self.old_routers + + @unittest.skipUnless(len(settings.DATABASES) > 1, 'multiple databases required') + def test_layermapping_default_db(self): + lm = LayerMapping(City, city_shp, city_mapping) + self.assertEqual(lm.using, 'other') diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py index e898f6de2e..8a793b96c3 100644 --- a/django/contrib/gis/utils/layermapping.py +++ b/django/contrib/gis/utils/layermapping.py @@ -9,7 +9,7 @@ import sys from decimal import Decimal from django.core.exceptions import ObjectDoesNotExist -from django.db import connections, DEFAULT_DB_ALIAS +from django.db import connections, router from django.contrib.gis.db.models import GeometryField from django.contrib.gis.gdal import (CoordTransform, DataSource, OGRException, OGRGeometry, OGRGeomType, SpatialReference) @@ -18,6 +18,8 @@ from django.contrib.gis.gdal.field import ( from django.db import models, transaction from django.contrib.localflavor.us.models import USStateField from django.utils import six +from django.utils.encoding import force_text + # LayerMapping exceptions. class LayerMapError(Exception): pass @@ -65,9 +67,9 @@ class LayerMapping(object): } def __init__(self, model, data, mapping, layer=0, - source_srs=None, encoding=None, + source_srs=None, encoding='utf-8', transaction_mode='commit_on_success', - transform=True, unique=None, using=DEFAULT_DB_ALIAS): + transform=True, unique=None, using=None): """ A LayerMapping object is initialized using the given Model (not an instance), a DataSource (or string path to an OGR-supported data file), and a mapping @@ -76,13 +78,13 @@ class LayerMapping(object): """ # Getting the DataSource and the associated Layer. if isinstance(data, six.string_types): - self.ds = DataSource(data) + self.ds = DataSource(data, encoding=encoding) else: self.ds = data self.layer = self.ds[layer] - self.using = using - self.spatial_backend = connections[using].ops + self.using = using if using is not None else router.db_for_write(model) + self.spatial_backend = connections[self.using].ops # Setting the mapping & model attributes. self.mapping = mapping @@ -330,7 +332,7 @@ class LayerMapping(object): if self.encoding: # The encoding for OGR data sources may be specified here # (e.g., 'cp437' for Census Bureau boundary files). - val = six.text_type(ogr_field.value, self.encoding) + val = force_text(ogr_field.value, self.encoding) else: val = ogr_field.value if model_field.max_length and len(val) > model_field.max_length: diff --git a/django/contrib/gis/utils/ogrinspect.py b/django/contrib/gis/utils/ogrinspect.py index 4266ee4b4c..1c870eaa30 100644 --- a/django/contrib/gis/utils/ogrinspect.py +++ b/django/contrib/gis/utils/ogrinspect.py @@ -2,8 +2,6 @@ This module is for inspecting OGR data sources and generating either models for GeoDjango and/or mapping dictionaries for use with the `LayerMapping` utility. - -Author: Travis Pinney, Dane Springmeyer, & Justin Bronn """ from django.utils.six.moves import zip # Requires GDAL to use. diff --git a/django/contrib/humanize/locale/en/LC_MESSAGES/django.po b/django/contrib/humanize/locale/en/LC_MESSAGES/django.po index 7d64ec0c2d..0e46126adb 100644 --- a/django/contrib/humanize/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/humanize/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-03-23 02:40+0100\n" +"POT-Creation-Date: 2012-10-15 10:57+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -13,276 +13,276 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: tests.py:108 templatetags/humanize.py:164 +#: tests.py:125 templatetags/humanize.py:167 msgid "today" msgstr "" -#: tests.py:108 templatetags/humanize.py:168 +#: tests.py:125 templatetags/humanize.py:171 msgid "yesterday" msgstr "" -#: tests.py:108 templatetags/humanize.py:166 +#: tests.py:125 templatetags/humanize.py:169 msgid "tomorrow" msgstr "" -#: templatetags/humanize.py:24 +#: templatetags/humanize.py:25 msgid "th" msgstr "" -#: templatetags/humanize.py:24 +#: templatetags/humanize.py:25 msgid "st" msgstr "" -#: templatetags/humanize.py:24 +#: templatetags/humanize.py:25 msgid "nd" msgstr "" -#: templatetags/humanize.py:24 +#: templatetags/humanize.py:25 msgid "rd" msgstr "" -#: templatetags/humanize.py:53 +#: templatetags/humanize.py:54 #, python-format msgid "%(value).1f million" msgid_plural "%(value).1f million" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:54 +#: templatetags/humanize.py:55 #, python-format msgid "%(value)s million" msgid_plural "%(value)s million" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:57 +#: templatetags/humanize.py:58 #, python-format msgid "%(value).1f billion" msgid_plural "%(value).1f billion" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:58 +#: templatetags/humanize.py:59 #, python-format msgid "%(value)s billion" msgid_plural "%(value)s billion" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:61 +#: templatetags/humanize.py:62 #, python-format msgid "%(value).1f trillion" msgid_plural "%(value).1f trillion" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:62 +#: templatetags/humanize.py:63 #, python-format msgid "%(value)s trillion" msgid_plural "%(value)s trillion" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:65 +#: templatetags/humanize.py:66 #, python-format msgid "%(value).1f quadrillion" msgid_plural "%(value).1f quadrillion" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:66 +#: templatetags/humanize.py:67 #, python-format msgid "%(value)s quadrillion" msgid_plural "%(value)s quadrillion" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:69 +#: templatetags/humanize.py:70 #, python-format msgid "%(value).1f quintillion" msgid_plural "%(value).1f quintillion" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:70 +#: templatetags/humanize.py:71 #, python-format msgid "%(value)s quintillion" msgid_plural "%(value)s quintillion" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:73 +#: templatetags/humanize.py:74 #, python-format msgid "%(value).1f sextillion" msgid_plural "%(value).1f sextillion" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:74 +#: templatetags/humanize.py:75 #, python-format msgid "%(value)s sextillion" msgid_plural "%(value)s sextillion" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:77 +#: templatetags/humanize.py:78 #, python-format msgid "%(value).1f septillion" msgid_plural "%(value).1f septillion" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:78 +#: templatetags/humanize.py:79 #, python-format msgid "%(value)s septillion" msgid_plural "%(value)s septillion" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:81 +#: templatetags/humanize.py:82 #, python-format msgid "%(value).1f octillion" msgid_plural "%(value).1f octillion" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:82 +#: templatetags/humanize.py:83 #, python-format msgid "%(value)s octillion" msgid_plural "%(value)s octillion" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:85 +#: templatetags/humanize.py:86 #, python-format msgid "%(value).1f nonillion" msgid_plural "%(value).1f nonillion" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:86 +#: templatetags/humanize.py:87 #, python-format msgid "%(value)s nonillion" msgid_plural "%(value)s nonillion" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:89 +#: templatetags/humanize.py:90 #, python-format msgid "%(value).1f decillion" msgid_plural "%(value).1f decillion" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:90 +#: templatetags/humanize.py:91 #, python-format msgid "%(value)s decillion" msgid_plural "%(value)s decillion" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:93 +#: templatetags/humanize.py:94 #, python-format msgid "%(value).1f googol" msgid_plural "%(value).1f googol" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:94 +#: templatetags/humanize.py:95 #, python-format msgid "%(value)s googol" msgid_plural "%(value)s googol" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:143 +#: templatetags/humanize.py:144 msgid "one" msgstr "" -#: templatetags/humanize.py:143 +#: templatetags/humanize.py:144 msgid "two" msgstr "" -#: templatetags/humanize.py:143 +#: templatetags/humanize.py:144 msgid "three" msgstr "" -#: templatetags/humanize.py:143 +#: templatetags/humanize.py:144 msgid "four" msgstr "" -#: templatetags/humanize.py:143 +#: templatetags/humanize.py:144 msgid "five" msgstr "" -#: templatetags/humanize.py:143 +#: templatetags/humanize.py:144 msgid "six" msgstr "" -#: templatetags/humanize.py:143 +#: templatetags/humanize.py:144 msgid "seven" msgstr "" -#: templatetags/humanize.py:143 +#: templatetags/humanize.py:144 msgid "eight" msgstr "" -#: templatetags/humanize.py:143 +#: templatetags/humanize.py:144 msgid "nine" msgstr "" -#: templatetags/humanize.py:185 +#: templatetags/humanize.py:190 #, python-format msgctxt "naturaltime" msgid "%(delta)s ago" msgstr "" -#: templatetags/humanize.py:188 templatetags/humanize.py:210 +#: templatetags/humanize.py:193 templatetags/humanize.py:215 msgid "now" msgstr "" -#: templatetags/humanize.py:191 +#: templatetags/humanize.py:196 #, python-format msgid "a second ago" msgid_plural "%(count)s seconds ago" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:196 +#: templatetags/humanize.py:201 #, python-format msgid "a minute ago" msgid_plural "%(count)s minutes ago" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:201 +#: templatetags/humanize.py:206 #, python-format msgid "an hour ago" msgid_plural "%(count)s hours ago" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:207 +#: templatetags/humanize.py:212 #, python-format msgctxt "naturaltime" msgid "%(delta)s from now" msgstr "" -#: templatetags/humanize.py:213 +#: templatetags/humanize.py:218 #, python-format msgid "a second from now" msgid_plural "%(count)s seconds from now" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:218 +#: templatetags/humanize.py:223 #, python-format msgid "a minute from now" msgid_plural "%(count)s minutes from now" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:223 +#: templatetags/humanize.py:228 #, python-format msgid "an hour from now" msgid_plural "%(count)s hours from now" diff --git a/django/contrib/localflavor/__init__.py b/django/contrib/localflavor/__init__.py index e69de29bb2..785186fd28 100644 --- a/django/contrib/localflavor/__init__.py +++ b/django/contrib/localflavor/__init__.py @@ -0,0 +1,2 @@ +import warnings +warnings.warn("django.contrib.localflavor is deprecated. Use the separate django-localflavor-* packages instead.", DeprecationWarning) diff --git a/django/contrib/localflavor/locale/en/LC_MESSAGES/django.po b/django/contrib/localflavor/locale/en/LC_MESSAGES/django.po index 678bef78d0..ffccefdfbc 100644 --- a/django/contrib/localflavor/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/localflavor/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-03-23 02:41+0100\n" +"POT-Creation-Date: 2012-10-15 10:57+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -33,6 +33,10 @@ msgstr "" msgid "Invalid CUIT." msgstr "" +#: ar/forms.py:84 +msgid "Invalid legal type. Type must be 27, 20, 23 or 30." +msgstr "" + #: at/at_states.py:5 msgid "Burgenland" msgstr "" @@ -194,6 +198,14 @@ msgstr "" msgid "Enter a postal code in the format XXX XXX." msgstr "" +#: ca/forms.py:47 us/forms.py:30 +msgid "Phone numbers must be in XXX-XXX-XXXX format." +msgstr "" + +#: ca/forms.py:69 +msgid "Enter a Canadian province or territory." +msgstr "" + #: ca/forms.py:110 msgid "Enter a valid Canadian Social Insurance number in XXX-XXX-XXX format." msgstr "" @@ -302,6 +314,10 @@ msgstr "" msgid "Zurich" msgstr "" +#: ch/forms.py:37 +msgid "Phone numbers must be in 0XX XXX XX XX format." +msgstr "" + #: ch/forms.py:68 msgid "" "Enter a valid Swiss identity or passport card number in X1234567<0 or " @@ -413,14 +429,10 @@ msgid "Enter a birth number in the format XXXXXX/XXXX or XXXXXXXXXX." msgstr "" #: cz/forms.py:53 -msgid "Invalid optional parameter Gender, valid values are 'f' and 'm'" -msgstr "" - -#: cz/forms.py:54 msgid "Enter a valid birth number." msgstr "" -#: cz/forms.py:115 +#: cz/forms.py:102 msgid "Enter a valid IC number." msgstr "" @@ -499,7 +511,7 @@ msgid "" msgstr "" #: es/es_provinces.py:5 -msgid "Arava" +msgid "Araba" msgstr "" #: es/es_provinces.py:6 @@ -1093,11 +1105,26 @@ msgstr "" msgid "Wales" msgstr "" -#: hr/forms.py:75 -msgid "Enter a valid 13 digit JMBG" +#: hk/forms.py:37 +#, python-format +msgid "Phone number should not start with one of the followings: %s." +msgstr "" + +#: hk/forms.py:40 +#, python-format +msgid "Phone number must be in one of the following formats: %s." +msgstr "" + +#: hk/forms.py:42 +#, python-format +msgid "Phone number should start with one of the followings: %s." msgstr "" #: hr/forms.py:76 +msgid "Enter a valid 13 digit JMBG" +msgstr "" + +#: hr/forms.py:77 msgid "Error in date segment" msgstr "" @@ -1141,87 +1168,87 @@ msgstr "" msgid "Card issue number cannot be zero" msgstr "" -#: hr/hr_choices.py:12 +#: hr/hr_choices.py:14 msgid "Grad Zagreb" msgstr "" -#: hr/hr_choices.py:13 +#: hr/hr_choices.py:15 msgid "Bjelovarsko-bilogorska županija" msgstr "" -#: hr/hr_choices.py:14 +#: hr/hr_choices.py:16 msgid "Brodsko-posavska županija" msgstr "" -#: hr/hr_choices.py:15 +#: hr/hr_choices.py:17 msgid "Dubrovačko-neretvanska županija" msgstr "" -#: hr/hr_choices.py:16 +#: hr/hr_choices.py:18 msgid "Istarska županija" msgstr "" -#: hr/hr_choices.py:17 +#: hr/hr_choices.py:19 msgid "Karlovačka županija" msgstr "" -#: hr/hr_choices.py:18 +#: hr/hr_choices.py:20 msgid "Koprivničko-križevačka županija" msgstr "" -#: hr/hr_choices.py:19 +#: hr/hr_choices.py:21 msgid "Krapinsko-zagorska županija" msgstr "" -#: hr/hr_choices.py:20 +#: hr/hr_choices.py:22 msgid "Ličko-senjska županija" msgstr "" -#: hr/hr_choices.py:21 +#: hr/hr_choices.py:23 msgid "Međimurska županija" msgstr "" -#: hr/hr_choices.py:22 +#: hr/hr_choices.py:24 msgid "Osječko-baranjska županija" msgstr "" -#: hr/hr_choices.py:23 +#: hr/hr_choices.py:25 msgid "Požeško-slavonska županija" msgstr "" -#: hr/hr_choices.py:24 +#: hr/hr_choices.py:26 msgid "Primorsko-goranska županija" msgstr "" -#: hr/hr_choices.py:25 +#: hr/hr_choices.py:27 msgid "Sisačko-moslavačka županija" msgstr "" -#: hr/hr_choices.py:26 +#: hr/hr_choices.py:28 msgid "Splitsko-dalmatinska županija" msgstr "" -#: hr/hr_choices.py:27 +#: hr/hr_choices.py:29 msgid "Šibensko-kninska županija" msgstr "" -#: hr/hr_choices.py:28 +#: hr/hr_choices.py:30 msgid "Varaždinska županija" msgstr "" -#: hr/hr_choices.py:29 +#: hr/hr_choices.py:31 msgid "Virovitičko-podravska županija" msgstr "" -#: hr/hr_choices.py:30 +#: hr/hr_choices.py:32 msgid "Vukovarsko-srijemska županija" msgstr "" -#: hr/hr_choices.py:31 +#: hr/hr_choices.py:33 msgid "Zadarska županija" msgstr "" -#: hr/hr_choices.py:32 +#: hr/hr_choices.py:34 msgid "Zagrebačka županija" msgstr "" @@ -1625,11 +1652,11 @@ msgstr "" msgid "Wicklow" msgstr "" -#: il/forms.py:31 +#: il/forms.py:32 msgid "Enter a postal code in the format XXXXX" msgstr "" -#: il/forms.py:50 +#: il/forms.py:51 msgid "Enter a valid ID number." msgstr "" @@ -1858,7 +1885,7 @@ msgstr "" msgid "Okinawa" msgstr "" -#: kw/forms.py:25 +#: kw/forms.py:27 msgid "Enter a valid Kuwaiti Civil ID number" msgstr "" @@ -1880,339 +1907,339 @@ msgstr "" msgid "The UMCN is not valid." msgstr "" -#: mk/mk_choices.py:8 +#: mk/mk_choices.py:10 msgid "Aerodrom" msgstr "" -#: mk/mk_choices.py:9 +#: mk/mk_choices.py:11 msgid "Aračinovo" msgstr "" -#: mk/mk_choices.py:10 +#: mk/mk_choices.py:12 msgid "Berovo" msgstr "" -#: mk/mk_choices.py:11 +#: mk/mk_choices.py:13 msgid "Bitola" msgstr "" -#: mk/mk_choices.py:12 +#: mk/mk_choices.py:14 msgid "Bogdanci" msgstr "" -#: mk/mk_choices.py:13 +#: mk/mk_choices.py:15 msgid "Bogovinje" msgstr "" -#: mk/mk_choices.py:14 +#: mk/mk_choices.py:16 msgid "Bosilovo" msgstr "" -#: mk/mk_choices.py:15 +#: mk/mk_choices.py:17 msgid "Brvenica" msgstr "" -#: mk/mk_choices.py:16 +#: mk/mk_choices.py:18 msgid "Butel" msgstr "" -#: mk/mk_choices.py:17 +#: mk/mk_choices.py:19 msgid "Valandovo" msgstr "" -#: mk/mk_choices.py:18 +#: mk/mk_choices.py:20 msgid "Vasilevo" msgstr "" -#: mk/mk_choices.py:19 +#: mk/mk_choices.py:21 msgid "Vevčani" msgstr "" -#: mk/mk_choices.py:20 +#: mk/mk_choices.py:22 msgid "Veles" msgstr "" -#: mk/mk_choices.py:21 +#: mk/mk_choices.py:23 msgid "Vinica" msgstr "" -#: mk/mk_choices.py:22 +#: mk/mk_choices.py:24 msgid "Vraneštica" msgstr "" -#: mk/mk_choices.py:23 +#: mk/mk_choices.py:25 msgid "Vrapčište" msgstr "" -#: mk/mk_choices.py:24 +#: mk/mk_choices.py:26 msgid "Gazi Baba" msgstr "" -#: mk/mk_choices.py:25 +#: mk/mk_choices.py:27 msgid "Gevgelija" msgstr "" -#: mk/mk_choices.py:26 +#: mk/mk_choices.py:28 msgid "Gostivar" msgstr "" -#: mk/mk_choices.py:27 +#: mk/mk_choices.py:29 msgid "Gradsko" msgstr "" -#: mk/mk_choices.py:28 +#: mk/mk_choices.py:30 msgid "Debar" msgstr "" -#: mk/mk_choices.py:29 +#: mk/mk_choices.py:31 msgid "Debarca" msgstr "" -#: mk/mk_choices.py:30 +#: mk/mk_choices.py:32 msgid "Delčevo" msgstr "" -#: mk/mk_choices.py:31 +#: mk/mk_choices.py:33 msgid "Demir Kapija" msgstr "" -#: mk/mk_choices.py:32 +#: mk/mk_choices.py:34 msgid "Demir Hisar" msgstr "" -#: mk/mk_choices.py:33 +#: mk/mk_choices.py:35 msgid "Dolneni" msgstr "" -#: mk/mk_choices.py:34 +#: mk/mk_choices.py:36 msgid "Drugovo" msgstr "" -#: mk/mk_choices.py:35 +#: mk/mk_choices.py:37 msgid "Gjorče Petrov" msgstr "" -#: mk/mk_choices.py:36 +#: mk/mk_choices.py:38 msgid "Želino" msgstr "" -#: mk/mk_choices.py:37 +#: mk/mk_choices.py:39 msgid "Zajas" msgstr "" -#: mk/mk_choices.py:38 +#: mk/mk_choices.py:40 msgid "Zelenikovo" msgstr "" -#: mk/mk_choices.py:39 +#: mk/mk_choices.py:41 msgid "Zrnovci" msgstr "" -#: mk/mk_choices.py:40 +#: mk/mk_choices.py:42 msgid "Ilinden" msgstr "" -#: mk/mk_choices.py:41 +#: mk/mk_choices.py:43 msgid "Jegunovce" msgstr "" -#: mk/mk_choices.py:42 +#: mk/mk_choices.py:44 msgid "Kavadarci" msgstr "" -#: mk/mk_choices.py:43 +#: mk/mk_choices.py:45 msgid "Karbinci" msgstr "" -#: mk/mk_choices.py:44 +#: mk/mk_choices.py:46 msgid "Karpoš" msgstr "" -#: mk/mk_choices.py:45 +#: mk/mk_choices.py:47 msgid "Kisela Voda" msgstr "" -#: mk/mk_choices.py:46 +#: mk/mk_choices.py:48 msgid "Kičevo" msgstr "" -#: mk/mk_choices.py:47 +#: mk/mk_choices.py:49 msgid "Konče" msgstr "" -#: mk/mk_choices.py:48 +#: mk/mk_choices.py:50 msgid "Koćani" msgstr "" -#: mk/mk_choices.py:49 +#: mk/mk_choices.py:51 msgid "Kratovo" msgstr "" -#: mk/mk_choices.py:50 +#: mk/mk_choices.py:52 msgid "Kriva Palanka" msgstr "" -#: mk/mk_choices.py:51 +#: mk/mk_choices.py:53 msgid "Krivogaštani" msgstr "" -#: mk/mk_choices.py:52 +#: mk/mk_choices.py:54 msgid "Kruševo" msgstr "" -#: mk/mk_choices.py:53 +#: mk/mk_choices.py:55 msgid "Kumanovo" msgstr "" -#: mk/mk_choices.py:54 +#: mk/mk_choices.py:56 msgid "Lipkovo" msgstr "" -#: mk/mk_choices.py:55 +#: mk/mk_choices.py:57 msgid "Lozovo" msgstr "" -#: mk/mk_choices.py:56 +#: mk/mk_choices.py:58 msgid "Mavrovo i Rostuša" msgstr "" -#: mk/mk_choices.py:57 +#: mk/mk_choices.py:59 msgid "Makedonska Kamenica" msgstr "" -#: mk/mk_choices.py:58 +#: mk/mk_choices.py:60 msgid "Makedonski Brod" msgstr "" -#: mk/mk_choices.py:59 +#: mk/mk_choices.py:61 msgid "Mogila" msgstr "" -#: mk/mk_choices.py:60 +#: mk/mk_choices.py:62 msgid "Negotino" msgstr "" -#: mk/mk_choices.py:61 +#: mk/mk_choices.py:63 msgid "Novaci" msgstr "" -#: mk/mk_choices.py:62 +#: mk/mk_choices.py:64 msgid "Novo Selo" msgstr "" -#: mk/mk_choices.py:63 +#: mk/mk_choices.py:65 msgid "Oslomej" msgstr "" -#: mk/mk_choices.py:64 +#: mk/mk_choices.py:66 msgid "Ohrid" msgstr "" -#: mk/mk_choices.py:65 +#: mk/mk_choices.py:67 msgid "Petrovec" msgstr "" -#: mk/mk_choices.py:66 +#: mk/mk_choices.py:68 msgid "Pehčevo" msgstr "" -#: mk/mk_choices.py:67 +#: mk/mk_choices.py:69 msgid "Plasnica" msgstr "" -#: mk/mk_choices.py:68 +#: mk/mk_choices.py:70 msgid "Prilep" msgstr "" -#: mk/mk_choices.py:69 +#: mk/mk_choices.py:71 msgid "Probištip" msgstr "" -#: mk/mk_choices.py:70 +#: mk/mk_choices.py:72 msgid "Radoviš" msgstr "" -#: mk/mk_choices.py:71 +#: mk/mk_choices.py:73 msgid "Rankovce" msgstr "" -#: mk/mk_choices.py:72 +#: mk/mk_choices.py:74 msgid "Resen" msgstr "" -#: mk/mk_choices.py:73 +#: mk/mk_choices.py:75 msgid "Rosoman" msgstr "" -#: mk/mk_choices.py:74 +#: mk/mk_choices.py:76 msgid "Saraj" msgstr "" -#: mk/mk_choices.py:75 +#: mk/mk_choices.py:77 msgid "Sveti Nikole" msgstr "" -#: mk/mk_choices.py:76 +#: mk/mk_choices.py:78 msgid "Sopište" msgstr "" -#: mk/mk_choices.py:77 +#: mk/mk_choices.py:79 msgid "Star Dojran" msgstr "" -#: mk/mk_choices.py:78 +#: mk/mk_choices.py:80 msgid "Staro Nagoričane" msgstr "" -#: mk/mk_choices.py:79 +#: mk/mk_choices.py:81 msgid "Struga" msgstr "" -#: mk/mk_choices.py:80 +#: mk/mk_choices.py:82 msgid "Strumica" msgstr "" -#: mk/mk_choices.py:81 +#: mk/mk_choices.py:83 msgid "Studeničani" msgstr "" -#: mk/mk_choices.py:82 +#: mk/mk_choices.py:84 msgid "Tearce" msgstr "" -#: mk/mk_choices.py:83 +#: mk/mk_choices.py:85 msgid "Tetovo" msgstr "" -#: mk/mk_choices.py:84 +#: mk/mk_choices.py:86 msgid "Centar" msgstr "" -#: mk/mk_choices.py:85 +#: mk/mk_choices.py:87 msgid "Centar-Župa" msgstr "" -#: mk/mk_choices.py:86 +#: mk/mk_choices.py:88 msgid "Čair" msgstr "" -#: mk/mk_choices.py:87 +#: mk/mk_choices.py:89 msgid "Čaška" msgstr "" -#: mk/mk_choices.py:88 +#: mk/mk_choices.py:90 msgid "Češinovo-Obleševo" msgstr "" -#: mk/mk_choices.py:89 +#: mk/mk_choices.py:91 msgid "Čučer-Sandevo" msgstr "" -#: mk/mk_choices.py:90 +#: mk/mk_choices.py:92 msgid "Štip" msgstr "" -#: mk/mk_choices.py:91 +#: mk/mk_choices.py:93 msgid "Šuto Orizari" msgstr "" @@ -2228,23 +2255,23 @@ msgstr "" msgid "Unique master citizen number (13 digits)" msgstr "" -#: mx/forms.py:65 +#: mx/forms.py:67 msgid "Enter a valid zip code in the format XXXXX." msgstr "" -#: mx/forms.py:108 +#: mx/forms.py:110 msgid "Enter a valid RFC." msgstr "" -#: mx/forms.py:109 +#: mx/forms.py:111 msgid "Invalid checksum for RFC." msgstr "" -#: mx/forms.py:189 +#: mx/forms.py:191 msgid "Enter a valid CURP." msgstr "" -#: mx/forms.py:190 +#: mx/forms.py:192 msgid "Invalid checksum for CURP." msgstr "" @@ -2264,131 +2291,131 @@ msgstr "" msgid "Mexican CURP" msgstr "" -#: mx/mx_states.py:13 +#: mx/mx_states.py:14 msgid "Aguascalientes" msgstr "" -#: mx/mx_states.py:14 +#: mx/mx_states.py:15 msgid "Baja California" msgstr "" -#: mx/mx_states.py:15 +#: mx/mx_states.py:16 msgid "Baja California Sur" msgstr "" -#: mx/mx_states.py:16 +#: mx/mx_states.py:17 msgid "Campeche" msgstr "" -#: mx/mx_states.py:17 +#: mx/mx_states.py:18 msgid "Chihuahua" msgstr "" -#: mx/mx_states.py:18 +#: mx/mx_states.py:19 msgid "Chiapas" msgstr "" -#: mx/mx_states.py:19 +#: mx/mx_states.py:20 msgid "Coahuila" msgstr "" -#: mx/mx_states.py:20 +#: mx/mx_states.py:21 msgid "Colima" msgstr "" -#: mx/mx_states.py:21 +#: mx/mx_states.py:22 msgid "Distrito Federal" msgstr "" -#: mx/mx_states.py:22 +#: mx/mx_states.py:23 msgid "Durango" msgstr "" -#: mx/mx_states.py:23 +#: mx/mx_states.py:24 msgid "Guerrero" msgstr "" -#: mx/mx_states.py:24 +#: mx/mx_states.py:25 msgid "Guanajuato" msgstr "" -#: mx/mx_states.py:25 +#: mx/mx_states.py:26 msgid "Hidalgo" msgstr "" -#: mx/mx_states.py:26 +#: mx/mx_states.py:27 msgid "Jalisco" msgstr "" -#: mx/mx_states.py:27 +#: mx/mx_states.py:28 msgid "Estado de México" msgstr "" -#: mx/mx_states.py:28 +#: mx/mx_states.py:29 msgid "Michoacán" msgstr "" -#: mx/mx_states.py:29 +#: mx/mx_states.py:30 msgid "Morelos" msgstr "" -#: mx/mx_states.py:30 +#: mx/mx_states.py:31 msgid "Nayarit" msgstr "" -#: mx/mx_states.py:31 +#: mx/mx_states.py:32 msgid "Nuevo León" msgstr "" -#: mx/mx_states.py:32 +#: mx/mx_states.py:33 msgid "Oaxaca" msgstr "" -#: mx/mx_states.py:33 +#: mx/mx_states.py:34 msgid "Puebla" msgstr "" -#: mx/mx_states.py:34 +#: mx/mx_states.py:35 msgid "Querétaro" msgstr "" -#: mx/mx_states.py:35 +#: mx/mx_states.py:36 msgid "Quintana Roo" msgstr "" -#: mx/mx_states.py:36 +#: mx/mx_states.py:37 msgid "Sinaloa" msgstr "" -#: mx/mx_states.py:37 +#: mx/mx_states.py:38 msgid "San Luis Potosí" msgstr "" -#: mx/mx_states.py:38 +#: mx/mx_states.py:39 msgid "Sonora" msgstr "" -#: mx/mx_states.py:39 +#: mx/mx_states.py:40 msgid "Tabasco" msgstr "" -#: mx/mx_states.py:40 +#: mx/mx_states.py:41 msgid "Tamaulipas" msgstr "" -#: mx/mx_states.py:41 +#: mx/mx_states.py:42 msgid "Tlaxcala" msgstr "" -#: mx/mx_states.py:42 +#: mx/mx_states.py:43 msgid "Veracruz" msgstr "" -#: mx/mx_states.py:43 +#: mx/mx_states.py:44 msgid "Yucatán" msgstr "" -#: mx/mx_states.py:44 +#: mx/mx_states.py:45 msgid "Zacatecas" msgstr "" @@ -2558,31 +2585,31 @@ msgstr "" msgid "West Pomerania" msgstr "" -#: pt/forms.py:17 +#: pt/forms.py:19 msgid "Enter a zip code in the format XXXX-XXX." msgstr "" -#: pt/forms.py:37 +#: pt/forms.py:39 msgid "Phone numbers must have 9 digits, or start by + or 00." msgstr "" -#: ro/forms.py:20 +#: ro/forms.py:22 msgid "Enter a valid CIF." msgstr "" -#: ro/forms.py:57 +#: ro/forms.py:59 msgid "Enter a valid CNP." msgstr "" -#: ro/forms.py:142 +#: ro/forms.py:143 msgid "Enter a valid IBAN in ROXX-XXXX-XXXX-XXXX-XXXX-XXXX format" msgstr "" -#: ro/forms.py:174 +#: ro/forms.py:175 msgid "Phone numbers must be in XXXX-XXXXXX format." msgstr "" -#: ro/forms.py:199 +#: ro/forms.py:200 msgid "Enter a valid postal code in the format XXXXXX" msgstr "" @@ -2978,87 +3005,87 @@ msgstr "" msgid "Enter a Swedish postal code in the format XXXXX." msgstr "" -#: se/se_counties.py:15 +#: se/se_counties.py:16 msgid "Stockholm" msgstr "" -#: se/se_counties.py:16 +#: se/se_counties.py:17 msgid "Västerbotten" msgstr "" -#: se/se_counties.py:17 +#: se/se_counties.py:18 msgid "Norrbotten" msgstr "" -#: se/se_counties.py:18 +#: se/se_counties.py:19 msgid "Uppsala" msgstr "" -#: se/se_counties.py:19 +#: se/se_counties.py:20 msgid "Södermanland" msgstr "" -#: se/se_counties.py:20 +#: se/se_counties.py:21 msgid "Östergötland" msgstr "" -#: se/se_counties.py:21 +#: se/se_counties.py:22 msgid "Jönköping" msgstr "" -#: se/se_counties.py:22 +#: se/se_counties.py:23 msgid "Kronoberg" msgstr "" -#: se/se_counties.py:23 +#: se/se_counties.py:24 msgid "Kalmar" msgstr "" -#: se/se_counties.py:24 +#: se/se_counties.py:25 msgid "Gotland" msgstr "" -#: se/se_counties.py:25 +#: se/se_counties.py:26 msgid "Blekinge" msgstr "" -#: se/se_counties.py:26 +#: se/se_counties.py:27 msgid "Skåne" msgstr "" -#: se/se_counties.py:27 +#: se/se_counties.py:28 msgid "Halland" msgstr "" -#: se/se_counties.py:28 +#: se/se_counties.py:29 msgid "Västra Götaland" msgstr "" -#: se/se_counties.py:29 +#: se/se_counties.py:30 msgid "Värmland" msgstr "" -#: se/se_counties.py:30 +#: se/se_counties.py:31 msgid "Örebro" msgstr "" -#: se/se_counties.py:31 +#: se/se_counties.py:32 msgid "Västmanland" msgstr "" -#: se/se_counties.py:32 +#: se/se_counties.py:33 msgid "Dalarna" msgstr "" -#: se/se_counties.py:33 +#: se/se_counties.py:34 msgid "Gävleborg" msgstr "" -#: se/se_counties.py:34 +#: se/se_counties.py:35 msgid "Västernorrland" msgstr "" -#: se/se_counties.py:35 +#: se/se_counties.py:36 msgid "Jämtland" msgstr "" @@ -3446,10 +3473,6 @@ msgstr "" msgid "Enter a zip code in the format XXXXX or XXXXX-XXXX." msgstr "" -#: us/forms.py:30 -msgid "Phone numbers must be in XXX-XXX-XXXX format." -msgstr "" - #: us/forms.py:59 msgid "Enter a valid U.S. Social Security number in XXX-XX-XXXX format." msgstr "" @@ -3478,11 +3501,11 @@ msgstr "" msgid "Enter a valid CI number." msgstr "" -#: za/forms.py:21 +#: za/forms.py:22 msgid "Enter a valid South African ID number" msgstr "" -#: za/forms.py:55 +#: za/forms.py:56 msgid "Enter a valid South African postal code" msgstr "" diff --git a/django/contrib/messages/locale/en/LC_MESSAGES/django.po b/django/contrib/messages/locale/en/LC_MESSAGES/django.po index 7789b497fa..6f16dd0411 100644 --- a/django/contrib/messages/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/messages/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-03-23 02:43+0100\n" +"POT-Creation-Date: 2012-10-15 10:57+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -13,6 +13,6 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: tests/base.py:100 +#: tests/base.py:101 msgid "lazy message" msgstr "" diff --git a/django/contrib/redirects/locale/en/LC_MESSAGES/django.po b/django/contrib/redirects/locale/en/LC_MESSAGES/django.po index 5209952d17..885b5fdb97 100644 --- a/django/contrib/redirects/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/redirects/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-03-23 02:43+0100\n" +"POT-Creation-Date: 2012-10-15 10:57+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -13,30 +13,30 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: models.py:7 +#: models.py:9 msgid "redirect from" msgstr "" -#: models.py:8 +#: models.py:10 msgid "" "This should be an absolute path, excluding the domain name. Example: '/" "events/search/'." msgstr "" -#: models.py:9 +#: models.py:11 msgid "redirect to" msgstr "" -#: models.py:10 +#: models.py:12 msgid "" "This can be either an absolute path (as above) or a full URL starting with " "'http://'." msgstr "" -#: models.py:13 +#: models.py:15 msgid "redirect" msgstr "" -#: models.py:14 +#: models.py:16 msgid "redirects" msgstr "" diff --git a/django/contrib/redirects/middleware.py b/django/contrib/redirects/middleware.py index 8998c2ce3e..927220d44d 100644 --- a/django/contrib/redirects/middleware.py +++ b/django/contrib/redirects/middleware.py @@ -1,4 +1,5 @@ from django.contrib.redirects.models import Redirect +from django.contrib.sites.models import get_current_site from django import http from django.conf import settings @@ -7,14 +8,15 @@ class RedirectFallbackMiddleware(object): if response.status_code != 404: return response # No need to check for a redirect for non-404 responses. path = request.get_full_path() + current_site = get_current_site(request) try: - r = Redirect.objects.get(site__id__exact=settings.SITE_ID, old_path=path) + r = Redirect.objects.get(site__id__exact=current_site.id, old_path=path) except Redirect.DoesNotExist: r = None if r is None and settings.APPEND_SLASH: # Try removing the trailing slash. try: - r = Redirect.objects.get(site__id__exact=settings.SITE_ID, + r = Redirect.objects.get(site__id__exact=current_site.id, old_path=path[:path.rfind('/')]+path[path.rfind('/')+1:]) except Redirect.DoesNotExist: pass diff --git a/django/contrib/sessions/locale/en/LC_MESSAGES/django.po b/django/contrib/sessions/locale/en/LC_MESSAGES/django.po index b0c82d86a5..5147a71fe5 100644 --- a/django/contrib/sessions/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/sessions/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-03-23 02:43+0100\n" +"POT-Creation-Date: 2012-10-15 10:57+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" diff --git a/django/contrib/sessions/tests.py b/django/contrib/sessions/tests.py index 9aa602f416..fc2d8753d7 100644 --- a/django/contrib/sessions/tests.py +++ b/django/contrib/sessions/tests.py @@ -12,6 +12,7 @@ from django.contrib.sessions.backends.file import SessionStore as FileSession from django.contrib.sessions.backends.signed_cookies import SessionStore as CookieSession from django.contrib.sessions.models import Session from django.contrib.sessions.middleware import SessionMiddleware +from django.core.cache import DEFAULT_CACHE_ALIAS from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation from django.http import HttpResponse from django.test import TestCase, RequestFactory @@ -133,6 +134,9 @@ class SessionTestsMixin(object): self.assertTrue(self.session.modified) def test_save(self): + if (hasattr(self.session, '_cache') and + 'DummyCache' in settings.CACHES[DEFAULT_CACHE_ALIAS]['BACKEND']): + raise unittest.SkipTest("Session saving tests require a real cache backend") self.session.save() self.assertTrue(self.session.exists(self.session.session_key)) @@ -296,6 +300,8 @@ class CacheDBSessionTests(SessionTestsMixin, TestCase): backend = CacheDBSession + @unittest.skipIf('DummyCache' in settings.CACHES[DEFAULT_CACHE_ALIAS]['BACKEND'], + "Session saving tests require a real cache backend") def test_exists_searches_cache_first(self): self.session.save() with self.assertNumQueries(0): diff --git a/django/contrib/sitemaps/tests/base.py b/django/contrib/sitemaps/tests/base.py index 224277e2a0..bbe8229aae 100644 --- a/django/contrib/sitemaps/tests/base.py +++ b/django/contrib/sitemaps/tests/base.py @@ -1,5 +1,6 @@ from django.contrib.auth.models import User from django.contrib.sites.models import Site +from django.core.cache import cache from django.test import TestCase @@ -11,6 +12,7 @@ class SitemapTestsBase(TestCase): def setUp(self): self.base_url = '%s://%s' % (self.protocol, self.domain) self.old_Site_meta_installed = Site._meta.installed + cache.clear() # Create a user that will double as sitemap content User.objects.create_user('testuser', 'test@example.com', 's3krit') diff --git a/django/contrib/sitemaps/tests/generic.py b/django/contrib/sitemaps/tests/generic.py index e0b0a827a6..ae054c95c2 100644 --- a/django/contrib/sitemaps/tests/generic.py +++ b/django/contrib/sitemaps/tests/generic.py @@ -19,4 +19,4 @@ class GenericViewsSitemapTests(SitemapTestsBase): %s """ % expected - self.assertEqual(response.content, expected_content.encode('utf-8')) + self.assertXMLEqual(response.content.decode('utf-8'), expected_content) diff --git a/django/contrib/sitemaps/tests/http.py b/django/contrib/sitemaps/tests/http.py index 8da971876f..99042fef03 100644 --- a/django/contrib/sitemaps/tests/http.py +++ b/django/contrib/sitemaps/tests/http.py @@ -26,7 +26,7 @@ class HTTPSitemapTests(SitemapTestsBase): %s/simple/sitemap-simple.xml """ % self.base_url - self.assertEqual(response.content, expected_content.encode('utf-8')) + self.assertXMLEqual(response.content.decode('utf-8'), expected_content) @override_settings( TEMPLATE_DIRS=(os.path.join(os.path.dirname(__file__), 'templates'),) @@ -40,7 +40,7 @@ class HTTPSitemapTests(SitemapTestsBase): %s/simple/sitemap-simple.xml """ % self.base_url - self.assertEqual(response.content, expected_content.encode('utf-8')) + self.assertXMLEqual(response.content.decode('utf-8'), expected_content) def test_simple_sitemap_section(self): @@ -51,7 +51,7 @@ class HTTPSitemapTests(SitemapTestsBase): %s/location/%snever0.5 """ % (self.base_url, date.today()) - self.assertEqual(response.content, expected_content.encode('utf-8')) + self.assertXMLEqual(response.content.decode('utf-8'), expected_content) def test_simple_sitemap(self): "A simple sitemap can be rendered" @@ -61,7 +61,7 @@ class HTTPSitemapTests(SitemapTestsBase): %s/location/%snever0.5 """ % (self.base_url, date.today()) - self.assertEqual(response.content, expected_content.encode('utf-8')) + self.assertXMLEqual(response.content.decode('utf-8'), expected_content) @override_settings( TEMPLATE_DIRS=(os.path.join(os.path.dirname(__file__), 'templates'),) @@ -75,7 +75,7 @@ class HTTPSitemapTests(SitemapTestsBase): %s/location/%snever0.5 """ % (self.base_url, date.today()) - self.assertEqual(response.content, expected_content.encode('utf-8')) + self.assertXMLEqual(response.content.decode('utf-8'), expected_content) @skipUnless(settings.USE_I18N, "Internationalization is not enabled") @override_settings(USE_L10N=True) @@ -101,7 +101,7 @@ class HTTPSitemapTests(SitemapTestsBase): http://testserver/location/%snever0.5 """ % date.today() - self.assertEqual(response.content, expected_content.encode('utf-8')) + self.assertXMLEqual(response.content.decode('utf-8'), expected_content) @skipUnless("django.contrib.sites" in settings.INSTALLED_APPS, "django.contrib.sites app not installed.") @@ -143,4 +143,4 @@ class HTTPSitemapTests(SitemapTestsBase): %s/cached/sitemap-simple.xml """ % self.base_url - self.assertEqual(response.content, expected_content.encode('utf-8')) + self.assertXMLEqual(response.content.decode('utf-8'), expected_content) diff --git a/django/contrib/sitemaps/tests/https.py b/django/contrib/sitemaps/tests/https.py index 26241eb30b..baad02ac07 100644 --- a/django/contrib/sitemaps/tests/https.py +++ b/django/contrib/sitemaps/tests/https.py @@ -18,7 +18,7 @@ class HTTPSSitemapTests(SitemapTestsBase): %s/secure/sitemap-simple.xml """ % self.base_url - self.assertEqual(response.content, expected_content.encode('utf-8')) + self.assertXMLEqual(response.content.decode('utf-8'), expected_content) def test_secure_sitemap_section(self): "A secure sitemap section can be rendered" @@ -28,7 +28,7 @@ class HTTPSSitemapTests(SitemapTestsBase): %s/location/%snever0.5 """ % (self.base_url, date.today()) - self.assertEqual(response.content, expected_content.encode('utf-8')) + self.assertXMLEqual(response.content.decode('utf-8'), expected_content) @override_settings(SECURE_PROXY_SSL_HEADER=False) @@ -43,7 +43,7 @@ class HTTPSDetectionSitemapTests(SitemapTestsBase): %s/simple/sitemap-simple.xml """ % self.base_url.replace('http://', 'https://') - self.assertEqual(response.content, expected_content.encode('utf-8')) + self.assertXMLEqual(response.content.decode('utf-8'), expected_content) def test_sitemap_section_with_https_request(self): "A sitemap section requested in HTTPS is rendered with HTTPS links" @@ -53,4 +53,4 @@ class HTTPSDetectionSitemapTests(SitemapTestsBase): %s/location/%snever0.5 """ % (self.base_url.replace('http://', 'https://'), date.today()) - self.assertEqual(response.content, expected_content.encode('utf-8')) + self.assertXMLEqual(response.content.decode('utf-8'), expected_content) diff --git a/django/contrib/sites/locale/en/LC_MESSAGES/django.po b/django/contrib/sites/locale/en/LC_MESSAGES/django.po index 5d70211d57..410b61b9c7 100644 --- a/django/contrib/sites/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/sites/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-03-23 02:43+0100\n" +"POT-Creation-Date: 2012-10-15 10:57+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -13,18 +13,18 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: models.py:37 +#: models.py:39 msgid "domain name" msgstr "" -#: models.py:38 +#: models.py:40 msgid "display name" msgstr "" -#: models.py:43 +#: models.py:45 msgid "site" msgstr "" -#: models.py:44 +#: models.py:46 msgid "sites" msgstr "" diff --git a/django/contrib/syndication/views.py b/django/contrib/syndication/views.py index 4815ce5567..996b7dfb40 100644 --- a/django/contrib/syndication/views.py +++ b/django/contrib/syndication/views.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +from calendar import timegm + from django.conf import settings from django.contrib.sites.models import get_current_site from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist @@ -8,6 +10,7 @@ from django.template import loader, TemplateDoesNotExist, RequestContext from django.utils import feedgenerator, tzinfo from django.utils.encoding import force_text, iri_to_uri, smart_text from django.utils.html import escape +from django.utils.http import http_date from django.utils.timezone import is_naive @@ -22,6 +25,7 @@ def add_domain(domain, url, secure=False): url = iri_to_uri('%s://%s%s' % (protocol, domain, url)) return url + class FeedDoesNotExist(ObjectDoesNotExist): pass @@ -38,6 +42,11 @@ class Feed(object): raise Http404('Feed object does not exist.') feedgen = self.get_feed(obj, request) response = HttpResponse(content_type=feedgen.mime_type) + if hasattr(self, 'item_pubdate'): + # if item_pubdate is defined for the feed, set header so as + # ConditionalGetMiddleware is able to send 304 NOT MODIFIED + response['Last-Modified'] = http_date( + timegm(feedgen.latest_post_date().utctimetuple())) feedgen.write(response, 'utf-8') return response diff --git a/django/core/exceptions.py b/django/core/exceptions.py index f0f14cffda..233af40f88 100644 --- a/django/core/exceptions.py +++ b/django/core/exceptions.py @@ -3,42 +3,54 @@ Global Django exception and warning classes. """ from functools import reduce + class DjangoRuntimeWarning(RuntimeWarning): pass + class ObjectDoesNotExist(Exception): "The requested object does not exist" silent_variable_failure = True + class MultipleObjectsReturned(Exception): "The query returned multiple objects when only one was expected." pass + class SuspiciousOperation(Exception): "The user did something suspicious" pass + class PermissionDenied(Exception): "The user did not have permission to do that" pass + class ViewDoesNotExist(Exception): "The requested view does not exist" pass + class MiddlewareNotUsed(Exception): "This middleware is not used in this server configuration" pass + class ImproperlyConfigured(Exception): "Django is somehow improperly configured" pass + class FieldError(Exception): """Some kind of problem with a model field.""" pass + NON_FIELD_ERRORS = '__all__' + + class ValidationError(Exception): """An error while validating data.""" def __init__(self, message, code=None, params=None): @@ -85,4 +97,3 @@ class ValidationError(Exception): else: error_dict[NON_FIELD_ERRORS] = self.messages return error_dict - diff --git a/django/core/files/storage.py b/django/core/files/storage.py index 0b300cd31e..650373f0c3 100644 --- a/django/core/files/storage.py +++ b/django/core/files/storage.py @@ -192,7 +192,10 @@ class FileSystemStorage(Storage): else: # This fun binary flag incantation makes os.open throw an # OSError if the file already exists before we open it. - fd = os.open(full_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, 'O_BINARY', 0)) + flags = (os.O_WRONLY | os.O_CREAT | os.O_EXCL | + getattr(os, 'O_BINARY', 0)) + # The current umask value is masked out by os.open! + fd = os.open(full_path, flags, 0o666) try: locks.lock(fd, locks.LOCK_EX) _file = None diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index 791382bac0..23572465cf 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import logging import sys import types @@ -7,10 +8,9 @@ from django import http from django.core import signals from django.utils.encoding import force_text from django.utils.importlib import import_module -from django.utils.log import getLogger from django.utils import six -logger = getLogger('django.request') +logger = logging.getLogger('django.request') class BaseHandler(object): @@ -95,14 +95,15 @@ class BaseHandler(object): break if response is None: - if hasattr(request, "urlconf"): + if hasattr(request, 'urlconf'): # Reset url resolver with a custom urlconf. urlconf = request.urlconf urlresolvers.set_urlconf(urlconf) resolver = urlresolvers.RegexURLResolver(r'^/', urlconf) - callback, callback_args, callback_kwargs = resolver.resolve( - request.path_info) + resolver_match = resolver.resolve(request.path_info) + callback, callback_args, callback_kwargs = resolver_match + request.resolver_match = resolver_match # Apply view middleware for middleware_method in self._view_middleware: diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index 7a32a3dac7..45cb2268ed 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import logging import sys from io import BytesIO from threading import Lock @@ -10,9 +11,8 @@ from django.core.handlers import base from django.core.urlresolvers import set_script_prefix from django.utils import datastructures from django.utils.encoding import force_str, force_text, iri_to_uri -from django.utils.log import getLogger -logger = getLogger('django.request') +logger = logging.getLogger('django.request') # See http://www.iana.org/assignments/http-status-codes @@ -152,11 +152,6 @@ class WSGIRequest(http.HttpRequest): self._stream = LimitedStream(self.environ['wsgi.input'], content_length) self._read_started = False - def get_full_path(self): - # RFC 3986 requires query string arguments to be in the ASCII range. - # Rather than crash if this doesn't happen, we encode defensively. - return '%s%s' % (self.path, self.environ.get('QUERY_STRING', '') and ('?' + iri_to_uri(self.environ.get('QUERY_STRING', ''))) or '') - def _is_secure(self): return 'wsgi.url_scheme' in self.environ and self.environ['wsgi.url_scheme'] == 'https' diff --git a/django/core/mail/backends/locmem.py b/django/core/mail/backends/locmem.py index 642bfc49fb..6826d09ee5 100644 --- a/django/core/mail/backends/locmem.py +++ b/django/core/mail/backends/locmem.py @@ -20,5 +20,7 @@ class EmailBackend(BaseEmailBackend): def send_messages(self, messages): """Redirect messages to the dummy outbox""" + for message in messages: # .message() triggers header validation + message.message() mail.outbox.extend(messages) return len(messages) diff --git a/django/core/mail/message.py b/django/core/mail/message.py index db9023a0bb..98ab3c9075 100644 --- a/django/core/mail/message.py +++ b/django/core/mail/message.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import mimetypes import os import random +import sys import time from email import charset as Charset, encoders as Encoders from email.generator import Generator @@ -138,6 +139,9 @@ class SafeMIMEText(MIMEText): """ fp = six.StringIO() g = Generator(fp, mangle_from_ = False) + if sys.version_info < (2, 6, 6) and isinstance(self._payload, six.text_type): + # Workaround for http://bugs.python.org/issue1368247 + self._payload = self._payload.encode(self._charset.output_charset) g.flatten(self, unixfrom=unixfrom) return fp.getvalue() diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index b40570efc9..c61ab2b663 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -103,10 +103,12 @@ def get_commands(): _commands = dict([(name, 'django.core') for name in find_commands(__path__[0])]) # Find the installed apps + from django.conf import settings try: - from django.conf import settings apps = settings.INSTALLED_APPS - except (AttributeError, ImproperlyConfigured): + except ImproperlyConfigured: + # Still useful for commands that do not require functional settings, + # like startproject or help apps = [] # Find and load the management module for each installed app. diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index 30cf740cdf..32ae8abf5a 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -23,6 +23,7 @@ try: except ImportError: has_bz2 = False + class Command(BaseCommand): help = 'Installs the named fixture(s) in the database.' args = "fixture [fixture ...]" @@ -31,9 +32,14 @@ class Command(BaseCommand): make_option('--database', action='store', dest='database', default=DEFAULT_DB_ALIAS, help='Nominates a specific database to load ' 'fixtures into. Defaults to the "default" database.'), + make_option('--ignorenonexistent', '-i', action='store_true', dest='ignore', + default=False, help='Ignores entries in the serialized data for fields' + ' that do not currently exist on the model.'), ) def handle(self, *fixture_labels, **options): + + ignore = options.get('ignore') using = options.get('database') connection = connections[using] @@ -175,7 +181,7 @@ class Command(BaseCommand): self.stdout.write("Installing %s fixture '%s' from %s." % \ (format, fixture_name, humanize(fixture_dir))) - objects = serializers.deserialize(format, fixture, using=using) + objects = serializers.deserialize(format, fixture, using=using, ignorenonexistent=ignore) for obj in objects: objects_in_fixture += 1 diff --git a/django/core/management/commands/runserver.py b/django/core/management/commands/runserver.py index 9c24701d2e..391e0b440a 100644 --- a/django/core/management/commands/runserver.py +++ b/django/core/management/commands/runserver.py @@ -123,7 +123,7 @@ class Command(BaseCommand): error_text = ERRORS[e.args[0].args[0]] except (AttributeError, KeyError): error_text = str(e) - sys.stderr.write("Error: %s" % error_text) + self.stderr.write("Error: %s" % error_text) # Need to use an OS exit because sys.exit doesn't work in a thread os._exit(1) except KeyboardInterrupt: diff --git a/django/core/management/commands/sqlall.py b/django/core/management/commands/sqlall.py index 6d0735a6f9..0e2c05ba82 100644 --- a/django/core/management/commands/sqlall.py +++ b/django/core/management/commands/sqlall.py @@ -6,6 +6,7 @@ from django.core.management.base import AppCommand from django.core.management.sql import sql_all from django.db import connections, DEFAULT_DB_ALIAS + class Command(AppCommand): help = "Prints the CREATE TABLE, custom SQL and CREATE INDEX SQL statements for the given model module name(s)." diff --git a/django/core/management/commands/syncdb.py b/django/core/management/commands/syncdb.py index 56e94d9e80..d6f6fc6b76 100644 --- a/django/core/management/commands/syncdb.py +++ b/django/core/management/commands/syncdb.py @@ -68,6 +68,7 @@ class Command(NoArgsCommand): if router.allow_syncdb(db, m)]) for app in models.get_apps() ] + def model_installed(model): opts = model._meta converter = connection.introspection.table_name_converter @@ -103,7 +104,6 @@ class Command(NoArgsCommand): cursor.execute(statement) tables.append(connection.introspection.table_name_converter(model._meta.db_table)) - transaction.commit_unless_managed(using=db) # Send the post_syncdb signal, so individual apps can do whatever they need diff --git a/django/core/management/commands/validate.py b/django/core/management/commands/validate.py index 760d41c5bf..0dec3ea8b9 100644 --- a/django/core/management/commands/validate.py +++ b/django/core/management/commands/validate.py @@ -1,5 +1,6 @@ from django.core.management.base import NoArgsCommand + class Command(NoArgsCommand): help = "Validates all installed models." diff --git a/django/core/management/sql.py b/django/core/management/sql.py index b02a548314..78cd17a23a 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -9,6 +9,7 @@ from django.core.management.base import CommandError from django.db import models from django.db.models import get_models + def sql_create(app, style, connection): "Returns a list of the CREATE TABLE SQL statements for the given app." @@ -16,9 +17,8 @@ def sql_create(app, style, connection): # This must be the "dummy" database backend, which means the user # hasn't set ENGINE for the database. raise CommandError("Django doesn't know which syntax to use for your SQL statements,\n" + - "because you haven't specified the ENGINE setting for the database.\n" + - "Edit your settings file and change DATBASES['default']['ENGINE'] to something like\n" + - "'django.db.backends.postgresql' or 'django.db.backends.mysql'.") + "because you haven't properly specified the ENGINE setting for the database.\n" + + "see: https://docs.djangoproject.com/en/dev/ref/settings/#databases") # Get installed models, so we generate REFERENCES right. # We trim models from the current app so that the sqlreset command does not @@ -55,6 +55,7 @@ def sql_create(app, style, connection): return final_output + def sql_delete(app, style, connection): "Returns a list of the DROP TABLE SQL statements for the given app." @@ -83,7 +84,7 @@ def sql_delete(app, style, connection): opts = model._meta for f in opts.local_fields: if f.rel and f.rel.to not in to_delete: - references_to_delete.setdefault(f.rel.to, []).append( (model, f) ) + references_to_delete.setdefault(f.rel.to, []).append((model, f)) to_delete.add(model) @@ -97,7 +98,8 @@ def sql_delete(app, style, connection): cursor.close() connection.close() - return output[::-1] # Reverse it, to deal with table dependencies. + return output[::-1] # Reverse it, to deal with table dependencies. + def sql_flush(style, connection, only_django=False, reset_sequences=True): """ @@ -114,6 +116,7 @@ def sql_flush(style, connection, only_django=False, reset_sequences=True): statements = connection.ops.sql_flush(style, tables, seqs) return statements + def sql_custom(app, style, connection): "Returns a list of the custom table modifying SQL statements for the given app." output = [] @@ -125,6 +128,7 @@ def sql_custom(app, style, connection): return output + def sql_indexes(app, style, connection): "Returns a list of the CREATE INDEX SQL statements for all models in the given app." output = [] @@ -132,10 +136,12 @@ def sql_indexes(app, style, connection): output.extend(connection.creation.sql_indexes_for_model(model, style)) return output + def sql_all(app, style, connection): "Returns a list of CREATE TABLE SQL, initial-data inserts, and CREATE INDEX SQL for the given module." return sql_create(app, style, connection) + sql_custom(app, style, connection) + sql_indexes(app, style, connection) + def _split_statements(content): comment_re = re.compile(r"^((?:'[^']*'|[^'])*?)--.*$") statements = [] @@ -150,6 +156,7 @@ def _split_statements(content): statement = "" return statements + def custom_sql_for_model(model, style, connection): opts = model._meta app_dir = os.path.normpath(os.path.join(os.path.dirname(models.get_app(model._meta.app_label).__file__), 'sql')) diff --git a/django/core/management/templates.py b/django/core/management/templates.py index aa65593e9c..d34a0deb7e 100644 --- a/django/core/management/templates.py +++ b/django/core/management/templates.py @@ -138,7 +138,7 @@ class TemplateCommand(BaseCommand): os.mkdir(target_dir) for dirname in dirs[:]: - if dirname.startswith('.'): + if dirname.startswith('.') or dirname == '__pycache__': dirs.remove(dirname) for filename in files: diff --git a/django/core/management/validation.py b/django/core/management/validation.py index 6cd66f3a6a..957a712b72 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -1,10 +1,12 @@ import sys +from django.conf import settings from django.core.management.color import color_style from django.utils.encoding import force_str from django.utils.itercompat import is_iterable from django.utils import six + class ModelErrorCollection: def __init__(self, outfile=sys.stdout): self.errors = [] @@ -15,13 +17,13 @@ class ModelErrorCollection: self.errors.append((context, error)) self.outfile.write(self.style.ERROR(force_str("%s: %s\n" % (context, error)))) + def get_validation_errors(outfile, app=None): """ Validates all models that are part of the specified app. If no app name is provided, validates all models of all installed apps. Writes errors, if any, to outfile. Returns number of errors. """ - from django.conf import settings from django.db import models, connection from django.db.models.loading import get_app_errors from django.db.models.fields.related import RelatedObject @@ -35,7 +37,25 @@ def get_validation_errors(outfile, app=None): for cls in models.get_models(app): opts = cls._meta - # Do field-specific validation. + # Check swappable attribute. + if opts.swapped: + try: + app_label, model_name = opts.swapped.split('.') + except ValueError: + e.add(opts, "%s is not of the form 'app_label.app_name'." % opts.swappable) + continue + if not models.get_model(app_label, model_name): + e.add(opts, "Model has been swapped out for '%s' which has not been installed or is abstract." % opts.swapped) + # No need to perform any other validation checks on a swapped model. + continue + + # This is the current User model. Check known validation problems with User models + if settings.AUTH_USER_MODEL == '%s.%s' % (opts.app_label, opts.object_name): + # Check that the USERNAME FIELD isn't included in REQUIRED_FIELDS. + if cls.USERNAME_FIELD in cls.REQUIRED_FIELDS: + e.add(opts, 'The field named as the USERNAME_FIELD should not be included in REQUIRED_FIELDS on a swappable User model.') + + # Model isn't swapped; do field-specific validation. for f in opts.local_fields: if f.name == 'id' and not f.primary_key and opts.pk.name == 'id': e.add(opts, '"%s": You can\'t use "id" as a field name, because each model automatically gets an "id" field if none of the fields have primary_key=True. You need to either remove/rename your "id" field or add primary_key=True to a field.' % f.name) @@ -56,7 +76,7 @@ def get_validation_errors(outfile, app=None): e.add(opts, '"%s": CharFields require a "max_length" attribute that is a positive integer.' % f.name) if isinstance(f, models.DecimalField): decimalp_ok, mdigits_ok = False, False - decimalp_msg ='"%s": DecimalFields require a "decimal_places" attribute that is a non-negative integer.' + decimalp_msg = '"%s": DecimalFields require a "decimal_places" attribute that is a non-negative integer.' try: decimal_places = int(f.decimal_places) if decimal_places < 0: @@ -123,6 +143,10 @@ def get_validation_errors(outfile, app=None): if isinstance(f.rel.to, six.string_types): continue + # Make sure the model we're related hasn't been swapped out + if f.rel.to._meta.swapped: + e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable)) + # Make sure the related field specified by a ForeignKey is unique if not f.rel.to._meta.get_field(f.rel.field_name).unique: e.add(opts, "Field '%s' under model '%s' must have a unique=True constraint." % (f.rel.field_name, f.rel.to.__name__)) @@ -165,6 +189,10 @@ def get_validation_errors(outfile, app=None): if isinstance(f.rel.to, six.string_types): continue + # Make sure the model we're related hasn't been swapped out + if f.rel.to._meta.swapped: + e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable)) + # Check that the field is not set to unique. ManyToManyFields do not support unique. if f.unique: e.add(opts, "ManyToManyFields cannot be unique. Remove the unique argument on '%s'." % f.name) @@ -176,7 +204,7 @@ def get_validation_errors(outfile, app=None): seen_from, seen_to, seen_self = False, False, 0 for inter_field in f.rel.through._meta.fields: rel_to = getattr(inter_field.rel, 'to', None) - if from_model == to_model: # relation to self + if from_model == to_model: # relation to self if rel_to == from_model: seen_self += 1 if seen_self > 2: @@ -278,7 +306,8 @@ def get_validation_errors(outfile, app=None): # Check ordering attribute. if opts.ordering: for field_name in opts.ordering: - if field_name == '?': continue + if field_name == '?': + continue if field_name.startswith('-'): field_name = field_name[1:] if opts.order_with_respect_to and field_name == '_order': diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py index a1fff6f9bb..37fa906280 100644 --- a/django/core/serializers/python.py +++ b/django/core/serializers/python.py @@ -11,6 +11,7 @@ from django.db import models, DEFAULT_DB_ALIAS from django.utils.encoding import smart_text, is_protected_type from django.utils import six + class Serializer(base.Serializer): """ Serializes a QuerySet to basic Python objects. @@ -72,6 +73,7 @@ class Serializer(base.Serializer): def getvalue(self): return self.objects + def Deserializer(object_list, **options): """ Deserialize simple Python objects back into Django ORM instances. @@ -80,15 +82,23 @@ def Deserializer(object_list, **options): stream or a string) to the constructor """ db = options.pop('using', DEFAULT_DB_ALIAS) + ignore = options.pop('ignorenonexistent', False) + models.get_apps() for d in object_list: # Look up the model and starting build a dict of data for it. Model = _get_model(d["model"]) - data = {Model._meta.pk.attname : Model._meta.pk.to_python(d["pk"])} + data = {Model._meta.pk.attname: Model._meta.pk.to_python(d["pk"])} m2m_data = {} + model_fields = Model._meta.get_all_field_names() # Handle each field for (field_name, field_value) in six.iteritems(d["fields"]): + + if ignore and field_name not in model_fields: + # skip fields no longer on model + continue + if isinstance(field_value, str): field_value = smart_text(field_value, options.get("encoding", settings.DEFAULT_CHARSET), strings_only=True) diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py index 19b287af87..a7004f2c2f 100644 --- a/django/core/servers/basehttp.py +++ b/django/core/servers/basehttp.py @@ -14,9 +14,8 @@ import socket import sys import traceback try: - from urllib.parse import unquote, urljoin + from urllib.parse import urljoin except ImportError: # Python 2 - from urllib import unquote from urlparse import urljoin from django.utils.six.moves import socketserver from wsgiref import simple_server @@ -139,37 +138,6 @@ class WSGIRequestHandler(simple_server.WSGIRequestHandler, object): self.style = color_style() super(WSGIRequestHandler, self).__init__(*args, **kwargs) - def get_environ(self): - env = self.server.base_environ.copy() - env['SERVER_PROTOCOL'] = self.request_version - env['REQUEST_METHOD'] = self.command - if '?' in self.path: - path,query = self.path.split('?',1) - else: - path,query = self.path,'' - - env['PATH_INFO'] = unquote(path) - env['QUERY_STRING'] = query - env['REMOTE_ADDR'] = self.client_address[0] - env['CONTENT_TYPE'] = self.headers.get('content-type', 'text/plain') - - length = self.headers.get('content-length') - if length: - env['CONTENT_LENGTH'] = length - - for key, value in self.headers.items(): - key = key.replace('-','_').upper() - value = value.strip() - if key in env: - # Skip content length, type, etc. - continue - if 'HTTP_' + key in env: - # Comma-separate multiple headers - env['HTTP_' + key] += ',' + value - else: - env['HTTP_' + key] = value - return env - def log_message(self, format, *args): # Don't bother logging requests for admin images or the favicon. if (self.path.startswith(self.admin_static_prefix) diff --git a/django/core/validators.py b/django/core/validators.py index 317e3880bf..251b5d8856 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -15,6 +15,7 @@ from django.utils import six # These values, if given to validate(), will trigger the self.required check. EMPTY_VALUES = (None, '', [], (), {}) + class RegexValidator(object): regex = '' message = _('Enter a valid value.') @@ -39,14 +40,15 @@ class RegexValidator(object): if not self.regex.search(force_text(value)): raise ValidationError(self.message, code=self.code) + class URLValidator(RegexValidator): regex = re.compile( - r'^(?:http|ftp)s?://' # http:// or https:// - r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain... - r'localhost|' #localhost... - r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4 - r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6 - r'(?::\d+)?' # optional port + r'^(?:http|ftp)s?://' # http:// or https:// + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... + r'localhost|' # localhost... + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4 + r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6 + r'(?::\d+)?' # optional port r'(?:/?|[/?]\S+)$', re.IGNORECASE) def __call__(self, value): @@ -58,8 +60,8 @@ class URLValidator(RegexValidator): value = force_text(value) scheme, netloc, path, query, fragment = urlsplit(value) try: - netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE - except UnicodeError: # invalid domain part + netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE + except UnicodeError: # invalid domain part raise e url = urlunsplit((scheme, netloc, path, query, fragment)) super(URLValidator, self).__call__(url) @@ -75,6 +77,7 @@ def validate_integer(value): except (ValueError, TypeError): raise ValidationError('') + class EmailValidator(RegexValidator): def __call__(self, value): @@ -96,9 +99,9 @@ email_re = re.compile( r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom # quoted-string, see also http://tools.ietf.org/html/rfc2822#section-3.2.5 r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"' - r')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$)' # domain + r')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)$)' # domain r'|\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$', re.IGNORECASE) # literal form, ipv4 address (SMTP 4.1.3) -validate_email = EmailValidator(email_re, _('Enter a valid e-mail address.'), 'invalid') +validate_email = EmailValidator(email_re, _('Enter a valid email address.'), 'invalid') slug_re = re.compile(r'^[-a-zA-Z0-9_]+$') validate_slug = RegexValidator(slug_re, _("Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."), 'invalid') @@ -106,10 +109,12 @@ validate_slug = RegexValidator(slug_re, _("Enter a valid 'slug' consisting of le ipv4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$') validate_ipv4_address = RegexValidator(ipv4_re, _('Enter a valid IPv4 address.'), 'invalid') + def validate_ipv6_address(value): if not is_valid_ipv6_address(value): raise ValidationError(_('Enter a valid IPv6 address.'), code='invalid') + def validate_ipv46_address(value): try: validate_ipv4_address(value) @@ -125,6 +130,7 @@ ip_address_validator_map = { 'ipv6': ([validate_ipv6_address], _('Enter a valid IPv6 address.')), } + def ip_address_validators(protocol, unpack_ipv4): """ Depending on the given parameters returns the appropriate validators for @@ -147,7 +153,7 @@ validate_comma_separated_integer_list = RegexValidator(comma_separated_int_list_ class BaseValidator(object): compare = lambda self, a, b: a is not b - clean = lambda self, x: x + clean = lambda self, x: x message = _('Ensure this value is %(limit_value)s (it is %(show_value)s).') code = 'limit_value' @@ -164,25 +170,28 @@ class BaseValidator(object): params=params, ) + class MaxValueValidator(BaseValidator): compare = lambda self, a, b: a > b message = _('Ensure this value is less than or equal to %(limit_value)s.') code = 'max_value' + class MinValueValidator(BaseValidator): compare = lambda self, a, b: a < b message = _('Ensure this value is greater than or equal to %(limit_value)s.') code = 'min_value' + class MinLengthValidator(BaseValidator): compare = lambda self, a, b: a < b - clean = lambda self, x: len(x) + clean = lambda self, x: len(x) message = _('Ensure this value has at least %(limit_value)d characters (it has %(show_value)d).') code = 'min_length' + class MaxLengthValidator(BaseValidator): compare = lambda self, a, b: a > b - clean = lambda self, x: len(x) + clean = lambda self, x: len(x) message = _('Ensure this value has at most %(limit_value)d characters (it has %(show_value)d).') code = 'max_length' - diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 6c3d00d140..28024c6428 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -796,7 +796,7 @@ class BaseDatabaseOperations(object): The `style` argument is a Style object as returned by either color_style() or no_style() in django.core.management.color. """ - return [] # No sequence reset required by default. + return [] # No sequence reset required by default. def start_transaction_sql(self): """ @@ -935,6 +935,7 @@ class BaseDatabaseOperations(object): conn = ' %s ' % connector return conn.join(sub_expressions) + class BaseDatabaseIntrospection(object): """ This class encapsulates all backend-specific introspection utilities @@ -1030,12 +1031,14 @@ class BaseDatabaseIntrospection(object): for model in models.get_models(app): if not model._meta.managed: continue + if model._meta.swapped: + continue if not router.allow_syncdb(self.connection.alias, model): continue for f in model._meta.local_fields: if isinstance(f, models.AutoField): sequence_list.append({'table': model._meta.db_table, 'column': f.column}) - break # Only one AutoField is allowed per model, so don't bother continuing. + break # Only one AutoField is allowed per model, so don't bother continuing. for f in model._meta.local_many_to_many: # If this is an m2m using an intermediate table, @@ -1097,6 +1100,7 @@ class BaseDatabaseClient(object): def runshell(self): raise NotImplementedError() + class BaseDatabaseValidation(object): """ This class encapsualtes all backend-specific model validation. diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py index 090f635df3..69eee35352 100644 --- a/django/db/backends/creation.py +++ b/django/db/backends/creation.py @@ -42,7 +42,7 @@ class BaseDatabaseCreation(object): (list_of_sql, pending_references_dict) """ opts = model._meta - if not opts.managed or opts.proxy: + if not opts.managed or opts.proxy or opts.swapped: return [], {} final_output = [] table_output = [] @@ -94,9 +94,9 @@ class BaseDatabaseCreation(object): full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + style.SQL_TABLE(qn(opts.db_table)) + ' ('] - for i, line in enumerate(table_output): # Combine and add commas. + for i, line in enumerate(table_output): # Combine and add commas. full_statement.append( - ' %s%s' % (line, i < len(table_output)-1 and ',' or '')) + ' %s%s' % (line, i < len(table_output) - 1 and ',' or '')) full_statement.append(')') if opts.db_tablespace: tablespace_sql = self.connection.ops.tablespace_sql( @@ -145,11 +145,11 @@ class BaseDatabaseCreation(object): """ from django.db.backends.util import truncate_name - if not model._meta.managed or model._meta.proxy: + opts = model._meta + if not opts.managed or opts.proxy or opts.swapped: return [] qn = self.connection.ops.quote_name final_output = [] - opts = model._meta if model in pending_references: for rel_class, f in pending_references[model]: rel_opts = rel_class._meta @@ -174,7 +174,7 @@ class BaseDatabaseCreation(object): """ Returns the CREATE INDEX SQL statements for a single model. """ - if not model._meta.managed or model._meta.proxy: + if not model._meta.managed or model._meta.proxy or model._meta.swapped: return [] output = [] for f in model._meta.local_fields: @@ -213,7 +213,7 @@ class BaseDatabaseCreation(object): Return the DROP TABLE and restraint dropping statements for a single model. """ - if not model._meta.managed or model._meta.proxy: + if not model._meta.managed or model._meta.proxy or model._meta.swapped: return [] # Drop the table now qn = self.connection.ops.quote_name @@ -230,7 +230,7 @@ class BaseDatabaseCreation(object): def sql_remove_table_constraints(self, model, references_to_delete, style): from django.db.backends.util import truncate_name - if not model._meta.managed or model._meta.proxy: + if not model._meta.managed or model._meta.proxy or model._meta.swapped: return [] output = [] qn = self.connection.ops.quote_name diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 26d87d6a85..797406859c 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -66,7 +66,7 @@ def adapt_datetime_with_timezone_support(value, conv): # Equivalent to DateTimeField.get_db_prep_value. Used only by raw SQL. if settings.USE_TZ: if timezone.is_naive(value): - warnings.warn("SQLite received a naive datetime (%s)" + warnings.warn("MySQL received a naive datetime (%s)" " while time zone support is active." % value, RuntimeWarning) default_timezone = timezone.get_default_timezone() diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index fb19931c60..cd26098caa 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -3,6 +3,7 @@ PostgreSQL database backend for Django. Requires psycopg 2: http://initd.org/projects/psycopg2 """ +import logging import sys from django.db import utils @@ -15,7 +16,6 @@ from django.db.backends.postgresql_psycopg2.version import get_version from django.db.backends.postgresql_psycopg2.introspection import DatabaseIntrospection from django.db.backends.postgresql_psycopg2.schema import DatabaseSchemaEditor from django.utils.encoding import force_str -from django.utils.log import getLogger from django.utils.safestring import SafeText, SafeBytes from django.utils import six from django.utils.timezone import utc @@ -34,7 +34,7 @@ psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) psycopg2.extensions.register_adapter(SafeBytes, psycopg2.extensions.QuotedString) psycopg2.extensions.register_adapter(SafeText, psycopg2.extensions.QuotedString) -logger = getLogger('django.db.backends') +logger = logging.getLogger('django.db.backends') def utc_tzinfo_factory(offset): if offset != 0: diff --git a/django/db/backends/sqlite3/creation.py b/django/db/backends/sqlite3/creation.py index c022b56c85..9dacac72e1 100644 --- a/django/db/backends/sqlite3/creation.py +++ b/django/db/backends/sqlite3/creation.py @@ -56,11 +56,11 @@ class DatabaseCreation(BaseDatabaseCreation): if not autoclobber: confirm = input("Type 'yes' if you would like to try deleting the test database '%s', or 'no' to cancel: " % test_database_name) if autoclobber or confirm == 'yes': - try: - os.remove(test_database_name) - except Exception as e: - sys.stderr.write("Got an error deleting the old test database: %s\n" % e) - sys.exit(2) + try: + os.remove(test_database_name) + except Exception as e: + sys.stderr.write("Got an error deleting the old test database: %s\n" % e) + sys.exit(2) else: print("Tests cancelled.") sys.exit(1) diff --git a/django/db/backends/util.py b/django/db/backends/util.py index 75d4d07a66..e029c42899 100644 --- a/django/db/backends/util.py +++ b/django/db/backends/util.py @@ -3,15 +3,15 @@ from __future__ import unicode_literals import datetime import decimal import hashlib +import logging from time import time from django.conf import settings from django.utils.encoding import force_bytes -from django.utils.log import getLogger from django.utils.timezone import utc -logger = getLogger('django.db.backends') +logger = logging.getLogger('django.db.backends') class CursorWrapper(object): diff --git a/django/db/models/base.py b/django/db/models/base.py index de94911601..35c607ac2d 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -5,7 +5,7 @@ import sys from functools import update_wrapper from django.utils.six.moves import zip -import django.db.models.manager # Imported to register signal handler. +import django.db.models.manager # Imported to register signal handler. from django.conf import settings from django.core.exceptions import (ObjectDoesNotExist, MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS) @@ -108,6 +108,11 @@ class ModelBase(type): is_proxy = new_class._meta.proxy + # If the model is a proxy, ensure that the base class + # hasn't been swapped out. + if is_proxy and base_meta and base_meta.swapped: + raise TypeError("%s cannot proxy the swapped model '%s'." % (name, base_meta.swapped)) + if getattr(new_class, '_default_manager', None): if not is_proxy: # Multi-table inheritance doesn't inherit default manager from @@ -265,6 +270,7 @@ class ModelBase(type): if opts.order_with_respect_to: cls.get_next_in_order = curry(cls._get_next_or_previous_in_order, is_next=True) cls.get_previous_in_order = curry(cls._get_next_or_previous_in_order, is_next=False) + # defer creating accessors on the foreign class until we are # certain it has been created def make_foreign_order_accessors(field, model, cls): @@ -295,6 +301,7 @@ class ModelBase(type): signals.class_prepared.send(sender=cls) + class ModelState(object): """ A class for storing instance state @@ -306,6 +313,7 @@ class ModelState(object): # This impacts validation only; it has no effect on the actual save. self.adding = True + class Model(six.with_metaclass(ModelBase, object)): _deferred = False @@ -635,7 +643,6 @@ class Model(six.with_metaclass(ModelBase, object)): signals.post_save.send(sender=origin, instance=self, created=(not record_exists), update_fields=update_fields, raw=raw, using=using) - save_base.alters_data = True def delete(self, using=None): @@ -659,7 +666,7 @@ class Model(six.with_metaclass(ModelBase, object)): order = not is_next and '-' or '' param = force_text(getattr(self, field.attname)) q = Q(**{'%s__%s' % (field.name, op): param}) - q = q|Q(**{field.name: param, 'pk__%s' % op: self.pk}) + q = q | Q(**{field.name: param, 'pk__%s' % op: self.pk}) qs = self.__class__._default_manager.using(self._state.db).filter(**kwargs).filter(q).order_by('%s%s' % (order, field.name), '%spk' % order) try: return qs[0] @@ -852,7 +859,7 @@ class Model(six.with_metaclass(ModelBase, object)): field = opts.get_field(field_name) field_label = capfirst(field.verbose_name) # Insert the error into the error dict, very sneaky - return field.error_messages['unique'] % { + return field.error_messages['unique'] % { 'model_name': six.text_type(model_name), 'field_label': six.text_type(field_label) } @@ -860,7 +867,7 @@ class Model(six.with_metaclass(ModelBase, object)): else: field_labels = [capfirst(opts.get_field(f).verbose_name) for f in unique_check] field_labels = get_text_list(field_labels, _('and')) - return _("%(model_name)s with this %(field_label)s already exists.") % { + return _("%(model_name)s with this %(field_label)s already exists.") % { 'model_name': six.text_type(model_name), 'field_label': six.text_type(field_labels) } @@ -924,7 +931,6 @@ class Model(six.with_metaclass(ModelBase, object)): raise ValidationError(errors) - ############################################ # HELPER FUNCTIONS (CURRIED MODEL METHODS) # ############################################ @@ -966,6 +972,7 @@ def get_absolute_url(opts, func, self, *args, **kwargs): class Empty(object): pass + def model_unpickle(model, attrs): """ Used to unpickle Model subclasses with deferred fields. @@ -974,6 +981,7 @@ def model_unpickle(model, attrs): return cls.__new__(cls) model_unpickle.__safe_for_unpickle__ = True + def unpickle_inner_exception(klass, exception_name): # Get the exception class from the class it is attached to: exception = getattr(klass, exception_name) diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py index 4449b75a81..6dff4a2882 100644 --- a/django/db/models/deletion.py +++ b/django/db/models/deletion.py @@ -77,6 +77,9 @@ class Collector(object): self.data = {} self.batches = {} # {model: {field: set([instances])}} self.field_updates = {} # {model: {(field, value): set([instances])}} + # fast_deletes is a list of queryset-likes that can be deleted without + # fetching the objects into memory. + self.fast_deletes = [] # Tracks deletion-order dependency for databases without transactions # or ability to defer constraint checks. Only concrete model classes @@ -131,6 +134,43 @@ class Collector(object): model, {}).setdefault( (field, value), set()).update(objs) + def can_fast_delete(self, objs, from_field=None): + """ + Determines if the objects in the given queryset-like can be + fast-deleted. This can be done if there are no cascades, no + parents and no signal listeners for the object class. + + The 'from_field' tells where we are coming from - we need this to + determine if the objects are in fact to be deleted. Allows also + skipping parent -> child -> parent chain preventing fast delete of + the child. + """ + if from_field and from_field.rel.on_delete is not CASCADE: + return False + if not (hasattr(objs, 'model') and hasattr(objs, '_raw_delete')): + return False + model = objs.model + if (signals.pre_delete.has_listeners(model) + or signals.post_delete.has_listeners(model) + or signals.m2m_changed.has_listeners(model)): + return False + # The use of from_field comes from the need to avoid cascade back to + # parent when parent delete is cascading to child. + opts = model._meta + if any(link != from_field for link in opts.concrete_model._meta.parents.values()): + return False + # Foreign keys pointing to this model, both from m2m and other + # models. + for related in opts.get_all_related_objects( + include_hidden=True, include_proxy_eq=True): + if related.field.rel.on_delete is not DO_NOTHING: + return False + # GFK deletes + for relation in opts.many_to_many: + if not relation.rel.through: + return False + return True + def collect(self, objs, source=None, nullable=False, collect_related=True, source_attr=None, reverse_dependency=False): """ @@ -148,6 +188,9 @@ class Collector(object): models, the one case in which the cascade follows the forwards direction of an FK rather than the reverse direction.) """ + if self.can_fast_delete(objs): + self.fast_deletes.append(objs) + return new_objs = self.add(objs, source, nullable, reverse_dependency=reverse_dependency) if not new_objs: @@ -160,6 +203,10 @@ class Collector(object): concrete_model = model._meta.concrete_model for ptr in six.itervalues(concrete_model._meta.parents): if ptr: + # FIXME: This seems to be buggy and execute a query for each + # parent object fetch. We have the parent data in the obj, + # but we don't have a nice way to turn that data into parent + # object instance. parent_objs = [getattr(obj, ptr.name) for obj in new_objs] self.collect(parent_objs, source=model, source_attr=ptr.rel.related_name, @@ -170,12 +217,12 @@ class Collector(object): for related in model._meta.get_all_related_objects( include_hidden=True, include_proxy_eq=True): field = related.field - if related.model._meta.auto_created: - self.add_batch(related.model, field, new_objs) - else: - sub_objs = self.related_objects(related, new_objs) - if not sub_objs: - continue + if field.rel.on_delete == DO_NOTHING: + continue + sub_objs = self.related_objects(related, new_objs) + if self.can_fast_delete(sub_objs, from_field=field): + self.fast_deletes.append(sub_objs) + elif sub_objs: field.rel.on_delete(self, field, sub_objs, self.using) # TODO This entire block is only needed as a special case to @@ -241,6 +288,10 @@ class Collector(object): sender=model, instance=obj, using=self.using ) + # fast deletes + for qs in self.fast_deletes: + qs._raw_delete(using=self.using) + # update fields for model, instances_for_fieldvalues in six.iteritems(self.field_updates): query = sql.UpdateQuery(model) diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index 639ef6ee10..30c44bacde 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -14,9 +14,11 @@ class ExpressionNode(tree.Node): # because it can be used in strings that also # have parameter substitution. - # Bitwise operators - AND = '&' - OR = '|' + # Bitwise operators - note that these are generated by .bitand() + # and .bitor(), the '&' and '|' are reserved for boolean operator + # usage. + BITAND = '&' + BITOR = '|' def __init__(self, children=None, connector=None, negated=False): if children is not None and len(children) > 1 and connector is None: @@ -66,10 +68,20 @@ class ExpressionNode(tree.Node): return self._combine(other, self.MOD, False) def __and__(self, other): - return self._combine(other, self.AND, False) + raise NotImplementedError( + "Use .bitand() and .bitor() for bitwise logical operations." + ) + + def bitand(self, other): + return self._combine(other, self.BITAND, False) def __or__(self, other): - return self._combine(other, self.OR, False) + raise NotImplementedError( + "Use .bitand() and .bitor() for bitwise logical operations." + ) + + def bitor(self, other): + return self._combine(other, self.BITOR, False) def __radd__(self, other): return self._combine(other, self.ADD, True) @@ -88,10 +100,14 @@ class ExpressionNode(tree.Node): return self._combine(other, self.MOD, True) def __rand__(self, other): - return self._combine(other, self.AND, True) + raise NotImplementedError( + "Use .bitand() and .bitor() for bitwise logical operations." + ) def __ror__(self, other): - return self._combine(other, self.OR, True) + raise NotImplementedError( + "Use .bitand() and .bitor() for bitwise logical operations." + ) def prepare_database_save(self, unused): return self diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index a0b09c9fec..30770a0639 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -925,7 +925,7 @@ class DecimalField(Field): class EmailField(CharField): default_validators = [validators.validate_email] - description = _("E-mail address") + description = _("Email address") def __init__(self, *args, **kwargs): # max_length should be overridden to 254 characters to be fully diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 37bf4e8072..92d35dc720 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -21,6 +21,7 @@ RECURSIVE_RELATIONSHIP_CONSTANT = 'self' pending_lookups = {} + def add_lazy_relation(cls, field, relation, operation): """ Adds a lookup on ``cls`` when a related field is defined using a string, @@ -77,6 +78,7 @@ def add_lazy_relation(cls, field, relation, operation): value = (cls, field, operation) pending_lookups.setdefault(key, []).append(value) + def do_pending_lookups(sender, **kwargs): """ Handle any pending relations to the sending model. Sent from class_prepared. @@ -87,6 +89,7 @@ def do_pending_lookups(sender, **kwargs): signals.class_prepared.connect(do_pending_lookups) + #HACK class RelatedField(object): def contribute_to_class(self, cls, name): @@ -220,6 +223,7 @@ class RelatedField(object): # "related_name" option. return self.rel.related_name or self.opts.object_name.lower() + class SingleRelatedObjectDescriptor(object): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have @@ -257,13 +261,17 @@ class SingleRelatedObjectDescriptor(object): try: rel_obj = getattr(instance, self.cache_name) except AttributeError: - params = {'%s__pk' % self.related.field.name: instance._get_pk_val()} - try: - rel_obj = self.get_query_set(instance=instance).get(**params) - except self.related.model.DoesNotExist: + related_pk = instance._get_pk_val() + if related_pk is None: rel_obj = None else: - setattr(rel_obj, self.related.field.get_cache_name(), instance) + params = {'%s__pk' % self.related.field.name: related_pk} + try: + rel_obj = self.get_query_set(instance=instance).get(**params) + except self.related.model.DoesNotExist: + rel_obj = None + else: + setattr(rel_obj, self.related.field.get_cache_name(), instance) setattr(instance, self.cache_name, rel_obj) if rel_obj is None: raise self.related.model.DoesNotExist @@ -297,8 +305,13 @@ class SingleRelatedObjectDescriptor(object): raise ValueError('Cannot assign "%r": instance is on database "%s", value is on database "%s"' % (value, instance._state.db, value._state.db)) + related_pk = getattr(instance, self.related.field.rel.get_related_field().attname) + if related_pk is None: + raise ValueError('Cannot assign "%r": "%s" instance isn\'t saved in the database.' % + (value, instance._meta.object_name)) + # Set the value of the related field to the value of the related object's related field - setattr(value, self.related.field.attname, getattr(instance, self.related.field.rel.get_related_field().attname)) + setattr(value, self.related.field.attname, related_pk) # Since we already know what the related object is, seed the related # object caches now, too. This avoids another db hit if you get the @@ -306,6 +319,7 @@ class SingleRelatedObjectDescriptor(object): setattr(instance, self.cache_name, value) setattr(value, self.related.field.get_cache_name(), instance) + class ReverseSingleRelatedObjectDescriptor(object): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have @@ -430,6 +444,7 @@ class ReverseSingleRelatedObjectDescriptor(object): if value is not None and not self.field.rel.multiple: setattr(value, self.field.related.get_cache_name(), instance) + class ForeignRelatedObjectsDescriptor(object): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have @@ -660,7 +675,7 @@ def create_many_related_manager(superclass, rel): for obj in objs: if isinstance(obj, self.model): if not router.allow_relation(obj, self.instance): - raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' % + raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' % (obj, self.instance._state.db, obj._state.db)) new_ids.add(obj.pk) elif isinstance(obj, Model): @@ -752,6 +767,7 @@ def create_many_related_manager(superclass, rel): return ManyRelatedManager + class ManyRelatedObjectsDescriptor(object): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have @@ -860,12 +876,13 @@ class ReverseManyRelatedObjectsDescriptor(object): manager.clear() manager.add(*value) + class ManyToOneRel(object): def __init__(self, to, field_name, related_name=None, limit_choices_to=None, parent_link=False, on_delete=None): try: to._meta - except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT + except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT assert isinstance(to, six.string_types), "'to' must be either a model, a model name or the string %r" % RECURSIVE_RELATIONSHIP_CONSTANT self.to, self.field_name = to, field_name self.related_name = related_name @@ -891,6 +908,7 @@ class ManyToOneRel(object): self.field_name) return data[0] + class OneToOneRel(ManyToOneRel): def __init__(self, to, field_name, related_name=None, limit_choices_to=None, parent_link=False, on_delete=None): @@ -900,6 +918,7 @@ class OneToOneRel(ManyToOneRel): ) self.multiple = False + class ManyToManyRel(object): def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True, through=None): @@ -924,16 +943,18 @@ class ManyToManyRel(object): """ return self.to._meta.pk + class ForeignKey(RelatedField, Field): empty_strings_allowed = False default_error_messages = { 'invalid': _('Model %(model)s with pk %(pk)r does not exist.') } description = _("Foreign Key (type determined by related field)") + def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs): try: to_name = to._meta.object_name.lower() - except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT + except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT assert isinstance(to, six.string_types), "%s(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT) else: assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name) @@ -1013,8 +1034,8 @@ class ForeignKey(RelatedField, Field): def contribute_to_related_class(self, cls, related): # Internal FK's - i.e., those with a related name ending with '+' - - # don't get a related descriptor. - if not self.rel.is_hidden(): + # and swapped models don't get a related descriptor. + if not self.rel.is_hidden() and not related.model._meta.swapped: setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) if self.rel.limit_choices_to: cls._meta.related_fkey_lookups.append(self.rel.limit_choices_to) @@ -1053,6 +1074,7 @@ class ForeignKey(RelatedField, Field): def db_parameters(self, connection): return {"type": self.db_type(connection), "check": []} + class OneToOneField(ForeignKey): """ A OneToOneField is essentially the same as a ForeignKey, with the exception @@ -1061,6 +1083,7 @@ class OneToOneField(ForeignKey): rather than returning a list. """ description = _("One-to-one relationship") + def __init__(self, to, to_field=None, **kwargs): kwargs['unique'] = True super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs) @@ -1080,12 +1103,14 @@ class OneToOneField(ForeignKey): else: setattr(instance, self.attname, data) + def create_many_to_many_intermediary_model(field, klass): from django.db import models managed = True if isinstance(field.rel.to, six.string_types) and field.rel.to != RECURSIVE_RELATIONSHIP_CONSTANT: to_model = field.rel.to to = to_model.split('.')[-1] + def set_managed(field, model, cls): field.rel.through._meta.managed = model._meta.managed or cls._meta.managed add_lazy_relation(klass, field, to_model, set_managed) @@ -1122,12 +1147,14 @@ def create_many_to_many_intermediary_model(field, klass): to: models.ForeignKey(to_model, related_name='%s+' % name, db_tablespace=field.db_tablespace) }) + class ManyToManyField(RelatedField, Field): description = _("Many-to-many relationship") + def __init__(self, to, **kwargs): try: assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name) - except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT + except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT assert isinstance(to, six.string_types), "%s(%r) is invalid. First parameter to ManyToManyField must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT) # Python 2.6 and earlier require dictionary keys to be of str type, # not unicode and class names must be ASCII (in Python 2.x), so we @@ -1138,7 +1165,7 @@ class ManyToManyField(RelatedField, Field): kwargs['rel'] = ManyToManyRel(to, related_name=kwargs.pop('related_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), - symmetrical=kwargs.pop('symmetrical', to==RECURSIVE_RELATIONSHIP_CONSTANT), + symmetrical=kwargs.pop('symmetrical', to == RECURSIVE_RELATIONSHIP_CONSTANT), through=kwargs.pop('through', None)) self.db_table = kwargs.pop('db_table', None) @@ -1169,7 +1196,7 @@ class ManyToManyField(RelatedField, Field): if hasattr(self, cache_attr): return getattr(self, cache_attr) for f in self.rel.through._meta.fields: - if hasattr(f,'rel') and f.rel and f.rel.to == related.model: + if hasattr(f, 'rel') and f.rel and f.rel.to == related.model: setattr(self, cache_attr, getattr(f, attr)) return getattr(self, cache_attr) @@ -1180,7 +1207,7 @@ class ManyToManyField(RelatedField, Field): return getattr(self, cache_attr) found = False for f in self.rel.through._meta.fields: - if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model: + if hasattr(f, 'rel') and f.rel and f.rel.to == related.parent_model: if related.model == related.parent_model: # If this is an m2m-intermediate to self, # the first foreign key you find will be @@ -1225,7 +1252,8 @@ class ManyToManyField(RelatedField, Field): # The intermediate m2m model is not auto created if: # 1) There is a manually specified intermediate, or # 2) The class owning the m2m field is abstract. - if not self.rel.through and not cls._meta.abstract: + # 3) The class owning the m2m field has been swapped out. + if not self.rel.through and not cls._meta.abstract and not cls._meta.swapped: self.rel.through = create_many_to_many_intermediary_model(self, cls) # Add the descriptor for the m2m relation @@ -1249,8 +1277,8 @@ class ManyToManyField(RelatedField, Field): def contribute_to_related_class(self, cls, related): # Internal M2Ms (i.e., those with a related name ending with '+') - # don't get a related descriptor. - if not self.rel.is_hidden(): + # and swapped models don't get a related descriptor. + if not self.rel.is_hidden() and not related.model._meta.swapped: setattr(cls, related.get_accessor_name(), ManyRelatedObjectsDescriptor(related)) # Set up the accessors for the column names on the m2m table diff --git a/django/db/models/manager.py b/django/db/models/manager.py index e1bbf6ebc5..8da8af487c 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -14,6 +14,10 @@ def ensure_default_manager(sender, **kwargs): """ cls = sender if cls._meta.abstract: + setattr(cls, 'objects', AbstractManagerDescriptor(cls)) + return + elif cls._meta.swapped: + setattr(cls, 'objects', SwappedManagerDescriptor(cls)) return if not getattr(cls, '_default_manager', None): # Create the default manager, if needed. @@ -42,6 +46,7 @@ def ensure_default_manager(sender, **kwargs): signals.class_prepared.connect(ensure_default_manager) + class Manager(object): # Tracks each time a Manager instance is created. Used to retain order. creation_counter = 0 @@ -56,7 +61,14 @@ class Manager(object): def contribute_to_class(self, model, name): # TODO: Use weakref because of possible memory leak / circular reference. self.model = model - setattr(model, name, ManagerDescriptor(self)) + # Only contribute the manager if the model is concrete + if model._meta.abstract: + setattr(model, name, AbstractManagerDescriptor(model)) + elif model._meta.swapped: + setattr(model, name, SwappedManagerDescriptor(model)) + else: + # if not model._meta.abstract and not model._meta.swapped: + setattr(model, name, ManagerDescriptor(self)) if not getattr(model, '_default_manager', None) or self.creation_counter < model._default_manager.creation_counter: model._default_manager = self if model._meta.abstract or (self._inherited and not self.model._meta.proxy): @@ -208,6 +220,7 @@ class Manager(object): def raw(self, raw_query, params=None, *args, **kwargs): return RawQuerySet(raw_query=raw_query, model=self.model, params=params, using=self._db, *args, **kwargs) + class ManagerDescriptor(object): # This class ensures managers aren't accessible via model instances. # For example, Poll.objects works, but poll_obj.objects raises AttributeError. @@ -219,6 +232,31 @@ class ManagerDescriptor(object): raise AttributeError("Manager isn't accessible via %s instances" % type.__name__) return self.manager + +class AbstractManagerDescriptor(object): + # This class provides a better error message when you try to access a + # manager on an abstract model. + def __init__(self, model): + self.model = model + + def __get__(self, instance, type=None): + raise AttributeError("Manager isn't available; %s is abstract" % ( + self.model._meta.object_name, + )) + + +class SwappedManagerDescriptor(object): + # This class provides a better error message when you try to access a + # manager on a swapped model. + def __init__(self, model): + self.model = model + + def __get__(self, instance, type=None): + raise AttributeError("Manager isn't available; %s has been swapped for '%s'" % ( + self.model._meta.object_name, self.model._meta.swapped + )) + + class EmptyManager(Manager): def get_query_set(self): return self.get_empty_query_set() diff --git a/django/db/models/options.py b/django/db/models/options.py index 820540559f..ace2816b0b 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -21,7 +21,8 @@ get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]| DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering', 'unique_together', 'permissions', 'get_latest_by', 'order_with_respect_to', 'app_label', 'db_tablespace', - 'abstract', 'managed', 'proxy', 'auto_created', 'auto_register') + 'abstract', 'managed', 'proxy', 'swappable', 'auto_created', 'auto_register') + @python_2_unicode_compatible class Options(object): @@ -32,8 +33,8 @@ class Options(object): self.verbose_name_plural = None self.db_table = '' self.ordering = [] - self.unique_together = [] - self.permissions = [] + self.unique_together = [] + self.permissions = [] self.object_name, self.app_label = None, app_label self.get_latest_by = None self.order_with_respect_to = None @@ -55,6 +56,7 @@ class Options(object): # in the end of the proxy_for_model chain. In particular, for # concrete models, the concrete_model is always the class itself. self.concrete_model = None + self.swappable = None self.parents = SortedDict() self.duplicate_targets = {} self.auto_created = False @@ -221,6 +223,19 @@ class Options(object): return raw verbose_name_raw = property(verbose_name_raw) + def _swapped(self): + """ + Has this model been swapped out for another? If so, return the model + name of the replacement; otherwise, return None. + """ + if self.swappable: + model_label = '%s.%s' % (self.app_label, self.object_name) + swapped_for = getattr(settings, self.swappable, None) + if swapped_for not in (None, model_label): + return swapped_for + return None + swapped = property(_swapped) + def _fields(self): """ The getter for self.fields. This returns the list of field objects @@ -406,13 +421,14 @@ class Options(object): # Collect also objects which are in relation to some proxy child/parent of self. proxy_cache = cache.copy() for klass in get_models(include_auto_created=True, only_installed=False): - for f in klass._meta.local_fields: - if f.rel and not isinstance(f.rel.to, six.string_types): - if self == f.rel.to._meta: - cache[RelatedObject(f.rel.to, klass, f)] = None - proxy_cache[RelatedObject(f.rel.to, klass, f)] = None - elif self.concrete_model == f.rel.to._meta.concrete_model: - proxy_cache[RelatedObject(f.rel.to, klass, f)] = None + if not klass._meta.swapped: + for f in klass._meta.local_fields: + if f.rel and not isinstance(f.rel.to, six.string_types): + if self == f.rel.to._meta: + cache[RelatedObject(f.rel.to, klass, f)] = None + proxy_cache[RelatedObject(f.rel.to, klass, f)] = None + elif self.concrete_model == f.rel.to._meta.concrete_model: + proxy_cache[RelatedObject(f.rel.to, klass, f)] = None self._related_objects_cache = cache self._related_objects_proxy_cache = proxy_cache @@ -448,9 +464,12 @@ class Options(object): else: cache[obj] = model for klass in get_models(only_installed=False): - for f in klass._meta.local_many_to_many: - if f.rel and not isinstance(f.rel.to, six.string_types) and self == f.rel.to._meta: - cache[RelatedObject(f.rel.to, klass, f)] = None + if not klass._meta.swapped: + for f in klass._meta.local_many_to_many: + if (f.rel + and not isinstance(f.rel.to, six.string_types) + and self == f.rel.to._meta): + cache[RelatedObject(f.rel.to, klass, f)] = None if app_cache_ready(): self._related_many_to_many_cache = cache return cache diff --git a/django/db/models/query.py b/django/db/models/query.py index 8bf08b7a93..dc1ddf1606 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -529,6 +529,14 @@ class QuerySet(object): self._result_cache = None delete.alters_data = True + def _raw_delete(self, using): + """ + Deletes objects found from the given queryset in single direct SQL + query. No signals are sent, and there is no protection for cascades. + """ + sql.DeleteQuery(self.model).delete_qs(self, using) + _raw_delete.alters_data = True + def update(self, **kwargs): """ Updates all elements in the current QuerySet, setting all the given @@ -975,6 +983,12 @@ class ValuesQuerySet(QuerySet): for row in self.query.get_compiler(self.db).results_iter(): yield dict(zip(names, row)) + def delete(self): + # values().delete() doesn't work currently - make sure it raises an + # user friendly error. + raise TypeError("Queries with .values() or .values_list() applied " + "can't be deleted") + def _setup_query(self): """ Constructs the field_names list that the values query will be @@ -1263,6 +1277,18 @@ class EmptyQuerySet(QuerySet): kwargs[arg.default_alias] = arg return dict([(key, None) for key in kwargs]) + def values(self, *fields): + """ + Always returns EmptyQuerySet. + """ + return self + + def values_list(self, *fields, **kwargs): + """ + Always returns EmptyQuerySet. + """ + return self + # EmptyQuerySet is always an empty result in where-clauses (and similar # situations). value_annotation = False diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index f06d6b11a4..b9095e503a 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -774,10 +774,20 @@ class SQLCompiler(object): # We only set this up here because # related_select_fields isn't populated until # execute_sql() has been called. + + # We also include types of fields of related models that + # will be included via select_related() for the benefit + # of MySQL/MySQLdb when boolean fields are involved + # (#15040). + + # This code duplicates the logic for the order of fields + # found in get_columns(). It would be nice to clean this up. if self.query.select_fields: - fields = self.query.select_fields + self.query.related_select_fields + fields = self.query.select_fields else: fields = self.query.model._meta.fields + fields = fields + self.query.related_select_fields + # If the field was deferred, exclude it from being passed # into `resolve_columns` because it wasn't selected. only_load = self.deferred_to_columns() @@ -897,8 +907,11 @@ class SQLInsertCompiler(SQLCompiler): col = "%s.%s" % (qn(opts.db_table), qn(opts.pk.column)) result.append("VALUES (%s)" % ", ".join(placeholders[0])) r_fmt, r_params = self.connection.ops.return_insert_id() - result.append(r_fmt % col) - params += r_params + # Skip empty r_fmt to allow subclasses to customize behaviour for + # 3rd party backends. Refs #19096. + if r_fmt: + result.append(r_fmt % col) + params += r_params return [(" ".join(result), tuple(params))] if can_bulk: result.append(self.connection.ops.bulk_insert_sql(fields, len(values))) @@ -934,7 +947,8 @@ class SQLDeleteCompiler(SQLCompiler): qn = self.quote_name_unless_alias result = ['DELETE FROM %s' % qn(self.query.tables[0])] where, params = self.query.where.as_sql(qn=qn, connection=self.connection) - result.append('WHERE %s' % where) + if where: + result.append('WHERE %s' % where) return ' '.join(result), tuple(params) class SQLUpdateCompiler(SQLCompiler): diff --git a/django/db/models/sql/expressions.py b/django/db/models/sql/expressions.py index ac8fea6da3..374509914d 100644 --- a/django/db/models/sql/expressions.py +++ b/django/db/models/sql/expressions.py @@ -6,7 +6,7 @@ class SQLEvaluator(object): def __init__(self, expression, query, allow_joins=True): self.expression = expression self.opts = query.get_meta() - self.cols = {} + self.cols = [] self.contains_aggregate = False self.expression.prepare(self, query, allow_joins) @@ -18,11 +18,15 @@ class SQLEvaluator(object): return self.expression.evaluate(self, qn, connection) def relabel_aliases(self, change_map): - for node, col in self.cols.items(): + new_cols = [] + for node, col in self.cols: if hasattr(col, "relabel_aliases"): col.relabel_aliases(change_map) + new_cols.append((node, col)) else: - self.cols[node] = (change_map.get(col[0], col[0]), col[1]) + new_cols.append((node, + (change_map.get(col[0], col[0]), col[1]))) + self.cols = new_cols ##################################################### # Vistor methods for initial expression preparation # @@ -41,7 +45,7 @@ class SQLEvaluator(object): if (len(field_list) == 1 and node.name in query.aggregate_select.keys()): self.contains_aggregate = True - self.cols[node] = query.aggregate_select[node.name] + self.cols.append((node, query.aggregate_select[node.name])) else: try: field, source, opts, join_list, last, _ = query.setup_joins( @@ -49,7 +53,7 @@ class SQLEvaluator(object): query.get_initial_alias(), False) col, _, join_list = query.trim_joins(source, join_list, last, False) - self.cols[node] = (join_list[-1], col) + self.cols.append((node, (join_list[-1], col))) except FieldDoesNotExist: raise FieldError("Cannot resolve keyword %r into field. " "Choices are: %s" % (self.name, @@ -80,7 +84,13 @@ class SQLEvaluator(object): return connection.ops.combine_expression(node.connector, expressions), expression_params def evaluate_leaf(self, node, qn, connection): - col = self.cols[node] + col = None + for n, c in self.cols: + if n is node: + col = c + break + if col is None: + raise ValueError("Given node not found") if hasattr(col, 'as_sql'): return col.as_sql(qn, connection), () else: diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 77f24fcf24..cef01c48ab 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -115,7 +115,6 @@ class Query(object): self.default_ordering = True self.standard_ordering = True self.ordering_aliases = [] - self.select_fields = [] self.related_select_fields = [] self.dupe_avoidance = {} self.used_aliases = set() @@ -124,6 +123,9 @@ class Query(object): # SQL-related attributes self.select = [] + # For each to-be-selected field in self.select there must be a + # corresponding entry in self.select - git seems to need this. + self.select_fields = [] self.tables = [] # Aliases in the order they are created. self.where = where() self.where_class = where @@ -431,13 +433,9 @@ class Query(object): def has_results(self, using): q = self.clone() + q.clear_select_clause() q.add_extra({'a': 1}, None, None, None, None, None) - q.select = [] - q.select_fields = [] - q.default_cols = False - q.select_related = False - q.set_extra_mask(('a',)) - q.set_aggregate_mask(()) + q.set_extra_mask(['a']) q.clear_ordering(True) q.set_limits(high=1) compiler = q.get_compiler(using=using) @@ -702,6 +700,11 @@ class Query(object): aliases = list(aliases) while aliases: alias = aliases.pop(0) + if self.alias_map[alias].rhs_join_col is None: + # This is the base table (first FROM entry) - this table + # isn't really joined at all in the query, so we should not + # alter its join type. + continue parent_alias = self.alias_map[alias].lhs_alias parent_louter = (parent_alias and self.alias_map[parent_alias].join_type == self.LOUTER) @@ -1188,6 +1191,9 @@ class Query(object): for alias in join_list: if self.alias_map[alias].join_type == self.LOUTER: j_col = self.alias_map[alias].rhs_join_col + # The join promotion logic should never produce + # a LOUTER join for the base join - assert that. + assert j_col is not None entry = self.where_class() entry.add( (Constraint(alias, j_col, None), 'isnull', True), @@ -1618,6 +1624,17 @@ class Query(object): """ return not self.low_mark and self.high_mark is None + def clear_select_clause(self): + """ + Removes all fields from SELECT clause. + """ + self.select = [] + self.select_fields = [] + self.default_cols = False + self.select_related = False + self.set_extra_mask(()) + self.set_aggregate_mask(()) + def clear_select_fields(self): """ Clears the list of fields to select (but not extra_select columns). diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index c6995c6abb..24ac957cbf 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -3,6 +3,7 @@ Query subclasses which provide extra functionality beyond simple data retrieval. """ from django.core.exceptions import FieldError +from django.db import connections from django.db.models.constants import LOOKUP_SEP from django.db.models.fields import DateField, FieldDoesNotExist from django.db.models.sql.constants import * @@ -46,6 +47,43 @@ class DeleteQuery(Query): pk_list[offset:offset + GET_ITERATOR_CHUNK_SIZE]), AND) self.do_query(self.model._meta.db_table, where, using=using) + def delete_qs(self, query, using): + """ + Delete the queryset in one SQL query (if possible). For simple queries + this is done by copying the query.query.where to self.query, for + complex queries by using subquery. + """ + innerq = query.query + # Make sure the inner query has at least one table in use. + innerq.get_initial_alias() + # The same for our new query. + self.get_initial_alias() + innerq_used_tables = [t for t in innerq.tables + if innerq.alias_refcount[t]] + if ((not innerq_used_tables or innerq_used_tables == self.tables) + and not len(innerq.having)): + # There is only the base table in use in the query, and there are + # no aggregate filtering going on. + self.where = innerq.where + else: + pk = query.model._meta.pk + if not connections[using].features.update_can_self_select: + # We can't do the delete using subquery. + values = list(query.values_list('pk', flat=True)) + if not values: + return + self.delete_batch(values, using) + return + else: + innerq.clear_select_clause() + innerq.select, innerq.select_fields = [(self.get_initial_alias(), pk.column)], [None] + values = innerq + where = self.where_class() + where.add((Constraint(None, pk.column, pk), 'in', values), AND) + self.where = where + self.get_compiler(using).execute_sql(None) + + class UpdateQuery(Query): """ Represents an "update" SQL query. @@ -205,11 +243,8 @@ class DateQuery(Query): % field.name alias = result[3][-1] select = Date((alias, field.column), lookup_type) - self.select = [select] - self.select_fields = [None] - self.select_related = False # See #7097. - self.aggregates = SortedDict() # See 18056. - self.set_extra_mask([]) + self.clear_select_clause() + self.select, self.select_fields = [select], [None] self.distinct = True self.order_by = order == 'ASC' and [1] or [-1] diff --git a/django/db/utils.py b/django/db/utils.py index 0ce09bab70..5fa78fe350 100644 --- a/django/db/utils.py +++ b/django/db/utils.py @@ -1,4 +1,5 @@ import os +import pkgutil from threading import local from django.conf import settings @@ -28,28 +29,19 @@ def load_backend(backend_name): # listing all possible (built-in) database backends. backend_dir = os.path.join(os.path.dirname(__file__), 'backends') try: - available_backends = [f for f in os.listdir(backend_dir) - if os.path.isdir(os.path.join(backend_dir, f)) - and not f.startswith('.')] + builtin_backends = [ + name for _, name, ispkg in pkgutil.iter_modules([backend_dir]) + if ispkg and name != 'dummy'] except EnvironmentError: - available_backends = [] - full_notation = backend_name.startswith('django.db.backends.') - if full_notation: - backend_name = backend_name[19:] # See #15621. - if backend_name not in available_backends: - backend_reprs = map(repr, sorted(available_backends)) + builtin_backends = [] + if backend_name not in ['django.db.backends.%s' % b for b in + builtin_backends]: + backend_reprs = map(repr, sorted(builtin_backends)) error_msg = ("%r isn't an available database backend.\n" - "Try using django.db.backends.XXX, where XXX " + "Try using 'django.db.backends.XXX', where XXX " "is one of:\n %s\nError was: %s" % (backend_name, ", ".join(backend_reprs), e_user)) raise ImproperlyConfigured(error_msg) - elif not full_notation: - # user tried to use the old notation for the database backend - error_msg = ("%r isn't an available database backend.\n" - "Try using django.db.backends.%s instead.\n" - "Error was: %s" % - (backend_name, backend_name, e_user)) - raise ImproperlyConfigured(error_msg) else: # If there's some other error, this must be an error in Django raise diff --git a/django/dispatch/dispatcher.py b/django/dispatch/dispatcher.py index ad7302176e..8d26e58bf4 100644 --- a/django/dispatch/dispatcher.py +++ b/django/dispatch/dispatcher.py @@ -141,6 +141,9 @@ class Signal(object): del self.receivers[index] break + def has_listeners(self, sender=None): + return bool(self._live_receivers(_make_id(sender))) + def send(self, sender, **named): """ Send signal from sender to all connected receivers. diff --git a/django/forms/fields.py b/django/forms/fields.py index 124e4f669a..4438812a37 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -341,7 +341,7 @@ class BaseTemporalField(Field): for format in self.input_formats: try: return self.strptime(value, format) - except ValueError: + except (ValueError, TypeError): continue raise ValidationError(self.error_messages['invalid']) @@ -461,7 +461,7 @@ class RegexField(CharField): class EmailField(CharField): default_error_messages = { - 'invalid': _('Enter a valid e-mail address.'), + 'invalid': _('Enter a valid email address.'), } default_validators = [validators.validate_email] diff --git a/django/forms/formsets.py b/django/forms/formsets.py index 42d25fac6d..c646eed506 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -123,7 +123,11 @@ class BaseFormSet(object): """ Instantiates and returns the i-th form instance in a formset. """ - defaults = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)} + defaults = { + 'auto_id': self.auto_id, + 'prefix': self.add_prefix(i), + 'error_class': self.error_class, + } if self.is_bound: defaults['data'] = self.data defaults['files'] = self.files diff --git a/django/forms/models.py b/django/forms/models.py index 1aa49eaaec..11fe0c09ea 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -1013,7 +1013,7 @@ class ModelMultipleChoiceField(ModelChoiceField): if self.required and not value: raise ValidationError(self.error_messages['required']) elif not self.required and not value: - return [] + return self.queryset.none() if not isinstance(value, (list, tuple)): raise ValidationError(self.error_messages['list']) key = self.to_field_name or 'pk' diff --git a/django/http/__init__.py b/django/http/__init__.py index ecb39129ad..46afa34ee7 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -1,812 +1,10 @@ -from __future__ import absolute_import, unicode_literals - -import copy -import datetime -from email.header import Header -import os -import re -import sys -import time -import warnings - -from io import BytesIO -from pprint import pformat -try: - from urllib.parse import quote, parse_qsl, urlencode, urljoin, urlparse -except ImportError: # Python 2 - from urllib import quote, urlencode - from urlparse import parse_qsl, urljoin, urlparse - -from django.utils.six.moves import http_cookies -# Some versions of Python 2.7 and later won't need this encoding bug fix: -_cookie_encodes_correctly = http_cookies.SimpleCookie().value_encode(';') == (';', '"\\073"') -# See ticket #13007, http://bugs.python.org/issue2193 and http://trac.edgewall.org/ticket/2256 -_tc = http_cookies.SimpleCookie() -try: - _tc.load(str('foo:bar=1')) - _cookie_allows_colon_in_names = True -except http_cookies.CookieError: - _cookie_allows_colon_in_names = False - -if _cookie_encodes_correctly and _cookie_allows_colon_in_names: - SimpleCookie = http_cookies.SimpleCookie -else: - Morsel = http_cookies.Morsel - - class SimpleCookie(http_cookies.SimpleCookie): - if not _cookie_encodes_correctly: - def value_encode(self, val): - # Some browsers do not support quoted-string from RFC 2109, - # including some versions of Safari and Internet Explorer. - # These browsers split on ';', and some versions of Safari - # are known to split on ', '. Therefore, we encode ';' and ',' - - # SimpleCookie already does the hard work of encoding and decoding. - # It uses octal sequences like '\\012' for newline etc. - # and non-ASCII chars. We just make use of this mechanism, to - # avoid introducing two encoding schemes which would be confusing - # and especially awkward for javascript. - - # NB, contrary to Python docs, value_encode returns a tuple containing - # (real val, encoded_val) - val, encoded = super(SimpleCookie, self).value_encode(val) - - encoded = encoded.replace(";", "\\073").replace(",","\\054") - # If encoded now contains any quoted chars, we need double quotes - # around the whole string. - if "\\" in encoded and not encoded.startswith('"'): - encoded = '"' + encoded + '"' - - return val, encoded - - if not _cookie_allows_colon_in_names: - def load(self, rawdata): - self.bad_cookies = set() - super(SimpleCookie, self).load(force_str(rawdata)) - for key in self.bad_cookies: - del self[key] - - # override private __set() method: - # (needed for using our Morsel, and for laxness with CookieError - def _BaseCookie__set(self, key, real_value, coded_value): - key = force_str(key) - try: - M = self.get(key, Morsel()) - M.set(key, real_value, coded_value) - dict.__setitem__(self, key, M) - except http_cookies.CookieError: - self.bad_cookies.add(key) - dict.__setitem__(self, key, http_cookies.Morsel()) - - -from django.conf import settings -from django.core import signing -from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation -from django.core.files import uploadhandler -from django.http.multipartparser import MultiPartParser -from django.http.utils import * -from django.utils.datastructures import MultiValueDict, ImmutableList -from django.utils.encoding import force_bytes, force_str, force_text, iri_to_uri -from django.utils.http import cookie_date -from django.utils import six -from django.utils import timezone - -RESERVED_CHARS="!*'();:@&=+$,/?%#[]" - -absolute_http_url_re = re.compile(r"^https?://", re.I) - -class Http404(Exception): - pass - -RAISE_ERROR = object() - - -def build_request_repr(request, path_override=None, GET_override=None, - POST_override=None, COOKIES_override=None, - META_override=None): - """ - Builds and returns the request's representation string. The request's - attributes may be overridden by pre-processed values. - """ - # Since this is called as part of error handling, we need to be very - # robust against potentially malformed input. - try: - get = (pformat(GET_override) - if GET_override is not None - else pformat(request.GET)) - except Exception: - get = '' - if request._post_parse_error: - post = '' - else: - try: - post = (pformat(POST_override) - if POST_override is not None - else pformat(request.POST)) - except Exception: - post = '' - try: - cookies = (pformat(COOKIES_override) - if COOKIES_override is not None - else pformat(request.COOKIES)) - except Exception: - cookies = '' - try: - meta = (pformat(META_override) - if META_override is not None - else pformat(request.META)) - except Exception: - meta = '' - path = path_override if path_override is not None else request.path - return force_str('<%s\npath:%s,\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s>' % - (request.__class__.__name__, - path, - six.text_type(get), - six.text_type(post), - six.text_type(cookies), - six.text_type(meta))) - -class UnreadablePostError(IOError): - pass - -class HttpRequest(object): - """A basic HTTP request.""" - - # The encoding used in GET/POST dicts. None means use default setting. - _encoding = None - _upload_handlers = [] - - def __init__(self): - self.GET, self.POST, self.COOKIES, self.META, self.FILES = {}, {}, {}, {}, {} - self.path = '' - self.path_info = '' - self.method = None - self._post_parse_error = False - - def __repr__(self): - return build_request_repr(self) - - def get_host(self): - """Returns the HTTP host using the environment or request headers.""" - # We try three options, in order of decreasing preference. - if settings.USE_X_FORWARDED_HOST and ( - 'HTTP_X_FORWARDED_HOST' in self.META): - host = self.META['HTTP_X_FORWARDED_HOST'] - elif 'HTTP_HOST' in self.META: - host = self.META['HTTP_HOST'] - else: - # Reconstruct the host using the algorithm from PEP 333. - host = self.META['SERVER_NAME'] - server_port = str(self.META['SERVER_PORT']) - if server_port != ('443' if self.is_secure() else '80'): - host = '%s:%s' % (host, server_port) - return host - - def get_full_path(self): - # RFC 3986 requires query string arguments to be in the ASCII range. - # Rather than crash if this doesn't happen, we encode defensively. - return '%s%s' % (self.path, ('?' + iri_to_uri(self.META.get('QUERY_STRING', ''))) if self.META.get('QUERY_STRING', '') else '') - - def get_signed_cookie(self, key, default=RAISE_ERROR, salt='', max_age=None): - """ - Attempts to return a signed cookie. If the signature fails or the - cookie has expired, raises an exception... unless you provide the - default argument in which case that value will be returned instead. - """ - try: - cookie_value = self.COOKIES[key] - except KeyError: - if default is not RAISE_ERROR: - return default - else: - raise - try: - value = signing.get_cookie_signer(salt=key + salt).unsign( - cookie_value, max_age=max_age) - except signing.BadSignature: - if default is not RAISE_ERROR: - return default - else: - raise - return value - - def build_absolute_uri(self, location=None): - """ - Builds an absolute URI from the location and the variables available in - this request. If no location is specified, the absolute URI is built on - ``request.get_full_path()``. - """ - if not location: - location = self.get_full_path() - if not absolute_http_url_re.match(location): - current_uri = '%s://%s%s' % ('https' if self.is_secure() else 'http', - self.get_host(), self.path) - location = urljoin(current_uri, location) - return iri_to_uri(location) - - def _is_secure(self): - return os.environ.get("HTTPS") == "on" - - def is_secure(self): - # First, check the SECURE_PROXY_SSL_HEADER setting. - if settings.SECURE_PROXY_SSL_HEADER: - try: - header, value = settings.SECURE_PROXY_SSL_HEADER - except ValueError: - raise ImproperlyConfigured('The SECURE_PROXY_SSL_HEADER setting must be a tuple containing two values.') - if self.META.get(header, None) == value: - return True - - # Failing that, fall back to _is_secure(), which is a hook for - # subclasses to implement. - return self._is_secure() - - def is_ajax(self): - return self.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest' - - @property - def encoding(self): - return self._encoding - - @encoding.setter - def encoding(self, val): - """ - Sets the encoding used for GET/POST accesses. If the GET or POST - dictionary has already been created, it is removed and recreated on the - next access (so that it is decoded correctly). - """ - self._encoding = val - if hasattr(self, '_get'): - del self._get - if hasattr(self, '_post'): - del self._post - - def _initialize_handlers(self): - self._upload_handlers = [uploadhandler.load_handler(handler, self) - for handler in settings.FILE_UPLOAD_HANDLERS] - - @property - def upload_handlers(self): - if not self._upload_handlers: - # If there are no upload handlers defined, initialize them from settings. - self._initialize_handlers() - return self._upload_handlers - - @upload_handlers.setter - def upload_handlers(self, upload_handlers): - if hasattr(self, '_files'): - raise AttributeError("You cannot set the upload handlers after the upload has been processed.") - self._upload_handlers = upload_handlers - - def parse_file_upload(self, META, post_data): - """Returns a tuple of (POST QueryDict, FILES MultiValueDict).""" - self.upload_handlers = ImmutableList( - self.upload_handlers, - warning="You cannot alter upload handlers after the upload has been processed." - ) - parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding) - return parser.parse() - - @property - def body(self): - if not hasattr(self, '_body'): - if self._read_started: - raise Exception("You cannot access body after reading from request's data stream") - try: - self._body = self.read() - except IOError as e: - six.reraise(UnreadablePostError, UnreadablePostError(*e.args), sys.exc_info()[2]) - self._stream = BytesIO(self._body) - return self._body - - @property - def raw_post_data(self): - warnings.warn('HttpRequest.raw_post_data has been deprecated. Use HttpRequest.body instead.', DeprecationWarning) - return self.body - - def _mark_post_parse_error(self): - self._post = QueryDict('') - self._files = MultiValueDict() - self._post_parse_error = True - - def _load_post_and_files(self): - # Populates self._post and self._files - if self.method != 'POST': - self._post, self._files = QueryDict('', encoding=self._encoding), MultiValueDict() - return - if self._read_started and not hasattr(self, '_body'): - self._mark_post_parse_error() - return - - if self.META.get('CONTENT_TYPE', '').startswith('multipart'): - if hasattr(self, '_body'): - # Use already read data - data = BytesIO(self._body) - else: - data = self - try: - self._post, self._files = self.parse_file_upload(self.META, data) - except: - # An error occured while parsing POST data. Since when - # formatting the error the request handler might access - # self.POST, set self._post and self._file to prevent - # attempts to parse POST data again. - # Mark that an error occured. This allows self.__repr__ to - # be explicit about it instead of simply representing an - # empty POST - self._mark_post_parse_error() - raise - else: - self._post, self._files = QueryDict(self.body, encoding=self._encoding), MultiValueDict() - - ## File-like and iterator interface. - ## - ## Expects self._stream to be set to an appropriate source of bytes by - ## a corresponding request subclass (e.g. WSGIRequest). - ## Also when request data has already been read by request.POST or - ## request.body, self._stream points to a BytesIO instance - ## containing that data. - - def read(self, *args, **kwargs): - self._read_started = True - return self._stream.read(*args, **kwargs) - - def readline(self, *args, **kwargs): - self._read_started = True - return self._stream.readline(*args, **kwargs) - - def xreadlines(self): - while True: - buf = self.readline() - if not buf: - break - yield buf - - __iter__ = xreadlines - - def readlines(self): - return list(iter(self)) - - -class QueryDict(MultiValueDict): - """ - A specialized MultiValueDict that takes a query string when initialized. - This is immutable unless you create a copy of it. - - Values retrieved from this class are converted from the given encoding - (DEFAULT_CHARSET by default) to unicode. - """ - # These are both reset in __init__, but is specified here at the class - # level so that unpickling will have valid values - _mutable = True - _encoding = None - - def __init__(self, query_string, mutable=False, encoding=None): - super(QueryDict, self).__init__() - if not encoding: - encoding = settings.DEFAULT_CHARSET - self.encoding = encoding - if six.PY3: - for key, value in parse_qsl(query_string or '', - keep_blank_values=True, - encoding=encoding): - self.appendlist(key, value) - else: - for key, value in parse_qsl(query_string or '', - keep_blank_values=True): - self.appendlist(force_text(key, encoding, errors='replace'), - force_text(value, encoding, errors='replace')) - self._mutable = mutable - - @property - def encoding(self): - if self._encoding is None: - self._encoding = settings.DEFAULT_CHARSET - return self._encoding - - @encoding.setter - def encoding(self, value): - self._encoding = value - - def _assert_mutable(self): - if not self._mutable: - raise AttributeError("This QueryDict instance is immutable") - - def __setitem__(self, key, value): - self._assert_mutable() - key = bytes_to_text(key, self.encoding) - value = bytes_to_text(value, self.encoding) - super(QueryDict, self).__setitem__(key, value) - - def __delitem__(self, key): - self._assert_mutable() - super(QueryDict, self).__delitem__(key) - - def __copy__(self): - result = self.__class__('', mutable=True, encoding=self.encoding) - for key, value in six.iterlists(self): - result.setlist(key, value) - return result - - def __deepcopy__(self, memo): - result = self.__class__('', mutable=True, encoding=self.encoding) - memo[id(self)] = result - for key, value in six.iterlists(self): - result.setlist(copy.deepcopy(key, memo), copy.deepcopy(value, memo)) - return result - - def setlist(self, key, list_): - self._assert_mutable() - key = bytes_to_text(key, self.encoding) - list_ = [bytes_to_text(elt, self.encoding) for elt in list_] - super(QueryDict, self).setlist(key, list_) - - def setlistdefault(self, key, default_list=None): - self._assert_mutable() - return super(QueryDict, self).setlistdefault(key, default_list) - - def appendlist(self, key, value): - self._assert_mutable() - key = bytes_to_text(key, self.encoding) - value = bytes_to_text(value, self.encoding) - super(QueryDict, self).appendlist(key, value) - - def pop(self, key, *args): - self._assert_mutable() - return super(QueryDict, self).pop(key, *args) - - def popitem(self): - self._assert_mutable() - return super(QueryDict, self).popitem() - - def clear(self): - self._assert_mutable() - super(QueryDict, self).clear() - - def setdefault(self, key, default=None): - self._assert_mutable() - key = bytes_to_text(key, self.encoding) - default = bytes_to_text(default, self.encoding) - return super(QueryDict, self).setdefault(key, default) - - def copy(self): - """Returns a mutable copy of this object.""" - return self.__deepcopy__({}) - - def urlencode(self, safe=None): - """ - Returns an encoded string of all query string arguments. - - :arg safe: Used to specify characters which do not require quoting, for - example:: - - >>> q = QueryDict('', mutable=True) - >>> q['next'] = '/a&b/' - >>> q.urlencode() - 'next=%2Fa%26b%2F' - >>> q.urlencode(safe='/') - 'next=/a%26b/' - - """ - output = [] - if safe: - safe = force_bytes(safe, self.encoding) - encode = lambda k, v: '%s=%s' % ((quote(k, safe), quote(v, safe))) - else: - encode = lambda k, v: urlencode({k: v}) - for k, list_ in self.lists(): - k = force_bytes(k, self.encoding) - output.extend([encode(k, force_bytes(v, self.encoding)) - for v in list_]) - return '&'.join(output) - - -def parse_cookie(cookie): - if cookie == '': - return {} - if not isinstance(cookie, http_cookies.BaseCookie): - try: - c = SimpleCookie() - c.load(cookie) - except http_cookies.CookieError: - # Invalid cookie - return {} - else: - c = cookie - cookiedict = {} - for key in c.keys(): - cookiedict[key] = c.get(key).value - return cookiedict - -class BadHeaderError(ValueError): - pass - -class HttpResponse(object): - """A basic HTTP response, with content and dictionary-accessed headers.""" - - status_code = 200 - - def __init__(self, content='', content_type=None, status=None, - mimetype=None): - # _headers is a mapping of the lower-case name to the original case of - # the header (required for working with legacy systems) and the header - # value. Both the name of the header and its value are ASCII strings. - self._headers = {} - self._charset = settings.DEFAULT_CHARSET - if mimetype: - warnings.warn("Using mimetype keyword argument is deprecated, use" - " content_type instead", PendingDeprecationWarning) - content_type = mimetype - if not content_type: - content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE, - self._charset) - # content is a bytestring. See the content property methods. - self.content = content - self.cookies = SimpleCookie() - if status: - self.status_code = status - - self['Content-Type'] = content_type - - def serialize(self): - """Full HTTP message, including headers, as a bytestring.""" - headers = [ - ('%s: %s' % (key, value)).encode('us-ascii') - for key, value in self._headers.values() - ] - return b'\r\n'.join(headers) + b'\r\n\r\n' + self.content - - if six.PY3: - __bytes__ = serialize - else: - __str__ = serialize - - def _convert_to_charset(self, value, charset, mime_encode=False): - """Converts headers key/value to ascii/latin1 native strings. - - `charset` must be 'ascii' or 'latin-1'. If `mime_encode` is True and - `value` value can't be represented in the given charset, MIME-encoding - is applied. - """ - if not isinstance(value, (bytes, six.text_type)): - value = str(value) - try: - if six.PY3: - if isinstance(value, str): - # Ensure string is valid in given charset - value.encode(charset) - else: - # Convert bytestring using given charset - value = value.decode(charset) - else: - if isinstance(value, str): - # Ensure string is valid in given charset - value.decode(charset) - else: - # Convert unicode string to given charset - value = value.encode(charset) - except UnicodeError as e: - if mime_encode: - # Wrapping in str() is a workaround for #12422 under Python 2. - value = str(Header(value, 'utf-8').encode()) - else: - e.reason += ', HTTP response headers must be in %s format' % charset - raise - if str('\n') in value or str('\r') in value: - raise BadHeaderError("Header values can't contain newlines (got %r)" % value) - return value - - def __setitem__(self, header, value): - header = self._convert_to_charset(header, 'ascii') - value = self._convert_to_charset(value, 'latin1', mime_encode=True) - self._headers[header.lower()] = (header, value) - - def __delitem__(self, header): - try: - del self._headers[header.lower()] - except KeyError: - pass - - def __getitem__(self, header): - return self._headers[header.lower()][1] - - def __getstate__(self): - # SimpleCookie is not pickeable with pickle.HIGHEST_PROTOCOL, so we - # serialise to a string instead - state = self.__dict__.copy() - state['cookies'] = str(state['cookies']) - return state - - def __setstate__(self, state): - self.__dict__.update(state) - self.cookies = SimpleCookie(self.cookies) - - def has_header(self, header): - """Case-insensitive check for a header.""" - return header.lower() in self._headers - - __contains__ = has_header - - def items(self): - return self._headers.values() - - def get(self, header, alternate=None): - return self._headers.get(header.lower(), (None, alternate))[1] - - def set_cookie(self, key, value='', max_age=None, expires=None, path='/', - domain=None, secure=False, httponly=False): - """ - Sets a cookie. - - ``expires`` can be: - - a string in the correct format, - - a naive ``datetime.datetime`` object in UTC, - - an aware ``datetime.datetime`` object in any time zone. - If it is a ``datetime.datetime`` object then ``max_age`` will be calculated. - - """ - self.cookies[key] = value - if expires is not None: - if isinstance(expires, datetime.datetime): - if timezone.is_aware(expires): - expires = timezone.make_naive(expires, timezone.utc) - delta = expires - expires.utcnow() - # Add one second so the date matches exactly (a fraction of - # time gets lost between converting to a timedelta and - # then the date string). - delta = delta + datetime.timedelta(seconds=1) - # Just set max_age - the max_age logic will set expires. - expires = None - max_age = max(0, delta.days * 86400 + delta.seconds) - else: - self.cookies[key]['expires'] = expires - if max_age is not None: - self.cookies[key]['max-age'] = max_age - # IE requires expires, so set it if hasn't been already. - if not expires: - self.cookies[key]['expires'] = cookie_date(time.time() + - max_age) - if path is not None: - self.cookies[key]['path'] = path - if domain is not None: - self.cookies[key]['domain'] = domain - if secure: - self.cookies[key]['secure'] = True - if httponly: - self.cookies[key]['httponly'] = True - - def set_signed_cookie(self, key, value, salt='', **kwargs): - value = signing.get_cookie_signer(salt=key + salt).sign(value) - return self.set_cookie(key, value, **kwargs) - - def delete_cookie(self, key, path='/', domain=None): - self.set_cookie(key, max_age=0, path=path, domain=domain, - expires='Thu, 01-Jan-1970 00:00:00 GMT') - - @property - def content(self): - if self.has_header('Content-Encoding'): - def make_bytes(value): - if isinstance(value, int): - value = six.text_type(value) - if isinstance(value, six.text_type): - value = value.encode('ascii') - # force conversion to bytes in case chunk is a subclass - return bytes(value) - return b''.join(make_bytes(e) for e in self._container) - return b''.join(force_bytes(e, self._charset) for e in self._container) - - @content.setter - def content(self, value): - if hasattr(value, '__iter__') and not isinstance(value, (bytes, six.string_types)): - self._container = value - self._base_content_is_iter = True - else: - self._container = [value] - self._base_content_is_iter = False - - def __iter__(self): - self._iterator = iter(self._container) - return self - - def __next__(self): - chunk = next(self._iterator) - if isinstance(chunk, int): - chunk = six.text_type(chunk) - if isinstance(chunk, six.text_type): - chunk = chunk.encode(self._charset) - # force conversion to bytes in case chunk is a subclass - return bytes(chunk) - - next = __next__ # Python 2 compatibility - - def close(self): - if hasattr(self._container, 'close'): - self._container.close() - - # The remaining methods partially implement the file-like object interface. - # See http://docs.python.org/lib/bltin-file-objects.html - def write(self, content): - if self._base_content_is_iter: - raise Exception("This %s instance is not writable" % self.__class__) - self._container.append(content) - - def flush(self): - pass - - def tell(self): - if self._base_content_is_iter: - raise Exception("This %s instance cannot tell its position" % self.__class__) - return sum([len(chunk) for chunk in self]) - -class HttpResponseRedirectBase(HttpResponse): - allowed_schemes = ['http', 'https', 'ftp'] - - def __init__(self, redirect_to, *args, **kwargs): - parsed = urlparse(redirect_to) - if parsed.scheme and parsed.scheme not in self.allowed_schemes: - raise SuspiciousOperation("Unsafe redirect to URL with protocol '%s'" % parsed.scheme) - super(HttpResponseRedirectBase, self).__init__(*args, **kwargs) - self['Location'] = iri_to_uri(redirect_to) - -class HttpResponseRedirect(HttpResponseRedirectBase): - status_code = 302 - -class HttpResponsePermanentRedirect(HttpResponseRedirectBase): - status_code = 301 - -class HttpResponseNotModified(HttpResponse): - status_code = 304 - - def __init__(self, *args, **kwargs): - super(HttpResponseNotModified, self).__init__(*args, **kwargs) - del self['content-type'] - - @HttpResponse.content.setter - def content(self, value): - if value: - raise AttributeError("You cannot set content to a 304 (Not Modified) response") - self._container = [] - -class HttpResponseBadRequest(HttpResponse): - status_code = 400 - -class HttpResponseNotFound(HttpResponse): - status_code = 404 - -class HttpResponseForbidden(HttpResponse): - status_code = 403 - -class HttpResponseNotAllowed(HttpResponse): - status_code = 405 - - def __init__(self, permitted_methods, *args, **kwargs): - super(HttpResponseNotAllowed, self).__init__(*args, **kwargs) - self['Allow'] = ', '.join(permitted_methods) - -class HttpResponseGone(HttpResponse): - status_code = 410 - -class HttpResponseServerError(HttpResponse): - status_code = 500 - -# A backwards compatible alias for HttpRequest.get_host. -def get_host(request): - return request.get_host() - -# It's neither necessary nor appropriate to use -# django.utils.encoding.smart_text for parsing URLs and form inputs. Thus, -# this slightly more restricted function, used by QueryDict. -def bytes_to_text(s, encoding): - """ - Converts basestring objects to unicode, using the given encoding. Illegally - encoded input characters are replaced with Unicode "unknown" codepoint - (\ufffd). - - Returns any non-basestring objects without change. - """ - if isinstance(s, bytes): - return six.text_type(s, encoding, 'replace') - else: - return s - +from django.http.cookie import SimpleCookie, parse_cookie +from django.http.request import (HttpRequest, QueryDict, UnreadablePostError, + build_request_repr) +from django.http.response import (HttpResponse, StreamingHttpResponse, + CompatibleStreamingHttpResponse, HttpResponsePermanentRedirect, + HttpResponseRedirect, HttpResponseNotModified, HttpResponseBadRequest, + HttpResponseForbidden, HttpResponseNotFound, HttpResponseNotAllowed, + HttpResponseGone, HttpResponseServerError, Http404, BadHeaderError) +from django.http.utils import (fix_location_header, conditional_content_removal, + fix_IE_for_attach, fix_IE_for_vary) diff --git a/django/http/cookie.py b/django/http/cookie.py new file mode 100644 index 0000000000..78adb09ce8 --- /dev/null +++ b/django/http/cookie.py @@ -0,0 +1,83 @@ +from __future__ import absolute_import, unicode_literals + +from django.utils.encoding import force_str +from django.utils.six.moves import http_cookies + + +# Some versions of Python 2.7 and later won't need this encoding bug fix: +_cookie_encodes_correctly = http_cookies.SimpleCookie().value_encode(';') == (';', '"\\073"') +# See ticket #13007, http://bugs.python.org/issue2193 and http://trac.edgewall.org/ticket/2256 +_tc = http_cookies.SimpleCookie() +try: + _tc.load(str('foo:bar=1')) + _cookie_allows_colon_in_names = True +except http_cookies.CookieError: + _cookie_allows_colon_in_names = False + +if _cookie_encodes_correctly and _cookie_allows_colon_in_names: + SimpleCookie = http_cookies.SimpleCookie +else: + Morsel = http_cookies.Morsel + + class SimpleCookie(http_cookies.SimpleCookie): + if not _cookie_encodes_correctly: + def value_encode(self, val): + # Some browsers do not support quoted-string from RFC 2109, + # including some versions of Safari and Internet Explorer. + # These browsers split on ';', and some versions of Safari + # are known to split on ', '. Therefore, we encode ';' and ',' + + # SimpleCookie already does the hard work of encoding and decoding. + # It uses octal sequences like '\\012' for newline etc. + # and non-ASCII chars. We just make use of this mechanism, to + # avoid introducing two encoding schemes which would be confusing + # and especially awkward for javascript. + + # NB, contrary to Python docs, value_encode returns a tuple containing + # (real val, encoded_val) + val, encoded = super(SimpleCookie, self).value_encode(val) + + encoded = encoded.replace(";", "\\073").replace(",","\\054") + # If encoded now contains any quoted chars, we need double quotes + # around the whole string. + if "\\" in encoded and not encoded.startswith('"'): + encoded = '"' + encoded + '"' + + return val, encoded + + if not _cookie_allows_colon_in_names: + def load(self, rawdata): + self.bad_cookies = set() + super(SimpleCookie, self).load(force_str(rawdata)) + for key in self.bad_cookies: + del self[key] + + # override private __set() method: + # (needed for using our Morsel, and for laxness with CookieError + def _BaseCookie__set(self, key, real_value, coded_value): + key = force_str(key) + try: + M = self.get(key, Morsel()) + M.set(key, real_value, coded_value) + dict.__setitem__(self, key, M) + except http_cookies.CookieError: + self.bad_cookies.add(key) + dict.__setitem__(self, key, http_cookies.Morsel()) + + +def parse_cookie(cookie): + if cookie == '': + return {} + if not isinstance(cookie, http_cookies.BaseCookie): + try: + c = SimpleCookie() + c.load(cookie) + except http_cookies.CookieError: + # Invalid cookie + return {} + else: + c = cookie + cookiedict = {} + for key in c.keys(): + cookiedict[key] = c.get(key).value + return cookiedict diff --git a/django/http/request.py b/django/http/request.py new file mode 100644 index 0000000000..96c7606c86 --- /dev/null +++ b/django/http/request.py @@ -0,0 +1,453 @@ +from __future__ import absolute_import, unicode_literals + +import copy +import os +import re +import sys +import warnings +from io import BytesIO +from pprint import pformat +try: + from urllib.parse import parse_qsl, urlencode, quote, urljoin +except ImportError: + from urllib import urlencode, quote + from urlparse import parse_qsl, urljoin + +from django.conf import settings +from django.core import signing +from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured +from django.core.files import uploadhandler +from django.http.multipartparser import MultiPartParser +from django.utils import six +from django.utils.datastructures import MultiValueDict, ImmutableList +from django.utils.encoding import force_bytes, force_text, force_str, iri_to_uri + + +RAISE_ERROR = object() +absolute_http_url_re = re.compile(r"^https?://", re.I) + + +class UnreadablePostError(IOError): + pass + + +class HttpRequest(object): + """A basic HTTP request.""" + + # The encoding used in GET/POST dicts. None means use default setting. + _encoding = None + _upload_handlers = [] + + def __init__(self): + self.GET, self.POST, self.COOKIES, self.META, self.FILES = {}, {}, {}, {}, {} + self.path = '' + self.path_info = '' + self.method = None + self._post_parse_error = False + + def __repr__(self): + return build_request_repr(self) + + def get_host(self): + """Returns the HTTP host using the environment or request headers.""" + # We try three options, in order of decreasing preference. + if settings.USE_X_FORWARDED_HOST and ( + 'HTTP_X_FORWARDED_HOST' in self.META): + host = self.META['HTTP_X_FORWARDED_HOST'] + elif 'HTTP_HOST' in self.META: + host = self.META['HTTP_HOST'] + else: + # Reconstruct the host using the algorithm from PEP 333. + host = self.META['SERVER_NAME'] + server_port = str(self.META['SERVER_PORT']) + if server_port != ('443' if self.is_secure() else '80'): + host = '%s:%s' % (host, server_port) + + # Disallow potentially poisoned hostnames. + if set(';/?@&=+$,').intersection(host): + raise SuspiciousOperation('Invalid HTTP_HOST header: %s' % host) + + return host + + def get_full_path(self): + # RFC 3986 requires query string arguments to be in the ASCII range. + # Rather than crash if this doesn't happen, we encode defensively. + return '%s%s' % (self.path, ('?' + iri_to_uri(self.META.get('QUERY_STRING', ''))) if self.META.get('QUERY_STRING', '') else '') + + def get_signed_cookie(self, key, default=RAISE_ERROR, salt='', max_age=None): + """ + Attempts to return a signed cookie. If the signature fails or the + cookie has expired, raises an exception... unless you provide the + default argument in which case that value will be returned instead. + """ + try: + cookie_value = self.COOKIES[key] + except KeyError: + if default is not RAISE_ERROR: + return default + else: + raise + try: + value = signing.get_cookie_signer(salt=key + salt).unsign( + cookie_value, max_age=max_age) + except signing.BadSignature: + if default is not RAISE_ERROR: + return default + else: + raise + return value + + def build_absolute_uri(self, location=None): + """ + Builds an absolute URI from the location and the variables available in + this request. If no location is specified, the absolute URI is built on + ``request.get_full_path()``. + """ + if not location: + location = self.get_full_path() + if not absolute_http_url_re.match(location): + current_uri = '%s://%s%s' % ('https' if self.is_secure() else 'http', + self.get_host(), self.path) + location = urljoin(current_uri, location) + return iri_to_uri(location) + + def _is_secure(self): + return os.environ.get("HTTPS") == "on" + + def is_secure(self): + # First, check the SECURE_PROXY_SSL_HEADER setting. + if settings.SECURE_PROXY_SSL_HEADER: + try: + header, value = settings.SECURE_PROXY_SSL_HEADER + except ValueError: + raise ImproperlyConfigured('The SECURE_PROXY_SSL_HEADER setting must be a tuple containing two values.') + if self.META.get(header, None) == value: + return True + + # Failing that, fall back to _is_secure(), which is a hook for + # subclasses to implement. + return self._is_secure() + + def is_ajax(self): + return self.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest' + + @property + def encoding(self): + return self._encoding + + @encoding.setter + def encoding(self, val): + """ + Sets the encoding used for GET/POST accesses. If the GET or POST + dictionary has already been created, it is removed and recreated on the + next access (so that it is decoded correctly). + """ + self._encoding = val + if hasattr(self, '_get'): + del self._get + if hasattr(self, '_post'): + del self._post + + def _initialize_handlers(self): + self._upload_handlers = [uploadhandler.load_handler(handler, self) + for handler in settings.FILE_UPLOAD_HANDLERS] + + @property + def upload_handlers(self): + if not self._upload_handlers: + # If there are no upload handlers defined, initialize them from settings. + self._initialize_handlers() + return self._upload_handlers + + @upload_handlers.setter + def upload_handlers(self, upload_handlers): + if hasattr(self, '_files'): + raise AttributeError("You cannot set the upload handlers after the upload has been processed.") + self._upload_handlers = upload_handlers + + def parse_file_upload(self, META, post_data): + """Returns a tuple of (POST QueryDict, FILES MultiValueDict).""" + self.upload_handlers = ImmutableList( + self.upload_handlers, + warning="You cannot alter upload handlers after the upload has been processed." + ) + parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding) + return parser.parse() + + @property + def body(self): + if not hasattr(self, '_body'): + if self._read_started: + raise Exception("You cannot access body after reading from request's data stream") + try: + self._body = self.read() + except IOError as e: + six.reraise(UnreadablePostError, UnreadablePostError(*e.args), sys.exc_info()[2]) + self._stream = BytesIO(self._body) + return self._body + + @property + def raw_post_data(self): + warnings.warn('HttpRequest.raw_post_data has been deprecated. Use HttpRequest.body instead.', DeprecationWarning) + return self.body + + def _mark_post_parse_error(self): + self._post = QueryDict('') + self._files = MultiValueDict() + self._post_parse_error = True + + def _load_post_and_files(self): + """Populate self._post and self._files if the content-type is a form type""" + if self.method != 'POST': + self._post, self._files = QueryDict('', encoding=self._encoding), MultiValueDict() + return + if self._read_started and not hasattr(self, '_body'): + self._mark_post_parse_error() + return + + if self.META.get('CONTENT_TYPE', '').startswith('multipart/form-data'): + if hasattr(self, '_body'): + # Use already read data + data = BytesIO(self._body) + else: + data = self + try: + self._post, self._files = self.parse_file_upload(self.META, data) + except: + # An error occured while parsing POST data. Since when + # formatting the error the request handler might access + # self.POST, set self._post and self._file to prevent + # attempts to parse POST data again. + # Mark that an error occured. This allows self.__repr__ to + # be explicit about it instead of simply representing an + # empty POST + self._mark_post_parse_error() + raise + elif self.META.get('CONTENT_TYPE', '').startswith('application/x-www-form-urlencoded'): + self._post, self._files = QueryDict(self.body, encoding=self._encoding), MultiValueDict() + else: + self._post, self._files = QueryDict('', encoding=self._encoding), MultiValueDict() + + ## File-like and iterator interface. + ## + ## Expects self._stream to be set to an appropriate source of bytes by + ## a corresponding request subclass (e.g. WSGIRequest). + ## Also when request data has already been read by request.POST or + ## request.body, self._stream points to a BytesIO instance + ## containing that data. + + def read(self, *args, **kwargs): + self._read_started = True + return self._stream.read(*args, **kwargs) + + def readline(self, *args, **kwargs): + self._read_started = True + return self._stream.readline(*args, **kwargs) + + def xreadlines(self): + while True: + buf = self.readline() + if not buf: + break + yield buf + + __iter__ = xreadlines + + def readlines(self): + return list(iter(self)) + + +class QueryDict(MultiValueDict): + """ + A specialized MultiValueDict that takes a query string when initialized. + This is immutable unless you create a copy of it. + + Values retrieved from this class are converted from the given encoding + (DEFAULT_CHARSET by default) to unicode. + """ + # These are both reset in __init__, but is specified here at the class + # level so that unpickling will have valid values + _mutable = True + _encoding = None + + def __init__(self, query_string, mutable=False, encoding=None): + super(QueryDict, self).__init__() + if not encoding: + encoding = settings.DEFAULT_CHARSET + self.encoding = encoding + if six.PY3: + for key, value in parse_qsl(query_string or '', + keep_blank_values=True, + encoding=encoding): + self.appendlist(key, value) + else: + for key, value in parse_qsl(query_string or '', + keep_blank_values=True): + self.appendlist(force_text(key, encoding, errors='replace'), + force_text(value, encoding, errors='replace')) + self._mutable = mutable + + @property + def encoding(self): + if self._encoding is None: + self._encoding = settings.DEFAULT_CHARSET + return self._encoding + + @encoding.setter + def encoding(self, value): + self._encoding = value + + def _assert_mutable(self): + if not self._mutable: + raise AttributeError("This QueryDict instance is immutable") + + def __setitem__(self, key, value): + self._assert_mutable() + key = bytes_to_text(key, self.encoding) + value = bytes_to_text(value, self.encoding) + super(QueryDict, self).__setitem__(key, value) + + def __delitem__(self, key): + self._assert_mutable() + super(QueryDict, self).__delitem__(key) + + def __copy__(self): + result = self.__class__('', mutable=True, encoding=self.encoding) + for key, value in six.iterlists(self): + result.setlist(key, value) + return result + + def __deepcopy__(self, memo): + result = self.__class__('', mutable=True, encoding=self.encoding) + memo[id(self)] = result + for key, value in six.iterlists(self): + result.setlist(copy.deepcopy(key, memo), copy.deepcopy(value, memo)) + return result + + def setlist(self, key, list_): + self._assert_mutable() + key = bytes_to_text(key, self.encoding) + list_ = [bytes_to_text(elt, self.encoding) for elt in list_] + super(QueryDict, self).setlist(key, list_) + + def setlistdefault(self, key, default_list=None): + self._assert_mutable() + return super(QueryDict, self).setlistdefault(key, default_list) + + def appendlist(self, key, value): + self._assert_mutable() + key = bytes_to_text(key, self.encoding) + value = bytes_to_text(value, self.encoding) + super(QueryDict, self).appendlist(key, value) + + def pop(self, key, *args): + self._assert_mutable() + return super(QueryDict, self).pop(key, *args) + + def popitem(self): + self._assert_mutable() + return super(QueryDict, self).popitem() + + def clear(self): + self._assert_mutable() + super(QueryDict, self).clear() + + def setdefault(self, key, default=None): + self._assert_mutable() + key = bytes_to_text(key, self.encoding) + default = bytes_to_text(default, self.encoding) + return super(QueryDict, self).setdefault(key, default) + + def copy(self): + """Returns a mutable copy of this object.""" + return self.__deepcopy__({}) + + def urlencode(self, safe=None): + """ + Returns an encoded string of all query string arguments. + + :arg safe: Used to specify characters which do not require quoting, for + example:: + + >>> q = QueryDict('', mutable=True) + >>> q['next'] = '/a&b/' + >>> q.urlencode() + 'next=%2Fa%26b%2F' + >>> q.urlencode(safe='/') + 'next=/a%26b/' + + """ + output = [] + if safe: + safe = force_bytes(safe, self.encoding) + encode = lambda k, v: '%s=%s' % ((quote(k, safe), quote(v, safe))) + else: + encode = lambda k, v: urlencode({k: v}) + for k, list_ in self.lists(): + k = force_bytes(k, self.encoding) + output.extend([encode(k, force_bytes(v, self.encoding)) + for v in list_]) + return '&'.join(output) + + +def build_request_repr(request, path_override=None, GET_override=None, + POST_override=None, COOKIES_override=None, + META_override=None): + """ + Builds and returns the request's representation string. The request's + attributes may be overridden by pre-processed values. + """ + # Since this is called as part of error handling, we need to be very + # robust against potentially malformed input. + try: + get = (pformat(GET_override) + if GET_override is not None + else pformat(request.GET)) + except Exception: + get = '' + if request._post_parse_error: + post = '' + else: + try: + post = (pformat(POST_override) + if POST_override is not None + else pformat(request.POST)) + except Exception: + post = '' + try: + cookies = (pformat(COOKIES_override) + if COOKIES_override is not None + else pformat(request.COOKIES)) + except Exception: + cookies = '' + try: + meta = (pformat(META_override) + if META_override is not None + else pformat(request.META)) + except Exception: + meta = '' + path = path_override if path_override is not None else request.path + return force_str('<%s\npath:%s,\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s>' % + (request.__class__.__name__, + path, + six.text_type(get), + six.text_type(post), + six.text_type(cookies), + six.text_type(meta))) + + +# It's neither necessary nor appropriate to use +# django.utils.encoding.smart_text for parsing URLs and form inputs. Thus, +# this slightly more restricted function, used by QueryDict. +def bytes_to_text(s, encoding): + """ + Converts basestring objects to unicode, using the given encoding. Illegally + encoded input characters are replaced with Unicode "unknown" codepoint + (\ufffd). + + Returns any non-basestring objects without change. + """ + if isinstance(s, bytes): + return six.text_type(s, encoding, 'replace') + else: + return s diff --git a/django/http/response.py b/django/http/response.py new file mode 100644 index 0000000000..56e3d00096 --- /dev/null +++ b/django/http/response.py @@ -0,0 +1,441 @@ +from __future__ import absolute_import, unicode_literals + +import datetime +import time +import warnings +from email.header import Header +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse + +from django.conf import settings +from django.core import signing +from django.core.exceptions import SuspiciousOperation +from django.http.cookie import SimpleCookie +from django.utils import six, timezone +from django.utils.encoding import force_bytes, iri_to_uri +from django.utils.http import cookie_date +from django.utils.six.moves import map + + +class BadHeaderError(ValueError): + pass + + +class HttpResponseBase(object): + """ + An HTTP response base class with dictionary-accessed headers. + + This class doesn't handle content. It should not be used directly. + Use the HttpResponse and StreamingHttpResponse subclasses instead. + """ + + status_code = 200 + + def __init__(self, content_type=None, status=None, mimetype=None): + # _headers is a mapping of the lower-case name to the original case of + # the header (required for working with legacy systems) and the header + # value. Both the name of the header and its value are ASCII strings. + self._headers = {} + self._charset = settings.DEFAULT_CHARSET + self._closable_objects = [] + if mimetype: + warnings.warn("Using mimetype keyword argument is deprecated, use" + " content_type instead", PendingDeprecationWarning) + content_type = mimetype + if not content_type: + content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE, + self._charset) + self.cookies = SimpleCookie() + if status: + self.status_code = status + + self['Content-Type'] = content_type + + def serialize_headers(self): + """HTTP headers as a bytestring.""" + headers = [ + ('%s: %s' % (key, value)).encode('us-ascii') + for key, value in self._headers.values() + ] + return b'\r\n'.join(headers) + + if six.PY3: + __bytes__ = serialize_headers + else: + __str__ = serialize_headers + + def _convert_to_charset(self, value, charset, mime_encode=False): + """Converts headers key/value to ascii/latin1 native strings. + + `charset` must be 'ascii' or 'latin-1'. If `mime_encode` is True and + `value` value can't be represented in the given charset, MIME-encoding + is applied. + """ + if not isinstance(value, (bytes, six.text_type)): + value = str(value) + try: + if six.PY3: + if isinstance(value, str): + # Ensure string is valid in given charset + value.encode(charset) + else: + # Convert bytestring using given charset + value = value.decode(charset) + else: + if isinstance(value, str): + # Ensure string is valid in given charset + value.decode(charset) + else: + # Convert unicode string to given charset + value = value.encode(charset) + except UnicodeError as e: + if mime_encode: + # Wrapping in str() is a workaround for #12422 under Python 2. + value = str(Header(value, 'utf-8').encode()) + else: + e.reason += ', HTTP response headers must be in %s format' % charset + raise + if str('\n') in value or str('\r') in value: + raise BadHeaderError("Header values can't contain newlines (got %r)" % value) + return value + + def __setitem__(self, header, value): + header = self._convert_to_charset(header, 'ascii') + value = self._convert_to_charset(value, 'latin1', mime_encode=True) + self._headers[header.lower()] = (header, value) + + def __delitem__(self, header): + try: + del self._headers[header.lower()] + except KeyError: + pass + + def __getitem__(self, header): + return self._headers[header.lower()][1] + + def __getstate__(self): + # SimpleCookie is not pickeable with pickle.HIGHEST_PROTOCOL, so we + # serialise to a string instead + state = self.__dict__.copy() + state['cookies'] = str(state['cookies']) + return state + + def __setstate__(self, state): + self.__dict__.update(state) + self.cookies = SimpleCookie(self.cookies) + + def has_header(self, header): + """Case-insensitive check for a header.""" + return header.lower() in self._headers + + __contains__ = has_header + + def items(self): + return self._headers.values() + + def get(self, header, alternate=None): + return self._headers.get(header.lower(), (None, alternate))[1] + + def set_cookie(self, key, value='', max_age=None, expires=None, path='/', + domain=None, secure=False, httponly=False): + """ + Sets a cookie. + + ``expires`` can be: + - a string in the correct format, + - a naive ``datetime.datetime`` object in UTC, + - an aware ``datetime.datetime`` object in any time zone. + If it is a ``datetime.datetime`` object then ``max_age`` will be calculated. + + """ + self.cookies[key] = value + if expires is not None: + if isinstance(expires, datetime.datetime): + if timezone.is_aware(expires): + expires = timezone.make_naive(expires, timezone.utc) + delta = expires - expires.utcnow() + # Add one second so the date matches exactly (a fraction of + # time gets lost between converting to a timedelta and + # then the date string). + delta = delta + datetime.timedelta(seconds=1) + # Just set max_age - the max_age logic will set expires. + expires = None + max_age = max(0, delta.days * 86400 + delta.seconds) + else: + self.cookies[key]['expires'] = expires + if max_age is not None: + self.cookies[key]['max-age'] = max_age + # IE requires expires, so set it if hasn't been already. + if not expires: + self.cookies[key]['expires'] = cookie_date(time.time() + + max_age) + if path is not None: + self.cookies[key]['path'] = path + if domain is not None: + self.cookies[key]['domain'] = domain + if secure: + self.cookies[key]['secure'] = True + if httponly: + self.cookies[key]['httponly'] = True + + def set_signed_cookie(self, key, value, salt='', **kwargs): + value = signing.get_cookie_signer(salt=key + salt).sign(value) + return self.set_cookie(key, value, **kwargs) + + def delete_cookie(self, key, path='/', domain=None): + self.set_cookie(key, max_age=0, path=path, domain=domain, + expires='Thu, 01-Jan-1970 00:00:00 GMT') + + # Common methods used by subclasses + + def make_bytes(self, value): + """Turn a value into a bytestring encoded in the output charset.""" + # Per PEP 3333, this response body must be bytes. To avoid returning + # an instance of a subclass, this function returns `bytes(value)`. + # This doesn't make a copy when `value` already contains bytes. + + # If content is already encoded (eg. gzip), assume bytes. + if self.has_header('Content-Encoding'): + return bytes(value) + + # Handle string types -- we can't rely on force_bytes here because: + # - under Python 3 it attemps str conversion first + # - when self._charset != 'utf-8' it re-encodes the content + if isinstance(value, bytes): + return bytes(value) + if isinstance(value, six.text_type): + return bytes(value.encode(self._charset)) + + # Handle non-string types (#16494) + return force_bytes(value, self._charset) + + def __iter__(self): + return self + + def __next__(self): + # Subclasses must define self._iterator for this function. + return self.make_bytes(next(self._iterator)) + + next = __next__ # Python 2 compatibility + + # These methods partially implement the file-like object interface. + # See http://docs.python.org/lib/bltin-file-objects.html + + # The WSGI server must call this method upon completion of the request. + # See http://blog.dscpl.com.au/2012/10/obligations-for-calling-close-on.html + def close(self): + for closable in self._closable_objects: + closable.close() + + def write(self, content): + raise Exception("This %s instance is not writable" % self.__class__.__name__) + + def flush(self): + pass + + def tell(self): + raise Exception("This %s instance cannot tell its position" % self.__class__.__name__) + + +class HttpResponse(HttpResponseBase): + """ + An HTTP response class with a string as content. + + This content that can be read, appended to or replaced. + """ + + streaming = False + + def __init__(self, content='', *args, **kwargs): + super(HttpResponse, self).__init__(*args, **kwargs) + # Content is a bytestring. See the `content` property methods. + self.content = content + + def serialize(self): + """Full HTTP message, including headers, as a bytestring.""" + return self.serialize_headers() + b'\r\n\r\n' + self.content + + if six.PY3: + __bytes__ = serialize + else: + __str__ = serialize + + def _consume_content(self): + # If the response was instantiated with an iterator, when its content + # is accessed, the iterator is going be exhausted and the content + # loaded in memory. At this point, it's better to abandon the original + # iterator and save the content for later reuse. This is a temporary + # solution. See the comment in __iter__ below for the long term plan. + if self._base_content_is_iter: + self.content = b''.join(self.make_bytes(e) for e in self._container) + + @property + def content(self): + self._consume_content() + return b''.join(self.make_bytes(e) for e in self._container) + + @content.setter + def content(self, value): + if hasattr(value, '__iter__') and not isinstance(value, (bytes, six.string_types)): + self._container = value + self._base_content_is_iter = True + if hasattr(value, 'close'): + self._closable_objects.append(value) + else: + self._container = [value] + self._base_content_is_iter = False + + def __iter__(self): + # Raise a deprecation warning only if the content wasn't consumed yet, + # because the response may be intended to be streamed. + # Once the deprecation completes, iterators should be consumed upon + # assignment rather than upon access. The _consume_content method + # should be removed. See #6527. + if self._base_content_is_iter: + warnings.warn( + 'Creating streaming responses with `HttpResponse` is ' + 'deprecated. Use `StreamingHttpResponse` instead ' + 'if you need the streaming behavior.', + PendingDeprecationWarning, stacklevel=2) + if not hasattr(self, '_iterator'): + self._iterator = iter(self._container) + return self + + def write(self, content): + self._consume_content() + self._container.append(content) + + def tell(self): + self._consume_content() + return len(self.content) + + +class StreamingHttpResponse(HttpResponseBase): + """ + A streaming HTTP response class with an iterator as content. + + This should only be iterated once, when the response is streamed to the + client. However, it can be appended to or replaced with a new iterator + that wraps the original content (or yields entirely new content). + """ + + streaming = True + + def __init__(self, streaming_content=(), *args, **kwargs): + super(StreamingHttpResponse, self).__init__(*args, **kwargs) + # `streaming_content` should be an iterable of bytestrings. + # See the `streaming_content` property methods. + self.streaming_content = streaming_content + + @property + def content(self): + raise AttributeError("This %s instance has no `content` attribute. " + "Use `streaming_content` instead." % self.__class__.__name__) + + @property + def streaming_content(self): + return map(self.make_bytes, self._iterator) + + @streaming_content.setter + def streaming_content(self, value): + # Ensure we can never iterate on "value" more than once. + self._iterator = iter(value) + if hasattr(value, 'close'): + self._closable_objects.append(value) + + +class CompatibleStreamingHttpResponse(StreamingHttpResponse): + """ + This class maintains compatibility with middleware that doesn't know how + to handle the content of a streaming response by exposing a `content` + attribute that will consume and cache the content iterator when accessed. + + These responses will stream only if no middleware attempts to access the + `content` attribute. Otherwise, they will behave like a regular response, + and raise a `PendingDeprecationWarning`. + """ + @property + def content(self): + warnings.warn( + 'Accessing the `content` attribute on a streaming response is ' + 'deprecated. Use the `streaming_content` attribute instead.', + PendingDeprecationWarning) + content = b''.join(self) + self.streaming_content = [content] + return content + + @content.setter + def content(self, content): + warnings.warn( + 'Accessing the `content` attribute on a streaming response is ' + 'deprecated. Use the `streaming_content` attribute instead.', + PendingDeprecationWarning) + self.streaming_content = [content] + + +class HttpResponseRedirectBase(HttpResponse): + allowed_schemes = ['http', 'https', 'ftp'] + + def __init__(self, redirect_to, *args, **kwargs): + parsed = urlparse(redirect_to) + if parsed.scheme and parsed.scheme not in self.allowed_schemes: + raise SuspiciousOperation("Unsafe redirect to URL with protocol '%s'" % parsed.scheme) + super(HttpResponseRedirectBase, self).__init__(*args, **kwargs) + self['Location'] = iri_to_uri(redirect_to) + + +class HttpResponseRedirect(HttpResponseRedirectBase): + status_code = 302 + + +class HttpResponsePermanentRedirect(HttpResponseRedirectBase): + status_code = 301 + + +class HttpResponseNotModified(HttpResponse): + status_code = 304 + + def __init__(self, *args, **kwargs): + super(HttpResponseNotModified, self).__init__(*args, **kwargs) + del self['content-type'] + + @HttpResponse.content.setter + def content(self, value): + if value: + raise AttributeError("You cannot set content to a 304 (Not Modified) response") + self._container = [] + self._base_content_is_iter = False + + +class HttpResponseBadRequest(HttpResponse): + status_code = 400 + + +class HttpResponseNotFound(HttpResponse): + status_code = 404 + + +class HttpResponseForbidden(HttpResponse): + status_code = 403 + + +class HttpResponseNotAllowed(HttpResponse): + status_code = 405 + + def __init__(self, permitted_methods, *args, **kwargs): + super(HttpResponseNotAllowed, self).__init__(*args, **kwargs) + self['Allow'] = ', '.join(permitted_methods) + + +class HttpResponseGone(HttpResponse): + status_code = 410 + + +class HttpResponseServerError(HttpResponse): + status_code = 500 + + +class Http404(Exception): + pass diff --git a/django/http/utils.py b/django/http/utils.py index 01808648ba..fcb3fecb6c 100644 --- a/django/http/utils.py +++ b/django/http/utils.py @@ -8,6 +8,7 @@ Functions that modify an HTTP request or response in some way. # it's a little fiddly to override this behavior, so they should be truly # universally applicable. + def fix_location_header(request, response): """ Ensures that we always use an absolute URI in any location header in the @@ -20,18 +21,26 @@ def fix_location_header(request, response): response['Location'] = request.build_absolute_uri(response['Location']) return response + def conditional_content_removal(request, response): """ Removes the content of responses for HEAD requests, 1xx, 204 and 304 responses. Ensures compliance with RFC 2616, section 4.3. """ if 100 <= response.status_code < 200 or response.status_code in (204, 304): - response.content = '' - response['Content-Length'] = 0 + if response.streaming: + response.streaming_content = [] + else: + response.content = '' + response['Content-Length'] = '0' if request.method == 'HEAD': - response.content = '' + if response.streaming: + response.streaming_content = [] + else: + response.content = '' return response + def fix_IE_for_attach(request, response): """ This function will prevent Django from serving a Content-Disposition header @@ -60,6 +69,7 @@ def fix_IE_for_attach(request, response): return response + def fix_IE_for_vary(request, response): """ This function will fix the bug reported at @@ -84,4 +94,3 @@ def fix_IE_for_vary(request, response): pass return response - diff --git a/django/middleware/common.py b/django/middleware/common.py index bb24977ce8..6fbbf43044 100644 --- a/django/middleware/common.py +++ b/django/middleware/common.py @@ -1,4 +1,5 @@ import hashlib +import logging import re from django.conf import settings @@ -6,9 +7,9 @@ from django import http from django.core.mail import mail_managers from django.utils.http import urlquote from django.core import urlresolvers -from django.utils.log import getLogger -logger = getLogger('django.request') + +logger = logging.getLogger('django.request') class CommonMiddleware(object): @@ -112,14 +113,18 @@ class CommonMiddleware(object): if settings.USE_ETAGS: if response.has_header('ETag'): etag = response['ETag'] + elif response.streaming: + etag = None else: etag = '"%s"' % hashlib.md5(response.content).hexdigest() - if response.status_code >= 200 and response.status_code < 300 and request.META.get('HTTP_IF_NONE_MATCH') == etag: - cookies = response.cookies - response = http.HttpResponseNotModified() - response.cookies = cookies - else: - response['ETag'] = etag + if etag is not None: + if (200 <= response.status_code < 300 + and request.META.get('HTTP_IF_NONE_MATCH') == etag): + cookies = response.cookies + response = http.HttpResponseNotModified() + response.cookies = cookies + else: + response['ETag'] = etag return response diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py index c9e8d73c82..b2eb0df3f5 100644 --- a/django/middleware/csrf.py +++ b/django/middleware/csrf.py @@ -7,6 +7,7 @@ against request forgeries from other sites. from __future__ import unicode_literals import hashlib +import logging import re import random @@ -15,10 +16,10 @@ from django.core.urlresolvers import get_callable from django.utils.cache import patch_vary_headers from django.utils.encoding import force_text from django.utils.http import same_origin -from django.utils.log import getLogger from django.utils.crypto import constant_time_compare, get_random_string -logger = getLogger('django.request') + +logger = logging.getLogger('django.request') REASON_NO_REFERER = "Referer checking failed - no Referer." REASON_BAD_REFERER = "Referer checking failed - %s does not match %s." diff --git a/django/middleware/gzip.py b/django/middleware/gzip.py index 69f938cf0a..fb54501a03 100644 --- a/django/middleware/gzip.py +++ b/django/middleware/gzip.py @@ -1,6 +1,6 @@ import re -from django.utils.text import compress_string +from django.utils.text import compress_sequence, compress_string from django.utils.cache import patch_vary_headers re_accepts_gzip = re.compile(r'\bgzip\b') @@ -13,7 +13,7 @@ class GZipMiddleware(object): """ def process_response(self, request, response): # It's not worth attempting to compress really short responses. - if len(response.content) < 200: + if not response.streaming and len(response.content) < 200: return response patch_vary_headers(response, ('Accept-Encoding',)) @@ -32,15 +32,21 @@ class GZipMiddleware(object): if not re_accepts_gzip.search(ae): return response - # Return the compressed content only if it's actually shorter. - compressed_content = compress_string(response.content) - if len(compressed_content) >= len(response.content): - return response + if response.streaming: + # Delete the `Content-Length` header for streaming content, because + # we won't know the compressed size until we stream it. + response.streaming_content = compress_sequence(response.streaming_content) + del response['Content-Length'] + else: + # Return the compressed content only if it's actually shorter. + compressed_content = compress_string(response.content) + if len(compressed_content) >= len(response.content): + return response + response.content = compressed_content + response['Content-Length'] = str(len(response.content)) if response.has_header('ETag'): response['ETag'] = re.sub('"$', ';gzip"', response['ETag']) - - response.content = compressed_content response['Content-Encoding'] = 'gzip' - response['Content-Length'] = str(len(response.content)) + return response diff --git a/django/middleware/http.py b/django/middleware/http.py index 86e46cea82..5a46e04946 100644 --- a/django/middleware/http.py +++ b/django/middleware/http.py @@ -10,7 +10,7 @@ class ConditionalGetMiddleware(object): """ def process_response(self, request, response): response['Date'] = http_date() - if not response.has_header('Content-Length'): + if not response.streaming and not response.has_header('Content-Length'): response['Content-Length'] = str(len(response.content)) if response.has_header('ETag'): diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index 1bfb627023..e15440f90e 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -96,7 +96,7 @@ def fix_ampersands_filter(value): # Values for testing floatformat input against infinity and NaN representations, # which differ across platforms and Python versions. Some (i.e. old Windows # ones) are not recognized by Decimal but we want to return them unchanged vs. -# returning an empty string as we do for completley invalid input. Note these +# returning an empty string as we do for completely invalid input. Note these # need to be built up from values that are not inf/nan, since inf/nan values do # not reload properly from .pyc files on Windows prior to some level of Python 2.5 # (see Python Issue757815 and Issue1080440). diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index ea1dd0281e..fa2e840cbf 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -48,7 +48,7 @@ class CsrfTokenNode(Node): if csrf_token == 'NOTPROVIDED': return format_html("") else: - return format_html("
", csrf_token) + return format_html("", csrf_token) else: # It's very probable that the token is missing because of # misconfiguration, so we raise a warning diff --git a/django/templatetags/i18n.py b/django/templatetags/i18n.py index 30eb6b5f76..5576beb4a3 100644 --- a/django/templatetags/i18n.py +++ b/django/templatetags/i18n.py @@ -110,13 +110,13 @@ class BlockTranslateNode(Node): vars = [] for token in tokens: if token.token_type == TOKEN_TEXT: - result.append(token.contents) + result.append(token.contents.replace('%', '%%')) elif token.token_type == TOKEN_VAR: result.append('%%(%s)s' % token.contents) vars.append(token.contents) return ''.join(result), vars - def render(self, context): + def render(self, context, nested=False): if self.message_context: message_context = self.message_context.resolve(context) else: @@ -128,13 +128,10 @@ class BlockTranslateNode(Node): # the end of function context.update(tmp_context) singular, vars = self.render_token_list(self.singular) - # Escape all isolated '%' - singular = re.sub('%(?!\()', '%%', singular) if self.plural and self.countervar and self.counter: count = self.counter.resolve(context) context[self.countervar] = count plural, plural_vars = self.render_token_list(self.plural) - plural = re.sub('%(?!\()', '%%', plural) if message_context: result = translation.npgettext(message_context, singular, plural, count) @@ -151,8 +148,12 @@ class BlockTranslateNode(Node): try: result = result % data except (KeyError, ValueError): + if nested: + # Either string is malformed, or it's a bug + raise TemplateSyntaxError("'blocktrans' is unable to format " + "string returned by gettext: %r using %r" % (result, data)) with translation.override(None): - result = self.render(context) + result = self.render(context, nested=True) return result diff --git a/django/test/__init__.py b/django/test/__init__.py index 21a4841a6b..7a4987508e 100644 --- a/django/test/__init__.py +++ b/django/test/__init__.py @@ -5,5 +5,6 @@ Django Unit Test and Doctest framework. from django.test.client import Client, RequestFactory from django.test.testcases import (TestCase, TransactionTestCase, SimpleTestCase, LiveServerTestCase, skipIfDBFeature, - skipUnlessDBFeature) + skipUnlessDBFeature +) from django.test.utils import Approximate diff --git a/django/test/client.py b/django/test/client.py index 8fd765ec9a..6d12321075 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -43,11 +43,20 @@ class FakePayload(object): length. This makes sure that views can't do anything under the test client that wouldn't work in Real Life. """ - def __init__(self, content): - self.__content = BytesIO(content) - self.__len = len(content) + def __init__(self, content=None): + self.__content = BytesIO() + self.__len = 0 + self.read_started = False + if content is not None: + self.write(content) + + def __len__(self): + return self.__len def read(self, num_bytes=None): + if not self.read_started: + self.__content.seek(0) + self.read_started = True if num_bytes is None: num_bytes = self.__len or 0 assert self.__len >= num_bytes, "Cannot read more than the available bytes from the HTTP incoming data." @@ -55,6 +64,13 @@ class FakePayload(object): self.__len -= num_bytes return content + def write(self, content): + if self.read_started: + raise ValueError("Unable to write a payload after he's been read") + content = force_bytes(content) + self.__content.write(content) + self.__len += len(content) + class ClientHandler(BaseHandler): """ diff --git a/django/test/testcases.py b/django/test/testcases.py index 3c681db329..1239275264 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -11,7 +11,6 @@ try: from urllib.parse import urlsplit, urlunsplit except ImportError: # Python 2 from urlparse import urlsplit, urlunsplit -from xml.dom.minidom import parseString, Node import select import socket import threading @@ -38,12 +37,13 @@ from django.test.client import Client from django.test.html import HTMLParseError, parse_html from django.test.signals import template_rendered from django.test.utils import (get_warnings_state, restore_warnings_state, - override_settings) + override_settings, compare_xml, strip_quotes) from django.test.utils import ContextList from django.utils import unittest as ut2 from django.utils.encoding import force_text from django.utils import six from django.utils.unittest.util import safe_repr +from django.utils.unittest import skipIf from django.views.static import serve __all__ = ('DocTestRunner', 'OutputChecker', 'TestCase', 'TransactionTestCase', @@ -53,6 +53,7 @@ normalize_long_ints = lambda s: re.sub(r'(?" - if not want.startswith('%s' - want = wrapper % want - got = wrapper % got - - # Parse the want and got strings, and compare the parsings. try: - want_root = parseString(want).firstChild - got_root = parseString(got).firstChild + return compare_xml(want, got) except Exception: return False - return check_element(want_root, got_root) def check_output_json(self, want, got, optionsflags): """ Tries to compare want and got as if they were JSON-encoded data """ - want, got = self._strip_quotes(want, got) + want, got = strip_quotes(want, got) try: want_json = json.loads(want) got_json = json.loads(got) @@ -203,37 +150,6 @@ class OutputChecker(doctest.OutputChecker): return False return want_json == got_json - def _strip_quotes(self, want, got): - """ - Strip quotes of doctests output values: - - >>> o = OutputChecker() - >>> o._strip_quotes("'foo'") - "foo" - >>> o._strip_quotes('"foo"') - "foo" - """ - def is_quoted_string(s): - s = s.strip() - return (len(s) >= 2 - and s[0] == s[-1] - and s[0] in ('"', "'")) - - def is_quoted_unicode(s): - s = s.strip() - return (len(s) >= 3 - and s[0] == 'u' - and s[1] == s[-1] - and s[1] in ('"', "'")) - - if is_quoted_string(want) and is_quoted_string(got): - want = want.strip()[1:-1] - got = got.strip()[1:-1] - elif is_quoted_unicode(want) and is_quoted_unicode(got): - want = want.strip()[2:-1] - got = got.strip()[2:-1] - return want, got - class DocTestRunner(doctest.DocTestRunner): def __init__(self, *args, **kwargs): @@ -443,6 +359,38 @@ class SimpleTestCase(ut2.TestCase): safe_repr(dom1, True), safe_repr(dom2, True)) self.fail(self._formatMessage(msg, standardMsg)) + def assertXMLEqual(self, xml1, xml2, msg=None): + """ + Asserts that two XML snippets are semantically the same. + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid XML. + """ + try: + result = compare_xml(xml1, xml2) + except Exception as e: + standardMsg = 'First or second argument is not valid XML\n%s' % e + self.fail(self._formatMessage(msg, standardMsg)) + else: + if not result: + standardMsg = '%s != %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) + self.fail(self._formatMessage(msg, standardMsg)) + + def assertXMLNotEqual(self, xml1, xml2, msg=None): + """ + Asserts that two XML snippets are not semantically equivalent. + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid XML. + """ + try: + result = compare_xml(xml1, xml2) + except Exception as e: + standardMsg = 'First or second argument is not valid XML\n%s' % e + self.fail(self._formatMessage(msg, standardMsg)) + else: + if result: + standardMsg = '%s == %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) + self.fail(self._formatMessage(msg, standardMsg)) + class TransactionTestCase(SimpleTestCase): @@ -647,7 +595,14 @@ class TransactionTestCase(SimpleTestCase): self.assertEqual(response.status_code, status_code, msg_prefix + "Couldn't retrieve content: Response code was %d" " (expected %d)" % (response.status_code, status_code)) - content = response.content.decode(response._charset) + text = force_text(text, encoding=response._charset) + if response.streaming: + content = b''.join(response.streaming_content) + else: + content = response.content + content = content.decode(response._charset) + # Avoid ResourceWarning about unclosed files. + response.close() if html: content = assert_and_parse_html(self, content, None, "Response's content is not valid HTML:") @@ -682,6 +637,7 @@ class TransactionTestCase(SimpleTestCase): self.assertEqual(response.status_code, status_code, msg_prefix + "Couldn't retrieve content: Response code was %d" " (expected %d)" % (response.status_code, status_code)) + text = force_text(text, encoding=response._charset) content = response.content.decode(response._charset) if html: content = assert_and_parse_html(self, content, None, @@ -917,7 +873,9 @@ class QuietWSGIRequestHandler(WSGIRequestHandler): pass -if sys.version_info >= (2, 7, 0): +if sys.version_info >= (3, 3, 0): + _ImprovedEvent = threading.Event +elif sys.version_info >= (2, 7, 0): _ImprovedEvent = threading._Event else: class _ImprovedEvent(threading._Event): diff --git a/django/test/utils.py b/django/test/utils.py index e2c439fd80..f10d388227 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -1,4 +1,7 @@ +import re import warnings +from xml.dom.minidom import parseString, Node + from django.conf import settings, UserSettingsHolder from django.core import mail from django.test.signals import template_rendered, setting_changed @@ -28,7 +31,7 @@ class Approximate(object): def __eq__(self, other): if self.val == other: return True - return round(abs(self.val-other), self.places) == 0 + return round(abs(self.val - other), self.places) == 0 class ContextList(list): @@ -46,7 +49,7 @@ class ContextList(list): def __contains__(self, key): try: - value = self[key] + self[key] except KeyError: return False return True @@ -188,9 +191,11 @@ class override_settings(object): if isinstance(test_func, type) and issubclass(test_func, TransactionTestCase): original_pre_setup = test_func._pre_setup original_post_teardown = test_func._post_teardown + def _pre_setup(innerself): self.enable() original_pre_setup(innerself) + def _post_teardown(innerself): original_post_teardown(innerself) self.disable() @@ -220,5 +225,101 @@ class override_settings(object): setting_changed.send(sender=settings._wrapped.__class__, setting=key, value=new_value) + +def compare_xml(want, got): + """Tries to do a 'xml-comparison' of want and got. Plain string + comparison doesn't always work because, for example, attribute + ordering should not be important. Comment nodes are not considered in the + comparison. + + Based on http://codespeak.net/svn/lxml/trunk/src/lxml/doctestcompare.py + """ + _norm_whitespace_re = re.compile(r'[ \t\n][ \t\n]+') + def norm_whitespace(v): + return _norm_whitespace_re.sub(' ', v) + + def child_text(element): + return ''.join([c.data for c in element.childNodes + if c.nodeType == Node.TEXT_NODE]) + + def children(element): + return [c for c in element.childNodes + if c.nodeType == Node.ELEMENT_NODE] + + def norm_child_text(element): + return norm_whitespace(child_text(element)) + + def attrs_dict(element): + return dict(element.attributes.items()) + + def check_element(want_element, got_element): + if want_element.tagName != got_element.tagName: + return False + if norm_child_text(want_element) != norm_child_text(got_element): + return False + if attrs_dict(want_element) != attrs_dict(got_element): + return False + want_children = children(want_element) + got_children = children(got_element) + if len(want_children) != len(got_children): + return False + for want, got in zip(want_children, got_children): + if not check_element(want, got): + return False + return True + + def first_node(document): + for node in document.childNodes: + if node.nodeType != Node.COMMENT_NODE: + return node + + want, got = strip_quotes(want, got) + want = want.replace('\\n','\n') + got = got.replace('\\n','\n') + + # If the string is not a complete xml document, we may need to add a + # root element. This allow us to compare fragments, like "" + if not want.startswith('%s' + want = wrapper % want + got = wrapper % got + + # Parse the want and got strings, and compare the parsings. + want_root = first_node(parseString(want)) + got_root = first_node(parseString(got)) + + return check_element(want_root, got_root) + + +def strip_quotes(want, got): + """ + Strip quotes of doctests output values: + + >>> strip_quotes("'foo'") + "foo" + >>> strip_quotes('"foo"') + "foo" + """ + def is_quoted_string(s): + s = s.strip() + return (len(s) >= 2 + and s[0] == s[-1] + and s[0] in ('"', "'")) + + def is_quoted_unicode(s): + s = s.strip() + return (len(s) >= 3 + and s[0] == 'u' + and s[1] == s[-1] + and s[1] in ('"', "'")) + + if is_quoted_string(want) and is_quoted_string(got): + want = want.strip()[1:-1] + got = got.strip()[1:-1] + elif is_quoted_unicode(want) and is_quoted_unicode(got): + want = want.strip()[2:-1] + got = got.strip()[2:-1] + return want, got + def str_prefix(s): return s % {'_': '' if six.PY3 else 'u'} diff --git a/django/utils/cache.py b/django/utils/cache.py index 91c4796988..0fceaa96e6 100644 --- a/django/utils/cache.py +++ b/django/utils/cache.py @@ -95,7 +95,8 @@ def get_max_age(response): pass def _set_response_etag(response): - response['ETag'] = '"%s"' % hashlib.md5(response.content).hexdigest() + if not response.streaming: + response['ETag'] = '"%s"' % hashlib.md5(response.content).hexdigest() return response def patch_response_headers(response, cache_timeout=None): diff --git a/django/utils/datastructures.py b/django/utils/datastructures.py index ad17573104..d94a05dfb4 100644 --- a/django/utils/datastructures.py +++ b/django/utils/datastructures.py @@ -160,6 +160,9 @@ class SortedDict(dict): def __iter__(self): return iter(self.keyOrder) + def __reversed__(self): + return reversed(self.keyOrder) + def pop(self, k, *args): result = super(SortedDict, self).pop(k, *args) try: diff --git a/django/utils/dateformat.py b/django/utils/dateformat.py index 6a91a370e5..b2586ba1ff 100644 --- a/django/utils/dateformat.py +++ b/django/utils/dateformat.py @@ -110,8 +110,8 @@ class TimeFormat(Formatter): return '%02d' % self.data.second def u(self): - "Microseconds" - return self.data.microsecond + "Microseconds; i.e. '000000' to '999999'" + return '%06d' %self.data.microsecond class DateFormat(TimeFormat): diff --git a/django/utils/encoding.py b/django/utils/encoding.py index 3b284f3ed0..b0872471c2 100644 --- a/django/utils/encoding.py +++ b/django/utils/encoding.py @@ -98,25 +98,13 @@ def force_text(s, encoding='utf-8', strings_only=False, errors='strict'): if hasattr(s, '__unicode__'): s = s.__unicode__() else: - try: - if six.PY3: - if isinstance(s, bytes): - s = six.text_type(s, encoding, errors) - else: - s = six.text_type(s) + if six.PY3: + if isinstance(s, bytes): + s = six.text_type(s, encoding, errors) else: - s = six.text_type(bytes(s), encoding, errors) - except UnicodeEncodeError: - if not isinstance(s, Exception): - raise - # If we get to here, the caller has passed in an Exception - # subclass populated with non-ASCII data without special - # handling to display as a string. We need to handle this - # without raising a further exception. We do an - # approximation to what the Exception's standard str() - # output should be. - s = ' '.join([force_text(arg, encoding, strings_only, - errors) for arg in s]) + s = six.text_type(s) + else: + s = six.text_type(bytes(s), encoding, errors) else: # Note: We use .decode() here, instead of six.text_type(s, encoding, # errors), so that if s is a SafeBytes, it ends up being a diff --git a/django/utils/functional.py b/django/utils/functional.py index 085a8fce59..505931e158 100644 --- a/django/utils/functional.py +++ b/django/utils/functional.py @@ -293,6 +293,16 @@ class SimpleLazyObject(LazyObject): self._setup() return self._wrapped.__dict__ + # Python 3.3 will call __reduce__ when pickling; these methods are needed + # to serialize and deserialize correctly. They are not called in earlier + # versions of Python. + @classmethod + def __newobj__(cls, *args): + return cls.__new__(cls, *args) + + def __reduce__(self): + return (self.__newobj__, (self.__class__,), self.__getstate__()) + # Need to pretend to be the wrapped class, for the sake of objects that care # about this (especially in equality tests) __class__ = property(new_method_proxy(operator.attrgetter("__class__"))) diff --git a/django/utils/html.py b/django/utils/html.py index 2b669cc8ec..cc8372906b 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -42,29 +42,26 @@ def escape(text): return mark_safe(force_text(text).replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''')) escape = allow_lazy(escape, six.text_type) -_base_js_escapes = ( - ('\\', '\\u005C'), - ('\'', '\\u0027'), - ('"', '\\u0022'), - ('>', '\\u003E'), - ('<', '\\u003C'), - ('&', '\\u0026'), - ('=', '\\u003D'), - ('-', '\\u002D'), - (';', '\\u003B'), - ('\u2028', '\\u2028'), - ('\u2029', '\\u2029') -) +_js_escapes = { + ord('\\'): '\\u005C', + ord('\''): '\\u0027', + ord('"'): '\\u0022', + ord('>'): '\\u003E', + ord('<'): '\\u003C', + ord('&'): '\\u0026', + ord('='): '\\u003D', + ord('-'): '\\u002D', + ord(';'): '\\u003B', + ord('\u2028'): '\\u2028', + ord('\u2029'): '\\u2029' +} # Escape every ASCII character with a value less than 32. -_js_escapes = (_base_js_escapes + - tuple([('%c' % z, '\\u%04X' % z) for z in range(32)])) +_js_escapes.update((ord('%c' % z), '\\u%04X' % z) for z in range(32)) def escapejs(value): """Hex encodes characters for use in JavaScript strings.""" - for bad, good in _js_escapes: - value = mark_safe(force_text(value).replace(bad, good)) - return value + return mark_safe(force_text(value).translate(_js_escapes)) escapejs = allow_lazy(escapejs, six.text_type) def conditional_escape(text): diff --git a/django/utils/http.py b/django/utils/http.py index d3c70f1209..1c3b0039b5 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -118,8 +118,7 @@ def parse_http_date(date): The three formats allowed by the RFC are accepted, even if only the first one is still in widespread use. - Returns an floating point number expressed in seconds since the epoch, in - UTC. + Returns an integer expressed in seconds since the epoch, in UTC. """ # emails.Util.parsedate does the job for RFC1123 dates; unfortunately # RFC2616 makes it mandatory to support RFC850 dates too. So we roll diff --git a/django/utils/log.py b/django/utils/log.py index df2089f924..ea0122794b 100644 --- a/django/utils/log.py +++ b/django/utils/log.py @@ -5,6 +5,7 @@ from django.conf import settings from django.core import mail from django.views.debug import ExceptionReporter, get_exception_reporter_filter + # Make sure a NullHandler is available # This was added in Python 2.7/3.2 try: @@ -23,12 +24,46 @@ except ImportError: getLogger = logging.getLogger -# Ensure the creation of the Django logger -# with a null handler. This ensures we don't get any -# 'No handlers could be found for logger "django"' messages -logger = getLogger('django') -if not logger.handlers: - logger.addHandler(NullHandler()) +# Default logging for Django. This sends an email to the site admins on every +# HTTP 500 error. Depending on DEBUG, all other log records are either sent to +# the console (DEBUG=True) or discarded by mean of the NullHandler (DEBUG=False). +DEFAULT_LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', + }, + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, + }, + 'handlers': { + 'console':{ + 'level': 'INFO', + 'filters': ['require_debug_true'], + 'class': 'logging.StreamHandler', + }, + 'null': { + 'class': 'django.utils.log.NullHandler', + }, + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django': { + 'handlers': ['console'], + }, + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': False, + }, + } +} class AdminEmailHandler(logging.Handler): @@ -103,3 +138,8 @@ class CallbackFilter(logging.Filter): class RequireDebugFalse(logging.Filter): def filter(self, record): return not settings.DEBUG + + +class RequireDebugTrue(logging.Filter): + def filter(self, record): + return settings.DEBUG diff --git a/django/utils/numberformat.py b/django/utils/numberformat.py index d51b230823..6a31237f13 100644 --- a/django/utils/numberformat.py +++ b/django/utils/numberformat.py @@ -21,12 +21,10 @@ def format(number, decimal_sep, decimal_pos=None, grouping=0, thousand_sep='', if isinstance(number, int) and not use_grouping and not decimal_pos: return mark_safe(six.text_type(number)) # sign - if float(number) < 0: - sign = '-' - else: - sign = '' + sign = '' str_number = six.text_type(number) if str_number[0] == '-': + sign = '-' str_number = str_number[1:] # decimal part if '.' in str_number: @@ -48,4 +46,3 @@ def format(number, decimal_sep, decimal_pos=None, grouping=0, thousand_sep='', int_part_gd += digit int_part = int_part_gd[::-1] return sign + int_part + dec_part - diff --git a/django/utils/text.py b/django/utils/text.py index c19708458b..d75ca8dbca 100644 --- a/django/utils/text.py +++ b/django/utils/text.py @@ -288,6 +288,37 @@ def compress_string(s): zfile.close() return zbuf.getvalue() +class StreamingBuffer(object): + def __init__(self): + self.vals = [] + + def write(self, val): + self.vals.append(val) + + def read(self): + ret = b''.join(self.vals) + self.vals = [] + return ret + + def flush(self): + return + + def close(self): + return + +# Like compress_string, but for iterators of strings. +def compress_sequence(sequence): + buf = StreamingBuffer() + zfile = GzipFile(mode='wb', compresslevel=6, fileobj=buf) + # Output headers... + yield buf.read() + for item in sequence: + zfile.write(item) + zfile.flush() + yield buf.read() + zfile.close() + yield buf.read() + ustring_re = re.compile("([\u0080-\uffff])") def javascript_quote(s, quote_double_quotes=False): diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index 9fd33a7ea8..9e94840ee0 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -30,9 +30,10 @@ _accepted = {} # magic gettext number to separate context from message CONTEXT_SEPARATOR = "\x04" -# Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9. +# Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9 +# and RFC 3066, section 2.1 accept_language_re = re.compile(r''' - ([A-Za-z]{1,8}(?:-[A-Za-z]{1,8})*|\*) # "en", "en-au", "x-y-z", "*" + ([A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*|\*) # "en", "en-au", "x-y-z", "es-419", "*" (?:\s*;\s*q=(0(?:\.\d{,3})?|1(?:.0{,3})?))? # Optional "q=1.00", "q=0.8" (?:\s*,\s*|$) # Multiple accepts per header. ''', re.VERBOSE) @@ -273,7 +274,8 @@ else: return do_translate(message, 'ugettext') def pgettext(context, message): - result = ugettext("%s%s%s" % (context, CONTEXT_SEPARATOR, message)) + msg_with_ctxt = "%s%s%s" % (context, CONTEXT_SEPARATOR, message) + result = ugettext(msg_with_ctxt) if CONTEXT_SEPARATOR in result: # Translation not found result = message @@ -319,9 +321,10 @@ else: return do_ntranslate(singular, plural, number, 'ungettext') def npgettext(context, singular, plural, number): - result = ungettext("%s%s%s" % (context, CONTEXT_SEPARATOR, singular), - "%s%s%s" % (context, CONTEXT_SEPARATOR, plural), - number) + msgs_with_ctxt = ("%s%s%s" % (context, CONTEXT_SEPARATOR, singular), + "%s%s%s" % (context, CONTEXT_SEPARATOR, plural), + number) + result = ungettext(*msgs_with_ctxt) if CONTEXT_SEPARATOR in result: # Translation not found result = ungettext(singular, plural, number) @@ -437,8 +440,8 @@ def blankout(src, char): return dot_re.sub(char, src) context_re = re.compile(r"""^\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?'))\s*""") -inline_re = re.compile(r"""^\s*trans\s+((?:"[^"]*?")|(?:'[^']*?'))(\s+.*context\s+(?:"[^"]*?")|(?:'[^']*?'))?\s*""") -block_re = re.compile(r"""^\s*blocktrans(\s+.*context\s+(?:"[^"]*?")|(?:'[^']*?'))?(?:\s+|$)""") +inline_re = re.compile(r"""^\s*trans\s+((?:"[^"]*?")|(?:'[^']*?'))(\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?')))?\s*""") +block_re = re.compile(r"""^\s*blocktrans(\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?')))?(?:\s+|$)""") endblock_re = re.compile(r"""^\s*endblocktrans$""") plural_re = re.compile(r"""^\s*plural$""") constant_re = re.compile(r"""_\(((?:".*?")|(?:'.*?'))\)""") diff --git a/django/utils/version.py b/django/utils/version.py index 2271d415db..e0a8286e48 100644 --- a/django/utils/version.py +++ b/django/utils/version.py @@ -30,7 +30,7 @@ def get_version(version=None): mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'} sub = mapping[version[3]] + str(version[4]) - return main + sub + return str(main + sub) def get_git_changeset(): """Returns a numeric identifier of the latest git changeset. diff --git a/django/views/debug.py b/django/views/debug.py index ed99d8dfe6..aaa7e40efe 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -354,15 +354,19 @@ class ExceptionReporter(object): if source is None: return None, [], None, [] - encoding = 'ascii' - for line in source[:2]: - # File coding may be specified. Match pattern from PEP-263 - # (http://www.python.org/dev/peps/pep-0263/) - match = re.search(br'coding[:=]\s*([-\w.]+)', line) - if match: - encoding = match.group(1) - break - source = [six.text_type(sline, encoding, 'replace') for sline in source] + # If we just read the source from a file, or if the loader did not + # apply tokenize.detect_encoding to decode the source into a Unicode + # string, then we should do that ourselves. + if isinstance(source[0], six.binary_type): + encoding = 'ascii' + for line in source[:2]: + # File coding may be specified. Match pattern from PEP-263 + # (http://www.python.org/dev/peps/pep-0263/) + match = re.search(br'coding[:=]\s*([-\w.]+)', line) + if match: + encoding = match.group(1).decode('ascii') + break + source = [six.text_type(sline, encoding, 'replace') for sline in source] lower_bound = max(0, lineno - context_lines) upper_bound = lineno + context_lines diff --git a/django/views/decorators/cache.py b/django/views/decorators/cache.py index ac8b3752d7..06925c1f4a 100644 --- a/django/views/decorators/cache.py +++ b/django/views/decorators/cache.py @@ -12,7 +12,7 @@ def cache_page(*args, **kwargs): 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 + sites.get_current_site().domain, for example, as that is unique across a Django project. Additionally, all headers from the response's Vary header will be taken diff --git a/django/views/decorators/http.py b/django/views/decorators/http.py index d5c4bff744..410979e1e4 100644 --- a/django/views/decorators/http.py +++ b/django/views/decorators/http.py @@ -2,18 +2,18 @@ Decorators for views based on HTTP headers. """ +import logging from calendar import timegm from functools import wraps from django.utils.decorators import decorator_from_middleware, available_attrs from django.utils.http import http_date, parse_http_date_safe, parse_etags, quote_etag -from django.utils.log import getLogger from django.middleware.http import ConditionalGetMiddleware from django.http import HttpResponseNotAllowed, HttpResponseNotModified, HttpResponse conditional_page = decorator_from_middleware(ConditionalGetMiddleware) -logger = getLogger('django.request') +logger = logging.getLogger('django.request') def require_http_methods(request_method_list): diff --git a/django/views/defaults.py b/django/views/defaults.py index 2bbc23321e..ec7a233ff7 100644 --- a/django/views/defaults.py +++ b/django/views/defaults.py @@ -1,6 +1,6 @@ from django import http from django.template import (Context, RequestContext, - loader, TemplateDoesNotExist) + loader, Template, TemplateDoesNotExist) from django.views.decorators.csrf import requires_csrf_token @@ -17,8 +17,13 @@ def page_not_found(request, template_name='404.html'): request_path The path of the requested URL (e.g., '/app/pages/bad_page/') """ - t = loader.get_template(template_name) # You need to create a 404.html template. - return http.HttpResponseNotFound(t.render(RequestContext(request, {'request_path': request.path}))) + try: + template = loader.get_template(template_name) + except TemplateDoesNotExist: + template = Template( + '

Not Found

' + '

The requested URL {{ request_path }} was not found on this server.

') + return http.HttpResponseNotFound(template.render(RequestContext(request, {'request_path': request.path}))) @requires_csrf_token @@ -29,8 +34,11 @@ def server_error(request, template_name='500.html'): Templates: :template:`500.html` Context: None """ - t = loader.get_template(template_name) # You need to create a 500.html template. - return http.HttpResponseServerError(t.render(Context({}))) + try: + template = loader.get_template(template_name) + except TemplateDoesNotExist: + return http.HttpResponseServerError('

Server Error (500)

') + return http.HttpResponseServerError(template.render(Context({}))) # This can be called when CsrfViewMiddleware.process_view has not run, diff --git a/django/views/generic/base.py b/django/views/generic/base.py index e11412ba4d..23e18c54a0 100644 --- a/django/views/generic/base.py +++ b/django/views/generic/base.py @@ -1,14 +1,15 @@ from __future__ import unicode_literals +import logging from functools import update_wrapper + from django import http from django.core.exceptions import ImproperlyConfigured from django.template.response import TemplateResponse -from django.utils.log import getLogger from django.utils.decorators import classonlymethod from django.utils import six -logger = getLogger('django.request') +logger = logging.getLogger('django.request') class ContextMixin(object): @@ -98,7 +99,7 @@ class View(object): """ response = http.HttpResponse() response['Allow'] = ', '.join(self._allowed_methods()) - response['Content-Length'] = 0 + response['Content-Length'] = '0' return response def _allowed_methods(self): diff --git a/django/views/generic/dates.py b/django/views/generic/dates.py index 52e13a4533..e1b0eb99fe 100644 --- a/django/views/generic/dates.py +++ b/django/views/generic/dates.py @@ -377,7 +377,7 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View): """ return self.date_list_period - def get_date_list(self, queryset, date_type=None): + def get_date_list(self, queryset, date_type=None, ordering='ASC'): """ Get a date list by calling `queryset.dates()`, checking along the way for empty lists that aren't allowed. @@ -387,7 +387,7 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View): if date_type is None: date_type = self.get_date_list_period() - date_list = queryset.dates(date_field, date_type)[::-1] + date_list = queryset.dates(date_field, date_type, ordering) if date_list is not None and not date_list and not allow_empty: name = force_text(queryset.model._meta.verbose_name_plural) raise Http404(_("No %(verbose_name_plural)s available") % @@ -409,7 +409,7 @@ class BaseArchiveIndexView(BaseDateListView): Return (date_list, items, extra_context) for this request. """ qs = self.get_dated_queryset(ordering='-%s' % self.get_date_field()) - date_list = self.get_date_list(qs) + date_list = self.get_date_list(qs, ordering='DESC') if not date_list: qs = qs.none() diff --git a/django/views/static.py b/django/views/static.py index 2ff22ce13f..f61ba28bd5 100644 --- a/django/views/static.py +++ b/django/views/static.py @@ -14,7 +14,8 @@ try: except ImportError: # Python 2 from urllib import unquote -from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseNotModified +from django.http import (CompatibleStreamingHttpResponse, Http404, + HttpResponse, HttpResponseRedirect, HttpResponseNotModified) from django.template import loader, Template, Context, TemplateDoesNotExist from django.utils.http import http_date, parse_http_date from django.utils.translation import ugettext as _, ugettext_noop @@ -62,8 +63,7 @@ def serve(request, path, document_root=None, show_indexes=False): if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), statobj.st_mtime, statobj.st_size): return HttpResponseNotModified() - with open(fullpath, 'rb') as f: - response = HttpResponse(f.read(), content_type=mimetype) + response = CompatibleStreamingHttpResponse(open(fullpath, 'rb'), content_type=mimetype) response["Last-Modified"] = http_date(statobj.st_mtime) if stat.S_ISREG(statobj.st_mode): response["Content-Length"] = statobj.st_size @@ -138,7 +138,7 @@ def was_modified_since(header=None, mtime=0, size=0): header_len = matches.group(3) if header_len and int(header_len) != size: raise ValueError - if mtime > header_mtime: + if int(mtime) > header_mtime: raise ValueError except (AttributeError, ValueError, OverflowError): return True diff --git a/docs/faq/admin.txt b/docs/faq/admin.txt index ea6aa2e74e..872ad254c9 100644 --- a/docs/faq/admin.txt +++ b/docs/faq/admin.txt @@ -68,6 +68,18 @@ For example, if your ``list_filter`` includes ``sites``, and there's only one site in your database, it won't display a "Site" filter. In that case, filtering by site would be meaningless. +Some objects aren't appearing in the admin. +------------------------------------------- + +Inconsistent row counts may be caused by missing foreign key values or a +foreign key field incorrectly set to :attr:`null=False +`. If you have a record with a +:class:`~django.db.models.ForeignKey` pointing to a non-existent object and +that foreign key is included is +:attr:`~django.contrib.admin.ModelAdmin.list_display`, the record will not be +shown in the admin changelist because the Django model is declaring an +integrity constraint that is not implemented at the database level. + How can I customize the functionality of the admin interface? ------------------------------------------------------------- @@ -104,4 +116,3 @@ example, some browsers may not support rounded corners. These are considered acceptable variations in rendering. .. _YUI's A-grade: http://yuilibrary.com/yui/docs/tutorials/gbs/ - diff --git a/docs/howto/apache-auth.txt b/docs/howto/apache-auth.txt deleted file mode 100644 index 719fbc1769..0000000000 --- a/docs/howto/apache-auth.txt +++ /dev/null @@ -1,45 +0,0 @@ -========================================================= -Authenticating against Django's user database from Apache -========================================================= - -Since keeping multiple authentication databases in sync is a common problem when -dealing with Apache, you can configuring Apache to authenticate against Django's -:doc:`authentication system ` directly. This requires Apache -version >= 2.2 and mod_wsgi >= 2.0. For example, you could: - -* Serve static/media files directly from Apache only to authenticated users. - -* Authenticate access to a Subversion_ repository against Django users with - a certain permission. - -* Allow certain users to connect to a WebDAV share created with mod_dav_. - -.. _Subversion: http://subversion.tigris.org/ -.. _mod_dav: http://httpd.apache.org/docs/2.2/mod/mod_dav.html - -Configuring Apache -================== - -To check against Django's authorization database from a Apache configuration -file, you'll need to set 'wsgi' as the value of ``AuthBasicProvider`` or -``AuthDigestProvider`` directive and then use the ``WSGIAuthUserScript`` -directive to set the path to your authentification script: - -.. code-block:: apache - - - AuthType Basic - AuthName "example.com" - AuthBasicProvider wsgi - WSGIAuthUserScript /usr/local/wsgi/scripts/auth.wsgi - Require valid-user - - -Your auth.wsgi script will have to implement either a -``check_password(environ, user, password)`` function (for ``AuthBasicProvider``) -or a ``get_realm_hash(environ, user, realm)`` function (for ``AuthDigestProvider``). - -See the `mod_wsgi documentation`_ for more details about the implementation -of such a solution. - -.. _mod_wsgi documentation: http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms#Apache_Authentication_Provider diff --git a/docs/howto/custom-model-fields.txt b/docs/howto/custom-model-fields.txt index 9ff06479c6..1e9d5d8701 100644 --- a/docs/howto/custom-model-fields.txt +++ b/docs/howto/custom-model-fields.txt @@ -448,6 +448,13 @@ called when it is created, you should be using `The SubfieldBase metaclass`_ mentioned earlier. Otherwise :meth:`.to_python` won't be called automatically. +.. warning:: + + If your custom field allows ``null=True``, any field method that takes + ``value`` as an argument, like :meth:`~Field.to_python` and + :meth:`~Field.get_prep_value`, should handle the case when ``value`` is + ``None``. + Converting Python objects to query values ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt index 5b27af82d6..70b6288bee 100644 --- a/docs/howto/custom-template-tags.txt +++ b/docs/howto/custom-template-tags.txt @@ -760,8 +760,6 @@ A few things to note about the ``simple_tag`` helper function: * If the argument was a template variable, our function is passed the current value of the variable, not the variable itself. -.. versionadded:: 1.3 - If your template tag needs to access the current context, you can use the ``takes_context`` argument when registering your tag: diff --git a/docs/howto/deployment/wsgi/apache-auth.txt b/docs/howto/deployment/wsgi/apache-auth.txt new file mode 100644 index 0000000000..5f700f1cb3 --- /dev/null +++ b/docs/howto/deployment/wsgi/apache-auth.txt @@ -0,0 +1,130 @@ +========================================================= +Authenticating against Django's user database from Apache +========================================================= + +Since keeping multiple authentication databases in sync is a common problem when +dealing with Apache, you can configure Apache to authenticate against Django's +:doc:`authentication system ` directly. This requires Apache +version >= 2.2 and mod_wsgi >= 2.0. For example, you could: + +* Serve static/media files directly from Apache only to authenticated users. + +* Authenticate access to a Subversion_ repository against Django users with + a certain permission. + +* Allow certain users to connect to a WebDAV share created with mod_dav_. + +.. note:: + If you have installed a :ref:`custom User model ` and + want to use this default auth handler, it must support an `is_active` + attribute. If you want to use group based authorization, your custom user + must have a relation named 'groups', referring to a related object that has + a 'name' field. You can also specify your own custom mod_wsgi + auth handler if your custom cannot conform to these requirements. + +.. _Subversion: http://subversion.tigris.org/ +.. _mod_dav: http://httpd.apache.org/docs/2.2/mod/mod_dav.html + +Authentication with mod_wsgi +============================ + +Make sure that mod_wsgi is installed and activated and that you have +followed the steps to setup +:doc:`Apache with mod_wsgi ` + +Next, edit your Apache configuration to add a location that you want +only authenticated users to be able to view: + +.. code-block:: apache + + WSGIScriptAlias / /path/to/mysite.com/mysite/wsgi.py + + WSGIProcessGroup %{GLOBAL} + WSGIApplicationGroup django + + + AuthType Basic + AuthName "Top Secret" + Require valid-user + AuthBasicProvider wsgi + WSGIAuthUserScript /path/to/mysite.com/mysite/wsgi.py + + +The ``WSGIAuthUserScript`` directive tells mod_wsgi to execute the +``check_password`` function in specified wsgi script, passing the user name and +password that it receives from the prompt. In this example, the +``WSGIAuthUserScript`` is the same as the ``WSGIScriptAlias`` that defines your +application :doc:`that is created by django-admin.py startproject +`. + +.. admonition:: Using Apache 2.2 with authentication + + Make sure that ``mod_auth_basic`` and ``mod_authz_user`` are loaded. + + These might be compiled statically into Apache, or you might need to use + LoadModule to load them dynamically in your ``httpd.conf``: + + .. code-block:: apache + + LoadModule auth_basic_module modules/mod_auth_basic.so + LoadModule authz_user_module modules/mod_authz_user.so + +Finally, edit your WSGI script ``mysite.wsgi`` to tie Apache's +authentication to your site's authentication mechanisms by importing the +check_user function: + +.. code-block:: python + + import os + import sys + + os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings' + + from django.contrib.auth.handlers.modwsgi import check_password + + from django.core.handlers.wsgi import WSGIHandler + application = WSGIHandler() + + +Requests beginning with ``/secret/`` will now require a user to authenticate. + +The mod_wsgi `access control mechanisms documentation`_ provides additional +details and information about alternative methods of authentication. + +.. _access control mechanisms documentation: http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms + +Authorization with mod_wsgi and Django groups +--------------------------------------------- + +mod_wsgi also provides functionality to restrict a particular location to +members of a group. + +In this case, the Apache configuration should look like this: + +.. code-block:: apache + + WSGIScriptAlias / /path/to/mysite.com/mysite/wsgi.py + + WSGIProcessGroup %{GLOBAL} + WSGIApplicationGroup django + + + AuthType Basic + AuthName "Top Secret" + AuthBasicProvider wsgi + WSGIAuthUserScript /path/to/mysite.com/mysite/wsgi.py + WSGIAuthGroupScript /path/to/mysite.com/mysite/wsgi.py + Require group secret-agents + Require valid-user + + +To support the ``WSGIAuthGroupScript`` directive, the same WSGI script +``mysite.wsgi`` must also import the ``groups_for_user`` function which +returns a list groups the given user belongs to. + +.. code-block:: python + + from django.contrib.auth.handlers.modwsgi import check_password, groups_for_user + +Requests for ``/secret/`` will now also require user to be a member of the +"secret-agents" group. diff --git a/docs/howto/deployment/wsgi/index.txt b/docs/howto/deployment/wsgi/index.txt index ecb302cee3..769d406b1b 100644 --- a/docs/howto/deployment/wsgi/index.txt +++ b/docs/howto/deployment/wsgi/index.txt @@ -16,6 +16,7 @@ documentation for the following WSGI servers: :maxdepth: 1 modwsgi + apache-auth gunicorn uwsgi diff --git a/docs/howto/deployment/wsgi/modwsgi.txt b/docs/howto/deployment/wsgi/modwsgi.txt index 8398f12eb7..7f68485dff 100644 --- a/docs/howto/deployment/wsgi/modwsgi.txt +++ b/docs/howto/deployment/wsgi/modwsgi.txt @@ -25,7 +25,9 @@ Basic configuration =================== Once you've got mod_wsgi installed and activated, edit your Apache server's -``httpd.conf`` file and add:: +``httpd.conf`` file and add + +.. code-block:: apache WSGIScriptAlias / /path/to/mysite.com/mysite/wsgi.py WSGIPythonPath /path/to/mysite.com @@ -56,21 +58,32 @@ for you; otherwise, you'll need to create it. See the :doc:`WSGI overview documentation` for the default contents you should put in this file, and what else you can add to it. +.. warning:: + + If multiple Django sites are run in a single mod_wsgi process, all of them + will use the settings of whichever one happens to run first. This can be + solved with a minor edit to ``wsgi.py`` (see comment in the file for + details), or by :ref:`using mod_wsgi daemon mode` and ensuring + that each site runs in its own daemon process. + + Using a virtualenv ================== If you install your project's Python dependencies inside a `virtualenv`_, you'll need to add the path to this virtualenv's ``site-packages`` directory to -your Python path as well. To do this, you can add another line to your -Apache configuration:: +your Python path as well. To do this, add an additional path to your +`WSGIPythonPath` directive, with multiple paths separated by a colon:: - WSGIPythonPath /path/to/your/venv/lib/python2.X/site-packages + WSGIPythonPath /path/to/mysite.com:/path/to/your/venv/lib/python2.X/site-packages Make sure you give the correct path to your virtualenv, and replace ``python2.X`` with the correct Python version (e.g. ``python2.7``). .. _virtualenv: http://www.virtualenv.org +.. _daemon-mode: + Using mod_wsgi daemon mode ========================== @@ -177,6 +190,13 @@ other approaches: 3. Copy the admin static files so that they live within your Apache document root. +Authenticating against Django's user database from Apache +========================================================= + +Django provides a handler to allow Apache to authenticate users directly +against Django's authentication backends. See the :doc:`mod_wsgi authentication +documentation `. + If you get a UnicodeEncodeError =============================== diff --git a/docs/howto/error-reporting.txt b/docs/howto/error-reporting.txt index 64af2a0980..78e797b607 100644 --- a/docs/howto/error-reporting.txt +++ b/docs/howto/error-reporting.txt @@ -44,8 +44,6 @@ setting. .. seealso:: - .. versionadded:: 1.3 - Server error emails are sent using the logging framework, so you can customize this behavior by :doc:`customizing your logging configuration `. @@ -99,8 +97,6 @@ The best way to disable this behavior is to set .. seealso:: - .. versionadded:: 1.3 - 404 errors are logged using the logging framework. By default, these log records are ignored, but you can use them for error reporting by writing a handler and :doc:`configuring logging ` appropriately. diff --git a/docs/howto/index.txt b/docs/howto/index.txt index 737ee71da4..d39222be26 100644 --- a/docs/howto/index.txt +++ b/docs/howto/index.txt @@ -9,7 +9,6 @@ you quickly accomplish common tasks. .. toctree:: :maxdepth: 1 - apache-auth auth-remote-user custom-management-commands custom-model-fields diff --git a/docs/howto/jython.txt b/docs/howto/jython.txt index 762250212a..461a5d3804 100644 --- a/docs/howto/jython.txt +++ b/docs/howto/jython.txt @@ -6,9 +6,10 @@ Running Django on Jython .. admonition:: Python 2.6 support - Django 1.5 has dropped support for Python 2.5. Until Jython provides a new - version that supports 2.6, Django 1.5 is no more compatible with Jython. - Please use Django 1.4 if you want to use Django over Jython. + Django 1.5 has dropped support for Python 2.5. Therefore, you have to use + a Jython 2.7 alpha release if you want to use Django 1.5 with Jython. + Please use Django 1.4 if you want to keep using Django on a stable Jython + version. Jython_ is an implementation of Python that runs on the Java platform (JVM). Django runs cleanly on Jython version 2.5 or later, which means you can deploy diff --git a/docs/howto/outputting-csv.txt b/docs/howto/outputting-csv.txt index 1a606069b8..bcc6f3827b 100644 --- a/docs/howto/outputting-csv.txt +++ b/docs/howto/outputting-csv.txt @@ -21,7 +21,7 @@ Here's an example:: def some_view(request): # Create the HttpResponse object with the appropriate CSV header. response = HttpResponse(mimetype='text/csv') - response['Content-Disposition'] = 'attachment; filename=somefilename.csv' + response['Content-Disposition'] = 'attachment; filename="somefilename.csv"' writer = csv.writer(response) writer.writerow(['First row', 'Foo', 'Bar', 'Baz']) @@ -93,7 +93,7 @@ Here's an example, which generates the same CSV file as above:: def some_view(request): # Create the HttpResponse object with the appropriate CSV header. response = HttpResponse(mimetype='text/csv') - response['Content-Disposition'] = 'attachment; filename=somefilename.csv' + response['Content-Disposition'] = 'attachment; filename="somefilename.csv"' # The data is hard-coded here, but you could load it from a database or # some other source. diff --git a/docs/howto/outputting-pdf.txt b/docs/howto/outputting-pdf.txt index e7e4bdcfa5..9d87b97710 100644 --- a/docs/howto/outputting-pdf.txt +++ b/docs/howto/outputting-pdf.txt @@ -52,7 +52,7 @@ Here's a "Hello World" example:: def some_view(request): # Create the HttpResponse object with the appropriate PDF headers. response = HttpResponse(mimetype='application/pdf') - response['Content-Disposition'] = 'attachment; filename=somefilename.pdf' + response['Content-Disposition'] = 'attachment; filename="somefilename.pdf"' # Create the PDF object, using the response object as its "file." p = canvas.Canvas(response) @@ -87,7 +87,7 @@ mention: the PDF using whatever program/plugin they've been configured to use for PDFs. Here's what that code would look like:: - response['Content-Disposition'] = 'filename=somefilename.pdf' + response['Content-Disposition'] = 'filename="somefilename.pdf"' * Hooking into the ReportLab API is easy: Just pass ``response`` as the first argument to ``canvas.Canvas``. The ``Canvas`` class expects a @@ -121,7 +121,7 @@ Here's the above "Hello World" example rewritten to use :mod:`io`:: def some_view(request): # Create the HttpResponse object with the appropriate PDF headers. response = HttpResponse(mimetype='application/pdf') - response['Content-Disposition'] = 'attachment; filename=somefilename.pdf' + response['Content-Disposition'] = 'attachment; filename="somefilename.pdf"' buffer = BytesIO() diff --git a/docs/howto/static-files.txt b/docs/howto/static-files.txt index f8c591891d..964b5fab61 100644 --- a/docs/howto/static-files.txt +++ b/docs/howto/static-files.txt @@ -2,8 +2,6 @@ Managing static files ===================== -.. versionadded:: 1.3 - Django developers mostly concern themselves with the dynamic parts of web applications -- the views and templates that render anew for each request. But web applications have other parts: the static files (images, CSS, diff --git a/docs/index.txt b/docs/index.txt index 8b29c95fa2..5055edf7e7 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -181,7 +181,6 @@ testing of Django applications: :doc:`Overview ` | :doc:`WSGI servers ` | :doc:`FastCGI/SCGI/AJP ` | - :doc:`Apache authentication ` | :doc:`Handling static files ` | :doc:`Tracking code errors by email ` @@ -241,7 +240,7 @@ applications: * :doc:`Authentication ` * :doc:`Caching ` * :doc:`Logging ` -* :doc:`Sending e-mails ` +* :doc:`Sending emails ` * :doc:`Syndication feeds (RSS/Atom) ` * :doc:`Comments `, :doc:`comment moderation ` and :doc:`custom comments ` * :doc:`Pagination ` diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index ca56d36880..7900dd8cd0 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -407,6 +407,18 @@ Jeremy Dunck .. _vlogger: http://youtube.com/bryanveloso/ .. _shoutcaster: http://twitch.tv/vlogalonstar/ +`Preston Holmes`_ + Preston is a recovering neuroscientist who originally discovered Django as + part of a sweeping move to Python from a grab bag of half a dozen + languages. He was drawn to Django's balance of practical batteries included + philosophy, care and thought in code design, and strong open source + community. In addition to his current job in private progressive education, + Preston contributes some developer time to local non-profits. + + Preston lives with his family and animal menagerie in Santa Barbara, CA, USA. + +.. _Preston Holmes: http://www.ptone.com/ + Specialists ----------- diff --git a/docs/internals/contributing/committing-code.txt b/docs/internals/contributing/committing-code.txt index d36bc78fe1..67dda02f8b 100644 --- a/docs/internals/contributing/committing-code.txt +++ b/docs/internals/contributing/committing-code.txt @@ -187,7 +187,15 @@ Django's Git repository: For the curious, we're using a `Trac plugin`_ for this. - .. _Trac plugin: https://github.com/aaugustin/trac-github +.. note:: + + Note that the Trac integration doesn't know anything about pull requests. + So if you try to close a pull request with the phrase "closes #400" in your + commit message, GitHub will close the pull request, but the Trac plugin + will also close the same numbered ticket in Trac. + + +.. _Trac plugin: https://github.com/aaugustin/trac-github * If your commit references a ticket in the Django `ticket tracker`_ but does *not* close the ticket, include the phrase "Refs #xxxxx", where "xxxxx" diff --git a/docs/internals/contributing/localizing.txt b/docs/internals/contributing/localizing.txt index 263087b5fa..0cde77882c 100644 --- a/docs/internals/contributing/localizing.txt +++ b/docs/internals/contributing/localizing.txt @@ -55,7 +55,7 @@ The format files aren't managed by the use of Transifex. To change them, you must :doc:`create a patch` against the Django source tree, as for any code change: -* Create a diff against the current Subversion trunk. +* Create a diff against the current Git master branch. * Open a ticket in Django's ticket system, set its ``Component`` field to ``Translations``, and attach the patch to it. diff --git a/docs/internals/contributing/triaging-tickets.txt b/docs/internals/contributing/triaging-tickets.txt index ab879e5caf..84f70fd731 100644 --- a/docs/internals/contributing/triaging-tickets.txt +++ b/docs/internals/contributing/triaging-tickets.txt @@ -171,15 +171,6 @@ concrete actionable issues. They are enhancement requests that we might consider adding someday to the framework if an excellent patch is submitted. These tickets are not a high priority. -Fixed on a branch -~~~~~~~~~~~~~~~~~ - -Used to indicate that a ticket is resolved as part of a major body of work -that will eventually be merged to trunk. Tickets in this stage generally -don't need further work. This may happen in the case of major -features/refactors in each release cycle, or as part of the annual Google -Summer of Code efforts. - Other triage attributes ----------------------- diff --git a/docs/internals/contributing/writing-code/coding-style.txt b/docs/internals/contributing/writing-code/coding-style.txt index 2fa0233e3d..a699e39bd8 100644 --- a/docs/internals/contributing/writing-code/coding-style.txt +++ b/docs/internals/contributing/writing-code/coding-style.txt @@ -140,9 +140,9 @@ Model style a tuple of tuples, with an all-uppercase name, either near the top of the model module or just above the model class. Example:: - GENDER_CHOICES = ( - ('M', 'Male'), - ('F', 'Female'), + DIRECTION_CHOICES = ( + ('U', 'Up'), + ('D', 'Down'), ) Use of ``django.conf.settings`` diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index 4de506a654..a828b06b36 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -163,6 +163,26 @@ associated tests will be skipped. .. _gettext: http://www.gnu.org/software/gettext/manual/gettext.html .. _selenium: http://pypi.python.org/pypi/selenium +Code coverage +~~~~~~~~~~~~~ + +Contributors are encouraged to run coverage on the test suite to identify areas +that need additional tests. The coverage tool installation and use is described +in :ref:`testing code coverage`. + +To run coverage on the Django test suite using the standard test settings:: + + coverage run ./runtests.py --settings=test_sqlite + +After running coverage, generate the html report by running:: + + coverage html + +When running coverage for the Django tests, the included ``.coveragerc`` +settings file defines ``coverage_html`` as the output directory for the report +and also excludes several directories not relevant to the results +(test code or external code included in Django). + .. _contrib-apps: Contrib apps diff --git a/docs/internals/contributing/writing-documentation.txt b/docs/internals/contributing/writing-documentation.txt index c8d7039a68..469f8614b9 100644 --- a/docs/internals/contributing/writing-documentation.txt +++ b/docs/internals/contributing/writing-documentation.txt @@ -30,8 +30,9 @@ If you'd like to start contributing to our docs, get the development version of Django from the source code repository (see :ref:`installing-development-version`). The development version has the latest-and-greatest documentation, just as it has latest-and-greatest code. -Generally, we only revise documentation in the development version, as our -policy is to freeze documentation for existing releases (see +We also backport documentation fixes and improvements, at the discretion of the +committer, to the last release branch. That's because it's highly advantageous +to have the docs for the last release be up-to-date and correct (see :ref:`differences-between-doc-versions`). Getting started with Sphinx diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 4add751912..10bbfe1a91 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -23,7 +23,7 @@ these changes. * The :mod:`django.contrib.gis.db.backend` module will be removed in favor of the specific backends. -* ``SMTPConnection`` will be removed in favor of a generic E-mail backend API. +* ``SMTPConnection`` will be removed in favor of a generic Email backend API. * The many to many SQL generation functions on the database backends will be removed. @@ -134,16 +134,16 @@ these changes. * The function-based generic view modules will be removed in favor of their class-based equivalents, outlined :doc:`here - `: + `. * The :class:`~django.core.servers.basehttp.AdminMediaHandler` will be removed. In its place use :class:`~django.contrib.staticfiles.handlers.StaticFilesHandler`. * The :ttag:`url` and :ttag:`ssi` template tags will be - modified so that the first argument to each tag is a - template variable, not an implied string. Until then, the new-style - behavior is provided in the ``future`` template tag library. + modified so that the first argument to each tag is a template variable, not + an implied string. In 1.4, this behavior is provided by a version of the tag + in the ``future`` template tag library. * The :djadmin:`reset` and :djadmin:`sqlreset` management commands will be removed. @@ -286,6 +286,13 @@ these changes. * The ``mimetype`` argument to :class:`~django.http.HttpResponse` ``__init__`` will be removed (``content_type`` should be used instead). +* When :class:`~django.http.HttpResponse` is instantiated with an iterator, + or when :attr:`~django.http.HttpResponse.content` is set to an iterator, + that iterator will be immediately consumed. + +* The ``AUTH_PROFILE_MODULE`` setting, and the ``get_profile()`` method on + the User model, will be removed. + 2.0 --- diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt index fd13230c8b..b87b280d7c 100644 --- a/docs/intro/tutorial02.txt +++ b/docs/intro/tutorial02.txt @@ -440,20 +440,30 @@ Open your settings file (``mysite/settings.py``, remember) and look at the filesystem directories to check when loading Django templates. It's a search path. +Create a ``mytemplates`` directory in your project directory. Templates can +live anywhere on your filesystem that Django can access. (Django runs as +whatever user your server runs.) However, keeping your templates within the +project is a good convention to follow. + +When you’ve done that, create a directory polls in your template directory. +Within that, create a file called index.html. Note that our +``loader.get_template('polls/index.html')`` code from above maps to +[template_directory]/polls/index.html” on the filesystem. + By default, :setting:`TEMPLATE_DIRS` is empty. So, let's add a line to it, to tell Django where our templates live:: TEMPLATE_DIRS = ( - '/home/my_username/mytemplates', # Change this to your own directory. + '/path/to/mysite/mytemplates', # Change this to your own directory. ) Now copy the template ``admin/base_site.html`` from within the default Django admin template directory in the source code of Django itself (``django/contrib/admin/templates``) into an ``admin`` subdirectory of whichever directory you're using in :setting:`TEMPLATE_DIRS`. For example, if -your :setting:`TEMPLATE_DIRS` includes ``'/home/my_username/mytemplates'``, as +your :setting:`TEMPLATE_DIRS` includes ``'/path/to/mysite/mytemplates'``, as above, then copy ``django/contrib/admin/templates/admin/base_site.html`` to -``/home/my_username/mytemplates/admin/base_site.html``. Don't forget that +``/path/to/mysite/mytemplates/admin/base_site.html``. Don't forget that ``admin`` subdirectory. .. admonition:: Where are the Django source files? diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index 03d4bf68b3..169e6cd59f 100644 --- a/docs/intro/tutorial03.txt +++ b/docs/intro/tutorial03.txt @@ -10,7 +10,7 @@ Philosophy ========== A view is a "type" of Web page in your Django application that generally serves -a specific function and has a specific template. For example, in a Weblog +a specific function and has a specific template. For example, in a blog application, you might have the following views: * Blog homepage -- displays the latest few entries. @@ -41,42 +41,55 @@ In our poll application, we'll have the following four views: In Django, each view is represented by a simple Python function. -Design your URLs -================ +Write your first view +===================== -The first step of writing views is to design your URL structure. You do this by -creating a Python module, called a URLconf. URLconfs are how Django associates -a given URL with given Python code. +Let's write the first view. Open the file ``polls/views.py`` +and put the following Python code in it:: -When a user requests a Django-powered page, the system looks at the -:setting:`ROOT_URLCONF` setting, which contains a string in Python dotted -syntax. Django loads that module and looks for a module-level variable called -``urlpatterns``, which is a sequence of tuples in the following format:: + from django.http import HttpResponse - (regular expression, Python callback function [, optional dictionary]) + def index(request): + return HttpResponse("Hello, world. You're at the poll index.") -Django starts at the first regular expression and makes its way down the list, -comparing the requested URL against each regular expression until it finds one -that matches. +This is the simplest view possible in Django. Now we have a problem, how does +this view get called? For that we need to map it to a URL, in Django this is +done in a configuration file called a URLconf. -When it finds a match, Django calls the Python callback function, with an -:class:`~django.http.HttpRequest` object as the first argument, any "captured" -values from the regular expression as keyword arguments, and, optionally, -arbitrary keyword arguments from the dictionary (an optional third item in the -tuple). +.. admonition:: What is a URLconf? -For more on :class:`~django.http.HttpRequest` objects, see the -:doc:`/ref/request-response`. For more details on URLconfs, see the -:doc:`/topics/http/urls`. + In Django, web pages and other content are delivered by views and + determining which view is called is done by Python modules informally + titled 'URLconfs'. These modules are pure Python code and are a simple + mapping between URL patterns (as simple regular expressions) to Python + callback functions (your views). This tutorial provides basic instruction + in their use, and you can refer to :mod:`django.core.urlresolvers` for + more information. -When you ran ``django-admin.py startproject mysite`` at the beginning of -Tutorial 1, it created a default URLconf in ``mysite/urls.py``. It also -automatically set your :setting:`ROOT_URLCONF` setting (in ``settings.py``) to -point at that file:: +To create a URLconf in the polls directory, create a file called ``urls.py``. +Your app directory should now look like:: - ROOT_URLCONF = 'mysite.urls' + polls/ + __init__.py + admin.py + models.py + tests.py + urls.py + views.py -Time for an example. Edit ``mysite/urls.py`` so it looks like this:: +In the ``polls/urls.py`` file include the following code:: + + from django.conf.urls import patterns, url + + from polls import views + + urlpatterns = patterns('', + url(r'^$', views.index, name='index') + ) + +The next step is to point the root URLconf at the ``polls.urls`` module. In +``mysite/urls.py`` insert an :func:`~django.conf.urls.include`, leaving you +with:: from django.conf.urls import patterns, include, url @@ -84,27 +97,141 @@ Time for an example. Edit ``mysite/urls.py`` so it looks like this:: admin.autodiscover() urlpatterns = patterns('', - url(r'^polls/$', 'polls.views.index'), - url(r'^polls/(?P\d+)/$', 'polls.views.detail'), - url(r'^polls/(?P\d+)/results/$', 'polls.views.results'), - url(r'^polls/(?P\d+)/vote/$', 'polls.views.vote'), + url(r'^polls/', include('polls.urls')), url(r'^admin/', include(admin.site.urls)), ) -This is worth a review. When somebody requests a page from your Web site -- say, -"/polls/23/", Django will load this Python module, because it's pointed to by -the :setting:`ROOT_URLCONF` setting. It finds the variable named ``urlpatterns`` -and traverses the regular expressions in order. When it finds a regular -expression that matches -- ``r'^polls/(?P\d+)/$'`` -- it loads the -function ``detail()`` from ``polls/views.py``. Finally, it calls that -``detail()`` function like so:: +You have now wired an `index` view into the URLconf. Go to +http://localhost:8000/polls/ in your browser, and you should see the text +"*Hello, world. You're at the poll index.*", which you defined in the +``index`` view. - detail(request=, poll_id='23') +The :func:`~django.conf.urls.url` function is passed four arguments, two +required: ``regex`` and ``view``, and two optional: ``kwargs``, and ``name``. +At this point, it's worth reviewing what these arguments are for. -The ``poll_id='23'`` part comes from ``(?P\d+)``. Using parentheses +:func:`~django.conf.urls.url` argument: regex +--------------------------------------------- + +The term `regex` is a commonly used short form meaning `regular expression`, +which is a syntax for matching patterns in strings, or in this case, url +patterns. Django starts at the first regular expression and makes its way down +the list, comparing the requested URL against each regular expression until it +finds one that matches. + +Note that these regular expressions do not search GET and POST parameters, or +the domain name. For example, in a request to +``http://www.example.com/myapp/``, the URLconf will look for ``myapp/``. In a +request to ``http://www.example.com/myapp/?page=3``, the URLconf will also +look for ``myapp/``. + +If you need help with regular expressions, see `Wikipedia's entry`_ and the +documentation of the :mod:`re` module. Also, the O'Reilly book "Mastering +Regular Expressions" by Jeffrey Friedl is fantastic. In practice, however, +you don't need to be an expert on regular expressions, as you really only need +to know how to capture simple patterns. In fact, complex regexes can have poor +lookup performance, so you probably shouldn't rely on the full power of regexes. + +Finally, a performance note: these regular expressions are compiled the first +time the URLconf module is loaded. They're super fast (as long as the lookups +aren't too complex as noted above). + +.. _Wikipedia's entry: http://en.wikipedia.org/wiki/Regular_expression + +:func:`~django.conf.urls.url` argument: view +-------------------------------------------- + +When Django finds a regular expression match, Django calls the specified view +function, with an :class:`~django.http.HttpRequest` object as the first +argument and any “captured” values from the regular expression as other +arguments. If the regex uses simple captures, values are passed as positional +arguments; if it uses named captures, values are passed as keyword arguments. +We'll give an example of this in a bit. + +:func:`~django.conf.urls.url` argument: kwargs +---------------------------------------------- + +Arbitrary keyword arguments can be passed in a dictionary to the target view. We +aren't going to use this feature of Django in the tutorial. + +:func:`~django.conf.urls.url` argument: name +--------------------------------------------- + +Naming your URL lets you refer to it unambiguously from elsewhere in Django +especially templates. This powerful feature allows you to make global changes +to the url patterns of your project while only touching a single file. + +Writing more views +================== + +Now let's add a few more views to ``polls/views.py``. These views are +slightly different, because they take an argument:: + + def detail(request, poll_id): + return HttpResponse("You're looking at poll %s." % poll_id) + + def results(request, poll_id): + return HttpResponse("You're looking at the results of poll %s." % poll_id) + + def vote(request, poll_id): + return HttpResponse("You're voting on poll %s." % poll_id) + +Wire these news views into the ``polls.urls`` module by adding the following +:func:`~django.conf.urls.url` calls:: + + from django.conf.urls import patterns, url + + from polls import views + + urlpatterns = patterns('', + # ex: /polls/ + url(r'^$', views.index, name='index'), + # ex: /polls/5/ + url(r'^(?P\d+)/$', views.detail, name='detail'), + # ex: /polls/5/results/ + url(r'^(?P\d+)/results/$', views.results, name='results'), + # ex: /polls/5/vote/ + url(r'^(?P\d+)/vote/$', views.vote, name='vote'), + ) + +Take a look in your browser, at "/polls/34/". It'll run the ``detail()`` +method and display whatever ID you provide in the URL. Try +"/polls/34/results/" and "/polls/34/vote/" too -- these will display the +placeholder results and voting pages. + +When somebody requests a page from your Web site -- say, "/polls/34/", Django +will load the ``mysite.urls`` Python module because it's pointed to by the +:setting:`ROOT_URLCONF` setting. It finds the variable named ``urlpatterns`` +and traverses the regular expressions in order. The +:func:`~django.conf.urls.include` functions we are using simply reference +other URLconfs. Note that the regular expressions for the +:func:`~django.conf.urls.include` functions don't have a ``$`` (end-of-string +match character) but rather a trailing slash. Whenever Django encounters +:func:`~django.conf.urls.include`, it chops off whatever part of the URL +matched up to that point and sends the remaining string to the included +URLconf for further processing. + +The idea behind :func:`~django.conf.urls.include` is to make it easy to +plug-and-play URLs. Since polls are in their own URLconf +(``polls/urls.py``), they can be placed under "/polls/", or under +"/fun_polls/", or under "/content/polls/", or any other path root, and the +app will still work. + +Here's what happens if a user goes to "/polls/34/" in this system: + +* Django will find the match at ``'^polls/'`` + +* Then, Django will strip off the matching text (``"polls/"``) and send the + remaining text -- ``"34/"`` -- to the 'polls.urls' URLconf for + further processing which matches ``r'^(?P\d+)/$'`` resulting in a + call to the ``detail()`` view like so:: + + detail(request=, poll_id='34') + +The ``poll_id='34'`` part comes from ``(?P\d+)``. Using parentheses around a pattern "captures" the text matched by that pattern and sends it as an -argument to the view function; the ``?P`` defines the name that will be -used to identify the matched pattern; and ``\d+`` is a regular expression to +argument to the view function; ``?P`` defines the name that will +be used to identify the matched pattern; and ``\d+`` is a regular expression to match a sequence of digits (i.e., a number). Because the URL patterns are regular expressions, there really is no limit on @@ -116,79 +243,10 @@ like this:: But, don't do that. It's silly. -Note that these regular expressions do not search GET and POST parameters, or -the domain name. For example, in a request to ``http://www.example.com/myapp/``, -the URLconf will look for ``myapp/``. In a request to -``http://www.example.com/myapp/?page=3``, the URLconf will look for ``myapp/``. - -If you need help with regular expressions, see `Wikipedia's entry`_ and the -documentation of the :mod:`re` module. Also, the O'Reilly book "Mastering -Regular Expressions" by Jeffrey Friedl is fantastic. - -Finally, a performance note: these regular expressions are compiled the first -time the URLconf module is loaded. They're super fast. - -.. _Wikipedia's entry: http://en.wikipedia.org/wiki/Regular_expression - -Write your first view -===================== - -Well, we haven't created any views yet -- we just have the URLconf. But let's -make sure Django is following the URLconf properly. - -Fire up the Django development Web server: - -.. code-block:: bash - - python manage.py runserver - -Now go to "http://localhost:8000/polls/" on your domain in your Web browser. -You should get a pleasantly-colored error page with the following message:: - - ViewDoesNotExist at /polls/ - - Could not import polls.views.index. View does not exist in module polls.views. - -This error happened because you haven't written a function ``index()`` in the -module ``polls/views.py``. - -Try "/polls/23/", "/polls/23/results/" and "/polls/23/vote/". The error -messages tell you which view Django tried (and failed to find, because you -haven't written any views yet). - -Time to write the first view. Open the file ``polls/views.py`` -and put the following Python code in it:: - - from django.http import HttpResponse - - def index(request): - return HttpResponse("Hello, world. You're at the poll index.") - -This is the simplest view possible. Go to "/polls/" in your browser, and you -should see your text. - -Now lets add a few more views. These views are slightly different, because -they take an argument (which, remember, is passed in from whatever was -captured by the regular expression in the URLconf):: - - def detail(request, poll_id): - return HttpResponse("You're looking at poll %s." % poll_id) - - def results(request, poll_id): - return HttpResponse("You're looking at the results of poll %s." % poll_id) - - def vote(request, poll_id): - return HttpResponse("You're voting on poll %s." % poll_id) - -Take a look in your browser, at "/polls/34/". It'll run the `detail()` method -and display whatever ID you provide in the URL. Try "/polls/34/results/" and -"/polls/34/vote/" too -- these will display the placeholder results and voting -pages. - Write views that actually do something ====================================== -Each view is responsible for doing one of two things: Returning an +Each view is responsible for doing one of two things: returning an :class:`~django.http.HttpResponse` object containing the content for the requested page, or raising an exception such as :exc:`~django.http.Http404`. The rest is up to you. @@ -205,51 +263,21 @@ in :doc:`Tutorial 1 `. Here's one stab at the ``index()`` view, which displays the latest 5 poll questions in the system, separated by commas, according to publication date:: - from polls.models import Poll from django.http import HttpResponse + from polls.models import Poll + def index(request): - latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5] + latest_poll_list = Poll.objects.order_by('-pub_date')[:5] output = ', '.join([p.question for p in latest_poll_list]) return HttpResponse(output) -There's a problem here, though: The page's design is hard-coded in the view. If +There's a problem here, though: the page's design is hard-coded in the view. If you want to change the way the page looks, you'll have to edit this Python code. -So let's use Django's template system to separate the design from Python:: - - from django.template import Context, loader - from polls.models import Poll - from django.http import HttpResponse - - def index(request): - latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5] - t = loader.get_template('polls/index.html') - c = Context({ - 'latest_poll_list': latest_poll_list, - }) - return HttpResponse(t.render(c)) - -That code loads the template called "polls/index.html" and passes it a context. -The context is a dictionary mapping template variable names to Python objects. - -Reload the page. Now you'll see an error:: - - TemplateDoesNotExist at /polls/ - polls/index.html - -Ah. There's no template yet. First, create a directory, somewhere on your -filesystem, whose contents Django can access. (Django runs as whatever user your -server runs.) Don't put them under your document root, though. You probably -shouldn't make them public, just for security's sake. -Then edit :setting:`TEMPLATE_DIRS` in your ``settings.py`` to tell Django where -it can find templates -- just as you did in the "Customize the admin look and -feel" section of Tutorial 2. - -When you've done that, create a directory ``polls`` in your template directory. -Within that, create a file called ``index.html``. Note that our -``loader.get_template('polls/index.html')`` code from above maps to -"[template_directory]/polls/index.html" on the filesystem. +So let's use Django's template system to separate the design from Python. +First, create a directory ``polls`` in your template directory you specified +in setting:`TEMPLATE_DIRS`. Within that, create a file called ``index.html``. Put the following code in that template: .. code-block:: html+django @@ -264,36 +292,58 @@ Put the following code in that template:

No polls are available.

{% endif %} +Now let's use that html template in our index view:: + + from django.http import HttpResponse + from django.template import Context, loader + + from polls.models import Poll + + def index(request): + latest_poll_list = Poll.objects.order_by('-pub_date')[:5] + template = loader.get_template('polls/index.html') + context = Context({ + 'latest_poll_list': latest_poll_list, + }) + return HttpResponse(template.render(context)) + +That code loads the template called ``polls/index.html`` and passes it a +context. The context is a dictionary mapping template variable names to Python +objects. + Load the page in your Web browser, and you should see a bulleted-list containing the "What's up" poll from Tutorial 1. The link points to the poll's detail page. -A shortcut: render_to_response() --------------------------------- +A shortcut: :func:`~django.shortcuts.render` +-------------------------------------------- It's a very common idiom to load a template, fill a context and return an :class:`~django.http.HttpResponse` object with the result of the rendered template. Django provides a shortcut. Here's the full ``index()`` view, rewritten:: - from django.shortcuts import render_to_response + from django.shortcuts import render + from polls.models import Poll def index(request): latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5] - return render_to_response('polls/index.html', {'latest_poll_list': latest_poll_list}) + context = {'latest_poll_list': latest_poll_list} + return render(request, 'polls/index.html', context) Note that once we've done this in all these views, we no longer need to import :mod:`~django.template.loader`, :class:`~django.template.Context` and -:class:`~django.http.HttpResponse`. +:class:`~django.http.HttpResponse` (you'll want to keep ``HttpResponse`` if you +still have the stub methods for ``detail``, ``results``, and ``vote``). -The :func:`~django.shortcuts.render_to_response` function takes a template name -as its first argument and a dictionary as its optional second argument. It -returns an :class:`~django.http.HttpResponse` object of the given template -rendered with the given context. +The :func:`~django.shortcuts.render` function takes the request object as its +first argument, a template name as its second argument and a dictionary as its +optional third argument. It returns an :class:`~django.http.HttpResponse` +object of the given template rendered with the given context. -Raising 404 -=========== +Raising a 404 error +=================== Now, let's tackle the poll detail view -- the page that displays the question for a given poll. Here's the view:: @@ -302,10 +352,10 @@ for a given poll. Here's the view:: # ... def detail(request, poll_id): try: - p = Poll.objects.get(pk=poll_id) + poll = Poll.objects.get(pk=poll_id) except Poll.DoesNotExist: raise Http404 - return render_to_response('polls/detail.html', {'poll': p}) + return render(request, 'polls/detail.html', {'poll': poll}) The new concept here: The view raises the :exc:`~django.http.Http404` exception if a poll with the requested ID doesn't exist. @@ -317,18 +367,18 @@ later, but if you'd like to quickly get the above example working, just:: will get you started for now. -A shortcut: get_object_or_404() -------------------------------- +A shortcut: :func:`~django.shortcuts.get_object_or_404` +------------------------------------------------------- It's a very common idiom to use :meth:`~django.db.models.query.QuerySet.get` and raise :exc:`~django.http.Http404` if the object doesn't exist. Django provides a shortcut. Here's the ``detail()`` view, rewritten:: - from django.shortcuts import render_to_response, get_object_or_404 + from django.shortcuts import render, get_object_or_404 # ... def detail(request, poll_id): - p = get_object_or_404(Poll, pk=poll_id) - return render_to_response('polls/detail.html', {'poll': p}) + poll = get_object_or_404(Poll, pk=poll_id) + return render(request, 'polls/detail.html', {'poll': poll}) The :func:`~django.shortcuts.get_object_or_404` function takes a Django model as its first argument and an arbitrary number of keyword arguments, which it @@ -345,7 +395,8 @@ exist. :exc:`~django.core.exceptions.ObjectDoesNotExist`? Because that would couple the model layer to the view layer. One of the - foremost design goals of Django is to maintain loose coupling. + foremost design goals of Django is to maintain loose coupling. Some + controlled coupling is introduced in the :mod:`django.shortcuts` module. There's also a :func:`~django.shortcuts.get_list_or_404` function, which works just as :func:`~django.shortcuts.get_object_or_404` -- except using @@ -366,11 +417,11 @@ special: It's just a normal view. You normally won't have to bother with writing 404 views. If you don't set ``handler404``, the built-in view :func:`django.views.defaults.page_not_found` -is used by default. In this case, you still have one obligation: create a -``404.html`` template in the root of your template directory. The default 404 -view will use that template for all 404 errors. If :setting:`DEBUG` is set to -``False`` (in your settings module) and if you didn't create a ``404.html`` -file, an ``Http500`` is raised instead. So remember to create a ``404.html``. +is used by default. Optionally, you can create a ``404.html`` template +in the root of your template directory. The default 404 view will then use that +template for all 404 errors when :setting:`DEBUG` is set to ``False`` (in your +settings module). If you do create the template, add at least some dummy +content like "Page not found". A couple more things to note about 404 views: @@ -388,11 +439,14 @@ Similarly, your root URLconf may define a ``handler500``, which points to a view to call in case of server errors. Server errors happen when you have runtime errors in view code. +Likewise, you should create a ``500.html`` template at the root of your +template directory and add some content like "Something went wrong". + Use the template system ======================= Back to the ``detail()`` view for our poll application. Given the context -variable ``poll``, here's what the "polls/detail.html" template might look +variable ``poll``, here's what the ``polls/detail.html`` template might look like: .. code-block:: html+django @@ -417,150 +471,89 @@ suitable for use in the :ttag:`{% for %}` tag. See the :doc:`template guide
` for more about templates. -Simplifying the URLconfs -======================== - -Take some time to play around with the views and template system. As you edit -the URLconf, you may notice there's a fair bit of redundancy in it:: - - urlpatterns = patterns('', - url(r'^polls/$', 'polls.views.index'), - url(r'^polls/(?P\d+)/$', 'polls.views.detail'), - url(r'^polls/(?P\d+)/results/$', 'polls.views.results'), - url(r'^polls/(?P\d+)/vote/$', 'polls.views.vote'), - ) - -Namely, ``polls.views`` is in every callback. - -Because this is a common case, the URLconf framework provides a shortcut for -common prefixes. You can factor out the common prefixes and add them as the -first argument to :func:`~django.conf.urls.patterns`, like so:: - - urlpatterns = patterns('polls.views', - url(r'^polls/$', 'index'), - url(r'^polls/(?P\d+)/$', 'detail'), - url(r'^polls/(?P\d+)/results/$', 'results'), - url(r'^polls/(?P\d+)/vote/$', 'vote'), - ) - -This is functionally identical to the previous formatting. It's just a bit -tidier. - -Since you generally don't want the prefix for one app to be applied to every -callback in your URLconf, you can concatenate multiple -:func:`~django.conf.urls.patterns`. Your full ``mysite/urls.py`` might -now look like this:: - - from django.conf.urls import patterns, include, url - - from django.contrib import admin - admin.autodiscover() - - urlpatterns = patterns('polls.views', - url(r'^polls/$', 'index'), - url(r'^polls/(?P\d+)/$', 'detail'), - url(r'^polls/(?P\d+)/results/$', 'results'), - url(r'^polls/(?P\d+)/vote/$', 'vote'), - ) - - urlpatterns += patterns('', - url(r'^admin/', include(admin.site.urls)), - ) - -Decoupling the URLconfs -======================= - -While we're at it, we should take the time to decouple our poll-app URLs from -our Django project configuration. Django apps are meant to be pluggable -- that -is, each particular app should be transferable to another Django installation -with minimal fuss. - -Our poll app is pretty decoupled at this point, thanks to the strict directory -structure that ``python manage.py startapp`` created, but one part of it is -coupled to the Django settings: The URLconf. - -We've been editing the URLs in ``mysite/urls.py``, but the URL design of an -app is specific to the app, not to the Django installation -- so let's move the -URLs within the app directory. - -Copy the file ``mysite/urls.py`` to ``polls/urls.py``. Then, change -``mysite/urls.py`` to remove the poll-specific URLs and insert an -:func:`~django.conf.urls.include`, leaving you with:: - - from django.conf.urls import patterns, include, url - - from django.contrib import admin - admin.autodiscover() - - urlpatterns = patterns('', - url(r'^polls/', include('polls.urls')), - url(r'^admin/', include(admin.site.urls)), - ) - -:func:`~django.conf.urls.include` simply references another URLconf. -Note that the regular expression doesn't have a ``$`` (end-of-string match -character) but has the trailing slash. Whenever Django encounters -:func:`~django.conf.urls.include`, it chops off whatever part of the -URL matched up to that point and sends the remaining string to the included -URLconf for further processing. - -Here's what happens if a user goes to "/polls/34/" in this system: - -* Django will find the match at ``'^polls/'`` - -* Then, Django will strip off the matching text (``"polls/"``) and send the - remaining text -- ``"34/"`` -- to the 'polls.urls' URLconf for - further processing. - -Now that we've decoupled that, we need to decouple the ``polls.urls`` -URLconf by removing the leading "polls/" from each line, removing the -lines registering the admin site, and removing the ``include`` import which -is no longer used. Your ``polls/urls.py`` file should now look like -this:: - - from django.conf.urls import patterns, url - - urlpatterns = patterns('polls.views', - url(r'^$', 'index'), - url(r'^(?P\d+)/$', 'detail'), - url(r'^(?P\d+)/results/$', 'results'), - url(r'^(?P\d+)/vote/$', 'vote'), - ) - -The idea behind :func:`~django.conf.urls.include` and URLconf -decoupling is to make it easy to plug-and-play URLs. Now that polls are in their -own URLconf, they can be placed under "/polls/", or under "/fun_polls/", or -under "/content/polls/", or any other path root, and the app will still work. - -All the poll app cares about is its relative path, not its absolute path. - Removing hardcoded URLs in templates ------------------------------------- +==================================== -Remember, when we wrote the link to a poll in our template, the link was -partially hardcoded like this: +Remember, when we wrote the link to a poll in the ``polls/index.html`` +template, the link was partially hardcoded like this: .. code-block:: html+django
  • {{ poll.question }}
  • -To use the decoupled URLs we've just introduced, replace the hardcoded link -with the :ttag:`url` template tag: +The problem with this hardcoded, tightly-coupled approach is that it becomes +challenging to change URLs on projects with a lot of templates. However, since +you defined the name argument in the :func:`~django.conf.urls.url` functions in +the ``polls.urls`` module, you can remove a reliance on specific URL paths +defined in your url configurations by using the ``{% url %}`` template tag: .. code-block:: html+django -
  • {{ poll.question }}
  • +
  • {{ poll.question }}
  • .. note:: - If ``{% url 'polls.views.detail' poll.id %}`` (with quotes) doesn't work, - but ``{% url polls.views.detail poll.id %}`` (without quotes) does, that - means you're using a version of Django ≤ 1.4. In this case, add the - following declaration at the top of your template: + If ``{% url 'detail' poll.id %}`` (with quotes) doesn't work, but + ``{% url detail poll.id %}`` (without quotes) does, that means you're + using a version of Django < 1.5. In this case, add the following + declaration at the top of your template: .. code-block:: html+django {% load url from future %} +The way this works is by looking up the URL definition as specified in the +``polls.urls`` module. You can see exactly where the URL name of 'detail' is +defined below:: + + ... + # the 'name' value as called by the {% url %} template tag + url(r'^(?P\d+)/$', views.detail, name='detail'), + ... + +If you want to change the URL of the polls detail view to something else, +perhaps to something like ``polls/specifics/12/`` instead of doing it in the +template (or templates) you would change it in ``polls/urls.py``:: + + ... + # added the word 'specifics' + url(r'^specifics/(?P\d+)/$', views.detail, name='detail'), + ... + +Namespacing URL names +====================== + +The tutorial project has just one app, ``polls``. In real Django projects, +there might be five, ten, twenty apps or more. How does Django differentiate +the URL names between them? For example, the ``polls`` app has a ``detail`` +view, and so might an app on the same project that is for a blog. How does one +make it so that Django knows which app view to create for a url when using the +``{% url %}`` template tag? + +The answer is to add namespaces to your root URLconf. In the +``mysite/urls.py`` file, go ahead and change it to include namespacing:: + + from django.conf.urls import patterns, include, url + + from django.contrib import admin + admin.autodiscover() + + urlpatterns = patterns('', + url(r'^polls/', include('polls.urls', namespace="polls")), + url(r'^admin/', include(admin.site.urls)), + ) + +Now change your ``polls/index.html`` template from: + +.. code-block:: html+django + +
  • {{ poll.question }}
  • + +to point at the namespaced detail view: + +.. code-block:: html+django + +
  • {{ poll.question }}
  • + When you're comfortable with writing views, read :doc:`part 4 of this tutorial ` to learn about simple form processing and generic views. diff --git a/docs/intro/tutorial04.txt b/docs/intro/tutorial04.txt index 49e597ca29..8909caf98b 100644 --- a/docs/intro/tutorial04.txt +++ b/docs/intro/tutorial04.txt @@ -18,7 +18,7 @@ tutorial, so that the template contains an HTML ``
    `` element: {% if error_message %}

    {{ error_message }}

    {% endif %} - + {% csrf_token %} {% for choice in poll.choice_set.all %} @@ -35,7 +35,7 @@ A quick rundown: selects one of the radio buttons and submits the form, it'll send the POST data ``choice=3``. This is HTML Forms 101. -* We set the form's ``action`` to ``{% url 'polls.views.vote' poll.id %}``, and we +* We set the form's ``action`` to ``{% url 'polls:vote' poll.id %}``, and we set ``method="post"``. Using ``method="post"`` (as opposed to ``method="get"``) is very important, because the act of submitting this form will alter data server-side. Whenever you create a form that alters @@ -52,34 +52,18 @@ A quick rundown: forms that are targeted at internal URLs should use the :ttag:`{% csrf_token %}` template tag. -The :ttag:`{% csrf_token %}` tag requires information from the -request object, which is not normally accessible from within the template -context. To fix this, a small adjustment needs to be made to the ``detail`` -view, so that it looks like the following:: - - from django.template import RequestContext - # ... - def detail(request, poll_id): - p = get_object_or_404(Poll, pk=poll_id) - return render_to_response('polls/detail.html', {'poll': p}, - context_instance=RequestContext(request)) - -The details of how this works are explained in the documentation for -:ref:`RequestContext `. - Now, let's create a Django view that handles the submitted data and does something with it. Remember, in :doc:`Tutorial 3 `, we created a URLconf for the polls application that includes this line:: - (r'^(?P\d+)/vote/$', 'vote'), + url(r'^(?P\d+)/vote/$', views.vote, name='vote'), We also created a dummy implementation of the ``vote()`` function. Let's create a real version. Add the following to ``polls/views.py``:: - from django.shortcuts import get_object_or_404, render_to_response + from django.shortcuts import get_object_or_404, render from django.http import HttpResponseRedirect, HttpResponse from django.core.urlresolvers import reverse - from django.template import RequestContext from polls.models import Choice, Poll # ... def vote(request, poll_id): @@ -88,17 +72,17 @@ create a real version. Add the following to ``polls/views.py``:: selected_choice = p.choice_set.get(pk=request.POST['choice']) except (KeyError, Choice.DoesNotExist): # Redisplay the poll voting form. - return render_to_response('polls/detail.html', { + return render(request, 'polls/detail.html', { 'poll': p, 'error_message': "You didn't select a choice.", - }, context_instance=RequestContext(request)) + }) else: selected_choice.votes += 1 selected_choice.save() # Always return an HttpResponseRedirect after successfully dealing # with POST data. This prevents data from being posted twice if a # user hits the Back button. - return HttpResponseRedirect(reverse('polls.views.results', args=(p.id,))) + return HttpResponseRedirect(reverse('polls:results', args=(p.id,))) This code includes a few things we haven't covered yet in this tutorial: @@ -142,8 +126,7 @@ This code includes a few things we haven't covered yet in this tutorial: '/polls/3/results/' ... where the ``3`` is the value of ``p.id``. This redirected URL will - then call the ``'results'`` view to display the final page. Note that you - need to use the full name of the view here (including the prefix). + then call the ``'results'`` view to display the final page. As mentioned in Tutorial 3, ``request`` is a :class:`~django.http.HttpRequest` object. For more on :class:`~django.http.HttpRequest` objects, see the @@ -153,14 +136,14 @@ After somebody votes in a poll, the ``vote()`` view redirects to the results page for the poll. Let's write that view:: def results(request, poll_id): - p = get_object_or_404(Poll, pk=poll_id) - return render_to_response('polls/results.html', {'poll': p}) + poll = get_object_or_404(Poll, pk=poll_id) + return render(request, 'polls/results.html', {'poll': poll}) This is almost exactly the same as the ``detail()`` view from :doc:`Tutorial 3 `. The only difference is the template name. We'll fix this redundancy later. -Now, create a ``results.html`` template: +Now, create a ``polls/results.html`` template: .. code-block:: html+django @@ -172,7 +155,7 @@ Now, create a ``results.html`` template: {% endfor %} - Vote again? + Vote again? Now, go to ``/polls/1/`` in your browser and vote in the poll. You should see a results page that gets updated each time you vote. If you submit the form @@ -215,19 +198,7 @@ Read on for details. You should know basic math before you start using a calculator. -First, open the ``polls/urls.py`` URLconf. It looks like this, according to the -tutorial so far:: - - from django.conf.urls import patterns, url - - urlpatterns = patterns('polls.views', - url(r'^$', 'index'), - url(r'^(?P\d+)/$', 'detail'), - url(r'^(?P\d+)/results/$', 'results'), - url(r'^(?P\d+)/vote/$', 'vote'), - ) - -Change it like so:: +First, open the ``polls/urls.py`` URLconf and change it like so:: from django.conf.urls import patterns, url from django.views.generic import DetailView, ListView @@ -239,18 +210,18 @@ Change it like so:: queryset=Poll.objects.order_by('-pub_date')[:5], context_object_name='latest_poll_list', template_name='polls/index.html'), - name='poll_index'), + name='index'), url(r'^(?P\d+)/$', DetailView.as_view( model=Poll, template_name='polls/detail.html'), - name='poll_detail'), + name='detail'), url(r'^(?P\d+)/results/$', DetailView.as_view( model=Poll, template_name='polls/results.html'), - name='poll_results'), - url(r'^(?P\d+)/vote/$', 'polls.views.vote'), + name='results'), + url(r'^(?P\d+)/vote/$', 'polls.views.vote', name='vote'), ) We're using two generic views here: @@ -267,15 +238,6 @@ two views abstract the concepts of "display a list of objects" and ``"pk"``, so we've changed ``poll_id`` to ``pk`` for the generic views. -* We've added the ``name`` argument to the views (e.g. ``name='poll_results'``) - so that we have a way to refer to their URL later on (see the - documentation about :ref:`naming URL patterns - ` for information). We're also using the - :func:`~django.conf.urls.url` function from - :mod:`django.conf.urls` here. It's a good habit to use - :func:`~django.conf.urls.url` when you are providing a - pattern name like this. - By default, the :class:`~django.views.generic.list.DetailView` generic view uses a template called ``/_detail.html``. In our case, it'll use the template ``"polls/poll_detail.html"``. The @@ -308,41 +270,13 @@ You can now delete the ``index()``, ``detail()`` and ``results()`` views from ``polls/views.py``. We don't need them anymore -- they have been replaced by generic views. -The last thing to do is fix the URL handling to account for the use of -generic views. In the vote view above, we used the -:func:`~django.core.urlresolvers.reverse` function to avoid -hard-coding our URLs. Now that we've switched to a generic view, we'll -need to change the :func:`~django.core.urlresolvers.reverse` call to -point back to our new generic view. We can't simply use the view -function anymore -- generic views can be (and are) used multiple times --- but we can use the name we've given:: - - return HttpResponseRedirect(reverse('poll_results', args=(p.id,))) - -The same rule apply for the :ttag:`url` template tag. For example in the -``results.html`` template: - -.. code-block:: html+django - - Vote again? - Run the server, and use your new polling app based on generic views. For full details on generic views, see the :doc:`generic views documentation `. -Coming soon -=========== +What's next? +============ -The tutorial ends here for the time being. Future installments of the tutorial -will cover: - -* Advanced form processing -* Using the RSS framework -* Using the cache framework -* Using the comments framework -* Advanced admin features: Permissions -* Advanced admin features: Custom JavaScript - -In the meantime, you might want to check out some pointers on :doc:`where to go -from here ` +The tutorial ends here for the time being. In the meantime, you might want to +check out some pointers on :doc:`where to go from here `. diff --git a/docs/intro/whatsnext.txt b/docs/intro/whatsnext.txt index ea4b18de03..500a858d47 100644 --- a/docs/intro/whatsnext.txt +++ b/docs/intro/whatsnext.txt @@ -216,15 +216,13 @@ We follow this policy: "New in version X.Y", being X.Y the next release version (hence, the one being developed). -* Documentation for a particular Django release is frozen once the version - has been released officially. It remains a snapshot of the docs as of the - moment of the release. We will make exceptions to this rule in - the case of retroactive security updates or other such retroactive - changes. Once documentation is frozen, we add a note to the top of each - frozen document that says "These docs are frozen for Django version XXX" - and links to the current version of that document. +* Documentation fixes and improvements may be backported to the last release + branch, at the discretion of the committer, however, once a version of + Django is :ref:`no longer supported`, that + version of the docs won't get any further updates. * The `main documentation Web page`_ includes links to documentation for - all previous versions. + all previous versions. Be sure you are using the version of the docs + corresponding to the version of Django you are using! .. _main documentation Web page: https://docs.djangoproject.com/en/dev/ diff --git a/docs/misc/api-stability.txt b/docs/misc/api-stability.txt index 2839ee3594..4f232e795b 100644 --- a/docs/misc/api-stability.txt +++ b/docs/misc/api-stability.txt @@ -155,8 +155,6 @@ Certain APIs are explicitly marked as "internal" in a couple of ways: Local flavors ------------- -.. versionchanged:: 1.3 - :mod:`django.contrib.localflavor` contains assorted pieces of code that are useful for particular countries or cultures. This data is local in nature, and is subject to change on timelines that will diff --git a/docs/misc/distributions.txt b/docs/misc/distributions.txt index 729ce0717b..1b324234d1 100644 --- a/docs/misc/distributions.txt +++ b/docs/misc/distributions.txt @@ -11,7 +11,7 @@ requires. Typically, these packages are based on the latest stable release of Django, so if you want to use the development version of Django you'll need to follow the instructions for :ref:`installing the development version -` from our Subversion repository. +` from our Git repository. If you're using Linux or a Unix installation, such as OpenSolaris, check with your distributor to see if they already package Django. If diff --git a/docs/ref/class-based-views/generic-date-based.txt b/docs/ref/class-based-views/generic-date-based.txt index 64b269f514..c6af23e421 100644 --- a/docs/ref/class-based-views/generic-date-based.txt +++ b/docs/ref/class-based-views/generic-date-based.txt @@ -87,16 +87,24 @@ YearArchiveView * ``year``: A :class:`~datetime.date` object representing the given year. + .. versionchanged:: 1.5 + + Previously, this returned a string. + * ``next_year``: A :class:`~datetime.date` object representing the first day of the next year, according to :attr:`~BaseDateListView.allow_empty` and :attr:`~DateMixin.allow_future`. + .. versionadded:: 1.5 + * ``previous_year``: A :class:`~datetime.date` object representing the first day of the previous year, according to :attr:`~BaseDateListView.allow_empty` and :attr:`~DateMixin.allow_future`. + .. versionadded:: 1.5 + **Notes** * Uses a default ``template_name_suffix`` of ``_archive_year``. diff --git a/docs/ref/class-based-views/mixins-date-based.txt b/docs/ref/class-based-views/mixins-date-based.txt index 01181ebb6c..561e525e70 100644 --- a/docs/ref/class-based-views/mixins-date-based.txt +++ b/docs/ref/class-based-views/mixins-date-based.txt @@ -318,12 +318,16 @@ BaseDateListView Returns the aggregation period for ``date_list``. Returns :attr:`~BaseDateListView.date_list_period` by default. - .. method:: get_date_list(queryset, date_type=None) + .. method:: get_date_list(queryset, date_type=None, ordering='ASC') Returns the list of dates of type ``date_type`` for which ``queryset`` contains entries. For example, ``get_date_list(qs, 'year')`` will return the list of years for which ``qs`` has entries. If ``date_type`` isn't provided, the result of - :meth:`BaseDateListView.get_date_list_period` is used. See - :meth:`~django.db.models.query.QuerySet.dates()` for the ways that the - ``date_type`` argument can be used. + :meth:`~BaseDateListView.get_date_list_period` is used. ``date_type`` + and ``ordering`` are simply passed to + :meth:`QuerySet.dates()`. + + .. versionchanged:: 1.5 + The ``ordering`` parameter was added, and the default order was + changed to ascending. diff --git a/docs/ref/contrib/admin/_images/raw_id_fields.png b/docs/ref/contrib/admin/_images/raw_id_fields.png new file mode 100644 index 0000000000..0774c40469 Binary files /dev/null and b/docs/ref/contrib/admin/_images/raw_id_fields.png differ diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 66a5a2cc4f..6ed929cb7d 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -60,6 +60,8 @@ Other topics For information about serving the static files (images, JavaScript, and CSS) associated with the admin in production, see :ref:`serving-files`. + Having problems? Try :doc:`/faq/admin`. + ``ModelAdmin`` objects ====================== @@ -129,8 +131,6 @@ subclass:: date_hierarchy = 'pub_date' - .. versionadded:: 1.3 - This will intelligently populate itself based on available data, e.g. if all the dates are in one month, it'll show the day-level drill-down only. @@ -307,7 +307,9 @@ subclass:: By default a ``ModelForm`` is dynamically created for your model. It is used to create the form presented on both the add/change pages. You can easily provide your own ``ModelForm`` to override any default form behavior - on the add/change pages. + on the add/change pages. Alternatively, you can customize the default + form rather than specifying an entirely new one by using the + :meth:`ModelAdmin.get_form` method. For an example see the section `Adding custom validation to the admin`_. @@ -373,7 +375,8 @@ subclass:: .. attribute:: ModelAdmin.inlines - See :class:`InlineModelAdmin` objects below. + See :class:`InlineModelAdmin` objects below as well as + :meth:`ModelAdmin.get_formsets`. .. attribute:: ModelAdmin.list_display @@ -576,8 +579,6 @@ subclass:: class PersonAdmin(ModelAdmin): list_filter = ('is_staff', 'company') - .. versionadded:: 1.3 - Field names in ``list_filter`` can also span relations using the ``__`` lookup, for example:: @@ -748,8 +749,6 @@ subclass:: .. attribute:: ModelAdmin.paginator - .. versionadded:: 1.3 - The paginator class to be used for pagination. By default, :class:`django.core.paginator.Paginator` is used. If the custom paginator class doesn't have the same constructor interface as @@ -804,6 +803,14 @@ subclass:: class ArticleAdmin(admin.ModelAdmin): raw_id_fields = ("newspaper",) + The ``raw_id_fields`` ``Input`` widget should contain a primary key if the + field is a ``ForeignKey`` or a comma separated list of values if the field + is a ``ManyToManyField``. The ``raw_id_fields`` widget shows a magnifying + glass button next to the field which allows users to search for and select + a value: + + .. image:: _images/raw_id_fields.png + .. attribute:: ModelAdmin.readonly_fields By default the admin shows all fields as editable. Any fields in this @@ -966,8 +973,6 @@ templates used by the :class:`ModelAdmin` views: .. method:: ModelAdmin.delete_model(self, request, obj) - .. versionadded:: 1.3 - The ``delete_model`` method is given the ``HttpRequest`` and a model instance. Use this method to do pre- or post-delete operations. @@ -1049,6 +1054,16 @@ templates used by the :class:`ModelAdmin` views: changelist that will be linked to the change view, as described in the :attr:`ModelAdmin.list_display_links` section. +.. method:: ModelAdmin.get_inline_instances(self, request, obj=None) + + .. versionadded:: 1.5 + + The ``get_inline_instances`` method is given the ``HttpRequest`` and the + ``obj`` being edited (or ``None`` on an add form) and is expected to return + a ``list`` or ``tuple`` of :class:`~django.contrib.admin.InlineModelAdmin` + objects, as described below in the :class:`~django.contrib.admin.InlineModelAdmin` + section. + .. method:: ModelAdmin.get_urls(self) The ``get_urls`` method on a ``ModelAdmin`` returns the URLs to be used for @@ -1115,6 +1130,38 @@ templates used by the :class:`ModelAdmin` views: (r'^my_view/$', self.admin_site.admin_view(self.my_view, cacheable=True)) +.. method:: ModelAdmin.get_form(self, request, obj=None, **kwargs) + + Returns a :class:`~django.forms.ModelForm` class for use in the admin add + and change views, see :meth:`add_view` and :meth:`change_view`. + + If you wanted to hide a field from non-superusers, for example, you could + override ``get_form`` as follows:: + + class MyModelAdmin(admin.ModelAdmin): + def get_form(self, request, obj=None, **kwargs): + self.exclude = [] + if not request.user.is_superuser: + self.exclude.append('field_to_hide') + return super(MyModelAdmin, self).get_form(request, obj, **kwargs) + +.. method:: ModelAdmin.get_formsets(self, request, obj=None) + + Yields :class:`InlineModelAdmin`\s for use in admin add and change views. + + For example if you wanted to display a particular inline only in the change + view, you could override ``get_formsets`` as follows:: + + class MyModelAdmin(admin.ModelAdmin): + inlines = [MyInline, SomeOtherInline] + + def get_formsets(self, request, obj=None): + for inline in self.get_inline_instances(request, obj): + # hide MyInline in the add view + if isinstance(inline, MyInline) and obj is None: + continue + yield inline.get_formset(request, obj) + .. method:: ModelAdmin.formfield_for_foreignkey(self, db_field, request, **kwargs) The ``formfield_for_foreignkey`` method on a ``ModelAdmin`` allows you to @@ -1213,8 +1260,6 @@ templates used by the :class:`ModelAdmin` views: .. method:: ModelAdmin.get_paginator(queryset, per_page, orphans=0, allow_empty_first_page=True) - .. versionadded:: 1.3 - Returns an instance of the paginator to use for this view. By default, instantiates an instance of :attr:`paginator`. @@ -1295,8 +1340,6 @@ on your ``ModelAdmin``:: } js = ("my_code.js",) -.. versionchanged:: 1.3 - The :doc:`staticfiles app ` prepends :setting:`STATIC_URL` (or :setting:`MEDIA_URL` if :setting:`STATIC_URL` is ``None``) to any media paths. The same rules apply as :ref:`regular media @@ -1394,18 +1437,15 @@ adds some of its own (the shared features are actually defined in the - :attr:`~ModelAdmin.exclude` - :attr:`~ModelAdmin.filter_horizontal` - :attr:`~ModelAdmin.filter_vertical` +- :attr:`~ModelAdmin.ordering` - :attr:`~ModelAdmin.prepopulated_fields` +- :meth:`~ModelAdmin.queryset` - :attr:`~ModelAdmin.radio_fields` - :attr:`~ModelAdmin.readonly_fields` - :attr:`~InlineModelAdmin.raw_id_fields` - :meth:`~ModelAdmin.formfield_for_foreignkey` - :meth:`~ModelAdmin.formfield_for_manytomany` -.. versionadded:: 1.3 - -- :attr:`~ModelAdmin.ordering` -- :meth:`~ModelAdmin.queryset` - .. versionadded:: 1.4 - :meth:`~ModelAdmin.has_add_permission` @@ -1436,8 +1476,6 @@ The ``InlineModelAdmin`` class adds: through to ``inlineformset_factory`` when creating the formset for this inline. - .. _ref-contrib-admin-inline-extra: - .. attribute:: InlineModelAdmin.extra This controls the number of extra forms the formset will display in @@ -1813,8 +1851,6 @@ Templates can override or extend base admin templates as described in .. attribute:: AdminSite.login_form - .. versionadded:: 1.3 - Subclass of :class:`~django.contrib.auth.forms.AuthenticationForm` that will be used by the admin site login view. diff --git a/docs/ref/contrib/comments/example.txt b/docs/ref/contrib/comments/example.txt index e78d83c35d..2bff778c2f 100644 --- a/docs/ref/contrib/comments/example.txt +++ b/docs/ref/contrib/comments/example.txt @@ -152,27 +152,6 @@ enable it in your project's ``urls.py``: Now you should have the latest comment feeds being served off ``/feeds/latest/``. -.. versionchanged:: 1.3 - -Prior to Django 1.3, the LatestCommentFeed was deployed using the -syndication feed view: - -.. code-block:: python - - from django.conf.urls import patterns - from django.contrib.comments.feeds import LatestCommentFeed - - feeds = { - 'latest': LatestCommentFeed, - } - - urlpatterns = patterns('', - # ... - (r'^feeds/(?P.*)/$', 'django.contrib.syndication.views.feed', - {'feed_dict': feeds}), - # ... - ) - Moderation ========== diff --git a/docs/ref/contrib/comments/index.txt b/docs/ref/contrib/comments/index.txt index 4b1dd96280..1c6ff7c7ed 100644 --- a/docs/ref/contrib/comments/index.txt +++ b/docs/ref/contrib/comments/index.txt @@ -254,6 +254,56 @@ you can include a hidden form input called ``next`` in your comment form. For ex +Providing a comment form for authenticated users +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a user is already authenticated, it makes little sense to display the name, +email, and URL fields, since these can already be retrieved from their login +data and profile. In addition, some sites will only accept comments from +authenticated users. + +To provide a comment form for authenticated users, you can manually provide the +additional fields expected by the Django comments framework. For example, +assuming comments are attached to the model "object":: + + {% if user.is_authenticated %} + {% get_comment_form for object as form %} + + {% csrf_token %} + {{ form.comment }} + {{ form.honeypot }} + {{ form.content_type }} + {{ form.object_pk }} + {{ form.timestamp }} + {{ form.security_hash }} + + + + {% else %} +

    Please log in to leave a comment.

    + {% endif %} + +The honeypot, content_type, object_pk, timestamp, and security_hash fields are +fields that would have been created automatically if you had simply used +``{{ form }}`` in your template, and are referred to in `Notes on the comment +form`_ below. + +Note that we do not need to specify the user to be associated with comments +submitted by authenticated users. This is possible because the :doc:`Built-in +Comment Models` that come with Django associate +comments with authenticated users by default. + +In this example, the honeypot field will still be visible to the user; you'll +need to hide that field in your CSS:: + + #id_honeypot { + display: none; + } + +If you want to accept either anonymous or authenticated comments, replace the +contents of the "else" clause above with a standard comment form and the right +thing will happen whether a user is logged in or not. + .. _notes-on-the-comment-form: Notes on the comment form diff --git a/docs/ref/contrib/comments/moderation.txt b/docs/ref/contrib/comments/moderation.txt index f03c7fda0d..39b3ea7913 100644 --- a/docs/ref/contrib/comments/moderation.txt +++ b/docs/ref/contrib/comments/moderation.txt @@ -136,10 +136,6 @@ Simply subclassing :class:`CommentModerator` and changing the values of these options will automatically enable the various moderation methods for any models registered using the subclass. -.. versionchanged:: 1.3 - -``moderate_after`` and ``close_after`` now accept 0 as a valid value. - Adding custom moderation methods -------------------------------- diff --git a/docs/ref/contrib/contenttypes.txt b/docs/ref/contrib/contenttypes.txt index e98da6e429..dfbeabc302 100644 --- a/docs/ref/contrib/contenttypes.txt +++ b/docs/ref/contrib/contenttypes.txt @@ -423,8 +423,6 @@ pointing at it will be deleted as well. In the example above, this means that if a ``Bookmark`` object were deleted, any ``TaggedItem`` objects pointing at it would be deleted at the same time. -.. versionadded:: 1.3 - Unlike :class:`~django.db.models.ForeignKey`, :class:`~django.contrib.contenttypes.generic.GenericForeignKey` does not accept an :attr:`~django.db.models.ForeignKey.on_delete` argument to customize this diff --git a/docs/ref/contrib/csrf.txt b/docs/ref/contrib/csrf.txt index 8d352ff8b2..32d8a705bc 100644 --- a/docs/ref/contrib/csrf.txt +++ b/docs/ref/contrib/csrf.txt @@ -72,9 +72,9 @@ To enable CSRF protection for your views, follow these steps: :func:`~django.shortcuts.render_to_response()` wrapper that takes care of this step for you. -The utility script ``extras/csrf_migration_helper.py`` can help to automate the -finding of code and templates that may need these steps. It contains full help -on how to use it. +The utility script ``extras/csrf_migration_helper.py`` (located in the Django +distribution, but not installed) can help to automate the finding of code and +templates that may need these steps. It contains full help on how to use it. .. _csrf-ajax: diff --git a/docs/ref/contrib/flatpages.txt b/docs/ref/contrib/flatpages.txt index 3de449708f..7ff9165642 100644 --- a/docs/ref/contrib/flatpages.txt +++ b/docs/ref/contrib/flatpages.txt @@ -158,9 +158,7 @@ For more on middleware, read the :doc:`middleware docs :class:`~django.contrib.flatpages.middleware.FlatpageFallbackMiddleware` only steps in once another view has successfully produced a 404 response. If another view or middleware class attempts to produce a 404 but ends up - raising an exception instead (such as a ``TemplateDoesNotExist`` - exception if your site does not have an appropriate template to - use for HTTP 404 responses), the response will become an HTTP 500 + raising an exception instead, the response will become an HTTP 500 ("Internal Server Error") and the :class:`~django.contrib.flatpages.middleware.FlatpageFallbackMiddleware` will not attempt to serve a flat page. @@ -239,8 +237,6 @@ template. Getting a list of :class:`~django.contrib.flatpages.models.FlatPage` objects in your templates ============================================================================================== -.. versionadded:: 1.3 - The flatpages app provides a template tag that allows you to iterate over all of the available flatpages on the :ref:`current site `. diff --git a/docs/ref/contrib/formtools/form-wizard.txt b/docs/ref/contrib/formtools/form-wizard.txt index d5231de3e5..0ced1bf155 100644 --- a/docs/ref/contrib/formtools/form-wizard.txt +++ b/docs/ref/contrib/formtools/form-wizard.txt @@ -226,7 +226,7 @@ Hooking the wizard into a URLconf --------------------------------- Finally, we need to specify which forms to use in the wizard, and then -deploy the new :class:`WizardView` object at an URL in the ``urls.py``. The +deploy the new :class:`WizardView` object at a URL in the ``urls.py``. The wizard's :meth:`as_view` method takes a list of your :class:`~django.forms.Form` classes as an argument during instantiation:: diff --git a/docs/ref/contrib/gis/admin.txt b/docs/ref/contrib/gis/admin.txt index aa6ba58630..d1a9fc1dcb 100644 --- a/docs/ref/contrib/gis/admin.txt +++ b/docs/ref/contrib/gis/admin.txt @@ -45,7 +45,7 @@ GeoDjango's admin site .. attribute:: openlayers_url Link to the URL of the OpenLayers JavaScript. Defaults to - ``'http://openlayers.org/api/2.8/OpenLayers.js'``. + ``'http://openlayers.org/api/2.11/OpenLayers.js'``. .. attribute:: modifiable diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt index 318110ef04..519f79f0d4 100644 --- a/docs/ref/contrib/gis/db-api.txt +++ b/docs/ref/contrib/gis/db-api.txt @@ -282,7 +282,7 @@ Method PostGIS Oracle SpatiaLite :meth:`GeoQuerySet.extent3d` X :meth:`GeoQuerySet.force_rhr` X :meth:`GeoQuerySet.geohash` X -:meth:`GeoQuerySet.geojson` X +:meth:`GeoQuerySet.geojson` X X :meth:`GeoQuerySet.gml` X X X :meth:`GeoQuerySet.intersection` X X X :meth:`GeoQuerySet.kml` X X diff --git a/docs/ref/contrib/gis/geoip.txt b/docs/ref/contrib/gis/geoip.txt index a30573d860..e37c4c60b0 100644 --- a/docs/ref/contrib/gis/geoip.txt +++ b/docs/ref/contrib/gis/geoip.txt @@ -23,10 +23,10 @@ to the GPL-licensed `Python GeoIP`__ interface provided by MaxMind. In order to perform IP-based geolocation, the :class:`GeoIP` object requires the GeoIP C libary and either the GeoIP `Country`__ or `City`__ datasets in binary format (the CSV files will not work!). These datasets may be -`downloaded from MaxMind`__. Grab the ``GeoIP.dat.gz`` and ``GeoLiteCity.dat.gz`` -and unzip them in a directory corresponding to what you set -:setting:`GEOIP_PATH` with in your settings. See the example and reference below -for more details. +`downloaded from MaxMind`__. Grab the ``GeoLiteCountry/GeoIP.dat.gz`` and +``GeoLiteCity.dat.gz`` files and unzip them in a directory corresponding to what +you set :setting:`GEOIP_PATH` with in your settings. See the example and +reference below for more details. __ http://www.maxmind.com/app/c __ http://www.maxmind.com/app/python diff --git a/docs/ref/contrib/gis/geoquerysets.txt b/docs/ref/contrib/gis/geoquerysets.txt index eeec2e2133..69280dc028 100644 --- a/docs/ref/contrib/gis/geoquerysets.txt +++ b/docs/ref/contrib/gis/geoquerysets.txt @@ -947,7 +947,7 @@ __ http://geohash.org/ .. method:: GeoQuerySet.geojson(**kwargs) -*Availability*: PostGIS +*Availability*: PostGIS, SpatiaLite Attaches a ``geojson`` attribute to every model in the queryset that contains the `GeoJSON`__ representation of the geometry. diff --git a/docs/ref/contrib/gis/geos.txt b/docs/ref/contrib/gis/geos.txt index f4e706d275..eb20b1f411 100644 --- a/docs/ref/contrib/gis/geos.txt +++ b/docs/ref/contrib/gis/geos.txt @@ -163,6 +163,11 @@ WKB / EWKB ``buffer`` GeoJSON ``str`` or ``unicode`` ============= ====================== +.. note:: + + The new 3D/4D WKT notation with an intermediary Z or M (like + ``POINT Z (3, 4, 5)``) is only supported with GEOS 3.3.0 or later. + Properties ~~~~~~~~~~ @@ -232,8 +237,6 @@ Returns a boolean indicating whether the geometry is valid. .. attribute:: GEOSGeometry.valid_reason -.. versionadded:: 1.3 - Returns a string describing the reason why a geometry is invalid. .. attribute:: GEOSGeometry.srid @@ -270,14 +273,18 @@ Essentially the SRID is prepended to the WKT representation, for example .. attribute:: GEOSGeometry.hex Returns the WKB of this Geometry in hexadecimal form. Please note -that the SRID and Z values are not included in this representation +that the SRID value is not included in this representation because it is not a part of the OGC specification (use the :attr:`GEOSGeometry.hexewkb` property instead). +.. versionchanged:: 1.5 + + Prior to Django 1.5, the Z value of the geometry was dropped. + .. attribute:: GEOSGeometry.hexewkb Returns the EWKB of this Geometry in hexadecimal form. This is an -extension of the WKB specification that includes SRID and Z values +extension of the WKB specification that includes the SRID value that are a part of this geometry. .. note:: @@ -316,16 +323,20 @@ correspondg to the GEOS geometry. .. attribute:: GEOSGeometry.wkb Returns the WKB (Well-Known Binary) representation of this Geometry -as a Python buffer. SRID and Z values are not included, use the +as a Python buffer. SRID value is not included, use the :attr:`GEOSGeometry.ewkb` property instead. +.. versionchanged:: 1.5 + + Prior to Django 1.5, the Z value of the geometry was dropped. + .. _ewkb: .. attribute:: GEOSGeometry.ewkb Return the EWKB representation of this Geometry as a Python buffer. This is an extension of the WKB specification that includes any SRID -and Z values that are a part of this geometry. +value that are a part of this geometry. .. note:: @@ -413,11 +424,36 @@ quarter circle (defaults is 8). Returns a :class:`GEOSGeometry` representing the points making up this geometry that do not make up other. +.. method:: GEOSGeometry.interpolate(distance) +.. method:: GEOSGeometry.interpolate_normalized(distance) + +.. versionadded:: 1.5 + +Given a distance (float), returns the point (or closest point) within the +geometry (:class:`LineString` or :class:`MultiLineString`) at that distance. +The normalized version takes the distance as a float between 0 (origin) and 1 +(endpoint). + +Reverse of :meth:`GEOSGeometry.project`. + .. method:: GEOSGeometry:intersection(other) Returns a :class:`GEOSGeometry` representing the points shared by this geometry and other. +.. method:: GEOSGeometry.project(point) +.. method:: GEOSGeometry.project_normalized(point) + +.. versionadded:: 1.5 + +Returns the distance (float) from the origin of the geometry +(:class:`LineString` or :class:`MultiLineString`) to the point projected on the +geometry (that is to a point of the line the closest to the given point). +The normalized version returns the distance as a float between 0 (origin) and 1 +(endpoint). + +Reverse of :meth:`GEOSGeometry.interpolate`. + .. method:: GEOSGeometry.relate(other) Returns the DE-9IM intersection matrix (a string) representing the @@ -530,8 +566,6 @@ corresponding to the SRID of the geometry or ``None``. .. method:: GEOSGeometry.transform(ct, clone=False) -.. versionchanged:: 1.3 - Transforms the geometry according to the given coordinate transformation paramter (``ct``), which may be an integer SRID, spatial reference WKT string, a PROJ.4 string, a :class:`~django.contrib.gis.gdal.SpatialReference` object, or a @@ -796,7 +830,7 @@ Writer Objects All writer objects have a ``write(geom)`` method that returns either the WKB or WKT of the given geometry. In addition, :class:`WKBWriter` objects also have properties that may be used to change the byte order, and or -include the SRID and 3D values (in other words, EWKB). +include the SRID value (in other words, EWKB). .. class:: WKBWriter @@ -858,7 +892,7 @@ so that the Z value is included in the WKB. Outdim Value Description ============ =========================== 2 The default, output 2D WKB. -3 Output 3D EWKB. +3 Output 3D WKB. ============ =========================== Example:: diff --git a/docs/ref/contrib/gis/index.txt b/docs/ref/contrib/gis/index.txt index 1b1e7688d0..6a1402bfab 100644 --- a/docs/ref/contrib/gis/index.txt +++ b/docs/ref/contrib/gis/index.txt @@ -15,7 +15,7 @@ of spatially enabled data. :maxdepth: 2 tutorial - install + install/index model-api db-api geoquerysets diff --git a/docs/ref/contrib/gis/install.txt b/docs/ref/contrib/gis/install.txt deleted file mode 100644 index b815973202..0000000000 --- a/docs/ref/contrib/gis/install.txt +++ /dev/null @@ -1,1267 +0,0 @@ -.. _ref-gis-install: - -====================== -GeoDjango Installation -====================== - -.. highlight:: console - -Overview -======== -In general, GeoDjango installation requires: - -1. :ref:`Python and Django ` -2. :ref:`spatial_database` -3. :ref:`geospatial_libs` - -Details for each of the requirements and installation instructions -are provided in the sections below. In addition, platform-specific -instructions are available for: - -* :ref:`macosx` -* :ref:`ubuntudebian` -* :ref:`windows` - -.. admonition:: Use the Source - - Because GeoDjango takes advantage of the latest in the open source geospatial - software technology, recent versions of the libraries are necessary. - If binary packages aren't available for your platform, - :ref:`installation from source ` - may be required. When compiling the libraries from source, please follow the - directions closely, especially if you're a beginner. - -Requirements -============ - -.. _django: - -Python and Django ------------------ - -Because GeoDjango is included with Django, please refer to Django's -:ref:`installation instructions ` for details on -how to install. - - -.. _spatial_database: - -Spatial database ----------------- -PostgreSQL (with PostGIS), MySQL, Oracle, and SQLite (with SpatiaLite) are -the spatial databases currently supported. - -.. note:: - - PostGIS is recommended, because it is the most mature and feature-rich - open source spatial database. - -The geospatial libraries required for a GeoDjango installation depends -on the spatial database used. The following lists the library requirements, -supported versions, and any notes for each of the supported database backends: - -================== ============================== ================== ========================================= -Database Library Requirements Supported Versions Notes -================== ============================== ================== ========================================= -PostgreSQL GEOS, PROJ.4, PostGIS 8.1+ Requires PostGIS. -MySQL GEOS 5.x Not OGC-compliant; limited functionality. -Oracle GEOS 10.2, 11 XE not supported; not tested with 9. -SQLite GEOS, GDAL, PROJ.4, SpatiaLite 3.6.+ Requires SpatiaLite 2.3+, pysqlite2 2.5+ -================== ============================== ================== ========================================= - -.. _geospatial_libs: - -Geospatial libraries --------------------- -GeoDjango uses and/or provides interfaces for the following open source -geospatial libraries: - -======================== ==================================== ================================ ========================== -Program Description Required Supported Versions -======================== ==================================== ================================ ========================== -:ref:`GEOS ` Geometry Engine Open Source Yes 3.3, 3.2, 3.1, 3.0 -`PROJ.4`_ Cartographic Projections library Yes (PostgreSQL and SQLite only) 4.8, 4.7, 4.6, 4.5, 4.4 -:ref:`GDAL ` Geospatial Data Abstraction Library No (but, required for SQLite) 1.9, 1.8, 1.7, 1.6, 1.5 -:ref:`GeoIP ` IP-based geolocation library No 1.4 -`PostGIS`__ Spatial extensions for PostgreSQL Yes (PostgreSQL only) 1.5, 1.4, 1.3 -`SpatiaLite`__ Spatial extensions for SQLite Yes (SQLite only) 3.0, 2.4, 2.3 -======================== ==================================== ================================ ========================== - -.. admonition:: Install GDAL - - While :ref:`gdalbuild` is technically not required, it is *recommended*. - Important features of GeoDjango (including the :ref:`ref-layermapping`, - geometry reprojection, and the geographic admin) depend on its - functionality. - -.. note:: - - The GeoDjango interfaces to GEOS, GDAL, and GeoIP may be used - independently of Django. In other words, no database or settings file - required -- just import them as normal from :mod:`django.contrib.gis`. - -.. _PROJ.4: http://trac.osgeo.org/proj/ -__ http://postgis.refractions.net/ -__ http://www.gaia-gis.it/gaia-sins/ - -.. _build_from_source: - -Building from source -==================== - -When installing from source on UNIX and GNU/Linux systems, please follow -the installation instructions carefully, and install the libraries in the -given order. If using MySQL or Oracle as the spatial database, only GEOS -is required. - -.. note:: - - On Linux platforms, it may be necessary to run the ``ldconfig`` - command after installing each library. For example:: - - $ sudo make install - $ sudo ldconfig - -.. note:: - - OS X users are required to install `Apple Developer Tools`_ in order - to compile software from source. This is typically included on your - OS X installation DVDs. - -.. _Apple Developer Tools: https://developer.apple.com/technologies/tools/ - -.. _geosbuild: - -GEOS ----- - -GEOS is a C++ library for performing geometric operations, and is the default -internal geometry representation used by GeoDjango (it's behind the "lazy" -geometries). Specifically, the C API library is called (e.g., ``libgeos_c.so``) -directly from Python using ctypes. - -First, download GEOS 3.3.5 from the refractions Web site and untar the source -archive:: - - $ wget http://download.osgeo.org/geos/geos-3.3.5.tar.bz2 - $ tar xjf geos-3.3.5.tar.bz2 - -Next, change into the directory where GEOS was unpacked, run the configure -script, compile, and install:: - - $ cd geos-3.3.5 - $ ./configure - $ make - $ sudo make install - $ cd .. - -Troubleshooting -^^^^^^^^^^^^^^^ - -Can't find GEOS library -~~~~~~~~~~~~~~~~~~~~~~~ - -When GeoDjango can't find GEOS, this error is raised: - -.. code-block:: text - - ImportError: Could not find the GEOS library (tried "geos_c"). Try setting GEOS_LIBRARY_PATH in your settings. - -The most common solution is to properly configure your :ref:`libsettings` *or* set -:ref:`geoslibrarypath` in your settings. - -If using a binary package of GEOS (e.g., on Ubuntu), you may need to :ref:`binutils`. - -.. _geoslibrarypath: - -``GEOS_LIBRARY_PATH`` -~~~~~~~~~~~~~~~~~~~~~ - -If your GEOS library is in a non-standard location, or you don't want to -modify the system's library path then the :setting:`GEOS_LIBRARY_PATH` -setting may be added to your Django settings file with the full path to the -GEOS C library. For example: - -.. code-block:: python - - GEOS_LIBRARY_PATH = '/home/bob/local/lib/libgeos_c.so' - -.. note:: - - The setting must be the *full* path to the **C** shared library; in - other words you want to use ``libgeos_c.so``, not ``libgeos.so``. - -See also :ref:`My logs are filled with GEOS-related errors `. - -.. _proj4: - -PROJ.4 ------- - -`PROJ.4`_ is a library for converting geospatial data to different coordinate -reference systems. - -First, download the PROJ.4 source code and datum shifting files [#]_:: - - $ wget http://download.osgeo.org/proj/proj-4.8.0.tar.gz - $ wget http://download.osgeo.org/proj/proj-datumgrid-1.5.tar.gz - -Next, untar the source code archive, and extract the datum shifting files in the -``nad`` subdirectory. This must be done *prior* to configuration:: - - $ tar xzf proj-4.8.0.tar.gz - $ cd proj-4.8.0/nad - $ tar xzf ../../proj-datumgrid-1.5.tar.gz - $ cd .. - -Finally, configure, make and install PROJ.4:: - - $ ./configure - $ make - $ sudo make install - $ cd .. - -.. _postgis: - -PostGIS -------- - -`PostGIS`__ adds geographic object support to PostgreSQL, turning it -into a spatial database. :ref:`geosbuild` and :ref:`proj4` should be -installed prior to building PostGIS. - -.. note:: - - The `psycopg2`_ module is required for use as the database adaptor - when using GeoDjango with PostGIS. - -.. _psycopg2: http://initd.org/psycopg/ - -First download the source archive, and extract:: - - $ wget http://postgis.refractions.net/download/postgis-1.5.5.tar.gz - $ tar xzf postgis-1.5.5.tar.gz - $ cd postgis-1.5.5 - -Next, configure, make and install PostGIS:: - - $ ./configure - -Finally, make and install:: - - $ make - $ sudo make install - $ cd .. - -.. note:: - - GeoDjango does not automatically create a spatial database. Please - consult the section on :ref:`spatialdb_template` for more information. - -__ http://postgis.refractions.net/ - -.. _gdalbuild: - -GDAL ----- - -`GDAL`__ is an excellent open source geospatial library that has support for -reading most vector and raster spatial data formats. Currently, GeoDjango only -supports :ref:`GDAL's vector data ` capabilities [#]_. -:ref:`geosbuild` and :ref:`proj4` should be installed prior to building GDAL. - -First download the latest GDAL release version and untar the archive:: - - $ wget http://download.osgeo.org/gdal/gdal-1.9.1.tar.gz - $ tar xzf gdal-1.9.1.tar.gz - $ cd gdal-1.9.1 - -Configure, make and install:: - - $ ./configure - $ make # Go get some coffee, this takes a while. - $ sudo make install - $ cd .. - -.. note:: - - Because GeoDjango has it's own Python interface, the preceding instructions - do not build GDAL's own Python bindings. The bindings may be built by - adding the ``--with-python`` flag when running ``configure``. See - `GDAL/OGR In Python`__ for more information on GDAL's bindings. - -If you have any problems, please see the troubleshooting section below for -suggestions and solutions. - -__ http://trac.osgeo.org/gdal/ -__ http://trac.osgeo.org/gdal/wiki/GdalOgrInPython - -.. _gdaltrouble: - -Troubleshooting -^^^^^^^^^^^^^^^ - -Can't find GDAL library -~~~~~~~~~~~~~~~~~~~~~~~ - -When GeoDjango can't find the GDAL library, the ``HAS_GDAL`` flag -will be false: - -.. code-block:: pycon - - >>> from django.contrib.gis import gdal - >>> gdal.HAS_GDAL - False - -The solution is to properly configure your :ref:`libsettings` *or* set -:ref:`gdallibrarypath` in your settings. - -.. _gdallibrarypath: - -``GDAL_LIBRARY_PATH`` -~~~~~~~~~~~~~~~~~~~~~ - -If your GDAL library is in a non-standard location, or you don't want to -modify the system's library path then the :setting:`GDAL_LIBRARY_PATH` -setting may be added to your Django settings file with the full path to -the GDAL library. For example: - -.. code-block:: python - - GDAL_LIBRARY_PATH = '/home/sue/local/lib/libgdal.so' - -.. _gdaldata: - -Can't find GDAL data files (``GDAL_DATA``) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When installed from source, GDAL versions 1.5.1 and below have an autoconf bug -that places data in the wrong location. [#]_ This can lead to error messages -like this: - -.. code-block:: text - - ERROR 4: Unable to open EPSG support file gcs.csv. - ... - OGRException: OGR failure. - -The solution is to set the ``GDAL_DATA`` environment variable to the location of the -GDAL data files before invoking Python (typically ``/usr/local/share``; use -``gdal-config --datadir`` to find out). For example:: - - $ export GDAL_DATA=`gdal-config --datadir` - $ python manage.py shell - -If using Apache, you may need to add this environment variable to your configuration -file: - -.. code-block:: apache - - SetEnv GDAL_DATA /usr/local/share - -.. _spatialite: - -SpatiaLite ----------- - -.. note:: - - Mac OS X users should follow the instructions in the :ref:`kyngchaos` section, - as it is much easier than building from source. - -`SpatiaLite`__ adds spatial support to SQLite, turning it into a full-featured -spatial database. Because SpatiaLite has special requirements, it typically -requires SQLite and pysqlite2 (the Python SQLite DB-API adaptor) to be built from -source. :ref:`geosbuild` and :ref:`proj4` should be installed prior to building -SpatiaLite. - -After installation is complete, don't forget to read the post-installation -docs on :ref:`create_spatialite_db`. - -__ http://www.gaia-gis.it/gaia-sins/ - -.. _sqlite: - -SQLite -^^^^^^ - -Typically, SQLite packages are not compiled to include the `R*Tree module`__ -- -thus it must be compiled from source. First download the latest amalgamation -source archive from the `SQLite download page`__, and extract:: - - $ wget http://sqlite.org/sqlite-amalgamation-3.6.23.1.tar.gz - $ tar xzf sqlite-amalgamation-3.6.23.1.tar.gz - $ cd sqlite-3.6.23.1 - -Next, run the ``configure`` script -- however the ``CFLAGS`` environment variable -needs to be customized so that SQLite knows to build the R*Tree module:: - - $ CFLAGS="-DSQLITE_ENABLE_RTREE=1" ./configure - $ make - $ sudo make install - $ cd .. - -.. note:: - - If using Ubuntu, installing a newer SQLite from source can be very difficult - because it links to the existing ``libsqlite3.so`` in ``/usr/lib`` which - many other packages depend on. Unfortunately, the best solution at this time - is to overwrite the existing library by adding ``--prefix=/usr`` to the - ``configure`` command. - -__ http://www.sqlite.org/rtree.html -__ http://www.sqlite.org/download.html - -.. _spatialitebuild : - -SpatiaLite library (``libspatialite``) and tools (``spatialite``) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -After SQLite has been built with the R*Tree module enabled, get the latest -SpatiaLite library source and tools bundle from the `download page`__:: - - $ wget http://www.gaia-gis.it/gaia-sins/libspatialite-sources/libspatialite-amalgamation-2.3.1.tar.gz - $ wget http://www.gaia-gis.it/gaia-sins/spatialite-tools-sources/spatialite-tools-2.3.1.tar.gz - $ tar xzf libspatialite-amalgamation-2.3.1.tar.gz - $ tar xzf spatialite-tools-2.3.1.tar.gz - -Prior to attempting to build, please read the important notes below to see if -customization of the ``configure`` command is necessary. If not, then run the -``configure`` script, make, and install for the SpatiaLite library:: - - $ cd libspatialite-amalgamation-2.3.1 - $ ./configure # May need to modified, see notes below. - $ make - $ sudo make install - $ cd .. - -Finally, do the same for the SpatiaLite tools:: - - $ cd spatialite-tools-2.3.1 - $ ./configure # May need to modified, see notes below. - $ make - $ sudo make install - $ cd .. - -.. note:: - - If you've installed GEOS and PROJ.4 from binary packages, you will have to specify - their paths when running the ``configure`` scripts for *both* the library and the - tools (the configure scripts look, by default, in ``/usr/local``). For example, - on Debian/Ubuntu distributions that have GEOS and PROJ.4 packages, the command would be:: - - $ ./configure --with-proj-include=/usr/include --with-proj-lib=/usr/lib --with-geos-include=/usr/include --with-geos-lib=/usr/lib - -.. note:: - - For Mac OS X users building from source, the SpatiaLite library *and* tools - need to have their ``target`` configured:: - - $ ./configure --target=macosx - -__ http://www.gaia-gis.it/gaia-sins/libspatialite-sources/ - -.. _pysqlite2: - -pysqlite2 -^^^^^^^^^ - -Because SpatiaLite must be loaded as an external extension, it requires the -``enable_load_extension`` method, which is only available in versions 2.5+ of -pysqlite2. Thus, download pysqlite2 2.6, and untar:: - - $ wget http://pysqlite.googlecode.com/files/pysqlite-2.6.0.tar.gz - $ tar xzf pysqlite-2.6.0.tar.gz - $ cd pysqlite-2.6.0 - -Next, use a text editor (e.g., ``emacs`` or ``vi``) to edit the ``setup.cfg`` file -to look like the following: - -.. code-block:: ini - - [build_ext] - #define= - include_dirs=/usr/local/include - library_dirs=/usr/local/lib - libraries=sqlite3 - #define=SQLITE_OMIT_LOAD_EXTENSION - -.. note:: - - The important thing here is to make sure you comment out the - ``define=SQLITE_OMIT_LOAD_EXTENSION`` flag and that the ``include_dirs`` - and ``library_dirs`` settings are uncommented and set to the appropriate - path if the SQLite header files and libraries are not in ``/usr/include`` - and ``/usr/lib``, respectively. - -After modifying ``setup.cfg`` appropriately, then run the ``setup.py`` script -to build and install:: - - $ sudo python setup.py install - -Post-installation -================= - -.. _spatialdb_template: - -Creating a spatial database template for PostGIS ------------------------------------------------- - -Creating a spatial database with PostGIS is different than normal because -additional SQL must be loaded to enable spatial functionality. Because of -the steps in this process, it's better to create a database template that -can be reused later. - -First, you need to be able to execute the commands as a privileged database -user. For example, you can use the following to become the ``postgres`` user:: - - $ sudo su - postgres - -.. note:: - - The location *and* name of the PostGIS SQL files (e.g., from - ``POSTGIS_SQL_PATH`` below) depends on the version of PostGIS. - PostGIS versions 1.3 and below use ``/contrib/lwpostgis.sql``; - whereas version 1.4 uses ``/contrib/postgis.sql`` and - version 1.5 uses ``/contrib/postgis-1.5/postgis.sql``. - - To complicate matters, :ref:`ubuntudebian` distributions have their - own separate directory naming system that changes each release. - - The example below assumes PostGIS 1.5, thus you may need to modify - ``POSTGIS_SQL_PATH`` and the name of the SQL file for the specific - version of PostGIS you are using. - -Once you're a database super user, then you may execute the following commands -to create a PostGIS spatial database template:: - - $ POSTGIS_SQL_PATH=`pg_config --sharedir`/contrib/postgis-1.5 - # Creating the template spatial database. - $ createdb -E UTF8 template_postgis - $ createlang -d template_postgis plpgsql # Adding PLPGSQL language support. - # Allows non-superusers the ability to create from this template - $ psql -d postgres -c "UPDATE pg_database SET datistemplate='true' WHERE datname='template_postgis';" - # Loading the PostGIS SQL routines - $ psql -d template_postgis -f $POSTGIS_SQL_PATH/postgis.sql - $ psql -d template_postgis -f $POSTGIS_SQL_PATH/spatial_ref_sys.sql - # Enabling users to alter spatial tables. - $ psql -d template_postgis -c "GRANT ALL ON geometry_columns TO PUBLIC;" - $ psql -d template_postgis -c "GRANT ALL ON geography_columns TO PUBLIC;" - $ psql -d template_postgis -c "GRANT ALL ON spatial_ref_sys TO PUBLIC;" - -These commands may be placed in a shell script for later use; for convenience -the following scripts are available: - -=============== ============================================= -PostGIS version Bash shell script -=============== ============================================= -1.3 :download:`create_template_postgis-1.3.sh` -1.4 :download:`create_template_postgis-1.4.sh` -1.5 :download:`create_template_postgis-1.5.sh` -Debian/Ubuntu :download:`create_template_postgis-debian.sh` -=============== ============================================= - -Afterwards, you may create a spatial database by simply specifying -``template_postgis`` as the template to use (via the ``-T`` option):: - - $ createdb -T template_postgis - -.. note:: - - While the ``createdb`` command does not require database super-user privileges, - it must be executed by a database user that has permissions to create databases. - You can create such a user with the following command:: - - $ createuser --createdb - -.. _create_spatialite_db: - -Creating a spatial database for SpatiaLite ------------------------------------------- - -After you've installed SpatiaLite, you'll need to create a number of spatial -metadata tables in your database in order to perform spatial queries. - -If you're using SpatiaLite 2.4 or newer, use the ``spatialite`` utility to -call the ``InitSpatialMetaData()`` function, like this:: - - $ spatialite geodjango.db "SELECT InitSpatialMetaData();" - the SPATIAL_REF_SYS table already contains some row(s) - InitSpatiaMetaData ()error:"table spatial_ref_sys already exists" - 0 - -You can safely ignore the error messages shown. When you've done this, you can -skip the rest of this section. - -If you're using SpatiaLite 2.3, you'll need to download a -database-initialization file and execute its SQL queries in your database. - -First, get it from the `SpatiaLite Resources`__ page:: - - $ wget http://www.gaia-gis.it/spatialite-2.3.1/init_spatialite-2.3.sql.gz - $ gunzip init_spatialite-2.3.sql.gz - -Then, use the ``spatialite`` command to initialize a spatial database:: - - $ spatialite geodjango.db < init_spatialite-2.3.sql - -.. note:: - - The parameter ``geodjango.db`` is the *filename* of the SQLite database - you want to use. Use the same in the :setting:`DATABASES` ``"name"`` key - inside your ``settings.py``. - -__ http://www.gaia-gis.it/spatialite-2.3.1/resources.html - -Add ``django.contrib.gis`` to :setting:`INSTALLED_APPS` -------------------------------------------------------- - -Like other Django contrib applications, you will *only* need to add -:mod:`django.contrib.gis` to :setting:`INSTALLED_APPS` in your settings. -This is the so that ``gis`` templates can be located -- if not done, then -features such as the geographic admin or KML sitemaps will not function properly. - -.. _addgoogleprojection: - -Add Google projection to ``spatial_ref_sys`` table --------------------------------------------------- - -.. note:: - - If you're running PostGIS 1.4 or above, you can skip this step. The entry - is already included in the default ``spatial_ref_sys`` table. - -In order to conduct database transformations to the so-called "Google" -projection (a spherical mercator projection used by Google Maps), -an entry must be added to your spatial database's ``spatial_ref_sys`` table. -Invoke the Django shell from your project and execute the -``add_srs_entry`` function: - -.. code-block:: pycon - - $ python manage shell - >>> from django.contrib.gis.utils import add_srs_entry - >>> add_srs_entry(900913) - -This adds an entry for the 900913 SRID to the ``spatial_ref_sys`` (or equivalent) -table, making it possible for the spatial database to transform coordinates in -this projection. You only need to execute this command *once* per spatial database. - -Troubleshooting -=============== - -If you can't find the solution to your problem here then participate in the -community! You can: - -* Join the ``#geodjango`` IRC channel on FreeNode. Please be patient and polite - -- while you may not get an immediate response, someone will attempt to answer - your question as soon as they see it. -* Ask your question on the `GeoDjango`__ mailing list. -* File a ticket on the `Django trac`__ if you think there's a bug. Make - sure to provide a complete description of the problem, versions used, - and specify the component as "GIS". - -__ http://groups.google.com/group/geodjango -__ https://code.djangoproject.com/newticket - -.. _libsettings: - -Library environment settings ----------------------------- - -By far, the most common problem when installing GeoDjango is that the -external shared libraries (e.g., for GEOS and GDAL) cannot be located. [#]_ -Typically, the cause of this problem is that the operating system isn't aware -of the directory where the libraries built from source were installed. - -In general, the library path may be set on a per-user basis by setting -an environment variable, or by configuring the library path for the entire -system. - -``LD_LIBRARY_PATH`` environment variable -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -A user may set this environment variable to customize the library paths -they want to use. The typical library directory for software -built from source is ``/usr/local/lib``. Thus, ``/usr/local/lib`` needs -to be included in the ``LD_LIBRARY_PATH`` variable. For example, the user -could place the following in their bash profile:: - - export LD_LIBRARY_PATH=/usr/local/lib - -Setting system library path -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -On GNU/Linux systems, there is typically a file in ``/etc/ld.so.conf``, which may include -additional paths from files in another directory, such as ``/etc/ld.so.conf.d``. -As the root user, add the custom library path (like ``/usr/local/lib``) on a -new line in ``ld.so.conf``. This is *one* example of how to do so:: - - $ sudo echo /usr/local/lib >> /etc/ld.so.conf - $ sudo ldconfig - -For OpenSolaris users, the system library path may be modified using the -``crle`` utility. Run ``crle`` with no options to see the current configuration -and use ``crle -l`` to set with the new library path. Be *very* careful when -modifying the system library path:: - - # crle -l $OLD_PATH:/usr/local/lib - -.. _binutils: - -Install ``binutils`` -^^^^^^^^^^^^^^^^^^^^ - -GeoDjango uses the ``find_library`` function (from the ``ctypes.util`` Python -module) to discover libraries. The ``find_library`` routine uses a program -called ``objdump`` (part of the ``binutils`` package) to verify a shared -library on GNU/Linux systems. Thus, if ``binutils`` is not installed on your -Linux system then Python's ctypes may not be able to find your library even if -your library path is set correctly and geospatial libraries were built perfectly. - -The ``binutils`` package may be installed on Debian and Ubuntu systems using the -following command:: - - $ sudo apt-get install binutils - -Similarly, on Red Hat and CentOS systems:: - - $ sudo yum install binutils - -Platform-specific instructions -============================== - -.. _macosx: - -Mac OS X --------- - -Because of the variety of packaging systems available for OS X, users have -several different options for installing GeoDjango. These options are: - -* :ref:`homebrew` -* :ref:`kyngchaos` -* :ref:`fink` -* :ref:`macports` -* :ref:`build_from_source` - -.. note:: - - Currently, the easiest and recommended approach for installing GeoDjango - on OS X is to use the KyngChaos packages. - -This section also includes instructions for installing an upgraded version -of :ref:`macosx_python` from packages provided by the Python Software -Foundation, however, this is not required. - -.. _macosx_python: - -Python -^^^^^^ - -Although OS X comes with Python installed, users can use framework -installers (`2.6`__ and `2.7`__ are available) provided by -the Python Software Foundation. An advantage to using the installer is -that OS X's Python will remain "pristine" for internal operating system -use. - -__ http://python.org/ftp/python/2.6.6/python-2.6.6-macosx10.3.dmg -__ http://python.org/ftp/python/2.7.3/ - -.. note:: - - You will need to modify the ``PATH`` environment variable in your - ``.profile`` file so that the new version of Python is used when - ``python`` is entered at the command-line:: - - export PATH=/Library/Frameworks/Python.framework/Versions/Current/bin:$PATH - -.. _homebrew: - -Homebrew -^^^^^^^^ - -`Homebrew`__ provides "recipes" for building binaries and packages from source. -It provides recipes for the GeoDjango prerequisites on Macintosh computers -running OS X. Because Homebrew still builds the software from source, the -`Apple Developer Tools`_ are required. - -Summary:: - - $ brew install postgresql - $ brew install postgis - $ brew install gdal - $ brew install libgeoip - -__ http://mxcl.github.com/homebrew/ - -.. _kyngchaos: - -KyngChaos packages -^^^^^^^^^^^^^^^^^^ - -William Kyngesburye provides a number of `geospatial library binary packages`__ -that make it simple to get GeoDjango installed on OS X without compiling -them from source. However, the `Apple Developer Tools`_ are still necessary -for compiling the Python database adapters :ref:`psycopg2_kyngchaos` (for PostGIS) -and :ref:`pysqlite2_kyngchaos` (for SpatiaLite). - -.. note:: - - SpatiaLite users should consult the :ref:`spatialite_kyngchaos` section - after installing the packages for additional instructions. - -Download the framework packages for: - -* UnixImageIO -* PROJ -* GEOS -* SQLite3 (includes the SpatiaLite library) -* GDAL - -Install the packages in the order they are listed above, as the GDAL and SQLite -packages require the packages listed before them. - -Afterwards, you can also install the KyngChaos binary packages for `PostgreSQL -and PostGIS`__. - -After installing the binary packages, you'll want to add the following to -your ``.profile`` to be able to run the package programs from the command-line:: - - export PATH=/Library/Frameworks/UnixImageIO.framework/Programs:$PATH - export PATH=/Library/Frameworks/PROJ.framework/Programs:$PATH - export PATH=/Library/Frameworks/GEOS.framework/Programs:$PATH - export PATH=/Library/Frameworks/SQLite3.framework/Programs:$PATH - export PATH=/Library/Frameworks/GDAL.framework/Programs:$PATH - export PATH=/usr/local/pgsql/bin:$PATH - -__ http://www.kyngchaos.com/software/frameworks -__ http://www.kyngchaos.com/software/postgres - -.. _psycopg2_kyngchaos: - -psycopg2 -~~~~~~~~ - -After you've installed the KyngChaos binaries and modified your ``PATH``, as -described above, ``psycopg2`` may be installed using the following command:: - - $ sudo pip install psycopg2 - -.. note:: - - If you don't have ``pip``, follow the the :ref:`installation instructions - ` to install it. - -.. _pysqlite2_kyngchaos: - -pysqlite2 -~~~~~~~~~ - -Follow the :ref:`pysqlite2` source install instructions, however, -when editing the ``setup.cfg`` use the following instead: - -.. code-block:: ini - - [build_ext] - #define= - include_dirs=/Library/Frameworks/SQLite3.framework/unix/include - library_dirs=/Library/Frameworks/SQLite3.framework/unix/lib - libraries=sqlite3 - #define=SQLITE_OMIT_LOAD_EXTENSION - -.. _spatialite_kyngchaos: - -SpatiaLite -~~~~~~~~~~ - -When :ref:`create_spatialite_db`, the ``spatialite`` program is required. -However, instead of attempting to compile the SpatiaLite tools from source, -download the `SpatiaLite Binaries`__ for OS X, and install ``spatialite`` in a -location available in your ``PATH``. For example:: - - $ curl -O http://www.gaia-gis.it/spatialite/spatialite-tools-osx-x86-2.3.1.tar.gz - $ tar xzf spatialite-tools-osx-x86-2.3.1.tar.gz - $ cd spatialite-tools-osx-x86-2.3.1/bin - $ sudo cp spatialite /Library/Frameworks/SQLite3.framework/Programs - -Finally, for GeoDjango to be able to find the KyngChaos SpatiaLite library, -add the following to your ``settings.py``: - -.. code-block:: python - - SPATIALITE_LIBRARY_PATH='/Library/Frameworks/SQLite3.framework/SQLite3' - -__ http://www.gaia-gis.it/spatialite-2.3.1/binaries.html - -.. _fink: - -Fink -^^^^ - -`Kurt Schwehr`__ has been gracious enough to create GeoDjango packages for users -of the `Fink`__ package system. The following packages are available, depending -on which version of Python you want to use: - -* ``django-gis-py26`` -* ``django-gis-py25`` -* ``django-gis-py24`` - -__ http://schwehr.org/blog/ -__ http://www.finkproject.org/ - -.. _macports: - -MacPorts -^^^^^^^^ - -`MacPorts`__ may be used to install GeoDjango prerequisites on Macintosh -computers running OS X. Because MacPorts still builds the software from source, -the `Apple Developer Tools`_ are required. - -Summary:: - - $ sudo port install postgresql83-server - $ sudo port install geos - $ sudo port install proj - $ sudo port install postgis - $ sudo port install gdal +geos - $ sudo port install libgeoip - -.. note:: - - You will also have to modify the ``PATH`` in your ``.profile`` so - that the MacPorts programs are accessible from the command-line:: - - export PATH=/opt/local/bin:/opt/local/lib/postgresql83/bin - - In addition, add the ``DYLD_FALLBACK_LIBRARY_PATH`` setting so that - the libraries can be found by Python:: - - export DYLD_FALLBACK_LIBRARY_PATH=/opt/local/lib:/opt/local/lib/postgresql83 - -__ http://www.macports.org/ - -.. _ubuntudebian: - -Ubuntu & Debian GNU/Linux -------------------------- - -.. note:: - - The PostGIS SQL files are not placed in the PostgreSQL share directory in - the Debian and Ubuntu packages. Instead, they're located in a special - directory depending on the release. In this case, use the - :download:`create_template_postgis-debian.sh` script - -.. _ubuntu: - -Ubuntu -^^^^^^ - -11.10 through 12.04 -~~~~~~~~~~~~~~~~~~~ - -In Ubuntu 11.10, PostgreSQL was upgraded to 9.1. The installation command is: - -.. code-block:: bash - - $ sudo apt-get install binutils gdal-bin libproj-dev \ - postgresql-9.1-postgis postgresql-server-dev-9.1 python-psycopg2 - -.. _ubuntu10: - -10.04 through 11.04 -~~~~~~~~~~~~~~~~~~~ - -In Ubuntu 10.04, PostgreSQL was upgraded to 8.4 and GDAL was upgraded to 1.6. -Ubuntu 10.04 uses PostGIS 1.4, while Ubuntu 10.10 uses PostGIS 1.5 (with -geography support). The installation command is: - -.. code-block:: bash - - $ sudo apt-get install binutils gdal-bin libproj-dev postgresql-8.4-postgis \ - postgresql-server-dev-8.4 python-psycopg2 - -.. _ibex: - -8.10 -~~~~ - -Use the synaptic package manager to install the following packages: - -.. code-block:: bash - - $ sudo apt-get install binutils gdal-bin postgresql-8.3-postgis \ - postgresql-server-dev-8.3 python-psycopg2 - -That's it! For the curious, the required binary prerequisites packages are: - -* ``binutils``: for ctypes to find libraries -* ``postgresql-8.3`` -* ``postgresql-server-dev-8.3``: for ``pg_config`` -* ``postgresql-8.3-postgis``: for PostGIS 1.3.3 -* ``libgeos-3.0.0``, and ``libgeos-c1``: for GEOS 3.0.0 -* ``libgdal1-1.5.0``: for GDAL 1.5.0 library -* ``proj``: for PROJ 4.6.0 -- but no datum shifting files, see note below -* ``python-psycopg2`` - -Optional packages to consider: - -* ``libgeoip1``: for :ref:`GeoIP ` support -* ``gdal-bin``: for GDAL command line programs like ``ogr2ogr`` -* ``python-gdal`` for GDAL's own Python bindings -- includes interfaces for raster manipulation - -.. note:: - - On this version of Ubuntu the ``proj`` package does not come with the - datum shifting files installed, which will cause problems with the - geographic admin because the ``null`` datum grid is not available for - transforming geometries to the spherical mercator projection. A solution - is to download the datum-shifting files, create the grid file, and - install it yourself: - - .. code-block:: bash - - $ wget http://download.osgeo.org/proj/proj-datumgrid-1.4.tar.gz - $ mkdir nad - $ cd nad - $ tar xzf ../proj-datumgrid-1.4.tar.gz - $ nad2bin null < null.lla - $ sudo cp null /usr/share/proj - - Otherwise, the Ubuntu ``proj`` package is fine for general use as long as you - do not plan on doing any database transformation of geometries to the - Google projection (900913). - -.. _debian: - -Debian ------- - -.. _lenny: - -5.0 (Lenny) -^^^^^^^^^^^ - -This version is comparable to Ubuntu :ref:`ibex`, so the command -is very similar: - -.. code-block:: bash - - $ sudo apt-get install binutils libgdal1-1.5.0 postgresql-8.3 \ - postgresql-8.3-postgis postgresql-server-dev-8.3 \ - python-psycopg2 python-setuptools - -This assumes that you are using PostgreSQL version 8.3. Else, replace ``8.3`` -in the above command with the appropriate PostgreSQL version. - -.. note:: - - Please read the note in the Ubuntu :ref:`ibex` install documentation - about the ``proj`` package -- it also applies here because the package does - not include the datum shifting files. - -.. _post_install: - -Post-installation notes -~~~~~~~~~~~~~~~~~~~~~~~ - -If the PostgreSQL database cluster was not initiated after installing, then it -can be created (and started) with the following command: - -.. code-block:: bash - - $ sudo pg_createcluster --start 8.3 main - -Afterwards, the ``/etc/init.d/postgresql-8.3`` script should be used to manage -the starting and stopping of PostgreSQL. - -In addition, the SQL files for PostGIS are placed in a different location on -Debian 5.0 . Thus when :ref:`spatialdb_template` either: - -* Create a symbolic link to these files: - - .. code-block:: bash - - $ sudo ln -s /usr/share/postgresql-8.3-postgis/{lwpostgis,spatial_ref_sys}.sql \ - /usr/share/postgresql/8.3 - - If not running PostgreSQL 8.3, then replace ``8.3`` in the command above with - the correct version. - -* Or use the :download:`create_template_postgis-debian.sh` to create the spatial database. - -.. _windows: - -Windows -------- - -Proceed through the following sections sequentially in order to install -GeoDjango on Windows. - -.. note:: - - These instructions assume that you are using 32-bit versions of - all programs. While 64-bit versions of Python and PostgreSQL 9.0 - are available, 64-bit versions of spatial libraries, like - GEOS and GDAL, are not yet provided by the :ref:`OSGeo4W` installer. - -Python -^^^^^^ - -First, download the latest `Python 2.7 installer`__ from the Python Web site. -Next, run the installer and keep the defaults -- for example, keep -'Install for all users' checked and the installation path set as -``C:\Python27``. - -.. note:: - - You may already have a version of Python installed in ``C:\python`` as ESRI - products sometimes install a copy there. *You should still install a - fresh version of Python 2.7.* - -__ http://python.org/download/ - -PostgreSQL -^^^^^^^^^^ - -First, download the latest `PostgreSQL 9.0 installer`__ from the -`EnterpriseDB`__ Web site. After downloading, simply run the installer, -follow the on-screen directions, and keep the default options unless -you know the consequences of changing them. - -.. note:: - - The PostgreSQL installer creates both a new Windows user to be the - 'postgres service account' and a ``postgres`` database superuser - You will be prompted once to set the password for both accounts -- - make sure to remember it! - -When the installer completes, it will ask to launch the Application Stack -Builder (ASB) on exit -- keep this checked, as it is necessary to -install :ref:`postgisasb`. - -.. note:: - - If installed successfully, the PostgreSQL server will run in the - background each time the system as started as a Windows service. - A :menuselection:`PostgreSQL 9.0` start menu group will created - and contains shortcuts for the ASB as well as the 'SQL Shell', - which will launch a ``psql`` command window. - -__ http://www.enterprisedb.com/products-services-training/pgdownload -__ http://www.enterprisedb.com - -.. _postgisasb: - -PostGIS -^^^^^^^ - -From within the Application Stack Builder (to run outside of the installer, -:menuselection:`Start --> Programs --> PostgreSQL 9.0`), select -:menuselection:`PostgreSQL Database Server 9.0 on port 5432` from the drop down -menu. Next, expand the :menuselection:`Categories --> Spatial Extensions` menu -tree and select :menuselection:`PostGIS 1.5 for PostgreSQL 9.0`. - -After clicking next, you will be prompted to select your mirror, PostGIS -will be downloaded, and the PostGIS installer will begin. Select only the -default options during install (e.g., do not uncheck the option to create a -default PostGIS database). - -.. note:: - - You will be prompted to enter your ``postgres`` database superuser - password in the 'Database Connection Information' dialog. - -psycopg2 -^^^^^^^^ - -The ``psycopg2`` Python module provides the interface between Python and the -PostgreSQL database. Download the latest `Windows installer`__ for your version -of Python and PostgreSQL and run using the default settings. [#]_ - -__ http://www.stickpeople.com/projects/python/win-psycopg/ - -.. _osgeo4w: - -OSGeo4W -^^^^^^^ - -The `OSGeo4W installer`_ makes it simple to install the PROJ.4, GDAL, and GEOS -libraries required by GeoDjango. First, download the `OSGeo4W installer`_, -and run it. Select :menuselection:`Express Web-GIS Install` and click next. -In the 'Select Packages' list, ensure that GDAL is selected; MapServer and -Apache are also enabled by default, but are not required by GeoDjango and -may be unchecked safely. After clicking next, the packages will be -automatically downloaded and installed, after which you may exit the -installer. - -.. _OSGeo4W installer: http://trac.osgeo.org/osgeo4w/ - -Modify Windows environment -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In order to use GeoDjango, you will need to add your Python and OSGeo4W -directories to your Windows system ``Path``, as well as create ``GDAL_DATA`` -and ``PROJ_LIB`` environment variables. The following set of commands, -executable with ``cmd.exe``, will set this up: - -.. code-block:: bat - - set OSGEO4W_ROOT=C:\OSGeo4W - set PYTHON_ROOT=C:\Python27 - set GDAL_DATA=%OSGEO4W_ROOT%\share\gdal - set PROJ_LIB=%OSGEO4W_ROOT%\share\proj - set PATH=%PATH%;%PYTHON_ROOT%;%OSGEO4W_ROOT%\bin - reg ADD "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v Path /t REG_EXPAND_SZ /f /d "%PATH%" - reg ADD "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v GDAL_DATA /t REG_EXPAND_SZ /f /d "%GDAL_DATA%" - reg ADD "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v PROJ_LIB /t REG_EXPAND_SZ /f /d "%PROJ_LIB%" - -For your convenience, these commands are available in the executable batch -script, :download:`geodjango_setup.bat`. - -.. note:: - - Administrator privileges are required to execute these commands. - To do this, right-click on :download:`geodjango_setup.bat` and select - :menuselection:`Run as administrator`. You need to log out and log back in again - for the settings to take effect. - -.. note:: - - If you customized the Python or OSGeo4W installation directories, - then you will need to modify the ``OSGEO4W_ROOT`` and/or ``PYTHON_ROOT`` - variables accordingly. - -Install Django and set up database -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Finally, :ref:`install Django ` on your system. -You do not need to create a spatial database template, as one named -``template_postgis`` is created for you when installing PostGIS. - -To administer the database, you can either use the pgAdmin III program -(:menuselection:`Start --> PostgreSQL 9.0 --> pgAdmin III`) or the -SQL Shell (:menuselection:`Start --> PostgreSQL 9.0 --> SQL Shell`). -For example, to create a ``geodjango`` spatial database and user, the following -may be executed from the SQL Shell as the ``postgres`` user:: - - postgres# CREATE USER geodjango PASSWORD 'my_passwd'; - postgres# CREATE DATABASE geodjango OWNER geodjango TEMPLATE template_postgis ENCODING 'utf8'; - -.. rubric:: Footnotes -.. [#] The datum shifting files are needed for converting data to and from - certain projections. - For example, the PROJ.4 string for the `Google projection (900913 or 3857) - `_ requires the - ``null`` grid file only included in the extra datum shifting files. - It is easier to install the shifting files now, then to have debug a - problem caused by their absence later. -.. [#] Specifically, GeoDjango provides support for the `OGR - `_ library, a component of GDAL. -.. [#] See `GDAL ticket #2382 `_. -.. [#] GeoDjango uses the :func:`~ctypes.util.find_library` routine from - :mod:`ctypes.util` to locate shared libraries. -.. [#] The ``psycopg2`` Windows installers are packaged and maintained by - `Jason Erickson `_. diff --git a/docs/ref/contrib/gis/create_template_postgis-1.3.sh b/docs/ref/contrib/gis/install/create_template_postgis-1.3.sh similarity index 100% rename from docs/ref/contrib/gis/create_template_postgis-1.3.sh rename to docs/ref/contrib/gis/install/create_template_postgis-1.3.sh diff --git a/docs/ref/contrib/gis/create_template_postgis-1.4.sh b/docs/ref/contrib/gis/install/create_template_postgis-1.4.sh similarity index 100% rename from docs/ref/contrib/gis/create_template_postgis-1.4.sh rename to docs/ref/contrib/gis/install/create_template_postgis-1.4.sh diff --git a/docs/ref/contrib/gis/create_template_postgis-1.5.sh b/docs/ref/contrib/gis/install/create_template_postgis-1.5.sh similarity index 100% rename from docs/ref/contrib/gis/create_template_postgis-1.5.sh rename to docs/ref/contrib/gis/install/create_template_postgis-1.5.sh diff --git a/docs/ref/contrib/gis/create_template_postgis-debian.sh b/docs/ref/contrib/gis/install/create_template_postgis-debian.sh similarity index 100% rename from docs/ref/contrib/gis/create_template_postgis-debian.sh rename to docs/ref/contrib/gis/install/create_template_postgis-debian.sh diff --git a/docs/ref/contrib/gis/geodjango_setup.bat b/docs/ref/contrib/gis/install/geodjango_setup.bat similarity index 100% rename from docs/ref/contrib/gis/geodjango_setup.bat rename to docs/ref/contrib/gis/install/geodjango_setup.bat diff --git a/docs/ref/contrib/gis/install/geolibs.txt b/docs/ref/contrib/gis/install/geolibs.txt new file mode 100644 index 0000000000..c78f0c0e62 --- /dev/null +++ b/docs/ref/contrib/gis/install/geolibs.txt @@ -0,0 +1,282 @@ +.. _geospatial_libs: + +=============================== +Installing Geospatial libraries +=============================== + +GeoDjango uses and/or provides interfaces for the following open source +geospatial libraries: + +======================== ==================================== ================================ ========================== +Program Description Required Supported Versions +======================== ==================================== ================================ ========================== +:ref:`GEOS ` Geometry Engine Open Source Yes 3.3, 3.2, 3.1, 3.0 +`PROJ.4`_ Cartographic Projections library Yes (PostgreSQL and SQLite only) 4.8, 4.7, 4.6, 4.5, 4.4 +:ref:`GDAL ` Geospatial Data Abstraction Library No (but, required for SQLite) 1.9, 1.8, 1.7, 1.6, 1.5 +:ref:`GeoIP ` IP-based geolocation library No 1.4 +`PostGIS`__ Spatial extensions for PostgreSQL Yes (PostgreSQL only) 2.0, 1.5, 1.4, 1.3 +`SpatiaLite`__ Spatial extensions for SQLite Yes (SQLite only) 3.0, 2.4, 2.3 +======================== ==================================== ================================ ========================== + +.. admonition:: Install GDAL + + While :ref:`gdalbuild` is technically not required, it is *recommended*. + Important features of GeoDjango (including the :ref:`ref-layermapping`, + geometry reprojection, and the geographic admin) depend on its + functionality. + +.. note:: + + The GeoDjango interfaces to GEOS, GDAL, and GeoIP may be used + independently of Django. In other words, no database or settings file + required -- just import them as normal from :mod:`django.contrib.gis`. + +.. _PROJ.4: http://trac.osgeo.org/proj/ +__ http://postgis.refractions.net/ +__ http://www.gaia-gis.it/gaia-sins/ + + +On Debian/Ubuntu, you are advised to install the following packages which will +install, directly or by dependency, the required geospatial libraries: + +.. code-block:: bash + + $ sudo apt-get install binutils libproj-dev gdal-bin + +Optional packages to consider: + +* ``libgeoip1``: for :ref:`GeoIP ` support +* ``gdal-bin``: for GDAL command line programs like ``ogr2ogr`` +* ``python-gdal`` for GDAL's own Python bindings -- includes interfaces for raster manipulation + +Please also consult platform-specific instructions if you are on :ref:`macosx` +or :ref:`windows`. + +.. _build_from_source: + +Building from source +==================== + +When installing from source on UNIX and GNU/Linux systems, please follow +the installation instructions carefully, and install the libraries in the +given order. If using MySQL or Oracle as the spatial database, only GEOS +is required. + +.. note:: + + On Linux platforms, it may be necessary to run the ``ldconfig`` + command after installing each library. For example:: + + $ sudo make install + $ sudo ldconfig + +.. note:: + + OS X users are required to install `Apple Developer Tools`_ in order + to compile software from source. This is typically included on your + OS X installation DVDs. + +.. _Apple Developer Tools: https://developer.apple.com/technologies/tools/ + +.. _geosbuild: + +GEOS +---- + +GEOS is a C++ library for performing geometric operations, and is the default +internal geometry representation used by GeoDjango (it's behind the "lazy" +geometries). Specifically, the C API library is called (e.g., ``libgeos_c.so``) +directly from Python using ctypes. + +First, download GEOS 3.3.5 from the refractions Web site and untar the source +archive:: + + $ wget http://download.osgeo.org/geos/geos-3.3.5.tar.bz2 + $ tar xjf geos-3.3.5.tar.bz2 + +Next, change into the directory where GEOS was unpacked, run the configure +script, compile, and install:: + + $ cd geos-3.3.5 + $ ./configure + $ make + $ sudo make install + $ cd .. + +Troubleshooting +^^^^^^^^^^^^^^^ + +Can't find GEOS library +~~~~~~~~~~~~~~~~~~~~~~~ + +When GeoDjango can't find GEOS, this error is raised: + +.. code-block:: text + + ImportError: Could not find the GEOS library (tried "geos_c"). Try setting GEOS_LIBRARY_PATH in your settings. + +The most common solution is to properly configure your :ref:`libsettings` *or* set +:ref:`geoslibrarypath` in your settings. + +If using a binary package of GEOS (e.g., on Ubuntu), you may need to :ref:`binutils`. + +.. _geoslibrarypath: + +``GEOS_LIBRARY_PATH`` +~~~~~~~~~~~~~~~~~~~~~ + +If your GEOS library is in a non-standard location, or you don't want to +modify the system's library path then the :setting:`GEOS_LIBRARY_PATH` +setting may be added to your Django settings file with the full path to the +GEOS C library. For example: + +.. code-block:: python + + GEOS_LIBRARY_PATH = '/home/bob/local/lib/libgeos_c.so' + +.. note:: + + The setting must be the *full* path to the **C** shared library; in + other words you want to use ``libgeos_c.so``, not ``libgeos.so``. + +See also :ref:`My logs are filled with GEOS-related errors `. + +.. _proj4: + +PROJ.4 +------ + +`PROJ.4`_ is a library for converting geospatial data to different coordinate +reference systems. + +First, download the PROJ.4 source code and datum shifting files [#]_:: + + $ wget http://download.osgeo.org/proj/proj-4.8.0.tar.gz + $ wget http://download.osgeo.org/proj/proj-datumgrid-1.5.tar.gz + +Next, untar the source code archive, and extract the datum shifting files in the +``nad`` subdirectory. This must be done *prior* to configuration:: + + $ tar xzf proj-4.8.0.tar.gz + $ cd proj-4.8.0/nad + $ tar xzf ../../proj-datumgrid-1.5.tar.gz + $ cd .. + +Finally, configure, make and install PROJ.4:: + + $ ./configure + $ make + $ sudo make install + $ cd .. + +.. _gdalbuild: + +GDAL +---- + +`GDAL`__ is an excellent open source geospatial library that has support for +reading most vector and raster spatial data formats. Currently, GeoDjango only +supports :ref:`GDAL's vector data ` capabilities [#]_. +:ref:`geosbuild` and :ref:`proj4` should be installed prior to building GDAL. + +First download the latest GDAL release version and untar the archive:: + + $ wget http://download.osgeo.org/gdal/gdal-1.9.1.tar.gz + $ tar xzf gdal-1.9.1.tar.gz + $ cd gdal-1.9.1 + +Configure, make and install:: + + $ ./configure + $ make # Go get some coffee, this takes a while. + $ sudo make install + $ cd .. + +.. note:: + + Because GeoDjango has it's own Python interface, the preceding instructions + do not build GDAL's own Python bindings. The bindings may be built by + adding the ``--with-python`` flag when running ``configure``. See + `GDAL/OGR In Python`__ for more information on GDAL's bindings. + +If you have any problems, please see the troubleshooting section below for +suggestions and solutions. + +__ http://trac.osgeo.org/gdal/ +__ http://trac.osgeo.org/gdal/wiki/GdalOgrInPython + +.. _gdaltrouble: + +Troubleshooting +^^^^^^^^^^^^^^^ + +Can't find GDAL library +~~~~~~~~~~~~~~~~~~~~~~~ + +When GeoDjango can't find the GDAL library, the ``HAS_GDAL`` flag +will be false: + +.. code-block:: pycon + + >>> from django.contrib.gis import gdal + >>> gdal.HAS_GDAL + False + +The solution is to properly configure your :ref:`libsettings` *or* set +:ref:`gdallibrarypath` in your settings. + +.. _gdallibrarypath: + +``GDAL_LIBRARY_PATH`` +~~~~~~~~~~~~~~~~~~~~~ + +If your GDAL library is in a non-standard location, or you don't want to +modify the system's library path then the :setting:`GDAL_LIBRARY_PATH` +setting may be added to your Django settings file with the full path to +the GDAL library. For example: + +.. code-block:: python + + GDAL_LIBRARY_PATH = '/home/sue/local/lib/libgdal.so' + +.. _gdaldata: + +Can't find GDAL data files (``GDAL_DATA``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When installed from source, GDAL versions 1.5.1 and below have an autoconf bug +that places data in the wrong location. [#]_ This can lead to error messages +like this: + +.. code-block:: text + + ERROR 4: Unable to open EPSG support file gcs.csv. + ... + OGRException: OGR failure. + +The solution is to set the ``GDAL_DATA`` environment variable to the location of the +GDAL data files before invoking Python (typically ``/usr/local/share``; use +``gdal-config --datadir`` to find out). For example:: + + $ export GDAL_DATA=`gdal-config --datadir` + $ python manage.py shell + +If using Apache, you may need to add this environment variable to your configuration +file: + +.. code-block:: apache + + SetEnv GDAL_DATA /usr/local/share + +.. rubric:: Footnotes +.. [#] The datum shifting files are needed for converting data to and from + certain projections. + For example, the PROJ.4 string for the `Google projection (900913 or 3857) + `_ requires the + ``null`` grid file only included in the extra datum shifting files. + It is easier to install the shifting files now, then to have debug a + problem caused by their absence later. +.. [#] Specifically, GeoDjango provides support for the `OGR + `_ library, a component of GDAL. +.. [#] See `GDAL ticket #2382 `_. + diff --git a/docs/ref/contrib/gis/install/index.txt b/docs/ref/contrib/gis/install/index.txt new file mode 100644 index 0000000000..c710866813 --- /dev/null +++ b/docs/ref/contrib/gis/install/index.txt @@ -0,0 +1,535 @@ +.. _ref-gis-install: + +====================== +GeoDjango Installation +====================== + +.. highlight:: console + +Overview +======== +In general, GeoDjango installation requires: + +1. :ref:`Python and Django ` +2. :ref:`spatial_database` +3. :ref:`geospatial_libs` + +Details for each of the requirements and installation instructions +are provided in the sections below. In addition, platform-specific +instructions are available for: + +* :ref:`macosx` +* :ref:`windows` + +.. admonition:: Use the Source + + Because GeoDjango takes advantage of the latest in the open source geospatial + software technology, recent versions of the libraries are necessary. + If binary packages aren't available for your platform, installation from + source may be required. When compiling the libraries from source, please + follow the directions closely, especially if you're a beginner. + +Requirements +============ + +.. _django: + +Python and Django +----------------- + +Because GeoDjango is included with Django, please refer to Django's +:ref:`installation instructions ` for details on +how to install. + + +.. _spatial_database: + +Spatial database +---------------- +PostgreSQL (with PostGIS), MySQL, Oracle, and SQLite (with SpatiaLite) are +the spatial databases currently supported. + +.. note:: + + PostGIS is recommended, because it is the most mature and feature-rich + open source spatial database. + +The geospatial libraries required for a GeoDjango installation depends +on the spatial database used. The following lists the library requirements, +supported versions, and any notes for each of the supported database backends: + +================== ============================== ================== ========================================= +Database Library Requirements Supported Versions Notes +================== ============================== ================== ========================================= +PostgreSQL GEOS, PROJ.4, PostGIS 8.2+ Requires PostGIS. +MySQL GEOS 5.x Not OGC-compliant; limited functionality. +Oracle GEOS 10.2, 11 XE not supported; not tested with 9. +SQLite GEOS, GDAL, PROJ.4, SpatiaLite 3.6.+ Requires SpatiaLite 2.3+, pysqlite2 2.5+ +================== ============================== ================== ========================================= + +See also `this comparison matrix`__ on the OSGeo Wiki for +PostgreSQL/PostGIS/GEOS/GDAL possible combinations. + +__ http://trac.osgeo.org/postgis/wiki/UsersWikiPostgreSQLPostGIS + +Installation +============ + +Geospatial libraries +-------------------- + +.. toctree:: + :maxdepth: 1 + + geolibs + +Database installation +--------------------- + +.. toctree:: + :maxdepth: 1 + + postgis + spatialite + +Add ``django.contrib.gis`` to :setting:`INSTALLED_APPS` +------------------------------------------------------- + +Like other Django contrib applications, you will *only* need to add +:mod:`django.contrib.gis` to :setting:`INSTALLED_APPS` in your settings. +This is the so that ``gis`` templates can be located -- if not done, then +features such as the geographic admin or KML sitemaps will not function properly. + +.. _addgoogleprojection: + +Add Google projection to ``spatial_ref_sys`` table +-------------------------------------------------- + +.. note:: + + If you're running PostGIS 1.4 or above, you can skip this step. The entry + is already included in the default ``spatial_ref_sys`` table. + +In order to conduct database transformations to the so-called "Google" +projection (a spherical mercator projection used by Google Maps), +an entry must be added to your spatial database's ``spatial_ref_sys`` table. +Invoke the Django shell from your project and execute the +``add_srs_entry`` function: + +.. code-block:: pycon + + $ python manage shell + >>> from django.contrib.gis.utils import add_srs_entry + >>> add_srs_entry(900913) + +This adds an entry for the 900913 SRID to the ``spatial_ref_sys`` (or equivalent) +table, making it possible for the spatial database to transform coordinates in +this projection. You only need to execute this command *once* per spatial database. + +Troubleshooting +=============== + +If you can't find the solution to your problem here then participate in the +community! You can: + +* Join the ``#geodjango`` IRC channel on FreeNode. Please be patient and polite + -- while you may not get an immediate response, someone will attempt to answer + your question as soon as they see it. +* Ask your question on the `GeoDjango`__ mailing list. +* File a ticket on the `Django trac`__ if you think there's a bug. Make + sure to provide a complete description of the problem, versions used, + and specify the component as "GIS". + +__ http://groups.google.com/group/geodjango +__ https://code.djangoproject.com/newticket + +.. _libsettings: + +Library environment settings +---------------------------- + +By far, the most common problem when installing GeoDjango is that the +external shared libraries (e.g., for GEOS and GDAL) cannot be located. [#]_ +Typically, the cause of this problem is that the operating system isn't aware +of the directory where the libraries built from source were installed. + +In general, the library path may be set on a per-user basis by setting +an environment variable, or by configuring the library path for the entire +system. + +``LD_LIBRARY_PATH`` environment variable +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A user may set this environment variable to customize the library paths +they want to use. The typical library directory for software +built from source is ``/usr/local/lib``. Thus, ``/usr/local/lib`` needs +to be included in the ``LD_LIBRARY_PATH`` variable. For example, the user +could place the following in their bash profile:: + + export LD_LIBRARY_PATH=/usr/local/lib + +Setting system library path +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +On GNU/Linux systems, there is typically a file in ``/etc/ld.so.conf``, which may include +additional paths from files in another directory, such as ``/etc/ld.so.conf.d``. +As the root user, add the custom library path (like ``/usr/local/lib``) on a +new line in ``ld.so.conf``. This is *one* example of how to do so:: + + $ sudo echo /usr/local/lib >> /etc/ld.so.conf + $ sudo ldconfig + +For OpenSolaris users, the system library path may be modified using the +``crle`` utility. Run ``crle`` with no options to see the current configuration +and use ``crle -l`` to set with the new library path. Be *very* careful when +modifying the system library path:: + + # crle -l $OLD_PATH:/usr/local/lib + +.. _binutils: + +Install ``binutils`` +^^^^^^^^^^^^^^^^^^^^ + +GeoDjango uses the ``find_library`` function (from the ``ctypes.util`` Python +module) to discover libraries. The ``find_library`` routine uses a program +called ``objdump`` (part of the ``binutils`` package) to verify a shared +library on GNU/Linux systems. Thus, if ``binutils`` is not installed on your +Linux system then Python's ctypes may not be able to find your library even if +your library path is set correctly and geospatial libraries were built perfectly. + +The ``binutils`` package may be installed on Debian and Ubuntu systems using the +following command:: + + $ sudo apt-get install binutils + +Similarly, on Red Hat and CentOS systems:: + + $ sudo yum install binutils + +Platform-specific instructions +============================== + +.. _macosx: + +Mac OS X +-------- + +Because of the variety of packaging systems available for OS X, users have +several different options for installing GeoDjango. These options are: + +* :ref:`homebrew` +* :ref:`kyngchaos` +* :ref:`fink` +* :ref:`macports` +* :ref:`build_from_source` + +.. note:: + + Currently, the easiest and recommended approach for installing GeoDjango + on OS X is to use the KyngChaos packages. + +This section also includes instructions for installing an upgraded version +of :ref:`macosx_python` from packages provided by the Python Software +Foundation, however, this is not required. + +.. _macosx_python: + +Python +^^^^^^ + +Although OS X comes with Python installed, users can use framework +installers (`2.6`__ and `2.7`__ are available) provided by +the Python Software Foundation. An advantage to using the installer is +that OS X's Python will remain "pristine" for internal operating system +use. + +__ http://python.org/ftp/python/2.6.6/python-2.6.6-macosx10.3.dmg +__ http://python.org/ftp/python/2.7.3/ + +.. note:: + + You will need to modify the ``PATH`` environment variable in your + ``.profile`` file so that the new version of Python is used when + ``python`` is entered at the command-line:: + + export PATH=/Library/Frameworks/Python.framework/Versions/Current/bin:$PATH + +.. _homebrew: + +Homebrew +^^^^^^^^ + +`Homebrew`__ provides "recipes" for building binaries and packages from source. +It provides recipes for the GeoDjango prerequisites on Macintosh computers +running OS X. Because Homebrew still builds the software from source, the +`Apple Developer Tools`_ are required. + +Summary:: + + $ brew install postgresql + $ brew install postgis + $ brew install gdal + $ brew install libgeoip + +__ http://mxcl.github.com/homebrew/ +.. _Apple Developer Tools: https://developer.apple.com/technologies/tools/ + +.. _kyngchaos: + +KyngChaos packages +^^^^^^^^^^^^^^^^^^ + +William Kyngesburye provides a number of `geospatial library binary packages`__ +that make it simple to get GeoDjango installed on OS X without compiling +them from source. However, the `Apple Developer Tools`_ are still necessary +for compiling the Python database adapters :ref:`psycopg2_kyngchaos` (for PostGIS) +and :ref:`pysqlite2` (for SpatiaLite). + +.. note:: + + SpatiaLite users should consult the :ref:`spatialite_macosx` section + after installing the packages for additional instructions. + +Download the framework packages for: + +* UnixImageIO +* PROJ +* GEOS +* SQLite3 (includes the SpatiaLite library) +* GDAL + +Install the packages in the order they are listed above, as the GDAL and SQLite +packages require the packages listed before them. + +Afterwards, you can also install the KyngChaos binary packages for `PostgreSQL +and PostGIS`__. + +After installing the binary packages, you'll want to add the following to +your ``.profile`` to be able to run the package programs from the command-line:: + + export PATH=/Library/Frameworks/UnixImageIO.framework/Programs:$PATH + export PATH=/Library/Frameworks/PROJ.framework/Programs:$PATH + export PATH=/Library/Frameworks/GEOS.framework/Programs:$PATH + export PATH=/Library/Frameworks/SQLite3.framework/Programs:$PATH + export PATH=/Library/Frameworks/GDAL.framework/Programs:$PATH + export PATH=/usr/local/pgsql/bin:$PATH + +__ http://www.kyngchaos.com/software/frameworks +__ http://www.kyngchaos.com/software/postgres + +.. _psycopg2_kyngchaos: + +psycopg2 +~~~~~~~~ + +After you've installed the KyngChaos binaries and modified your ``PATH``, as +described above, ``psycopg2`` may be installed using the following command:: + + $ sudo pip install psycopg2 + +.. note:: + + If you don't have ``pip``, follow the the :ref:`installation instructions + ` to install it. + +.. _fink: + +Fink +^^^^ + +`Kurt Schwehr`__ has been gracious enough to create GeoDjango packages for users +of the `Fink`__ package system. The following packages are available, depending +on which version of Python you want to use: + +* ``django-gis-py26`` +* ``django-gis-py25`` +* ``django-gis-py24`` + +__ http://schwehr.org/blog/ +__ http://www.finkproject.org/ + +.. _macports: + +MacPorts +^^^^^^^^ + +`MacPorts`__ may be used to install GeoDjango prerequisites on Macintosh +computers running OS X. Because MacPorts still builds the software from source, +the `Apple Developer Tools`_ are required. + +Summary:: + + $ sudo port install postgresql83-server + $ sudo port install geos + $ sudo port install proj + $ sudo port install postgis + $ sudo port install gdal +geos + $ sudo port install libgeoip + +.. note:: + + You will also have to modify the ``PATH`` in your ``.profile`` so + that the MacPorts programs are accessible from the command-line:: + + export PATH=/opt/local/bin:/opt/local/lib/postgresql83/bin + + In addition, add the ``DYLD_FALLBACK_LIBRARY_PATH`` setting so that + the libraries can be found by Python:: + + export DYLD_FALLBACK_LIBRARY_PATH=/opt/local/lib:/opt/local/lib/postgresql83 + +__ http://www.macports.org/ + +.. _windows: + +Windows +------- + +Proceed through the following sections sequentially in order to install +GeoDjango on Windows. + +.. note:: + + These instructions assume that you are using 32-bit versions of + all programs. While 64-bit versions of Python and PostgreSQL 9.0 + are available, 64-bit versions of spatial libraries, like + GEOS and GDAL, are not yet provided by the :ref:`OSGeo4W` installer. + +Python +^^^^^^ + +First, download the latest `Python 2.7 installer`__ from the Python Web site. +Next, run the installer and keep the defaults -- for example, keep +'Install for all users' checked and the installation path set as +``C:\Python27``. + +.. note:: + + You may already have a version of Python installed in ``C:\python`` as ESRI + products sometimes install a copy there. *You should still install a + fresh version of Python 2.7.* + +__ http://python.org/download/ + +PostgreSQL +^^^^^^^^^^ + +First, download the latest `PostgreSQL 9.0 installer`__ from the +`EnterpriseDB`__ Web site. After downloading, simply run the installer, +follow the on-screen directions, and keep the default options unless +you know the consequences of changing them. + +.. note:: + + The PostgreSQL installer creates both a new Windows user to be the + 'postgres service account' and a ``postgres`` database superuser + You will be prompted once to set the password for both accounts -- + make sure to remember it! + +When the installer completes, it will ask to launch the Application Stack +Builder (ASB) on exit -- keep this checked, as it is necessary to +install :ref:`postgisasb`. + +.. note:: + + If installed successfully, the PostgreSQL server will run in the + background each time the system as started as a Windows service. + A :menuselection:`PostgreSQL 9.0` start menu group will created + and contains shortcuts for the ASB as well as the 'SQL Shell', + which will launch a ``psql`` command window. + +__ http://www.enterprisedb.com/products-services-training/pgdownload +__ http://www.enterprisedb.com + +.. _postgisasb: + +PostGIS +^^^^^^^ + +From within the Application Stack Builder (to run outside of the installer, +:menuselection:`Start --> Programs --> PostgreSQL 9.0`), select +:menuselection:`PostgreSQL Database Server 9.0 on port 5432` from the drop down +menu. Next, expand the :menuselection:`Categories --> Spatial Extensions` menu +tree and select :menuselection:`PostGIS 1.5 for PostgreSQL 9.0`. + +After clicking next, you will be prompted to select your mirror, PostGIS +will be downloaded, and the PostGIS installer will begin. Select only the +default options during install (e.g., do not uncheck the option to create a +default PostGIS database). + +.. note:: + + You will be prompted to enter your ``postgres`` database superuser + password in the 'Database Connection Information' dialog. + +psycopg2 +^^^^^^^^ + +The ``psycopg2`` Python module provides the interface between Python and the +PostgreSQL database. Download the latest `Windows installer`__ for your version +of Python and PostgreSQL and run using the default settings. [#]_ + +__ http://www.stickpeople.com/projects/python/win-psycopg/ + +.. _osgeo4w: + +OSGeo4W +^^^^^^^ + +The `OSGeo4W installer`_ makes it simple to install the PROJ.4, GDAL, and GEOS +libraries required by GeoDjango. First, download the `OSGeo4W installer`_, +and run it. Select :menuselection:`Express Web-GIS Install` and click next. +In the 'Select Packages' list, ensure that GDAL is selected; MapServer and +Apache are also enabled by default, but are not required by GeoDjango and +may be unchecked safely. After clicking next, the packages will be +automatically downloaded and installed, after which you may exit the +installer. + +.. _OSGeo4W installer: http://trac.osgeo.org/osgeo4w/ + +Modify Windows environment +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In order to use GeoDjango, you will need to add your Python and OSGeo4W +directories to your Windows system ``Path``, as well as create ``GDAL_DATA`` +and ``PROJ_LIB`` environment variables. The following set of commands, +executable with ``cmd.exe``, will set this up: + +.. code-block:: bat + + set OSGEO4W_ROOT=C:\OSGeo4W + set PYTHON_ROOT=C:\Python27 + set GDAL_DATA=%OSGEO4W_ROOT%\share\gdal + set PROJ_LIB=%OSGEO4W_ROOT%\share\proj + set PATH=%PATH%;%PYTHON_ROOT%;%OSGEO4W_ROOT%\bin + reg ADD "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v Path /t REG_EXPAND_SZ /f /d "%PATH%" + reg ADD "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v GDAL_DATA /t REG_EXPAND_SZ /f /d "%GDAL_DATA%" + reg ADD "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v PROJ_LIB /t REG_EXPAND_SZ /f /d "%PROJ_LIB%" + +For your convenience, these commands are available in the executable batch +script, :download:`geodjango_setup.bat`. + +.. note:: + + Administrator privileges are required to execute these commands. + To do this, right-click on :download:`geodjango_setup.bat` and select + :menuselection:`Run as administrator`. You need to log out and log back in again + for the settings to take effect. + +.. note:: + + If you customized the Python or OSGeo4W installation directories, + then you will need to modify the ``OSGEO4W_ROOT`` and/or ``PYTHON_ROOT`` + variables accordingly. + +Install Django and set up database +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Finally, :ref:`install Django ` on your system. + +.. rubric:: Footnotes +.. [#] GeoDjango uses the :func:`~ctypes.util.find_library` routine from + :mod:`ctypes.util` to locate shared libraries. +.. [#] The ``psycopg2`` Windows installers are packaged and maintained by + `Jason Erickson `_. diff --git a/docs/ref/contrib/gis/install/postgis.txt b/docs/ref/contrib/gis/install/postgis.txt new file mode 100644 index 0000000000..6d7fe88203 --- /dev/null +++ b/docs/ref/contrib/gis/install/postgis.txt @@ -0,0 +1,175 @@ +.. _postgis: + +================== +Installing PostGIS +================== + +`PostGIS`__ adds geographic object support to PostgreSQL, turning it +into a spatial database. :ref:`geosbuild`, :ref:`proj4` and +:ref:`gdalbuild` should be installed prior to building PostGIS. You +might also need additional libraries, see `PostGIS requirements`_. + +.. note:: + + The `psycopg2`_ module is required for use as the database adaptor + when using GeoDjango with PostGIS. + +.. _psycopg2: http://initd.org/psycopg/ +.. _PostGIS requirements: http://www.postgis.org/documentation/manual-2.0/postgis_installation.html#id2711662 + +On Debian/Ubuntu, you are advised to install the following packages: +postgresql-x.x, postgresql-x.x-postgis, postgresql-server-dev-x.x, +python-psycopg2 (x.x matching the PostgreSQL version you want to install). +Please also consult platform-specific instructions if you are on :ref:`macosx` +or :ref:`windows`. + +Building from source +==================== + +First download the source archive, and extract:: + + $ wget http://postgis.refractions.net/download/postgis-2.0.1.tar.gz + $ tar xzf postgis-2.0.1.tar.gz + $ cd postgis-2.0.1 + +Next, configure, make and install PostGIS:: + + $ ./configure + +Finally, make and install:: + + $ make + $ sudo make install + $ cd .. + +.. note:: + + GeoDjango does not automatically create a spatial database. Please consult + the section on :ref:`spatialdb_template91` or + :ref:`spatialdb_template_earlier` for more information. + +__ http://postgis.refractions.net/ + +Post-installation +================= + +.. _spatialdb_template: +.. _spatialdb_template91: + +Creating a spatial database with PostGIS 2.0 and PostgreSQL 9.1 +--------------------------------------------------------------- + +PostGIS 2 includes an extension for Postgres 9.1 that can be used to enable +spatial functionality:: + + $ createdb + $ psql + > CREATE EXTENSION postgis; + > CREATE EXTENSION postgis_topology; + +No PostGIS topology functionalities are yet available from GeoDjango, so the +creation of the ``postgis_topology`` extension is entirely optional. + +.. _spatialdb_template_earlier: + +Creating a spatial database template for earlier versions +--------------------------------------------------------- + +If you have an earlier version of PostGIS or PostgreSQL, the CREATE +EXTENSION isn't available and you need to create the spatial database +using the following instructions. + +Creating a spatial database with PostGIS is different than normal because +additional SQL must be loaded to enable spatial functionality. Because of +the steps in this process, it's better to create a database template that +can be reused later. + +First, you need to be able to execute the commands as a privileged database +user. For example, you can use the following to become the ``postgres`` user:: + + $ sudo su - postgres + +.. note:: + + The location *and* name of the PostGIS SQL files (e.g., from + ``POSTGIS_SQL_PATH`` below) depends on the version of PostGIS. + PostGIS versions 1.3 and below use ``/contrib/lwpostgis.sql``; + whereas version 1.4 uses ``/contrib/postgis.sql`` and + version 1.5 uses ``/contrib/postgis-1.5/postgis.sql``. + + To complicate matters, Debian/Ubuntu distributions have their own separate + directory naming system that might change with time. In this case, use the + :download:`create_template_postgis-debian.sh` script. + + The example below assumes PostGIS 1.5, thus you may need to modify + ``POSTGIS_SQL_PATH`` and the name of the SQL file for the specific + version of PostGIS you are using. + +Once you're a database super user, then you may execute the following commands +to create a PostGIS spatial database template:: + + $ POSTGIS_SQL_PATH=`pg_config --sharedir`/contrib/postgis-2.0 + # Creating the template spatial database. + $ createdb -E UTF8 template_postgis + $ createlang -d template_postgis plpgsql # Adding PLPGSQL language support. + # Allows non-superusers the ability to create from this template + $ psql -d postgres -c "UPDATE pg_database SET datistemplate='true' WHERE datname='template_postgis';" + # Loading the PostGIS SQL routines + $ psql -d template_postgis -f $POSTGIS_SQL_PATH/postgis.sql + $ psql -d template_postgis -f $POSTGIS_SQL_PATH/spatial_ref_sys.sql + # Enabling users to alter spatial tables. + $ psql -d template_postgis -c "GRANT ALL ON geometry_columns TO PUBLIC;" + $ psql -d template_postgis -c "GRANT ALL ON geography_columns TO PUBLIC;" + $ psql -d template_postgis -c "GRANT ALL ON spatial_ref_sys TO PUBLIC;" + +These commands may be placed in a shell script for later use; for convenience +the following scripts are available: + +=============== ============================================= +PostGIS version Bash shell script +=============== ============================================= +1.3 :download:`create_template_postgis-1.3.sh` +1.4 :download:`create_template_postgis-1.4.sh` +1.5 :download:`create_template_postgis-1.5.sh` +Debian/Ubuntu :download:`create_template_postgis-debian.sh` +=============== ============================================= + +Afterwards, you may create a spatial database by simply specifying +``template_postgis`` as the template to use (via the ``-T`` option):: + + $ createdb -T template_postgis + +.. note:: + + While the ``createdb`` command does not require database super-user privileges, + it must be executed by a database user that has permissions to create databases. + You can create such a user with the following command:: + + $ createuser --createdb + +PostgreSQL's createdb fails +--------------------------- + +When the PostgreSQL cluster uses a non-UTF8 encoding, the +:file:`create_template_postgis-*.sh` script will fail when executing +``createdb``:: + + createdb: database creation failed: ERROR: new encoding (UTF8) is incompatible + with the encoding of the template database (SQL_ASCII) + +The `current workaround`__ is to re-create the cluster using UTF8 (back up any +databases before dropping the cluster). + +__ http://jacobian.org/writing/pg-encoding-ubuntu/ + +Managing the database +--------------------- + +To administer the database, you can either use the pgAdmin III program +(:menuselection:`Start --> PostgreSQL 9.0 --> pgAdmin III`) or the +SQL Shell (:menuselection:`Start --> PostgreSQL 9.0 --> SQL Shell`). +For example, to create a ``geodjango`` spatial database and user, the following +may be executed from the SQL Shell as the ``postgres`` user:: + + postgres# CREATE USER geodjango PASSWORD 'my_passwd'; + postgres# CREATE DATABASE geodjango OWNER geodjango TEMPLATE template_postgis ENCODING 'utf8'; diff --git a/docs/ref/contrib/gis/install/spatialite.txt b/docs/ref/contrib/gis/install/spatialite.txt new file mode 100644 index 0000000000..941d559272 --- /dev/null +++ b/docs/ref/contrib/gis/install/spatialite.txt @@ -0,0 +1,222 @@ +.. _spatialite: + +===================== +Installing Spatialite +===================== + +`SpatiaLite`__ adds spatial support to SQLite, turning it into a full-featured +spatial database. + +Check first if you can install Spatialite from system packages or binaries. For +example, on Debian-based distributions, try to install the ``spatialite-bin`` +package. For Mac OS X, follow the +:ref:`specific instructions below`. For Windows, you may +find binaries on `Gaia-SINS`__ home page. In any case, you should always +be able to :ref:`install from source`. + +When you are done with the installation process, skip to :ref:`create_spatialite_db`. + +__ https://www.gaia-gis.it/fossil/libspatialite +__ http://www.gaia-gis.it/gaia-sins/ + +.. _spatialite_source: + +Installing from source +~~~~~~~~~~~~~~~~~~~~~~ + +:ref:`GEOS and PROJ.4` should be installed prior to building +SpatiaLite. + +SQLite +^^^^^^ + +Check first if SQLite is compiled with the `R*Tree module`__. Run the sqlite3 +command line interface and enter the following query:: + + sqlite> CREATE VIRTUAL TABLE testrtree USING rtree(id,minX,maxX,minY,maxY); + +If you obtain an error, you will have to recompile SQLite from source. Otherwise, +just skip this section. + +To install from sources, download the latest amalgamation source archive from +the `SQLite download page`__, and extract:: + + $ wget http://sqlite.org/sqlite-amalgamation-3.6.23.1.tar.gz + $ tar xzf sqlite-amalgamation-3.6.23.1.tar.gz + $ cd sqlite-3.6.23.1 + +Next, run the ``configure`` script -- however the ``CFLAGS`` environment variable +needs to be customized so that SQLite knows to build the R*Tree module:: + + $ CFLAGS="-DSQLITE_ENABLE_RTREE=1" ./configure + $ make + $ sudo make install + $ cd .. + +__ http://www.sqlite.org/rtree.html +__ http://www.sqlite.org/download.html + +.. _spatialitebuild : + +SpatiaLite library (``libspatialite``) and tools (``spatialite``) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Get the latest SpatiaLite library source and tools bundle from the +`download page`__:: + + $ wget http://www.gaia-gis.it/gaia-sins/libspatialite-sources/libspatialite-amalgamation-2.4.0-5.tar.gz + $ wget http://www.gaia-gis.it/gaia-sins/spatialite-tools-sources/spatialite-tools-2.4.0-5.tar.gz + $ tar xzf libspatialite-amalgamation-2.4.0-5.tar.gz + $ tar xzf spatialite-tools-2.4.0-5.tar.gz + +Prior to attempting to build, please read the important notes below to see if +customization of the ``configure`` command is necessary. If not, then run the +``configure`` script, make, and install for the SpatiaLite library:: + + $ cd libspatialite-amalgamation-2.3.1 + $ ./configure # May need to modified, see notes below. + $ make + $ sudo make install + $ cd .... _spatialite + +Finally, do the same for the SpatiaLite tools:: + + $ cd spatialite-tools-2.3.1 + $ ./configure # May need to modified, see notes below. + $ make + $ sudo make install + $ cd .. + +.. note:: + + If you've installed GEOS and PROJ.4 from binary packages, you will have to specify + their paths when running the ``configure`` scripts for *both* the library and the + tools (the configure scripts look, by default, in ``/usr/local``). For example, + on Debian/Ubuntu distributions that have GEOS and PROJ.4 packages, the command would be:: + + $ ./configure --with-proj-include=/usr/include --with-proj-lib=/usr/lib --with-geos-include=/usr/include --with-geos-lib=/usr/lib + +.. note:: + + For Mac OS X users building from source, the SpatiaLite library *and* tools + need to have their ``target`` configured:: + + $ ./configure --target=macosx + +__ http://www.gaia-gis.it/gaia-sins/libspatialite-sources/ + +.. _pysqlite2: + +pysqlite2 +^^^^^^^^^ + +If you are on Python 2.6, you will also have to compile pysqlite2, because +``SpatiaLite`` must be loaded as an external extension, and the required +``enable_load_extension`` method is only available in versions 2.5+ of +pysqlite2. Thus, download pysqlite2 2.6, and untar:: + + $ wget http://pysqlite.googlecode.com/files/pysqlite-2.6.3.tar.gz + $ tar xzf pysqlite-2.6.3.tar.gz + $ cd pysqlite-2.6.3 + +Next, use a text editor (e.g., ``emacs`` or ``vi``) to edit the ``setup.cfg`` file +to look like the following: + +.. code-block:: ini + + [build_ext] + #define= + include_dirs=/usr/local/include + library_dirs=/usr/local/lib + libraries=sqlite3 + #define=SQLITE_OMIT_LOAD_EXTENSION + +or if you are on Mac OS X: + +.. code-block:: ini + + [build_ext] + #define= + include_dirs=/Library/Frameworks/SQLite3.framework/unix/include + library_dirs=/Library/Frameworks/SQLite3.framework/unix/lib + libraries=sqlite3 + #define=SQLITE_OMIT_LOAD_EXTENSION + +.. note:: + + The important thing here is to make sure you comment out the + ``define=SQLITE_OMIT_LOAD_EXTENSION`` flag and that the ``include_dirs`` + and ``library_dirs`` settings are uncommented and set to the appropriate + path if the SQLite header files and libraries are not in ``/usr/include`` + and ``/usr/lib``, respectively. + +After modifying ``setup.cfg`` appropriately, then run the ``setup.py`` script +to build and install:: + + $ sudo python setup.py install + +.. _spatialite_macosx: + +Mac OS X-specific instructions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Mac OS X users should follow the instructions in the :ref:`kyngchaos` section, +as it is much easier than building from source. + +When :ref:`create_spatialite_db`, the ``spatialite`` program is required. +However, instead of attempting to compile the SpatiaLite tools from source, +download the `SpatiaLite Binaries`__ for OS X, and install ``spatialite`` in a +location available in your ``PATH``. For example:: + + $ curl -O http://www.gaia-gis.it/spatialite/spatialite-tools-osx-x86-2.3.1.tar.gz + $ tar xzf spatialite-tools-osx-x86-2.3.1.tar.gz + $ cd spatialite-tools-osx-x86-2.3.1/bin + $ sudo cp spatialite /Library/Frameworks/SQLite3.framework/Programs + +Finally, for GeoDjango to be able to find the KyngChaos SpatiaLite library, +add the following to your ``settings.py``: + +.. code-block:: python + + SPATIALITE_LIBRARY_PATH='/Library/Frameworks/SQLite3.framework/SQLite3' + +__ http://www.gaia-gis.it/spatialite-2.3.1/binaries.html + +.. _create_spatialite_db: + +Creating a spatial database for SpatiaLite +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +After you've installed SpatiaLite, you'll need to create a number of spatial +metadata tables in your database in order to perform spatial queries. + +If you're using SpatiaLite 2.4 or newer, use the ``spatialite`` utility to +call the ``InitSpatialMetaData()`` function, like this:: + + $ spatialite geodjango.db "SELECT InitSpatialMetaData();" + the SPATIAL_REF_SYS table already contains some row(s) + InitSpatiaMetaData ()error:"table spatial_ref_sys already exists" + 0 + +You can safely ignore the error messages shown. When you've done this, you can +skip the rest of this section. + +If you're using SpatiaLite 2.3, you'll need to download a +database-initialization file and execute its SQL queries in your database. + +First, get it from the `SpatiaLite Resources`__ page:: + + $ wget http://www.gaia-gis.it/spatialite-2.3.1/init_spatialite-2.3.sql.gz + $ gunzip init_spatialite-2.3.sql.gz + +Then, use the ``spatialite`` command to initialize a spatial database:: + + $ spatialite geodjango.db < init_spatialite-2.3.sql + +.. note:: + + The parameter ``geodjango.db`` is the *filename* of the SQLite database + you want to use. Use the same in the :setting:`DATABASES` ``"name"`` key + inside your ``settings.py``. + +__ http://www.gaia-gis.it/spatialite-2.3.1/resources.html diff --git a/docs/ref/contrib/gis/testing.txt b/docs/ref/contrib/gis/testing.txt index d12c884a1b..86979f0308 100644 --- a/docs/ref/contrib/gis/testing.txt +++ b/docs/ref/contrib/gis/testing.txt @@ -134,8 +134,6 @@ your settings:: GeoDjango tests =============== -.. versionchanged:: 1.3 - GeoDjango's test suite may be run in one of two ways, either by itself or with the rest of :ref:`Django's unit tests `. diff --git a/docs/ref/contrib/localflavor.txt b/docs/ref/contrib/localflavor.txt index 4595f51d9e..9bb27e6e74 100644 --- a/docs/ref/contrib/localflavor.txt +++ b/docs/ref/contrib/localflavor.txt @@ -6,86 +6,129 @@ The "local flavor" add-ons :synopsis: A collection of various Django snippets that are useful only for a particular country or culture. -Following its "batteries included" philosophy, Django comes with assorted -pieces of code that are useful for particular countries or cultures. These are -called the "local flavor" add-ons and live in the -:mod:`django.contrib.localflavor` package. +Historically, Django has shipped with ``django.contrib.localflavor`` -- +assorted pieces of code that are useful for particular countries or cultures. +Starting with Django 1.5, we've started the process of moving the code to +outside packages (i.e., packages distributed separately from Django), for +easier maintenance and to trim the size of Django's codebase. -Inside that package, country- or culture-specific code is organized into -subpackages, named using `ISO 3166 country codes`_. +The localflavor packages are named ``django-localflavor-*``, where the asterisk +is an `ISO 3166 country code`_. For example: ``django-localflavor-us`` is the +localflavor package for the U.S.A. -Most of the ``localflavor`` add-ons are localized form components deriving -from the :doc:`forms ` framework -- for example, a -:class:`~django.contrib.localflavor.us.forms.USStateField` that knows how to -validate U.S. state abbreviations, and a -:class:`~django.contrib.localflavor.fi.forms.FISocialSecurityNumber` that -knows how to validate Finnish social security numbers. +Most of these ``localflavor`` add-ons are country-specific fields for the +:doc:`forms ` framework -- for example, a +``USStateField`` that knows how to validate U.S. state abbreviations and a +``FISocialSecurityNumber`` that knows how to validate Finnish social security +numbers. To use one of these localized components, just import the relevant subpackage. For example, here's how you can create a form with a field representing a French telephone number:: from django import forms - from django.contrib.localflavor.fr.forms import FRPhoneNumberField + from django_localflavor_fr.forms import FRPhoneNumberField class MyForm(forms.Form): my_french_phone_no = FRPhoneNumberField() +For documentation on a given country's localflavor helpers, see its README +file. + +.. _ISO 3166 country code: http://www.iso.org/iso/country_codes.htm + +How to migrate +============== + +If you've used the old ``django.contrib.localflavor`` package, follow these two +easy steps to update your code: + +1. Install the appropriate third-party ``django-localflavor-*`` package(s). + Go to https://github.com/django/ and find the package for your country. + +2. Change your app's import statements to reference the new packages. + + For example, change this:: + + from django.contrib.localflavor.fr.forms import FRPhoneNumberField + + ...to this:: + + from django_localflavor_fr.forms import FRPhoneNumberField + +The code in the new packages is the same (it was copied directly from Django), +so you don't have to worry about backwards compatibility in terms of +functionality. Only the imports have changed. + +Deprecation policy +================== + +In Django 1.5, importing from ``django.contrib.localflavor`` will result in a +``DeprecationWarning``. This means your code will still work, but you should +change it as soon as possible. + +In Django 1.6, importing from ``django.contrib.localflavor`` will no longer +work. + Supported countries =================== -Countries currently supported by :mod:`~django.contrib.localflavor` are: +The following countries have django-localflavor- packages. -* Argentina_ -* Australia_ -* Austria_ -* Belgium_ -* Brazil_ -* Canada_ -* Chile_ -* China_ -* Colombia_ -* Croatia_ -* Czech_ -* Ecuador_ -* Finland_ -* France_ -* Germany_ -* `Hong Kong`_ -* Iceland_ -* India_ -* Indonesia_ -* Ireland_ -* Israel_ -* Italy_ -* Japan_ -* Kuwait_ -* Macedonia_ -* Mexico_ -* `The Netherlands`_ -* Norway_ -* Peru_ -* Poland_ -* Portugal_ -* Paraguay_ -* Romania_ -* Russia_ -* Slovakia_ -* Slovenia_ -* `South Africa`_ -* Spain_ -* Sweden_ -* Switzerland_ -* Turkey_ -* `United Kingdom`_ -* `United States of America`_ -* Uruguay_ +* Argentina: https://github.com/django/django-localflavor-ar +* Australia: https://github.com/django/django-localflavor-au +* Austria: https://github.com/django/django-localflavor-at +* Belgium: https://github.com/django/django-localflavor-be +* Brazil: https://github.com/django/django-localflavor-br +* Canada: https://github.com/django/django-localflavor-ca +* Chile: https://github.com/django/django-localflavor-cl +* China: https://github.com/django/django-localflavor-cn +* Colombia: https://github.com/django/django-localflavor-co +* Croatia: https://github.com/django/django-localflavor-cr +* Czech Republic: https://github.com/django/django-localflavor-cz +* Ecuador: https://github.com/django/django-localflavor-ec +* Finland: https://github.com/django/django-localflavor-fi +* France: https://github.com/django/django-localflavor-fr +* Germany: https://github.com/django/django-localflavor-de +* Hong Kong: https://github.com/django/django-localflavor-hk +* Iceland: https://github.com/django/django-localflavor-is +* India: https://github.com/django/django-localflavor-in +* Indonesia: https://github.com/django/django-localflavor-id +* Ireland: https://github.com/django/django-localflavor-ie +* Israel: https://github.com/django/django-localflavor-il +* Italy: https://github.com/django/django-localflavor-it +* Japan: https://github.com/django/django-localflavor-jp +* Kuwait: https://github.com/django/django-localflavor-kw +* Lithuania: https://github.com/simukis/django-localflavor-lt +* Macedonia: https://github.com/django/django-localflavor-mk +* Mexico: https://github.com/django/django-localflavor-mx +* The Netherlands: https://github.com/django/django-localflavor-nl +* Norway: https://github.com/django/django-localflavor-no +* Peru: https://github.com/django/django-localflavor-pe +* Poland: https://github.com/django/django-localflavor-pl +* Portugal: https://github.com/django/django-localflavor-pt +* Paraguay: https://github.com/django/django-localflavor-py +* Romania: https://github.com/django/django-localflavor-ro +* Russia: https://github.com/django/django-localflavor-ru +* Slovakia: https://github.com/django/django-localflavor-sk +* Slovenia: https://github.com/django/django-localflavor-si +* South Africa: https://github.com/django/django-localflavor-za +* Spain: https://github.com/django/django-localflavor-es +* Sweden: https://github.com/django/django-localflavor-se +* Switzerland: https://github.com/django/django-localflavor-ch +* Turkey: https://github.com/django/django-localflavor-tr +* United Kingdom: https://github.com/django/django-localflavor-gb +* United States of America: https://github.com/django/django-localflavor-us +* Uruguay: https://github.com/django/django-localflavor-uy -The ``django.contrib.localflavor`` package also includes a ``generic`` subpackage, -containing useful code that is not specific to one particular country or culture. -Currently, it defines date, datetime and split datetime input fields based on -those from :doc:`forms `, but with non-US default formats. -Here's an example of how to use them:: +django.contrib.localflavor.generic +================================== + +The ``django.contrib.localflavor.generic`` package, which hasn't been removed from +Django yet, contains useful code that is not specific to one particular country +or culture. Currently, it defines date, datetime and split datetime input +fields based on those from :doc:`forms `, but with non-US +default formats. Here's an example of how to use them:: from django import forms from django.contrib.localflavor import generic @@ -93,52 +136,6 @@ Here's an example of how to use them:: class MyForm(forms.Form): my_date_field = generic.forms.DateField() -.. _ISO 3166 country codes: http://www.iso.org/iso/country_codes.htm -.. _Argentina: `Argentina (ar)`_ -.. _Australia: `Australia (au)`_ -.. _Austria: `Austria (at)`_ -.. _Belgium: `Belgium (be)`_ -.. _Brazil: `Brazil (br)`_ -.. _Canada: `Canada (ca)`_ -.. _Chile: `Chile (cl)`_ -.. _China: `China (cn)`_ -.. _Colombia: `Colombia (co)`_ -.. _Croatia: `Croatia (hr)`_ -.. _Czech: `Czech (cz)`_ -.. _Ecuador: `Ecuador (ec)`_ -.. _Finland: `Finland (fi)`_ -.. _France: `France (fr)`_ -.. _Germany: `Germany (de)`_ -.. _Hong Kong: `Hong Kong (hk)`_ -.. _The Netherlands: `The Netherlands (nl)`_ -.. _Iceland: `Iceland (is\_)`_ -.. _India: `India (in\_)`_ -.. _Indonesia: `Indonesia (id)`_ -.. _Ireland: `Ireland (ie)`_ -.. _Israel: `Israel (il)`_ -.. _Italy: `Italy (it)`_ -.. _Japan: `Japan (jp)`_ -.. _Kuwait: `Kuwait (kw)`_ -.. _Macedonia: `Macedonia (mk)`_ -.. _Mexico: `Mexico (mx)`_ -.. _Norway: `Norway (no)`_ -.. _Paraguay: `Paraguay (py)`_ -.. _Peru: `Peru (pe)`_ -.. _Poland: `Poland (pl)`_ -.. _Portugal: `Portugal (pt)`_ -.. _Romania: `Romania (ro)`_ -.. _Russia: `Russia (ru)`_ -.. _Slovakia: `Slovakia (sk)`_ -.. _Slovenia: `Slovenia (si)`_ -.. _South Africa: `South Africa (za)`_ -.. _Spain: `Spain (es)`_ -.. _Sweden: `Sweden (se)`_ -.. _Switzerland: `Switzerland (ch)`_ -.. _Turkey: `Turkey (tr)`_ -.. _United Kingdom: `United Kingdom (gb)`_ -.. _United States of America: `United States of America (us)`_ -.. _Uruguay: `Uruguay (uy)`_ - Internationalization of localflavor =================================== @@ -149,1260 +146,3 @@ texts to be translated, like form fields error messages, you must include :mod:`django.contrib.localflavor` in the :setting:`INSTALLED_APPS` setting, so the internationalization system can find the catalog, as explained in :ref:`how-django-discovers-translations`. - -Adding flavors -============== - -We'd love to add more of these to Django, so please `create a ticket`_ with -any code you'd like to contribute. One thing we ask is that you please use -Unicode objects (``u'mystring'``) for strings, rather than setting the encoding -in the file. See any of the existing flavors for examples. - -.. _create a ticket: https://code.djangoproject.com/newticket - -Localflavor and backwards compatibility -======================================= - -As documented in our :ref:`API stability -` policy, Django will always attempt -to make :mod:`django.contrib.localflavor` reflect the officially -gazetted policies of the appropriate local government authority. For -example, if a government body makes a change to add, alter, or remove -a province (or state, or county), that change will be reflected in -Django's localflavor in the next stable Django release. - -When a backwards-incompatible change is made (for example, the removal -or renaming of a province) the localflavor in question will raise a -warning when that localflavor is imported. This provides a runtime -indication that something may require attention. - -However, once you have addressed the backwards compatibility (for -example, auditing your code to see if any data migration is required), -the warning serves no purpose. The warning can then be supressed. -For example, to suppress the warnings raised by the Indonesian -localflavor you would use the following code:: - - import warnings - warnings.filterwarnings('ignore', - category=RuntimeWarning, - module='django.contrib.localflavor.id') - from django.contrib.localflavor.id import forms as id_forms - - -Argentina (``ar``) -============================================= - -.. class:: ar.forms.ARPostalCodeField - - A form field that validates input as either a classic four-digit Argentinian - postal code or a CPA_. - -.. _CPA: http://www.correoargentino.com.ar/consulta_cpa/home.php - -.. class:: ar.forms.ARDNIField - - A form field that validates input as a Documento Nacional de Identidad (DNI) - number. - -.. class:: ar.forms.ARCUITField - - A form field that validates input as a Codigo Unico de Identificacion - Tributaria (CUIT) number. - -.. class:: ar.forms.ARProvinceSelect - - A ``Select`` widget that uses a list of Argentina's provinces and autonomous - cities as its choices. - -Australia (``au``) -============================================= - -.. versionadded:: 1.4 - -.. class:: au.forms.AUPostCodeField - - A form field that validates input as an Australian postcode. - -.. class:: au.forms.AUPhoneNumberField - - A form field that validates input as an Australian phone number. Valid numbers - have ten digits. - -.. class:: au.forms.AUStateSelect - - A ``Select`` widget that uses a list of Australian states/territories as its - choices. - -.. class:: au.models.AUPhoneNumberField - - A model field that checks that the value is a valid Australian phone - number (ten digits). - -.. class:: au.models.AUStateField - - A model field that forms represent as a ``forms.AUStateField`` field and - stores the three-letter Australian state abbreviation in the database. - -.. class:: au.models.AUPostCodeField - - A model field that forms represent as a ``forms.AUPostCodeField`` field - and stores the four-digit Australian postcode in the database. - -Austria (``at``) -================ - -.. class:: at.forms.ATZipCodeField - - A form field that validates its input as an Austrian zip code, with the - format XXXX (first digit must be greater than 0). - -.. class:: at.forms.ATStateSelect - - A ``Select`` widget that uses a list of Austrian states as its choices. - -.. class:: at.forms.ATSocialSecurityNumberField - - A form field that validates its input as an Austrian social security number. - -Belgium (``be``) -================ - -.. versionadded:: 1.3 - -.. class:: be.forms.BEPhoneNumberField - - A form field that validates input as a Belgium phone number, with one of - the formats 0x xxx xx xx, 0xx xx xx xx, 04xx xx xx xx, 0x/xxx.xx.xx, - 0xx/xx.xx.xx, 04xx/xx.xx.xx, 0x.xxx.xx.xx, 0xx.xx.xx.xx, 04xx.xx.xx.xx, - 0xxxxxxxx or 04xxxxxxxx. - -.. class:: be.forms.BEPostalCodeField - - A form field that validates input as a Belgium postal code, in the range - and format 1XXX-9XXX. - -.. class:: be.forms.BEProvinceSelect - - A ``Select`` widget that uses a list of Belgium provinces as its - choices. - -.. class:: be.forms.BERegionSelect - - A ``Select`` widget that uses a list of Belgium regions as its - choices. - -Brazil (``br``) -=============== - -.. class:: br.forms.BRPhoneNumberField - - A form field that validates input as a Brazilian phone number, with the format - XX-XXXX-XXXX. - -.. class:: br.forms.BRZipCodeField - - A form field that validates input as a Brazilian zip code, with the format - XXXXX-XXX. - -.. class:: br.forms.BRStateSelect - - A ``Select`` widget that uses a list of Brazilian states/territories as its - choices. - -.. class:: br.forms.BRCPFField - - A form field that validates input as `Brazilian CPF`_. - - Input can either be of the format XXX.XXX.XXX-VD or be a group of 11 digits. - -.. _Brazilian CPF: http://en.wikipedia.org/wiki/Cadastro_de_Pessoas_F%C3%ADsicas - -.. class:: br.forms.BRCNPJField - - A form field that validates input as `Brazilian CNPJ`_. - - Input can either be of the format XX.XXX.XXX/XXXX-XX or be a group of 14 - digits. - -.. _Brazilian CNPJ: http://en.wikipedia.org/wiki/National_identification_number#Brazil - -Canada (``ca``) -=============== - -.. class:: ca.forms.CAPhoneNumberField - - A form field that validates input as a Canadian phone number, with the format - XXX-XXX-XXXX. - -.. class:: ca.forms.CAPostalCodeField - - A form field that validates input as a Canadian postal code, with the format - XXX XXX. - -.. class:: ca.forms.CAProvinceField - - A form field that validates input as a Canadian province name or abbreviation. - -.. class:: ca.forms.CASocialInsuranceNumberField - - A form field that validates input as a Canadian Social Insurance Number (SIN). - A valid number must have the format XXX-XXX-XXX and pass a `Luhn mod-10 - checksum`_. - -.. _Luhn mod-10 checksum: http://en.wikipedia.org/wiki/Luhn_algorithm - -.. class:: ca.forms.CAProvinceSelect - - A ``Select`` widget that uses a list of Canadian provinces and territories as - its choices. - -Chile (``cl``) -============== - -.. class:: cl.forms.CLRutField - - A form field that validates input as a Chilean national identification number - ('Rol Unico Tributario' or RUT). The valid format is XX.XXX.XXX-X. - -.. class:: cl.forms.CLRegionSelect - - A ``Select`` widget that uses a list of Chilean regions (Regiones) as its - choices. - -China (``cn``) -============== - -.. versionadded:: 1.4 - -.. class:: cn.forms.CNProvinceSelect - - A ``Select`` widget that uses a list of Chinese regions as its choices. - -.. class:: cn.forms.CNPostCodeField - - A form field that validates input as a Chinese post code. - Valid formats are XXXXXX where X is digit. - -.. class:: cn.forms.CNIDCardField - - A form field that validates input as a Chinese Identification Card Number. - Both 1st and 2nd generation ID Card Number are validated. - -.. class:: cn.forms.CNPhoneNumberField - - A form field that validates input as a Chinese phone number. - Valid formats are 0XX-XXXXXXXX, composed of 3 or 4 digits of region code - and 7 or 8 digits of phone number. - -.. class:: cn.forms.CNCellNumberField - - A form field that validates input as a Chinese mobile phone number. - Valid formats are like 1XXXXXXXXXX, where X is digit. - The second digit could only be 3, 5 and 8. - -Colombia (``co``) -================= - -.. versionadded:: 1.4 - -.. class:: co.forms.CoDepartmentSelect - - A ``Select`` widget that uses a list of Colombian departments - as its choices. - -Croatia (``hr``) -================ - -.. versionadded:: 1.4 - -.. class:: hr.forms.HRCountySelect - - A ``Select`` widget that uses a list of counties of Croatia as its choices. - -.. class:: hr.forms.HRPhoneNumberPrefixSelect - - A ``Select`` widget that uses a list of phone number prefixes of Croatia as - its choices. - -.. class:: hr.forms.HRLicensePlatePrefixSelect - - A ``Select`` widget that uses a list of vehicle license plate prefixes of - Croatia as its choices. - -.. class:: hr.forms.HRPhoneNumberField - - A form field that validates input as a phone number of Croatia. - A valid format is a country code or a leading zero, area code prefix, 6 or 7 - digit number; e.g. +385XXXXXXXX or 0XXXXXXXX - Validates fixed, mobile and FGSM numbers. Normalizes to a full number with - country code (+385 prefix). - -.. class:: hr.forms.HRLicensePlateField - - A form field that validates input as a vehicle license plate of Croatia. - Normalizes to the specific format XX YYYY-XX where X is a letter and Y a - digit. There can be three or four digits. - Suffix is constructed from the shared letters of the Croatian and English - alphabets. - It is used for standardized license plates only. Special cases like license - plates for oldtimers, temporary license plates, government institution - license plates and customized license plates are not covered by this field. - -.. class:: hr.forms.HRPostalCodeField - - A form field that validates input as a postal code of Croatia. - It consists of exactly five digits ranging from 10000 to 59999 inclusive. - -.. class:: hr.forms.HROIBField - - A form field that validates input as a Personal Identification Number (OIB) - of Croatia. - It consists of exactly eleven digits. - -.. class:: hr.forms.HRJMBGField - - A form field that validates input as a Unique Master Citizen Number (JMBG). - The number is still in use in Croatia, but it is being replaced by OIB. - This field works for other ex-Yugoslavia countries as well where the JMBG is - still in use. - The area segment of the JMBG is not validated because the citizens might - have emigrated to another ex-Yugoslavia country. - The number consists of exactly thirteen digits. - -.. class:: hr.forms.HRJMBAGField - - A form field that validates input as a Unique Master Academic Citizen Number - (JMBAG) of Croatia. - This number is used by college students and professors in Croatia. - The number consists of exactly nineteen digits. - -Czech (``cz``) -============== - -.. class:: cz.forms.CZPostalCodeField - - A form field that validates input as a Czech postal code. Valid formats - are XXXXX or XXX XX, where X is a digit. - -.. class:: cz.forms.CZBirthNumberField - - A form field that validates input as a Czech Birth Number. - A valid number must be in format XXXXXX/XXXX (slash is optional). - -.. class:: cz.forms.CZICNumberField - - A form field that validates input as a Czech IC number field. - -.. class:: cz.forms.CZRegionSelect - - A ``Select`` widget that uses a list of Czech regions as its choices. - -Ecuador (``ec``) -================ - -.. versionadded:: 1.4 - -.. class:: ec.forms.EcProvinceSelect - - A ``Select`` widget that uses a list of Ecuatorian provinces as - its choices. - -Finland (``fi``) -================ - -.. class:: fi.forms.FISocialSecurityNumber - - A form field that validates input as a Finnish social security number. - -.. class:: fi.forms.FIZipCodeField - - A form field that validates input as a Finnish zip code. Valid codes - consist of five digits. - -.. class:: fi.forms.FIMunicipalitySelect - - A ``Select`` widget that uses a list of Finnish municipalities as its - choices. - -France (``fr``) -=============== - -.. class:: fr.forms.FRPhoneNumberField - - A form field that validates input as a French local phone number. The - correct format is 0X XX XX XX XX. 0X.XX.XX.XX.XX and 0XXXXXXXXX validate - but are corrected to 0X XX XX XX XX. - -.. class:: fr.forms.FRZipCodeField - - A form field that validates input as a French zip code. Valid codes - consist of five digits. - -.. class:: fr.forms.FRDepartmentSelect - - A ``Select`` widget that uses a list of French departments as its choices. - -Germany (``de``) -================ - -.. class:: de.forms.DEIdentityCardNumberField - - A form field that validates input as a German identity card number - (Personalausweis_). Valid numbers have the format - XXXXXXXXXXX-XXXXXXX-XXXXXXX-X, with no group consisting entirely of zeroes. - -.. _Personalausweis: http://de.wikipedia.org/wiki/Personalausweis - -.. class:: de.forms.DEZipCodeField - - A form field that validates input as a German zip code. Valid codes - consist of five digits. - -.. class:: de.forms.DEStateSelect - - A ``Select`` widget that uses a list of German states as its choices. - -Hong Kong (``hk``) -================== - -.. class:: hk.forms.HKPhoneNumberField - - A form field that validates input as a Hong Kong phone number. - - -The Netherlands (``nl``) -======================== - -.. class:: nl.forms.NLPhoneNumberField - - A form field that validates input as a Dutch telephone number. - -.. class:: nl.forms.NLSofiNumberField - - A form field that validates input as a Dutch social security number - (SoFI/BSN). - -.. class:: nl.forms.NLZipCodeField - - A form field that validates input as a Dutch zip code. - -.. class:: nl.forms.NLProvinceSelect - - A ``Select`` widget that uses a list of Dutch provinces as its list of - choices. - -Iceland (``is_``) -================= - -.. class:: is_.forms.ISIdNumberField - - A form field that validates input as an Icelandic identification number - (kennitala). The format is XXXXXX-XXXX. - -.. class:: is_.forms.ISPhoneNumberField - - A form field that validates input as an Icelandtic phone number (seven - digits with an optional hyphen or space after the first three digits). - -.. class:: is_.forms.ISPostalCodeSelect - - A ``Select`` widget that uses a list of Icelandic postal codes as its - choices. - -India (``in_``) -=============== - -.. class:: in_.forms.INStateField - - A form field that validates input as an Indian state/territory name or - abbreviation. Input is normalized to the standard two-letter vehicle - registration abbreviation for the given state or territory. - -.. class:: in_.forms.INZipCodeField - - A form field that validates input as an Indian zip code, with the - format XXXXXXX. - -.. class:: in_.forms.INStateSelect - - A ``Select`` widget that uses a list of Indian states/territories as its - choices. - -.. versionadded:: 1.4 - -.. class:: in_.forms.INPhoneNumberField - - A form field that validates that the data is a valid Indian phone number, - including the STD code. It's normalised to 0XXX-XXXXXXX or 0XXX XXXXXXX - format. The first string is the STD code which is a '0' followed by 2-4 - digits. The second string is 8 digits if the STD code is 3 digits, 7 - digits if the STD code is 4 digits and 6 digits if the STD code is 5 - digits. The second string will start with numbers between 1 and 6. The - separator is either a space or a hyphen. - -Ireland (``ie``) -================ - -.. class:: ie.forms.IECountySelect - - A ``Select`` widget that uses a list of Irish Counties as its choices. - -Indonesia (``id``) -================== - -.. class:: id.forms.IDPostCodeField - - A form field that validates input as an Indonesian post code field. - -.. class:: id.forms.IDProvinceSelect - - A ``Select`` widget that uses a list of Indonesian provinces as its choices. - -.. versionchanged:: 1.3 - The province "Nanggroe Aceh Darussalam (NAD)" has been removed - from the province list in favor of the new official designation - "Aceh (ACE)". - -.. class:: id.forms.IDPhoneNumberField - - A form field that validates input as an Indonesian telephone number. - -.. class:: id.forms.IDLicensePlatePrefixSelect - - A ``Select`` widget that uses a list of Indonesian license plate - prefix code as its choices. - -.. class:: id.forms.IDLicensePlateField - - A form field that validates input as an Indonesian vehicle license plate. - -.. class:: id.forms.IDNationalIdentityNumberField - - A form field that validates input as an Indonesian national identity - number (`NIK`_/KTP). The output will be in the format of - 'XX.XXXX.DDMMYY.XXXX'. Dots or spaces can be used in the input to break - down the numbers. - -.. _NIK: http://en.wikipedia.org/wiki/Indonesian_identity_card - -Israel (``il``) -=============== - -.. class:: il.forms.ILPostalCodeField - - A form field that validates its input as an Israeli five-digit postal code. - -.. class:: il.forms.ILIDNumberField - - A form field that validates its input as an `Israeli identification number`_. - The output will be in the format of a 2-9 digit number, consisting of a - 1-8 digit ID number followed by a single checksum digit, calculated using - the `Luhn algorithm`_. - - Input may contain an optional hyphen separating the ID number from the checksum - digit. - -.. _Israeli identification number: http://he.wikipedia.org/wiki/%D7%9E%D7%A1%D7%A4%D7%A8_%D7%96%D7%94%D7%95%D7%AA_(%D7%99%D7%A9%D7%A8%D7%90%D7%9C) -.. _Luhn algorithm: http://en.wikipedia.org/wiki/Luhn_algorithm - -Italy (``it``) -============== - -.. class:: it.forms.ITSocialSecurityNumberField - - A form field that validates input as an Italian social security number - (`codice fiscale`_). - -.. _codice fiscale: http://www.agenziaentrate.gov.it/wps/content/Nsilib/Nsi/Home/CosaDeviFare/Richiedere/Codice+fiscale+e+tessera+sanitaria/Richiesta+TS_CF/SchedaI/Informazioni+codificazione+pf/ - -.. class:: it.forms.ITVatNumberField - - A form field that validates Italian VAT numbers (partita IVA). - -.. class:: it.forms.ITZipCodeField - - A form field that validates input as an Italian zip code. Valid codes - must have five digits. - -.. class:: it.forms.ITProvinceSelect - - A ``Select`` widget that uses a list of Italian provinces as its choices. - -.. class:: it.forms.ITRegionSelect - - A ``Select`` widget that uses a list of Italian regions as its choices. - -Japan (``jp``) -============== - -.. class:: jp.forms.JPPostalCodeField - - A form field that validates input as a Japanese postcode. It accepts seven - digits, with or without a hyphen. - -.. class:: jp.forms.JPPrefectureSelect - - A ``Select`` widget that uses a list of Japanese prefectures as its choices. - -Kuwait (``kw``) -=============== - -.. class:: kw.forms.KWCivilIDNumberField - - A form field that validates input as a Kuwaiti Civil ID number. A valid - Civil ID number must obey the following rules: - - * The number consist of 12 digits. - * The birthdate of the person is a valid date. - * The calculated checksum equals to the last digit of the Civil ID. - -Macedonia (``mk``) -=================== - -.. versionadded:: 1.4 - -.. class:: mk.forms.MKIdentityCardNumberField - - A form field that validates input as a Macedonian identity card number. - Both old and new identity card numbers are supported. - - -.. class:: mk.forms.MKMunicipalitySelect - - A form ``Select`` widget that uses a list of Macedonian municipalities as - choices. - - -.. class:: mk.forms.UMCNField - - A form field that validates input as a unique master citizen - number. - - The format of the unique master citizen number is not unique - to Macedonia. For more information see: - https://secure.wikimedia.org/wikipedia/en/wiki/Unique_Master_Citizen_Number - - A value will pass validation if it complies to the following rules: - - * Consists of exactly 13 digits - * The first 7 digits represent a valid past date in the format DDMMYYY - * The last digit of the UMCN passes a checksum test - - -.. class:: mk.models.MKIdentityCardNumberField - - A model field that forms represent as a - ``forms.MKIdentityCardNumberField`` field. - - -.. class:: mk.models.MKMunicipalityField - - A model field that forms represent as a - ``forms.MKMunicipalitySelect`` and stores the 2 character code of the - municipality in the database. - - -.. class:: mk.models.UMCNField - - A model field that forms represent as a ``forms.UMCNField`` field. - - -Mexico (``mx``) -=============== - -.. class:: mx.forms.MXZipCodeField - - .. versionadded:: 1.4 - - A form field that accepts a Mexican Zip Code. - - More info about this: List of postal codes in Mexico (zipcodes_) - -.. _zipcodes: http://en.wikipedia.org/wiki/List_of_postal_codes_in_Mexico - -.. class:: mx.forms.MXRFCField - - .. versionadded:: 1.4 - - A form field that validates a Mexican *Registro Federal de Contribuyentes* for - either **Persona física** or **Persona moral**. This field accepts RFC strings - whether or not it contains a *homoclave*. - - More info about this: Registro Federal de Contribuyentes (rfc_) - -.. _rfc: http://es.wikipedia.org/wiki/Registro_Federal_de_Contribuyentes_(M%C3%A9xico) - -.. class:: mx.forms.MXCURPField - - .. versionadded:: 1.4 - - A field that validates a Mexican *Clave Única de Registro de Población*. - - More info about this: Clave Unica de Registro de Poblacion (curp_) - -.. _curp: http://www.condusef.gob.mx/index.php/clave-unica-de-registro-de-poblacion-curp - -.. class:: mx.forms.MXStateSelect - - A ``Select`` widget that uses a list of Mexican states as its choices. - -.. class:: mx.models.MXStateField - - .. versionadded:: 1.4 - - A model field that stores the three-letter Mexican state abbreviation in the - database. - -.. class:: mx.models.MXZipCodeField - - .. versionadded:: 1.4 - - A model field that forms represent as a ``forms.MXZipCodeField`` field and - stores the five-digit Mexican zip code. - -.. class:: mx.models.MXRFCField - - .. versionadded:: 1.4 - - A model field that forms represent as a ``forms.MXRFCField`` field and - stores the value of a valid Mexican RFC. - -.. class:: mx.models.MXCURPField - - .. versionadded:: 1.4 - - A model field that forms represent as a ``forms.MXCURPField`` field and - stores the value of a valid Mexican CURP. - -Additionally, a choice tuple is provided in ``django.contrib.localflavor.mx.mx_states``, -allowing customized model and form fields, and form presentations, for subsets of -Mexican states abbreviations: - -.. data:: mx.mx_states.STATE_CHOICES - - A tuple of choices of the states abbreviations for all 31 Mexican states, - plus the `Distrito Federal`. - -Norway (``no``) -=============== - -.. class:: no.forms.NOSocialSecurityNumber - - A form field that validates input as a Norwegian social security number - (personnummer_). - -.. _personnummer: http://no.wikipedia.org/wiki/Personnummer - -.. class:: no.forms.NOZipCodeField - - A form field that validates input as a Norwegian zip code. Valid codes - have four digits. - -.. class:: no.forms.NOMunicipalitySelect - - A ``Select`` widget that uses a list of Norwegian municipalities (fylker) as - its choices. - -Paraguay (``py``) -================= - -.. versionadded:: 1.4 - -.. class:: py.forms.PyDepartmentSelect - - A ``Select`` widget with a list of Paraguayan departments as choices. - -.. class:: py.forms.PyNumberedDepartmentSelect - - A ``Select`` widget with a roman numbered list of Paraguayan departments as choices. - -Peru (``pe``) -============= - -.. class:: pe.forms.PEDNIField - - A form field that validates input as a DNI (Peruvian national identity) - number. - -.. class:: pe.forms.PERUCField - - A form field that validates input as an RUC (Registro Unico de - Contribuyentes) number. Valid RUC numbers have 11 digits. - -.. class:: pe.forms.PEDepartmentSelect - - A ``Select`` widget that uses a list of Peruvian Departments as its choices. - -Poland (``pl``) -=============== - -.. class:: pl.forms.PLPESELField - - A form field that validates input as a Polish national identification number - (PESEL_). - -.. _PESEL: http://en.wikipedia.org/wiki/PESEL - -.. versionadded:: 1.4 - -.. class:: pl.forms.PLNationalIDCardNumberField - - A form field that validates input as a Polish National ID Card number. The - valid format is AAAXXXXXX, where A is letter (A-Z), X is digit and left-most - digit is checksum digit. More information about checksum calculation algorithm - see `Polish identity card`_. - -.. _`Polish identity card`: http://en.wikipedia.org/wiki/Polish_identity_card - -.. class:: pl.forms.PLREGONField - - A form field that validates input as a Polish National Official Business - Register Number (REGON_), having either seven or nine digits. The checksum - algorithm used for REGONs is documented at - http://wipos.p.lodz.pl/zylla/ut/nip-rego.html. - -.. _REGON: http://www.stat.gov.pl/bip/regon_ENG_HTML.htm - -.. class:: pl.forms.PLPostalCodeField - - A form field that validates input as a Polish postal code. The valid format - is XX-XXX, where X is a digit. - -.. class:: pl.forms.PLNIPField - - A form field that validates input as a Polish Tax Number (NIP). Valid formats - are XXX-XXX-XX-XX, XXX-XX-XX-XXX or XXXXXXXXXX. The checksum algorithm used - for NIPs is documented at http://wipos.p.lodz.pl/zylla/ut/nip-rego.html. - -.. class:: pl.forms.PLCountySelect - - A ``Select`` widget that uses a list of Polish administrative units as its - choices. - -.. class:: pl.forms.PLProvinceSelect - - A ``Select`` widget that uses a list of Polish voivodeships (administrative - provinces) as its choices. - -Portugal (``pt``) -================= - -.. class:: pt.forms.PTZipCodeField - - A form field that validates input as a Portuguese zip code. - -.. class:: pt.forms.PTPhoneNumberField - - A form field that validates input as a Portuguese phone number. - Valid numbers have 9 digits (may include spaces) or start by 00 - or + (international). - -Romania (``ro``) -================ - -.. class:: ro.forms.ROCIFField - - A form field that validates Romanian fiscal identification codes (CIF). The - return value strips the leading RO, if given. - -.. class:: ro.forms.ROCNPField - - A form field that validates Romanian personal numeric codes (CNP). - -.. class:: ro.forms.ROCountyField - - A form field that validates its input as a Romanian county (judet) name or - abbreviation. It normalizes the input to the standard vehicle registration - abbreviation for the given county. This field will only accept names written - with diacritics; consider using ROCountySelect as an alternative. - -.. class:: ro.forms.ROCountySelect - - A ``Select`` widget that uses a list of Romanian counties (judete) as its - choices. - -.. class:: ro.forms.ROIBANField - - A form field that validates its input as a Romanian International Bank - Account Number (IBAN). The valid format is ROXX-XXXX-XXXX-XXXX-XXXX-XXXX, - with or without hyphens. - -.. class:: ro.forms.ROPhoneNumberField - - A form field that validates Romanian phone numbers, short special numbers - excluded. - -.. class:: ro.forms.ROPostalCodeField - - A form field that validates Romanian postal codes. - -Russia (``ru``) -=============== - -.. versionadded:: 1.4 - -.. class:: ru.forms.RUPostalCodeField - - Russian Postal code field. The valid format is XXXXXX, where X is any - digit and the first digit is not zero. - -.. class:: ru.forms.RUCountySelect - - A ``Select`` widget that uses a list of Russian Counties as its choices. - -.. class:: ru.forms.RURegionSelect - - A ``Select`` widget that uses a list of Russian Regions as its choices. - -.. class:: ru.forms.RUPassportNumberField - - Russian internal passport number. The valid format is XXXX XXXXXX, where X - is any digit. - -.. class:: ru.forms.RUAlienPassportNumberField - - Russian alien's passport number. The valid format is XX XXXXXXX, where X - is any digit. - -Slovakia (``sk``) -================= - -.. class:: sk.forms.SKPostalCodeField - - A form field that validates input as a Slovak postal code. Valid formats - are XXXXX or XXX XX, where X is a digit. - -.. class:: sk.forms.SKDistrictSelect - - A ``Select`` widget that uses a list of Slovak districts as its choices. - -.. class:: sk.forms.SKRegionSelect - - A ``Select`` widget that uses a list of Slovak regions as its choices. - -Slovenia (``si``) -================= - -.. class:: si.forms.SIEMSOField - - A form field that validates input as Slovenian personal identification - number and stores gender and birthday to self.info dictionary. - -.. class:: si.forms.SITaxNumberField - - A form field that validates input as a Slovenian tax number. Valid input - is SIXXXXXXXX or XXXXXXXX. - -.. class:: si.forms.SIPhoneNumberField - - A form field that validates input as a Slovenian phone number. Phone - number must contain at least local area code with optional country code. - -.. class:: si.forms.SIPostalCodeField - - A form field that provides a choice field of major Slovenian postal - codes. - -.. class:: si.forms.SIPostalCodeSelect - - A ``Select`` widget that uses a list of major Slovenian postal codes as - its choices. - - -South Africa (``za``) -===================== - -.. class:: za.forms.ZAIDField - - A form field that validates input as a South African ID number. Validation - uses the Luhn checksum and a simplistic (i.e., not entirely accurate) check - for birth date. - -.. class:: za.forms.ZAPostCodeField - - A form field that validates input as a South African postcode. Valid - postcodes must have four digits. - -Spain (``es``) -============== - -.. class:: es.forms.ESIdentityCardNumberField - - A form field that validates input as a Spanish NIF/NIE/CIF (Fiscal - Identification Number) code. - -.. class:: es.forms.ESCCCField - - A form field that validates input as a Spanish bank account number (Codigo - Cuenta Cliente or CCC). A valid CCC number has the format - EEEE-OOOO-CC-AAAAAAAAAA, where the E, O, C and A digits denote the entity, - office, checksum and account, respectively. The first checksum digit - validates the entity and office. The second checksum digit validates the - account. It is also valid to use a space as a delimiter, or to use no - delimiter. - -.. class:: es.forms.ESPhoneNumberField - - A form field that validates input as a Spanish phone number. Valid numbers - have nine digits, the first of which is 6, 8 or 9. - -.. class:: es.forms.ESPostalCodeField - - A form field that validates input as a Spanish postal code. Valid codes - have five digits, the first two being in the range 01 to 52, representing - the province. - -.. class:: es.forms.ESProvinceSelect - - A ``Select`` widget that uses a list of Spanish provinces as its choices. - -.. class:: es.forms.ESRegionSelect - - A ``Select`` widget that uses a list of Spanish regions as its choices. - -Sweden (``se``) -=============== - -.. class:: se.forms.SECountySelect - - A Select form widget that uses a list of the Swedish counties (län) as its - choices. - - The cleaned value is the official county code -- see - http://en.wikipedia.org/wiki/Counties_of_Sweden for a list. - -.. class:: se.forms.SEOrganisationNumber - - A form field that validates input as a Swedish organisation number - (organisationsnummer). - - It accepts the same input as SEPersonalIdentityField (for sole - proprietorships (enskild firma). However, co-ordination numbers are not - accepted. - - It also accepts ordinary Swedish organisation numbers with the format - NNNNNNNNNN. - - The return value will be YYYYMMDDXXXX for sole proprietors, and NNNNNNNNNN - for other organisations. - -.. class:: se.forms.SEPersonalIdentityNumber - - A form field that validates input as a Swedish personal identity number - (personnummer). - - The correct formats are YYYYMMDD-XXXX, YYYYMMDDXXXX, YYMMDD-XXXX, - YYMMDDXXXX and YYMMDD+XXXX. - - A \+ indicates that the person is older than 100 years, which will be taken - into consideration when the date is validated. - - The checksum will be calculated and checked. The birth date is checked - to be a valid date. - - By default, co-ordination numbers (samordningsnummer) will be accepted. To - only allow real personal identity numbers, pass the keyword argument - coordination_number=False to the constructor. - - The cleaned value will always have the format YYYYMMDDXXXX. - -.. class:: se.forms.SEPostalCodeField - - A form field that validates input as a Swedish postal code (postnummer). - Valid codes consist of five digits (XXXXX). The number can optionally be - formatted with a space after the third digit (XXX XX). - - The cleaned value will never contain the space. - -Switzerland (``ch``) -==================== - -.. class:: ch.forms.CHIdentityCardNumberField - - A form field that validates input as a Swiss identity card number. - A valid number must confirm to the X1234567<0 or 1234567890 format and - have the correct checksums. - -.. class:: ch.forms.CHPhoneNumberField - - A form field that validates input as a Swiss phone number. The correct - format is 0XX XXX XX XX. 0XX.XXX.XX.XX and 0XXXXXXXXX validate but are - corrected to 0XX XXX XX XX. - -.. class:: ch.forms.CHZipCodeField - - A form field that validates input as a Swiss zip code. Valid codes - consist of four digits. - -.. class:: ch.forms.CHStateSelect - - A ``Select`` widget that uses a list of Swiss states as its choices. - -Turkey (``tr``) -=============== - -.. class:: tr.forms.TRZipCodeField - - A form field that validates input as a Turkish zip code. Valid codes - consist of five digits. - -.. class:: tr.forms.TRPhoneNumberField - - A form field that validates input as a Turkish phone number. The correct - format is 0xxx xxx xxxx. +90xxx xxx xxxx and inputs without spaces also - validates. The result is normalized to xxx xxx xxxx format. - -.. class:: tr.forms.TRIdentificationNumberField - - A form field that validates input as a TR identification number. A valid - number must satisfy the following: - - * The number consist of 11 digits. - * The first digit cannot be 0. - * (sum(1st, 3rd, 5th, 7th, 9th)*7 - sum(2nd,4th,6th,8th)) % 10) must be - equal to the 10th digit. - * (sum(1st to 10th) % 10) must be equal to the 11th digit. - -.. class:: tr.forms.TRProvinceSelect - - A ``select`` widget that uses a list of Turkish provinces as its choices. - -United Kingdom (``gb``) -======================= - -.. class:: gb.forms.GBPostcodeField - - A form field that validates input as a UK postcode. The regular - expression used is sourced from the schema for British Standard BS7666 - address types at http://www.cabinetoffice.gov.uk/media/291293/bs7666-v2-0.xml. - -.. class:: gb.forms.GBCountySelect - - A ``Select`` widget that uses a list of UK counties/regions as its choices. - -.. class:: gb.forms.GBNationSelect - - A ``Select`` widget that uses a list of UK nations as its choices. - -United States of America (``us``) -================================= - -.. class:: us.forms.USPhoneNumberField - - A form field that validates input as a U.S. phone number. - -.. class:: us.forms.USSocialSecurityNumberField - - A form field that validates input as a U.S. Social Security Number (SSN). - A valid SSN must obey the following rules: - - * Format of XXX-XX-XXXX - * No group of digits consisting entirely of zeroes - * Leading group of digits cannot be 666 - * Number not in promotional block 987-65-4320 through 987-65-4329 - * Number not one known to be invalid due to widespread promotional - use or distribution (e.g., the Woolworth's number or the 1962 - promotional number) - -.. class:: us.forms.USStateField - - A form field that validates input as a U.S. state name or abbreviation. It - normalizes the input to the standard two-letter postal service abbreviation - for the given state. - -.. class:: us.forms.USZipCodeField - - A form field that validates input as a U.S. ZIP code. Valid formats are - XXXXX or XXXXX-XXXX. - -.. class:: us.forms.USStateSelect - - A form ``Select`` widget that uses a list of U.S. states/territories as its - choices. - -.. class:: us.forms.USPSSelect - - A form ``Select`` widget that uses a list of U.S Postal Service - state, territory and country abbreviations as its choices. - -.. class:: us.models.PhoneNumberField - - A :class:`CharField` that checks that the value is a valid U.S.A.-style phone - number (in the format ``XXX-XXX-XXXX``). - -.. class:: us.models.USStateField - - A model field that forms represent as a ``forms.USStateField`` field and - stores the two-letter U.S. state abbreviation in the database. - -.. class:: us.models.USPostalCodeField - - A model field that forms represent as a ``forms.USPSSelect`` field - and stores the two-letter U.S Postal Service abbreviation in the - database. - -Additionally, a variety of choice tuples are provided in -``django.contrib.localflavor.us.us_states``, allowing customized model -and form fields, and form presentations, for subsets of U.S states, -territories and U.S Postal Service abbreviations: - -.. data:: us.us_states.CONTIGUOUS_STATES - - A tuple of choices of the postal abbreviations for the - contiguous or "lower 48" states (i.e., all except Alaska and - Hawaii), plus the District of Columbia. - -.. data:: us.us_states.US_STATES - - A tuple of choices of the postal abbreviations for all - 50 U.S. states, plus the District of Columbia. - -.. data:: us.us_states.US_TERRITORIES - - A tuple of choices of the postal abbreviations for U.S - territories: American Samoa, Guam, the Northern Mariana Islands, - Puerto Rico and the U.S. Virgin Islands. - -.. data:: us.us_states.ARMED_FORCES_STATES - - A tuple of choices of the postal abbreviations of the three U.S - military postal "states": Armed Forces Americas, Armed Forces - Europe and Armed Forces Pacific. - -.. data:: us.us_states.COFA_STATES - - A tuple of choices of the postal abbreviations of the three - independent nations which, under the Compact of Free Association, - are served by the U.S. Postal Service: the Federated States of - Micronesia, the Marshall Islands and Palau. - -.. data:: us.us_states.OBSOLETE_STATES - - A tuple of choices of obsolete U.S Postal Service state - abbreviations: the former abbreviation for the Northern Mariana - Islands, plus the Panama Canal Zone, the Philippines and the - former Pacific trust territories. - -.. data:: us.us_states.STATE_CHOICES - - A tuple of choices of all postal abbreviations corresponding to U.S states or - territories, and the District of Columbia.. - -.. data:: us.us_states.USPS_CHOICES - - A tuple of choices of all postal abbreviations recognized by the - U.S Postal Service (including all states and territories, the - District of Columbia, armed forces "states" and independent - nations serviced by USPS). - -Uruguay (``uy``) -================ - -.. class:: uy.forms.UYCIField - - A field that validates Uruguayan 'Cedula de identidad' (CI) numbers. - -.. class:: uy.forms.UYDepartamentSelect - - A ``Select`` widget that uses a list of Uruguayan departments as its - choices. diff --git a/docs/ref/contrib/sitemaps.txt b/docs/ref/contrib/sitemaps.txt index 2393a4a9a3..ef6c64dc61 100644 --- a/docs/ref/contrib/sitemaps.txt +++ b/docs/ref/contrib/sitemaps.txt @@ -330,8 +330,6 @@ with a caching decorator -- you must name your sitemap view and pass Template customization ====================== -.. versionadded:: 1.3 - If you wish to use a different template for each sitemap or sitemap index available on your site, you may specify it by passing a ``template_name`` parameter to the ``sitemap`` and ``index`` views via the URLconf:: diff --git a/docs/ref/contrib/sites.txt b/docs/ref/contrib/sites.txt index 8fc434ba9b..790e003453 100644 --- a/docs/ref/contrib/sites.txt +++ b/docs/ref/contrib/sites.txt @@ -80,11 +80,11 @@ This accomplishes several things quite nicely: The view code that displays a given story just checks to make sure the requested story is on the current site. It looks something like this:: - from django.conf import settings + from django.contrib.sites.models import get_current_site def article_detail(request, article_id): try: - a = Article.objects.get(id=article_id, sites__id__exact=settings.SITE_ID) + a = Article.objects.get(id=article_id, sites__id__exact=get_current_site(request).id) except Article.DoesNotExist: raise Http404 # ... @@ -131,53 +131,36 @@ For example:: # Do something else. Of course, it's ugly to hard-code the site IDs like that. This sort of -hard-coding is best for hackish fixes that you need done quickly. A slightly +hard-coding is best for hackish fixes that you need done quickly. The cleaner way of accomplishing the same thing is to check the current site's domain:: - from django.conf import settings - from django.contrib.sites.models import Site + from django.contrib.sites.models import get_current_site def my_view(request): - current_site = Site.objects.get(id=settings.SITE_ID) + current_site = get_current_site(request) if current_site.domain == 'foo.com': # Do something else: # Do something else. -The idiom of retrieving the :class:`~django.contrib.sites.models.Site` object -for the value of :setting:`settings.SITE_ID ` is quite common, so -the :class:`~django.contrib.sites.models.Site` model's manager has a -``get_current()`` method. This example is equivalent to the previous one:: +This has also the advantage of checking if the sites framework is installed, and +return a :class:`RequestSite` instance if it is not. + +If you don't have access to the request object, you can use the +``get_current()`` method of the :class:`~django.contrib.sites.models.Site` +model's manager. You should then ensure that your settings file does contain +the :setting:`SITE_ID` setting. This example is equivalent to the previous one:: from django.contrib.sites.models import Site - def my_view(request): + def my_function_without_request(): current_site = Site.objects.get_current() if current_site.domain == 'foo.com': # Do something else: # Do something else. -.. versionchanged:: 1.3 - -For code which relies on getting the current domain but cannot be certain -that the sites framework will be installed for any given project, there is a -utility function :func:`~django.contrib.sites.models.get_current_site` that -takes a request object as an argument and returns either a Site instance (if -the sites framework is installed) or a RequestSite instance (if it is not). -This allows loose coupling with the sites framework and provides a usable -fallback for cases where it is not installed. - -.. versionadded:: 1.3 - -.. function:: get_current_site(request) - - Checks if contrib.sites is installed and returns either the current - :class:`~django.contrib.sites.models.Site` object or a - :class:`~django.contrib.sites.models.RequestSite` object based on - the request. - Getting the current domain for display -------------------------------------- @@ -196,14 +179,14 @@ current site's :attr:`~django.contrib.sites.models.Site.name` and Here's an example of what the form-handling view looks like:: - from django.contrib.sites.models import Site + from django.contrib.sites.models import get_current_site from django.core.mail import send_mail def register_for_newsletter(request): # Check form values, etc., and subscribe the user. # ... - current_site = Site.objects.get_current() + current_site = get_current_site(request) send_mail('Thanks for subscribing to %s alerts' % current_site.name, 'Thanks for your subscription. We appreciate it.\n\n-The %s team.' % current_site.name, 'editor@%s' % current_site.domain, @@ -374,19 +357,19 @@ Here's how Django uses the sites framework: * In the :mod:`redirects framework `, each redirect object is associated with a particular site. When Django searches - for a redirect, it takes into account the current :setting:`SITE_ID`. + for a redirect, it takes into account the current site. * In the comments framework, each comment is associated with a particular site. When a comment is posted, its - :class:`~django.contrib.sites.models.Site` is set to the current - :setting:`SITE_ID`, and when comments are listed via the appropriate - template tag, only the comments for the current site are displayed. + :class:`~django.contrib.sites.models.Site` is set to the current site, + and when comments are listed via the appropriate template tag, only the + comments for the current site are displayed. * In the :mod:`flatpages framework `, each flatpage is associated with a particular site. When a flatpage is created, you specify its :class:`~django.contrib.sites.models.Site`, and the :class:`~django.contrib.flatpages.middleware.FlatpageFallbackMiddleware` - checks the current :setting:`SITE_ID` in retrieving flatpages to display. + checks the current site in retrieving flatpages to display. * In the :mod:`syndication framework `, the templates for ``title`` and ``description`` automatically have access to a @@ -437,7 +420,7 @@ fallback when the database-backed sites framework is not available. Sets the ``name`` and ``domain`` attributes to the value of :meth:`~django.http.HttpRequest.get_host`. - + A :class:`~django.contrib.sites.models.RequestSite` object has a similar interface to a normal :class:`~django.contrib.sites.models.Site` object, except diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt index cbe8ad54b8..3a74797145 100644 --- a/docs/ref/contrib/staticfiles.txt +++ b/docs/ref/contrib/staticfiles.txt @@ -5,8 +5,6 @@ The staticfiles app .. module:: django.contrib.staticfiles :synopsis: An app for handling static files. -.. versionadded:: 1.3 - ``django.contrib.staticfiles`` collects static files from each of your applications (and any other places you specify) into a single location that can easily be served in production. diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt index 5653397748..27b8fc0875 100644 --- a/docs/ref/contrib/syndication.txt +++ b/docs/ref/contrib/syndication.txt @@ -455,7 +455,7 @@ This example illustrates all possible attributes and methods for a author_name = 'Sally Smith' # Hard-coded author name. - # AUTHOR E-MAIL --One of the following three is optional. The framework + # AUTHOR EMAIL --One of the following three is optional. The framework # looks for them in this order. def author_email(self, obj): @@ -635,7 +635,7 @@ This example illustrates all possible attributes and methods for a item_author_name = 'Sally Smith' # Hard-coded author name. - # ITEM AUTHOR E-MAIL --One of the following three is optional. The + # ITEM AUTHOR EMAIL --One of the following three is optional. The # framework looks for them in this order. # # If you specify this, you must specify item_author_name. diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index 3e256e9d9e..3a52f838e7 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -158,6 +158,16 @@ Since MySQL 5.5.5, the default storage engine is InnoDB_. This engine is fully transactional and supports foreign key references. It's probably the best choice at this point. +If you upgrade an existing project to MySQL 5.5.5 and subsequently add some +tables, ensure that your tables are using the same storage engine (i.e. MyISAM +vs. InnoDB). Specifically, if tables that have a ``ForeignKey`` between them +use different storage engines, you may see an error like the following when +running ``syncdb``:: + + _mysql_exceptions.OperationalError: ( + 1005, "Can't create table '\\db_name\\.#sql-4a8_ab' (errno: 150)" + ) + .. versionchanged:: 1.4 In previous versions of Django, fixtures with forward references (i.e. diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 5ff7ecba2c..7fa7539985 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -176,8 +176,6 @@ records to dump. If you're using a :ref:`custom manager ` as the default manager and it filters some of the available records, not all of the objects will be dumped. -.. versionadded:: 1.3 - The :djadminopt:`--all` option may be provided to specify that ``dumpdata`` should use Django's base manager, dumping records which might otherwise be filtered or modified by a custom manager. @@ -195,18 +193,10 @@ easy for humans to read, so you can use the ``--indent`` option to pretty-print the output with a number of indentation spaces. The :djadminopt:`--exclude` option may be provided to prevent specific -applications from being dumped. - -.. versionadded:: 1.3 - -The :djadminopt:`--exclude` option may also be provided to prevent specific -models (specified as in the form of ``appname.ModelName``) from being dumped. - -In addition to specifying application names, you can provide a list of -individual models, in the form of ``appname.Model``. If you specify a model -name to ``dumpdata``, the dumped output will be restricted to that model, -rather than the entire application. You can also mix application names and -model names. +applications or models (specified as in the form of ``appname.ModelName``) from +being dumped. If you specify a model name to ``dumpdata``, the dumped output +will be restricted to that model, rather than the entire application. You can +also mix application names and model names. The :djadminopt:`--database` option can be used to specify the database from which data will be dumped. @@ -299,6 +289,11 @@ Searches for and loads the contents of the named fixture into the database. The :djadminopt:`--database` option can be used to specify the database onto which the data will be loaded. +.. versionadded:: 1.5 + +The :djadminopt:`--ignorenonexistent` option can be used to ignore fields that +may have been removed from models since the fixture was originally generated. + What's a "fixture"? ~~~~~~~~~~~~~~~~~~~ @@ -463,8 +458,6 @@ Use the ``--no-default-ignore`` option to disable the default values of .. django-admin-option:: --no-wrap -.. versionadded:: 1.3 - Use the ``--no-wrap`` option to disable breaking long message lines into several lines in language files. @@ -640,18 +633,14 @@ machines on your network. To make your development server viewable to other machines on the network, use its own IP address (e.g. ``192.168.2.1``) or ``0.0.0.0`` or ``::`` (with IPv6 enabled). -.. versionchanged:: 1.3 - You can provide an IPv6 address surrounded by brackets (e.g. ``[200a::1]:8000``). This will automatically enable IPv6 support. A hostname containing ASCII-only characters can also be used. -.. versionchanged:: 1.3 - If the :doc:`staticfiles` contrib app is enabled (default in new projects) the :djadmin:`runserver` command will be overriden -with an own :djadmin:`runserver` command. +with its own :ref:`runserver` command. .. django-admin-option:: --noreload @@ -674,8 +663,6 @@ development server. .. django-admin-option:: --ipv6, -6 -.. versionadded:: 1.3 - Use the ``--ipv6`` (or shorter ``-6``) option to tell Django to use IPv6 for the development server. This changes the default IP address from ``127.0.0.1`` to ``::1``. @@ -1113,8 +1100,6 @@ To run on 1.2.3.4:7000 with a ``test`` fixture:: django-admin.py testserver --addrport 1.2.3.4:7000 test -.. versionadded:: 1.3 - The :djadminopt:`--noinput` option may be provided to suppress all user prompts. diff --git a/docs/ref/files/storage.txt b/docs/ref/files/storage.txt index b3f8909847..f9bcf9b61e 100644 --- a/docs/ref/files/storage.txt +++ b/docs/ref/files/storage.txt @@ -18,7 +18,7 @@ Django provides two convenient ways to access the current storage class: .. function:: get_storage_class([import_path=None]) Returns a class or module which implements the storage API. - + When called without the ``import_path`` parameter ``get_storage_class`` will return the current default storage system as defined by :setting:`DEFAULT_FILE_STORAGE`. If ``import_path`` is provided, @@ -35,9 +35,9 @@ The FileSystemStorage Class basic file storage on a local filesystem. It inherits from :class:`~django.core.files.storage.Storage` and provides implementations for all the public methods thereof. - + .. note:: - + The :class:`FileSystemStorage.delete` method will not raise raise an exception if the given file name does not exist. @@ -53,16 +53,12 @@ The Storage Class .. method:: accessed_time(name) - .. versionadded:: 1.3 - Returns a ``datetime`` object containing the last accessed time of the file. For storage systems that aren't able to return the last accessed time this will raise ``NotImplementedError`` instead. .. method:: created_time(name) - .. versionadded:: 1.3 - Returns a ``datetime`` object containing the creation time of the file. For storage systems that aren't able to return the creation time this will raise ``NotImplementedError`` instead. @@ -100,8 +96,6 @@ The Storage Class .. method:: modified_time(name) - .. versionadded:: 1.3 - Returns a ``datetime`` object containing the last modified time. For storage systems that aren't able to return the last modified time, this will raise ``NotImplementedError`` instead. diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 777d73e015..dffef314b7 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -105,7 +105,7 @@ Access the :attr:`~Form.errors` attribute to get a dictionary of error messages:: >>> f.errors - {'sender': [u'Enter a valid e-mail address.'], 'subject': [u'This field is required.']} + {'sender': [u'Enter a valid email address.'], 'subject': [u'This field is required.']} In this dictionary, the keys are the field names, and the values are lists of Unicode strings representing the error messages. The error messages are stored @@ -538,18 +538,18 @@ method you're using:: >>> print(f.as_table()) Subject:
    • This field is required.
    Message: - Sender:
    • Enter a valid e-mail address.
    + Sender:
    • Enter a valid email address.
    Cc myself: >>> print(f.as_ul())
    • This field is required.
    Subject:
  • Message:
  • -
    • Enter a valid e-mail address.
    Sender:
  • +
    • Enter a valid email address.
    Sender:
  • Cc myself:
  • >>> print(f.as_p())

    • This field is required.

    Subject:

    Message:

    -

    • Enter a valid e-mail address.

    +

    • Enter a valid email address.

    Sender:

    Cc myself:

    @@ -572,7 +572,7 @@ pass that in at construction time::
    This field is required.

    Subject:

    Message:

    -
    Enter a valid e-mail address.
    +
    Enter a valid email address.

    Sender:

    Cc myself:

    @@ -658,8 +658,6 @@ those classes as an argument:: .. method:: BoundField.value() - .. versionadded:: 1.3 - Use this method to render the raw value of this field as it would be rendered by a ``Widget``:: diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 7c06bf97ee..7c8d509031 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -28,7 +28,7 @@ exception or returns the clean value:: >>> f.clean('invalid email address') Traceback (most recent call last): ... - ValidationError: [u'Enter a valid e-mail address.'] + ValidationError: [u'Enter a valid email address.'] Core field arguments -------------------- @@ -405,7 +405,7 @@ For each field, we describe the default widget used if you don't specify Additionally, if you specify :setting:`USE_L10N=False` in your settings, the following will also be included in the default input formats:: - '%b %m %d', # 'Oct 25 2006' + '%b %d %Y', # 'Oct 25 2006' '%b %d, %Y', # 'Oct 25, 2006' '%d %b %Y', # '25 Oct 2006' '%d %b, %Y', # '25 Oct, 2006' @@ -414,6 +414,8 @@ For each field, we describe the default widget used if you don't specify '%d %B %Y', # '25 October 2006' '%d %B, %Y', # '25 October, 2006' + See also :ref:`format localization `. + ``DateTimeField`` ~~~~~~~~~~~~~~~~~ @@ -445,6 +447,8 @@ For each field, we describe the default widget used if you don't specify '%m/%d/%y %H:%M', # '10/25/06 14:30' '%m/%d/%y', # '10/25/06' + See also :ref:`format localization `. + ``DecimalField`` ~~~~~~~~~~~~~~~~ @@ -704,8 +708,6 @@ For each field, we describe the default widget used if you don't specify ``TypedMultipleChoiceField`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 1.3 - .. class:: TypedMultipleChoiceField(**kwargs) Just like a :class:`MultipleChoiceField`, except :class:`TypedMultipleChoiceField` @@ -999,13 +1001,17 @@ objects (in the case of ``ModelMultipleChoiceField``) into the .. class:: ModelMultipleChoiceField(**kwargs) * Default widget: ``SelectMultiple`` - * Empty value: ``[]`` (an empty list) - * Normalizes to: A list of model instances. + * Empty value: An empty ``QuerySet`` (self.queryset.none()) + * Normalizes to: A ``QuerySet`` of model instances. * Validates that every id in the given list of values exists in the queryset. * Error message keys: ``required``, ``list``, ``invalid_choice``, ``invalid_pk_value`` + .. versionchanged:: 1.5 + The empty and normalized values were changed to be consistently + ``QuerySets`` instead of ``[]`` and ``QuerySet`` respectively. + Allows the selection of one or more model objects, suitable for representing a many-to-many relation. As with :class:`ModelChoiceField`, you can use ``label_from_instance`` to customize the object diff --git a/docs/ref/forms/validation.txt b/docs/ref/forms/validation.txt index 1af32da875..e89bce748f 100644 --- a/docs/ref/forms/validation.txt +++ b/docs/ref/forms/validation.txt @@ -185,7 +185,7 @@ a look at Django's ``EmailField``:: class EmailField(CharField): default_error_messages = { - 'invalid': _('Enter a valid e-mail address.'), + 'invalid': _('Enter a valid email address.'), } default_validators = [validators.validate_email] @@ -198,7 +198,7 @@ on field definition so:: is equivalent to:: email = forms.CharField(validators=[validators.validate_email], - error_messages={'invalid': _('Enter a valid e-mail address.')}) + error_messages={'invalid': _('Enter a valid email address.')}) Form field default cleaning diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt index 4724cbdec2..3c458930fa 100644 --- a/docs/ref/forms/widgets.txt +++ b/docs/ref/forms/widgets.txt @@ -294,11 +294,6 @@ These widgets make use of the HTML elements ``input`` and ``textarea``. Determines whether the widget will have a value filled in when the form is re-displayed after a validation error (default is ``False``). - .. versionchanged:: 1.3 - The default value for - :attr:`~PasswordInput.render_value` was - changed from ``True`` to ``False`` - ``HiddenInput`` ~~~~~~~~~~~~~~~ @@ -532,8 +527,6 @@ File upload widgets .. class:: ClearableFileInput - .. versionadded:: 1.3 - File upload input: ````, with an additional checkbox input to clear the field's value, if the field is not required and has initial data. diff --git a/docs/ref/index.txt b/docs/ref/index.txt index 01a8ab22d1..e1959d44a6 100644 --- a/docs/ref/index.txt +++ b/docs/ref/index.txt @@ -6,7 +6,7 @@ API Reference :maxdepth: 1 authbackends - class-based-views/index + class-based-views/index clickjacking contrib/index databases @@ -22,5 +22,7 @@ API Reference signals templates/index unicode + urlresolvers + urls utils validators diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index a6ea9a6c41..b542aee6e2 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -93,8 +93,8 @@ GZip middleware Compresses content for browsers that understand GZip compression (all modern browsers). -It is suggested to place this first in the middleware list, so that the -compression of the response content is the last thing that happens. +This middleware should be placed before any other middleware that need to +read or write the response body so that compression happens afterward. It will NOT compress content if any of the following are true: @@ -203,9 +203,9 @@ Transaction middleware .. class:: TransactionMiddleware -Binds commit and rollback to the request/response phase. If a view function -runs successfully, a commit is done. If it fails with an exception, a rollback -is done. +Binds commit and rollback of the default database to the request/response +phase. If a view function runs successfully, a commit is done. If it fails with +an exception, a rollback is done. The order of this middleware in the stack is important: middleware modules running outside of it run with commit-on-save - the default Django behavior. diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 8b3c31f029..809d56eaf5 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -668,6 +668,11 @@ the field. Note: This method will close the file if it happens to be open when The optional ``save`` argument controls whether or not the instance is saved after the file has been deleted. Defaults to ``True``. +Note that when a model is deleted, related files are not deleted. If you need +to cleanup orphaned files, you'll need to handle it yourself (for instance, +with a custom management command that can be run manually or scheduled to run +periodically via e.g. cron). + ``FilePathField`` ----------------- @@ -966,6 +971,12 @@ need to use:: This sort of reference can be useful when resolving circular import dependencies between two applications. +A database index is automatically created on the ``ForeignKey``. You can +disable this by setting :attr:`~Field.db_index` to ``False``. You may want to +avoid the overhead of an index if you are creating a foreign key for +consistency rather than joins, or if you will be creating an alternative index +like a partial or multiple column index. + Database Representation ~~~~~~~~~~~~~~~~~~~~~~~ @@ -1023,8 +1034,6 @@ define the details of how the relation works. The field on the related object that the relation is to. By default, Django uses the primary key of the related object. -.. versionadded:: 1.3 - .. attribute:: ForeignKey.on_delete When an object referenced by a :class:`ForeignKey` is deleted, Django by @@ -1081,6 +1090,9 @@ the model is related. This works exactly the same as it does for :class:`ForeignKey`, including all the options regarding :ref:`recursive ` and :ref:`lazy ` relationships. +Related objects can be added, removed, or created with the field's +:class:`~django.db.models.fields.related.RelatedManager`. + Database Representation ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index 2fdc87df8c..1ba41148b0 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -387,10 +387,11 @@ perform an update on all fields. Specifying ``update_fields`` will force an update. When saving a model fetched through deferred model loading -(:meth:`~Model.only()` or :meth:`~Model.defer()`) only the fields loaded from -the DB will get updated. In effect there is an automatic ``update_fields`` in -this case. If you assign or change any deferred field value, these fields will -be added to the updated fields. +(:meth:`~django.db.models.query.QuerySet.only()` or +:meth:`~django.db.models.query.QuerySet.defer()`) only the fields loaded +from the DB will get updated. In effect there is an automatic +``update_fields`` in this case. If you assign or change any deferred field +value, the field will be added to the updated fields. Deleting objects ================ @@ -493,12 +494,16 @@ defined. If it makes sense for your model's instances to each have a unique URL, you should define ``get_absolute_url()``. It's good practice to use ``get_absolute_url()`` in templates, instead of -hard-coding your objects' URLs. For example, this template code is bad:: +hard-coding your objects' URLs. For example, this template code is bad: + +.. code-block:: html+django {{ object.name }} -This template code is much better:: +This template code is much better: + +.. code-block:: html+django {{ object.name }} @@ -534,7 +539,9 @@ pattern name) and a list of position or keyword arguments and uses the URLconf patterns to construct the correct, full URL. It returns a string for the correct URL, with all parameters substituted in the correct positions. -The ``permalink`` decorator is a Python-level equivalent to the :ttag:`url` template tag and a high-level wrapper for the :func:`django.core.urlresolvers.reverse()` function. +The ``permalink`` decorator is a Python-level equivalent to the :ttag:`url` +template tag and a high-level wrapper for the +:func:`django.core.urlresolvers.reverse()` function. An example should make it clear how to use ``permalink()``. Suppose your URLconf contains a line such as:: diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 8ec7cfc791..7138cd0e74 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -505,15 +505,8 @@ followed (optionally) by any output-affecting methods (such as ``values()``), but it doesn't really matter. This is your chance to really flaunt your individualism. -.. versionchanged:: 1.3 - -The ``values()`` method previously did not return anything for -:class:`~django.db.models.ManyToManyField` attributes and would raise an error -if you tried to pass this type of field to it. - -This restriction has been lifted, and you can now also refer to fields on -related models with reverse relations through ``OneToOneField``, ``ForeignKey`` -and ``ManyToManyField`` attributes:: +You can also refer to fields on related models with reverse relations through +``OneToOneField``, ``ForeignKey`` and ``ManyToManyField`` attributes:: Blog.objects.values('name', 'entry__headline') [{'name': 'My blog', 'entry__headline': 'An entry'}, @@ -1463,6 +1456,16 @@ evaluated will force it to evaluate again, repeating the query. Also, use of ``iterator()`` causes previous ``prefetch_related()`` calls to be ignored since these two optimizations do not make sense together. +.. warning:: + + Some Python database drivers like ``psycopg2`` perform caching if using + client side cursors (instantiated with ``connection.cursor()`` and what + Django's ORM uses). Using ``iterator()`` does not affect caching at the + database driver level. To disable this caching, look at `server side + cursors`_. + +.. _server side cursors: http://initd.org/psycopg/docs/usage.html#server-side-cursors + latest ~~~~~~ @@ -1571,7 +1574,8 @@ update .. method:: update(**kwargs) Performs an SQL update query for the specified fields, and returns -the number of rows affected. +the number of rows matched (which may not be equal to the number of rows +updated if some rows already have the new value). For example, to turn comments off for all blog entries published in 2010, you could do this:: @@ -1631,7 +1635,7 @@ does not call any ``save()`` methods on your models, nor does it emit the :attr:`~django.db.models.signals.post_save` signals (which are a consequence of calling :meth:`Model.save() <~django.db.models.Model.save()>`). If you want to update a bunch of records for a model that has a custom -:meth:`~django.db.models.Model.save()`` method, loop over them and call +:meth:`~django.db.models.Model.save()` method, loop over them and call :meth:`~django.db.models.Model.save()`, like this:: for e in Entry.objects.filter(pub_date__year=2010): @@ -1664,10 +1668,9 @@ For example:: # This will delete all Blogs and all of their Entry objects. blogs.delete() -.. versionadded:: 1.3 - This cascade behavior is customizable via the - :attr:`~django.db.models.ForeignKey.on_delete` argument to the - :class:`~django.db.models.ForeignKey`. +This cascade behavior is customizable via the +:attr:`~django.db.models.ForeignKey.on_delete` argument to the +:class:`~django.db.models.ForeignKey`. The ``delete()`` method does a bulk delete and does not call any ``delete()`` methods on your models. It does, however, emit the @@ -1675,6 +1678,21 @@ methods on your models. It does, however, emit the :data:`~django.db.models.signals.post_delete` signals for all deleted objects (including cascaded deletions). +.. versionadded:: 1.5 + Allow fast-path deletion of objects + +Django needs to fetch objects into memory to send signals and handle cascades. +However, if there are no cascades and no signals, then Django may take a +fast-path and delete objects without fetching into memory. For large +deletes this can result in significantly reduced memory usage. The amount of +executed queries can be reduced, too. + +ForeignKeys which are set to :attr:`~django.db.models.ForeignKey.on_delete` +DO_NOTHING do not prevent taking the fast-path in deletion. + +Note that the queries generated in object deletion is an implementation +detail subject to change. + .. _field-lookups: Field lookups diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 21e99de10d..c3ba99168d 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -16,7 +16,8 @@ passing the :class:`HttpRequest` as the first argument to the view function. Each view is responsible for returning an :class:`HttpResponse` object. This document explains the APIs for :class:`HttpRequest` and -:class:`HttpResponse` objects. +:class:`HttpResponse` objects, which are defined in the :mod:`django.http` +module. HttpRequest objects =================== @@ -42,8 +43,6 @@ All attributes should be considered read-only, unless stated otherwise below. data in different ways than conventional HTML forms: binary images, XML payload etc. For processing conventional form data, use ``HttpRequest.POST``. - .. versionadded:: 1.3 - You can also read from an HttpRequest using a file-like interface. See :meth:`HttpRequest.read()`. @@ -93,8 +92,14 @@ All attributes should be considered read-only, unless stated otherwise below. .. attribute:: HttpRequest.POST - A dictionary-like object containing all given HTTP POST parameters. See the - :class:`QueryDict` documentation below. + A dictionary-like object containing all given HTTP POST parameters, + providing that the request contains form data. See the + :class:`QueryDict` documentation below. If you need to access raw or + non-form data posted in the request, access this through the + :attr:`HttpRequest.body` attribute instead. + + .. versionchanged:: 1.5 + Before Django 1.5, HttpRequest.POST contained non-form data. It's possible that a request can come in via POST with an empty ``POST`` dictionary -- if, say, a form is requested via the POST HTTP method but @@ -192,6 +197,17 @@ All attributes should be considered read-only, unless stated otherwise below. URLconf for the current request, overriding the :setting:`ROOT_URLCONF` setting. See :ref:`how-django-processes-a-request` for details. +.. attribute:: HttpRequest.resolver_match + + .. versionadded:: 1.5 + + An instance of :class:`~django.core.urlresolvers.ResolverMatch` representing + the resolved url. This attribute is only set after url resolving took place, + which means it's available in all views but not in middleware methods which + are executed before url resolving takes place (like ``process_request``, you + can use ``process_view`` instead). + + Methods ------- @@ -305,8 +321,6 @@ Methods .. method:: HttpRequest.xreadlines() .. method:: HttpRequest.__iter__() - .. versionadded:: 1.3 - Methods implementing a file-like interface for reading from an HttpRequest instance. This makes it possible to consume an incoming request in a streaming fashion. A common use-case would be to process a @@ -509,9 +523,6 @@ In addition, ``QueryDict`` has the following methods: >>> q.urlencode() 'a=2&b=3&b=5' - .. versionchanged:: 1.3 - The ``safe`` parameter was added. - Optionally, urlencode can be passed characters which do not require encoding. For example:: @@ -555,13 +566,28 @@ file-like object:: Passing iterators ~~~~~~~~~~~~~~~~~ -Finally, you can pass ``HttpResponse`` an iterator rather than passing it -hard-coded strings. If you use this technique, follow these guidelines: +Finally, you can pass ``HttpResponse`` an iterator rather than strings. If you +use this technique, the iterator should return strings. -* The iterator should return strings. -* If an :class:`HttpResponse` has been initialized with an iterator as its - content, you can't use the :class:`HttpResponse` instance as a file-like - object. Doing so will raise ``Exception``. +Passing an iterator as content to :class:`HttpResponse` creates a +streaming response if (and only if) no middleware accesses the +:attr:`HttpResponse.content` attribute before the response is returned. + +.. versionchanged:: 1.5 + +This technique is fragile and was deprecated in Django 1.5. If you need the +response to be streamed from the iterator to the client, you should use the +:class:`StreamingHttpResponse` class instead. + +As of Django 1.7, when :class:`HttpResponse` is instantiated with an +iterator, it will consume it immediately, store the response content as a +string, and discard the iterator. + +.. versionchanged:: 1.5 + +You can now use :class:`HttpResponse` as a file-like object even if it was +instantiated with an iterator. Django will consume and save the content of +the iterator on first access. Setting headers ~~~~~~~~~~~~~~~ @@ -586,7 +612,7 @@ To tell the browser to treat the response as a file attachment, use the this is how you might return a Microsoft Excel spreadsheet:: >>> response = HttpResponse(my_data, content_type='application/vnd.ms-excel') - >>> response['Content-Disposition'] = 'attachment; filename=foo.xls' + >>> response['Content-Disposition'] = 'attachment; filename="foo.xls"' There's nothing Django-specific about the ``Content-Disposition`` header, but it's easy to forget the syntax, so we've included it here. @@ -603,6 +629,13 @@ Attributes The `HTTP Status code`_ for the response. +.. attribute:: HttpResponse.streaming + + This is always ``False``. + + This attribute exists so middleware can treat streaming responses + differently from regular responses. + Methods ------- @@ -646,17 +679,7 @@ Methods Returns ``True`` or ``False`` based on a case-insensitive check for a header with the given name. -.. method:: HttpResponse.set_cookie(key, value='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=True) - - .. versionchanged:: 1.3 - - The possibility of specifying a ``datetime.datetime`` object in - ``expires``, and the auto-calculation of ``max_age`` in such case - was added. The ``httponly`` argument was also added. - - .. versionchanged:: 1.4 - - The default value for httponly was changed from ``False`` to ``True``. +.. method:: HttpResponse.set_cookie(key, value='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=False) Sets a cookie. The parameters are the same as in the :class:`Cookie.Morsel` object in the Python standard library. @@ -780,3 +803,60 @@ types of HTTP responses. Like ``HttpResponse``, these subclasses live in method, Django will treat it as emulating a :class:`~django.template.response.SimpleTemplateResponse`, and the ``render`` method must itself return a valid response object. + +StreamingHttpResponse objects +============================= + +.. versionadded:: 1.5 + +.. class:: StreamingHttpResponse + +The :class:`StreamingHttpResponse` class is used to stream a response from +Django to the browser. You might want to do this if generating the response +takes too long or uses too much memory. For instance, it's useful for +generating large CSV files. + +.. admonition:: Performance considerations + + Django is designed for short-lived requests. Streaming responses will tie + a worker process and keep a database connection idle in transaction for + the entire duration of the response. This may result in poor performance. + + Generally speaking, you should perform expensive tasks outside of the + request-response cycle, rather than resorting to a streamed response. + +The :class:`StreamingHttpResponse` is not a subclass of :class:`HttpResponse`, +because it features a slightly different API. However, it is almost identical, +with the following notable differences: + +* It should be given an iterator that yields strings as content. + +* You cannot access its content, except by iterating the response object + itself. This should only occur when the response is returned to the client. + +* It has no ``content`` attribute. Instead, it has a + :attr:`~StreamingHttpResponse.streaming_content` attribute. + +* You cannot use the file-like object ``tell()`` or ``write()`` methods. + Doing so will raise an exception. + +:class:`StreamingHttpResponse` should only be used in situations where it is +absolutely required that the whole content isn't iterated before transferring +the data to the client. Because the content can't be accessed, many +middlewares can't function normally. For example the ``ETag`` and ``Content- +Length`` headers can't be generated for streaming responses. + +Attributes +---------- + +.. attribute:: StreamingHttpResponse.streaming_content + + An iterator of strings representing the content. + +.. attribute:: HttpResponse.status_code + + The `HTTP Status code`_ for the response. + +.. attribute:: HttpResponse.streaming + + This is always ``True``. diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 16d067172d..a909c12665 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -110,23 +110,20 @@ A tuple of authentication backend classes (as strings) to use when attempting to authenticate a user. See the :doc:`authentication backends documentation ` for details. -.. setting:: AUTH_PROFILE_MODULE +.. setting:: AUTH_USER_MODEL -AUTH_PROFILE_MODULE -------------------- +AUTH_USER_MODEL +--------------- -Default: Not defined +Default: 'auth.User' -The site-specific user profile model used by this site. See -:ref:`auth-profiles`. +The model to use to represent a User. See :ref:`auth-custom-user`. .. setting:: CACHES CACHES ------ -.. versionadded:: 1.3 - Default:: { @@ -167,12 +164,6 @@ backend class (i.e. ``mypackage.backends.whatever.WhateverCache``). Writing a whole new cache backend from scratch is left as an exercise to the reader; see the other backends for examples. -.. note:: - Prior to Django 1.3, you could use a URI based version of the backend - name to reference the built-in cache backends (e.g., you could use - ``'db://tablename'`` to refer to the database backend). This format has - been deprecated, and will be removed in Django 1.5. - .. setting:: CACHES-KEY_FUNCTION KEY_FUNCTION @@ -534,8 +525,6 @@ Only supported for the ``mysql`` backend (see the `MySQL manual`_ for details). TEST_DEPENDENCIES ~~~~~~~~~~~~~~~~~ -.. versionadded:: 1.3 - Default: ``['default']``, for all databases other than ``default``, which has no dependencies. @@ -1262,8 +1251,6 @@ the ``locale`` directory (i.e. ``'/path/to/locale'``). LOGGING ------- -.. versionadded:: 1.3 - Default: A logging configuration dictionary. A data structure containing configuration information. The contents of @@ -1278,8 +1265,6 @@ email log handler; all other log messages are given to a NullHandler. LOGGING_CONFIG -------------- -.. versionadded:: 1.3 - Default: ``'django.utils.log.dictConfig'`` A path to a callable that will be used to configure logging in the @@ -1371,13 +1356,11 @@ MEDIA_URL Default: ``''`` (Empty string) URL that handles the media served from :setting:`MEDIA_ROOT`, used -for :doc:`managing stored files `. +for :doc:`managing stored files `. It must end in a slash if set +to a non-empty value. Example: ``"http://media.example.com/"`` -.. versionchanged:: 1.3 - It must end in a slash if set to a non-empty value. - MESSAGE_LEVEL ------------- @@ -1896,10 +1879,6 @@ A tuple of callables that are used to populate the context in ``RequestContext`` These callables take a request object as their argument and return a dictionary of items to be merged into the context. -.. versionadded:: 1.3 - The ``django.core.context_processors.static`` context processor - was added in this release. - .. versionadded:: 1.4 The ``django.core.context_processors.tz`` context processor was added in this release. @@ -2160,8 +2139,6 @@ See also :setting:`TIME_ZONE`, :setting:`USE_I18N` and :setting:`USE_L10N`. USE_X_FORWARDED_HOST -------------------- -.. versionadded:: 1.3.1 - Default: ``False`` A boolean that specifies whether to use the X-Forwarded-Host header in @@ -2231,6 +2208,22 @@ ADMIN_MEDIA_PREFIX integration. See the :doc:`Django 1.4 release notes` for more information. +.. setting:: AUTH_PROFILE_MODULE + +AUTH_PROFILE_MODULE +------------------- + +.. deprecated:: 1.5 + With the introduction of :ref:`custom User models `, + the use of :setting:`AUTH_PROFILE_MODULE` to define a single profile + model is no longer supported. See the + :doc:`Django 1.5 release notes` for more information. + +Default: Not defined + +The site-specific user profile model used by this site. See +:ref:`auth-profiles`. + .. setting:: IGNORABLE_404_ENDS IGNORABLE_404_ENDS diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index b2f2e85abc..0db540370d 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -46,7 +46,7 @@ pre_init .. ^^^^^^^ this :module: hack keeps Sphinx from prepending the module. -Whenever you instantiate a Django model,, this signal is sent at the beginning +Whenever you instantiate a Django model, this signal is sent at the beginning of the model's :meth:`~django.db.models.Model.__init__` method. Arguments sent with this signal: @@ -118,8 +118,6 @@ Arguments sent with this signal: records in the database as the database might not be in a consistent state yet. -.. versionadded:: 1.3 - ``using`` The database alias being used. @@ -155,8 +153,6 @@ Arguments sent with this signal: records in the database as the database might not be in a consistent state yet. -.. versionadded:: 1.3 - ``using`` The database alias being used. @@ -183,8 +179,6 @@ Arguments sent with this signal: ``instance`` The actual instance being deleted. -.. versionadded:: 1.3 - ``using`` The database alias being used. @@ -209,8 +203,6 @@ Arguments sent with this signal: Note that the object will no longer be in the database, so be very careful what you do with this instance. -.. versionadded:: 1.3 - ``using`` The database alias being used. @@ -271,8 +263,6 @@ Arguments sent with this signal: For the ``pre_clear`` and ``post_clear`` actions, this is ``None``. -.. versionadded:: 1.3 - ``using`` The database alias being used. @@ -287,13 +277,22 @@ like this:: # ... toppings = models.ManyToManyField(Topping) -If we would do something like this: +If we connected a handler like this:: + + def toppings_changed(sender, **kwargs): + # Do something + pass + + m2m_changed.connect(toppings_changed, sender=Pizza.toppings.through) + +and then did something like this:: >>> p = Pizza.object.create(...) >>> t = Topping.objects.create(...) >>> p.toppings.add(t) -the arguments sent to a :data:`m2m_changed` handler would be: +the arguments sent to a :data:`m2m_changed` handler (``topppings_changed`` in +the example above) would be: ============== ============================================================ Argument Value diff --git a/docs/ref/template-response.txt b/docs/ref/template-response.txt index 9e09077adc..d9b7130362 100644 --- a/docs/ref/template-response.txt +++ b/docs/ref/template-response.txt @@ -2,8 +2,6 @@ TemplateResponse and SimpleTemplateResponse =========================================== -.. versionadded:: 1.3 - .. module:: django.template.response :synopsis: Classes dealing with lazy-rendered HTTP responses. diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt index 48bd346788..db57d2de96 100644 --- a/docs/ref/templates/api.txt +++ b/docs/ref/templates/api.txt @@ -160,11 +160,6 @@ it. Example:: >>> t.render(Context({"person": PersonClass2})) "My name is Samantha." -.. versionchanged:: 1.3 - Previously, only variables that originated with an attribute lookup would - be called by the template system. This change was made for consistency - across lookup types. - Callable variables are slightly more complex than variables which only require straight lookups. Here are some things to keep in mind: @@ -438,7 +433,7 @@ django.contrib.auth.context_processors.auth ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If :setting:`TEMPLATE_CONTEXT_PROCESSORS` contains this processor, every -``RequestContext`` will contain these three variables: +``RequestContext`` will contain these variables: * ``user`` -- An ``auth.User`` instance representing the currently logged-in user (or an ``AnonymousUser`` instance, if the client isn't @@ -448,11 +443,6 @@ If :setting:`TEMPLATE_CONTEXT_PROCESSORS` contains this processor, every ``django.contrib.auth.context_processors.PermWrapper``, representing the permissions that the currently logged-in user has. -.. versionchanged:: 1.3 - Prior to version 1.3, ``PermWrapper`` was located in - ``django.contrib.auth.context_processors``. - - django.core.context_processors.debug ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -491,8 +481,6 @@ django.core.context_processors.static .. function:: django.core.context_processors.static -.. versionadded:: 1.3 - If :setting:`TEMPLATE_CONTEXT_PROCESSORS` contains this processor, every ``RequestContext`` will contain a variable ``STATIC_URL``, providing the value of the :setting:`STATIC_URL` setting. diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 072eebf69f..3b8d058fb4 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -156,8 +156,6 @@ In this syntax, each value gets interpreted as a literal string, and there's no way to specify variable values. Or literal commas. Or spaces. Did we mention you shouldn't use this syntax in any new projects? -.. versionadded:: 1.3 - By default, when you use the ``as`` keyword with the cycle tag, the usage of ``{% cycle %}`` that declares the cycle will itself output the first value in the cycle. This could be a problem if you want to @@ -676,9 +674,6 @@ including it. This example produces the output ``"Hello, John"``: {{ greeting }}, {{ person|default:"friend" }}! -.. versionchanged:: 1.3 - Additional context and exclusive context. - You can pass additional context to the template using keyword arguments:: {% include "name_snippet.html" with person="Jane" greeting="Hello" %} @@ -710,8 +705,6 @@ registered in ``somelibrary`` and ``otherlibrary`` located in package {% load somelibrary package.otherlibrary %} -.. versionchanged:: 1.3 - You can also selectively load individual filters or tags from a library, using the ``from`` argument. In this example, the template tags/filters named ``foo`` and ``bar`` will be loaded from ``somelibrary``:: @@ -1004,7 +997,7 @@ refer to the name of the pattern in the ``url`` tag instead of using the path to the view. Note that if the URL you're reversing doesn't exist, you'll get an -:exc:`^django.core.urlresolvers.NoReverseMatch` exception raised, which will +:exc:`~django.core.urlresolvers.NoReverseMatch` exception raised, which will cause your site to display an error page. If you'd like to retrieve a URL without displaying it, you can use a slightly @@ -1076,9 +1069,6 @@ which is rounded up to 88). with ^^^^ -.. versionchanged:: 1.3 - New keyword argument format and multiple variable assignments. - Caches a complex variable under a simpler name. This is useful when accessing an "expensive" method (e.g., one that hits the database) multiple times. @@ -1261,7 +1251,7 @@ S English ordinal suffix for day of the ``'st'``, ``'nd'``, month, 2 characters. t Number of days in the given month. ``28`` to ``31`` T Time zone of this machine. ``'EST'``, ``'MDT'`` -u Microseconds. ``0`` to ``999999`` +u Microseconds. ``000000`` to ``999999`` U Seconds since the Unix Epoch (January 1 1970 00:00:00 UTC). w Day of the week, digits without ``'0'`` (Sunday) to ``'6'`` (Saturday) @@ -2126,8 +2116,6 @@ For example:: If ``value`` is ``"http://www.example.org/foo?a=b&c=d"``, the output will be ``"http%3A//www.example.org/foo%3Fa%3Db%26c%3Dd"``. -.. versionadded:: 1.3 - An optional argument containing the characters which should not be escaped can be provided. diff --git a/docs/ref/urlresolvers.txt b/docs/ref/urlresolvers.txt new file mode 100644 index 0000000000..1bb33c7ca1 --- /dev/null +++ b/docs/ref/urlresolvers.txt @@ -0,0 +1,202 @@ +============================================== +``django.core.urlresolvers`` utility functions +============================================== + +.. module:: django.core.urlresolvers + +reverse() +--------- + +If you need to use something similar to the :ttag:`url` template tag in +your code, Django provides the following function: + +.. function:: reverse(viewname, [urlconf=None, args=None, kwargs=None, current_app=None]) + +``viewname`` is either the function name (either a function reference, or the +string version of the name, if you used that form in ``urlpatterns``) or the +:ref:`URL pattern name `. Normally, you won't need to +worry about the ``urlconf`` parameter and will only pass in the positional and +keyword arguments to use in the URL matching. For example:: + + from django.core.urlresolvers import reverse + + def myview(request): + return HttpResponseRedirect(reverse('arch-summary', args=[1945])) + +The ``reverse()`` function can reverse a large variety of regular expression +patterns for URLs, but not every possible one. The main restriction at the +moment is that the pattern cannot contain alternative choices using the +vertical bar (``"|"``) character. You can quite happily use such patterns for +matching against incoming URLs and sending them off to views, but you cannot +reverse such patterns. + +The ``current_app`` argument allows you to provide a hint to the resolver +indicating the application to which the currently executing view belongs. +This ``current_app`` argument is used as a hint to resolve application +namespaces into URLs on specific application instances, according to the +:ref:`namespaced URL resolution strategy `. + +You can use ``kwargs`` instead of ``args``. For example:: + + >>> reverse('admin:app_list', kwargs={'app_label': 'auth'}) + '/admin/auth/' + +``args`` and ``kwargs`` cannot be passed to ``reverse()`` at the same time. + +.. admonition:: Make sure your views are all correct. + + As part of working out which URL names map to which patterns, the + ``reverse()`` function has to import all of your URLconf files and examine + the name of each view. This involves importing each view function. If + there are *any* errors whilst importing any of your view functions, it + will cause ``reverse()`` to raise an error, even if that view function is + not the one you are trying to reverse. + + Make sure that any views you reference in your URLconf files exist and can + be imported correctly. Do not include lines that reference views you + haven't written yet, because those views will not be importable. + +.. note:: + + The string returned by ``reverse()`` is already + :ref:`urlquoted `. For example:: + + >>> reverse('cities', args=[u'Orléans']) + '.../Orl%C3%A9ans/' + + Applying further encoding (such as :meth:`~django.utils.http.urlquote` or + ``urllib.quote``) to the output of ``reverse()`` may produce undesirable + results. + +reverse_lazy() +-------------- + +.. versionadded:: 1.4 + +A lazily evaluated version of `reverse()`_. + +.. function:: reverse_lazy(viewname, [urlconf=None, args=None, kwargs=None, current_app=None]) + +It is useful for when you need to use a URL reversal before your project's +URLConf is loaded. Some common cases where this function is necessary are: + +* providing a reversed URL as the ``url`` attribute of a generic class-based + view. + +* providing a reversed URL to a decorator (such as the ``login_url`` argument + for the :func:`django.contrib.auth.decorators.permission_required` + decorator). + +* providing a reversed URL as a default value for a parameter in a function's + signature. + +resolve() +--------- + +The ``resolve()`` function can be used for resolving URL paths to the +corresponding view functions. It has the following signature: + +.. function:: resolve(path, urlconf=None) + +``path`` is the URL path you want to resolve. As with +:func:`~django.core.urlresolvers.reverse`, you don't need to +worry about the ``urlconf`` parameter. The function returns a +:class:`ResolverMatch` object that allows you +to access various meta-data about the resolved URL. + +If the URL does not resolve, the function raises an +:class:`~django.http.Http404` exception. + +.. class:: ResolverMatch + + .. attribute:: ResolverMatch.func + + The view function that would be used to serve the URL + + .. attribute:: ResolverMatch.args + + The arguments that would be passed to the view function, as + parsed from the URL. + + .. attribute:: ResolverMatch.kwargs + + The keyword arguments that would be passed to the view + function, as parsed from the URL. + + .. attribute:: ResolverMatch.url_name + + The name of the URL pattern that matches the URL. + + .. attribute:: ResolverMatch.app_name + + The application namespace for the URL pattern that matches the + URL. + + .. attribute:: ResolverMatch.namespace + + The instance namespace for the URL pattern that matches the + URL. + + .. attribute:: ResolverMatch.namespaces + + The list of individual namespace components in the full + instance namespace for the URL pattern that matches the URL. + i.e., if the namespace is ``foo:bar``, then namespaces will be + ``['foo', 'bar']``. + +A :class:`ResolverMatch` object can then be interrogated to provide +information about the URL pattern that matches a URL:: + + # Resolve a URL + match = resolve('/some/path/') + # Print the URL pattern that matches the URL + print(match.url_name) + +A :class:`ResolverMatch` object can also be assigned to a triple:: + + func, args, kwargs = resolve('/some/path/') + +One possible use of :func:`~django.core.urlresolvers.resolve` would be to test +whether a view would raise a ``Http404`` error before redirecting to it:: + + from urlparse import urlparse + from django.core.urlresolvers import resolve + from django.http import HttpResponseRedirect, Http404 + + def myview(request): + next = request.META.get('HTTP_REFERER', None) or '/' + response = HttpResponseRedirect(next) + + # modify the request and response as required, e.g. change locale + # and set corresponding locale cookie + + view, args, kwargs = resolve(urlparse(next)[2]) + kwargs['request'] = request + try: + view(*args, **kwargs) + except Http404: + return HttpResponseRedirect('/') + return response + + +permalink() +----------- + +The :func:`~django.db.models.permalink` decorator is useful for writing short +methods that return a full URL path. For example, a model's +``get_absolute_url()`` method. See :func:`django.db.models.permalink` for more. + +get_script_prefix() +------------------- + +.. function:: get_script_prefix() + +Normally, you should always use :func:`~django.core.urlresolvers.reverse` or +:func:`~django.db.models.permalink` to define URLs within your application. +However, if your application constructs part of the URL hierarchy itself, you +may occasionally need to generate URLs. In that case, you need to be able to +find the base URL of the Django project within its Web server +(normally, :func:`~django.core.urlresolvers.reverse` takes care of this for +you). In that case, you can call ``get_script_prefix()``, which will return the +script prefix portion of the URL for your Django project. If your Django +project is at the root of its Web server, this is always ``"/"``. diff --git a/docs/ref/urls.txt b/docs/ref/urls.txt new file mode 100644 index 0000000000..b9a0199984 --- /dev/null +++ b/docs/ref/urls.txt @@ -0,0 +1,156 @@ +====================================== +``django.conf.urls`` utility functions +====================================== + +.. module:: django.conf.urls + +.. versionchanged:: 1.4 + Starting with Django 1.4 functions ``patterns``, ``url``, ``include`` plus + the ``handler*`` symbols described below live in the ``django.conf.urls`` + module. + + Until Django 1.3 they were located in ``django.conf.urls.defaults``. You + still can import them from there but it will be removed in Django 1.6. + +patterns() +---------- + +.. function:: patterns(prefix, pattern_description, ...) + +A function that takes a prefix, and an arbitrary number of URL patterns, and +returns a list of URL patterns in the format Django needs. + +The first argument to ``patterns()`` is a string ``prefix``. See +:ref:`The view prefix `. + +The remaining arguments should be tuples in this format:: + + (regular expression, Python callback function [, optional_dictionary [, optional_name]]) + +The ``optional_dictionary`` and ``optional_name`` parameters are described in +:ref:`Passing extra options to view functions `. + +.. note:: + Because `patterns()` is a function call, it accepts a maximum of 255 + arguments (URL patterns, in this case). This is a limit for all Python + function calls. This is rarely a problem in practice, because you'll + typically structure your URL patterns modularly by using `include()` + sections. However, on the off-chance you do hit the 255-argument limit, + realize that `patterns()` returns a Python list, so you can split up the + construction of the list. + + :: + + urlpatterns = patterns('', + ... + ) + urlpatterns += patterns('', + ... + ) + + Python lists have unlimited size, so there's no limit to how many URL + patterns you can construct. The only limit is that you can only create 254 + at a time (the 255th argument is the initial prefix argument). + +url() +----- + +.. function:: url(regex, view, kwargs=None, name=None, prefix='') + +You can use the ``url()`` function, instead of a tuple, as an argument to +``patterns()``. This is convenient if you want to specify a name without the +optional extra arguments dictionary. For example:: + + urlpatterns = patterns('', + url(r'^index/$', index_view, name="main-view"), + ... + ) + +This function takes five arguments, most of which are optional:: + + url(regex, view, kwargs=None, name=None, prefix='') + +See :ref:`Naming URL patterns ` for why the ``name`` +parameter is useful. + +The ``prefix`` parameter has the same meaning as the first argument to +``patterns()`` and is only relevant when you're passing a string as the +``view`` parameter. + +include() +--------- + +.. function:: include(module[, namespace=None, app_name=None]) + include(pattern_list) + include((pattern_list, app_namespace, instance_namespace)) + + A function that takes a full Python import path to another URLconf module + that should be "included" in this place. Optionally, the :term:`application + namespace` and :term:`instance namespace` where the entries will be included + into can also be specified. + + ``include()`` also accepts as an argument either an iterable that returns + URL patterns or a 3-tuple containing such iterable plus the names of the + application and instance namespaces. + + :arg module: URLconf module (or module name) + :type module: Module or string + :arg namespace: Instance namespace for the URL entries being included + :type namespace: string + :arg app_name: Application namespace for the URL entries being included + :type app_name: string + :arg pattern_list: Iterable of URL entries as returned by :func:`patterns` + :arg app_namespace: Application namespace for the URL entries being included + :type app_namespace: string + :arg instance_namespace: Instance namespace for the URL entries being included + :type instance_namespace: string + +See :ref:`including-other-urlconfs` and :ref:`namespaces-and-include`. + +handler403 +---------- + +.. data:: handler403 + +A callable, or a string representing the full Python import path to the view +that should be called if the user doesn't have the permissions required to +access a resource. + +By default, this is ``'django.views.defaults.permission_denied'``. That default +value should suffice. + +See the documentation about :ref:`the 403 (HTTP Forbidden) view +` for more information. + +.. versionadded:: 1.4 + ``handler403`` is new in Django 1.4. + +handler404 +---------- + +.. data:: handler404 + +A callable, or a string representing the full Python import path to the view +that should be called if none of the URL patterns match. + +By default, this is ``'django.views.defaults.page_not_found'``. That default +value should suffice. + +See the documentation about :ref:`the 404 (HTTP Not Found) view +` for more information. + +handler500 +---------- + +.. data:: handler500 + +A callable, or a string representing the full Python import path to the view +that should be called in case of server errors. Server errors happen when you +have runtime errors in view code. + +By default, this is ``'django.views.defaults.server_error'``. That default +value should suffice. + +See the documentation about :ref:`the 500 (HTTP Internal Server Error) view +` for more information. + diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index de19578cac..bd3898172a 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -170,6 +170,37 @@ The functions defined in this module share the following properties: ``tzinfo`` attribute is a :class:`~django.utils.tzinfo.FixedOffset` instance. +``django.utils.decorators`` +=========================== + +.. module:: django.utils.decorators + :synopsis: Functions that help with creating decorators for views. + +.. function:: method_decorator(decorator) + + Converts a function decorator into a method decorator. See :ref:`decorating + class based views` for example usage. + +.. function:: decorator_from_middleware(middleware_class) + + Given a middleware class, returns a view decorator. This lets you use + middleware functionality on a per-view basis. The middleware is created + with no params passed. + +.. function:: decorator_from_middleware_with_args(middleware_class) + + Like ``decorator_from_middleware``, but returns a function + that accepts the arguments to be passed to the middleware_class. + For example, the :func:`~django.views.decorators.cache.cache_page` + decorator is created from the + :class:`~django.middleware.cache.CacheMiddleware` like this:: + + cache_page = decorator_from_middleware_with_args(CacheMiddleware) + + @cache_page(3600) + def my_view(request): + pass + ``django.utils.encoding`` ========================= diff --git a/docs/releases/1.4.2.txt b/docs/releases/1.4.2.txt index 6f2e9aca2e..07eec39764 100644 --- a/docs/releases/1.4.2.txt +++ b/docs/releases/1.4.2.txt @@ -2,13 +2,54 @@ Django 1.4.2 release notes ========================== -*TO BE RELEASED* +*October 17, 2012* This is the second security release in the Django 1.4 series. +Host header poisoning +--------------------- + +Some parts of Django -- independent of end-user-written applications -- make +use of full URLs, including domain name, which are generated from the HTTP Host +header. Some attacks against this are beyond Django's ability to control, and +require the web server to be properly configured; Django's documentation has +for some time contained notes advising users on such configuration. + +Django's own built-in parsing of the Host header is, however, still vulnerable, +as was reported to us recently. The Host header parsing in Django 1.3.3 and +Django 1.4.1 -- specifically, django.http.HttpRequest.get_host() -- was +incorrectly handling username/password information in the header. Thus, for +example, the following Host header would be accepted by Django when running on +"validsite.com":: + + Host: validsite.com:random@evilsite.com + +Using this, an attacker can cause parts of Django -- particularly the +password-reset mechanism -- to generate and display arbitrary URLs to users. + +To remedy this, the parsing in HttpRequest.get_host() is being modified; Host +headers which contain potentially dangerous content (such as username/password +pairs) now raise the exception django.core.exceptions.SuspiciousOperation + +Details of this issue were initially posted online as a `security advisory`_. + +.. _security advisory: https://www.djangoproject.com/weblog/2012/oct/17/security/ + Backwards incompatible changes ============================== * The newly introduced :class:`~django.db.models.GenericIPAddressField` constructor arguments have been adapted to match those of all other model fields. The first two keyword arguments are now verbose_name and name. + +Other bugfixes and changes +========================== + +* Subclass HTMLParser only for appropriate Python versions (#18239). +* Added batch_size argument to qs.bulk_create() (#17788). +* Fixed a small regression in the admin filters where wrongly formatted dates passed as url parameters caused an unhandled ValidationError (#18530). +* Fixed an endless loop bug when accessing permissions in templates (#18979) +* Fixed some Python 2.5 compatibility issues +* Fixed an issue with quoted filenames in Content-Disposition header (#19006) +* Made the context option in ``trans`` and ``blocktrans`` tags accept literals wrapped in single quotes (#18881). +* Numerous documentation improvements and fixes. diff --git a/docs/releases/1.5-alpha-1.txt b/docs/releases/1.5-alpha-1.txt new file mode 100644 index 0000000000..8f027c6859 --- /dev/null +++ b/docs/releases/1.5-alpha-1.txt @@ -0,0 +1,631 @@ +============================================ +Django 1.5 release notes - UNDER DEVELOPMENT +============================================ + +October 25, 2012. + +Welcome to Django 1.5 alpha! + +This is the first in a series of preview/development releases leading up to the +eventual release of Django 1.5, scheduled for December 2012. This release is +primarily targeted at developers who are interested in trying out new features +and testing the Django codebase to help identify and resolve bugs prior to the +final 1.5 release. + +As such, this release is *not* intended for production use, and any such use +is discouraged. + +In particular, we need the community's help to test Django 1.5's new `Python 3 +support`_ -- not just to report bugs on Python 3, but also regressions on Python +2. While Django is very conservative with regards to backwards compatibility, +mistakes are always possible, and it's likely that the Python 3 refactoring work +introduced some regressions. + +Django 1.5 alpha includes various `new features`_ and some minor `backwards +incompatible changes`_. There are also some features that have been dropped, +which are detailed in :doc:`our deprecation plan `, +and we've `begun the deprecation process for some features`_. + +.. _`new features`: `What's new in Django 1.5`_ +.. _`backwards incompatible changes`: `Backwards incompatible changes in 1.5`_ +.. _`begun the deprecation process for some features`: `Features deprecated in 1.5`_ + +Overview +======== + +The biggest new feature in Django 1.5 is the `configurable User model`_. Before +Django 1.5, applications that wanted to use Django's auth framework +(:mod:`django.contrib.auth`) were forced to use Django's definition of a "user". +In Django 1.5, you can now swap out the ``User`` model for one that you write +yourself. This could be a simple extension to the existing ``User`` model -- for +example, you could add a Twitter or Facebook ID field -- or you could completely +replace the ``User`` with one totally customized for your site. + +Django 1.5 is also the first release with `Python 3 support`_! We're labeling +this support "experimental" because we don't yet consider it production-ready, +but everything's in place for you to start porting your apps to Python 3. +Our next release, Django 1.6, will support Python 3 without reservations. + +Other notable new features in Django 1.5 include: + +* `Support for saving a subset of model's fields`_ - + :meth:`Model.save() ` now accepts an + ``update_fields`` argument, letting you specify which fields are + written back to the databse when you call ``save()``. This can help + in high-concurrancy operations, and can improve performance. + +* Better `support for streaming responses <#explicit-streaming-responses>`_ via + the new :class:`~django.http.StreamingHttpResponse` response class. + +* `GeoDjango`_ now supports PostGIS 2.0. + +* ... and more; `see below <#what-s-new-in-django-1-5>`_. + +Wherever possible we try to introduce new features in a backwards-compatible +manner per :doc:`our API stability policy ` policy. +However, as with previous releases, Django 1.5 ships with some minor +`backwards incompatible changes`_; people upgrading from previous versions +of Django should read that list carefully. + +One deprecated feature worth noting is the shift to "new-style" :ttag:`url` tag. +Prior to Django 1.3, syntax like ``{% url myview %}`` was interpreted +incorrectly (Django considered ``"myview"`` to be a literal name of a view, not +a template variable named ``myview``). Django 1.3 and above introduced the +``{% load url from future %}`` syntax to bring in the corrected behavior where +``myview`` was seen as a variable. + +The upshot of this is that if you are not using ``{% load url from future %}`` +in your templates, you'll need to change tags like ``{% url myview %}`` to +``{% url "myview" %}``. If you *were* using ``{% load url from future %}`` you +can simply remove that line under Django 1.5 + +Python compatibility +==================== + +Django 1.5 requires Python 2.6.5 or above, though we **highly recommended** +Python 2.7.3 or above. Support for Python 2.5 and below as been dropped. + +This change should affect only a small number of Django users, as most +operating-system vendors today are shipping Python 2.6 or newer as their default +version. If you're still using Python 2.5, however, you'll need to stick to +Django 1.4 until you can upgrade your Python version. Per :doc:`our support +policy `, Django 1.4 will continue to receive +security support until the release of Django 1.6. + +Django 1.5 does not run on a Jython final release, because Jython's latest +release doesn't currently support Python 2.6. However, Jython currently does +offer an alpha release featuring 2.7 support, and Django 1.5 supports that alpha +release. + +Python 3 support +~~~~~~~~~~~~~~~~ + +Django 1.5 introduces support for Python 3 - specifically, Python +3.2 and above. This comes in the form of a **single** codebase; you don't +need to install a different version of Django on Python 3. This means that +you can write application targeted for just Python 2, just Python 3, or single +applications that support both platforms. + +However, we're labling this support "experimental" for now: although it's +receved extensive testing via our automated test suite, it's recieved very +little real-world testing. We've done our best to eliminate bugs, but we can't +be sure we covered all possible uses of Django. Further, Django's more than a +web framework; it's an ecosystem of pluggable components. At this point, very +few third-party applications have been ported to Python 3, so it's unliukely +that a real-world application will have all its dependecies satisfied under +Python 3. + +Thus, we're recommending that Django 1.5 not be used in production under Python +3. Instead, use this oportunity to begin :doc:`porting applications to Python 3 +`. If you're an author of a pluggable component, we encourage you +to start porting now. + +We plan to offer first-class, production-ready support for Python 3 in our next +release, Django 1.6. + +What's new in Django 1.5 +======================== + +Configurable User model +~~~~~~~~~~~~~~~~~~~~~~~ + +In Django 1.5, you can now use your own model as the store for user-related +data. If your project needs a username with more than 30 characters, or if +you want to store usernames in a format other than first name/last name, or +you want to put custom profile information onto your User object, you can +now do so. + +If you have a third-party reusable application that references the User model, +you may need to make some changes to the way you reference User instances. You +should also document any specific features of the User model that your +application relies upon. + +See the :ref:`documentation on custom User models ` for +more details. + +Support for saving a subset of model's fields +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The method :meth:`Model.save() ` has a new +keyword argument ``update_fields``. By using this argument it is possible to +save only a select list of model's fields. This can be useful for performance +reasons or when trying to avoid overwriting concurrent changes. + +Deferred instances (those loaded by .only() or .defer()) will automatically +save just the loaded fields. If any field is set manually after load, that +field will also get updated on save. + +See the :meth:`Model.save() ` documentation for +more details. + +Caching of related model instances +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When traversing relations, the ORM will avoid re-fetching objects that were +previously loaded. For example, with the tutorial's models:: + + >>> first_poll = Poll.objects.all()[0] + >>> first_choice = first_poll.choice_set.all()[0] + >>> first_choice.poll is first_poll + True + +In Django 1.5, the third line no longer triggers a new SQL query to fetch +``first_choice.poll``; it was set by the second line. + +For one-to-one relationships, both sides can be cached. For many-to-one +relationships, only the single side of the relationship can be cached. This +is particularly helpful in combination with ``prefetch_related``. + +Explicit support for streaming responses +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Before Django 1.5, it was possible to create a streaming response by passing +an iterator to :class:`~django.http.HttpResponse`. But this was unreliable: +any middleware that accessed the :attr:`~django.http.HttpResponse.content` +attribute would consume the iterator prematurely. + +You can now explicitly generate a streaming response with the new +:class:`~django.http.StreamingHttpResponse` class. This class exposes a +:class:`~django.http.StreamingHttpResponse.streaming_content` attribute which +is an iterator. + +Since :class:`~django.http.StreamingHttpResponse` does not have a ``content`` +attribute, middleware that needs access to the response content must test for +streaming responses and behave accordingly. See :ref:`response-middleware` for +more information. + +``{% verbatim %}`` template tag +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To make it easier to deal with javascript templates which collide with Django's +syntax, you can now use the :ttag:`verbatim` block tag to avoid parsing the +tag's content. + +Retrieval of ``ContentType`` instances associated with proxy models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The methods :meth:`ContentTypeManager.get_for_model() ` +and :meth:`ContentTypeManager.get_for_models() ` +have a new keyword argument – respectively ``for_concrete_model`` and ``for_concrete_models``. +By passing ``False`` using this argument it is now possible to retreive the +:class:`ContentType ` +associated with proxy models. + +New ``view`` variable in class-based views context +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In all :doc:`generic class-based views ` +(or any class-based view inheriting from ``ContextMixin``), the context dictionary +contains a ``view`` variable that points to the ``View`` instance. + +GeoDjango +~~~~~~~~~ + +* :class:`~django.contrib.gis.geos.LineString` and + :class:`~django.contrib.gis.geos.MultiLineString` GEOS objects now support the + :meth:`~django.contrib.gis.geos.GEOSGeometry.interpolate()` and + :meth:`~django.contrib.gis.geos.GEOSGeometry.project()` methods + (so-called linear referencing). + +* The wkb and hex properties of `GEOSGeometry` objects preserve the Z dimension. + +* Support for PostGIS 2.0 has been added and support for GDAL < 1.5 has been + dropped. + +Minor features +~~~~~~~~~~~~~~ + +Django 1.5 also includes several smaller improvements worth noting: + +* The template engine now interprets ``True``, ``False`` and ``None`` as the + corresponding Python objects. + +* :mod:`django.utils.timezone` provides a helper for converting aware + datetimes between time zones. See :func:`~django.utils.timezone.localtime`. + +* The generic views support OPTIONS requests. + +* Management commands do not raise ``SystemExit`` any more when called by code + from :ref:`call_command `. Any exception raised by the command + (mostly :ref:`CommandError `) is propagated. + +* The dumpdata management command outputs one row at a time, preventing + out-of-memory errors when dumping large datasets. + +* In the localflavor for Canada, "pq" was added to the acceptable codes for + Quebec. It's an old abbreviation. + +* The :ref:`receiver ` decorator is now able to + connect to more than one signal by supplying a list of signals. + +* In the admin, you can now filter users by groups which they are members of. + +* :meth:`QuerySet.bulk_create() + ` now has a batch_size + argument. By default the batch_size is unlimited except for SQLite where + single batch is limited so that 999 parameters per query isn't exceeded. + +* The :setting:`LOGIN_URL` and :setting:`LOGIN_REDIRECT_URL` settings now also + accept view function names and + :ref:`named URL patterns `. This allows you to reduce + configuration duplication. More information can be found in the + :func:`~django.contrib.auth.decorators.login_required` documentation. + +* Django now provides a mod_wsgi :doc:`auth handler + `. + +* The :meth:`QuerySet.delete() ` + and :meth:`Model.delete() ` can now take + fast-path in some cases. The fast-path allows for less queries and less + objects fetched into memory. See :meth:`QuerySet.delete() + ` for details. + +* An instance of :class:`~django.core.urlresolvers.ResolverMatch` is stored on + the request as ``resolver_match``. + +* By default, all logging messages reaching the `django` logger when + :setting:`DEBUG` is `True` are sent to the console (unless you redefine the + logger in your :setting:`LOGGING` setting). + +* When using :class:`~django.template.RequestContext`, it is now possible to + look up permissions by using ``{% if 'someapp.someperm' in perms %}`` + in templates. + +* It's not required any more to have ``404.html`` and ``500.html`` templates in + the root templates directory. Django will output some basic error messages for + both situations when those templates are not found. Of course, it's still + recommended as good practice to provide those templates in order to present + pretty error pages to the user. + +* :mod:`django.contrib.auth` provides a new signal that is emitted + whenever a user fails to login successfully. See + :data:`~django.contrib.auth.signals.user_login_failed` + +* The loaddata management command now supports an `ignorenonexistent` option to + ignore data for fields that no longer exist. + +* :meth:`~django.test.SimpleTestCase.assertXMLEqual` and + :meth:`~django.test.SimpleTestCase.assertXMLNotEqual` new assertions allow + you to test equality for XML content at a semantic level, without caring for + syntax differences (spaces, attribute order, etc.). + +Backwards incompatible changes in 1.5 +===================================== + +.. warning:: + + In addition to the changes outlined in this section, be sure to review the + :doc:`deprecation plan ` for any features that + have been removed. If you haven't updated your code within the + deprecation timeline for a given feature, its removal may appear as a + backwards incompatible change. + +Context in year archive class-based views +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For consistency with the other date-based generic views, +:class:`~django.views.generic.dates.YearArchiveView` now passes ``year`` in +the context as a :class:`datetime.date` rather than a string. If you are +using ``{{ year }}`` in your templates, you must replace it with ``{{ +year|date:"Y" }}``. + +``next_year`` and ``previous_year`` were also added in the context. They are +calculated according to ``allow_empty`` and ``allow_future``. + +Context in year and month archive class-based views +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:class:`~django.views.generic.dates.YearArchiveView` and +:class:`~django.views.generic.dates.MonthArchiveView` were documented to +provide a ``date_list`` sorted in ascending order in the context, like their +function-based predecessors, but it actually was in descending order. In 1.5, +the documented order was restored. You may want to add (or remove) the +``reversed`` keyword when you're iterating on ``date_list`` in a template:: + + {% for date in date_list reversed %} + +:class:`~django.views.generic.dates.ArchiveIndexView` still provides a +``date_list`` in descending order. + +Context in TemplateView +~~~~~~~~~~~~~~~~~~~~~~~ + +For consistency with the design of the other generic views, +:class:`~django.views.generic.base.TemplateView` no longer passes a ``params`` +dictionary into the context, instead passing the variables from the URLconf +directly into the context. + +Non-form data in HTTP requests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:attr:`request.POST ` will no longer include data +posted via HTTP requests with non form-specific content-types in the header. +In prior versions, data posted with content-types other than +``multipart/form-data`` or ``application/x-www-form-urlencoded`` would still +end up represented in the :attr:`request.POST ` +attribute. Developers wishing to access the raw POST data for these cases, +should use the :attr:`request.body ` attribute +instead. + +OPTIONS, PUT and DELETE requests in the test client +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Unlike GET and POST, these HTTP methods aren't implemented by web browsers. +Rather, they're used in APIs, which transfer data in various formats such as +JSON or XML. Since such requests may contain arbitrary data, Django doesn't +attempt to decode their body. + +However, the test client used to build a query string for OPTIONS and DELETE +requests like for GET, and a request body for PUT requests like for POST. This +encoding was arbitrary and inconsistent with Django's behavior when it +receives the requests, so it was removed in Django 1.5. + +If you were using the ``data`` parameter in an OPTIONS or a DELETE request, +you must convert it to a query string and append it to the ``path`` parameter. + +If you were using the ``data`` parameter in a PUT request without a +``content_type``, you must encode your data before passing it to the test +client and set the ``content_type`` argument. + +System version of :mod:`simplejson` no longer used +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As explained below, Django 1.5 deprecates +:mod:`django.utils.simplejson` in favor of Python 2.6's built-in :mod:`json` +module. In theory, this change is harmless. Unfortunately, because of +incompatibilities between versions of :mod:`simplejson`, it may trigger errors +in some circumstances. + +JSON-related features in Django 1.4 always used :mod:`django.utils.simplejson`. +This module was actually: + +- A system version of :mod:`simplejson`, if one was available (ie. ``import + simplejson`` works), if it was more recent than Django's built-in copy or it + had the C speedups, or +- The :mod:`json` module from the standard library, if it was available (ie. + Python 2.6 or greater), or +- A built-in copy of version 2.0.7 of :mod:`simplejson`. + +In Django 1.5, those features use Python's :mod:`json` module, which is based +on version 2.0.9 of :mod:`simplejson`. + +There are no known incompatibilities between Django's copy of version 2.0.7 and +Python's copy of version 2.0.9. However, there are some incompatibilities +between other versions of :mod:`simplejson`: + +- While the :mod:`simplejson` API is documented as always returning unicode + strings, the optional C implementation can return a byte string. This was + fixed in Python 2.7. +- :class:`simplejson.JSONEncoder` gained a ``namedtuple_as_object`` keyword + argument in version 2.2. + +More information on these incompatibilities is available in `ticket #18023`_. + +The net result is that, if you have installed :mod:`simplejson` and your code +uses Django's serialization internals directly -- for instance +:class:`django.core.serializers.json.DjangoJSONEncoder`, the switch from +:mod:`simplejson` to :mod:`json` could break your code. (In general, changes to +internals aren't documented; we're making an exception here.) + +At this point, the maintainers of Django believe that using :mod:`json` from +the standard library offers the strongest guarantee of backwards-compatibility. +They recommend to use it from now on. + +.. _ticket #18023: https://code.djangoproject.com/ticket/18023#comment:10 + +String types of hasher method parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you have written a :ref:`custom password hasher `, +your ``encode()``, ``verify()`` or ``safe_summary()`` methods should accept +Unicode parameters (``password``, ``salt`` or ``encoded``). If any of the +hashing methods need byte strings, you can use the +:func:`~django.utils.encoding.force_bytes` utility to encode the strings. + +Validation of previous_page_number and next_page_number +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using :doc:`object pagination `, +the ``previous_page_number()`` and ``next_page_number()`` methods of the +:class:`~django.core.paginator.Page` object did not check if the returned +number was inside the existing page range. +It does check it now and raises an :exc:`InvalidPage` exception when the number +is either too low or too high. + +Behavior of autocommit database option on PostgreSQL changed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +PostgreSQL's autocommit option didn't work as advertised previously. It did +work for single transaction block, but after the first block was left the +autocommit behavior was never restored. This bug is now fixed in 1.5. While +this is only a bug fix, it is worth checking your applications behavior if +you are using PostgreSQL together with the autocommit option. + +Session not saved on 500 responses +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Django's session middleware will skip saving the session data if the +response's status code is 500. + +Email checks on failed admin login +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Prior to Django 1.5, if you attempted to log into the admin interface and +mistakenly used your email address instead of your username, the admin +interface would provide a warning advising that your email address was +not your username. In Django 1.5, the introduction of +:ref:`custom User models ` has required the removal of this +warning. This doesn't change the login behavior of the admin site; it only +affects the warning message that is displayed under one particular mode of +login failure. + +Changes in tests execution +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some changes have been introduced in the execution of tests that might be +backward-incompatible for some testing setups: + +Database flushing in ``django.test.TransactionTestCase`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, the test database was truncated *before* each test run in a +:class:`~django.test.TransactionTestCase`. + +In order to be able to run unit tests in any order and to make sure they are +always isolated from each other, :class:`~django.test.TransactionTestCase` will +now reset the database *after* each test run instead. + +No more implict DB sequences reset +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:class:`~django.test.TransactionTestCase` tests used to reset primary key +sequences automatically together with the database flushing actions described +above. + +This has been changed so no sequences are implicitly reset. This can cause +:class:`~django.test.TransactionTestCase` tests that depend on hard-coded +primary key values to break. + +The new :attr:`~django.test.TransactionTestCase.reset_sequences` attribute can +be used to force the old behavior for :class:`~django.test.TransactionTestCase` +that might need it. + +Ordering of tests +^^^^^^^^^^^^^^^^^ + +In order to make sure all ``TestCase`` code starts with a clean database, +tests are now executed in the following order: + +* First, all unittests (including :class:`unittest.TestCase`, + :class:`~django.test.SimpleTestCase`, :class:`~django.test.TestCase` and + :class:`~django.test.TransactionTestCase`) are run with no particular ordering + guaranteed nor enforced among them. + +* Then any other tests (e.g. doctests) that may alter the database without + restoring it to its original state are run. + +This should not cause any problems unless you have existing doctests which +assume a :class:`~django.test.TransactionTestCase` executed earlier left some +database state behind or unit tests that rely on some form of state being +preserved after the execution of other tests. Such tests are already very +fragile, and must now be changed to be able to run independently. + +`cleaned_data` dictionary kept for invalid forms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :attr:`~django.forms.Form.cleaned_data` dictionary is now always present +after form validation. When the form doesn't validate, it contains only the +fields that passed validation. You should test the success of the validation +with the :meth:`~django.forms.Form.is_valid()` method and not with the +presence or absence of the :attr:`~django.forms.Form.cleaned_data` attribute +on the form. + +Miscellaneous +~~~~~~~~~~~~~ + +* :class:`django.forms.ModelMultipleChoiceField` now returns an empty + ``QuerySet`` as the empty value instead of an empty list. + +* :func:`~django.utils.http.int_to_base36` properly raises a :exc:`TypeError` + instead of :exc:`ValueError` for non-integer inputs. + +* The ``slugify`` template filter is now available as a standard python + function at :func:`django.utils.text.slugify`. Similarly, ``remove_tags`` is + available at :func:`django.utils.html.remove_tags`. + +* Uploaded files are no longer created as executable by default. If you need + them to be executeable change :setting:`FILE_UPLOAD_PERMISSIONS` to your + needs. The new default value is `0666` (octal) and the current umask value + is first masked out. + +* The :ref:`F() expressions ` supported bitwise operators by + ``&`` and ``|``. These operators are now available using ``.bitand()`` and + ``.bitor()`` instead. The removal of ``&`` and ``|`` was done to be consistent with + :ref:`Q() expressions ` and ``QuerySet`` combining where + the operators are used as boolean AND and OR operators. + +* The :ttag:`csrf_token` template tag is no longer enclosed in a div. If you need + HTML validation against pre-HTML5 Strict DTDs, you should add a div around it + in your pages. + +Features deprecated in 1.5 +========================== + +:setting:`AUTH_PROFILE_MODULE` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +With the introduction of :ref:`custom User models `, there is +no longer any need for a built-in mechanism to store user profile data. + +You can still define user profiles models that have a one-to-one relation with +the User model - in fact, for many applications needing to associate data with +a User account, this will be an appropriate design pattern to follow. However, +the :setting:`AUTH_PROFILE_MODULE` setting, and the +:meth:`~django.contrib.auth.models.User.get_profile()` method for accessing +the user profile model, should not be used any longer. + +Streaming behavior of :class:`HttpResponse` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Django 1.5 deprecates the ability to stream a response by passing an iterator +to :class:`~django.http.HttpResponse`. If you rely on this behavior, switch to +:class:`~django.http.StreamingHttpResponse`. See above for more details. + +In Django 1.7 and above, the iterator will be consumed immediately by +:class:`~django.http.HttpResponse`. + +``django.utils.simplejson`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Since Django 1.5 drops support for Python 2.5, we can now rely on the +:mod:`json` module being available in Python's standard library, so we've +removed our own copy of :mod:`simplejson`. You should now import :mod:`json` +instead :mod:`django.utils.simplejson`. + +Unfortunately, this change might have unwanted side-effects, because of +incompatibilities between versions of :mod:`simplejson` -- see the backwards- +incompatible changes section. If you rely on features added to :mod:`simplejson` +after it became Python's :mod:`json`, you should import :mod:`simplejson` +explicitly. + +``django.utils.encoding.StrAndUnicode`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :class:`~django.utils.encoding.StrAndUnicode` mix-in has been deprecated. +Define a ``__str__`` method and apply the +:func:`~django.utils.encoding.python_2_unicode_compatible` decorator instead. + +``django.utils.itercompat.product`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :func:`~django.utils.itercompat.product` function has been deprecated. Use +the built-in :func:`itertools.product` instead. + + +``django.utils.markup`` +~~~~~~~~~~~~~~~~~~~~~~~ + +The markup contrib module has been deprecated and will follow an accelerated +deprecation schedule. Direct use of python markup libraries or 3rd party tag +libraries is preferred to Django maintaining this functionality in the +framework. diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 6420239f47..a0ce3cc7a4 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -2,6 +2,8 @@ Django 1.5 release notes - UNDER DEVELOPMENT ============================================ +Welcome to Django 1.5! + These release notes cover the `new features`_, as well as some `backwards incompatible changes`_ you'll want to be aware of when upgrading from Django 1.4 or older versions. We've also dropped some @@ -13,27 +15,119 @@ features`_. .. _`backwards incompatible changes`: `Backwards incompatible changes in 1.5`_ .. _`begun the deprecation process for some features`: `Features deprecated in 1.5`_ +Overview +======== + +The biggest new feature in Django 1.5 is the `configurable User model`_. Before +Django 1.5, applications that wanted to use Django's auth framework +(:mod:`django.contrib.auth`) were forced to use Django's definition of a "user". +In Django 1.5, you can now swap out the ``User`` model for one that you write +yourself. This could be a simple extension to the existing ``User`` model -- for +example, you could add a Twitter or Facebook ID field -- or you could completely +replace the ``User`` with one totally customized for your site. + +Django 1.5 is also the first release with `Python 3 support`_! We're labeling +this support "experimental" because we don't yet consider it production-ready, +but everything's in place for you to start porting your apps to Python 3. +Our next release, Django 1.6, will support Python 3 without reservations. + +Other notable new features in Django 1.5 include: + +* `Support for saving a subset of model's fields`_ - + :meth:`Model.save() ` now accepts an + ``update_fields`` argument, letting you specify which fields are + written back to the database when you call ``save()``. This can help + in high-concurrency operations, and can improve performance. + +* Better `support for streaming responses <#explicit-streaming-responses>`_ via + the new :class:`~django.http.StreamingHttpResponse` response class. + +* `GeoDjango`_ now supports PostGIS 2.0. + +* ... and more; `see below <#what-s-new-in-django-1-5>`_. + +Wherever possible we try to introduce new features in a backwards-compatible +manner per :doc:`our API stability policy ` policy. +However, as with previous releases, Django 1.5 ships with some minor +`backwards incompatible changes`_; people upgrading from previous versions +of Django should read that list carefully. + +One deprecated feature worth noting is the shift to "new-style" :ttag:`url` tag. +Prior to Django 1.3, syntax like ``{% url myview %}`` was interpreted +incorrectly (Django considered ``"myview"`` to be a literal name of a view, not +a template variable named ``myview``). Django 1.3 and above introduced the +``{% load url from future %}`` syntax to bring in the corrected behavior where +``myview`` was seen as a variable. + +The upshot of this is that if you are not using ``{% load url from future %}`` +in your templates, you'll need to change tags like ``{% url myview %}`` to +``{% url "myview" %}``. If you *were* using ``{% load url from future %}`` you +can simply remove that line under Django 1.5 + Python compatibility ==================== -Django 1.5 has dropped support for Python 2.5. Python 2.6.5 is now the minimum -required Python version. Django is tested and supported on Python 2.6 and -2.7. +Django 1.5 requires Python 2.6.5 or above, though we **highly recommended** +Python 2.7.3 or above. Support for Python 2.5 and below as been dropped. This change should affect only a small number of Django users, as most operating-system vendors today are shipping Python 2.6 or newer as their default version. If you're still using Python 2.5, however, you'll need to stick to -Django 1.4 until you can upgrade your Python version. Per :doc:`our support policy -`, Django 1.4 will continue to receive security -support until the release of Django 1.6. +Django 1.4 until you can upgrade your Python version. Per :doc:`our support +policy `, Django 1.4 will continue to receive +security support until the release of Django 1.6. -Django 1.5 does not run on a Jython final release, because Jython's latest release -doesn't currently support Python 2.6. However, Jython currently does offer an alpha -release featuring 2.7 support. +Django 1.5 does not run on a Jython final release, because Jython's latest +release doesn't currently support Python 2.6. However, Jython currently does +offer an alpha release featuring 2.7 support, and Django 1.5 supports that alpha +release. + +Python 3 support +~~~~~~~~~~~~~~~~ + +Django 1.5 introduces support for Python 3 - specifically, Python +3.2 and above. This comes in the form of a **single** codebase; you don't +need to install a different version of Django on Python 3. This means that +you can write application targeted for just Python 2, just Python 3, or single +applications that support both platforms. + +However, we're labeling this support "experimental" for now: although it's +received extensive testing via our automated test suite, it's received very +little real-world testing. We've done our best to eliminate bugs, but we can't +be sure we covered all possible uses of Django. Further, Django's more than a +web framework; it's an ecosystem of pluggable components. At this point, very +few third-party applications have been ported to Python 3, so it's unlikely +that a real-world application will have all its dependencies satisfied under +Python 3. + +Thus, we're recommending that Django 1.5 not be used in production under Python +3. Instead, use this opportunity to begin :doc:`porting applications to Python 3 +`. If you're an author of a pluggable component, we encourage you +to start porting now. + +We plan to offer first-class, production-ready support for Python 3 in our next +release, Django 1.6. What's new in Django 1.5 ======================== +Configurable User model +~~~~~~~~~~~~~~~~~~~~~~~ + +In Django 1.5, you can now use your own model as the store for user-related +data. If your project needs a username with more than 30 characters, or if +you want to store user's names in a format other than first name/last name, +or you want to put custom profile information onto your User object, you can +now do so. + +If you have a third-party reusable application that references the User model, +you may need to make some changes to the way you reference User instances. You +should also document any specific features of the User model that your +application relies upon. + +See the :ref:`documentation on custom User models ` for +more details. + Support for saving a subset of model's fields ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -67,6 +161,26 @@ For one-to-one relationships, both sides can be cached. For many-to-one relationships, only the single side of the relationship can be cached. This is particularly helpful in combination with ``prefetch_related``. +.. _explicit-streaming-responses: + +Explicit support for streaming responses +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Before Django 1.5, it was possible to create a streaming response by passing +an iterator to :class:`~django.http.HttpResponse`. But this was unreliable: +any middleware that accessed the :attr:`~django.http.HttpResponse.content` +attribute would consume the iterator prematurely. + +You can now explicitly generate a streaming response with the new +:class:`~django.http.StreamingHttpResponse` class. This class exposes a +:class:`~django.http.StreamingHttpResponse.streaming_content` attribute which +is an iterator. + +Since :class:`~django.http.StreamingHttpResponse` does not have a ``content`` +attribute, middleware that needs access to the response content must test for +streaming responses and behave accordingly. See :ref:`response-middleware` for +more information. + ``{% verbatim %}`` template tag ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -80,16 +194,31 @@ Retrieval of ``ContentType`` instances associated with proxy models The methods :meth:`ContentTypeManager.get_for_model() ` and :meth:`ContentTypeManager.get_for_models() ` have a new keyword argument – respectively ``for_concrete_model`` and ``for_concrete_models``. -By passing ``False`` using this argument it is now possible to retreive the +By passing ``False`` using this argument it is now possible to retrieve the :class:`ContentType ` associated with proxy models. New ``view`` variable in class-based views context ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + In all :doc:`generic class-based views ` (or any class-based view inheriting from ``ContextMixin``), the context dictionary contains a ``view`` variable that points to the ``View`` instance. +GeoDjango +~~~~~~~~~ + +* :class:`~django.contrib.gis.geos.LineString` and + :class:`~django.contrib.gis.geos.MultiLineString` GEOS objects now support the + :meth:`~django.contrib.gis.geos.GEOSGeometry.interpolate()` and + :meth:`~django.contrib.gis.geos.GEOSGeometry.project()` methods + (so-called linear referencing). + +* The wkb and hex properties of `GEOSGeometry` objects preserve the Z dimension. + +* Support for PostGIS 2.0 has been added and support for GDAL < 1.5 has been + dropped. + Minor features ~~~~~~~~~~~~~~ @@ -116,6 +245,8 @@ Django 1.5 also includes several smaller improvements worth noting: * The :ref:`receiver ` decorator is now able to connect to more than one signal by supplying a list of signals. +* In the admin, you can now filter users by groups which they are members of. + * :meth:`QuerySet.bulk_create() ` now has a batch_size argument. By default the batch_size is unlimited except for SQLite where @@ -127,6 +258,44 @@ Django 1.5 also includes several smaller improvements worth noting: configuration duplication. More information can be found in the :func:`~django.contrib.auth.decorators.login_required` documentation. +* Django now provides a mod_wsgi :doc:`auth handler + `. + +* The :meth:`QuerySet.delete() ` + and :meth:`Model.delete() ` can now take + fast-path in some cases. The fast-path allows for less queries and less + objects fetched into memory. See :meth:`QuerySet.delete() + ` for details. + +* An instance of :class:`~django.core.urlresolvers.ResolverMatch` is stored on + the request as ``resolver_match``. + +* By default, all logging messages reaching the `django` logger when + :setting:`DEBUG` is `True` are sent to the console (unless you redefine the + logger in your :setting:`LOGGING` setting). + +* When using :class:`~django.template.RequestContext`, it is now possible to + look up permissions by using ``{% if 'someapp.someperm' in perms %}`` + in templates. + +* It's not required any more to have ``404.html`` and ``500.html`` templates in + the root templates directory. Django will output some basic error messages for + both situations when those templates are not found. Of course, it's still + recommended as good practice to provide those templates in order to present + pretty error pages to the user. + +* :mod:`django.contrib.auth` provides a new signal that is emitted + whenever a user fails to login successfully. See + :data:`~django.contrib.auth.signals.user_login_failed` + +* The loaddata management command now supports an `ignorenonexistent` option to + ignore data for fields that no longer exist. + +* :meth:`~django.test.SimpleTestCase.assertXMLEqual` and + :meth:`~django.test.SimpleTestCase.assertXMLNotEqual` new assertions allow + you to test equality for XML content at a semantic level, without caring for + syntax differences (spaces, attribute order, etc.). + Backwards incompatible changes in 1.5 ===================================== @@ -150,6 +319,21 @@ year|date:"Y" }}``. ``next_year`` and ``previous_year`` were also added in the context. They are calculated according to ``allow_empty`` and ``allow_future``. +Context in year and month archive class-based views +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:class:`~django.views.generic.dates.YearArchiveView` and +:class:`~django.views.generic.dates.MonthArchiveView` were documented to +provide a ``date_list`` sorted in ascending order in the context, like their +function-based predecessors, but it actually was in descending order. In 1.5, +the documented order was restored. You may want to add (or remove) the +``reversed`` keyword when you're iterating on ``date_list`` in a template:: + + {% for date in date_list reversed %} + +:class:`~django.views.generic.dates.ArchiveIndexView` still provides a +``date_list`` in descending order. + Context in TemplateView ~~~~~~~~~~~~~~~~~~~~~~~ @@ -158,6 +342,18 @@ For consistency with the design of the other generic views, dictionary into the context, instead passing the variables from the URLconf directly into the context. +Non-form data in HTTP requests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:attr:`request.POST ` will no longer include data +posted via HTTP requests with non form-specific content-types in the header. +In prior versions, data posted with content-types other than +``multipart/form-data`` or ``application/x-www-form-urlencoded`` would still +end up represented in the :attr:`request.POST ` +attribute. Developers wishing to access the raw POST data for these cases, +should use the :attr:`request.body ` attribute +instead. + OPTIONS, PUT and DELETE requests in the test client ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -260,6 +456,18 @@ Session not saved on 500 responses Django's session middleware will skip saving the session data if the response's status code is 500. +Email checks on failed admin login +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Prior to Django 1.5, if you attempted to log into the admin interface and +mistakenly used your email address instead of your username, the admin +interface would provide a warning advising that your email address was +not your username. In Django 1.5, the introduction of +:ref:`custom User models ` has required the removal of this +warning. This doesn't change the login behavior of the admin site; it only +affects the warning message that is displayed under one particular mode of +login failure. + Changes in tests execution ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -276,8 +484,8 @@ In order to be able to run unit tests in any order and to make sure they are always isolated from each other, :class:`~django.test.TransactionTestCase` will now reset the database *after* each test run instead. -No more implict DB sequences reset -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +No more implicit DB sequences reset +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :class:`~django.test.TransactionTestCase` tests used to reset primary key sequences automatically together with the database flushing actions described @@ -324,7 +532,8 @@ on the form. Miscellaneous ~~~~~~~~~~~~~ -* GeoDjango dropped support for GDAL < 1.5 +* :class:`django.forms.ModelMultipleChoiceField` now returns an empty + ``QuerySet`` as the empty value instead of an empty list. * :func:`~django.utils.http.int_to_base36` properly raises a :exc:`TypeError` instead of :exc:`ValueError` for non-integer inputs. @@ -333,11 +542,50 @@ Miscellaneous function at :func:`django.utils.text.slugify`. Similarly, ``remove_tags`` is available at :func:`django.utils.html.remove_tags`. +* Uploaded files are no longer created as executable by default. If you need + them to be executable change :setting:`FILE_UPLOAD_PERMISSIONS` to your + needs. The new default value is `0666` (octal) and the current umask value + is first masked out. + +* The :ref:`F() expressions ` supported bitwise operators by + ``&`` and ``|``. These operators are now available using ``.bitand()`` and + ``.bitor()`` instead. The removal of ``&`` and ``|`` was done to be consistent with + :ref:`Q() expressions ` and ``QuerySet`` combining where + the operators are used as boolean AND and OR operators. + +* The :ttag:`csrf_token` template tag is no longer enclosed in a div. If you need + HTML validation against pre-HTML5 Strict DTDs, you should add a div around it + in your pages. + Features deprecated in 1.5 ========================== .. _simplejson-deprecation: +:setting:`AUTH_PROFILE_MODULE` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +With the introduction of :ref:`custom User models `, there is +no longer any need for a built-in mechanism to store user profile data. + +You can still define user profiles models that have a one-to-one relation with +the User model - in fact, for many applications needing to associate data with +a User account, this will be an appropriate design pattern to follow. However, +the :setting:`AUTH_PROFILE_MODULE` setting, and the +:meth:`~django.contrib.auth.models.User.get_profile()` method for accessing +the user profile model, should not be used any longer. + +Streaming behavior of :class:`HttpResponse` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Django 1.5 deprecates the ability to stream a response by passing an iterator +to :class:`~django.http.HttpResponse`. If you rely on this behavior, switch to +:class:`~django.http.StreamingHttpResponse`. See +:ref:`explicit-streaming-responses` above. + +In Django 1.7 and above, the iterator will be consumed immediately by +:class:`~django.http.HttpResponse`. + ``django.utils.simplejson`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -352,12 +600,6 @@ incompatibilities between versions of :mod:`simplejson` -- see the If you rely on features added to :mod:`simplejson` after it became Python's :mod:`json`, you should import :mod:`simplejson` explicitly. -``itercompat.product`` -~~~~~~~~~~~~~~~~~~~~~~ - -The :func:`~django.utils.itercompat.product` function has been deprecated. Use -the built-in :func:`itertools.product` instead. - ``django.utils.encoding.StrAndUnicode`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -365,6 +607,13 @@ The :class:`~django.utils.encoding.StrAndUnicode` mix-in has been deprecated. Define a ``__str__`` method and apply the :func:`~django.utils.encoding.python_2_unicode_compatible` decorator instead. +``django.utils.itercompat.product`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :func:`~django.utils.itercompat.product` function has been deprecated. Use +the built-in :func:`itertools.product` instead. + + ``django.utils.markup`` ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releases/index.txt b/docs/releases/index.txt index 2329d1effa..6df9821f56 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -28,7 +28,7 @@ Final releases .. toctree:: :maxdepth: 1 - .. 1.4.2 (uncomment on release) + 1.4.2 1.4.1 1.4 @@ -92,6 +92,7 @@ notes. .. toctree:: :maxdepth: 1 + 1.5-alpha-1 1.4-beta-1 1.4-alpha-1 1.3-beta-1 diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index ef03d5479c..41159984f6 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -149,6 +149,12 @@ Methods :class:`~django.contrib.auth.models.User` objects have the following custom methods: + .. method:: models.User.get_username() + + Returns the username for the user. Since the User model can be swapped + out, you should use this method instead of referencing the username + attribute directly. + .. method:: models.User.is_anonymous() Always returns ``False``. This is a way of differentiating @@ -250,6 +256,12 @@ Methods .. method:: models.User.get_profile() + .. deprecated:: 1.5 + With the introduction of :ref:`custom User models `, + the use of :setting:`AUTH_PROFILE_MODULE` to define a single profile + model is no longer supported. See the + :doc:`Django 1.5 release notes` for more information. + Returns a site-specific profile for this user. Raises :exc:`django.contrib.auth.models.SiteProfileNotAvailable` if the current site doesn't allow profiles, or @@ -582,6 +594,12 @@ correct path and environment for you. Storing additional information about users ------------------------------------------ +.. deprecated:: 1.5 + With the introduction of :ref:`custom User models `, + the use of :setting:`AUTH_PROFILE_MODULE` to define a single profile + model is no longer supported. See the + :doc:`Django 1.5 release notes` for more information. + If you'd like to store additional information related to your users, Django provides a method to specify a site-specific related model -- termed a "user profile" -- for this purpose. @@ -860,19 +878,18 @@ How to log a user out Login and logout signals ------------------------ -.. versionadded:: 1.3 - The auth framework uses two :doc:`signals ` that can be used for notification when a user logs in or out. .. data:: django.contrib.auth.signals.user_logged_in + :module: Sent when a user logs in successfully. Arguments sent with this signal: ``sender`` - As above: the class of the user that just logged in. + The class of the user that just logged in. ``request`` The current :class:`~django.http.HttpRequest` instance. @@ -881,6 +898,7 @@ Arguments sent with this signal: The user instance that just logged in. .. data:: django.contrib.auth.signals.user_logged_out + :module: Sent when the logout method is called. @@ -895,6 +913,21 @@ Sent when the logout method is called. The user instance that just logged out or ``None`` if the user was not authenticated. +.. data:: django.contrib.auth.signals.user_login_failed + :module: +.. versionadded:: 1.5 + +Sent when the user failed to login successfully + +``sender`` + The name of the module used for authentication. + +``credentials`` + A dictonary of keyword arguments containing the user credentials that were + passed to :func:`~django.contrib.auth.authenticate()` or your own custom + authentication backend. Credentials matching a set of 'sensitive' patterns, + (including password) will not be sent in the clear as part of the signal. + Limiting access to logged-in users ---------------------------------- @@ -960,8 +993,6 @@ The login_required decorator context variable which stores the redirect path will use the value of ``redirect_field_name`` as its key rather than ``"next"`` (the default). - .. versionadded:: 1.3 - :func:`~django.contrib.auth.decorators.login_required` also takes an optional ``login_url`` parameter. Example:: @@ -1189,9 +1220,6 @@ includes a few other useful built-in views located in that can be used to reset the password, and sending that link to the user's registered email address. - .. versionchanged:: 1.3 - The ``from_email`` argument was added. - .. versionchanged:: 1.4 Users flagged with an unusable password (see :meth:`~django.contrib.auth.models.User.set_unusable_password()` @@ -1352,6 +1380,9 @@ Helper functions URL to redirect to after log out. Overrides ``next`` if the given ``GET`` parameter is passed. + +.. _built-in-auth-forms: + Built-in forms -------------- @@ -1672,10 +1703,6 @@ The currently logged-in user's permissions are stored in the template variable :class:`django.contrib.auth.context_processors.PermWrapper`, which is a template-friendly proxy of permissions. -.. versionchanged:: 1.3 - Prior to version 1.3, ``PermWrapper`` was located in - ``django.core.context_processors``. - In the ``{{ perms }}`` object, single-attribute lookup is a proxy to :meth:`User.has_module_perms `. This example would display ``True`` if the logged-in user had any permissions @@ -1706,6 +1733,20 @@ Thus, you can check permissions in template ``{% if %}`` statements:

    You don't have permission to do anything in the foo app.

    {% endif %} +.. versionadded:: 1.5 + Permission lookup by "if in". + +It is possible to also look permissions up by ``{% if in %}`` statements. +For example: + +.. code-block:: html+django + + {% if 'foo' in perms %} + {% if 'foo.can_vote' in perms %} +

    In lookup works, too.

    + {% endif %} + {% endif %} + Groups ====== @@ -1746,6 +1787,533 @@ Fields group.permissions.remove(permission, permission, ...) group.permissions.clear() +.. _auth-custom-user: + +Customizing the User model +========================== + +.. versionadded:: 1.5 + +Some kinds of projects may have authentication requirements for which Django's +built-in :class:`~django.contrib.auth.models.User` model is not always +appropriate. For instance, on some sites it makes more sense to use an email +address as your identification token instead of a username. + +Django allows you to override the default User model by providing a value for +the :setting:`AUTH_USER_MODEL` setting that references a custom model:: + + AUTH_USER_MODEL = 'myapp.MyUser' + +This dotted pair describes the name of the Django app, and the name of the Django +model that you wish to use as your User model. + +.. admonition:: Warning + + Changing :setting:`AUTH_USER_MODEL` has a big effect on your database + structure. It changes the tables that are available, and it will affect the + construction of foreign keys and many-to-many relationships. If you intend + to set :setting:`AUTH_USER_MODEL`, you should set it before running + ``manage.py syncdb`` for the first time. + + If you have an existing project and you want to migrate to using a custom + User model, you may need to look into using a migration tool like South_ + to ease the transition. + +.. _South: http://south.aeracode.org + +Referencing the User model +-------------------------- + +If you reference :class:`~django.contrib.auth.models.User` directly (for +example, by referring to it in a foreign key), your code will not work in +projects where the :setting:`AUTH_USER_MODEL` setting has been changed to a +different User model. + +Instead of referring to :class:`~django.contrib.auth.models.User` directly, +you should reference the user model using +:func:`django.contrib.auth.get_user_model()`. This method will return the +currently active User model -- the custom User model if one is specified, or +:class:`~django.contrib.auth.User` otherwise. + +When you define a foreign key or many-to-many relations to the User model, +you should specify the custom model using the :setting:`AUTH_USER_MODEL` +setting. For example:: + + from django.conf import settings + from django.db import models + + class Article(models.Model) + author = models.ForeignKey(settings.AUTH_USER_MODEL) + +Specifying a custom User model +------------------------------ + +.. admonition:: Model design considerations + + Think carefully before handling information not directly related to + authentication in your custom User Model. + + It may be better to store app-specific user information in a model + that has a relation with the User model. That allows each app to specify + its own user data requirements without risking conflicts with other + apps. On the other hand, queries to retrieve this related information + will involve a database join, which may have an effect on performance. + +Django expects your custom User model to meet some minimum requirements. + +1. Your model must have a single unique field that can be used for + identification purposes. This can be a username, an email address, + or any other unique attribute. + +2. Your model must provide a way to address the user in a "short" and + "long" form. The most common interpretation of this would be to use + the user's given name as the "short" identifier, and the user's full + name as the "long" identifier. However, there are no constraints on + what these two methods return - if you want, they can return exactly + the same value. + +The easiest way to construct a compliant custom User model is to inherit from +:class:`~django.contrib.auth.models.AbstractBaseUser`. +:class:`~django.contrib.auth.models.AbstractBaseUser` provides the core +implementation of a `User` model, including hashed passwords and tokenized +password resets. You must then provide some key implementation details: + +.. class:: models.CustomUser + + .. attribute:: User.USERNAME_FIELD + + A string describing the name of the field on the User model that is + used as the unique identifier. This will usually be a username of + some kind, but it can also be an email address, or any other unique + identifier. In the following example, the field `identifier` is used + as the identifying field:: + + class MyUser(AbstractBaseUser): + identfier = models.CharField(max_length=40, unique=True, db_index=True) + ... + USERNAME_FIELD = 'identifier' + + .. attribute:: User.REQUIRED_FIELDS + + A list of the field names that *must* be provided when creating + a user. For example, here is the partial definition for a User model + that defines two required fields - a date of birth and height:: + + class MyUser(AbstractBaseUser): + ... + date_of_birth = models.DateField() + height = models.FloatField() + ... + REQUIRED_FIELDS = ['date_of_birth', 'height'] + + .. note:: + + ``REQUIRED_FIELDS`` must contain all required fields on your User + model, but should *not* contain the ``USERNAME_FIELD``. + + .. method:: User.get_full_name(): + + A longer formal identifier for the user. A common interpretation + would be the full name name of the user, but it can be any string that + identifies the user. + + .. method:: User.get_short_name(): + + A short, informal identifier for the user. A common interpretation + would be the first name of the user, but it can be any string that + identifies the user in an informal way. It may also return the same + value as :meth:`django.contrib.auth.User.get_full_name()`. + +The following methods are available on any subclass of +:class:`~django.contrib.auth.models.AbstractBaseUser`: + +.. class:: models.AbstractBaseUser + + .. method:: models.AbstractBaseUser.get_username() + + Returns the value of the field nominated by ``USERNAME_FIELD``. + + .. method:: models.AbstractBaseUser.is_anonymous() + + Always returns ``False``. This is a way of differentiating + from :class:`~django.contrib.auth.models.AnonymousUser` objects. + Generally, you should prefer using + :meth:`~django.contrib.auth.models.AbstractBaseUser.is_authenticated()` to this + method. + + .. method:: models.AbstractBaseUser.is_authenticated() + + Always returns ``True``. This is a way to tell if the user has been + authenticated. This does not imply any permissions, and doesn't check + if the user is active - it only indicates that the user has provided a + valid username and password. + + .. method:: models.AbstractBaseUser.set_password(raw_password) + + Sets the user's password to the given raw string, taking care of the + password hashing. Doesn't save the + :class:`~django.contrib.auth.models.AbstractBaseUser` object. + + .. method:: models.AbstractBaseUser.check_password(raw_password) + + Returns ``True`` if the given raw string is the correct password for + the user. (This takes care of the password hashing in making the + comparison.) + + .. method:: models.AbstractBaseUser.set_unusable_password() + + Marks the user as having no password set. This isn't the same as + having a blank string for a password. + :meth:`~django.contrib.auth.models.AbstractBaseUser.check_password()` for this user + will never return ``True``. Doesn't save the + :class:`~django.contrib.auth.models.AbstractBaseUser` object. + + You may need this if authentication for your application takes place + against an existing external source such as an LDAP directory. + + .. method:: models.AbstractBaseUser.has_usable_password() + + Returns ``False`` if + :meth:`~django.contrib.auth.models.AbstractBaseUser.set_unusable_password()` has + been called for this user. + + +You should also define a custom manager for your User model. If your User +model defines `username` and `email` fields the same as Django's default User, +you can just install Django's +:class:`~django.contrib.auth.models.UserManager`; however, if your User model +defines different fields, you will need to define a custom manager that +extends :class:`~django.contrib.auth.models.BaseUserManager` providing two +additional methods: + +.. class:: models.CustomUserManager + + .. method:: models.CustomUserManager.create_user(*username_field*, password=None, **other_fields) + + The prototype of `create_user()` should accept the username field, + plus all required fields as arguments. For example, if your user model + uses `email` as the username field, and has `date_of_birth` as a required + fields, then create_user should be defined as:: + + def create_user(self, email, date_of_birth, password=None): + # create user here + + .. method:: models.CustomUserManager.create_superuser(*username_field*, password, **other_fields) + + The prototype of `create_user()` should accept the username field, + plus all required fields as arguments. For example, if your user model + uses `email` as the username field, and has `date_of_birth` as a required + fields, then create_superuser should be defined as:: + + def create_superuser(self, email, date_of_birth, password): + # create superuser here + + Unlike `create_user()`, `create_superuser()` *must* require the caller + to provider a password. + +:class:`~django.contrib.auth.models.BaseUserManager` provides the following +utility methods: + +.. class:: models.BaseUserManager + + .. method:: models.BaseUserManager.normalize_email(email) + + A classmethod that normalizes email addresses by lowercasing + the domain portion of the email address. + + .. method:: models.BaseUserManager.get_by_natural_key(username) + + Retrieves a user instance using the contents of the field + nominated by ``USERNAME_FIELD``. + + .. method:: models.BaseUserManager.make_random_password(length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789') + + Returns a random password with the given length and given string of + allowed characters. (Note that the default value of ``allowed_chars`` + doesn't contain letters that can cause user confusion, including: + + * ``i``, ``l``, ``I``, and ``1`` (lowercase letter i, lowercase + letter L, uppercase letter i, and the number one) + * ``o``, ``O``, and ``0`` (uppercase letter o, lowercase letter o, + and zero) + +Extending Django's default User +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you're entirely happy with Django's :class:`~django.contrib.auth.models.User` +model and you just want to add some additional profile information, you can +simply subclass :class:`~django.contrib.auth.models.AbstractUser` and add your +custom profile fields. + +Custom users and the built-in auth forms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As you may expect, built-in Django's :ref:`forms ` +and :ref:`views ` make certain assumptions about +the user model that they are working with. + +If your user model doesn't follow the same assumptions, it may be necessary to define +a replacement form, and pass that form in as part of the configuration of the +auth views. + +* :class:`~django.contrib.auth.forms.UserCreationForm` + + Depends on the :class:`~django.contrib.auth.models.User` model. + Must be re-written for any custom user model. + +* :class:`~django.contrib.auth.forms.UserChangeForm` + + Depends on the :class:`~django.contrib.auth.models.User` model. + Must be re-written for any custom user model. + +* :class:`~django.contrib.auth.forms.AuthenticationForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser`, + and will adapt to use the field defined in `USERNAME_FIELD`. + +* :class:`~django.contrib.auth.forms.PasswordResetForm` + + Assumes that the user model has an integer primary key, has a field named + `email` that can be used to identify the user, and a boolean field + named `is_active` to prevent password resets for inactive users. + +* :class:`~django.contrib.auth.forms.SetPasswordForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser` + +* :class:`~django.contrib.auth.forms.PasswordChangeForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser` + +* :class:`~django.contrib.auth.forms.AdminPasswordChangeForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser` + + +Custom users and django.contrib.admin +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want your custom User model to also work with Admin, your User model must +define some additional attributes and methods. These methods allow the admin to +control access of the User to admin content: + +.. attribute:: User.is_staff + + Returns True if the user is allowed to have access to the admin site. + +.. attribute:: User.is_active + + Returns True if the user account is currently active. + +.. method:: User.has_perm(perm, obj=None): + + Returns True if the user has the named permission. If `obj` is + provided, the permission needs to be checked against a specific object + instance. + +.. method:: User.has_module_perms(app_label): + + Returns True if the user has permission to access models in + the given app. + +You will also need to register your custom User model with the admin. If +your custom User model extends :class:`~django.contrib.auth.models.AbstractUser`, +you can use Django's existing :class:`~django.contrib.auth.admin.UserAdmin` +class. However, if your User model extends +:class:`~django.contrib.auth.models.AbstractBaseUser`, you'll need to define +a custom ModelAdmin class. It may be possible to subclass the default +:class:`~django.contrib.auth.admin.UserAdmin`; however, you'll need to +override any of the definitions that refer to fields on +:class:`~django.contrib.auth.models.AbstractUser` that aren't on your +custom User class. + +Custom users and Proxy models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +One limitation of custom User models is that installing a custom User model +will break any proxy model extending :class:`~django.contrib.auth.models.User`. +Proxy models must be based on a concrete base class; by defining a custom User +model, you remove the ability of Django to reliably identify the base class. + +If your project uses proxy models, you must either modify the proxy to extend +the User model that is currently in use in your project, or merge your proxy's +behavior into your User subclass. + +A full example +-------------- + +Here is an example of an admin-compliant custom user app. This user model uses +an email address as the username, and has a required date of birth; it +provides no permission checking, beyond a simple `admin` flag on the user +account. This model would be compatible with all the built-in auth forms and +views, except for the User creation forms. + +This code would all live in a ``models.py`` file for a custom +authentication app:: + + from django.db import models + from django.contrib.auth.models import ( + BaseUserManager, AbstractBaseUser + ) + + + class MyUserManager(BaseUserManager): + def create_user(self, email, date_of_birth, password=None): + """ + Creates and saves a User with the given email, date of + birth and password. + """ + if not email: + raise ValueError('Users must have an email address') + + user = self.model( + email=MyUserManager.normalize_email(email), + date_of_birth=date_of_birth, + ) + + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, email, date_of_birth, password): + """ + Creates and saves a superuser with the given email, date of + birth and password. + """ + user = self.create_user(email, + password=password, + date_of_birth=date_of_birth + ) + user.is_admin = True + user.save(using=self._db) + return user + + + class MyUser(AbstractBaseUser): + email = models.EmailField( + verbose_name='email address', + max_length=255, + unique=True, + db_index=True, + ) + date_of_birth = models.DateField() + is_active = models.BooleanField(default=True) + is_admin = models.BooleanField(default=False) + + objects = MyUserManager() + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['date_of_birth'] + + def get_full_name(self): + # The user is identified by their email address + return self.email + + def get_short_name(self): + # The user is identified by their email address + return self.email + + def __unicode__(self): + return self.email + + def has_perm(self, perm, obj=None): + "Does the user have a specific permission?" + # Simplest possible answer: Yes, always + return True + + def has_module_perms(self, app_label): + "Does the user have permissions to view the app `app_label`?" + # Simplest possible answer: Yes, always + return True + + @property + def is_staff(self): + "Is the user a member of staff?" + # Simplest possible answer: All admins are staff + return self.is_admin + +Then, to register this custom User model with Django's admin, the following +code would be required in the app's ``admin.py`` file:: + + from django import forms + from django.contrib import admin + from django.contrib.auth.models import Group + from django.contrib.auth.admin import UserAdmin + from django.contrib.auth.forms import ReadOnlyPasswordHashField + + from customauth.models import MyUser + + + class UserCreationForm(forms.ModelForm): + """A form for creating new users. Includes all the required + fields, plus a repeated password.""" + password1 = forms.CharField(label='Password', widget=forms.PasswordInput) + password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput) + + class Meta: + model = MyUser + fields = ('email', 'date_of_birth') + + def clean_password2(self): + # Check that the two password entries match + password1 = self.cleaned_data.get("password1") + password2 = self.cleaned_data.get("password2") + if password1 and password2 and password1 != password2: + raise forms.ValidationError("Passwords don't match") + return password2 + + def save(self, commit=True): + # Save the provided password in hashed format + user = super(UserCreationForm, self).save(commit=False) + user.set_password(self.cleaned_data["password1"]) + if commit: + user.save() + return user + + + class UserChangeForm(forms.ModelForm): + """A form for updateing users. Includes all the fields on + the user, but replaces the password field with admin's + pasword hash display field. + """ + password = ReadOnlyPasswordHashField() + + class Meta: + model = MyUser + + + class MyUserAdmin(UserAdmin): + # The forms to add and change user instances + form = UserChangeForm + add_form = UserCreationForm + + # The fields to be used in displaying the User model. + # These override the definitions on the base UserAdmin + # that reference specific fields on auth.User. + list_display = ('email', 'date_of_birth', 'is_admin') + list_filter = ('is_admin',) + fieldsets = ( + (None, {'fields': ('email', 'password')}), + ('Personal info', {'fields': ('date_of_birth',)}), + ('Permissions', {'fields': ('is_admin',)}), + ('Important dates', {'fields': ('last_login',)}), + ) + add_fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ('email', 'date_of_birth', 'password1', 'password2')} + ), + ) + search_fields = ('email',) + ordering = ('email',) + filter_horizontal = () + + # Now register the new UserAdmin... + admin.site.register(MyUser, MyUserAdmin) + # ... and, since we're not using Django's builtin permissions, + # unregister the Group model from admin. + admin.site.unregister(Group) + .. _authentication-backends: Other authentication sources @@ -1951,8 +2519,6 @@ for example, to control anonymous access. Authorization for inactive users ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionchanged:: 1.3 - An inactive user is a one that is authenticated but has its attribute ``is_active`` set to ``False``. However this does not mean they are not authorized to do anything. For example they are allowed to activate their diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index f13238e342..2f95c33dd5 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -51,13 +51,6 @@ Your cache preference goes in the :setting:`CACHES` setting in your settings file. Here's an explanation of all available values for :setting:`CACHES`. -.. versionchanged:: 1.3 - The settings used to configure caching changed in Django 1.3. In - Django 1.2 and earlier, you used a single string-based - :setting:`CACHE_BACKEND` setting to configure caches. This has - been replaced with the new dictionary-based :setting:`CACHES` - setting. - .. _memcached: Memcached @@ -83,9 +76,6 @@ two most common are `python-memcached`_ and `pylibmc`_. .. _`python-memcached`: ftp://ftp.tummy.com/pub/python-memcached/ .. _`pylibmc`: http://sendapatch.se/projects/pylibmc/ -.. versionchanged:: 1.3 - Support for ``pylibmc`` was added. - To use Memcached with Django: * Set :setting:`BACKEND ` to @@ -296,7 +286,7 @@ cache is multi-process and thread-safe. To use it, set The cache :setting:`LOCATION ` is used to identify individual memory stores. If you only have one locmem cache, you can omit the -:setting:`LOCATION `; however, if you have more that one local +:setting:`LOCATION `; however, if you have more than one local memory cache, you will need to assign a name to at least one of them in order to keep them separate. @@ -673,12 +663,27 @@ dictionaries, lists of model objects, and so forth. (Most common Python objects can be pickled; refer to the Python documentation for more information about pickling.) +Accessing the cache +------------------- + The cache module, ``django.core.cache``, has a ``cache`` object that's automatically created from the ``'default'`` entry in the :setting:`CACHES` setting:: >>> from django.core.cache import cache +If you have multiple caches defined in :setting:`CACHES`, then you can use +:func:`django.core.cache.get_cache` to retrieve a cache object for any key:: + + >>> from django.core.cache import get_cache + >>> cache = get_cache('alternate') + +If the named key does not exist, :exc:`InvalidCacheBackendError` will be raised. + + +Basic usage +----------- + The basic interface is ``set(key, value, timeout)`` and ``get(key)``:: >>> cache.set('my_key', 'hello, world!', 30) @@ -686,7 +691,7 @@ The basic interface is ``set(key, value, timeout)`` and ``get(key)``:: 'hello, world!' The ``timeout`` argument is optional and defaults to the ``timeout`` -argument of the ``'default'`` backend in :setting:`CACHES` setting +argument of the appropriate backend in the :setting:`CACHES` setting (explained above). It's the number of seconds the value should be stored in the cache. @@ -785,8 +790,6 @@ nonexistent cache key.:: Cache key prefixing ------------------- -.. versionadded:: 1.3 - If you are sharing a cache instance between servers, or between your production and development environments, it's possible for data cached by one server to be used by another server. If the format of cached @@ -807,8 +810,6 @@ collisions in cache values. Cache versioning ---------------- -.. versionadded:: 1.3 - When you change running code that uses cached values, you may need to purge any existing cached values. The easiest way to do this is to flush the entire cache, but this can lead to the loss of cache values @@ -856,8 +857,6 @@ keys unaffected. Continuing our previous example:: Cache key transformation ------------------------ -.. versionadded:: 1.3 - As described in the previous two sections, the cache key provided by a user is not used verbatim -- it is combined with the cache prefix and key version to provide a final cache key. By default, the three parts @@ -878,8 +877,6 @@ be used instead of the default key combining function. Cache key warnings ------------------ -.. versionadded:: 1.3 - Memcached, the most commonly-used production cache backend, does not allow cache keys longer than 250 characters or containing whitespace or control characters, and using such keys will cause an exception. To encourage @@ -966,10 +963,6 @@ mechanism should take into account when building its cache key. For example, if the contents of a Web page depend on a user's language preference, the page is said to "vary on language." -.. versionchanged:: 1.3 - In Django 1.3 the full request path -- including the query -- is used - to create the cache keys, instead of only the path component in Django 1.2. - By default, Django's cache system creates its cache keys using the requested path and query -- e.g., ``"/stories/2005/?order_by=author"``. This means every request to that URL will use the same cached version, regardless of user-agent diff --git a/docs/topics/class-based-views/generic-display.txt b/docs/topics/class-based-views/generic-display.txt index 0d4cb6244d..10279c0f63 100644 --- a/docs/topics/class-based-views/generic-display.txt +++ b/docs/topics/class-based-views/generic-display.txt @@ -4,11 +4,6 @@ Class-based generic views ========================= -.. note:: - Prior to Django 1.3, generic views were implemented as functions. The - function-based implementation has been removed in favor of the - class-based approach described here. - Writing Web applications can be monotonous, because we repeat certain patterns again and again. Django tries to take away some of that monotony at the model and template layers, but Web developers also experience this boredom at the view diff --git a/docs/topics/class-based-views/generic-editing.txt b/docs/topics/class-based-views/generic-editing.txt index 23d346a32a..7bae3c692d 100644 --- a/docs/topics/class-based-views/generic-editing.txt +++ b/docs/topics/class-based-views/generic-editing.txt @@ -203,3 +203,43 @@ Note that you'll need to :ref:`decorate this view` using :func:`~django.contrib.auth.decorators.login_required`, or alternatively handle unauthorised users in the :meth:`form_valid()`. + +AJAX example +------------ + +Here is a simple example showing how you might go about implementing a form that +works for AJAX requests as well as 'normal' form POSTs:: + + import json + + from django.http import HttpResponse + from django.views.generic.edit import CreateView + from django.views.generic.detail import SingleObjectTemplateResponseMixin + + class AjaxableResponseMixin(object): + """ + Mixin to add AJAX support to a form. + Must be used with an object-based FormView (e.g. CreateView) + """ + def render_to_json_response(self, context, **response_kwargs): + data = json.dumps(context) + response_kwargs['content_type'] = 'application/json' + return HttpResponse(data, **response_kwargs) + + def form_invalid(self, form): + if self.request.is_ajax(): + return self.render_to_json_response(form.errors, status=400) + else: + return super(AjaxableResponseMixin, self).form_invalid(form) + + def form_valid(self, form): + if self.request.is_ajax(): + data = { + 'pk': form.instance.pk, + } + return self.render_to_json_response(data) + else: + return super(AjaxableResponseMixin, self).form_valid(form) + + class AuthorCreate(AjaxableResponseMixin, CreateView): + model = Author diff --git a/docs/topics/class-based-views/index.txt b/docs/topics/class-based-views/index.txt index 2d3e00ab4c..a738221892 100644 --- a/docs/topics/class-based-views/index.txt +++ b/docs/topics/class-based-views/index.txt @@ -2,8 +2,6 @@ Class-based views ================= -.. versionadded:: 1.3 - A view is a callable which takes a request and returns a response. This can be more than just a function, and Django provides an example of some classes which can be used as views. These allow you diff --git a/docs/topics/class-based-views/mixins.txt b/docs/topics/class-based-views/mixins.txt index f07769fb8a..f349c23626 100644 --- a/docs/topics/class-based-views/mixins.txt +++ b/docs/topics/class-based-views/mixins.txt @@ -2,8 +2,6 @@ Using mixins with class-based views =================================== -.. versionadded:: 1.3 - .. caution:: This is an advanced topic. A working knowledge of :doc:`Django's diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt index f29cc28332..84b4c84061 100644 --- a/docs/topics/db/models.txt +++ b/docs/topics/db/models.txt @@ -84,7 +84,9 @@ Fields The most important part of a model -- and the only required part of a model -- is the list of database fields it defines. Fields are specified by class -attributes. +attributes. Be careful not to choose field names that conflict with the +:doc:`models API ` like ``clean``, ``save``, or +``delete``. Example:: @@ -762,7 +764,7 @@ built-in model methods, adding new arguments. If you use ``*args, **kwargs`` in your method definitions, you are guaranteed that your code will automatically support those arguments when they are added. -.. admonition:: Overriding Delete +.. admonition:: Overridden model methods are not called on bulk operations Note that the :meth:`~Model.delete()` method for an object is not necessarily called when :ref:`deleting objects in bulk using a @@ -770,6 +772,13 @@ code will automatically support those arguments when they are added. gets executed, you can use :data:`~django.db.models.signals.pre_delete` and/or :data:`~django.db.models.signals.post_delete` signals. + Unfortunately, there isn't a workaround when + :meth:`creating` or + :meth:`updating` objects in bulk, + since none of :meth:`~Model.save()`, + :data:`~django.db.models.signals.pre_save`, and + :data:`~django.db.models.signals.post_save` are called. + Executing custom SQL -------------------- diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index 5385b2a72d..d826a39562 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -633,8 +633,6 @@ issue the query:: >>> Entry.objects.filter(authors__name=F('blog__name')) -.. versionadded:: 1.3 - For date and date/time fields, you can add or subtract a :class:`~datetime.timedelta` object. The following would return all entries that were modified more than 3 days after they were published:: @@ -642,6 +640,18 @@ that were modified more than 3 days after they were published:: >>> from datetime import timedelta >>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3)) +.. versionadded:: 1.5 + ``.bitand()`` and ``.bitor()`` + +The ``F()`` objects now support bitwise operations by ``.bitand()`` and +``.bitor()``, for example:: + + >>> F('somefield').bitand(16) + +.. versionchanged:: 1.5 + The previously undocumented operators ``&`` and ``|`` no longer produce + bitwise operations, use ``.bitand()`` and ``.bitor()`` instead. + The pk lookup shortcut ---------------------- @@ -876,10 +886,9 @@ it. For example:: # This will delete the Blog and all of its Entry objects. b.delete() -.. versionadded:: 1.3 - This cascade behavior is customizable via the - :attr:`~django.db.models.ForeignKey.on_delete` argument to the - :class:`~django.db.models.ForeignKey`. +This cascade behavior is customizable via the +:attr:`~django.db.models.ForeignKey.on_delete` argument to the +:class:`~django.db.models.ForeignKey`. Note that :meth:`~django.db.models.query.QuerySet.delete` is the only :class:`~django.db.models.query.QuerySet` method that is not exposed on a @@ -953,7 +962,8 @@ new value to be the new model instance you want to point to. For example:: >>> Entry.objects.all().update(blog=b) The ``update()`` method is applied instantly and returns the number of rows -affected by the query. The only restriction on the +matched by the query (which may not be equal to the number of rows updated if +some rows already have the new value). The only restriction on the :class:`~django.db.models.query.QuerySet` that is updated is that it can only access one database table, the model's main table. You can filter based on related fields, but you can only update columns in the model's main diff --git a/docs/topics/db/sql.txt b/docs/topics/db/sql.txt index 19daffd464..310dcb5ae6 100644 --- a/docs/topics/db/sql.txt +++ b/docs/topics/db/sql.txt @@ -242,7 +242,7 @@ By default, the Python DB API will return results without their field names, which means you end up with a ``list`` of values, rather than a ``dict``. At a small performance cost, you can return results as a ``dict`` by using something like this:: - + def dictfetchall(cursor): "Returns all rows from a cursor as a dict" desc = cursor.description @@ -256,7 +256,7 @@ Here is an example of the difference between the two:: >>> cursor.execute("SELECT id, parent_id from test LIMIT 2"); >>> cursor.fetchall() ((54360982L, None), (54360880L, None)) - + >>> cursor.execute("SELECT id, parent_id from test LIMIT 2"); >>> dictfetchall(cursor) [{'parent_id': None, 'id': 54360982L}, {'parent_id': None, 'id': 54360880L}] @@ -273,11 +273,6 @@ transaction containing those calls is closed correctly. See :ref:`the notes on the requirements of Django's transaction handling ` for more details. -.. versionchanged:: 1.3 - -Prior to Django 1.3, it was necessary to manually mark a transaction -as dirty using ``transaction.set_dirty()`` when using raw SQL calls. - Connections and cursors ----------------------- diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index 9928354664..4a52c5af35 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -66,9 +66,6 @@ database cursor (which is mapped to its own database connection internally). Controlling transaction management in views =========================================== -.. versionchanged:: 1.3 - Transaction management context managers are new in Django 1.3. - For most people, implicit request-based transactions work wonderfully. However, if you need more fine-grained control over how transactions are managed, you can use a set of functions in ``django.db.transaction`` to control transactions on a @@ -195,8 +192,6 @@ managers, too. Requirements for transaction handling ===================================== -.. versionadded:: 1.3 - Django requires that every transaction that is opened is closed before the completion of a request. If you are using :func:`autocommit` (the default commit mode) or :func:`commit_on_success`, this will be done diff --git a/docs/topics/email.txt b/docs/topics/email.txt index 0cc476e02c..b3d7254e7f 100644 --- a/docs/topics/email.txt +++ b/docs/topics/email.txt @@ -119,8 +119,6 @@ The "From:" header of the email will be the value of the This method exists for convenience and readability. -.. versionchanged:: 1.3 - If ``html_message`` is provided, the resulting email will be a :mimetype:`multipart/alternative` email with ``message`` as the :mimetype:`text/plain` content type and ``html_message`` as the @@ -236,9 +234,6 @@ following parameters (in the given order, if positional arguments are used). All parameters are optional and can be set at any time prior to calling the ``send()`` method. -.. versionchanged:: 1.3 - The ``cc`` argument was added. - * ``subject``: The subject line of the email. * ``body``: The body text. This should be a plain text message. diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index 2a83172e17..7c1771b758 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -35,19 +35,9 @@ display two blank forms:: >>> ArticleFormSet = formset_factory(ArticleForm, extra=2) -.. versionchanged:: 1.3 - -Prior to Django 1.3, formset instances were not iterable. To render -the formset you iterated over the ``forms`` attribute:: - - >>> formset = ArticleFormSet() - >>> for form in formset.forms: - ... print(form.as_table()) - -Iterating over ``formset.forms`` will render the forms in the order -they were created. The default formset iterator also renders the forms -in this order, but you can change this order by providing an alternate -implementation for the :meth:`__iter__()` method. +Iterating over the ``formset`` will render the forms in the order they were +created. You can change this order by providing an alternate implementation for +the :meth:`__iter__()` method. Formsets can also be indexed into, which returns the corresponding form. If you override ``__iter__``, you will need to also override ``__getitem__`` to have diff --git a/docs/topics/forms/media.txt b/docs/topics/forms/media.txt index 29a7829799..98e70e5e77 100644 --- a/docs/topics/forms/media.txt +++ b/docs/topics/forms/media.txt @@ -195,8 +195,6 @@ return values for dynamic media properties. Paths in media definitions -------------------------- -.. versionchanged:: 1.3 - Paths used to specify media can be either relative or absolute. If a path starts with ``/``, ``http://`` or ``https://``, it will be interpreted as an absolute path, and left as-is. All other paths will be prepended with the value diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index caff03c581..692be7cd7c 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -202,7 +202,7 @@ of cleaning the model you pass to the ``ModelForm`` constructor. For instance, calling ``is_valid()`` on your form will convert any date fields on your model to actual date objects. If form validation fails, only some of the updates may be applied. For this reason, you'll probably want to avoid reusing the -model instance. +model instance passed to the form, especially if validation fails. The ``save()`` method diff --git a/docs/topics/http/middleware.txt b/docs/topics/http/middleware.txt index fe92bc59a9..c27e7e8690 100644 --- a/docs/topics/http/middleware.txt +++ b/docs/topics/http/middleware.txt @@ -117,8 +117,6 @@ middleware is always called on every response. ``process_template_response`` ----------------------------- -.. versionadded:: 1.3 - .. method:: process_template_response(self, request, response) ``request`` is an :class:`~django.http.HttpRequest` object. ``response`` is a @@ -166,6 +164,23 @@ an earlier middleware method returned an :class:`~django.http.HttpResponse` classes are applied in reverse order, from the bottom up. This means classes defined at the end of :setting:`MIDDLEWARE_CLASSES` will be run first. +.. versionchanged:: 1.5 + ``response`` may also be an :class:`~django.http.StreamingHttpResponse` + object. + +Unlike :class:`~django.http.HttpResponse`, +:class:`~django.http.StreamingHttpResponse` does not have a ``content`` +attribute. As a result, middleware can no longer assume that all responses +will have a ``content`` attribute. If they need access to the content, they +must test for streaming responses and adjust their behavior accordingly:: + + if response.streaming: + response.streaming_content = wrap_streaming_content(response.streaming_content) + else: + response.content = wrap_content(response.content) + +``streaming_content`` should be assumed to be too large to hold in memory. +Middleware may wrap it in a new generator, but must not consume it. .. _exception-middleware: diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt index 1f55293413..15f9f7feba 100644 --- a/docs/topics/http/sessions.txt +++ b/docs/topics/http/sessions.txt @@ -524,6 +524,9 @@ consistently by all browsers. However, when it is honored, it can be a useful way to mitigate the risk of client side script accessing the protected cookie data. +.. versionchanged:: 1.4 + The default value of the setting was changed from ``False`` to ``True``. + .. _HTTPOnly: https://www.owasp.org/index.php/HTTPOnly SESSION_COOKIE_NAME diff --git a/docs/topics/http/shortcuts.txt b/docs/topics/http/shortcuts.txt index 10be353e80..0dc38b1459 100644 --- a/docs/topics/http/shortcuts.txt +++ b/docs/topics/http/shortcuts.txt @@ -17,8 +17,6 @@ introduce controlled coupling for convenience's sake. .. function:: render(request, template_name[, dictionary][, context_instance][, content_type][, status][, current_app]) - .. versionadded:: 1.3 - Combines a given template with a given context dictionary and returns an :class:`~django.http.HttpResponse` object with that rendered text. diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 4503bbd6ef..e178df2af2 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -2,8 +2,6 @@ URL dispatcher ============== -.. module:: django.core.urlresolvers - A clean, elegant URL scheme is an important detail in a high-quality Web application. Django lets you design URLs however you want, with no framework limitations. @@ -55,7 +53,8 @@ algorithm the system follows to determine which Python code to execute: one that matches the requested URL. 4. Once one of the regexes matches, Django imports and calls the given - view, which is a simple Python function. The view gets passed an + view, which is a simple Python function (or a :doc:`class based view + `). The view gets passed an :class:`~django.http.HttpRequest` as its first argument and any values captured in the regex as remaining arguments. @@ -159,7 +158,8 @@ vs. non-named groups in a regular expression: 2. Otherwise, it will pass all non-named arguments as positional arguments. -In both cases, any extra keyword arguments that have been given as per `Passing extra options to view functions`_ (below) will also be passed to the view. +In both cases, any extra keyword arguments that have been given as per `Passing +extra options to view functions`_ (below) will also be passed to the view. What the URLconf searches against ================================= @@ -214,7 +214,6 @@ Performance Each regular expression in a ``urlpatterns`` is compiled the first time it's accessed. This makes the system blazingly fast. - Syntax of the urlpatterns variable ================================== @@ -222,154 +221,35 @@ Syntax of the urlpatterns variable :func:`django.conf.urls.patterns`. Always use ``patterns()`` to create the ``urlpatterns`` variable. -``django.conf.urls`` utility functions -====================================== - -.. module:: django.conf.urls - -.. deprecated:: 1.4 - Starting with Django 1.4 functions ``patterns``, ``url``, ``include`` plus - the ``handler*`` symbols described below live in the ``django.conf.urls`` - module. - - Until Django 1.3 they were located in ``django.conf.urls.defaults``. You - still can import them from there but it will be removed in Django 1.6. - -patterns --------- - -.. function:: patterns(prefix, pattern_description, ...) - -A function that takes a prefix, and an arbitrary number of URL patterns, and -returns a list of URL patterns in the format Django needs. - -The first argument to ``patterns()`` is a string ``prefix``. See -`The view prefix`_ below. - -The remaining arguments should be tuples in this format:: - - (regular expression, Python callback function [, optional_dictionary [, optional_name]]) - -The ``optional_dictionary`` and ``optional_name`` parameters are described in -`Passing extra options to view functions`_ below. - -.. note:: - Because `patterns()` is a function call, it accepts a maximum of 255 - arguments (URL patterns, in this case). This is a limit for all Python - function calls. This is rarely a problem in practice, because you'll - typically structure your URL patterns modularly by using `include()` - sections. However, on the off-chance you do hit the 255-argument limit, - realize that `patterns()` returns a Python list, so you can split up the - construction of the list. - - :: - - urlpatterns = patterns('', - ... - ) - urlpatterns += patterns('', - ... - ) - - Python lists have unlimited size, so there's no limit to how many URL - patterns you can construct. The only limit is that you can only create 254 - at a time (the 255th argument is the initial prefix argument). - -url ---- - -.. function:: url(regex, view, kwargs=None, name=None, prefix='') - -You can use the ``url()`` function, instead of a tuple, as an argument to -``patterns()``. This is convenient if you want to specify a name without the -optional extra arguments dictionary. For example:: - - urlpatterns = patterns('', - url(r'^index/$', index_view, name="main-view"), - ... - ) - -This function takes five arguments, most of which are optional:: - - url(regex, view, kwargs=None, name=None, prefix='') - -See `Naming URL patterns`_ for why the ``name`` parameter is useful. - -The ``prefix`` parameter has the same meaning as the first argument to -``patterns()`` and is only relevant when you're passing a string as the -``view`` parameter. - -include -------- - -.. function:: include() - -A function that takes a full Python import path to another URLconf module that -should be "included" in this place. - -:func:`include` also accepts as an argument an iterable that returns URL -patterns. - -See `Including other URLconfs`_ below. - Error handling ============== When Django can't find a regex matching the requested URL, or when an -exception is raised, Django will invoke an error-handling view. The -views to use for these cases are specified by three variables which can -be set in your root URLconf. Setting these variables in any other -URLconf will have no effect. +exception is raised, Django will invoke an error-handling view. + +The views to use for these cases are specified by three variables. Their +default values should suffice for most projects, but further customization is +possible by assigning values to them. See the documentation on :ref:`customizing error views -` for more details. +` for the full details. -handler403 ----------- +Such values can be set in your root URLconf. Setting these variables in any +other URLconf will have no effect. -.. data:: handler403 +Values must be callables, or strings representing the full Python import path +to the view that should be called to handle the error condition at hand. -A callable, or a string representing the full Python import path to the view -that should be called if the user doesn't have the permissions required to -access a resource. +The variables are: -By default, this is ``'django.views.defaults.permission_denied'``. That default -value should suffice. - -See the documentation about :ref:`the 403 (HTTP Forbidden) view -` for more information. +* ``handler404`` -- See :data:`django.conf.urls.handler404`. +* ``handler500`` -- See :data:`django.conf.urls.handler500`. +* ``handler403`` -- See :data:`django.conf.urls.handler403`. .. versionadded:: 1.4 ``handler403`` is new in Django 1.4. -handler404 ----------- - -.. data:: handler404 - -A callable, or a string representing the full Python import path to the view -that should be called if none of the URL patterns match. - -By default, this is ``'django.views.defaults.page_not_found'``. That default -value should suffice. - -See the documentation about :ref:`the 404 (HTTP Not Found) view -` for more information. - -handler500 ----------- - -.. data:: handler500 - -A callable, or a string representing the full Python import path to the view -that should be called in case of server errors. Server errors happen when you -have runtime errors in view code. - -By default, this is ``'django.views.defaults.server_error'``. That default -value should suffice. - -See the documentation about :ref:`the 500 (HTTP Internal Server Error) view -` for more information. +.. _urlpatterns-view-prefix: The view prefix =============== @@ -436,6 +316,8 @@ New:: (r'^tag/(?P\w+)/$', 'tag'), ) +.. _including-other-urlconfs: + Including other URLconfs ======================== @@ -445,7 +327,7 @@ essentially "roots" a set of URLs below other ones. For example, here's an excerpt of the URLconf for the `Django Web site`_ itself. It includes a number of other URLconfs:: - from django.conf.urls import patterns, url, include + from django.conf.urls import patterns, include urlpatterns = patterns('', # ... snip ... @@ -458,34 +340,30 @@ itself. It includes a number of other URLconfs:: Note that the regular expressions in this example don't have a ``$`` (end-of-string match character) but do include a trailing slash. Whenever -Django encounters ``include()``, it chops off whatever part of the URL matched -up to that point and sends the remaining string to the included URLconf for -further processing. +Django encounters ``include()`` (:func:`django.conf.urls.include()`), it chops +off whatever part of the URL matched up to that point and sends the remaining +string to the included URLconf for further processing. Another possibility is to include additional URL patterns not by specifying the -URLconf Python module defining them as the `include`_ argument but by using -directly the pattern list as returned by `patterns`_ instead. For example:: +URLconf Python module defining them as the ``include()`` argument but by using +directly the pattern list as returned by :func:`~django.conf.urls.patterns` +instead. For example, consider this URLconf:: from django.conf.urls import patterns, url, include extra_patterns = patterns('', - url(r'^reports/(?P\d+)/$', 'credit.views.report', name='credit-reports'), - url(r'^charge/$', 'credit.views.charge', name='credit-charge'), + url(r'^reports/(?P\d+)/$', 'credit.views.report'), + url(r'^charge/$', 'credit.views.charge'), ) urlpatterns = patterns('', - url(r'^$', 'apps.main.views.homepage', name='site-homepage'), + url(r'^$', 'apps.main.views.homepage'), (r'^help/', include('apps.help.urls')), (r'^credit/', include(extra_patterns)), ) -This approach can be seen in use when you deploy an instance of the Django -Admin application. The Django Admin is deployed as instances of a -:class:`~django.contrib.admin.AdminSite`; each -:class:`~django.contrib.admin.AdminSite` instance has an attribute ``urls`` -that returns the url patterns available to that instance. It is this attribute -that you ``include()`` into your projects ``urlpatterns`` when you deploy the -admin instance. +In this example, the ``/credit/reports/`` URL will be handled by the +``credit.views.report()`` Django view. .. _`Django Web site`: https://www.djangoproject.com/ @@ -509,57 +387,7 @@ the following example is valid:: In the above example, the captured ``"username"`` variable is passed to the included URLconf, as expected. -.. _topics-http-defining-url-namespaces: - -Defining URL namespaces ------------------------ - -When you need to deploy multiple instances of a single application, it can be -helpful to be able to differentiate between instances. This is especially -important when using :ref:`named URL patterns `, since -multiple instances of a single application will share named URLs. Namespaces -provide a way to tell these named URLs apart. - -A URL namespace comes in two parts, both of which are strings: - -* An **application namespace**. This describes the name of the application - that is being deployed. Every instance of a single application will have - the same application namespace. For example, Django's admin application - has the somewhat predictable application namespace of ``admin``. - -* An **instance namespace**. This identifies a specific instance of an - application. Instance namespaces should be unique across your entire - project. However, an instance namespace can be the same as the - application namespace. This is used to specify a default instance of an - application. For example, the default Django Admin instance has an - instance namespace of ``admin``. - -URL Namespaces can be specified in two ways. - -Firstly, you can provide the application and instance namespace as arguments -to ``include()`` when you construct your URL patterns. For example,:: - - (r'^help/', include('apps.help.urls', namespace='foo', app_name='bar')), - -This will include the URLs defined in ``apps.help.urls`` into the application -namespace ``bar``, with the instance namespace ``foo``. - -Secondly, you can include an object that contains embedded namespace data. If -you ``include()`` a ``patterns`` object, that object will be added to the -global namespace. However, you can also ``include()`` an object that contains -a 3-tuple containing:: - - (, , ) - -This will include the nominated URL patterns into the given application and -instance namespace. For example, the ``urls`` attribute of Django's -:class:`~django.contrib.admin.AdminSite` object returns a 3-tuple that contains -all the patterns in an admin site, plus the name of the admin instance, and the -application namespace ``admin``. - -Once you have defined namespaced URLs, you can reverse them. For details on -reversing namespaced urls, see the documentation on :ref:`reversing namespaced -URLs `. +.. _views-extra-options: Passing extra options to view functions ======================================= @@ -593,9 +421,9 @@ options to views. Passing extra options to ``include()`` -------------------------------------- -Similarly, you can pass extra options to ``include()``. When you pass extra -options to ``include()``, *each* line in the included URLconf will be passed -the extra options. +Similarly, you can pass extra options to :func:`~django.conf.urls.include`. +When you pass extra options to ``include()``, *each* line in the included +URLconf will be passed the extra options. For example, these two URLconf sets are functionally identical: @@ -673,6 +501,112 @@ The style you use is up to you. Note that if you use this technique -- passing objects rather than strings -- the view prefix (as explained in "The view prefix" above) will have no effect. +Note that :doc:`class based views` must be +imported:: + + from mysite.views import ClassBasedView + + urlpatterns = patterns('', + (r'^myview/$', ClassBasedView.as_view()), + ) + +Reverse resolution of URLs +========================== + +A common need when working on a Django project is the possibility to obtain URLs +in their final forms either for embedding in generated content (views and assets +URLs, URLs shown to the user, etc.) or for handling of the navigation flow on +the server side (redirections, etc.) + +It is strongly desirable not having to hard-code these URLs (a laborious, +non-scalable and error-prone strategy) or having to devise ad-hoc mechanisms for +generating URLs that are parallel to the design described by the URLconf and as +such in danger of producing stale URLs at some point. + +In other words, what's needed is a DRY mechanism. Among other advantages it +would allow evolution of the URL design without having to go all over the +project source code to search and replace outdated URLs. + +The piece of information we have available as a starting point to get a URL is +an identification (e.g. the name) of the view in charge of handling it, other +pieces of information that necessarily must participate in the lookup of the +right URL are the types (positional, keyword) and values of the view arguments. + +Django provides a solution such that the URL mapper is the only repository of +the URL design. You feed it with your URLconf and then it can be used in both +directions: + +* Starting with a URL requested by the user/browser, it calls the right Django + view providing any arguments it might need with their values as extracted from + the URL. + +* Starting with the identification of the corresponding Django view plus the + values of arguments that would be passed to it, obtain the associated URL. + +The first one is the usage we've been discussing in the previous sections. The +second one is what is known as *reverse resolution of URLs*, *reverse URL +matching*, *reverse URL lookup*, or simply *URL reversing*. + +Django provides tools for performing URL reversing that match the different +layers where URLs are needed: + +* In templates: Using the :ttag:`url` template tag. + +* In Python code: Using the :func:`django.core.urlresolvers.reverse()` + function. + +* In higher level code related to handling of URLs of Django model instances: + The :meth:`django.db.models.Model.get_absolute_url()` method and the + :func:`django.db.models.permalink` decorator. + +Examples +-------- + +Consider again this URLconf entry:: + + from django.conf.urls import patterns, url + + urlpatterns = patterns('', + #... + url(r'^articles/(\d{4})/$', 'news.views.year_archive'), + #... + ) + +According to this design, the URL for the archive corresponding to year *nnnn* +is ``/articles/nnnn/``. + +You can obtain these in template code by using: + +.. code-block:: html+django + + 2012 Archive + {# Or with the year in a template context variable: #} + + +Or in Python code:: + + from django.core.urlresolvers import reverse + from django.http import HttpResponseRedirect + + def redirect_to_year(request): + # ... + year = 2006 + # ... + return HttpResponseRedirect(reverse('news.views.year_archive', args=(year,))) + +If, for some reason, it was decided that the URLs where content for yearly +article archives are published at should be changed then you would only need to +change the entry in the URLconf. + +In some scenarios where views are of a generic nature, a many-to-one +relationship might exist between URLs and views. For these cases the view name +isn't a good enough identificator for it when it comes the time of reversing +URLs. Read the next section to know about the solution Django provides for this. + .. _naming-url-patterns: Naming URL patterns @@ -688,10 +622,10 @@ view:: ) This is completely valid, but it leads to problems when you try to do reverse -URL matching (through the ``permalink()`` decorator or the :ttag:`url` template -tag). Continuing this example, if you wanted to retrieve the URL for the -``archive`` view, Django's reverse URL matcher would get confused, because *two* -URL patterns point at that view. +URL matching (through the :func:`~django.db.models.permalink` decorator or the +:ttag:`url` template tag). Continuing this example, if you wanted to retrieve +the URL for the ``archive`` view, Django's reverse URL matcher would get +confused, because *two* URL patterns point at that view. To solve this problem, Django supports **named URL patterns**. That is, you can give a name to a URL pattern in order to distinguish it from other patterns @@ -731,24 +665,55 @@ not restricted to valid Python names. name, will decrease the chances of collision. We recommend something like ``myapp-comment`` instead of ``comment``. -.. _topics-http-reversing-url-namespaces: +.. _topics-http-defining-url-namespaces: URL namespaces --------------- +============== -Namespaced URLs are specified using the ``:`` operator. For example, the main -index page of the admin application is referenced using ``admin:index``. This -indicates a namespace of ``admin``, and a named URL of ``index``. +Introduction +------------ -Namespaces can also be nested. The named URL ``foo:bar:whiz`` would look for -a pattern named ``whiz`` in the namespace ``bar`` that is itself defined within -the top-level namespace ``foo``. +When you need to deploy multiple instances of a single application, it can be +helpful to be able to differentiate between instances. This is especially +important when using :ref:`named URL patterns `, since +multiple instances of a single application will share named URLs. Namespaces +provide a way to tell these named URLs apart. -When given a namespaced URL (e.g. ``myapp:index``) to resolve, Django splits +A URL namespace comes in two parts, both of which are strings: + +.. glossary:: + + application namespace + This describes the name of the application that is being deployed. Every + instance of a single application will have the same application namespace. + For example, Django's admin application has the somewhat predictable + application namespace of ``'admin'``. + + instance namespace + This identifies a specific instance of an application. Instance namespaces + should be unique across your entire project. However, an instance namespace + can be the same as the application namespace. This is used to specify a + default instance of an application. For example, the default Django Admin + instance has an instance namespace of ``'admin'``. + +Namespaced URLs are specified using the ``':'`` operator. For example, the main +index page of the admin application is referenced using ``'admin:index'``. This +indicates a namespace of ``'admin'``, and a named URL of ``'index'``. + +Namespaces can also be nested. The named URL ``'foo:bar:whiz'`` would look for +a pattern named ``'whiz'`` in the namespace ``'bar'`` that is itself defined +within the top-level namespace ``'foo'``. + +.. _topics-http-reversing-url-namespaces: + +Reversing namespaced URLs +------------------------- + +When given a namespaced URL (e.g. ``'myapp:index'``) to resolve, Django splits the fully qualified name into parts, and then tries the following lookup: -1. First, Django looks for a matching application namespace (in this - example, ``myapp``). This will yield a list of instances of that +1. First, Django looks for a matching :term:`application namespace` (in this + example, ``'myapp'``). This will yield a list of instances of that application. 2. If there is a *current* application defined, Django finds and returns @@ -759,265 +724,101 @@ the fully qualified name into parts, and then tries the following lookup: render a template. The current application can also be specified manually as an argument - to the :func:`reverse()` function. + to the :func:`django.core.urlresolvers.reverse()` function. 3. If there is no current application. Django looks for a default application instance. The default application instance is the instance - that has an instance namespace matching the application namespace (in - this example, an instance of the ``myapp`` called ``myapp``). + that has an :term:`instance namespace` matching the :term:`application + namespace` (in this example, an instance of the ``myapp`` called + ``'myapp'``). 4. If there is no default application instance, Django will pick the last deployed instance of the application, whatever its instance name may be. -5. If the provided namespace doesn't match an application namespace in +5. If the provided namespace doesn't match an :term:`application namespace` in step 1, Django will attempt a direct lookup of the namespace as an - instance namespace. + :term:`instance namespace`. If there are nested namespaces, these steps are repeated for each part of the namespace until only the view name is unresolved. The view name will then be resolved into a URL in the namespace that has been found. +Example +~~~~~~~ + To show this resolution strategy in action, consider an example of two instances -of ``myapp``: one called ``foo``, and one called ``bar``. ``myapp`` has a main -index page with a URL named `index`. Using this setup, the following lookups are -possible: +of ``myapp``: one called ``'foo'``, and one called ``'bar'``. ``myapp`` has a +main index page with a URL named ``'index'``. Using this setup, the following +lookups are possible: * If one of the instances is current - say, if we were rendering a utility page - in the instance ``bar`` - ``myapp:index`` will resolve to the index page of - the instance ``bar``. + in the instance ``'bar'`` - ``'myapp:index'`` will resolve to the index page + of the instance ``'bar'``. * If there is no current instance - say, if we were rendering a page - somewhere else on the site - ``myapp:index`` will resolve to the last + somewhere else on the site - ``'myapp:index'`` will resolve to the last registered instance of ``myapp``. Since there is no default instance, the last instance of ``myapp`` that is registered will be used. This could - be ``foo`` or ``bar``, depending on the order they are introduced into the + be ``'foo'`` or ``'bar'``, depending on the order they are introduced into the urlpatterns of the project. -* ``foo:index`` will always resolve to the index page of the instance ``foo``. +* ``'foo:index'`` will always resolve to the index page of the instance + ``'foo'``. -If there was also a default instance - i.e., an instance named `myapp` - the +If there was also a default instance - i.e., an instance named ``'myapp'`` - the following would happen: * If one of the instances is current - say, if we were rendering a utility page - in the instance ``bar`` - ``myapp:index`` will resolve to the index page of - the instance ``bar``. + in the instance ``'bar'`` - ``'myapp:index'`` will resolve to the index page + of the instance ``'bar'``. * If there is no current instance - say, if we were rendering a page somewhere - else on the site - ``myapp:index`` will resolve to the index page of the + else on the site - ``'myapp:index'`` will resolve to the index page of the default instance. -* ``foo:index`` will again resolve to the index page of the instance ``foo``. +* ``'foo:index'`` will again resolve to the index page of the instance + ``'foo'``. +.. _namespaces-and-include: -``django.core.urlresolvers`` utility functions -============================================== +URL namespaces and included URLconfs +------------------------------------ -.. currentmodule:: django.core.urlresolvers +URL namespaces of included URLconfs can be specified in two ways. -reverse() ---------- +Firstly, you can provide the :term:`application ` and +:term:`instance ` namespaces as arguments to +:func:`django.conf.urls.include()` when you construct your URL patterns. For +example,:: -If you need to use something similar to the :ttag:`url` template tag in -your code, Django provides the following function (in the -:mod:`django.core.urlresolvers` module): + (r'^help/', include('apps.help.urls', namespace='foo', app_name='bar')), -.. function:: reverse(viewname, [urlconf=None, args=None, kwargs=None, current_app=None]) +This will include the URLs defined in ``apps.help.urls`` into the +:term:`application namespace` ``'bar'``, with the :term:`instance namespace` +``'foo'``. -``viewname`` is either the function name (either a function reference, or the -string version of the name, if you used that form in ``urlpatterns``) or the -`URL pattern name`_. Normally, you won't need to worry about the -``urlconf`` parameter and will only pass in the positional and keyword -arguments to use in the URL matching. For example:: +Secondly, you can include an object that contains embedded namespace data. If +you ``include()`` an object as returned by :func:`~django.conf.urls.patterns`, +the URLs contained in that object will be added to the global namespace. +However, you can also ``include()`` a 3-tuple containing:: - from django.core.urlresolvers import reverse + (, , ) - def myview(request): - return HttpResponseRedirect(reverse('arch-summary', args=[1945])) +For example:: -.. _URL pattern name: `Naming URL patterns`_ + help_patterns = patterns('', + url(r'^basic/$', 'apps.help.views.views.basic'), + url(r'^advanced/$', 'apps.help.views.views.advanced'), + ) -The ``reverse()`` function can reverse a large variety of regular expression -patterns for URLs, but not every possible one. The main restriction at the -moment is that the pattern cannot contain alternative choices using the -vertical bar (``"|"``) character. You can quite happily use such patterns for -matching against incoming URLs and sending them off to views, but you cannot -reverse such patterns. + (r'^help/', include(help_patterns, 'bar', 'foo')), -The ``current_app`` argument allows you to provide a hint to the resolver -indicating the application to which the currently executing view belongs. -This ``current_app`` argument is used as a hint to resolve application -namespaces into URLs on specific application instances, according to the -:ref:`namespaced URL resolution strategy `. +This will include the nominated URL patterns into the given application and +instance namespace. -You can use ``kwargs`` instead of ``args``. For example:: - - >>> reverse('admin:app_list', kwargs={'app_label': 'auth'}) - '/admin/auth/' - -``args`` and ``kwargs`` cannot be passed to ``reverse()`` at the same time. - -.. admonition:: Make sure your views are all correct. - - As part of working out which URL names map to which patterns, the - ``reverse()`` function has to import all of your URLconf files and examine - the name of each view. This involves importing each view function. If - there are *any* errors whilst importing any of your view functions, it - will cause ``reverse()`` to raise an error, even if that view function is - not the one you are trying to reverse. - - Make sure that any views you reference in your URLconf files exist and can - be imported correctly. Do not include lines that reference views you - haven't written yet, because those views will not be importable. - -.. note:: - - The string returned by :meth:`~django.core.urlresolvers.reverse` is already - :ref:`urlquoted `. For example:: - - >>> reverse('cities', args=[u'Orléans']) - '.../Orl%C3%A9ans/' - - Applying further encoding (such as :meth:`~django.utils.http.urlquote` or - ``urllib.quote``) to the output of :meth:`~django.core.urlresolvers.reverse` - may produce undesirable results. - -reverse_lazy() --------------- - -.. versionadded:: 1.4 - -A lazily evaluated version of `reverse()`_. - -.. function:: reverse_lazy(viewname, [urlconf=None, args=None, kwargs=None, current_app=None]) - -It is useful for when you need to use a URL reversal before your project's -URLConf is loaded. Some common cases where this function is necessary are: - -* providing a reversed URL as the ``url`` attribute of a generic class-based - view. - -* providing a reversed URL to a decorator (such as the ``login_url`` argument - for the :func:`django.contrib.auth.decorators.permission_required` - decorator). - -* providing a reversed URL as a default value for a parameter in a function's - signature. - -resolve() ---------- - -The :func:`django.core.urlresolvers.resolve` function can be used for -resolving URL paths to the corresponding view functions. It has the -following signature: - -.. function:: resolve(path, urlconf=None) - -``path`` is the URL path you want to resolve. As with -:func:`~django.core.urlresolvers.reverse`, you don't need to -worry about the ``urlconf`` parameter. The function returns a -:class:`ResolverMatch` object that allows you -to access various meta-data about the resolved URL. - -If the URL does not resolve, the function raises an -:class:`~django.http.Http404` exception. - -.. class:: ResolverMatch - - .. attribute:: ResolverMatch.func - - The view function that would be used to serve the URL - - .. attribute:: ResolverMatch.args - - The arguments that would be passed to the view function, as - parsed from the URL. - - .. attribute:: ResolverMatch.kwargs - - The keyword arguments that would be passed to the view - function, as parsed from the URL. - - .. attribute:: ResolverMatch.url_name - - The name of the URL pattern that matches the URL. - - .. attribute:: ResolverMatch.app_name - - The application namespace for the URL pattern that matches the - URL. - - .. attribute:: ResolverMatch.namespace - - The instance namespace for the URL pattern that matches the - URL. - - .. attribute:: ResolverMatch.namespaces - - The list of individual namespace components in the full - instance namespace for the URL pattern that matches the URL. - i.e., if the namespace is ``foo:bar``, then namespaces will be - ``['foo', 'bar']``. - -A :class:`ResolverMatch` object can then be interrogated to provide -information about the URL pattern that matches a URL:: - - # Resolve a URL - match = resolve('/some/path/') - # Print the URL pattern that matches the URL - print(match.url_name) - -A :class:`ResolverMatch` object can also be assigned to a triple:: - - func, args, kwargs = resolve('/some/path/') - -.. versionchanged:: 1.3 - Triple-assignment exists for backwards-compatibility. Prior to - Django 1.3, :func:`~django.core.urlresolvers.resolve` returned a - triple containing (view function, arguments, keyword arguments); - the :class:`ResolverMatch` object (as well as the namespace and pattern - information it provides) is not available in earlier Django releases. - -One possible use of :func:`~django.core.urlresolvers.resolve` would be to test -whether a view would raise a ``Http404`` error before redirecting to it:: - - from urlparse import urlparse - from django.core.urlresolvers import resolve - from django.http import HttpResponseRedirect, Http404 - - def myview(request): - next = request.META.get('HTTP_REFERER', None) or '/' - response = HttpResponseRedirect(next) - - # modify the request and response as required, e.g. change locale - # and set corresponding locale cookie - - view, args, kwargs = resolve(urlparse(next)[2]) - kwargs['request'] = request - try: - view(*args, **kwargs) - except Http404: - return HttpResponseRedirect('/') - return response - - -permalink() ------------ - -The :func:`django.db.models.permalink` decorator is useful for writing short -methods that return a full URL path. For example, a model's -``get_absolute_url()`` method. See :func:`django.db.models.permalink` for more. - -get_script_prefix() -------------------- - -.. function:: get_script_prefix() - -Normally, you should always use :func:`~django.core.urlresolvers.reverse` or -:func:`~django.db.models.permalink` to define URLs within your application. -However, if your application constructs part of the URL hierarchy itself, you -may occasionally need to generate URLs. In that case, you need to be able to -find the base URL of the Django project within its Web server -(normally, :func:`~django.core.urlresolvers.reverse` takes care of this for -you). In that case, you can call ``get_script_prefix()``, which will return the -script prefix portion of the URL for your Django project. If your Django -project is at the root of its Web server, this is always ``"/"``. +For example, the Django Admin is deployed as instances of +:class:`~django.contrib.admin.AdminSite`. ``AdminSite`` objects have a ``urls`` +attribute: A 3-tuple that contains all the patterns in the corresponding admin +site, plus the application namespace ``'admin'``, and the name of the admin +instance. It is this ``urls`` attribute that you ``include()`` into your +projects ``urlpatterns`` when you deploy an Admin instance. diff --git a/docs/topics/http/views.txt b/docs/topics/http/views.txt index c4bd15e72e..7c4d1bbb6e 100644 --- a/docs/topics/http/views.txt +++ b/docs/topics/http/views.txt @@ -134,13 +134,12 @@ The 404 (page not found) view When you raise an ``Http404`` exception, Django loads a special view devoted to handling 404 errors. By default, it's the view -``django.views.defaults.page_not_found``, which loads and renders the template -``404.html``. +``django.views.defaults.page_not_found``, which either produces a very simple +"Not Found" message or loads and renders the template ``404.html`` if you +created it in your root template directory. -This means you need to define a ``404.html`` template in your root template -directory. This template will be used for all 404 errors. The default 404 view -will pass one variable to the template: ``request_path``, which is the URL -that resulted in the error. +The default 404 view will pass one variable to the template: ``request_path``, +which is the URL that resulted in the error. The ``page_not_found`` view should suffice for 99% of Web applications, but if you want to override it, you can specify ``handler404`` in your URLconf, like @@ -152,15 +151,11 @@ Behind the scenes, Django determines the 404 view by looking for ``handler404`` in your root URLconf, and falling back to ``django.views.defaults.page_not_found`` if you did not define one. -Four things to note about 404 views: +Three things to note about 404 views: * The 404 view is also called if Django doesn't find a match after checking every regular expression in the URLconf. -* If you don't define your own 404 view — and simply use the default, - which is recommended — you still have one obligation: you must create a - ``404.html`` template in the root of your template directory. - * The 404 view is passed a :class:`~django.template.RequestContext` and will have access to variables supplied by your :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting (e.g., ``MEDIA_URL``). @@ -176,13 +171,12 @@ The 500 (server error) view Similarly, Django executes special-case behavior in the case of runtime errors in view code. If a view results in an exception, Django will, by default, call -the view ``django.views.defaults.server_error``, which loads and renders the -template ``500.html``. +the view ``django.views.defaults.server_error``, which either produces a very +simple "Server Error" message or loads and renders the template ``500.html`` if +you created it in your root template directory. -This means you need to define a ``500.html`` template in your root template -directory. This template will be used for all server errors. The default 500 -view passes no variables to this template and is rendered with an empty -``Context`` to lessen the chance of additional errors. +The default 500 view passes no variables to the ``500.html`` template and is +rendered with an empty ``Context`` to lessen the chance of additional errors. This ``server_error`` view should suffice for 99% of Web applications, but if you want to override the view, you can specify ``handler500`` in your URLconf, @@ -194,11 +188,7 @@ Behind the scenes, Django determines the 500 view by looking for ``handler500`` in your root URLconf, and falling back to ``django.views.defaults.server_error`` if you did not define one. -Two things to note about 500 views: - -* If you don't define your own 500 view — and simply use the default, - which is recommended — you still have one obligation: you must create a - ``500.html`` template in the root of your template directory. +One thing to note about 500 views: * If :setting:`DEBUG` is set to ``True`` (in your settings module), then your 500 view will never be used, and the traceback will be displayed diff --git a/docs/topics/i18n/formatting.txt b/docs/topics/i18n/formatting.txt index b09164769e..fc3f37de32 100644 --- a/docs/topics/i18n/formatting.txt +++ b/docs/topics/i18n/formatting.txt @@ -80,8 +80,6 @@ Template tags localize ~~~~~~~~ -.. versionadded:: 1.3 - Enables or disables localization of template variables in the contained block. @@ -116,8 +114,6 @@ Template filters localize ~~~~~~~~ -.. versionadded:: 1.3 - Forces localization of a single value. For example:: @@ -136,8 +132,6 @@ tag. unlocalize ~~~~~~~~~~ -.. versionadded:: 1.3 - Forces a single value to be printed without localization. For example:: diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index a7f48fe1fd..65c6fe2445 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -134,8 +134,6 @@ translations wouldn't be able to reorder placeholder text. Comments for translators ------------------------ -.. versionadded:: 1.3 - If you would like to give translators hints about a translatable string, you can add a comment prefixed with the ``Translators`` keyword on the line preceding the string, e.g.:: @@ -255,8 +253,6 @@ cardinality of the elements at play. Contextual markers ------------------ -.. versionadded:: 1.3 - Sometimes words have several meanings, such as ``"May"`` in English, which refers to a month name and to a verb. To enable translators to translate these words correctly in different contexts, you can use the @@ -431,13 +427,29 @@ In this case, the lazy translations in ``result`` will only be converted to strings when ``result`` itself is used in a string (usually at template rendering time). +Other uses of lazy in delayed translations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For any other case where you would like to delay the translation, but have to +pass the translatable string as argument to another function, you can wrap +this function inside a lazy call yourself. For example:: + + from django.utils import six # Python 3 compatibility + from django.utils.functional import lazy + from django.utils.safestring import mark_safe + from django.utils.translation import ugettext_lazy as _ + + mark_safe_lazy = lazy(mark_safe, six.text_type) + +And then later:: + + lazy_string = mark_safe_lazy(_("

    My string!

    ")) + Localized names of languages ---------------------------- .. function:: get_language_info -.. versionadded:: 1.3 - The ``get_language_info()`` function provides detailed information about languages:: @@ -535,9 +547,6 @@ using the ``context`` keyword: ``blocktrans`` template tag --------------------------- -.. versionchanged:: 1.3 - New keyword argument format. - Contrarily to the :ttag:`trans` tag, the ``blocktrans`` tag allows you to mark complex sentences consisting of literals and variable content for translation by making use of placeholders:: @@ -664,8 +673,6 @@ string, so they don't need to be aware of translations. translator might translate the string ``"yes,no"`` as ``"ja,nein"`` (keeping the comma intact). -.. versionadded:: 1.3 - You can also retrieve information about any of the available languages using provided template tags and filters. To get information about a single language, use the ``{% get_language_info %}`` tag:: @@ -787,10 +794,6 @@ directories listed in :setting:`LOCALE_PATHS` have the highest precedence with the ones appearing first having higher precedence than the ones appearing later. -.. versionchanged:: 1.3 - Directories listed in :setting:`LOCALE_PATHS` weren't included in the - lookup algorithm until version 1.3. - Using the JavaScript translation catalog ---------------------------------------- diff --git a/docs/topics/install.txt b/docs/topics/install.txt index 39b9a93c04..52994ed16a 100644 --- a/docs/topics/install.txt +++ b/docs/topics/install.txt @@ -80,7 +80,14 @@ Get your database running If you plan to use Django's database API functionality, you'll need to make sure a database server is running. Django supports many different database servers and is officially supported with PostgreSQL_, MySQL_, Oracle_ and -SQLite_ (although SQLite doesn't require a separate server to be running). +SQLite_. + +If you are developing a simple project or something you don't plan to deploy +in a production environment, SQLite is generally the simplest option as it +doesn't require running a separate server. However, SQLite has many differences +from other databases, so if you are working on something substantial, it's +recommended to develop with the same database as you plan on using in +production. In addition to the officially supported databases, there are backends provided by 3rd parties that allow you to use other databases with Django: diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt index 28baf87522..7bd56e92ec 100644 --- a/docs/topics/logging.txt +++ b/docs/topics/logging.txt @@ -2,8 +2,6 @@ Logging ======= -.. versionadded:: 1.3 - .. module:: django.utils.log :synopsis: Logging tools for Django applications @@ -194,6 +192,8 @@ There are two other logging calls available: * ``logger.exception()``: Creates an ``ERROR`` level logging message wrapping the current exception stack frame. +.. _configuring-logging: + Configuring logging =================== @@ -218,6 +218,14 @@ handlers, filters and formatters that you want in your logging setup, and the log levels and other properties that you want those components to have. +Prior to Django 1.5, the :setting:`LOGGING` setting overwrote the :ref:`default +Django logging configuration `. From Django +1.5 forward, the project's logging configuration is merged with Django's +defaults, hence you can decide if you want to add to, or replace the existing +configuration. To completely override the default configuration, set the +``disable_existing_loggers`` key to True in the :setting:`LOGGING` +dictConfig. Alternatively you can redefine some or all of the loggers. + Logging is configured as soon as settings have been loaded (either manually using :func:`~django.conf.settings.configure` or when at least one setting is accessed). Since the loading of settings is one of the first @@ -347,36 +355,6 @@ This logging configuration does the following things: printed to the console; ``ERROR`` and ``CRITICAL`` messages will also be output via email. -.. admonition:: Custom handlers and circular imports - - If your ``settings.py`` specifies a custom handler class and the file - defining that class also imports ``settings.py`` a circular import will - occur. - - For example, if ``settings.py`` contains the following config for - :setting:`LOGGING`:: - - LOGGING = { - 'version': 1, - 'handlers': { - 'custom_handler': { - 'level': 'INFO', - 'class': 'myproject.logconfig.MyHandler', - } - } - } - - and ``myproject/logconfig.py`` has the following line before the - ``MyHandler`` definition:: - - from django.conf import settings - - then the ``dictconfig`` module will raise an exception like the following:: - - ValueError: Unable to configure handler 'custom_handler': - Unable to configure handler 'custom_handler': - 'module' object has no attribute 'logconfig' - .. _formatter documentation: http://docs.python.org/library/logging.html#formatter-objects Custom logging configuration @@ -567,3 +545,31 @@ logging module. 'class': 'django.utils.log.AdminEmailHandler' } }, + +.. class:: RequireDebugTrue() + + .. versionadded:: 1.5 + + This filter is similar to :class:`RequireDebugFalse`, except that records are + passed only when :setting:`DEBUG` is `True`. + +.. _default-logging-configuration: + +Django's default logging configuration +====================================== + +By default, Django configures the ``django.request`` logger so that all messages +with ``ERROR`` or ``CRITICAL`` level are sent to :class:`AdminEmailHandler`, as +long as the :setting:`DEBUG` setting is set to ``False``. + +All messages reaching the ``django`` catch-all logger when :setting:`DEBUG` is +`True` are sent ot the console. They are simply discarded (sent to +``NullHandler``) when :setting:`DEBUG` is `False`. + +.. versionchanged:: 1.5 + + Before Django 1.5, all messages reaching the ``django`` logger were + discarded, regardless of :setting:`DEBUG`. + +See also :ref:`Configuring logging ` to learn how you can +complement or replace this default logging configuration. diff --git a/docs/topics/serialization.txt b/docs/topics/serialization.txt index ac1a77ed98..9b44166e42 100644 --- a/docs/topics/serialization.txt +++ b/docs/topics/serialization.txt @@ -130,6 +130,14 @@ trust your data source you could just save the object and move on. The Django object itself can be inspected as ``deserialized_object.object``. +.. versionadded:: 1.5 + +If fields in the serialized data do not exist on a model, +a ``DeserializationError`` will be raised unless the ``ignorenonexistent`` +argument is passed in as True:: + + serializers.deserialize("xml", data, ignorenonexistent=True) + .. _serialization-formats: Serialization formats diff --git a/docs/topics/signals.txt b/docs/topics/signals.txt index db1bcb03df..1078d0372c 100644 --- a/docs/topics/signals.txt +++ b/docs/topics/signals.txt @@ -132,10 +132,6 @@ Now, our ``my_callback`` function will be called each time a request finishes. Note that ``receiver`` can also take a list of signals to connect a function to. -.. versionadded:: 1.3 - -The ``receiver`` decorator was added in Django 1.3. - .. versionchanged:: 1.5 The ability to pass a list of signals was added. diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index c4c73733f5..d0b2e7cdf9 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -73,8 +73,6 @@ module defines tests in class-based approach. .. admonition:: unittest2 - .. versionchanged:: 1.3 - Python 2.7 introduced some major changes to the unittest library, adding some extremely useful features. To ensure that every Django project can benefit from these new features, Django ships with a @@ -436,8 +434,6 @@ two databases. Controlling creation order for test databases ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 1.3 - By default, Django will always create the ``default`` database first. However, no guarantees are made on the creation order of any other databases in your test setup. @@ -512,6 +508,13 @@ file, all Django tests run with :setting:`DEBUG`\=False. This is to ensure that the observed output of your code matches what will be seen in a production setting. +Caches are not cleared after each test, and running "manage.py test fooapp" can +insert data from the tests into the cache of a live system if you run your +tests in production because, unlike databases, a separate "test cache" is not +used. This behavior `may change`_ in the future. + +.. _may change: https://code.djangoproject.com/ticket/11505 + Understanding the test output ----------------------------- @@ -586,6 +589,36 @@ to a faster hashing algorithm:: Don't forget to also include in :setting:`PASSWORD_HASHERS` any hashing algorithm used in fixtures, if any. +.. _topics-testing-code-coverage: + +Integration with coverage.py +---------------------------- + +Code coverage describes how much source code has been tested. It shows which +parts of your code are being exercised by tests and which are not. It's an +important part of testing applications, so it's strongly recommended to check +the coverage of your tests. + +Django can be easily integrated with `coverage.py`_, a tool for measuring code +coverage of Python programs. First, `install coverage.py`_. Next, run the +following from your project folder containing ``manage.py``:: + + coverage run --source='.' manage.py test myapp + +This runs your tests and collects coverage data of the executed files in your +project. You can see a report of this data by typing following command:: + + coverage report + +Note that some Django code was executed while running tests, but it is not +listed here because of the ``source`` flag passed to the previous command. + +For more options like annotated HTML listings detailing missed lines, see the +`coverage.py`_ docs. + +.. _coverage.py: http://nedbatchelder.com/code/coverage/ +.. _install coverage.py: http://pypi.python.org/pypi/coverage + Testing tools ============= @@ -766,7 +799,7 @@ Use the ``django.test.client.Client`` class to make requests. and a ``redirect_chain`` attribute will be set in the response object containing tuples of the intermediate urls and status codes. - If you had an url ``/redirect_me/`` that redirected to ``/next/``, that + If you had a URL ``/redirect_me/`` that redirected to ``/next/``, that redirected to ``/final/``, this is what you'd see:: >>> response = c.get('/redirect_me/', follow=True) @@ -1001,8 +1034,6 @@ Specifically, a ``Response`` object has the following attributes: The HTTP status of the response, as an integer. See :rfc:`2616#section-10` for a full list of HTTP status codes. - .. versionadded:: 1.3 - .. attribute:: templates A list of ``Template`` instances used to render the final content, in @@ -1089,8 +1120,6 @@ The request factory .. class:: RequestFactory -.. versionadded:: 1.3 - The :class:`~django.test.client.RequestFactory` shares the same API as the test client. However, instead of behaving like a browser, the RequestFactory provides a way to generate a request instance that can @@ -1327,8 +1356,6 @@ This means, instead of instantiating a ``Client`` in each test:: Customizing the test client ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 1.3 - .. attribute:: TestCase.client_class If you want to use a different ``Client`` class (for example, a subclass @@ -1625,7 +1652,7 @@ your test suite. "a@a.com" as a valid email address, but rejects "aaa" with a reasonable error message:: - self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': [u'Enter a valid e-mail address.']}) + self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': [u'Enter a valid email address.']}) .. method:: TestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False) @@ -1708,8 +1735,6 @@ your test suite. .. method:: TestCase.assertQuerysetEqual(qs, values, transform=repr, ordered=True) - .. versionadded:: 1.3 - Asserts that a queryset ``qs`` returns a particular list of values ``values``. The comparison of the contents of ``qs`` and ``values`` is performed using @@ -1730,8 +1755,6 @@ your test suite. .. method:: TestCase.assertNumQueries(num, func, *args, **kwargs) - .. versionadded:: 1.3 - Asserts that when ``func`` is called with ``*args`` and ``**kwargs`` that ``num`` database queries are executed. @@ -1790,6 +1813,25 @@ your test suite. ``html1`` and ``html2`` must be valid HTML. An ``AssertionError`` will be raised if one of them cannot be parsed. +.. method:: SimpleTestCase.assertXMLEqual(xml1, xml2, msg=None) + + .. versionadded:: 1.5 + + Asserts that the strings ``xml1`` and ``xml2`` are equal. The + comparison is based on XML semantics. Similarily to + :meth:`~SimpleTestCase.assertHTMLEqual`, the comparison is + made on parsed content, hence only semantic differences are considered, not + syntax differences. When unvalid XML is passed in any parameter, an + ``AssertionError`` is always raised, even if both string are identical. + +.. method:: SimpleTestCase.assertXMLNotEqual(xml1, xml2, msg=None) + + .. versionadded:: 1.5 + + Asserts that the strings ``xml1`` and ``xml2`` are *not* equal. The + comparison is based on XML semantics. See + :meth:`~SimpleTestCase.assertXMLEqual` for details. + .. _topics-testing-email: Email services @@ -1854,8 +1896,6 @@ Skipping tests .. currentmodule:: django.test -.. versionadded:: 1.3 - The unittest library provides the :func:`@skipIf ` and :func:`@skipUnless ` decorators to allow you to skip tests if you know ahead of time that those tests are going to fail under certain @@ -1982,8 +2022,8 @@ Then, add a ``LiveServerTestCase``-based test to your app's tests module @classmethod def tearDownClass(cls): - super(MySeleniumTests, cls).tearDownClass() cls.selenium.quit() + super(MySeleniumTests, cls).tearDownClass() def test_login(self): self.selenium.get('%s%s' % (self.live_server_url, '/login/')) diff --git a/setup.py b/setup.py index 165c5e9f73..333d57ac70 100644 --- a/setup.py +++ b/setup.py @@ -67,9 +67,10 @@ if root_dir != '': django_dir = 'django' for dirpath, dirnames, filenames in os.walk(django_dir): - # Ignore dirnames that start with '.' + # Ignore PEP 3147 cache dirs and those whose names start with '.' for i, dirname in enumerate(dirnames): - if dirname.startswith('.'): del dirnames[i] + if dirname.startswith('.') or dirname == '__pycache__': + del dirnames[i] if '__init__.py' in filenames: packages.append('.'.join(fullsplit(dirpath))) elif filenames: diff --git a/tests/.coveragerc b/tests/.coveragerc new file mode 100644 index 0000000000..b979e94c58 --- /dev/null +++ b/tests/.coveragerc @@ -0,0 +1,5 @@ +[run] +omit = runtests,test_sqlite,regressiontests*,modeltests*,*/django/contrib/*/tests*,*/django/utils/unittest*,*/django/utils/simplejson*,*/django/utils/importlib.py,*/django/test/_doctest.py,*/django/core/servers/fastcgi.py,*/django/utils/autoreload.py,*/django/utils/dictconfig.py + +[html] +directory = coverage_html diff --git a/tests/modeltests/basic/tests.py b/tests/modeltests/basic/tests.py index 6ec9ca03af..ebd70d14d9 100644 --- a/tests/modeltests/basic/tests.py +++ b/tests/modeltests/basic/tests.py @@ -259,9 +259,8 @@ class ModelTest(TestCase): "datetime.datetime(2005, 7, 28, 0, 0)"]) # dates() requires valid arguments. - six.assertRaisesRegex(self, + self.assertRaises( TypeError, - "dates\(\) takes at least 3 arguments \(1 given\)", Article.objects.dates, ) diff --git a/tests/modeltests/delete/models.py b/tests/modeltests/delete/models.py index e0cec426ea..65d4e6f725 100644 --- a/tests/modeltests/delete/models.py +++ b/tests/modeltests/delete/models.py @@ -95,7 +95,7 @@ class MRNull(models.Model): class Avatar(models.Model): - pass + desc = models.TextField(null=True) class User(models.Model): @@ -108,3 +108,21 @@ class HiddenUser(models.Model): class HiddenUserProfile(models.Model): user = models.ForeignKey(HiddenUser) + +class M2MTo(models.Model): + pass + +class M2MFrom(models.Model): + m2m = models.ManyToManyField(M2MTo) + +class Parent(models.Model): + pass + +class Child(Parent): + pass + +class Base(models.Model): + pass + +class RelToBase(models.Model): + base = models.ForeignKey(Base, on_delete=models.DO_NOTHING) diff --git a/tests/modeltests/delete/tests.py b/tests/modeltests/delete/tests.py index 26f2fd52c1..20b815c33d 100644 --- a/tests/modeltests/delete/tests.py +++ b/tests/modeltests/delete/tests.py @@ -1,11 +1,12 @@ from __future__ import absolute_import -from django.db import models, IntegrityError +from django.db import models, IntegrityError, connection from django.test import TestCase, skipUnlessDBFeature, skipIfDBFeature from django.utils.six.moves import xrange from .models import (R, RChild, S, T, U, A, M, MR, MRNull, - create_a, get_default_r, User, Avatar, HiddenUser, HiddenUserProfile) + create_a, get_default_r, User, Avatar, HiddenUser, HiddenUserProfile, + M2MTo, M2MFrom, Parent, Child, Base) class OnDeleteTests(TestCase): @@ -74,6 +75,16 @@ class OnDeleteTests(TestCase): self.assertEqual(replacement_r, a.donothing) models.signals.pre_delete.disconnect(check_do_nothing) + def test_do_nothing_qscount(self): + """ + Test that a models.DO_NOTHING relation doesn't trigger a query. + """ + b = Base.objects.create() + with self.assertNumQueries(1): + # RelToBase should not be queried. + b.delete() + self.assertEqual(Base.objects.count(), 0) + def test_inheritance_cascade_up(self): child = RChild.objects.create() child.delete() @@ -229,16 +240,34 @@ class DeletionTests(TestCase): # 1 query to delete the avatar # The important thing is that when we can defer constraint checks there # is no need to do an UPDATE on User.avatar to null it out. + + # Attach a signal to make sure we will not do fast_deletes. + calls = [] + def noop(*args, **kwargs): + calls.append('') + models.signals.post_delete.connect(noop, sender=User) + self.assertNumQueries(3, a.delete) self.assertFalse(User.objects.exists()) self.assertFalse(Avatar.objects.exists()) + self.assertEqual(len(calls), 1) + models.signals.post_delete.disconnect(noop, sender=User) @skipIfDBFeature("can_defer_constraint_checks") def test_cannot_defer_constraint_checks(self): u = User.objects.create( avatar=Avatar.objects.create() ) + # Attach a signal to make sure we will not do fast_deletes. + calls = [] + def noop(*args, **kwargs): + calls.append('') + models.signals.post_delete.connect(noop, sender=User) + a = Avatar.objects.get(pk=u.avatar_id) + # The below doesn't make sense... Why do we need to null out + # user.avatar if we are going to delete the user immediately after it, + # and there are no more cascades. # 1 query to find the users for the avatar. # 1 query to delete the user # 1 query to null out user.avatar, because we can't defer the constraint @@ -246,6 +275,8 @@ class DeletionTests(TestCase): self.assertNumQueries(4, a.delete) self.assertFalse(User.objects.exists()) self.assertFalse(Avatar.objects.exists()) + self.assertEqual(len(calls), 1) + models.signals.post_delete.disconnect(noop, sender=User) def test_hidden_related(self): r = R.objects.create() @@ -254,3 +285,69 @@ class DeletionTests(TestCase): r.delete() self.assertEqual(HiddenUserProfile.objects.count(), 0) + +class FastDeleteTests(TestCase): + + def test_fast_delete_fk(self): + u = User.objects.create( + avatar=Avatar.objects.create() + ) + a = Avatar.objects.get(pk=u.avatar_id) + # 1 query to fast-delete the user + # 1 query to delete the avatar + self.assertNumQueries(2, a.delete) + self.assertFalse(User.objects.exists()) + self.assertFalse(Avatar.objects.exists()) + + def test_fast_delete_m2m(self): + t = M2MTo.objects.create() + f = M2MFrom.objects.create() + f.m2m.add(t) + # 1 to delete f, 1 to fast-delete m2m for f + self.assertNumQueries(2, f.delete) + + def test_fast_delete_revm2m(self): + t = M2MTo.objects.create() + f = M2MFrom.objects.create() + f.m2m.add(t) + # 1 to delete t, 1 to fast-delete t's m_set + self.assertNumQueries(2, f.delete) + + def test_fast_delete_qs(self): + u1 = User.objects.create() + u2 = User.objects.create() + self.assertNumQueries(1, User.objects.filter(pk=u1.pk).delete) + self.assertEqual(User.objects.count(), 1) + self.assertTrue(User.objects.filter(pk=u2.pk).exists()) + + def test_fast_delete_joined_qs(self): + a = Avatar.objects.create(desc='a') + User.objects.create(avatar=a) + u2 = User.objects.create() + expected_queries = 1 if connection.features.update_can_self_select else 2 + self.assertNumQueries(expected_queries, + User.objects.filter(avatar__desc='a').delete) + self.assertEqual(User.objects.count(), 1) + self.assertTrue(User.objects.filter(pk=u2.pk).exists()) + + def test_fast_delete_inheritance(self): + c = Child.objects.create() + p = Parent.objects.create() + # 1 for self, 1 for parent + # However, this doesn't work as child.parent access creates a query, + # and this means we will be generating extra queries (a lot for large + # querysets). This is not a fast-delete problem. + # self.assertNumQueries(2, c.delete) + c.delete() + self.assertFalse(Child.objects.exists()) + self.assertEqual(Parent.objects.count(), 1) + self.assertEqual(Parent.objects.filter(pk=p.pk).count(), 1) + # 1 for self delete, 1 for fast delete of empty "child" qs. + self.assertNumQueries(2, p.delete) + self.assertFalse(Parent.objects.exists()) + # 1 for self delete, 1 for fast delete of empty "child" qs. + c = Child.objects.create() + p = c.parent_ptr + self.assertNumQueries(2, p.delete) + self.assertFalse(Parent.objects.exists()) + self.assertFalse(Child.objects.exists()) diff --git a/tests/modeltests/invalid_models/invalid_models/models.py b/tests/modeltests/invalid_models/invalid_models/models.py index b2ba253c5d..ccb6396352 100644 --- a/tests/modeltests/invalid_models/invalid_models/models.py +++ b/tests/modeltests/invalid_models/invalid_models/models.py @@ -21,11 +21,12 @@ class FieldErrors(models.Model): decimalfield5 = models.DecimalField(max_digits=10, decimal_places=10) filefield = models.FileField() choices = models.CharField(max_length=10, choices='bad') - choices2 = models.CharField(max_length=10, choices=[(1,2,3),(1,2,3)]) + choices2 = models.CharField(max_length=10, choices=[(1, 2, 3), (1, 2, 3)]) index = models.CharField(max_length=10, db_index='bad') field_ = models.CharField(max_length=10) nullbool = models.BooleanField(null=True) + class Target(models.Model): tgt_safe = models.CharField(max_length=10) clash1 = models.CharField(max_length=10) @@ -33,12 +34,14 @@ class Target(models.Model): clash1_set = models.CharField(max_length=10) + class Clash1(models.Model): src_safe = models.CharField(max_length=10) foreign = models.ForeignKey(Target) m2m = models.ManyToManyField(Target) + class Clash2(models.Model): src_safe = models.CharField(max_length=10) @@ -48,6 +51,7 @@ class Clash2(models.Model): m2m_1 = models.ManyToManyField(Target, related_name='id') m2m_2 = models.ManyToManyField(Target, related_name='src_safe') + class Target2(models.Model): clash3 = models.CharField(max_length=10) foreign_tgt = models.ForeignKey(Target) @@ -56,6 +60,7 @@ class Target2(models.Model): m2m_tgt = models.ManyToManyField(Target) clashm2m_set = models.ManyToManyField(Target) + class Clash3(models.Model): src_safe = models.CharField(max_length=10) @@ -65,12 +70,15 @@ class Clash3(models.Model): m2m_1 = models.ManyToManyField(Target2, related_name='foreign_tgt') m2m_2 = models.ManyToManyField(Target2, related_name='m2m_tgt') + class ClashForeign(models.Model): foreign = models.ForeignKey(Target2) + class ClashM2M(models.Model): m2m = models.ManyToManyField(Target2) + class SelfClashForeign(models.Model): src_safe = models.CharField(max_length=10) selfclashforeign = models.CharField(max_length=10) @@ -79,6 +87,7 @@ class SelfClashForeign(models.Model): foreign_1 = models.ForeignKey("SelfClashForeign", related_name='id') foreign_2 = models.ForeignKey("SelfClashForeign", related_name='src_safe') + class ValidM2M(models.Model): src_safe = models.CharField(max_length=10) validm2m = models.CharField(max_length=10) @@ -94,6 +103,7 @@ class ValidM2M(models.Model): m2m_3 = models.ManyToManyField('self') m2m_4 = models.ManyToManyField('self') + class SelfClashM2M(models.Model): src_safe = models.CharField(max_length=10) selfclashm2m = models.CharField(max_length=10) @@ -108,120 +118,148 @@ class SelfClashM2M(models.Model): m2m_3 = models.ManyToManyField('self', symmetrical=False) m2m_4 = models.ManyToManyField('self', symmetrical=False) + class Model(models.Model): "But it's valid to call a model Model." - year = models.PositiveIntegerField() #1960 - make = models.CharField(max_length=10) #Aston Martin - name = models.CharField(max_length=10) #DB 4 GT + year = models.PositiveIntegerField() # 1960 + make = models.CharField(max_length=10) # Aston Martin + name = models.CharField(max_length=10) # DB 4 GT + class Car(models.Model): colour = models.CharField(max_length=5) model = models.ForeignKey(Model) + class MissingRelations(models.Model): rel1 = models.ForeignKey("Rel1") rel2 = models.ManyToManyField("Rel2") + class MissingManualM2MModel(models.Model): name = models.CharField(max_length=5) missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel") + class Person(models.Model): name = models.CharField(max_length=5) + class Group(models.Model): name = models.CharField(max_length=5) primary = models.ManyToManyField(Person, through="Membership", related_name="primary") secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary") tertiary = models.ManyToManyField(Person, through="RelationshipDoubleFK", related_name="tertiary") + class GroupTwo(models.Model): name = models.CharField(max_length=5) primary = models.ManyToManyField(Person, through="Membership") secondary = models.ManyToManyField(Group, through="MembershipMissingFK") + class Membership(models.Model): person = models.ForeignKey(Person) group = models.ForeignKey(Group) not_default_or_null = models.CharField(max_length=5) + class MembershipMissingFK(models.Model): person = models.ForeignKey(Person) + class PersonSelfRefM2M(models.Model): name = models.CharField(max_length=5) friends = models.ManyToManyField('self', through="Relationship") too_many_friends = models.ManyToManyField('self', through="RelationshipTripleFK") + class PersonSelfRefM2MExplicit(models.Model): name = models.CharField(max_length=5) friends = models.ManyToManyField('self', through="ExplicitRelationship", symmetrical=True) + class Relationship(models.Model): first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set") second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set") date_added = models.DateTimeField() + class ExplicitRelationship(models.Model): first = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_from_set") second = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_to_set") date_added = models.DateTimeField() + class RelationshipTripleFK(models.Model): first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set_2") second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set_2") third = models.ForeignKey(PersonSelfRefM2M, related_name="too_many_by_far") date_added = models.DateTimeField() + class RelationshipDoubleFK(models.Model): first = models.ForeignKey(Person, related_name="first_related_name") second = models.ForeignKey(Person, related_name="second_related_name") third = models.ForeignKey(Group, related_name="rel_to_set") date_added = models.DateTimeField() + class AbstractModel(models.Model): name = models.CharField(max_length=10) + class Meta: abstract = True + class AbstractRelationModel(models.Model): fk1 = models.ForeignKey('AbstractModel') fk2 = models.ManyToManyField('AbstractModel') + class UniqueM2M(models.Model): """ Model to test for unique ManyToManyFields, which are invalid. """ unique_people = models.ManyToManyField(Person, unique=True) + class NonUniqueFKTarget1(models.Model): """ Model to test for non-unique FK target in yet-to-be-defined model: expect an error """ tgt = models.ForeignKey('FKTarget', to_field='bad') + class UniqueFKTarget1(models.Model): """ Model to test for unique FK target in yet-to-be-defined model: expect no error """ tgt = models.ForeignKey('FKTarget', to_field='good') + class FKTarget(models.Model): bad = models.IntegerField() good = models.IntegerField(unique=True) + class NonUniqueFKTarget2(models.Model): """ Model to test for non-unique FK target in previously seen model: expect an error """ tgt = models.ForeignKey(FKTarget, to_field='bad') + class UniqueFKTarget2(models.Model): """ Model to test for unique FK target in previously seen model: expect no error """ tgt = models.ForeignKey(FKTarget, to_field='good') + class NonExistingOrderingWithSingleUnderscore(models.Model): class Meta: ordering = ("does_not_exist",) + class InvalidSetNull(models.Model): fk = models.ForeignKey('self', on_delete=models.SET_NULL) + class InvalidSetDefault(models.Model): fk = models.ForeignKey('self', on_delete=models.SET_DEFAULT) + class UnicodeForeignKeys(models.Model): """Foreign keys which can translate to ascii should be OK, but fail if they're not.""" @@ -232,9 +270,11 @@ class UnicodeForeignKeys(models.Model): # when adding the errors in core/management/validation.py #bad = models.ForeignKey('★') + class PrimaryKeyNull(models.Model): my_pk_field = models.IntegerField(primary_key=True, null=True) + class OrderByPKModel(models.Model): """ Model to test that ordering by pk passes validation. @@ -245,6 +285,77 @@ class OrderByPKModel(models.Model): class Meta: ordering = ('pk',) + +class SwappableModel(models.Model): + """A model that can be, but isn't swapped out. + + References to this model *shoudln't* raise any validation error. + """ + name = models.CharField(max_length=100) + + class Meta: + swappable = 'TEST_SWAPPABLE_MODEL' + + +class SwappedModel(models.Model): + """A model that is swapped out. + + References to this model *should* raise a validation error. + Requires TEST_SWAPPED_MODEL to be defined in the test environment; + this is guaranteed by the test runner using @override_settings. + + The foreign keys and m2m relations on this model *shouldn't* + install related accessors, so there shouldn't be clashes with + the equivalent names on the replacement. + """ + name = models.CharField(max_length=100) + + foreign = models.ForeignKey(Target, related_name='swappable_fk_set') + m2m = models.ManyToManyField(Target, related_name='swappable_m2m_set') + + class Meta: + swappable = 'TEST_SWAPPED_MODEL' + + +class ReplacementModel(models.Model): + """A replacement model for swapping purposes.""" + name = models.CharField(max_length=100) + + foreign = models.ForeignKey(Target, related_name='swappable_fk_set') + m2m = models.ManyToManyField(Target, related_name='swappable_m2m_set') + + +class BadSwappableValue(models.Model): + """A model that can be swapped out; during testing, the swappable + value is not of the format app.model + """ + name = models.CharField(max_length=100) + + class Meta: + swappable = 'TEST_SWAPPED_MODEL_BAD_VALUE' + + +class BadSwappableModel(models.Model): + """A model that can be swapped out; during testing, the swappable + value references an unknown model. + """ + name = models.CharField(max_length=100) + + class Meta: + swappable = 'TEST_SWAPPED_MODEL_BAD_MODEL' + + +class HardReferenceModel(models.Model): + fk_1 = models.ForeignKey(SwappableModel, related_name='fk_hardref1') + fk_2 = models.ForeignKey('invalid_models.SwappableModel', related_name='fk_hardref2') + fk_3 = models.ForeignKey(SwappedModel, related_name='fk_hardref3') + fk_4 = models.ForeignKey('invalid_models.SwappedModel', related_name='fk_hardref4') + m2m_1 = models.ManyToManyField(SwappableModel, related_name='m2m_hardref1') + m2m_2 = models.ManyToManyField('invalid_models.SwappableModel', related_name='m2m_hardref2') + m2m_3 = models.ManyToManyField(SwappedModel, related_name='m2m_hardref3') + m2m_4 = models.ManyToManyField('invalid_models.SwappedModel', related_name='m2m_hardref4') + + model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute that is a positive integer. invalid_models.fielderrors: "charfield2": CharFields require a "max_length" attribute that is a positive integer. invalid_models.fielderrors: "charfield3": CharFields require a "max_length" attribute that is a positive integer. @@ -353,6 +464,12 @@ invalid_models.nonuniquefktarget2: Field 'bad' under model 'FKTarget' must have invalid_models.nonexistingorderingwithsingleunderscore: "ordering" refers to "does_not_exist", a field that doesn't exist. invalid_models.invalidsetnull: 'fk' specifies on_delete=SET_NULL, but cannot be null. invalid_models.invalidsetdefault: 'fk' specifies on_delete=SET_DEFAULT, but has no default value. +invalid_models.hardreferencemodel: 'fk_3' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. +invalid_models.hardreferencemodel: 'fk_4' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. +invalid_models.hardreferencemodel: 'm2m_3' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. +invalid_models.hardreferencemodel: 'm2m_4' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. +invalid_models.badswappablevalue: TEST_SWAPPED_MODEL_BAD_VALUE is not of the form 'app_label.app_name'. +invalid_models.badswappablemodel: Model has been swapped out for 'not_an_app.Target' which has not been installed or is abstract. """ if not connection.features.interprets_empty_strings_as_nulls: diff --git a/tests/modeltests/invalid_models/tests.py b/tests/modeltests/invalid_models/tests.py index e1fc68743e..5f6224c45d 100644 --- a/tests/modeltests/invalid_models/tests.py +++ b/tests/modeltests/invalid_models/tests.py @@ -4,6 +4,7 @@ import sys from django.core.management.validation import get_validation_errors from django.db.models.loading import cache, load_app +from django.test.utils import override_settings from django.utils import unittest from django.utils.six import StringIO @@ -31,14 +32,22 @@ class InvalidModelTestCase(unittest.TestCase): cache._get_models_cache = {} sys.stdout = self.old_stdout + # Technically, this isn't an override -- TEST_SWAPPED_MODEL must be + # set to *something* in order for the test to work. However, it's + # easier to set this up as an override than to require every developer + # to specify a value in their test settings. + @override_settings( + TEST_SWAPPED_MODEL='invalid_models.ReplacementModel', + TEST_SWAPPED_MODEL_BAD_VALUE='not-a-model', + TEST_SWAPPED_MODEL_BAD_MODEL='not_an_app.Target', + ) def test_invalid_models(self): - try: module = load_app("modeltests.invalid_models.invalid_models") except Exception: self.fail('Unable to load invalid model module') - count = get_validation_errors(self.stdout, module) + get_validation_errors(self.stdout, module) self.stdout.seek(0) error_log = self.stdout.read() actual = error_log.split('\n') diff --git a/tests/modeltests/model_forms/tests.py b/tests/modeltests/model_forms/tests.py index 038ce32287..947d0cf3c3 100644 --- a/tests/modeltests/model_forms/tests.py +++ b/tests/modeltests/model_forms/tests.py @@ -8,6 +8,7 @@ from django import forms from django.core.files.uploadedfile import SimpleUploadedFile from django.core.validators import ValidationError from django.db import connection +from django.db.models.query import EmptyQuerySet from django.forms.models import model_to_dict from django.utils.unittest import skipUnless from django.test import TestCase @@ -1035,8 +1036,8 @@ class OldFormForXTests(TestCase): f.clean([c6.id]) f = forms.ModelMultipleChoiceField(Category.objects.all(), required=False) - self.assertEqual(f.clean([]), []) - self.assertEqual(f.clean(()), []) + self.assertIsInstance(f.clean([]), EmptyQuerySet) + self.assertIsInstance(f.clean(()), EmptyQuerySet) with self.assertRaises(ValidationError): f.clean(['10']) with self.assertRaises(ValidationError): diff --git a/tests/modeltests/proxy_models/tests.py b/tests/modeltests/proxy_models/tests.py index 7ec86e9b22..d1c95467ee 100644 --- a/tests/modeltests/proxy_models/tests.py +++ b/tests/modeltests/proxy_models/tests.py @@ -1,10 +1,13 @@ from __future__ import absolute_import, unicode_literals +import copy +from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core import management from django.core.exceptions import FieldError from django.db import models, DEFAULT_DB_ALIAS from django.db.models import signals +from django.db.models.loading import cache from django.test import TestCase @@ -13,6 +16,7 @@ from .models import (MyPerson, Person, StatusPerson, LowerStatusPerson, Country, State, StateProxy, TrackerUser, BaseUser, Bug, ProxyTrackerUser, Improvement, ProxyProxyBug, ProxyBug, ProxyImprovement) + class ProxyModelTests(TestCase): def test_same_manager_queries(self): """ @@ -91,7 +95,7 @@ class ProxyModelTests(TestCase): ) self.assertRaises(Person.MultipleObjectsReturned, MyPersonProxy.objects.get, - id__lt=max_id+1 + id__lt=max_id + 1 ) self.assertRaises(Person.DoesNotExist, StatusPerson.objects.get, @@ -104,7 +108,7 @@ class ProxyModelTests(TestCase): self.assertRaises(Person.MultipleObjectsReturned, StatusPerson.objects.get, - id__lt=max_id+1 + id__lt=max_id + 1 ) def test_abc(self): @@ -138,10 +142,40 @@ class ProxyModelTests(TestCase): def build_new_fields(): class NoNewFields(Person): newfield = models.BooleanField() + class Meta: proxy = True self.assertRaises(FieldError, build_new_fields) + def test_swappable(self): + try: + # This test adds dummy applications to the app cache. These + # need to be removed in order to prevent bad interactions + # with the flush operation in other tests. + old_app_models = copy.deepcopy(cache.app_models) + old_app_store = copy.deepcopy(cache.app_store) + + settings.TEST_SWAPPABLE_MODEL = 'proxy_models.AlternateModel' + + class SwappableModel(models.Model): + + class Meta: + swappable = 'TEST_SWAPPABLE_MODEL' + + class AlternateModel(models.Model): + pass + + # You can't proxy a swapped model + with self.assertRaises(TypeError): + class ProxyModel(SwappableModel): + + class Meta: + proxy = True + finally: + del settings.TEST_SWAPPABLE_MODEL + cache.app_models = old_app_models + cache.app_store = old_app_store + def test_myperson_manager(self): Person.objects.create(name="fred") Person.objects.create(name="wilma") diff --git a/tests/modeltests/test_client/models.py b/tests/modeltests/test_client/models.py index 1d9c999f21..0f3cba7e88 100644 --- a/tests/modeltests/test_client/models.py +++ b/tests/modeltests/test_client/models.py @@ -215,7 +215,7 @@ class ClientTest(TestCase): self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "Invalid POST Template") - self.assertFormError(response, 'form', 'email', 'Enter a valid e-mail address.') + self.assertFormError(response, 'form', 'email', 'Enter a valid email address.') def test_valid_form_with_template(self): "POST valid data to a form using multiple templates" @@ -263,7 +263,7 @@ class ClientTest(TestCase): self.assertTemplateUsed(response, 'base.html') self.assertTemplateNotUsed(response, "Invalid POST Template") - self.assertFormError(response, 'form', 'email', 'Enter a valid e-mail address.') + self.assertFormError(response, 'form', 'email', 'Enter a valid email address.') def test_unknown_page(self): "GET an invalid URL" diff --git a/tests/modeltests/timezones/tests.py b/tests/modeltests/timezones/tests.py index 4621f0a60c..2fdf6733a0 100644 --- a/tests/modeltests/timezones/tests.py +++ b/tests/modeltests/timezones/tests.py @@ -5,6 +5,7 @@ import os import sys import time import warnings +from xml.dom.minidom import parseString try: import pytz @@ -492,27 +493,40 @@ class SerializationTests(TestCase): # returns a naive datetime object in UTC (http://pyyaml.org/ticket/202). # Tests are adapted to take these quirks into account. + def assert_python_contains_datetime(self, objects, dt): + self.assertEqual(objects[0]['fields']['dt'], dt) + + def assert_json_contains_datetime(self, json, dt): + self.assertIn('"fields": {"dt": "%s"}' % dt, json) + + def assert_xml_contains_datetime(self, xml, dt): + field = parseString(xml).getElementsByTagName('field')[0] + self.assertXMLEqual(field.childNodes[0].wholeText, dt) + + def assert_yaml_contains_datetime(self, yaml, dt): + self.assertIn("- fields: {dt: !!timestamp '%s'}" % dt, yaml) + def test_naive_datetime(self): dt = datetime.datetime(2011, 9, 1, 13, 20, 30) data = serializers.serialize('python', [Event(dt=dt)]) - self.assertEqual(data[0]['fields']['dt'], dt) + self.assert_python_contains_datetime(data, dt) obj = next(serializers.deserialize('python', data)).object self.assertEqual(obj.dt, dt) data = serializers.serialize('json', [Event(dt=dt)]) - self.assertIn('"fields": {"dt": "2011-09-01T13:20:30"}', data) + self.assert_json_contains_datetime(data, "2011-09-01T13:20:30") obj = next(serializers.deserialize('json', data)).object self.assertEqual(obj.dt, dt) data = serializers.serialize('xml', [Event(dt=dt)]) - self.assertIn('2011-09-01T13:20:30', data) + self.assert_xml_contains_datetime(data, "2011-09-01T13:20:30") obj = next(serializers.deserialize('xml', data)).object self.assertEqual(obj.dt, dt) if 'yaml' in serializers.get_serializer_formats(): data = serializers.serialize('yaml', [Event(dt=dt)]) - self.assertIn("- fields: {dt: !!timestamp '2011-09-01 13:20:30'}", data) + self.assert_yaml_contains_datetime(data, "2011-09-01 13:20:30") obj = next(serializers.deserialize('yaml', data)).object self.assertEqual(obj.dt, dt) @@ -520,23 +534,23 @@ class SerializationTests(TestCase): dt = datetime.datetime(2011, 9, 1, 13, 20, 30, 405060) data = serializers.serialize('python', [Event(dt=dt)]) - self.assertEqual(data[0]['fields']['dt'], dt) + self.assert_python_contains_datetime(data, dt) obj = next(serializers.deserialize('python', data)).object self.assertEqual(obj.dt, dt) data = serializers.serialize('json', [Event(dt=dt)]) - self.assertIn('"fields": {"dt": "2011-09-01T13:20:30.405"}', data) + self.assert_json_contains_datetime(data, "2011-09-01T13:20:30.405") obj = next(serializers.deserialize('json', data)).object self.assertEqual(obj.dt, dt.replace(microsecond=405000)) data = serializers.serialize('xml', [Event(dt=dt)]) - self.assertIn('2011-09-01T13:20:30.405060', data) + self.assert_xml_contains_datetime(data, "2011-09-01T13:20:30.405060") obj = next(serializers.deserialize('xml', data)).object self.assertEqual(obj.dt, dt) if 'yaml' in serializers.get_serializer_formats(): data = serializers.serialize('yaml', [Event(dt=dt)]) - self.assertIn("- fields: {dt: !!timestamp '2011-09-01 13:20:30.405060'}", data) + self.assert_yaml_contains_datetime(data, "2011-09-01 13:20:30.405060") obj = next(serializers.deserialize('yaml', data)).object self.assertEqual(obj.dt, dt) @@ -544,23 +558,23 @@ class SerializationTests(TestCase): dt = datetime.datetime(2011, 9, 1, 17, 20, 30, 405060, tzinfo=ICT) data = serializers.serialize('python', [Event(dt=dt)]) - self.assertEqual(data[0]['fields']['dt'], dt) + self.assert_python_contains_datetime(data, dt) obj = next(serializers.deserialize('python', data)).object self.assertEqual(obj.dt, dt) data = serializers.serialize('json', [Event(dt=dt)]) - self.assertIn('"fields": {"dt": "2011-09-01T17:20:30.405+07:00"}', data) + self.assert_json_contains_datetime(data, "2011-09-01T17:20:30.405+07:00") obj = next(serializers.deserialize('json', data)).object self.assertEqual(obj.dt, dt.replace(microsecond=405000)) data = serializers.serialize('xml', [Event(dt=dt)]) - self.assertIn('2011-09-01T17:20:30.405060+07:00', data) + self.assert_xml_contains_datetime(data, "2011-09-01T17:20:30.405060+07:00") obj = next(serializers.deserialize('xml', data)).object self.assertEqual(obj.dt, dt) if 'yaml' in serializers.get_serializer_formats(): data = serializers.serialize('yaml', [Event(dt=dt)]) - self.assertIn("- fields: {dt: !!timestamp '2011-09-01 17:20:30.405060+07:00'}", data) + self.assert_yaml_contains_datetime(data, "2011-09-01 17:20:30.405060+07:00") obj = next(serializers.deserialize('yaml', data)).object self.assertEqual(obj.dt.replace(tzinfo=UTC), dt) @@ -568,23 +582,23 @@ class SerializationTests(TestCase): dt = datetime.datetime(2011, 9, 1, 10, 20, 30, tzinfo=UTC) data = serializers.serialize('python', [Event(dt=dt)]) - self.assertEqual(data[0]['fields']['dt'], dt) + self.assert_python_contains_datetime(data, dt) obj = next(serializers.deserialize('python', data)).object self.assertEqual(obj.dt, dt) data = serializers.serialize('json', [Event(dt=dt)]) - self.assertIn('"fields": {"dt": "2011-09-01T10:20:30Z"}', data) + self.assert_json_contains_datetime(data, "2011-09-01T10:20:30Z") obj = next(serializers.deserialize('json', data)).object self.assertEqual(obj.dt, dt) data = serializers.serialize('xml', [Event(dt=dt)]) - self.assertIn('2011-09-01T10:20:30+00:00', data) + self.assert_xml_contains_datetime(data, "2011-09-01T10:20:30+00:00") obj = next(serializers.deserialize('xml', data)).object self.assertEqual(obj.dt, dt) if 'yaml' in serializers.get_serializer_formats(): data = serializers.serialize('yaml', [Event(dt=dt)]) - self.assertIn("- fields: {dt: !!timestamp '2011-09-01 10:20:30+00:00'}", data) + self.assert_yaml_contains_datetime(data, "2011-09-01 10:20:30+00:00") obj = next(serializers.deserialize('yaml', data)).object self.assertEqual(obj.dt.replace(tzinfo=UTC), dt) @@ -592,23 +606,23 @@ class SerializationTests(TestCase): dt = datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT) data = serializers.serialize('python', [Event(dt=dt)]) - self.assertEqual(data[0]['fields']['dt'], dt) + self.assert_python_contains_datetime(data, dt) obj = next(serializers.deserialize('python', data)).object self.assertEqual(obj.dt, dt) data = serializers.serialize('json', [Event(dt=dt)]) - self.assertIn('"fields": {"dt": "2011-09-01T13:20:30+03:00"}', data) + self.assert_json_contains_datetime(data, "2011-09-01T13:20:30+03:00") obj = next(serializers.deserialize('json', data)).object self.assertEqual(obj.dt, dt) data = serializers.serialize('xml', [Event(dt=dt)]) - self.assertIn('2011-09-01T13:20:30+03:00', data) + self.assert_xml_contains_datetime(data, "2011-09-01T13:20:30+03:00") obj = next(serializers.deserialize('xml', data)).object self.assertEqual(obj.dt, dt) if 'yaml' in serializers.get_serializer_formats(): data = serializers.serialize('yaml', [Event(dt=dt)]) - self.assertIn("- fields: {dt: !!timestamp '2011-09-01 13:20:30+03:00'}", data) + self.assert_yaml_contains_datetime(data, "2011-09-01 13:20:30+03:00") obj = next(serializers.deserialize('yaml', data)).object self.assertEqual(obj.dt.replace(tzinfo=UTC), dt) @@ -616,23 +630,23 @@ class SerializationTests(TestCase): dt = datetime.datetime(2011, 9, 1, 17, 20, 30, tzinfo=ICT) data = serializers.serialize('python', [Event(dt=dt)]) - self.assertEqual(data[0]['fields']['dt'], dt) + self.assert_python_contains_datetime(data, dt) obj = next(serializers.deserialize('python', data)).object self.assertEqual(obj.dt, dt) data = serializers.serialize('json', [Event(dt=dt)]) - self.assertIn('"fields": {"dt": "2011-09-01T17:20:30+07:00"}', data) + self.assert_json_contains_datetime(data, "2011-09-01T17:20:30+07:00") obj = next(serializers.deserialize('json', data)).object self.assertEqual(obj.dt, dt) data = serializers.serialize('xml', [Event(dt=dt)]) - self.assertIn('2011-09-01T17:20:30+07:00', data) + self.assert_xml_contains_datetime(data, "2011-09-01T17:20:30+07:00") obj = next(serializers.deserialize('xml', data)).object self.assertEqual(obj.dt, dt) if 'yaml' in serializers.get_serializer_formats(): data = serializers.serialize('yaml', [Event(dt=dt)]) - self.assertIn("- fields: {dt: !!timestamp '2011-09-01 17:20:30+07:00'}", data) + self.assert_yaml_contains_datetime(data, "2011-09-01 17:20:30+07:00") obj = next(serializers.deserialize('yaml', data)).object self.assertEqual(obj.dt.replace(tzinfo=UTC), dt) diff --git a/tests/modeltests/validators/tests.py b/tests/modeltests/validators/tests.py index 018be6ae59..0174a606df 100644 --- a/tests/modeltests/validators/tests.py +++ b/tests/modeltests/validators/tests.py @@ -26,13 +26,23 @@ TEST_DATA = ( (validate_email, 'email@here.com', None), (validate_email, 'weirder-email@here.and.there.com', None), (validate_email, 'email@[127.0.0.1]', None), + (validate_email, 'example@valid-----hyphens.com', None), + (validate_email, 'example@valid-with-hyphens.com', None), + (validate_email, 'test@domain.with.idn.tld.उदाहरण.परीक्षा', None), (validate_email, None, ValidationError), (validate_email, '', ValidationError), (validate_email, 'abc', ValidationError), + (validate_email, 'abc@', ValidationError), + (validate_email, 'abc@bar', ValidationError), (validate_email, 'a @x.cz', ValidationError), + (validate_email, 'abc@.com', ValidationError), (validate_email, 'something@@somewhere.com', ValidationError), (validate_email, 'email@127.0.0.1', ValidationError), + (validate_email, 'example@invalid-.com', ValidationError), + (validate_email, 'example@-invalid.com', ValidationError), + (validate_email, 'example@inv-.alid-.com', ValidationError), + (validate_email, 'example@inv-.-alid.com', ValidationError), # Quoted-string format (CR not allowed) (validate_email, '"\\\011"@here.com', None), (validate_email, '"\\\012"@here.com', ValidationError), @@ -162,11 +172,23 @@ def create_simple_test_method(validator, expected, value, num): if expected is not None and issubclass(expected, Exception): test_mask = 'test_%s_raises_error_%d' def test_func(self): - self.assertRaises(expected, validator, value) + # assertRaises not used, so as to be able to produce an error message + # containing the tested value + try: + validator(value) + except expected: + pass + else: + self.fail("%s not raised when validating '%s'" % ( + expected.__name__, value)) else: test_mask = 'test_%s_%d' def test_func(self): - self.assertEqual(expected, validator(value)) + try: + self.assertEqual(expected, validator(value)) + except ValidationError as e: + self.fail("Validation of '%s' failed. Error message was: %s" % ( + value, str(e))) if isinstance(validator, types.FunctionType): val_name = validator.__name__ else: diff --git a/tests/regressiontests/admin_changelist/tests.py b/tests/regressiontests/admin_changelist/tests.py index be88c9a161..2b1c1a9bcf 100644 --- a/tests/regressiontests/admin_changelist/tests.py +++ b/tests/regressiontests/admin_changelist/tests.py @@ -6,6 +6,7 @@ from django.contrib import admin from django.contrib.admin.options import IncorrectLookupParameters from django.contrib.admin.views.main import ChangeList, SEARCH_VAR, ALL_VAR from django.contrib.auth.models import User +from django.core.urlresolvers import reverse from django.template import Context, Template from django.test import TestCase from django.test.client import RequestFactory @@ -65,7 +66,8 @@ class ChangeListTests(TestCase): template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}') context = Context({'cl': cl}) table_output = template.render(context) - row_html = 'name(None)' % new_child.id + link = reverse('admin:admin_changelist_child_change', args=(new_child.id,)) + row_html = 'name(None)' % link self.assertFalse(table_output.find(row_html) == -1, 'Failed to find expected row element: %s' % table_output) @@ -87,7 +89,8 @@ class ChangeListTests(TestCase): template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}') context = Context({'cl': cl}) table_output = template.render(context) - row_html = 'nameParent object' % new_child.id + link = reverse('admin:admin_changelist_child_change', args=(new_child.id,)) + row_html = 'nameParent object' % link self.assertFalse(table_output.find(row_html) == -1, 'Failed to find expected row element: %s' % table_output) @@ -425,7 +428,8 @@ class ChangeListTests(TestCase): request = self._mocked_authenticated_request('/child/', superuser) response = m.changelist_view(request) for i in range(1, 10): - self.assertContains(response, '%s' % (i, i)) + link = reverse('admin:admin_changelist_child_change', args=(i,)) + self.assertContains(response, '%s' % (link, i)) list_display = m.get_list_display(request) list_display_links = m.get_list_display_links(request, list_display) diff --git a/tests/regressiontests/admin_custom_urls/fixtures/actions.json b/tests/regressiontests/admin_custom_urls/fixtures/actions.json index a63cf8135c..7c6341d71d 100644 --- a/tests/regressiontests/admin_custom_urls/fixtures/actions.json +++ b/tests/regressiontests/admin_custom_urls/fixtures/actions.json @@ -40,12 +40,5 @@ "fields": { "description": "An action with a name suspected of being a XSS attempt" } - }, - { - "pk": "The name of an action", - "model": "admin_custom_urls.action", - "fields": { - "description": "A generic action" - } } ] diff --git a/tests/regressiontests/admin_custom_urls/models.py b/tests/regressiontests/admin_custom_urls/models.py index a5b4983b09..b9b3285463 100644 --- a/tests/regressiontests/admin_custom_urls/models.py +++ b/tests/regressiontests/admin_custom_urls/models.py @@ -50,3 +50,40 @@ class ActionAdmin(admin.ModelAdmin): admin.site.register(Action, ActionAdmin) + + +class Person(models.Model): + nick = models.CharField(max_length=20) + + +class PersonAdmin(admin.ModelAdmin): + """A custom ModelAdmin that customizes the deprecated post_url_continue + argument to response_add()""" + def response_add(self, request, obj, post_url_continue='../%s/continue/', + continue_url=None, add_url=None, hasperm_url=None, + noperm_url=None): + return super(PersonAdmin, self).response_add(request, obj, + post_url_continue, + continue_url, add_url, + hasperm_url, noperm_url) + + +admin.site.register(Person, PersonAdmin) + + +class City(models.Model): + name = models.CharField(max_length=20) + + +class CityAdmin(admin.ModelAdmin): + """A custom ModelAdmin that redirects to the changelist when the user + presses the 'Save and add another' button when adding a model instance.""" + def response_add(self, request, obj, + add_another_url='admin:admin_custom_urls_city_changelist', + **kwargs): + return super(CityAdmin, self).response_add(request, obj, + add_another_url=add_another_url, + **kwargs) + + +admin.site.register(City, CityAdmin) diff --git a/tests/regressiontests/admin_custom_urls/tests.py b/tests/regressiontests/admin_custom_urls/tests.py index 64ff9f6692..87c72e2e71 100644 --- a/tests/regressiontests/admin_custom_urls/tests.py +++ b/tests/regressiontests/admin_custom_urls/tests.py @@ -1,11 +1,14 @@ from __future__ import absolute_import, unicode_literals +import warnings + +from django.contrib.admin.util import quote from django.core.urlresolvers import reverse from django.template.response import TemplateResponse from django.test import TestCase from django.test.utils import override_settings -from .models import Action +from .models import Action, Person, City @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @@ -67,7 +70,7 @@ class AdminCustomUrlsTest(TestCase): # Ditto, but use reverse() to build the URL url = reverse('admin:%s_action_change' % Action._meta.app_label, - args=('add',)) + args=(quote('add'),)) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertContains(response, 'Change action') @@ -75,19 +78,50 @@ class AdminCustomUrlsTest(TestCase): # Should correctly get the change_view for the model instance with the # funny-looking PK (the one wth a 'path/to/html/document.html' value) url = reverse('admin:%s_action_change' % Action._meta.app_label, - args=("path/to/html/document.html",)) + args=(quote("path/to/html/document.html"),)) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertContains(response, 'Change action') self.assertContains(response, 'value="path/to/html/document.html"') - def testChangeViewHistoryButton(self): - url = reverse('admin:%s_action_change' % Action._meta.app_label, - args=('The name of an action',)) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - expected_link = reverse('admin:%s_action_history' % - Action._meta.app_label, - args=('The name of an action',)) - self.assertContains(response, '', html=True) + @override_settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=True) + def test_localize_pk_shortcut(self): + """ + Ensure that the "View on Site" link is correct for locales that use + thousand separators + """ + holder = Holder.objects.create(pk=123456789, dummy=42) + inner = Inner.objects.create(pk=987654321, holder=holder, dummy=42, readonly='') + response = self.client.get('/admin/admin_inlines/holder/%i/' % holder.id) + inner_shortcut = 'r/%s/%s/'%(ContentType.objects.get_for_model(inner).pk, inner.pk) + self.assertContains(response, inner_shortcut) + def test_custom_pk_shortcut(self): """ Ensure that the "View on Site" link is correct for models with a diff --git a/tests/regressiontests/admin_scripts/tests.py b/tests/regressiontests/admin_scripts/tests.py index 6028eac846..3bb8bb0b50 100644 --- a/tests/regressiontests/admin_scripts/tests.py +++ b/tests/regressiontests/admin_scripts/tests.py @@ -982,13 +982,11 @@ class ManageMultipleSettings(AdminScriptTestCase): self.assertNoOutput(err) self.assertOutput(out, "EXECUTE:NoArgsCommand") + class ManageSettingsWithImportError(AdminScriptTestCase): """Tests for manage.py when using the default settings.py file with an import error. Ticket #14130. """ - def setUp(self): - self.write_settings_with_import_error('settings.py') - def tearDown(self): self.remove_settings('settings.py') @@ -1004,11 +1002,27 @@ class ManageSettingsWithImportError(AdminScriptTestCase): settings_file.write('# The next line will cause an import error:\nimport foo42bar\n') def test_builtin_command(self): - "import error: manage.py builtin commands shows useful diagnostic info when settings with import errors is provided" + """ + import error: manage.py builtin commands shows useful diagnostic info + when settings with import errors is provided + """ + self.write_settings_with_import_error('settings.py') args = ['sqlall', 'admin_scripts'] out, err = self.run_manage(args) self.assertNoOutput(out) - self.assertOutput(err, "No module named foo42bar") + self.assertOutput(err, "No module named") + self.assertOutput(err, "foo42bar") + + def test_builtin_command_with_attribute_error(self): + """ + manage.py builtin commands does not swallow attribute errors from bad settings (#18845) + """ + self.write_settings('settings.py', sdict={'BAD_VAR': 'INSTALLED_APPS.crash'}) + args = ['collectstatic', 'admin_scripts'] + out, err = self.run_manage(args) + self.assertNoOutput(out) + self.assertOutput(err, "AttributeError: 'list' object has no attribute 'crash'") + class ManageValidate(AdminScriptTestCase): def tearDown(self): @@ -1020,7 +1034,8 @@ class ManageValidate(AdminScriptTestCase): args = ['validate'] out, err = self.run_manage(args) self.assertNoOutput(out) - self.assertOutput(err, 'No module named admin_scriptz') + self.assertOutput(err, 'No module named') + self.assertOutput(err, 'admin_scriptz') def test_broken_app(self): "manage.py validate reports an ImportError if an app's models.py raises one on import" @@ -1590,3 +1605,15 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase): with codecs.open(path, 'r', 'utf-8') as f: self.assertEqual(f.read(), 'Some non-ASCII text for testing ticket #18091:\nüäö €\n') + + +class DiffSettings(AdminScriptTestCase): + """Tests for diffsettings management command.""" + def test_basic(self): + "Runs without error and emits settings diff." + self.write_settings('settings_to_diff.py', sdict={'FOO': '"bar"'}) + args = ['diffsettings', '--settings=settings_to_diff'] + out, err = self.run_manage(args) + self.remove_settings('settings_to_diff.py') + self.assertNoOutput(err) + self.assertOutput(out, "FOO = 'bar' ###") diff --git a/tests/regressiontests/admin_util/models.py b/tests/regressiontests/admin_util/models.py index b3504a1fa4..32a6cd6291 100644 --- a/tests/regressiontests/admin_util/models.py +++ b/tests/regressiontests/admin_util/models.py @@ -39,3 +39,6 @@ class Guest(models.Model): class Meta: verbose_name = "awesome guest" + +class EventGuide(models.Model): + event = models.ForeignKey(Event, on_delete=models.DO_NOTHING) diff --git a/tests/regressiontests/admin_util/tests.py b/tests/regressiontests/admin_util/tests.py index d04740ce95..ef8a91d1db 100644 --- a/tests/regressiontests/admin_util/tests.py +++ b/tests/regressiontests/admin_util/tests.py @@ -17,7 +17,7 @@ from django.utils.formats import localize from django.utils.safestring import mark_safe from django.utils import six -from .models import Article, Count, Event, Location +from .models import Article, Count, Event, Location, EventGuide class NestedObjectsTests(TestCase): @@ -71,6 +71,17 @@ class NestedObjectsTests(TestCase): # Should not require additional queries to populate the nested graph. self.assertNumQueries(2, self._collect, 0) + def test_on_delete_do_nothing(self): + """ + Check that the nested collector doesn't query for DO_NOTHING objects. + """ + n = NestedObjects(using=DEFAULT_DB_ALIAS) + objs = [Event.objects.create()] + EventGuide.objects.create(event=objs[0]) + with self.assertNumQueries(2): + # One for Location, one for Guest, and no query for EventGuide + n.collect(objs) + class UtilTests(unittest.TestCase): def test_values_from_lookup_field(self): """ diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 36fea59f2e..72dc6a3f97 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -52,6 +52,7 @@ from .models import (Article, BarAccount, CustomArticle, EmptyModel, FooAccount, ERROR_MESSAGE = "Please enter the correct username and password \ for a staff account. Note that both fields are case-sensitive." + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminViewBasicTest(TestCase): fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', @@ -141,7 +142,7 @@ class AdminViewBasicTest(TestCase): "article_set-MAX_NUM_FORMS": "0", } response = self.client.post('/test_admin/%s/admin_views/section/add/' % self.urlbit, post_data) - self.assertEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere def testPopupAddPost(self): """ @@ -205,7 +206,7 @@ class AdminViewBasicTest(TestCase): A smoke test to ensure POST on edit_view works. """ response = self.client.post('/test_admin/%s/admin_views/section/1/' % self.urlbit, self.inline_post_data) - self.assertEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere def testEditSaveAs(self): """ @@ -221,7 +222,7 @@ class AdminViewBasicTest(TestCase): "article_set-5-section": "1", }) response = self.client.post('/test_admin/%s/admin_views/section/1/' % self.urlbit, post_data) - self.assertEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere def testChangeListSortingCallable(self): """ @@ -260,19 +261,21 @@ class AdminViewBasicTest(TestCase): p1 = Person.objects.create(name="Chris", gender=1, alive=True) p2 = Person.objects.create(name="Chris", gender=2, alive=True) p3 = Person.objects.create(name="Bob", gender=1, alive=True) - link = 'Horizontal', msg_prefix=fail_msg, html=True) - self.assertContains(response, 'Vertical', msg_prefix=fail_msg, html=True) + self.assertContains(response, 'Horizontal' % link1, msg_prefix=fail_msg, html=True) + self.assertContains(response, 'Vertical' % link2, msg_prefix=fail_msg, html=True) def testNamedGroupFieldChoicesFilter(self): """ @@ -661,7 +672,6 @@ class AdminJavaScriptTest(TestCase): '' ) - def test_js_minified_only_if_debug_is_false(self): """ Ensure that the minified versions of the JS files are only used when @@ -699,7 +709,7 @@ class AdminJavaScriptTest(TestCase): @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class SaveAsTests(TestCase): urls = "regressiontests.admin_views.urls" - fixtures = ['admin-views-users.xml','admin-views-person.xml'] + fixtures = ['admin-views-users.xml', 'admin-views-person.xml'] def setUp(self): self.client.login(username='super', password='secret') @@ -709,7 +719,7 @@ class SaveAsTests(TestCase): def test_save_as_duplication(self): """Ensure save as actually creates a new person""" - post_data = {'_saveasnew':'', 'name':'John M', 'gender':1, 'age': 42} + post_data = {'_saveasnew': '', 'name': 'John M', 'gender': 1, 'age': 42} response = self.client.post('/test_admin/admin/admin_views/person/1/', post_data) self.assertEqual(len(Person.objects.filter(name='John M')), 1) self.assertEqual(len(Person.objects.filter(id=1)), 1) @@ -722,10 +732,11 @@ class SaveAsTests(TestCase): """ response = self.client.get('/test_admin/admin/admin_views/person/1/') self.assertTrue(response.context['save_as']) - post_data = {'_saveasnew':'', 'name':'John M', 'gender':3, 'alive':'checked'} + post_data = {'_saveasnew': '', 'name': 'John M', 'gender': 3, 'alive': 'checked'} response = self.client.post('/test_admin/admin/admin_views/person/1/', post_data) self.assertEqual(response.context['form_url'], '/test_admin/admin/admin_views/person/add/') + class CustomModelAdminTest(AdminViewBasicTest): urls = "regressiontests.admin_views.urls" urlbit = "admin2" @@ -781,11 +792,13 @@ class CustomModelAdminTest(AdminViewBasicTest): response = self.client.get('/test_admin/%s/my_view/' % self.urlbit) self.assertEqual(response.content, b"Django is a magical pony!") + def get_perm(Model, perm): """Return the permission object, for the Model""" ct = ContentType.objects.get_for_model(Model) return Permission.objects.get(content_type=ct, codename=perm) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminViewPermissionsTest(TestCase): """Tests for Admin Views Permissions.""" @@ -884,17 +897,17 @@ class AdminViewPermissionsTest(TestCase): self.assertFalse(login.context) self.client.get('/test_admin/admin/logout/') - # Test if user enters e-mail address + # Test if user enters email address response = self.client.get('/test_admin/admin/') self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/', self.super_email_login) - self.assertContains(login, "Your e-mail address is not your username") + self.assertContains(login, ERROR_MESSAGE) # only correct passwords get a username hint login = self.client.post('/test_admin/admin/', self.super_email_bad_login) self.assertContains(login, ERROR_MESSAGE) new_user = User(username='jondoe', password='secret', email='super@example.com') new_user.save() - # check to ensure if there are multiple e-mail addresses a user doesn't get a 500 + # check to ensure if there are multiple email addresses a user doesn't get a 500 login = self.client.post('/test_admin/admin/', self.super_email_login) self.assertContains(login, ERROR_MESSAGE) @@ -949,7 +962,7 @@ class AdminViewPermissionsTest(TestCase): def testAddView(self): """Test add view restricts access and actually adds items.""" - add_dict = {'title' : 'Døm ikke', + add_dict = {'title': 'Døm ikke', 'content': '

    great article

    ', 'date_0': '2008-03-18', 'date_1': '10:54:39', 'section': 1} @@ -1004,7 +1017,7 @@ class AdminViewPermissionsTest(TestCase): def testChangeView(self): """Change view should restrict access and allow users to edit items.""" - change_dict = {'title' : 'Ikke fordømt', + change_dict = {'title': 'Ikke fordømt', 'content': '

    edited article

    ', 'date_0': '2008-03-18', 'date_1': '10:54:39', 'section': 1} @@ -1336,6 +1349,7 @@ class AdminViewDeletedObjectsTest(TestCase): response = self.client.get('/test_admin/admin/admin_views/plot/%s/delete/' % quote(3)) self.assertContains(response, should_contain) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminViewStringPrimaryKeyTest(TestCase): urls = "regressiontests.admin_views.urls" @@ -1371,9 +1385,12 @@ class AdminViewStringPrimaryKeyTest(TestCase): self.assertEqual(response.status_code, 200) def test_changelist_to_changeform_link(self): - "The link from the changelist referring to the changeform of the object should be quoted" - response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/') - should_contain = """%s""" % (escape(quote(self.pk)), escape(self.pk)) + "Link to the changeform of the object in changelist should use reverse() and be quoted -- #18072" + prefix = '/test_admin/admin/admin_views/modelwithstringprimarykey/' + response = self.client.get(prefix) + # this URL now comes through reverse(), thus iri_to_uri encoding + pk_final_url = escape(iri_to_uri(quote(self.pk))) + should_contain = """%s""" % (prefix, pk_final_url, escape(self.pk)) self.assertContains(response, should_contain) def test_recentactions_link(self): @@ -1387,7 +1404,7 @@ class AdminViewStringPrimaryKeyTest(TestCase): response = self.client.get('/test_admin/admin/') should_contain = """%s""" % (escape(quote(self.pk)), escape(self.pk)) self.assertContains(response, should_contain) - should_contain = "Model with string primary key" # capitalized in Recent Actions + should_contain = "Model with string primary key" # capitalized in Recent Actions self.assertContains(response, should_contain) logentry = LogEntry.objects.get(content_type__name__iexact=should_contain) # http://code.djangoproject.com/ticket/10275 @@ -1441,6 +1458,18 @@ class AdminViewStringPrimaryKeyTest(TestCase): should_contain = '/%s/" class="viewsitelink">' % model.pk self.assertContains(response, should_contain) + def test_change_view_history_link(self): + """Object history button link should work and contain the pk value quoted.""" + url = reverse('admin:%s_modelwithstringprimarykey_change' % + ModelWithStringPrimaryKey._meta.app_label, + args=(quote(self.pk),)) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + expected_link = reverse('admin:%s_modelwithstringprimarykey_history' % + ModelWithStringPrimaryKey._meta.app_label, + args=(quote(self.pk),)) + self.assertContains(response, '
    %d' % (story1.id, story1.id), 1) - self.assertContains(response, '%d' % (story2.id, story2.id), 1) + self.assertContains(response, '%d' % (link1, story1.id), 1) + self.assertContains(response, '%d' % (link2, story2.id), 1) @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @@ -2082,7 +2114,7 @@ class AdminSearchTest(TestCase): @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminInheritedInlinesTest(TestCase): urls = "regressiontests.admin_views.urls" - fixtures = ['admin-views-users.xml',] + fixtures = ['admin-views-users.xml'] def setUp(self): self.client.login(username='super', password='secret') @@ -2119,7 +2151,7 @@ class AdminInheritedInlinesTest(TestCase): } response = self.client.post('/test_admin/admin/admin_views/persona/add/', post_data) - self.assertEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere self.assertEqual(Persona.objects.count(), 1) self.assertEqual(FooAccount.objects.count(), 1) self.assertEqual(BarAccount.objects.count(), 1) @@ -2166,6 +2198,7 @@ class AdminInheritedInlinesTest(TestCase): self.assertEqual(BarAccount.objects.all()[0].username, "%s-1" % bar_user) self.assertEqual(Persona.objects.all()[0].accounts.count(), 2) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminActionsTest(TestCase): urls = "regressiontests.admin_views.urls" @@ -2181,7 +2214,7 @@ class AdminActionsTest(TestCase): "Tests a custom action defined in a ModelAdmin method" action_data = { ACTION_CHECKBOX_NAME: [1], - 'action' : 'mail_admin', + 'action': 'mail_admin', 'index': 0, } response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) @@ -2192,12 +2225,12 @@ class AdminActionsTest(TestCase): "Tests the default delete action defined as a ModelAdmin method" action_data = { ACTION_CHECKBOX_NAME: [1, 2], - 'action' : 'delete_selected', + 'action': 'delete_selected', 'index': 0, } delete_confirmation_data = { ACTION_CHECKBOX_NAME: [1, 2], - 'action' : 'delete_selected', + 'action': 'delete_selected', 'post': 'yes', } confirmation = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) @@ -2221,12 +2254,12 @@ class AdminActionsTest(TestCase): subscriber.save() action_data = { ACTION_CHECKBOX_NAME: [9999, 2], - 'action' : 'delete_selected', + 'action': 'delete_selected', 'index': 0, } response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) self.assertTemplateUsed(response, 'admin/delete_selected_confirmation.html') - self.assertContains(response, 'value="9999"') # Instead of 9,999 + self.assertContains(response, 'value="9999"') # Instead of 9,999 self.assertContains(response, 'value="2"') settings.USE_THOUSAND_SEPARATOR = self.old_USE_THOUSAND_SEPARATOR settings.USE_L10N = self.old_USE_L10N @@ -2243,7 +2276,7 @@ class AdminActionsTest(TestCase): action_data = { ACTION_CHECKBOX_NAME: [q1.pk, q2.pk], - 'action' : 'delete_selected', + 'action': 'delete_selected', 'index': 0, } @@ -2257,7 +2290,7 @@ class AdminActionsTest(TestCase): "Tests a custom action defined in a function" action_data = { ACTION_CHECKBOX_NAME: [1], - 'action' : 'external_mail', + 'action': 'external_mail', 'index': 0, } response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) @@ -2268,7 +2301,7 @@ class AdminActionsTest(TestCase): "Tests a custom action defined in a function" action_data = { ACTION_CHECKBOX_NAME: [1], - 'action' : 'redirect_to', + 'action': 'redirect_to', 'index': 0, } response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) @@ -2282,7 +2315,7 @@ class AdminActionsTest(TestCase): """ action_data = { ACTION_CHECKBOX_NAME: [1], - 'action' : 'external_mail', + 'action': 'external_mail', 'index': 0, } url = '/test_admin/admin/admin_views/externalsubscriber/?o=1' @@ -2347,7 +2380,7 @@ class AdminActionsTest(TestCase): """ action_data = { ACTION_CHECKBOX_NAME: [], - 'action' : 'delete_selected', + 'action': 'delete_selected', 'index': 0, } response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) @@ -2361,7 +2394,7 @@ class AdminActionsTest(TestCase): """ action_data = { ACTION_CHECKBOX_NAME: [1, 2], - 'action' : '', + 'action': '', 'index': 0, } response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) @@ -2405,7 +2438,7 @@ class TestCustomChangeList(TestCase): # Insert some data post_data = {"name": "First Gadget"} response = self.client.post('/test_admin/%s/admin_views/gadget/add/' % self.urlbit, post_data) - self.assertEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere # Hit the page once to get messages out of the queue message list response = self.client.get('/test_admin/%s/admin_views/gadget/' % self.urlbit) # Ensure that that data is still not visible on the page @@ -2433,6 +2466,7 @@ class TestInlineNotEditable(TestCase): response = self.client.get('/test_admin/admin/admin_views/parent/add/') self.assertEqual(response.status_code, 200) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminCustomQuerysetTest(TestCase): urls = "regressiontests.admin_views.urls" @@ -2489,6 +2523,7 @@ class AdminCustomQuerysetTest(TestCase): # Message should contain non-ugly model name. Instance representation is set by model's __unicode__() self.assertContains(response, '
  • The cover letter "John Doe II" was changed successfully.
  • ', html=True) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminInlineFileUploadTest(TestCase): urls = "regressiontests.admin_views.urls" @@ -2629,7 +2664,7 @@ class AdminInlineTests(TestCase): result = self.client.login(username='super', password='secret') self.assertEqual(result, True) - self.collector = Collector(pk=1,name='John Fowles') + self.collector = Collector(pk=1, name='John Fowles') self.collector.save() def tearDown(self): @@ -2955,14 +2990,14 @@ class PrePopulatedTest(TestCase): self.assertNotContains(response, "field['dependency_ids'].push('#id_title');") self.assertNotContains(response, "id: '#id_prepopulatedsubpost_set-0-subslug',") - @override_settings(USE_THOUSAND_SEPARATOR = True, USE_L10N = True) + @override_settings(USE_THOUSAND_SEPARATOR=True, USE_L10N=True) def test_prepopulated_maxlength_localized(self): """ Regression test for #15938: if USE_THOUSAND_SEPARATOR is set, make sure that maxLength (in the JavaScript) is rendered without separators. """ response = self.client.get('/test_admin/admin/admin_views/prepopulatedpostlargeslug/add/') - self.assertContains(response, "maxLength: 1000") # instead of 1,000 + self.assertContains(response, "maxLength: 1000") # instead of 1,000 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @@ -3008,8 +3043,8 @@ class SeleniumPrePopulatedFirefoxTests(AdminSeleniumWebDriverTestCase): self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-1-name').send_keys(' now you haVe anöther sŤāÇkeð inline with a very ... loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooog text... ') slug1 = self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-1-slug1').get_attribute('value') slug2 = self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-1-slug2').get_attribute('value') - self.assertEqual(slug1, 'now-you-have-another-stacked-inline-very-loooooooo') # 50 characters maximum for slug1 field - self.assertEqual(slug2, 'option-two-now-you-have-another-stacked-inline-very-looooooo') # 60 characters maximum for slug2 field + self.assertEqual(slug1, 'now-you-have-another-stacked-inline-very-loooooooo') # 50 characters maximum for slug1 field + self.assertEqual(slug2, 'option-two-now-you-have-another-stacked-inline-very-looooooo') # 60 characters maximum for slug2 field # Tabular inlines ---------------------------------------------------- # Initial inline @@ -3060,7 +3095,7 @@ class SeleniumPrePopulatedFirefoxTests(AdminSeleniumWebDriverTestCase): slug2='option-one-here-stacked-inline', ) RelatedPrepopulated.objects.get( - name=' now you haVe anöther sŤāÇkeð inline with a very ... loooooooooooooooooo', # 75 characters in name field + name=' now you haVe anöther sŤāÇkeð inline with a very ... loooooooooooooooooo', # 75 characters in name field pubdate='1999-01-25', status='option two', slug1='now-you-have-another-stacked-inline-very-loooooooo', @@ -3085,6 +3120,7 @@ class SeleniumPrePopulatedFirefoxTests(AdminSeleniumWebDriverTestCase): class SeleniumPrePopulatedChromeTests(SeleniumPrePopulatedFirefoxTests): webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver' + class SeleniumPrePopulatedIETests(SeleniumPrePopulatedFirefoxTests): webdriver_class = 'selenium.webdriver.ie.webdriver.WebDriver' @@ -3145,7 +3181,7 @@ class ReadonlyTest(TestCase): p = Post.objects.get() self.assertEqual(p.posted, datetime.date.today()) - data["posted"] = "10-8-1990" # some date that's not today + data["posted"] = "10-8-1990" # some date that's not today response = self.client.post('/test_admin/admin/admin_views/post/add/', data) self.assertEqual(response.status_code, 302) self.assertEqual(Post.objects.count(), 2) @@ -3187,7 +3223,7 @@ class RawIdFieldsTest(TestCase): response = self.client.get('/test_admin/admin/admin_views/sketch/add/') # Find the link m = re.search(br']* id="lookup_id_inquisition"', response.content) - self.assertTrue(m) # Got a match + self.assertTrue(m) # Got a match popup_url = m.groups()[0].decode().replace("&", "&") # Handle relative links @@ -3197,6 +3233,7 @@ class RawIdFieldsTest(TestCase): self.assertContains(response2, "Spain") self.assertNotContains(response2, "England") + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class UserAdminTest(TestCase): """ @@ -3351,7 +3388,7 @@ class CSSTest(TestCase): self.assertContains(response, 'class="form-row field-awesomeness_level"') self.assertContains(response, 'class="form-row field-coolness"') self.assertContains(response, 'class="form-row field-value"') - self.assertContains(response, 'class="form-row"') # The lambda function + self.assertContains(response, 'class="form-row"') # The lambda function # The tabular inline self.assertContains(response, '') @@ -3363,6 +3400,7 @@ try: except ImportError: docutils = None + @unittest.skipUnless(docutils, "no docutils installed.") @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminDocsTest(TestCase): @@ -3421,7 +3459,7 @@ class ValidXHTMLTests(TestCase): @override_settings( TEMPLATE_CONTEXT_PROCESSORS=filter( - lambda t:t!='django.core.context_processors.i18n', + lambda t: t != 'django.core.context_processors.i18n', global_settings.TEMPLATE_CONTEXT_PROCESSORS), USE_I18N=False, ) @@ -3558,6 +3596,7 @@ class DateHierarchyTests(TestCase): self.assert_non_localized_year(response, 2003) self.assert_non_localized_year(response, 2005) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminCustomSaveRelatedTests(TestCase): """ diff --git a/tests/regressiontests/aggregation_regress/tests.py b/tests/regressiontests/aggregation_regress/tests.py index b9f3ab27eb..af0f421502 100644 --- a/tests/regressiontests/aggregation_regress/tests.py +++ b/tests/regressiontests/aggregation_regress/tests.py @@ -878,3 +878,14 @@ class AggregationTests(TestCase): connection.ops.convert_values(testData, testField), testData ) + + def test_annotate_joins(self): + """ + Test that the base table's join isn't promoted to LOUTER. This could + cause the query generation to fail if there is an exclude() for fk-field + in the query, too. Refs #19087. + """ + qs = Book.objects.annotate(n=Count('pk')) + self.assertIs(qs.query.alias_map['aggregation_regress_book'].join_type, None) + # Check that the query executes without problems. + self.assertEqual(len(qs.exclude(publisher=-1)), 6) diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py index cfa298253c..d284cfaac8 100644 --- a/tests/regressiontests/backends/tests.py +++ b/tests/regressiontests/backends/tests.py @@ -666,13 +666,6 @@ class ThreadTests(TestCase): self.assertEqual(len(exceptions), 0) -class BackendLoadingTests(TestCase): - def test_old_style_backends_raise_useful_exception(self): - six.assertRaisesRegex(self, ImproperlyConfigured, - "Try using django.db.backends.sqlite3 instead", - load_backend, 'sqlite3') - - class MySQLPKZeroTests(TestCase): """ Zero as id for AutoField should raise exception in MySQL, because MySQL diff --git a/tests/regressiontests/bulk_create/tests.py b/tests/regressiontests/bulk_create/tests.py index 33108ea9b0..5d61242b9b 100644 --- a/tests/regressiontests/bulk_create/tests.py +++ b/tests/regressiontests/bulk_create/tests.py @@ -3,7 +3,7 @@ from __future__ import absolute_import from operator import attrgetter from django.db import connection -from django.test import TestCase, skipIfDBFeature +from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature from django.test.utils import override_settings from .models import Country, Restaurant, Pizzeria, State, TwoFields @@ -29,6 +29,7 @@ class BulkCreateTests(TestCase): self.assertEqual(created, []) self.assertEqual(Country.objects.count(), 4) + @skipUnlessDBFeature('has_bulk_insert') def test_efficiency(self): with self.assertNumQueries(1): Country.objects.bulk_create(self.data) @@ -50,6 +51,16 @@ class BulkCreateTests(TestCase): ], attrgetter("name")) def test_non_auto_increment_pk(self): + State.objects.bulk_create([ + State(two_letter_code=s) + for s in ["IL", "NY", "CA", "ME"] + ]) + self.assertQuerysetEqual(State.objects.order_by("two_letter_code"), [ + "CA", "IL", "ME", "NY", + ], attrgetter("two_letter_code")) + + @skipUnlessDBFeature('has_bulk_insert') + def test_non_auto_increment_pk_efficiency(self): with self.assertNumQueries(1): State.objects.bulk_create([ State(two_letter_code=s) @@ -77,13 +88,21 @@ class BulkCreateTests(TestCase): TwoFields.objects.bulk_create([ TwoFields(f1=i, f2=i+1) for i in range(0, 1001) ]) - self.assertTrue(len(connection.queries) < 10) self.assertEqual(TwoFields.objects.count(), 1001) self.assertEqual( TwoFields.objects.filter(f1__gte=450, f1__lte=550).count(), 101) self.assertEqual(TwoFields.objects.filter(f2__gte=901).count(), 101) + @skipUnlessDBFeature('has_bulk_insert') + def test_large_batch_efficiency(self): + with override_settings(DEBUG=True): + connection.queries = [] + TwoFields.objects.bulk_create([ + TwoFields(f1=i, f2=i+1) for i in range(0, 1001) + ]) + self.assertTrue(len(connection.queries) < 10) + def test_large_batch_mixed(self): """ Test inserting a large batch with objects having primary key set @@ -94,7 +113,6 @@ class BulkCreateTests(TestCase): TwoFields.objects.bulk_create([ TwoFields(id=i if i % 2 == 0 else None, f1=i, f2=i+1) for i in range(100000, 101000)]) - self.assertTrue(len(connection.queries) < 10) self.assertEqual(TwoFields.objects.count(), 1000) # We can't assume much about the ID's created, except that the above # created IDs must exist. @@ -102,7 +120,29 @@ class BulkCreateTests(TestCase): self.assertEqual(TwoFields.objects.filter(id__in=id_range).count(), 500) self.assertEqual(TwoFields.objects.exclude(id__in=id_range).count(), 500) + @skipUnlessDBFeature('has_bulk_insert') + def test_large_batch_mixed_efficiency(self): + """ + Test inserting a large batch with objects having primary key set + mixed together with objects without PK set. + """ + with override_settings(DEBUG=True): + connection.queries = [] + TwoFields.objects.bulk_create([ + TwoFields(id=i if i % 2 == 0 else None, f1=i, f2=i+1) + for i in range(100000, 101000)]) + self.assertTrue(len(connection.queries) < 10) + def test_explicit_batch_size(self): + objs = [TwoFields(f1=i, f2=i) for i in range(0, 4)] + TwoFields.objects.bulk_create(objs, 2) + self.assertEqual(TwoFields.objects.count(), len(objs)) + TwoFields.objects.all().delete() + TwoFields.objects.bulk_create(objs, len(objs)) + self.assertEqual(TwoFields.objects.count(), len(objs)) + + @skipUnlessDBFeature('has_bulk_insert') + def test_explicit_batch_size_efficiency(self): objs = [TwoFields(f1=i, f2=i) for i in range(0, 100)] with self.assertNumQueries(2): TwoFields.objects.bulk_create(objs, 50) diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py index de27bc9476..a6eff8950b 100644 --- a/tests/regressiontests/cache/tests.py +++ b/tests/regressiontests/cache/tests.py @@ -19,7 +19,8 @@ from django.core.cache import get_cache from django.core.cache.backends.base import (CacheKeyWarning, InvalidCacheBackendError) from django.db import router -from django.http import HttpResponse, HttpRequest, QueryDict +from django.http import (HttpResponse, HttpRequest, StreamingHttpResponse, + QueryDict) from django.middleware.cache import (FetchFromCacheMiddleware, UpdateCacheMiddleware, CacheMiddleware) from django.template import Template @@ -1416,6 +1417,29 @@ class CacheI18nTest(TestCase): # reset the language translation.deactivate() + @override_settings( + CACHE_MIDDLEWARE_KEY_PREFIX="test", + CACHE_MIDDLEWARE_SECONDS=60, + USE_ETAGS=True, + ) + def test_middleware_with_streaming_response(self): + # cache with non empty request.GET + request = self._get_request_cache(query_string='foo=baz&other=true') + + # first access, cache must return None + get_cache_data = FetchFromCacheMiddleware().process_request(request) + self.assertEqual(get_cache_data, None) + + # pass streaming response through UpdateCacheMiddleware. + content = 'Check for cache with QUERY_STRING and streaming content' + response = StreamingHttpResponse(content) + UpdateCacheMiddleware().process_response(request, response) + + # second access, cache must still return None, because we can't cache + # streaming response. + get_cache_data = FetchFromCacheMiddleware().process_request(request) + self.assertEqual(get_cache_data, None) + @override_settings( CACHES={ diff --git a/tests/regressiontests/comment_tests/tests/feed_tests.py b/tests/regressiontests/comment_tests/tests/feed_tests.py index b15ec0c7b8..1ec316eab8 100644 --- a/tests/regressiontests/comment_tests/tests/feed_tests.py +++ b/tests/regressiontests/comment_tests/tests/feed_tests.py @@ -1,12 +1,32 @@ from __future__ import absolute_import +from django.conf import settings +from django.contrib.comments.models import Comment +from django.contrib.contenttypes.models import ContentType +from django.contrib.sites.models import Site + from . import CommentTestCase +from ..models import Article class CommentFeedTests(CommentTestCase): urls = 'regressiontests.comment_tests.urls' feed_url = '/rss/comments/' + def setUp(self): + site_2 = Site.objects.create(id=settings.SITE_ID+1, + domain="example2.com", name="example2.com") + # A comment for another site + c5 = Comment.objects.create( + content_type = ContentType.objects.get_for_model(Article), + object_pk = "1", + user_name = "Joe Somebody", + user_email = "jsomebody@example.com", + user_url = "http://example.com/~joe/", + comment = "A comment for the second site.", + site = site_2, + ) + def test_feed(self): response = self.client.get(self.feed_url) self.assertEqual(response.status_code, 200) @@ -15,3 +35,4 @@ class CommentFeedTests(CommentTestCase): self.assertContains(response, 'example.com comments') self.assertContains(response, 'http://example.com/') self.assertContains(response, '') + self.assertNotContains(response, "A comment for the second site.") diff --git a/tests/regressiontests/comment_tests/urls.py b/tests/regressiontests/comment_tests/urls.py index b2f676786f..0a7e8b5fdf 100644 --- a/tests/regressiontests/comment_tests/urls.py +++ b/tests/regressiontests/comment_tests/urls.py @@ -15,6 +15,7 @@ urlpatterns = patterns('', url(r'^flag/(\d+)/$', views.custom_flag_comment), url(r'^delete/(\d+)/$', views.custom_delete_comment), url(r'^approve/(\d+)/$', views.custom_approve_comment), + url(r'^cr/(\d+)/(.+)/$', 'django.contrib.contenttypes.views.shortcut', name='comments-url-redirect'), ) urlpatterns += patterns('', diff --git a/tests/regressiontests/conditional_processing/models.py b/tests/regressiontests/conditional_processing/models.py index d0838e153d..b47fdf6fb5 100644 --- a/tests/regressiontests/conditional_processing/models.py +++ b/tests/regressiontests/conditional_processing/models.py @@ -4,8 +4,6 @@ from __future__ import unicode_literals from datetime import datetime from django.test import TestCase -from django.utils import unittest -from django.utils.http import parse_etags, quote_etag, parse_http_date FULL_RESPONSE = 'Test conditional get response' @@ -129,30 +127,3 @@ class ConditionalGet(TestCase): self.client.defaults['HTTP_IF_NONE_MATCH'] = r'"\"' response = self.client.get('/condition/etag/') self.assertFullResponse(response, check_last_modified=False) - - -class ETagProcessing(unittest.TestCase): - def testParsing(self): - etags = parse_etags(r'"", "etag", "e\"t\"ag", "e\\tag", W/"weak"') - self.assertEqual(etags, ['', 'etag', 'e"t"ag', r'e\tag', 'weak']) - - def testQuoting(self): - quoted_etag = quote_etag(r'e\t"ag') - self.assertEqual(quoted_etag, r'"e\\t\"ag"') - - -class HttpDateProcessing(unittest.TestCase): - def testParsingRfc1123(self): - parsed = parse_http_date('Sun, 06 Nov 1994 08:49:37 GMT') - self.assertEqual(datetime.utcfromtimestamp(parsed), - datetime(1994, 11, 6, 8, 49, 37)) - - def testParsingRfc850(self): - parsed = parse_http_date('Sunday, 06-Nov-94 08:49:37 GMT') - self.assertEqual(datetime.utcfromtimestamp(parsed), - datetime(1994, 11, 6, 8, 49, 37)) - - def testParsingAsctime(self): - parsed = parse_http_date('Sun Nov 6 08:49:37 1994') - self.assertEqual(datetime.utcfromtimestamp(parsed), - datetime(1994, 11, 6, 8, 49, 37)) diff --git a/tests/regressiontests/delete_regress/models.py b/tests/regressiontests/delete_regress/models.py index 5db253f713..dbe383fb41 100644 --- a/tests/regressiontests/delete_regress/models.py +++ b/tests/regressiontests/delete_regress/models.py @@ -2,7 +2,6 @@ from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.db import models - class Award(models.Model): name = models.CharField(max_length=25) object_id = models.PositiveIntegerField() @@ -93,3 +92,10 @@ class FooPhoto(models.Model): class FooFileProxy(FooFile): class Meta: proxy = True + +class OrgUnit(models.Model): + name = models.CharField(max_length=64, unique=True) + +class Login(models.Model): + description = models.CharField(max_length=32) + orgunit = models.ForeignKey(OrgUnit) diff --git a/tests/regressiontests/delete_regress/tests.py b/tests/regressiontests/delete_regress/tests.py index 32feae2ded..f94bb2f20c 100644 --- a/tests/regressiontests/delete_regress/tests.py +++ b/tests/regressiontests/delete_regress/tests.py @@ -3,12 +3,13 @@ from __future__ import absolute_import import datetime from django.conf import settings -from django.db import backend, transaction, DEFAULT_DB_ALIAS +from django.db import backend, transaction, DEFAULT_DB_ALIAS, models from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature from .models import (Book, Award, AwardNote, Person, Child, Toy, PlayedWith, PlayedWithNote, Email, Researcher, Food, Eaten, Policy, Version, Location, - Item, Image, File, Photo, FooFile, FooImage, FooPhoto, FooFileProxy) + Item, Image, File, Photo, FooFile, FooImage, FooPhoto, FooFileProxy, Login, + OrgUnit) # Can't run this test under SQLite, because you can't @@ -139,17 +140,24 @@ class DeleteCascadeTransactionTests(TransactionTestCase): eaten = Eaten.objects.create(food=apple, meal="lunch") apple.delete() + self.assertFalse(Food.objects.exists()) + self.assertFalse(Eaten.objects.exists()) + class LargeDeleteTests(TestCase): def test_large_deletes(self): "Regression for #13309 -- if the number of objects > chunk size, deletion still occurs" for x in range(300): track = Book.objects.create(pagecount=x+100) + # attach a signal to make sure we will not fast-delete + def noop(*args, **kwargs): + pass + models.signals.post_delete.connect(noop, sender=Book) Book.objects.all().delete() + models.signals.post_delete.disconnect(noop, sender=Book) self.assertEqual(Book.objects.count(), 0) - class ProxyDeleteTest(TestCase): """ Tests on_delete behavior for proxy models. @@ -258,3 +266,92 @@ class ProxyDeleteTest(TestCase): Image.objects.all().delete() self.assertEqual(len(FooFileProxy.objects.all()), 0) + + def test_19187_values(self): + with self.assertRaises(TypeError): + Image.objects.values().delete() + with self.assertRaises(TypeError): + Image.objects.values_list().delete() + +class Ticket19102Tests(TestCase): + """ + Test different queries which alter the SELECT clause of the query. We + also must be using a subquery for the deletion (that is, the original + query has a join in it). The deletion should be done as "fast-path" + deletion (that is, just one query for the .delete() call). + + Note that .values() is not tested here on purpose. .values().delete() + doesn't work for non fast-path deletes at all. + """ + def setUp(self): + self.o1 = OrgUnit.objects.create(name='o1') + self.o2 = OrgUnit.objects.create(name='o2') + self.l1 = Login.objects.create(description='l1', orgunit=self.o1) + self.l2 = Login.objects.create(description='l2', orgunit=self.o2) + + @skipUnlessDBFeature("update_can_self_select") + def test_ticket_19102_annotate(self): + with self.assertNumQueries(1): + Login.objects.order_by('description').filter( + orgunit__name__isnull=False + ).annotate( + n=models.Count('description') + ).filter( + n=1, pk=self.l1.pk + ).delete() + self.assertFalse(Login.objects.filter(pk=self.l1.pk).exists()) + self.assertTrue(Login.objects.filter(pk=self.l2.pk).exists()) + + @skipUnlessDBFeature("update_can_self_select") + def test_ticket_19102_extra(self): + with self.assertNumQueries(1): + Login.objects.order_by('description').filter( + orgunit__name__isnull=False + ).extra( + select={'extraf':'1'} + ).filter( + pk=self.l1.pk + ).delete() + self.assertFalse(Login.objects.filter(pk=self.l1.pk).exists()) + self.assertTrue(Login.objects.filter(pk=self.l2.pk).exists()) + + @skipUnlessDBFeature("update_can_self_select") + @skipUnlessDBFeature('can_distinct_on_fields') + def test_ticket_19102_distinct_on(self): + # Both Login objs should have same description so that only the one + # having smaller PK will be deleted. + Login.objects.update(description='description') + with self.assertNumQueries(1): + Login.objects.distinct('description').order_by('pk').filter( + orgunit__name__isnull=False + ).delete() + # Assumed that l1 which is created first has smaller PK. + self.assertFalse(Login.objects.filter(pk=self.l1.pk).exists()) + self.assertTrue(Login.objects.filter(pk=self.l2.pk).exists()) + + @skipUnlessDBFeature("update_can_self_select") + def test_ticket_19102_select_related(self): + with self.assertNumQueries(1): + Login.objects.filter( + pk=self.l1.pk + ).filter( + orgunit__name__isnull=False + ).order_by( + 'description' + ).select_related('orgunit').delete() + self.assertFalse(Login.objects.filter(pk=self.l1.pk).exists()) + self.assertTrue(Login.objects.filter(pk=self.l2.pk).exists()) + + @skipUnlessDBFeature("update_can_self_select") + def test_ticket_19102_defer(self): + with self.assertNumQueries(1): + Login.objects.filter( + pk=self.l1.pk + ).filter( + orgunit__name__isnull=False + ).order_by( + 'description' + ).only('id').delete() + self.assertFalse(Login.objects.filter(pk=self.l1.pk).exists()) + self.assertTrue(Login.objects.filter(pk=self.l2.pk).exists()) + diff --git a/tests/regressiontests/dispatch/tests/test_dispatcher.py b/tests/regressiontests/dispatch/tests/test_dispatcher.py index 5f7094d5fa..5f8f92acaf 100644 --- a/tests/regressiontests/dispatch/tests/test_dispatcher.py +++ b/tests/regressiontests/dispatch/tests/test_dispatcher.py @@ -126,6 +126,17 @@ class DispatcherTests(unittest.TestCase): a_signal.disconnect(receiver_3) self._testIsClean(a_signal) + def test_has_listeners(self): + self.assertFalse(a_signal.has_listeners()) + self.assertFalse(a_signal.has_listeners(sender=object())) + receiver_1 = Callable() + a_signal.connect(receiver_1) + self.assertTrue(a_signal.has_listeners()) + self.assertTrue(a_signal.has_listeners(sender=object())) + a_signal.disconnect(receiver_1) + self.assertFalse(a_signal.has_listeners()) + self.assertFalse(a_signal.has_listeners(sender=object())) + class ReceiverTestCase(unittest.TestCase): """ diff --git a/tests/regressiontests/expressions_regress/tests.py b/tests/regressiontests/expressions_regress/tests.py index 80ddfadbe7..508a497151 100644 --- a/tests/regressiontests/expressions_regress/tests.py +++ b/tests/regressiontests/expressions_regress/tests.py @@ -128,7 +128,7 @@ class ExpressionOperatorTests(TestCase): def test_lefthand_bitwise_and(self): # LH Bitwise ands on integers - Number.objects.filter(pk=self.n.pk).update(integer=F('integer') & 56) + Number.objects.filter(pk=self.n.pk).update(integer=F('integer').bitand(56)) self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 40) self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(15.500, places=3)) @@ -136,7 +136,7 @@ class ExpressionOperatorTests(TestCase): @skipUnlessDBFeature('supports_bitwise_or') def test_lefthand_bitwise_or(self): # LH Bitwise or on integers - Number.objects.filter(pk=self.n.pk).update(integer=F('integer') | 48) + Number.objects.filter(pk=self.n.pk).update(integer=F('integer').bitor(48)) self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 58) self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(15.500, places=3)) @@ -181,20 +181,6 @@ class ExpressionOperatorTests(TestCase): self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 27) self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(15.500, places=3)) - def test_right_hand_bitwise_and(self): - # RH Bitwise ands on integers - Number.objects.filter(pk=self.n.pk).update(integer=15 & F('integer')) - - self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 10) - self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(15.500, places=3)) - - @skipUnlessDBFeature('supports_bitwise_or') - def test_right_hand_bitwise_or(self): - # RH Bitwise or on integers - Number.objects.filter(pk=self.n.pk).update(integer=15 | F('integer')) - - self.assertEqual(Number.objects.get(pk=self.n.pk).integer, 47) - self.assertEqual(Number.objects.get(pk=self.n.pk).float, Approximate(15.500, places=3)) class FTimeDeltaTests(TestCase): @@ -385,7 +371,7 @@ class FTimeDeltaTests(TestCase): def test_delta_invalid_op_and(self): raised = False try: - r = repr(Experiment.objects.filter(end__lt=F('start')&self.deltas[0])) + r = repr(Experiment.objects.filter(end__lt=F('start').bitand(self.deltas[0]))) except TypeError: raised = True self.assertTrue(raised, "TypeError not raised on attempt to binary and a datetime with a timedelta.") @@ -393,7 +379,7 @@ class FTimeDeltaTests(TestCase): def test_delta_invalid_op_or(self): raised = False try: - r = repr(Experiment.objects.filter(end__lt=F('start')|self.deltas[0])) + r = repr(Experiment.objects.filter(end__lt=F('start').bitor(self.deltas[0]))) except TypeError: raised = True self.assertTrue(raised, "TypeError not raised on attempt to binary or a datetime with a timedelta.") diff --git a/tests/regressiontests/file_storage/tests.py b/tests/regressiontests/file_storage/tests.py index 281041e651..595b65d9f1 100644 --- a/tests/regressiontests/file_storage/tests.py +++ b/tests/regressiontests/file_storage/tests.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, unicode_literals import errno import os import shutil +import sys import tempfile import time from datetime import datetime, timedelta @@ -23,6 +24,7 @@ from django.core.files.uploadedfile import UploadedFile from django.test import SimpleTestCase from django.utils import six from django.utils import unittest +from django.test.utils import override_settings from ..servers.tests import LiveServerBase # Try to import PIL in either of the two ways it can end up installed. @@ -76,7 +78,7 @@ class GetStorageClassTests(SimpleTestCase): six.assertRaisesRegex(self, ImproperlyConfigured, ('Error importing storage module django.core.files.non_existing_' - 'storage: "No module named .*non_existing_storage"'), + 'storage: "No module named .*non_existing_storage'), get_storage_class, 'django.core.files.non_existing_storage.NonExistingStorage' ) @@ -433,22 +435,29 @@ class FileSaveRaceConditionTest(unittest.TestCase): self.storage.delete('conflict') self.storage.delete('conflict_1') +@unittest.skipIf(sys.platform.startswith('win'), "Windows only partially supports umasks and chmod.") class FileStoragePermissions(unittest.TestCase): def setUp(self): - self.old_perms = settings.FILE_UPLOAD_PERMISSIONS - settings.FILE_UPLOAD_PERMISSIONS = 0o666 + self.umask = 0o027 + self.old_umask = os.umask(self.umask) self.storage_dir = tempfile.mkdtemp() self.storage = FileSystemStorage(self.storage_dir) def tearDown(self): - settings.FILE_UPLOAD_PERMISSIONS = self.old_perms shutil.rmtree(self.storage_dir) + os.umask(self.old_umask) + @override_settings(FILE_UPLOAD_PERMISSIONS=0o654) def test_file_upload_permissions(self): name = self.storage.save("the_file", ContentFile("data")) actual_mode = os.stat(self.storage.path(name))[0] & 0o777 - self.assertEqual(actual_mode, 0o666) + self.assertEqual(actual_mode, 0o654) + @override_settings(FILE_UPLOAD_PERMISSIONS=None) + def test_file_upload_default_permissions(self): + fname = self.storage.save("some_file", ContentFile("data")) + mode = os.stat(self.storage.path(fname))[0] & 0o777 + self.assertEqual(mode, 0o666 & ~self.umask) class FileStoragePathParsing(unittest.TestCase): def setUp(self): diff --git a/tests/regressiontests/file_uploads/tests.py b/tests/regressiontests/file_uploads/tests.py index 2a1ec7db21..918f77d73c 100644 --- a/tests/regressiontests/file_uploads/tests.py +++ b/tests/regressiontests/file_uploads/tests.py @@ -62,22 +62,20 @@ class FileUploadTests(TestCase): def test_base64_upload(self): test_string = "This data will be transmitted base64-encoded." - payload = "\r\n".join([ + payload = client.FakePayload("\r\n".join([ '--' + client.BOUNDARY, 'Content-Disposition: form-data; name="file"; filename="test.txt"', 'Content-Type: application/octet-stream', 'Content-Transfer-Encoding: base64', - '', - base64.b64encode(force_bytes(test_string)).decode('ascii'), - '--' + client.BOUNDARY + '--', - '', - ]).encode('utf-8') + '',])) + payload.write(b"\r\n" + base64.b64encode(force_bytes(test_string)) + b"\r\n") + payload.write('--' + client.BOUNDARY + '--\r\n') r = { 'CONTENT_LENGTH': len(payload), 'CONTENT_TYPE': client.MULTIPART_CONTENT, 'PATH_INFO': "/file_uploads/echo_content/", 'REQUEST_METHOD': 'POST', - 'wsgi.input': client.FakePayload(payload), + 'wsgi.input': payload, } response = self.client.request(**r) received = json.loads(response.content.decode('utf-8')) @@ -126,27 +124,23 @@ class FileUploadTests(TestCase): "../..\\hax0rd.txt" # Relative path, mixed. ] - payload = [] + payload = client.FakePayload() for i, name in enumerate(scary_file_names): - payload.extend([ + payload.write('\r\n'.join([ '--' + client.BOUNDARY, 'Content-Disposition: form-data; name="file%s"; filename="%s"' % (i, name), 'Content-Type: application/octet-stream', '', - 'You got pwnd.' - ]) - payload.extend([ - '--' + client.BOUNDARY + '--', - '', - ]) + 'You got pwnd.\r\n' + ])) + payload.write('\r\n--' + client.BOUNDARY + '--\r\n') - payload = "\r\n".join(payload).encode('utf-8') r = { 'CONTENT_LENGTH': len(payload), 'CONTENT_TYPE': client.MULTIPART_CONTENT, 'PATH_INFO': "/file_uploads/echo/", 'REQUEST_METHOD': 'POST', - 'wsgi.input': client.FakePayload(payload), + 'wsgi.input': payload, } response = self.client.request(**r) @@ -159,7 +153,7 @@ class FileUploadTests(TestCase): def test_filename_overflow(self): """File names over 256 characters (dangerous on some platforms) get fixed up.""" name = "%s.txt" % ("f"*500) - payload = "\r\n".join([ + payload = client.FakePayload("\r\n".join([ '--' + client.BOUNDARY, 'Content-Disposition: form-data; name="file"; filename="%s"' % name, 'Content-Type: application/octet-stream', @@ -167,13 +161,13 @@ class FileUploadTests(TestCase): 'Oops.' '--' + client.BOUNDARY + '--', '', - ]).encode('utf-8') + ])) r = { 'CONTENT_LENGTH': len(payload), 'CONTENT_TYPE': client.MULTIPART_CONTENT, 'PATH_INFO': "/file_uploads/echo/", 'REQUEST_METHOD': 'POST', - 'wsgi.input': client.FakePayload(payload), + 'wsgi.input': payload, } got = json.loads(self.client.request(**r).content.decode('utf-8')) self.assertTrue(len(got['file']) < 256, "Got a long file name (%s characters)." % len(got['file'])) @@ -184,7 +178,7 @@ class FileUploadTests(TestCase): attempt to read beyond the end of the stream, and simply will handle the part that can be parsed gracefully. """ - payload = "\r\n".join([ + payload_str = "\r\n".join([ '--' + client.BOUNDARY, 'Content-Disposition: form-data; name="file"; filename="foo.txt"', 'Content-Type: application/octet-stream', @@ -192,14 +186,14 @@ class FileUploadTests(TestCase): 'file contents' '--' + client.BOUNDARY + '--', '', - ]).encode('utf-8') - payload = payload[:-10] + ]) + payload = client.FakePayload(payload_str[:-10]) r = { 'CONTENT_LENGTH': len(payload), 'CONTENT_TYPE': client.MULTIPART_CONTENT, 'PATH_INFO': '/file_uploads/echo/', 'REQUEST_METHOD': 'POST', - 'wsgi.input': client.FakePayload(payload), + 'wsgi.input': payload, } got = json.loads(self.client.request(**r).content.decode('utf-8')) self.assertEqual(got, {}) diff --git a/tests/regressiontests/fixtures_regress/fixtures/sequence_extra.json b/tests/regressiontests/fixtures_regress/fixtures/sequence_extra.json new file mode 100644 index 0000000000..03c0f36696 --- /dev/null +++ b/tests/regressiontests/fixtures_regress/fixtures/sequence_extra.json @@ -0,0 +1,13 @@ +[ + { + "pk": "1", + "model": "fixtures_regress.animal", + "fields": { + "name": "Lion", + "extra_name": "Super Lion", + "latin_name": "Panthera leo", + "count": 3, + "weight": 1.2 + } + } +] diff --git a/tests/regressiontests/fixtures_regress/tests.py b/tests/regressiontests/fixtures_regress/tests.py index d675372c7a..55363bc5b7 100644 --- a/tests/regressiontests/fixtures_regress/tests.py +++ b/tests/regressiontests/fixtures_regress/tests.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, unicode_literals import os import re +from django.core.serializers.base import DeserializationError from django.core import management from django.core.management.base import CommandError from django.core.management.commands.dumpdata import sort_dependencies @@ -22,6 +23,7 @@ from .models import (Animal, Stuff, Absolute, Parent, Child, Article, Widget, class TestFixtures(TestCase): + def animal_pre_save_check(self, signal, sender, instance, **kwargs): self.pre_save_checks.append( ( @@ -54,6 +56,34 @@ class TestFixtures(TestCase): animal.save() self.assertGreater(animal.id, 1) + def test_loaddata_not_found_fields_not_ignore(self): + """ + Test for ticket #9279 -- Error is raised for entries in + the serialised data for fields that have been removed + from the database when not ignored. + """ + with self.assertRaises(DeserializationError): + management.call_command( + 'loaddata', + 'sequence_extra', + verbosity=0 + ) + + def test_loaddata_not_found_fields_ignore(self): + """ + Test for ticket #9279 -- Ignores entries in + the serialised data for fields that have been removed + from the database. + """ + management.call_command( + 'loaddata', + 'sequence_extra', + ignore=True, + verbosity=0, + commit=False + ) + self.assertEqual(Animal.specimens.all()[0].name, 'Lion') + @skipIfDBFeature('interprets_empty_strings_as_nulls') def test_pretty_print_xml(self): """ @@ -129,7 +159,7 @@ class TestFixtures(TestCase): Test that failing serializer import raises the proper error """ with six.assertRaisesRegex(self, ImportError, - "No module named unexistent.path"): + r"No module named.*unexistent"): management.call_command( 'loaddata', 'bad_fixture1.unkn', diff --git a/tests/regressiontests/forms/models.py b/tests/regressiontests/forms/models.py index 2f3ee9fa31..6e9c269356 100644 --- a/tests/regressiontests/forms/models.py +++ b/tests/regressiontests/forms/models.py @@ -63,6 +63,12 @@ class ChoiceFieldModel(models.Model): multi_choice_int = models.ManyToManyField(ChoiceOptionModel, blank=False, related_name='multi_choice_int', default=lambda: [1]) +class OptionalMultiChoiceModel(models.Model): + multi_choice = models.ManyToManyField(ChoiceOptionModel, blank=False, related_name='not_relevant', + default=lambda: ChoiceOptionModel.objects.filter(name='default')) + multi_choice_optional = models.ManyToManyField(ChoiceOptionModel, blank=True, null=True, + related_name='not_relevant2') + class FileModel(models.Model): file = models.FileField(storage=temp_storage, upload_to='tests') diff --git a/tests/regressiontests/forms/tests/extra.py b/tests/regressiontests/forms/tests/extra.py index 2ab5d40942..07acb29741 100644 --- a/tests/regressiontests/forms/tests/extra.py +++ b/tests/regressiontests/forms/tests/extra.py @@ -3,11 +3,11 @@ from __future__ import absolute_import, unicode_literals import datetime -from django.conf import settings from django.forms import * from django.forms.extras import SelectDateWidget from django.forms.util import ErrorList from django.test import TestCase +from django.test.utils import override_settings from django.utils import six from django.utils import translation from django.utils.encoding import force_text, smart_text, python_2_unicode_compatible @@ -613,7 +613,7 @@ class FormsExtraTestCase(TestCase, AssertFormErrorsMixin): data = dict(email='invalid') f = CommentForm(data, auto_id=False, error_class=DivErrorList) self.assertHTMLEqual(f.as_p(), """

    Name:

    -
    Enter a valid e-mail address.
    +
    Enter a valid email address.

    Email:

    This field is required.

    Comment:

    """) @@ -642,17 +642,14 @@ class FormsExtraTestCase(TestCase, AssertFormErrorsMixin): self.assertFalse(b.has_changed()) - +@override_settings(USE_L10N=True) class FormsExtraL10NTestCase(TestCase): def setUp(self): super(FormsExtraL10NTestCase, self).setUp() - self.old_use_l10n = getattr(settings, 'USE_L10N', False) - settings.USE_L10N = True translation.activate('nl') def tearDown(self): translation.deactivate() - settings.USE_L10N = self.old_use_l10n super(FormsExtraL10NTestCase, self).tearDown() def test_l10n(self): diff --git a/tests/regressiontests/forms/tests/fields.py b/tests/regressiontests/forms/tests/fields.py index 197ce1abd9..1027afceb1 100644 --- a/tests/regressiontests/forms/tests/fields.py +++ b/tests/regressiontests/forms/tests/fields.py @@ -356,6 +356,11 @@ class FieldsTests(SimpleTestCase): self.assertEqual(datetime.date(2006, 10, 25), f.clean(' 25 October 2006 ')) self.assertRaisesMessage(ValidationError, "'Enter a valid date.'", f.clean, ' ') + def test_datefield_5(self): + # Test null bytes (#18982) + f = DateField() + self.assertRaisesMessage(ValidationError, "'Enter a valid date.'", f.clean, 'a\x00b') + # TimeField ################################################################### def test_timefield_1(self): @@ -496,46 +501,33 @@ class FieldsTests(SimpleTestCase): self.assertRaisesMessage(ValidationError, "'Enter a valid value.'", f.clean, 'abcd') # EmailField ################################################################## + # See also modeltests/validators tests for validate_email specific tests def test_emailfield_1(self): f = EmailField() self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '') self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None) self.assertEqual('person@example.com', f.clean('person@example.com')) - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'foo') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'foo@') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'foo@bar') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'example@invalid-.com') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'example@-invalid.com') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'example@inv-.alid-.com') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'example@inv-.-alid.com') - self.assertEqual('example@valid-----hyphens.com', f.clean('example@valid-----hyphens.com')) - self.assertEqual('example@valid-with-hyphens.com', f.clean('example@valid-with-hyphens.com')) - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'example@.com') - self.assertEqual('local@domain.with.idn.xyz\xe4\xf6\xfc\xdfabc.part.com', f.clean('local@domain.with.idn.xyzäöüßabc.part.com')) + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo') + self.assertEqual('local@domain.with.idn.xyz\xe4\xf6\xfc\xdfabc.part.com', + f.clean('local@domain.with.idn.xyzäöüßabc.part.com')) def test_email_regexp_for_performance(self): f = EmailField() # Check for runaway regex security problem. This will take for-freeking-ever # if the security fix isn't in place. - self.assertRaisesMessage( - ValidationError, - "'Enter a valid e-mail address.'", - f.clean, - 'viewx3dtextx26qx3d@yahoo.comx26latlngx3d15854521645943074058' - ) + addr = 'viewx3dtextx26qx3d@yahoo.comx26latlngx3d15854521645943074058' + self.assertEqual(addr, f.clean(addr)) - def test_emailfield_2(self): + def test_emailfield_not_required(self): f = EmailField(required=False) self.assertEqual('', f.clean('')) self.assertEqual('', f.clean(None)) self.assertEqual('person@example.com', f.clean('person@example.com')) self.assertEqual('example@example.com', f.clean(' example@example.com \t \t ')) - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'foo') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'foo@') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'foo@bar') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo') - def test_emailfield_3(self): + def test_emailfield_min_max_length(self): f = EmailField(min_length=10, max_length=15) self.assertRaisesMessage(ValidationError, "'Ensure this value has at least 10 characters (it has 9).'", f.clean, 'a@foo.com') self.assertEqual('alf@foo.com', f.clean('alf@foo.com')) @@ -921,7 +913,7 @@ class FieldsTests(SimpleTestCase): f = ComboField(fields=[CharField(max_length=20), EmailField()]) self.assertEqual('test@example.com', f.clean('test@example.com')) self.assertRaisesMessage(ValidationError, "'Ensure this value has at most 20 characters (it has 28).'", f.clean, 'longemailaddress@example.com') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'not an e-mail') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'not an email') self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '') self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None) @@ -929,7 +921,7 @@ class FieldsTests(SimpleTestCase): f = ComboField(fields=[CharField(max_length=20), EmailField()], required=False) self.assertEqual('test@example.com', f.clean('test@example.com')) self.assertRaisesMessage(ValidationError, "'Ensure this value has at most 20 characters (it has 28).'", f.clean, 'longemailaddress@example.com') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'not an e-mail') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'not an email') self.assertEqual('', f.clean('')) self.assertEqual('', f.clean(None)) diff --git a/tests/regressiontests/forms/tests/formsets.py b/tests/regressiontests/forms/tests/formsets.py index 3decd1f085..b3ceee551b 100644 --- a/tests/regressiontests/forms/tests/formsets.py +++ b/tests/regressiontests/forms/tests/formsets.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from django.forms import Form, CharField, IntegerField, ValidationError, DateField from django.forms.formsets import formset_factory, BaseFormSet +from django.forms.util import ErrorList from django.test import TestCase @@ -847,6 +848,15 @@ class FormsFormsetTestCase(TestCase): self.assertTrue(formset) + def test_formset_error_class(self): + # Regression tests for #16479 -- formsets form use ErrorList instead of supplied error_class + class CustomErrorList(ErrorList): + pass + + formset = FavoriteDrinksFormSet(error_class=CustomErrorList) + self.assertEqual(formset.forms[0].error_class, CustomErrorList) + + data = { 'choices-TOTAL_FORMS': '1', # the number of forms rendered 'choices-INITIAL_FORMS': '0', # the number of forms with initial data diff --git a/tests/regressiontests/forms/tests/input_formats.py b/tests/regressiontests/forms/tests/input_formats.py index e362620ddb..d591804b9d 100644 --- a/tests/regressiontests/forms/tests/input_formats.py +++ b/tests/regressiontests/forms/tests/input_formats.py @@ -1,25 +1,17 @@ from datetime import time, date, datetime from django import forms -from django.conf import settings +from django.test.utils import override_settings from django.utils.translation import activate, deactivate from django.utils.unittest import TestCase +@override_settings(TIME_INPUT_FORMATS=["%I:%M:%S %p", "%I:%M %p"], USE_L10N=True) class LocalizedTimeTests(TestCase): def setUp(self): - self.old_TIME_INPUT_FORMATS = settings.TIME_INPUT_FORMATS - self.old_USE_L10N = settings.USE_L10N - - settings.TIME_INPUT_FORMATS = ["%I:%M:%S %p", "%I:%M %p"] - settings.USE_L10N = True - activate('de') def tearDown(self): - settings.TIME_INPUT_FORMATS = self.old_TIME_INPUT_FORMATS - settings.USE_L10N = self.old_USE_L10N - deactivate() def test_timeField(self): @@ -113,14 +105,8 @@ class LocalizedTimeTests(TestCase): self.assertEqual(text, "13:30:00") +@override_settings(TIME_INPUT_FORMATS=["%I:%M:%S %p", "%I:%M %p"]) class CustomTimeInputFormatsTests(TestCase): - def setUp(self): - self.old_TIME_INPUT_FORMATS = settings.TIME_INPUT_FORMATS - settings.TIME_INPUT_FORMATS = ["%I:%M:%S %p", "%I:%M %p"] - - def tearDown(self): - settings.TIME_INPUT_FORMATS = self.old_TIME_INPUT_FORMATS - def test_timeField(self): "TimeFields can parse dates in the default format" f = forms.TimeField() @@ -302,20 +288,12 @@ class SimpleTimeFormatTests(TestCase): self.assertEqual(text, "13:30:00") +@override_settings(DATE_INPUT_FORMATS=["%d/%m/%Y", "%d-%m-%Y"], USE_L10N=True) class LocalizedDateTests(TestCase): def setUp(self): - self.old_DATE_INPUT_FORMATS = settings.DATE_INPUT_FORMATS - self.old_USE_L10N = settings.USE_L10N - - settings.DATE_INPUT_FORMATS = ["%d/%m/%Y", "%d-%m-%Y"] - settings.USE_L10N = True - activate('de') def tearDown(self): - settings.DATE_INPUT_FORMATS = self.old_DATE_INPUT_FORMATS - settings.USE_L10N = self.old_USE_L10N - deactivate() def test_dateField(self): @@ -410,14 +388,9 @@ class LocalizedDateTests(TestCase): text = f.widget._format_value(result) self.assertEqual(text, "21.12.2010") + +@override_settings(DATE_INPUT_FORMATS=["%d.%m.%Y", "%d-%m-%Y"]) class CustomDateInputFormatsTests(TestCase): - def setUp(self): - self.old_DATE_INPUT_FORMATS = settings.DATE_INPUT_FORMATS - settings.DATE_INPUT_FORMATS = ["%d.%m.%Y", "%d-%m-%Y"] - - def tearDown(self): - settings.DATE_INPUT_FORMATS = self.old_DATE_INPUT_FORMATS - def test_dateField(self): "DateFields can parse dates in the default format" f = forms.DateField() @@ -597,20 +570,13 @@ class SimpleDateFormatTests(TestCase): text = f.widget._format_value(result) self.assertEqual(text, "2010-12-21") + +@override_settings(DATETIME_INPUT_FORMATS=["%I:%M:%S %p %d/%m/%Y", "%I:%M %p %d-%m-%Y"], USE_L10N=True) class LocalizedDateTimeTests(TestCase): def setUp(self): - self.old_DATETIME_INPUT_FORMATS = settings.DATETIME_INPUT_FORMATS - self.old_USE_L10N = settings.USE_L10N - - settings.DATETIME_INPUT_FORMATS = ["%I:%M:%S %p %d/%m/%Y", "%I:%M %p %d-%m-%Y"] - settings.USE_L10N = True - activate('de') def tearDown(self): - settings.DATETIME_INPUT_FORMATS = self.old_DATETIME_INPUT_FORMATS - settings.USE_L10N = self.old_USE_L10N - deactivate() def test_dateTimeField(self): @@ -706,14 +672,8 @@ class LocalizedDateTimeTests(TestCase): self.assertEqual(text, "21.12.2010 13:30:00") +@override_settings(DATETIME_INPUT_FORMATS=["%I:%M:%S %p %d/%m/%Y", "%I:%M %p %d-%m-%Y"]) class CustomDateTimeInputFormatsTests(TestCase): - def setUp(self): - self.old_DATETIME_INPUT_FORMATS = settings.DATETIME_INPUT_FORMATS - settings.DATETIME_INPUT_FORMATS = ["%I:%M:%S %p %d/%m/%Y", "%I:%M %p %d-%m-%Y"] - - def tearDown(self): - settings.DATETIME_INPUT_FORMATS = self.old_DATETIME_INPUT_FORMATS - def test_dateTimeField(self): "DateTimeFields can parse dates in the default format" f = forms.DateTimeField() diff --git a/tests/regressiontests/forms/tests/models.py b/tests/regressiontests/forms/tests/models.py index c351509cee..be75643b28 100644 --- a/tests/regressiontests/forms/tests/models.py +++ b/tests/regressiontests/forms/tests/models.py @@ -11,7 +11,7 @@ from django.test import TestCase from django.utils import six from ..models import (ChoiceOptionModel, ChoiceFieldModel, FileModel, Group, - BoundaryModel, Defaults) + BoundaryModel, Defaults, OptionalMultiChoiceModel) class ChoiceFieldForm(ModelForm): @@ -19,6 +19,11 @@ class ChoiceFieldForm(ModelForm): model = ChoiceFieldModel +class OptionalMultiChoiceModelForm(ModelForm): + class Meta: + model = OptionalMultiChoiceModel + + class FileForm(Form): file1 = FileField() @@ -34,6 +39,21 @@ class TestTicket12510(TestCase): field = ModelChoiceField(Group.objects.order_by('-name')) self.assertEqual('a', field.clean(self.groups[0].pk).name) + +class TestTicket14567(TestCase): + """ + Check that the return values of ModelMultipleChoiceFields are QuerySets + """ + def test_empty_queryset_return(self): + "If a model's ManyToManyField has blank=True and is saved with no data, a queryset is returned." + form = OptionalMultiChoiceModelForm({'multi_choice_optional': '', 'multi_choice': ['1']}) + self.assertTrue(form.is_valid()) + # Check that the empty value is a QuerySet + self.assertTrue(isinstance(form.cleaned_data['multi_choice_optional'], models.query.QuerySet)) + # While we're at it, test whether a QuerySet is returned if there *is* a value. + self.assertTrue(isinstance(form.cleaned_data['multi_choice'], models.query.QuerySet)) + + class ModelFormCallableModelDefault(TestCase): def test_no_empty_option(self): "If a model's ForeignKey has blank=False and a default, no empty option is created (Refs #10792)." @@ -103,7 +123,6 @@ class ModelFormCallableModelDefault(TestCase): Hold down "Control", or "Command" on a Mac, to select more than one.

    """) - class FormsModelTestCase(TestCase): def test_unicode_filename(self): # FileModel with unicode filename and data ######################### diff --git a/tests/regressiontests/forms/tests/widgets.py b/tests/regressiontests/forms/tests/widgets.py index 104144b288..4bdd3f76ea 100644 --- a/tests/regressiontests/forms/tests/widgets.py +++ b/tests/regressiontests/forms/tests/widgets.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import copy import datetime -from django.conf import settings from django.core.files.uploadedfile import SimpleUploadedFile from django.forms import * from django.forms.widgets import RadioFieldRenderer @@ -13,6 +12,7 @@ from django.utils.safestring import mark_safe from django.utils import six from django.utils.translation import activate, deactivate from django.test import TestCase +from django.test.utils import override_settings from django.utils.encoding import python_2_unicode_compatible @@ -989,16 +989,14 @@ class NullBooleanSelectLazyForm(Form): """Form to test for lazy evaluation. Refs #17190""" bool = BooleanField(widget=NullBooleanSelect()) +@override_settings(USE_L10N=True) class FormsI18NWidgetsTestCase(TestCase): def setUp(self): super(FormsI18NWidgetsTestCase, self).setUp() - self.old_use_l10n = getattr(settings, 'USE_L10N', False) - settings.USE_L10N = True activate('de-at') def tearDown(self): deactivate() - settings.USE_L10N = self.old_use_l10n super(FormsI18NWidgetsTestCase, self).tearDown() def test_splitdatetime(self): diff --git a/tests/regressiontests/generic_views/dates.py b/tests/regressiontests/generic_views/dates.py index c2fa71b376..0c565daf9f 100644 --- a/tests/regressiontests/generic_views/dates.py +++ b/tests/regressiontests/generic_views/dates.py @@ -23,29 +23,30 @@ requires_tz_support = skipUnless(TZ_SUPPORT, "time zone, but your operating system isn't able to do that.") +def _make_books(n, base_date): + for i in range(n): + b = Book.objects.create( + name='Book %d' % i, + slug='book-%d' % i, + pages=100+i, + pubdate=base_date - datetime.timedelta(days=i)) + class ArchiveIndexViewTests(TestCase): fixtures = ['generic-views-test-data.json'] urls = 'regressiontests.generic_views.urls' - def _make_books(self, n, base_date): - for i in range(n): - b = Book.objects.create( - name='Book %d' % i, - slug='book-%d' % i, - pages=100+i, - pubdate=base_date - datetime.timedelta(days=1)) def test_archive_view(self): res = self.client.get('/dates/books/') self.assertEqual(res.status_code, 200) - self.assertEqual(res.context['date_list'], Book.objects.dates('pubdate', 'year')[::-1]) + self.assertEqual(list(res.context['date_list']), list(Book.objects.dates('pubdate', 'year', 'DESC'))) self.assertEqual(list(res.context['latest']), list(Book.objects.all())) self.assertTemplateUsed(res, 'generic_views/book_archive.html') def test_archive_view_context_object_name(self): res = self.client.get('/dates/books/context_object_name/') self.assertEqual(res.status_code, 200) - self.assertEqual(res.context['date_list'], Book.objects.dates('pubdate', 'year')[::-1]) + self.assertEqual(list(res.context['date_list']), list(Book.objects.dates('pubdate', 'year', 'DESC'))) self.assertEqual(list(res.context['thingies']), list(Book.objects.all())) self.assertFalse('latest' in res.context) self.assertTemplateUsed(res, 'generic_views/book_archive.html') @@ -65,14 +66,14 @@ class ArchiveIndexViewTests(TestCase): def test_archive_view_template(self): res = self.client.get('/dates/books/template_name/') self.assertEqual(res.status_code, 200) - self.assertEqual(res.context['date_list'], Book.objects.dates('pubdate', 'year')[::-1]) + self.assertEqual(list(res.context['date_list']), list(Book.objects.dates('pubdate', 'year', 'DESC'))) self.assertEqual(list(res.context['latest']), list(Book.objects.all())) self.assertTemplateUsed(res, 'generic_views/list.html') def test_archive_view_template_suffix(self): res = self.client.get('/dates/books/template_name_suffix/') self.assertEqual(res.status_code, 200) - self.assertEqual(res.context['date_list'], Book.objects.dates('pubdate', 'year')[::-1]) + self.assertEqual(list(res.context['date_list']), list(Book.objects.dates('pubdate', 'year', 'DESC'))) self.assertEqual(list(res.context['latest']), list(Book.objects.all())) self.assertTemplateUsed(res, 'generic_views/book_detail.html') @@ -82,13 +83,13 @@ class ArchiveIndexViewTests(TestCase): def test_archive_view_by_month(self): res = self.client.get('/dates/books/by_month/') self.assertEqual(res.status_code, 200) - self.assertEqual(res.context['date_list'], Book.objects.dates('pubdate', 'month')[::-1]) + self.assertEqual(list(res.context['date_list']), list(Book.objects.dates('pubdate', 'month', 'DESC'))) def test_paginated_archive_view(self): - self._make_books(20, base_date=datetime.date.today()) + _make_books(20, base_date=datetime.date.today()) res = self.client.get('/dates/books/paginated/') self.assertEqual(res.status_code, 200) - self.assertEqual(res.context['date_list'], Book.objects.dates('pubdate', 'year')[::-1]) + self.assertEqual(list(res.context['date_list']), list(Book.objects.dates('pubdate', 'year', 'DESC'))) self.assertEqual(list(res.context['latest']), list(Book.objects.all()[0:10])) self.assertTemplateUsed(res, 'generic_views/book_archive.html') @@ -99,7 +100,7 @@ class ArchiveIndexViewTests(TestCase): def test_paginated_archive_view_does_not_load_entire_table(self): # Regression test for #18087 - self._make_books(20, base_date=datetime.date.today()) + _make_books(20, base_date=datetime.date.today()) # 1 query for years list + 1 query for books with self.assertNumQueries(2): self.client.get('/dates/books/') @@ -124,6 +125,13 @@ class ArchiveIndexViewTests(TestCase): res = self.client.get('/dates/booksignings/') self.assertEqual(res.status_code, 200) + def test_date_list_order(self): + """date_list should be sorted descending in index""" + _make_books(5, base_date=datetime.date(2011, 12, 25)) + res = self.client.get('/dates/books/') + self.assertEqual(res.status_code, 200) + self.assertEqual(list(res.context['date_list']), list(reversed(sorted(res.context['date_list'])))) + class YearArchiveViewTests(TestCase): fixtures = ['generic-views-test-data.json'] @@ -202,6 +210,12 @@ class YearArchiveViewTests(TestCase): res = self.client.get('/dates/booksignings/2008/') self.assertEqual(res.status_code, 200) + def test_date_list_order(self): + """date_list should be sorted ascending in year view""" + _make_books(10, base_date=datetime.date(2011, 12, 25)) + res = self.client.get('/dates/books/2011/') + self.assertEqual(list(res.context['date_list']), list(sorted(res.context['date_list']))) + class MonthArchiveViewTests(TestCase): fixtures = ['generic-views-test-data.json'] @@ -322,6 +336,12 @@ class MonthArchiveViewTests(TestCase): res = self.client.get('/dates/booksignings/2008/apr/') self.assertEqual(res.status_code, 200) + def test_date_list_order(self): + """date_list should be sorted ascending in month view""" + _make_books(10, base_date=datetime.date(2011, 12, 25)) + res = self.client.get('/dates/books/2011/dec/') + self.assertEqual(list(res.context['date_list']), list(sorted(res.context['date_list']))) + class WeekArchiveViewTests(TestCase): fixtures = ['generic-views-test-data.json'] diff --git a/tests/regressiontests/handlers/tests.py b/tests/regressiontests/handlers/tests.py index 34863b6493..8676a448d9 100644 --- a/tests/regressiontests/handlers/tests.py +++ b/tests/regressiontests/handlers/tests.py @@ -1,27 +1,23 @@ -from django.conf import settings from django.core.handlers.wsgi import WSGIHandler from django.test import RequestFactory +from django.test.utils import override_settings from django.utils import unittest - class HandlerTests(unittest.TestCase): + # Mangle settings so the handler will fail + @override_settings(MIDDLEWARE_CLASSES=42) def test_lock_safety(self): """ Tests for bug #11193 (errors inside middleware shouldn't leave the initLock locked). """ - # Mangle settings so the handler will fail - old_middleware_classes = settings.MIDDLEWARE_CLASSES - settings.MIDDLEWARE_CLASSES = 42 # Try running the handler, it will fail in load_middleware handler = WSGIHandler() self.assertEqual(handler.initLock.locked(), False) with self.assertRaises(Exception): handler(None, None) self.assertEqual(handler.initLock.locked(), False) - # Reset settings - settings.MIDDLEWARE_CLASSES = old_middleware_classes def test_bad_path_info(self): """Tests for bug #15672 ('request' referenced before assignment)""" diff --git a/tests/regressiontests/localflavor/at/__init__.py b/tests/regressiontests/http_utils/__init__.py similarity index 100% rename from tests/regressiontests/localflavor/at/__init__.py rename to tests/regressiontests/http_utils/__init__.py diff --git a/tests/regressiontests/localflavor/au/__init__.py b/tests/regressiontests/http_utils/models.py similarity index 100% rename from tests/regressiontests/localflavor/au/__init__.py rename to tests/regressiontests/http_utils/models.py diff --git a/tests/regressiontests/http_utils/tests.py b/tests/regressiontests/http_utils/tests.py new file mode 100644 index 0000000000..7dfd24d721 --- /dev/null +++ b/tests/regressiontests/http_utils/tests.py @@ -0,0 +1,45 @@ +from __future__ import unicode_literals + +from django.http import HttpRequest, HttpResponse, StreamingHttpResponse +from django.http.utils import conditional_content_removal +from django.test import TestCase + + +class HttpUtilTests(TestCase): + + def test_conditional_content_removal(self): + """ + Tests that content is removed from regular and streaming responses with + a status_code of 100-199, 204, 304 or a method of "HEAD". + """ + req = HttpRequest() + + # Do nothing for 200 responses. + res = HttpResponse('abc') + conditional_content_removal(req, res) + self.assertEqual(res.content, b'abc') + + res = StreamingHttpResponse(['abc']) + conditional_content_removal(req, res) + self.assertEqual(b''.join(res), b'abc') + + # Strip content for some status codes. + for status_code in (100, 150, 199, 204, 304): + res = HttpResponse('abc', status=status_code) + conditional_content_removal(req, res) + self.assertEqual(res.content, b'') + + res = StreamingHttpResponse(['abc'], status=status_code) + conditional_content_removal(req, res) + self.assertEqual(b''.join(res), b'') + + # Strip content for HEAD requests. + req.method = 'HEAD' + + res = HttpResponse('abc') + conditional_content_removal(req, res) + self.assertEqual(res.content, b'') + + res = StreamingHttpResponse(['abc']) + conditional_content_removal(req, res) + self.assertEqual(b''.join(res), b'') diff --git a/tests/regressiontests/httpwrappers/abc.txt b/tests/regressiontests/httpwrappers/abc.txt new file mode 100644 index 0000000000..6bac42b3ad --- /dev/null +++ b/tests/regressiontests/httpwrappers/abc.txt @@ -0,0 +1 @@ +random content diff --git a/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py index 4c6aed1b97..553b6ecff4 100644 --- a/tests/regressiontests/httpwrappers/tests.py +++ b/tests/regressiontests/httpwrappers/tests.py @@ -2,12 +2,14 @@ from __future__ import unicode_literals import copy +import os import pickle +import warnings from django.core.exceptions import SuspiciousOperation from django.http import (QueryDict, HttpResponse, HttpResponseRedirect, HttpResponsePermanentRedirect, HttpResponseNotAllowed, - HttpResponseNotModified, + HttpResponseNotModified, StreamingHttpResponse, SimpleCookie, BadHeaderError, parse_cookie) from django.test import TestCase @@ -127,10 +129,14 @@ class QueryDictTests(unittest.TestCase): self.assertTrue(q.has_key('foo')) self.assertTrue('foo' in q) - self.assertEqual(list(six.iteritems(q)), [('foo', 'another'), ('name', 'john')]) - self.assertEqual(list(six.iterlists(q)), [('foo', ['bar', 'baz', 'another']), ('name', ['john'])]) - self.assertEqual(list(six.iterkeys(q)), ['foo', 'name']) - self.assertEqual(list(six.itervalues(q)), ['another', 'john']) + self.assertEqual(sorted(list(six.iteritems(q))), + [('foo', 'another'), ('name', 'john')]) + self.assertEqual(sorted(list(six.iterlists(q))), + [('foo', ['bar', 'baz', 'another']), ('name', ['john'])]) + self.assertEqual(sorted(list(six.iterkeys(q))), + ['foo', 'name']) + self.assertEqual(sorted(list(six.itervalues(q))), + ['another', 'john']) self.assertEqual(len(q), 2) q.update({'foo': 'hello'}) @@ -312,23 +318,61 @@ class HttpResponseTests(unittest.TestCase): r.content = [1, 2, 3] self.assertEqual(r.content, b'123') - #test retrieval explicitly using iter and odd inputs + #test retrieval explicitly using iter (deprecated) and odd inputs r = HttpResponse() r.content = ['1', '2', 3, '\u079e'] - my_iter = r.__iter__() - result = list(my_iter) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always", PendingDeprecationWarning) + my_iter = iter(r) + self.assertEqual(w[0].category, PendingDeprecationWarning) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always", PendingDeprecationWarning) + result = list(my_iter) + self.assertEqual(w[0].category, PendingDeprecationWarning) #'\xde\x9e' == unichr(1950).encode('utf-8') self.assertEqual(result, [b'1', b'2', b'3', b'\xde\x9e']) self.assertEqual(r.content, b'123\xde\x9e') #with Content-Encoding header - r = HttpResponse([1,1,2,4,8]) + r = HttpResponse() r['Content-Encoding'] = 'winning' - self.assertEqual(r.content, b'11248') - r.content = ['\u079e',] - self.assertRaises(UnicodeEncodeError, + r.content = [b'abc', b'def'] + self.assertEqual(r.content, b'abcdef') + r.content = ['\u079e'] + self.assertRaises(TypeError if six.PY3 else UnicodeEncodeError, getattr, r, 'content') + # .content can safely be accessed multiple times. + r = HttpResponse(iter(['hello', 'world'])) + self.assertEqual(r.content, r.content) + self.assertEqual(r.content, b'helloworld') + # accessing the iterator works (once) after accessing .content + self.assertEqual(b''.join(r), b'helloworld') + self.assertEqual(b''.join(r), b'') + # accessing .content still works + self.assertEqual(r.content, b'helloworld') + + # XXX accessing .content doesn't work if the response was iterated first + # XXX change this when the deprecation completes in HttpResponse + r = HttpResponse(iter(['hello', 'world'])) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", PendingDeprecationWarning) + self.assertEqual(b''.join(r), b'helloworld') + self.assertEqual(r.content, b'') # not the expected result! + + # additional content can be written to the response. + r = HttpResponse(iter(['hello', 'world'])) + self.assertEqual(r.content, b'helloworld') + r.write('!') + self.assertEqual(r.content, b'helloworld!') + + def test_iterator_isnt_rewound(self): + # Regression test for #13222 + r = HttpResponse('abc') + i = iter(r) + self.assertEqual(list(i), [b'abc']) + self.assertEqual(list(i), []) + def test_file_interface(self): r = HttpResponse() r.write(b"hello") @@ -337,7 +381,9 @@ class HttpResponseTests(unittest.TestCase): self.assertEqual(r.tell(), 17) r = HttpResponse(['abc']) - self.assertRaises(Exception, r.write, 'def') + r.write('def') + self.assertEqual(r.tell(), 6) + self.assertEqual(r.content, b'abcdef') def test_unsafe_redirect(self): bad_urls = [ @@ -351,7 +397,6 @@ class HttpResponseTests(unittest.TestCase): self.assertRaises(SuspiciousOperation, HttpResponsePermanentRedirect, url) - class HttpResponseSubclassesTests(TestCase): def test_redirect(self): response = HttpResponseRedirect('/redirected/') @@ -379,6 +424,115 @@ class HttpResponseSubclassesTests(TestCase): content_type='text/html') self.assertContains(response, 'Only the GET method is allowed', status_code=405) +class StreamingHttpResponseTests(TestCase): + def test_streaming_response(self): + r = StreamingHttpResponse(iter(['hello', 'world'])) + + # iterating over the response itself yields bytestring chunks. + chunks = list(r) + self.assertEqual(chunks, [b'hello', b'world']) + for chunk in chunks: + self.assertIsInstance(chunk, six.binary_type) + + # and the response can only be iterated once. + self.assertEqual(list(r), []) + + # even when a sequence that can be iterated many times, like a list, + # is given as content. + r = StreamingHttpResponse(['abc', 'def']) + self.assertEqual(list(r), [b'abc', b'def']) + self.assertEqual(list(r), []) + + # streaming responses don't have a `content` attribute. + self.assertFalse(hasattr(r, 'content')) + + # and you can't accidentally assign to a `content` attribute. + with self.assertRaises(AttributeError): + r.content = 'xyz' + + # but they do have a `streaming_content` attribute. + self.assertTrue(hasattr(r, 'streaming_content')) + + # that exists so we can check if a response is streaming, and wrap or + # replace the content iterator. + r.streaming_content = iter(['abc', 'def']) + r.streaming_content = (chunk.upper() for chunk in r.streaming_content) + self.assertEqual(list(r), [b'ABC', b'DEF']) + + # coercing a streaming response to bytes doesn't return a complete HTTP + # message like a regular response does. it only gives us the headers. + r = StreamingHttpResponse(iter(['hello', 'world'])) + self.assertEqual( + six.binary_type(r), b'Content-Type: text/html; charset=utf-8') + + # and this won't consume its content. + self.assertEqual(list(r), [b'hello', b'world']) + + # additional content cannot be written to the response. + r = StreamingHttpResponse(iter(['hello', 'world'])) + with self.assertRaises(Exception): + r.write('!') + + # and we can't tell the current position. + with self.assertRaises(Exception): + r.tell() + +class FileCloseTests(TestCase): + def test_response(self): + filename = os.path.join(os.path.dirname(__file__), 'abc.txt') + + # file isn't closed until we close the response. + file1 = open(filename) + r = HttpResponse(file1) + self.assertFalse(file1.closed) + r.close() + self.assertTrue(file1.closed) + + # don't automatically close file when we finish iterating the response. + file1 = open(filename) + r = HttpResponse(file1) + self.assertFalse(file1.closed) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", PendingDeprecationWarning) + list(r) + self.assertFalse(file1.closed) + r.close() + self.assertTrue(file1.closed) + + # when multiple file are assigned as content, make sure they are all + # closed with the response. + file1 = open(filename) + file2 = open(filename) + r = HttpResponse(file1) + r.content = file2 + self.assertFalse(file1.closed) + self.assertFalse(file2.closed) + r.close() + self.assertTrue(file1.closed) + self.assertTrue(file2.closed) + + def test_streaming_response(self): + filename = os.path.join(os.path.dirname(__file__), 'abc.txt') + + # file isn't closed until we close the response. + file1 = open(filename) + r = StreamingHttpResponse(file1) + self.assertFalse(file1.closed) + r.close() + self.assertTrue(file1.closed) + + # when multiple file are assigned as content, make sure they are all + # closed with the response. + file1 = open(filename) + file2 = open(filename) + r = StreamingHttpResponse(file1) + r.streaming_content = file2 + self.assertFalse(file1.closed) + self.assertFalse(file2.closed) + r.close() + self.assertTrue(file1.closed) + self.assertTrue(file2.closed) + class CookieTests(unittest.TestCase): def test_encode(self): """ @@ -419,7 +573,7 @@ class CookieTests(unittest.TestCase): """ Test that a repeated non-standard name doesn't affect all cookies. Ticket #15852 """ - self.assertTrue('good_cookie' in parse_cookie('a,=b; a,=c; good_cookie=yes').keys()) + self.assertTrue('good_cookie' in parse_cookie('a:=b; a:=c; good_cookie=yes').keys()) def test_httponly_after_load(self): """ diff --git a/tests/regressiontests/i18n/commands/extraction.py b/tests/regressiontests/i18n/commands/extraction.py index 29d9e277ff..ca2c3cc026 100644 --- a/tests/regressiontests/i18n/commands/extraction.py +++ b/tests/regressiontests/i18n/commands/extraction.py @@ -156,6 +156,21 @@ class BasicExtractorTests(ExtractorTests): self.assertTrue('msgctxt "Special blocktrans context #4"' in po_contents) self.assertTrue("Translatable literal #8d" in po_contents) + def test_context_in_single_quotes(self): + os.chdir(self.test_dir) + management.call_command('makemessages', locale=LOCALE, verbosity=0) + self.assertTrue(os.path.exists(self.PO_FILE)) + with open(self.PO_FILE, 'r') as fp: + po_contents = fp.read() + # {% trans %} + self.assertTrue('msgctxt "Context wrapped in double quotes"' in po_contents) + self.assertTrue('msgctxt "Context wrapped in single quotes"' in po_contents) + + # {% blocktrans %} + self.assertTrue('msgctxt "Special blocktrans context wrapped in double quotes"' in po_contents) + self.assertTrue('msgctxt "Special blocktrans context wrapped in single quotes"' in po_contents) + + class JavascriptExtractorTests(ExtractorTests): PO_FILE='locale/%s/LC_MESSAGES/djangojs.po' % LOCALE diff --git a/tests/regressiontests/i18n/commands/templates/test.html b/tests/regressiontests/i18n/commands/templates/test.html index 5789346984..e7d7f3ca53 100644 --- a/tests/regressiontests/i18n/commands/templates/test.html +++ b/tests/regressiontests/i18n/commands/templates/test.html @@ -77,3 +77,8 @@ continued here.{% endcomment %} {% trans "Shouldn't double escape this sequence %% either" context "ctx1" %} {% trans "Looks like a str fmt spec %s but shouldn't be interpreted as such" %} {% trans "Looks like a str fmt spec % o but shouldn't be interpreted as such" %} + +{% trans "Translatable literal with context wrapped in single quotes" context 'Context wrapped in single quotes' as var %} +{% trans "Translatable literal with context wrapped in double quotes" context "Context wrapped in double quotes" as var %} +{% blocktrans context 'Special blocktrans context wrapped in single quotes' %}Translatable literal with context wrapped in single quotes{% endblocktrans %} +{% blocktrans context "Special blocktrans context wrapped in double quotes" %}Translatable literal with context wrapped in double quotes{% endblocktrans %} \ No newline at end of file diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py index bce3d617d2..4054f85ef0 100644 --- a/tests/regressiontests/i18n/tests.py +++ b/tests/regressiontests/i18n/tests.py @@ -23,7 +23,7 @@ from django.utils import six from django.utils.six import PY3 from django.utils.translation import (ugettext, ugettext_lazy, activate, deactivate, gettext_lazy, pgettext, npgettext, to_locale, - get_language_info, get_language, get_language_from_request) + get_language_info, get_language, get_language_from_request, trans_real) from .commands.tests import can_run_extraction_tests, can_run_compilation_tests @@ -45,6 +45,9 @@ from .patterns.tests import (URLRedirectWithoutTrailingSlashTests, here = os.path.dirname(os.path.abspath(__file__)) +extended_locale_paths = settings.LOCALE_PATHS + ( + os.path.join(here, 'other', 'locale'), +) class TranslationTests(TestCase): @@ -86,139 +89,129 @@ class TranslationTests(TestCase): s2 = pickle.loads(pickle.dumps(s1)) self.assertEqual(six.text_type(s2), "test") + @override_settings(LOCALE_PATHS=extended_locale_paths) def test_pgettext(self): - # Reset translation catalog to include other/locale/de - extended_locale_paths = settings.LOCALE_PATHS + ( - os.path.join(here, 'other', 'locale'), - ) - with self.settings(LOCALE_PATHS=extended_locale_paths): - from django.utils.translation import trans_real - trans_real._active = local() - trans_real._translations = {} - with translation.override('de'): - self.assertEqual(pgettext("unexisting", "May"), "May") - self.assertEqual(pgettext("month name", "May"), "Mai") - self.assertEqual(pgettext("verb", "May"), "Kann") - self.assertEqual(npgettext("search", "%d result", "%d results", 4) % 4, "4 Resultate") + trans_real._active = local() + trans_real._translations = {} + with translation.override('de'): + self.assertEqual(pgettext("unexisting", "May"), "May") + self.assertEqual(pgettext("month name", "May"), "Mai") + self.assertEqual(pgettext("verb", "May"), "Kann") + self.assertEqual(npgettext("search", "%d result", "%d results", 4) % 4, "4 Resultate") + @override_settings(LOCALE_PATHS=extended_locale_paths) def test_template_tags_pgettext(self): """ Ensure that message contexts are taken into account the {% trans %} and {% blocktrans %} template tags. Refs #14806. """ - # Reset translation catalog to include other/locale/de - extended_locale_paths = settings.LOCALE_PATHS + ( - os.path.join(here, 'other', 'locale'), - ) - with self.settings(LOCALE_PATHS=extended_locale_paths): - from django.utils.translation import trans_real - trans_real._active = local() - trans_real._translations = {} - with translation.override('de'): + trans_real._active = local() + trans_real._translations = {} + with translation.override('de'): - # {% trans %} ----------------------------------- + # {% trans %} ----------------------------------- - # Inexisting context... - t = Template('{% load i18n %}{% trans "May" context "unexisting" %}') - rendered = t.render(Context()) - self.assertEqual(rendered, 'May') + # Inexisting context... + t = Template('{% load i18n %}{% trans "May" context "unexisting" %}') + rendered = t.render(Context()) + self.assertEqual(rendered, 'May') - # Existing context... - # Using a literal - t = Template('{% load i18n %}{% trans "May" context "month name" %}') - rendered = t.render(Context()) - self.assertEqual(rendered, 'Mai') - t = Template('{% load i18n %}{% trans "May" context "verb" %}') - rendered = t.render(Context()) - self.assertEqual(rendered, 'Kann') + # Existing context... + # Using a literal + t = Template('{% load i18n %}{% trans "May" context "month name" %}') + rendered = t.render(Context()) + self.assertEqual(rendered, 'Mai') + t = Template('{% load i18n %}{% trans "May" context "verb" %}') + rendered = t.render(Context()) + self.assertEqual(rendered, 'Kann') - # Using a variable - t = Template('{% load i18n %}{% trans "May" context message_context %}') - rendered = t.render(Context({'message_context': 'month name'})) - self.assertEqual(rendered, 'Mai') - t = Template('{% load i18n %}{% trans "May" context message_context %}') - rendered = t.render(Context({'message_context': 'verb'})) - self.assertEqual(rendered, 'Kann') + # Using a variable + t = Template('{% load i18n %}{% trans "May" context message_context %}') + rendered = t.render(Context({'message_context': 'month name'})) + self.assertEqual(rendered, 'Mai') + t = Template('{% load i18n %}{% trans "May" context message_context %}') + rendered = t.render(Context({'message_context': 'verb'})) + self.assertEqual(rendered, 'Kann') - # Using a filter - t = Template('{% load i18n %}{% trans "May" context message_context|lower %}') - rendered = t.render(Context({'message_context': 'MONTH NAME'})) - self.assertEqual(rendered, 'Mai') - t = Template('{% load i18n %}{% trans "May" context message_context|lower %}') - rendered = t.render(Context({'message_context': 'VERB'})) - self.assertEqual(rendered, 'Kann') + # Using a filter + t = Template('{% load i18n %}{% trans "May" context message_context|lower %}') + rendered = t.render(Context({'message_context': 'MONTH NAME'})) + self.assertEqual(rendered, 'Mai') + t = Template('{% load i18n %}{% trans "May" context message_context|lower %}') + rendered = t.render(Context({'message_context': 'VERB'})) + self.assertEqual(rendered, 'Kann') - # Using 'as' - t = Template('{% load i18n %}{% trans "May" context "month name" as var %}Value: {{ var }}') - rendered = t.render(Context()) - self.assertEqual(rendered, 'Value: Mai') - t = Template('{% load i18n %}{% trans "May" as var context "verb" %}Value: {{ var }}') - rendered = t.render(Context()) - self.assertEqual(rendered, 'Value: Kann') + # Using 'as' + t = Template('{% load i18n %}{% trans "May" context "month name" as var %}Value: {{ var }}') + rendered = t.render(Context()) + self.assertEqual(rendered, 'Value: Mai') + t = Template('{% load i18n %}{% trans "May" as var context "verb" %}Value: {{ var }}') + rendered = t.render(Context()) + self.assertEqual(rendered, 'Value: Kann') - # Mis-uses - self.assertRaises(TemplateSyntaxError, Template, '{% load i18n %}{% trans "May" context as var %}{{ var }}') - self.assertRaises(TemplateSyntaxError, Template, '{% load i18n %}{% trans "May" as var context %}{{ var }}') + # Mis-uses + self.assertRaises(TemplateSyntaxError, Template, '{% load i18n %}{% trans "May" context as var %}{{ var }}') + self.assertRaises(TemplateSyntaxError, Template, '{% load i18n %}{% trans "May" as var context %}{{ var }}') - # {% blocktrans %} ------------------------------ + # {% blocktrans %} ------------------------------ - # Inexisting context... - t = Template('{% load i18n %}{% blocktrans context "unexisting" %}May{% endblocktrans %}') - rendered = t.render(Context()) - self.assertEqual(rendered, 'May') + # Inexisting context... + t = Template('{% load i18n %}{% blocktrans context "unexisting" %}May{% endblocktrans %}') + rendered = t.render(Context()) + self.assertEqual(rendered, 'May') - # Existing context... - # Using a literal - t = Template('{% load i18n %}{% blocktrans context "month name" %}May{% endblocktrans %}') - rendered = t.render(Context()) - self.assertEqual(rendered, 'Mai') - t = Template('{% load i18n %}{% blocktrans context "verb" %}May{% endblocktrans %}') - rendered = t.render(Context()) - self.assertEqual(rendered, 'Kann') + # Existing context... + # Using a literal + t = Template('{% load i18n %}{% blocktrans context "month name" %}May{% endblocktrans %}') + rendered = t.render(Context()) + self.assertEqual(rendered, 'Mai') + t = Template('{% load i18n %}{% blocktrans context "verb" %}May{% endblocktrans %}') + rendered = t.render(Context()) + self.assertEqual(rendered, 'Kann') - # Using a variable - t = Template('{% load i18n %}{% blocktrans context message_context %}May{% endblocktrans %}') - rendered = t.render(Context({'message_context': 'month name'})) - self.assertEqual(rendered, 'Mai') - t = Template('{% load i18n %}{% blocktrans context message_context %}May{% endblocktrans %}') - rendered = t.render(Context({'message_context': 'verb'})) - self.assertEqual(rendered, 'Kann') + # Using a variable + t = Template('{% load i18n %}{% blocktrans context message_context %}May{% endblocktrans %}') + rendered = t.render(Context({'message_context': 'month name'})) + self.assertEqual(rendered, 'Mai') + t = Template('{% load i18n %}{% blocktrans context message_context %}May{% endblocktrans %}') + rendered = t.render(Context({'message_context': 'verb'})) + self.assertEqual(rendered, 'Kann') - # Using a filter - t = Template('{% load i18n %}{% blocktrans context message_context|lower %}May{% endblocktrans %}') - rendered = t.render(Context({'message_context': 'MONTH NAME'})) - self.assertEqual(rendered, 'Mai') - t = Template('{% load i18n %}{% blocktrans context message_context|lower %}May{% endblocktrans %}') - rendered = t.render(Context({'message_context': 'VERB'})) - self.assertEqual(rendered, 'Kann') + # Using a filter + t = Template('{% load i18n %}{% blocktrans context message_context|lower %}May{% endblocktrans %}') + rendered = t.render(Context({'message_context': 'MONTH NAME'})) + self.assertEqual(rendered, 'Mai') + t = Template('{% load i18n %}{% blocktrans context message_context|lower %}May{% endblocktrans %}') + rendered = t.render(Context({'message_context': 'VERB'})) + self.assertEqual(rendered, 'Kann') - # Using 'count' - t = Template('{% load i18n %}{% blocktrans count number=1 context "super search" %}{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}') - rendered = t.render(Context()) - self.assertEqual(rendered, '1 Super-Ergebnis') - t = Template('{% load i18n %}{% blocktrans count number=2 context "super search" %}{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}') - rendered = t.render(Context()) - self.assertEqual(rendered, '2 Super-Ergebnisse') - t = Template('{% load i18n %}{% blocktrans context "other super search" count number=1 %}{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}') - rendered = t.render(Context()) - self.assertEqual(rendered, '1 anderen Super-Ergebnis') - t = Template('{% load i18n %}{% blocktrans context "other super search" count number=2 %}{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}') - rendered = t.render(Context()) - self.assertEqual(rendered, '2 andere Super-Ergebnisse') + # Using 'count' + t = Template('{% load i18n %}{% blocktrans count number=1 context "super search" %}{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}') + rendered = t.render(Context()) + self.assertEqual(rendered, '1 Super-Ergebnis') + t = Template('{% load i18n %}{% blocktrans count number=2 context "super search" %}{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}') + rendered = t.render(Context()) + self.assertEqual(rendered, '2 Super-Ergebnisse') + t = Template('{% load i18n %}{% blocktrans context "other super search" count number=1 %}{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}') + rendered = t.render(Context()) + self.assertEqual(rendered, '1 anderen Super-Ergebnis') + t = Template('{% load i18n %}{% blocktrans context "other super search" count number=2 %}{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}') + rendered = t.render(Context()) + self.assertEqual(rendered, '2 andere Super-Ergebnisse') - # Using 'with' - t = Template('{% load i18n %}{% blocktrans with num_comments=5 context "comment count" %}There are {{ num_comments }} comments{% endblocktrans %}') - rendered = t.render(Context()) - self.assertEqual(rendered, 'Es gibt 5 Kommentare') - t = Template('{% load i18n %}{% blocktrans with num_comments=5 context "other comment count" %}There are {{ num_comments }} comments{% endblocktrans %}') - rendered = t.render(Context()) - self.assertEqual(rendered, 'Andere: Es gibt 5 Kommentare') + # Using 'with' + t = Template('{% load i18n %}{% blocktrans with num_comments=5 context "comment count" %}There are {{ num_comments }} comments{% endblocktrans %}') + rendered = t.render(Context()) + self.assertEqual(rendered, 'Es gibt 5 Kommentare') + t = Template('{% load i18n %}{% blocktrans with num_comments=5 context "other comment count" %}There are {{ num_comments }} comments{% endblocktrans %}') + rendered = t.render(Context()) + self.assertEqual(rendered, 'Andere: Es gibt 5 Kommentare') - # Mis-uses - self.assertRaises(TemplateSyntaxError, Template, '{% load i18n %}{% blocktrans context with month="May" %}{{ month }}{% endblocktrans %}') - self.assertRaises(TemplateSyntaxError, Template, '{% load i18n %}{% blocktrans context %}{% endblocktrans %}') - self.assertRaises(TemplateSyntaxError, Template, '{% load i18n %}{% blocktrans count number=2 context %}{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}') + # Mis-uses + self.assertRaises(TemplateSyntaxError, Template, '{% load i18n %}{% blocktrans context with month="May" %}{{ month }}{% endblocktrans %}') + self.assertRaises(TemplateSyntaxError, Template, '{% load i18n %}{% blocktrans context %}{% endblocktrans %}') + self.assertRaises(TemplateSyntaxError, Template, '{% load i18n %}{% blocktrans count number=2 context %}{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}') def test_string_concat(self): @@ -247,8 +240,7 @@ class TranslationTests(TestCase): Translations on files with mac or dos end of lines will be converted to unix eof in .po catalogs, and they have to match when retrieved """ - from django.utils.translation.trans_real import translation as Trans - ca_translation = Trans('ca') + ca_translation = trans_real.translation('ca') ca_translation._catalog['Mac\nEOF\n'] = 'Catalan Mac\nEOF\n' ca_translation._catalog['Win\nEOF\n'] = 'Catalan Win\nEOF\n' with translation.override('ca', deactivate=True): @@ -267,9 +259,8 @@ class TranslationTests(TestCase): """ Test the to_language function """ - from django.utils.translation.trans_real import to_language - self.assertEqual(to_language('en_US'), 'en-us') - self.assertEqual(to_language('sr_Lat'), 'sr-lat') + self.assertEqual(trans_real.to_language('en_US'), 'en-us') + self.assertEqual(trans_real.to_language('sr_Lat'), 'sr-lat') @override_settings(LOCALE_PATHS=(os.path.join(here, 'other', 'locale'),)) def test_bad_placeholder_1(self): @@ -278,7 +269,6 @@ class TranslationTests(TestCase): (%(person)s is translated as %(personne)s in fr.po) Refs #16516. """ - from django.template import Template, Context with translation.override('fr'): t = Template('{% load i18n %}{% blocktrans %}My name is {{ person }}.{% endblocktrans %}') rendered = t.render(Context({'person': 'James'})) @@ -291,7 +281,6 @@ class TranslationTests(TestCase): (%(person) misses a 's' in fr.po, causing the string formatting to fail) Refs #18393. """ - from django.template import Template, Context with translation.override('fr'): t = Template('{% load i18n %}{% blocktrans %}My other name is {{ person }}.{% endblocktrans %}') rendered = t.render(Context({'person': 'James'})) @@ -710,11 +699,11 @@ class MiscTests(TestCase): values according to the spec (and that we extract all the pieces in the right order). """ - from django.utils.translation.trans_real import parse_accept_lang_header - p = parse_accept_lang_header + p = trans_real.parse_accept_lang_header # Good headers. self.assertEqual([('de', 1.0)], p('de')) self.assertEqual([('en-AU', 1.0)], p('en-AU')) + self.assertEqual([('es-419', 1.0)], p('es-419')) self.assertEqual([('*', 1.0)], p('*;q=1.00')) self.assertEqual([('en-AU', 0.123)], p('en-AU;q=0.123')) self.assertEqual([('en-au', 0.5)], p('en-au;q=0.5')) @@ -739,6 +728,7 @@ class MiscTests(TestCase): self.assertEqual([], p('da, en-gb;q=0.8, en;q=0.7,#')) self.assertEqual([], p('de;q=2.0')) self.assertEqual([], p('de;q=0.a')) + self.assertEqual([], p('12-345')) self.assertEqual([], p('')) def test_parse_literal_http_header(self): @@ -809,7 +799,7 @@ class MiscTests(TestCase): self.assertEqual(g(r), 'zh-cn') def test_get_language_from_path_real(self): - from django.utils.translation.trans_real import get_language_from_path as g + g = trans_real.get_language_from_path self.assertEqual(g('/pl/'), 'pl') self.assertEqual(g('/pl'), 'pl') self.assertEqual(g('/xyz/'), None) @@ -820,23 +810,33 @@ class MiscTests(TestCase): self.assertEqual(g('/pl'), None) self.assertEqual(g('/xyz/'), None) + @override_settings(LOCALE_PATHS=extended_locale_paths) def test_percent_in_translatable_block(self): - extended_locale_paths = settings.LOCALE_PATHS + ( - os.path.join(here, 'other', 'locale'), - ) - with self.settings(LOCALE_PATHS=extended_locale_paths): - t_sing = Template("{% load i18n %}{% blocktrans %}The result was {{ percent }}%{% endblocktrans %}") - t_plur = Template("{% load i18n %}{% blocktrans count num as number %}{{ percent }}% represents {{ num }} object{% plural %}{{ percent }}% represents {{ num }} objects{% endblocktrans %}") - with translation.override('de'): - self.assertEqual(t_sing.render(Context({'percent': 42})), 'Das Ergebnis war 42%') - self.assertEqual(t_plur.render(Context({'percent': 42, 'num': 1})), '42% stellt 1 Objekt dar') - self.assertEqual(t_plur.render(Context({'percent': 42, 'num': 4})), '42% stellt 4 Objekte dar') + t_sing = Template("{% load i18n %}{% blocktrans %}The result was {{ percent }}%{% endblocktrans %}") + t_plur = Template("{% load i18n %}{% blocktrans count num as number %}{{ percent }}% represents {{ num }} object{% plural %}{{ percent }}% represents {{ num }} objects{% endblocktrans %}") + with translation.override('de'): + self.assertEqual(t_sing.render(Context({'percent': 42})), 'Das Ergebnis war 42%') + self.assertEqual(t_plur.render(Context({'percent': 42, 'num': 1})), '42% stellt 1 Objekt dar') + self.assertEqual(t_plur.render(Context({'percent': 42, 'num': 4})), '42% stellt 4 Objekte dar') + + @override_settings(LOCALE_PATHS=extended_locale_paths) + def test_percent_formatting_in_blocktrans(self): + """ + Test that using Python's %-formatting is properly escaped in blocktrans, + singular or plural + """ + t_sing = Template("{% load i18n %}{% blocktrans %}There are %(num_comments)s comments{% endblocktrans %}") + t_plur = Template("{% load i18n %}{% blocktrans count num as number %}%(percent)s% represents {{ num }} object{% plural %}%(percent)s% represents {{ num }} objects{% endblocktrans %}") + with translation.override('de'): + # Strings won't get translated as they don't match after escaping % + self.assertEqual(t_sing.render(Context({'num_comments': 42})), 'There are %(num_comments)s comments') + self.assertEqual(t_plur.render(Context({'percent': 42, 'num': 1})), '%(percent)s% represents 1 object') + self.assertEqual(t_plur.render(Context({'percent': 42, 'num': 4})), '%(percent)s% represents 4 objects') class ResolutionOrderI18NTests(TestCase): def setUp(self): - from django.utils.translation import trans_real # Okay, this is brutal, but we have no other choice to fully reset # the translation framework trans_real._active = local() @@ -865,17 +865,9 @@ class AppResolutionOrderI18NTests(ResolutionOrderI18NTests): def test_app_translation(self): self.assertUgettext('Date/time', 'APP') +@override_settings(LOCALE_PATHS=extended_locale_paths) class LocalePathsResolutionOrderI18NTests(ResolutionOrderI18NTests): - def setUp(self): - self.old_locale_paths = settings.LOCALE_PATHS - settings.LOCALE_PATHS += (os.path.join(here, 'other', 'locale'),) - super(LocalePathsResolutionOrderI18NTests, self).setUp() - - def tearDown(self): - settings.LOCALE_PATHS = self.old_locale_paths - super(LocalePathsResolutionOrderI18NTests, self).tearDown() - def test_locale_paths_translation(self): self.assertUgettext('Time', 'LOCALE_PATHS') diff --git a/tests/regressiontests/localflavor/ar/tests.py b/tests/regressiontests/localflavor/ar/tests.py deleted file mode 100644 index 0bc228eae9..0000000000 --- a/tests/regressiontests/localflavor/ar/tests.py +++ /dev/null @@ -1,103 +0,0 @@ -from __future__ import unicode_literals - -from django.contrib.localflavor.ar.forms import (ARProvinceSelect, - ARPostalCodeField, ARDNIField, ARCUITField) - -from django.test import SimpleTestCase - - -class ARLocalFlavorTests(SimpleTestCase): - def test_ARProvinceSelect(self): - f = ARProvinceSelect() - out = '''''' - self.assertHTMLEqual(f.render('provincias', 'A'), out) - - def test_ARPostalCodeField(self): - error_format = ['Enter a postal code in the format NNNN or ANNNNAAA.'] - error_atmost = ['Ensure this value has at most 8 characters (it has 9).'] - error_atleast = ['Ensure this value has at least 4 characters (it has 3).'] - valid = { - '5000': '5000', - 'C1064AAB': 'C1064AAB', - 'c1064AAB': 'C1064AAB', - 'C1064aab': 'C1064AAB', - '4400': '4400', - 'C1064AAB': 'C1064AAB', - } - invalid = { - 'C1064AABB': error_atmost + error_format, - 'C1064AA': error_format, - 'C1064AB': error_format, - '106AAB': error_format, - '500': error_atleast + error_format, - '5PPP': error_format, - } - self.assertFieldOutput(ARPostalCodeField, valid, invalid) - - def test_ARDNIField(self): - error_length = ['This field requires 7 or 8 digits.'] - error_digitsonly = ['This field requires only numbers.'] - valid = { - '20123456': '20123456', - '20.123.456': '20123456', - '20123456': '20123456', - '20.123.456': '20123456', - '20.123456': '20123456', - '9123456': '9123456', - '9.123.456': '9123456', - } - invalid = { - '101234566': error_length, - 'W0123456': error_digitsonly, - '10,123,456': error_digitsonly, - } - self.assertFieldOutput(ARDNIField, valid, invalid) - - def test_ARCUITField(self): - error_format = ['Enter a valid CUIT in XX-XXXXXXXX-X or XXXXXXXXXXXX format.'] - error_invalid = ['Invalid CUIT.'] - error_legal_type = ['Invalid legal type. Type must be 27, 20, 23 or 30.'] - valid = { - '20-10123456-9': '20-10123456-9', - '20-10123456-9': '20-10123456-9', - '27-10345678-4': '27-10345678-4', - '20101234569': '20-10123456-9', - '27103456784': '27-10345678-4', - '30011111110': '30-01111111-0', - } - invalid = { - '2-10123456-9': error_format, - '210123456-9': error_format, - '20-10123456': error_format, - '20-10123456-': error_format, - '20-10123456-5': error_invalid, - '27-10345678-1': error_invalid, - '27-10345678-1': error_invalid, - '11211111110': error_legal_type, - } - self.assertFieldOutput(ARCUITField, valid, invalid) diff --git a/tests/regressiontests/localflavor/at/tests.py b/tests/regressiontests/localflavor/at/tests.py deleted file mode 100644 index 9123ba4e88..0000000000 --- a/tests/regressiontests/localflavor/at/tests.py +++ /dev/null @@ -1,49 +0,0 @@ -from __future__ import unicode_literals - -from django.contrib.localflavor.at.forms import (ATZipCodeField, ATStateSelect, - ATSocialSecurityNumberField) - -from django.test import SimpleTestCase - - -class ATLocalFlavorTests(SimpleTestCase): - def test_ATStateSelect(self): - f = ATStateSelect() - out = '''''' - self.assertHTMLEqual(f.render('bundesland', 'WI'), out) - - def test_ATZipCodeField(self): - error_format = ['Enter a zip code in the format XXXX.'] - valid = { - '1150': '1150', - '4020': '4020', - '8020': '8020', - } - invalid = { - '0000' : error_format, - '0123' : error_format, - '111222': error_format, - 'eeffee': error_format, - } - self.assertFieldOutput(ATZipCodeField, valid, invalid) - - def test_ATSocialSecurityNumberField(self): - error_format = ['Enter a valid Austrian Social Security Number in XXXX XXXXXX format.'] - valid = { - '1237 010180': '1237 010180', - } - invalid = { - '1237 010181': error_format, - '12370 010180': error_format, - } - self.assertFieldOutput(ATSocialSecurityNumberField, valid, invalid) diff --git a/tests/regressiontests/localflavor/au/forms.py b/tests/regressiontests/localflavor/au/forms.py deleted file mode 100644 index aec00694fe..0000000000 --- a/tests/regressiontests/localflavor/au/forms.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import absolute_import - -from django.forms import ModelForm - -from .models import AustralianPlace - - -class AustralianPlaceForm(ModelForm): - """ Form for storing an Australian place. """ - class Meta: - model = AustralianPlace diff --git a/tests/regressiontests/localflavor/au/models.py b/tests/regressiontests/localflavor/au/models.py deleted file mode 100644 index 39061c5bb3..0000000000 --- a/tests/regressiontests/localflavor/au/models.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.contrib.localflavor.au.models import AUStateField, AUPostCodeField -from django.db import models - -class AustralianPlace(models.Model): - state = AUStateField(blank=True) - state_required = AUStateField() - state_default = AUStateField(default="NSW", blank=True) - postcode = AUPostCodeField(blank=True) - postcode_required = AUPostCodeField() - postcode_default = AUPostCodeField(default="2500", blank=True) - name = models.CharField(max_length=20) - - class Meta: - app_label = 'localflavor' diff --git a/tests/regressiontests/localflavor/au/tests.py b/tests/regressiontests/localflavor/au/tests.py deleted file mode 100644 index 69472f0935..0000000000 --- a/tests/regressiontests/localflavor/au/tests.py +++ /dev/null @@ -1,108 +0,0 @@ -from __future__ import absolute_import, unicode_literals - -import re - -from django.test import SimpleTestCase -from django.contrib.localflavor.au.forms import (AUPostCodeField, - AUPhoneNumberField, AUStateSelect) - -from .forms import AustralianPlaceForm - - -SELECTED_OPTION_PATTERN = r'

    y

    ", "a b", "x

    y

    "), ) for value, tags, output in items: - self.assertEquals(f(value, tags), output) + self.assertEqual(f(value, tags), output) diff --git a/tests/regressiontests/utils/http.py b/tests/regressiontests/utils/http.py index f22e05496d..6d3bc025af 100644 --- a/tests/regressiontests/utils/http.py +++ b/tests/regressiontests/utils/http.py @@ -1,3 +1,4 @@ +from datetime import datetime import sys from django.http import HttpResponse, utils @@ -7,6 +8,7 @@ from django.utils import http from django.utils import six from django.utils import unittest + class TestUtilsHttp(unittest.TestCase): def test_same_origin_true(self): @@ -132,3 +134,30 @@ class TestUtilsHttp(unittest.TestCase): for n, b36 in [(0, '0'), (1, '1'), (42, '16'), (818469960, 'django')]: self.assertEqual(http.int_to_base36(n), b36) self.assertEqual(http.base36_to_int(b36), n) + + +class ETagProcessingTests(unittest.TestCase): + def testParsing(self): + etags = http.parse_etags(r'"", "etag", "e\"t\"ag", "e\\tag", W/"weak"') + self.assertEqual(etags, ['', 'etag', 'e"t"ag', r'e\tag', 'weak']) + + def testQuoting(self): + quoted_etag = http.quote_etag(r'e\t"ag') + self.assertEqual(quoted_etag, r'"e\\t\"ag"') + + +class HttpDateProcessingTests(unittest.TestCase): + def testParsingRfc1123(self): + parsed = http.parse_http_date('Sun, 06 Nov 1994 08:49:37 GMT') + self.assertEqual(datetime.utcfromtimestamp(parsed), + datetime(1994, 11, 6, 8, 49, 37)) + + def testParsingRfc850(self): + parsed = http.parse_http_date('Sunday, 06-Nov-94 08:49:37 GMT') + self.assertEqual(datetime.utcfromtimestamp(parsed), + datetime(1994, 11, 6, 8, 49, 37)) + + def testParsingAsctime(self): + parsed = http.parse_http_date('Sun Nov 6 08:49:37 1994') + self.assertEqual(datetime.utcfromtimestamp(parsed), + datetime(1994, 11, 6, 8, 49, 37)) diff --git a/tests/regressiontests/utils/numberformat.py b/tests/regressiontests/utils/numberformat.py new file mode 100644 index 0000000000..f9d9031e48 --- /dev/null +++ b/tests/regressiontests/utils/numberformat.py @@ -0,0 +1,47 @@ +from unittest import TestCase +from django.utils.numberformat import format as nformat +from sys import float_info + + +class TestNumberFormat(TestCase): + + def test_format_number(self): + self.assertEqual(nformat(1234, '.'), '1234') + self.assertEqual(nformat(1234.2, '.'), '1234.2') + self.assertEqual(nformat(1234, '.', decimal_pos=2), '1234.00') + self.assertEqual(nformat(1234, '.', grouping=2, thousand_sep=','), + '1234') + self.assertEqual(nformat(1234, '.', grouping=2, thousand_sep=',', + force_grouping=True), '12,34') + self.assertEqual(nformat(-1234.33, '.', decimal_pos=1), '-1234.3') + + def test_format_string(self): + self.assertEqual(nformat('1234', '.'), '1234') + self.assertEqual(nformat('1234.2', '.'), '1234.2') + self.assertEqual(nformat('1234', '.', decimal_pos=2), '1234.00') + self.assertEqual(nformat('1234', '.', grouping=2, thousand_sep=','), + '1234') + self.assertEqual(nformat('1234', '.', grouping=2, thousand_sep=',', + force_grouping=True), '12,34') + self.assertEqual(nformat('-1234.33', '.', decimal_pos=1), '-1234.3') + + def test_large_number(self): + most_max = ('{0}179769313486231570814527423731704356798070567525844996' + '598917476803157260780028538760589558632766878171540458953' + '514382464234321326889464182768467546703537516986049910576' + '551282076245490090389328944075868508455133942304583236903' + '222948165808559332123348274797826204144723168738177180919' + '29988125040402618412485836{1}') + most_max2 = ('{0}35953862697246314162905484746340871359614113505168999' + '31978349536063145215600570775211791172655337563430809179' + '07028764928468642653778928365536935093407075033972099821' + '15310256415249098018077865788815173701691026788460916647' + '38064458963316171186642466965495956524082894463374763543' + '61838599762500808052368249716736') + int_max = int(float_info.max) + self.assertEqual(nformat(int_max, '.'), most_max.format('', '8')) + self.assertEqual(nformat(int_max + 1, '.'), most_max.format('', '9')) + self.assertEqual(nformat(int_max * 2, '.'), most_max2.format('')) + self.assertEqual(nformat(0 - int_max, '.'), most_max.format('-', '8')) + self.assertEqual(nformat(-1 - int_max, '.'), most_max.format('-', '9')) + self.assertEqual(nformat(-2 * int_max, '.'), most_max2.format('-')) diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py index 061c669eb7..11dd7c320e 100644 --- a/tests/regressiontests/utils/tests.py +++ b/tests/regressiontests/utils/tests.py @@ -17,10 +17,11 @@ from .encoding import TestEncodingUtils from .feedgenerator import FeedgeneratorTest from .functional import FunctionalTestCase from .html import TestUtilsHtml -from .http import TestUtilsHttp +from .http import TestUtilsHttp, ETagProcessingTests, HttpDateProcessingTests from .ipv6 import TestUtilsIPv6 from .jslex import JsToCForGettextTest, JsTokensTest from .module_loading import CustomLoader, DefaultLoader, EggLoader +from .numberformat import TestNumberFormat from .os_utils import SafeJoinTests from .regex_helper import NormalizeTests from .simplelazyobject import TestUtilsSimpleLazyObject diff --git a/tests/regressiontests/views/tests/__init__.py b/tests/regressiontests/views/tests/__init__.py index 63db5da2a3..12d0c59014 100644 --- a/tests/regressiontests/views/tests/__init__.py +++ b/tests/regressiontests/views/tests/__init__.py @@ -7,4 +7,4 @@ from .defaults import DefaultsTests from .i18n import JsI18NTests, I18NTests, JsI18NTestsMultiPackage from .shortcuts import ShortcutTests from .specials import URLHandling -from .static import StaticHelperTest, StaticTests +from .static import StaticHelperTest, StaticUtilsTests, StaticTests diff --git a/tests/regressiontests/views/tests/debug.py b/tests/regressiontests/views/tests/debug.py index 8592b07efe..e616d184b8 100644 --- a/tests/regressiontests/views/tests/debug.py +++ b/tests/regressiontests/views/tests/debug.py @@ -1,3 +1,6 @@ +# -*- coding: utf-8 -*- +# This coding header is significant for tests, as the debug view is parsing +# files to search for such a header to decode the source file content from __future__ import absolute_import, unicode_literals import inspect diff --git a/tests/regressiontests/views/tests/defaults.py b/tests/regressiontests/views/tests/defaults.py index 2dd40b4a1a..3ca7f79136 100644 --- a/tests/regressiontests/views/tests/defaults.py +++ b/tests/regressiontests/views/tests/defaults.py @@ -1,7 +1,8 @@ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals -from django.test import TestCase from django.contrib.contenttypes.models import ContentType +from django.test import TestCase +from django.test.utils import setup_test_template_loader, restore_template_loaders from ..models import Author, Article, UrlArticle @@ -71,6 +72,23 @@ class DefaultsTests(TestCase): response = self.client.get('/views/server_error/') self.assertEqual(response.status_code, 500) + def test_custom_templates(self): + """ + Test that 404.html and 500.html templates are picked by their respective + handler. + """ + setup_test_template_loader( + {'404.html': 'This is a test template for a 404 error.', + '500.html': 'This is a test template for a 500 error.'} + ) + try: + for code, url in ((404, '/views/non_existing_url/'), (500, '/views/server_error/')): + response = self.client.get(url) + self.assertContains(response, "test template for a %d error" % code, + status_code=code) + finally: + restore_template_loaders() + def test_get_absolute_url_attributes(self): "A model can set attributes on the get_absolute_url method" self.assertTrue(getattr(UrlArticle.get_absolute_url, 'purge', False), diff --git a/tests/regressiontests/views/tests/static.py b/tests/regressiontests/views/tests/static.py index 9d87ade137..8b8ef8ba9b 100644 --- a/tests/regressiontests/views/tests/static.py +++ b/tests/regressiontests/views/tests/static.py @@ -2,54 +2,59 @@ from __future__ import absolute_import import mimetypes from os import path +import unittest -from django.conf import settings from django.conf.urls.static import static -from django.test import TestCase from django.http import HttpResponseNotModified +from django.test import TestCase +from django.test.utils import override_settings +from django.utils.http import http_date +from django.views.static import was_modified_since from .. import urls from ..urls import media_dir +@override_settings(DEBUG=True) class StaticTests(TestCase): """Tests django views in django/views/static.py""" - def setUp(self): - self.prefix = 'site_media' - self.old_debug = settings.DEBUG - settings.DEBUG = True - - def tearDown(self): - settings.DEBUG = self.old_debug + prefix = 'site_media' def test_serve(self): "The static view can serve static media" media_files = ['file.txt', 'file.txt.gz'] for filename in media_files: response = self.client.get('/views/%s/%s' % (self.prefix, filename)) + response_content = b''.join(response) + response.close() file_path = path.join(media_dir, filename) with open(file_path, 'rb') as fp: - self.assertEqual(fp.read(), response.content) - self.assertEqual(len(response.content), int(response['Content-Length'])) + self.assertEqual(fp.read(), response_content) + self.assertEqual(len(response_content), int(response['Content-Length'])) self.assertEqual(mimetypes.guess_type(file_path)[1], response.get('Content-Encoding', None)) def test_unknown_mime_type(self): response = self.client.get('/views/%s/file.unknown' % self.prefix) + response.close() self.assertEqual('application/octet-stream', response['Content-Type']) def test_copes_with_empty_path_component(self): file_name = 'file.txt' response = self.client.get('/views/%s//%s' % (self.prefix, file_name)) + response_content = b''.join(response) + response.close() with open(path.join(media_dir, file_name), 'rb') as fp: - self.assertEqual(fp.read(), response.content) + self.assertEqual(fp.read(), response_content) def test_is_modified_since(self): file_name = 'file.txt' response = self.client.get('/views/%s/%s' % (self.prefix, file_name), HTTP_IF_MODIFIED_SINCE='Thu, 1 Jan 1970 00:00:00 GMT') + response_content = b''.join(response) + response.close() with open(path.join(media_dir, file_name), 'rb') as fp: - self.assertEqual(fp.read(), response.content) + self.assertEqual(fp.read(), response_content) def test_not_modified_since(self): file_name = 'file.txt' @@ -71,9 +76,11 @@ class StaticTests(TestCase): invalid_date = 'Mon, 28 May 999999999999 28:25:26 GMT' response = self.client.get('/views/%s/%s' % (self.prefix, file_name), HTTP_IF_MODIFIED_SINCE=invalid_date) + response_content = b''.join(response) + response.close() with open(path.join(media_dir, file_name), 'rb') as fp: - self.assertEqual(fp.read(), response.content) - self.assertEqual(len(response.content), + self.assertEqual(fp.read(), response_content) + self.assertEqual(len(response_content), int(response['Content-Length'])) def test_invalid_if_modified_since2(self): @@ -86,9 +93,11 @@ class StaticTests(TestCase): invalid_date = ': 1291108438, Wed, 20 Oct 2010 14:05:00 GMT' response = self.client.get('/views/%s/%s' % (self.prefix, file_name), HTTP_IF_MODIFIED_SINCE=invalid_date) + response_content = b''.join(response) + response.close() with open(path.join(media_dir, file_name), 'rb') as fp: - self.assertEqual(fp.read(), response.content) - self.assertEqual(len(response.content), + self.assertEqual(fp.read(), response_content) + self.assertEqual(len(response_content), int(response['Content-Length'])) @@ -98,10 +107,20 @@ class StaticHelperTest(StaticTests): """ def setUp(self): super(StaticHelperTest, self).setUp() - self.prefix = 'media' self._old_views_urlpatterns = urls.urlpatterns[:] urls.urlpatterns += static('/media/', document_root=media_dir) def tearDown(self): super(StaticHelperTest, self).tearDown() urls.urlpatterns = self._old_views_urlpatterns + + +class StaticUtilsTests(unittest.TestCase): + def test_was_modified_since_fp(self): + """ + Test that a floating point mtime does not disturb was_modified_since. + (#18675) + """ + mtime = 1343416141.107817 + header = http_date(mtime) + self.assertFalse(was_modified_since(header, mtime)) diff --git a/tests/templates/404.html b/tests/templates/404.html deleted file mode 100644 index da627e2222..0000000000 --- a/tests/templates/404.html +++ /dev/null @@ -1 +0,0 @@ -Django Internal Tests: 404 Error \ No newline at end of file diff --git a/tests/templates/500.html b/tests/templates/500.html deleted file mode 100644 index ff028cbeb0..0000000000 --- a/tests/templates/500.html +++ /dev/null @@ -1 +0,0 @@ -Django Internal Tests: 500 Error \ No newline at end of file