1
0
mirror of https://github.com/django/django.git synced 2025-08-15 22:39:11 +00:00

Merge branch 'master' into schema-alteration

Conflicts:
	django/db/backends/__init__.py
	django/db/models/fields/related.py
	django/db/models/options.py
This commit is contained in:
Andrew Godwin 2012-10-26 08:41:13 +01:00
commit 6a632e0457
521 changed files with 11938 additions and 13372 deletions

4
.gitignore vendored
View File

@ -1,4 +1,8 @@
*.egg-info *.egg-info
*.pot *.pot
*.py[co] *.py[co]
MANIFEST
dist/
docs/_build/ docs/_build/
tests/coverage_html/
tests/.coverage

View File

@ -4,3 +4,5 @@ syntax:glob
*.pot *.pot
*.py[co] *.py[co]
docs/_build/ docs/_build/
tests/coverage_html/
tests/.coverage

View File

@ -1,5 +1,5 @@
[main] [main]
host = https://www.transifex.net host = https://www.transifex.com
lang_map = sr@latin:sr_Latn lang_map = sr@latin:sr_Latn
[django.core] [django.core]

View File

@ -33,6 +33,7 @@ The PRIMARY AUTHORS are (and/or have been):
* Florian Apolloner * Florian Apolloner
* Jeremy Dunck * Jeremy Dunck
* Bryan Veloso * Bryan Veloso
* Preston Holmes
More information on the main contributors to Django can be found in More information on the main contributors to Django can be found in
docs/internals/committers.txt. docs/internals/committers.txt.
@ -424,6 +425,7 @@ answer newbie questions, and generally made Django that much better:
phil@produxion.net phil@produxion.net
phil.h.smith@gmail.com phil.h.smith@gmail.com
Gustavo Picon Gustavo Picon
Travis Pinney
Michael Placentra II <someone@michaelplacentra2.net> Michael Placentra II <someone@michaelplacentra2.net>
plisk plisk
Daniel Poelzleithner <http://poelzi.org/> Daniel Poelzleithner <http://poelzi.org/>
@ -499,6 +501,7 @@ answer newbie questions, and generally made Django that much better:
Wiliam Alves de Souza <wiliamsouza83@gmail.com> Wiliam Alves de Souza <wiliamsouza83@gmail.com>
Don Spaulding <donspauldingii@gmail.com> Don Spaulding <donspauldingii@gmail.com>
Calvin Spealman <ironfroggy@gmail.com> Calvin Spealman <ironfroggy@gmail.com>
Dane Springmeyer
Bjørn Stabell <bjorn@exoweb.net> Bjørn Stabell <bjorn@exoweb.net>
Georgi Stanojevski <glisha@gmail.com> Georgi Stanojevski <glisha@gmail.com>
starrynight <cmorgh@gmail.com> starrynight <cmorgh@gmail.com>

16
CONTRIBUTING.rst Normal file
View File

@ -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/

View File

@ -32,3 +32,5 @@ recursive-include django/contrib/gis/tests/geogapp/fixtures *
recursive-include django/contrib/gis/tests/relatedapp/fixtures * recursive-include django/contrib/gis/tests/relatedapp/fixtures *
recursive-include django/contrib/sitemaps/templates * recursive-include django/contrib/sitemaps/templates *
recursive-include django/contrib/sitemaps/tests/templates * recursive-include django/contrib/sitemaps/tests/templates *
recursive-exclude * __pycache__
recursive-exclude * *.py[co]

View File

@ -25,7 +25,7 @@ class LazySettings(LazyObject):
The user can manually configure settings prior to using them. Otherwise, The user can manually configure settings prior to using them. Otherwise,
Django uses the settings module pointed to by DJANGO_SETTINGS_MODULE. 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 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 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. if not settings_module: # If it's set but is an empty string.
raise KeyError raise KeyError
except KeyError: except KeyError:
desc = ("setting %s" % name) if name else "settings"
raise ImproperlyConfigured( 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 " "You must either define the environment variable %s "
"or call settings.configure() before accessing settings." "or call settings.configure() before accessing settings."
% (name, ENVIRONMENT_VARIABLE)) % (desc, ENVIRONMENT_VARIABLE))
self._wrapped = Settings(settings_module) self._wrapped = Settings(settings_module)
self._configure_logging()
def __getattr__(self, name): def __getattr__(self, name):
if self._wrapped is empty: if self._wrapped is empty:
self._setup(name) self._setup(name)
return getattr(self._wrapped, 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): def configure(self, default_settings=global_settings, **options):
""" """
@ -133,19 +153,6 @@ class Settings(BaseSettings):
os.environ['TZ'] = self.TIME_ZONE os.environ['TZ'] = self.TIME_ZONE
time.tzset() 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): class UserSettingsHolder(BaseSettings):
""" """

View File

@ -144,7 +144,7 @@ DEFAULT_CHARSET = 'utf-8'
# Encoding of files read from disk (template and initial SQL files). # Encoding of files read from disk (template and initial SQL files).
FILE_CHARSET = 'utf-8' FILE_CHARSET = 'utf-8'
# E-mail address that error messages come from. # Email address that error messages come from.
SERVER_EMAIL = 'root@localhost' SERVER_EMAIL = 'root@localhost'
# Whether to send broken-link emails. # Whether to send broken-link emails.
@ -488,6 +488,8 @@ PROFANITIES_LIST = ()
# AUTHENTICATION # # AUTHENTICATION #
################## ##################
AUTH_USER_MODEL = 'auth.User'
AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',) AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
LOGIN_URL = '/accounts/login/' LOGIN_URL = '/accounts/login/'
@ -549,33 +551,8 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.fallback.FallbackStorage'
# The callable to use to configure logging # The callable to use to configure logging
LOGGING_CONFIG = 'django.utils.log.dictConfig' LOGGING_CONFIG = 'django.utils.log.dictConfig'
# The default logging configuration. This sends an email to # Custom logging configuration.
# the site admins on every HTTP 500 error. All other log LOGGING = {}
# 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,
},
}
}
# Default exception reporter filter class used in case none has been # Default exception reporter filter class used in case none has been
# specifically assigned to the HttpRequest instance. # specifically assigned to the HttpRequest instance.

View File

@ -35,7 +35,7 @@ DATETIME_INPUT_FORMATS = (
'%Y-%m-%d', # '2006-10-25' '%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 # the DECIMAL_SEPARATOR is a . (decimal point) and the THOUSAND_SEPARATOR is a
# ' (single quote). # ' (single quote).
# For details, please refer to http://www.bk.admin.ch/dokumentation/sprachen/04915/05016/index.html?lang=de # For details, please refer to http://www.bk.admin.ch/dokumentation/sprachen/04915/05016/index.html?lang=de

View File

@ -4,7 +4,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Django\n" "Project-Id-Version: Django\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2010-05-13 15:35+0200\n"
"Last-Translator: Django team\n" "Last-Translator: Django team\n"
"Language-Team: English <en@li.org>\n" "Language-Team: English <en@li.org>\n"
@ -297,385 +297,386 @@ msgstr ""
msgid "Traditional Chinese" msgid "Traditional Chinese"
msgstr "" msgstr ""
#: core/validators.py:24 forms/fields.py:51 #: core/validators.py:21 forms/fields.py:52
msgid "Enter a valid value." msgid "Enter a valid value."
msgstr "" msgstr ""
#: core/validators.py:99 forms/fields.py:601 #: core/validators.py:104 forms/fields.py:464
msgid "This URL appears to be a broken link." msgid "Enter a valid email address."
msgstr "" msgstr ""
#: core/validators.py:131 forms/fields.py:600 #: core/validators.py:107 forms/fields.py:1013
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
msgid "" msgid ""
"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
msgstr "" 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." msgid "Enter a valid IPv4 address."
msgstr "" msgstr ""
#: core/validators.py:175 core/validators.py:189 #: core/validators.py:115 core/validators.py:130
msgid "Enter a valid IPv6 address." msgid "Enter a valid IPv6 address."
msgstr "" 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." msgid "Enter a valid IPv4 or IPv6 address."
msgstr "" 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." msgid "Enter only digits separated by commas."
msgstr "" msgstr ""
#: core/validators.py:215 #: core/validators.py:157
#, python-format #, python-format
msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)."
msgstr "" 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 #, python-format
msgid "Ensure this value is less than or equal to %(limit_value)s." msgid "Ensure this value is less than or equal to %(limit_value)s."
msgstr "" 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 #, python-format
msgid "Ensure this value is greater than or equal to %(limit_value)s." msgid "Ensure this value is greater than or equal to %(limit_value)s."
msgstr "" msgstr ""
#: core/validators.py:244 #: core/validators.py:189
#, python-format #, python-format
msgid "" msgid ""
"Ensure this value has at least %(limit_value)d characters (it has " "Ensure this value has at least %(limit_value)d characters (it has "
"%(show_value)d)." "%(show_value)d)."
msgstr "" msgstr ""
#: core/validators.py:250 #: core/validators.py:196
#, python-format #, python-format
msgid "" msgid ""
"Ensure this value has at most %(limit_value)d characters (it has " "Ensure this value has at most %(limit_value)d characters (it has "
"%(show_value)d)." "%(show_value)d)."
msgstr "" msgstr ""
#: db/models/base.py:764 #: db/models/base.py:843
#, python-format #, python-format
msgid "%(field_name)s must be unique for %(date_field)s %(lookup)s." msgid "%(field_name)s must be unique for %(date_field)s %(lookup)s."
msgstr "" msgstr ""
#: db/models/base.py:787 forms/models.py:577 #: db/models/base.py:866 forms/models.py:573
msgid "and" msgid "and"
msgstr "" 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 #, python-format
msgid "%(model_name)s with this %(field_label)s already exists." msgid "%(model_name)s with this %(field_label)s already exists."
msgstr "" msgstr ""
#: db/models/fields/__init__.py:62 #: db/models/fields/__init__.py:67
#, python-format #, python-format
msgid "Value %r is not a valid choice." msgid "Value %r is not a valid choice."
msgstr "" msgstr ""
#: db/models/fields/__init__.py:63 #: db/models/fields/__init__.py:68
msgid "This field cannot be null." msgid "This field cannot be null."
msgstr "" msgstr ""
#: db/models/fields/__init__.py:64 #: db/models/fields/__init__.py:69
msgid "This field cannot be blank." msgid "This field cannot be blank."
msgstr "" msgstr ""
#: db/models/fields/__init__.py:71 #: db/models/fields/__init__.py:76
#, python-format #, python-format
msgid "Field of type: %(field_type)s" msgid "Field of type: %(field_type)s"
msgstr "" 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" msgid "Integer"
msgstr "" 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 #, python-format
msgid "'%s' value must be an integer." msgid "'%s' value must be an integer."
msgstr "" msgstr ""
#: db/models/fields/__init__.py:552 #: db/models/fields/__init__.py:569
#, python-format #, python-format
msgid "'%s' value must be either True or False." msgid "'%s' value must be either True or False."
msgstr "" msgstr ""
#: db/models/fields/__init__.py:554 #: db/models/fields/__init__.py:571
msgid "Boolean (Either True or False)" msgid "Boolean (Either True or False)"
msgstr "" msgstr ""
#: db/models/fields/__init__.py:605 #: db/models/fields/__init__.py:622
#, python-format #, python-format
msgid "String (up to %(max_length)s)" msgid "String (up to %(max_length)s)"
msgstr "" msgstr ""
#: db/models/fields/__init__.py:633 #: db/models/fields/__init__.py:650
msgid "Comma-separated integers" msgid "Comma-separated integers"
msgstr "" msgstr ""
#: db/models/fields/__init__.py:647 #: db/models/fields/__init__.py:664
#, python-format #, python-format
msgid "'%s' value has an invalid date format. It must be in YYYY-MM-DD format." msgid "'%s' value has an invalid date format. It must be in YYYY-MM-DD format."
msgstr "" 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 #, python-format
msgid "" msgid ""
"'%s' value has the correct format (YYYY-MM-DD) but it is an invalid date." "'%s' value has the correct format (YYYY-MM-DD) but it is an invalid date."
msgstr "" msgstr ""
#: db/models/fields/__init__.py:652 #: db/models/fields/__init__.py:669
msgid "Date (without time)" msgid "Date (without time)"
msgstr "" msgstr ""
#: db/models/fields/__init__.py:732 #: db/models/fields/__init__.py:752
#, python-format #, python-format
msgid "" msgid ""
"'%s' value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[." "'%s' value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[."
"uuuuuu]][TZ] format." "uuuuuu]][TZ] format."
msgstr "" msgstr ""
#: db/models/fields/__init__.py:736 #: db/models/fields/__init__.py:756
#, python-format #, python-format
msgid "" msgid ""
"'%s' value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) but " "'%s' value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) but "
"it is an invalid date/time." "it is an invalid date/time."
msgstr "" msgstr ""
#: db/models/fields/__init__.py:740 #: db/models/fields/__init__.py:760
msgid "Date (with time)" msgid "Date (with time)"
msgstr "" msgstr ""
#: db/models/fields/__init__.py:831 #: db/models/fields/__init__.py:849
#, python-format #, python-format
msgid "'%s' value must be a decimal number." msgid "'%s' value must be a decimal number."
msgstr "" msgstr ""
#: db/models/fields/__init__.py:833 #: db/models/fields/__init__.py:851
msgid "Decimal number" msgid "Decimal number"
msgstr "" msgstr ""
#: db/models/fields/__init__.py:890 #: db/models/fields/__init__.py:908
msgid "E-mail address" msgid "Email address"
msgstr "" msgstr ""
#: db/models/fields/__init__.py:906 #: db/models/fields/__init__.py:927
msgid "File path" msgid "File path"
msgstr "" msgstr ""
#: db/models/fields/__init__.py:930 #: db/models/fields/__init__.py:954
#, python-format #, python-format
msgid "'%s' value must be a float." msgid "'%s' value must be a float."
msgstr "" msgstr ""
#: db/models/fields/__init__.py:932 #: db/models/fields/__init__.py:956
msgid "Floating point number" msgid "Floating point number"
msgstr "" msgstr ""
#: db/models/fields/__init__.py:993 #: db/models/fields/__init__.py:1017
msgid "Big (8 byte) integer" msgid "Big (8 byte) integer"
msgstr "" msgstr ""
#: db/models/fields/__init__.py:1007 #: db/models/fields/__init__.py:1031
msgid "IPv4 address" msgid "IPv4 address"
msgstr "" msgstr ""
#: db/models/fields/__init__.py:1023 #: db/models/fields/__init__.py:1047
msgid "IP address" msgid "IP address"
msgstr "" msgstr ""
#: db/models/fields/__init__.py:1065 #: db/models/fields/__init__.py:1090
#, python-format #, python-format
msgid "'%s' value must be either None, True or False." msgid "'%s' value must be either None, True or False."
msgstr "" msgstr ""
#: db/models/fields/__init__.py:1067 #: db/models/fields/__init__.py:1092
msgid "Boolean (Either True, False or None)" msgid "Boolean (Either True, False or None)"
msgstr "" msgstr ""
#: db/models/fields/__init__.py:1116 #: db/models/fields/__init__.py:1141
msgid "Positive integer" msgid "Positive integer"
msgstr "" msgstr ""
#: db/models/fields/__init__.py:1127 #: db/models/fields/__init__.py:1152
msgid "Positive small integer" msgid "Positive small integer"
msgstr "" msgstr ""
#: db/models/fields/__init__.py:1138 #: db/models/fields/__init__.py:1163
#, python-format #, python-format
msgid "Slug (up to %(max_length)s)" msgid "Slug (up to %(max_length)s)"
msgstr "" msgstr ""
#: db/models/fields/__init__.py:1156 #: db/models/fields/__init__.py:1181
msgid "Small integer" msgid "Small integer"
msgstr "" msgstr ""
#: db/models/fields/__init__.py:1162 #: db/models/fields/__init__.py:1187
msgid "Text" msgid "Text"
msgstr "" msgstr ""
#: db/models/fields/__init__.py:1180 #: db/models/fields/__init__.py:1205
#, python-format #, python-format
msgid "" msgid ""
"'%s' value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] format." "'%s' value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] format."
msgstr "" msgstr ""
#: db/models/fields/__init__.py:1182 #: db/models/fields/__init__.py:1207
#, python-format #, python-format
msgid "" msgid ""
"'%s' value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an invalid " "'%s' value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an invalid "
"time." "time."
msgstr "" msgstr ""
#: db/models/fields/__init__.py:1185 #: db/models/fields/__init__.py:1210
msgid "Time" msgid "Time"
msgstr "" msgstr ""
#: db/models/fields/__init__.py:1249 #: db/models/fields/__init__.py:1272
msgid "URL" msgid "URL"
msgstr "" 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" msgid "File"
msgstr "" msgstr ""
#: db/models/fields/files.py:321 #: db/models/fields/files.py:347
msgid "Image" msgid "Image"
msgstr "" msgstr ""
#: db/models/fields/related.py:903 #: db/models/fields/related.py:950
#, python-format #, python-format
msgid "Model %(model)s with pk %(pk)r does not exist." msgid "Model %(model)s with pk %(pk)r does not exist."
msgstr "" msgstr ""
#: db/models/fields/related.py:905 #: db/models/fields/related.py:952
msgid "Foreign Key (type determined by related field)" msgid "Foreign Key (type determined by related field)"
msgstr "" msgstr ""
#: db/models/fields/related.py:1033 #: db/models/fields/related.py:1082
msgid "One-to-one relationship" msgid "One-to-one relationship"
msgstr "" msgstr ""
#: db/models/fields/related.py:1096 #: db/models/fields/related.py:1149
msgid "Many-to-many relationship" msgid "Many-to-many relationship"
msgstr "" msgstr ""
#: db/models/fields/related.py:1120 #: db/models/fields/related.py:1174
msgid "" msgid ""
"Hold down \"Control\", or \"Command\" on a Mac, to select more than one." "Hold down \"Control\", or \"Command\" on a Mac, to select more than one."
msgstr "" msgstr ""
#: forms/fields.py:50 #: forms/fields.py:51
msgid "This field is required." msgid "This field is required."
msgstr "" msgstr ""
#: forms/fields.py:208 #: forms/fields.py:209
msgid "Enter a whole number." msgid "Enter a whole number."
msgstr "" msgstr ""
#: forms/fields.py:240 forms/fields.py:261 #: forms/fields.py:241 forms/fields.py:262
msgid "Enter a number." msgid "Enter a number."
msgstr "" msgstr ""
#: forms/fields.py:264
#, python-format
msgid "Ensure that there are no more than %s digits in total."
msgstr ""
#: forms/fields.py:265 #: forms/fields.py:265
#, python-format #, 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 "" msgstr ""
#: forms/fields.py:266 #: forms/fields.py:266
#, python-format #, 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." msgid "Ensure that there are no more than %s digits before the decimal point."
msgstr "" msgstr ""
#: forms/fields.py:365 forms/fields.py:963 #: forms/fields.py:355 forms/fields.py:953
msgid "Enter a valid date." msgid "Enter a valid date."
msgstr "" msgstr ""
#: forms/fields.py:388 forms/fields.py:964 #: forms/fields.py:378 forms/fields.py:954
msgid "Enter a valid time." msgid "Enter a valid time."
msgstr "" msgstr ""
#: forms/fields.py:409 #: forms/fields.py:399
msgid "Enter a valid date/time." msgid "Enter a valid date/time."
msgstr "" msgstr ""
#: forms/fields.py:485 #: forms/fields.py:475
msgid "No file was submitted. Check the encoding type on the form." msgid "No file was submitted. Check the encoding type on the form."
msgstr "" msgstr ""
#: forms/fields.py:486 #: forms/fields.py:476
msgid "No file was submitted." msgid "No file was submitted."
msgstr "" msgstr ""
#: forms/fields.py:487 #: forms/fields.py:477
msgid "The submitted file is empty." msgid "The submitted file is empty."
msgstr "" msgstr ""
#: forms/fields.py:488 #: forms/fields.py:478
#, python-format #, python-format
msgid "" msgid ""
"Ensure this filename has at most %(max)d characters (it has %(length)d)." "Ensure this filename has at most %(max)d characters (it has %(length)d)."
msgstr "" msgstr ""
#: forms/fields.py:489 #: forms/fields.py:479
msgid "Please either submit a file or check the clear checkbox, not both." msgid "Please either submit a file or check the clear checkbox, not both."
msgstr "" msgstr ""
#: forms/fields.py:544 #: forms/fields.py:534
msgid "" msgid ""
"Upload a valid image. The file you uploaded was either not an image or a " "Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image." "corrupted image."
msgstr "" 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 #, python-format
msgid "Select a valid choice. %(value)s is not one of the available choices." msgid "Select a valid choice. %(value)s is not one of the available choices."
msgstr "" 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." msgid "Enter a list of values."
msgstr "" msgstr ""
#: forms/formsets.py:317 forms/formsets.py:319 #: forms/formsets.py:323 forms/formsets.py:325
msgid "Order" msgid "Order"
msgstr "" msgstr ""
#: forms/formsets.py:321 #: forms/formsets.py:327
msgid "Delete" msgid "Delete"
msgstr "" msgstr ""
#: forms/models.py:571 #: forms/models.py:567
#, python-format #, python-format
msgid "Please correct the duplicate data for %(field)s." msgid "Please correct the duplicate data for %(field)s."
msgstr "" msgstr ""
#: forms/models.py:575 #: forms/models.py:571
#, python-format #, python-format
msgid "Please correct the duplicate data for %(field)s, which must be unique." msgid "Please correct the duplicate data for %(field)s, which must be unique."
msgstr "" msgstr ""
#: forms/models.py:581 #: forms/models.py:577
#, python-format #, python-format
msgid "" msgid ""
"Please correct the duplicate data for %(field_name)s which must be unique " "Please correct the duplicate data for %(field_name)s which must be unique "
"for the %(lookup)s in %(date_field)s." "for the %(lookup)s in %(date_field)s."
msgstr "" msgstr ""
#: forms/models.py:589 #: forms/models.py:585
msgid "Please correct the duplicate values below." msgid "Please correct the duplicate values below."
msgstr "" msgstr ""
@ -697,94 +698,94 @@ msgstr ""
msgid "\"%s\" is not a valid value for a primary key." msgid "\"%s\" is not a valid value for a primary key."
msgstr "" msgstr ""
#: forms/util.py:70 #: forms/util.py:81
#, python-format #, python-format
msgid "" msgid ""
"%(datetime)s couldn't be interpreted in time zone %(current_timezone)s; it " "%(datetime)s couldn't be interpreted in time zone %(current_timezone)s; it "
"may be ambiguous or it may not exist." "may be ambiguous or it may not exist."
msgstr "" msgstr ""
#: forms/widgets.py:325 #: forms/widgets.py:336
msgid "Currently" msgid "Currently"
msgstr "" msgstr ""
#: forms/widgets.py:326 #: forms/widgets.py:337
msgid "Change" msgid "Change"
msgstr "" msgstr ""
#: forms/widgets.py:327 #: forms/widgets.py:338
msgid "Clear" msgid "Clear"
msgstr "" msgstr ""
#: forms/widgets.py:582 #: forms/widgets.py:591
msgid "Unknown" msgid "Unknown"
msgstr "" msgstr ""
#: forms/widgets.py:583 #: forms/widgets.py:592
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""
#: forms/widgets.py:584 #: forms/widgets.py:593
msgid "No" msgid "No"
msgstr "" msgstr ""
#: template/defaultfilters.py:797 #: template/defaultfilters.py:794
msgid "yes,no,maybe" msgid "yes,no,maybe"
msgstr "" msgstr ""
#: template/defaultfilters.py:825 template/defaultfilters.py:830 #: template/defaultfilters.py:822 template/defaultfilters.py:833
#, python-format #, python-format
msgid "%(size)d byte" msgid "%(size)d byte"
msgid_plural "%(size)d bytes" msgid_plural "%(size)d bytes"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: template/defaultfilters.py:832 #: template/defaultfilters.py:835
#, python-format #, python-format
msgid "%s KB" msgid "%s KB"
msgstr "" msgstr ""
#: template/defaultfilters.py:834 #: template/defaultfilters.py:837
#, python-format #, python-format
msgid "%s MB" msgid "%s MB"
msgstr "" msgstr ""
#: template/defaultfilters.py:836 #: template/defaultfilters.py:839
#, python-format #, python-format
msgid "%s GB" msgid "%s GB"
msgstr "" msgstr ""
#: template/defaultfilters.py:838 #: template/defaultfilters.py:841
#, python-format #, python-format
msgid "%s TB" msgid "%s TB"
msgstr "" msgstr ""
#: template/defaultfilters.py:839 #: template/defaultfilters.py:842
#, python-format #, python-format
msgid "%s PB" msgid "%s PB"
msgstr "" msgstr ""
#: utils/dateformat.py:45 #: utils/dateformat.py:47
msgid "p.m." msgid "p.m."
msgstr "" msgstr ""
#: utils/dateformat.py:46 #: utils/dateformat.py:48
msgid "a.m." msgid "a.m."
msgstr "" msgstr ""
#: utils/dateformat.py:51 #: utils/dateformat.py:53
msgid "PM" msgid "PM"
msgstr "" msgstr ""
#: utils/dateformat.py:52 #: utils/dateformat.py:54
msgid "AM" msgid "AM"
msgstr "" msgstr ""
#: utils/dateformat.py:101 #: utils/dateformat.py:103
msgid "midnight" msgid "midnight"
msgstr "" msgstr ""
#: utils/dateformat.py:103 #: utils/dateformat.py:105
msgid "noon" msgid "noon"
msgstr "" msgstr ""
@ -1060,148 +1061,133 @@ msgctxt "alt. month"
msgid "December" msgid "December"
msgstr "" msgstr ""
#: utils/text.py:65 #: utils/text.py:70
#, python-format #, python-format
msgctxt "String to return when truncating text" msgctxt "String to return when truncating text"
msgid "%(truncated_text)s..." msgid "%(truncated_text)s..."
msgstr "" msgstr ""
#: utils/text.py:234 #: utils/text.py:239
msgid "or" msgid "or"
msgstr "" msgstr ""
#. Translators: This string is used as a separator between list elements #. Translators: This string is used as a separator between list elements
#: utils/text.py:251 #: utils/text.py:256
msgid ", " msgid ", "
msgstr "" msgstr ""
#: utils/timesince.py:20 #: utils/timesince.py:22
msgid "year" msgid "year"
msgid_plural "years" msgid_plural "years"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: utils/timesince.py:21 #: utils/timesince.py:23
msgid "month" msgid "month"
msgid_plural "months" msgid_plural "months"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: utils/timesince.py:22 #: utils/timesince.py:24
msgid "week" msgid "week"
msgid_plural "weeks" msgid_plural "weeks"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: utils/timesince.py:23 #: utils/timesince.py:25
msgid "day" msgid "day"
msgid_plural "days" msgid_plural "days"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: utils/timesince.py:24 #: utils/timesince.py:26
msgid "hour" msgid "hour"
msgid_plural "hours" msgid_plural "hours"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: utils/timesince.py:25 #: utils/timesince.py:27
msgid "minute" msgid "minute"
msgid_plural "minutes" msgid_plural "minutes"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: utils/timesince.py:41 #: utils/timesince.py:43
msgid "minutes" msgid "minutes"
msgstr "" msgstr ""
#: utils/timesince.py:46 #: utils/timesince.py:48
#, python-format #, python-format
msgid "%(number)d %(type)s" msgid "%(number)d %(type)s"
msgstr "" msgstr ""
#: utils/timesince.py:52 #: utils/timesince.py:54
#, python-format #, python-format
msgid ", %(number)d %(type)s" msgid ", %(number)d %(type)s"
msgstr "" msgstr ""
#: views/static.py:52 #: views/static.py:55
msgid "Directory indexes are not allowed here." msgid "Directory indexes are not allowed here."
msgstr "" msgstr ""
#: views/static.py:54 #: views/static.py:57
#, python-format #, python-format
msgid "\"%(path)s\" does not exist" msgid "\"%(path)s\" does not exist"
msgstr "" msgstr ""
#: views/static.py:95 #: views/static.py:98
#, python-format #, python-format
msgid "Index of %(directory)s" msgid "Index of %(directory)s"
msgstr "" msgstr ""
#: views/generic/create_update.py:121 #: views/generic/dates.py:42
#, 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
msgid "No year specified" msgid "No year specified"
msgstr "" msgstr ""
#: views/generic/dates.py:58 #: views/generic/dates.py:98
msgid "No month specified" msgid "No month specified"
msgstr "" msgstr ""
#: views/generic/dates.py:99 #: views/generic/dates.py:157
msgid "No day specified" msgid "No day specified"
msgstr "" msgstr ""
#: views/generic/dates.py:138 #: views/generic/dates.py:213
msgid "No week specified" msgid "No week specified"
msgstr "" msgstr ""
#: views/generic/dates.py:198 views/generic/dates.py:215 #: views/generic/dates.py:368 views/generic/dates.py:393
#, python-format #, python-format
msgid "No %(verbose_name_plural)s available" msgid "No %(verbose_name_plural)s available"
msgstr "" msgstr ""
#: views/generic/dates.py:467 #: views/generic/dates.py:646
#, python-format #, python-format
msgid "" msgid ""
"Future %(verbose_name_plural)s not available because %(class_name)s." "Future %(verbose_name_plural)s not available because %(class_name)s."
"allow_future is False." "allow_future is False."
msgstr "" msgstr ""
#: views/generic/dates.py:501 #: views/generic/dates.py:678
#, python-format #, python-format
msgid "Invalid date string '%(datestr)s' given format '%(format)s'" msgid "Invalid date string '%(datestr)s' given format '%(format)s'"
msgstr "" msgstr ""
#: views/generic/detail.py:51 #: views/generic/detail.py:54
#, python-format #, python-format
msgid "No %(verbose_name)s found matching the query" msgid "No %(verbose_name)s found matching the query"
msgstr "" msgstr ""
#: views/generic/list.py:45 #: views/generic/list.py:49
msgid "Page is not 'last', nor can it be converted to an int." msgid "Page is not 'last', nor can it be converted to an int."
msgstr "" msgstr ""
#: views/generic/list.py:50 #: views/generic/list.py:54
#, python-format #, python-format
msgid "Invalid page (%(page_number)s)" msgid "Invalid page (%(page_number)s)"
msgstr "" msgstr ""
#: views/generic/list.py:117 #: views/generic/list.py:134
#, python-format #, python-format
msgid "Empty list and '%(class_name)s.allow_empty' is False." msgid "Empty list and '%(class_name)s.allow_empty' is False."
msgstr "" msgstr ""

View File

@ -15,6 +15,10 @@ framework.
""" """
import os 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") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings")
# This application object is used by any WSGI server configured to use this # This application object is used by any WSGI server configured to use this

View File

@ -4,12 +4,12 @@ from django import forms
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import User from django.utils.translation import ugettext_lazy
from django.utils.translation import ugettext_lazy, ugettext as _
ERROR_MESSAGE = ugettext_lazy("Please enter the correct username and password " ERROR_MESSAGE = ugettext_lazy("Please enter the correct username and password "
"for a staff account. Note that both fields are case-sensitive.") "for a staff account. Note that both fields are case-sensitive.")
class AdminAuthenticationForm(AuthenticationForm): class AdminAuthenticationForm(AuthenticationForm):
""" """
A custom authentication form used in the admin app. A custom authentication form used in the admin app.
@ -26,17 +26,6 @@ class AdminAuthenticationForm(AuthenticationForm):
if username and password: if username and password:
self.user_cache = authenticate(username=username, password=password) self.user_cache = authenticate(username=username, password=password)
if self.user_cache is None: 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) raise forms.ValidationError(message)
elif not self.user_cache.is_active or not self.user_cache.is_staff: elif not self.user_cache.is_active or not self.user_cache.is_staff:
raise forms.ValidationError(message) raise forms.ValidationError(message)

View File

@ -4,7 +4,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Django\n" "Project-Id-Version: Django\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2010-05-13 15:35+0200\n"
"Last-Translator: Django team\n" "Last-Translator: Django team\n"
"Language-Team: English <en@li.org>\n" "Language-Team: English <en@li.org>\n"
@ -32,39 +32,39 @@ msgstr ""
msgid "Delete selected %(verbose_name_plural)s" msgid "Delete selected %(verbose_name_plural)s"
msgstr "" 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" msgid "All"
msgstr "" msgstr ""
#: filters.py:232 #: filters.py:238
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""
#: filters.py:233 #: filters.py:239
msgid "No" msgid "No"
msgstr "" msgstr ""
#: filters.py:247 #: filters.py:253
msgid "Unknown" msgid "Unknown"
msgstr "" msgstr ""
#: filters.py:306 #: filters.py:308
msgid "Any date" msgid "Any date"
msgstr "" msgstr ""
#: filters.py:307 #: filters.py:309
msgid "Today" msgid "Today"
msgstr "" msgstr ""
#: filters.py:311 #: filters.py:313
msgid "Past 7 days" msgid "Past 7 days"
msgstr "" msgstr ""
#: filters.py:315 #: filters.py:317
msgid "This month" msgid "This month"
msgstr "" msgstr ""
#: filters.py:319 #: filters.py:321
msgid "This year" msgid "This year"
msgstr "" msgstr ""
@ -74,134 +74,129 @@ msgid ""
"that both fields are case-sensitive." "that both fields are case-sensitive."
msgstr "" msgstr ""
#: forms.py:18 #: forms.py:19
msgid "Please log in again, because your session has expired." msgid "Please log in again, because your session has expired."
msgstr "" msgstr ""
#: forms.py:37 #: helpers.py:23
#, python-format
msgid "Your e-mail address is not your username. Try '%s' instead."
msgstr ""
#: helpers.py:20
msgid "Action:" msgid "Action:"
msgstr "" msgstr ""
#: models.py:19 #: models.py:24
msgid "action time" msgid "action time"
msgstr "" msgstr ""
#: models.py:22 #: models.py:27
msgid "object id" msgid "object id"
msgstr "" msgstr ""
#: models.py:23 #: models.py:28
msgid "object repr" msgid "object repr"
msgstr "" msgstr ""
#: models.py:24 #: models.py:29
msgid "action flag" msgid "action flag"
msgstr "" msgstr ""
#: models.py:25 #: models.py:30
msgid "change message" msgid "change message"
msgstr "" msgstr ""
#: models.py:30 #: models.py:35
msgid "log entry" msgid "log entry"
msgstr "" msgstr ""
#: models.py:31 #: models.py:36
msgid "log entries" msgid "log entries"
msgstr "" msgstr ""
#: models.py:40 #: models.py:45
#, python-format #, python-format
msgid "Added \"%(object)s\"." msgid "Added \"%(object)s\"."
msgstr "" msgstr ""
#: models.py:42 #: models.py:47
#, python-format #, python-format
msgid "Changed \"%(object)s\" - %(changes)s" msgid "Changed \"%(object)s\" - %(changes)s"
msgstr "" msgstr ""
#: models.py:44 #: models.py:49
#, python-format #, python-format
msgid "Deleted \"%(object)s.\"" msgid "Deleted \"%(object)s.\""
msgstr "" msgstr ""
#: models.py:46 #: models.py:51
msgid "LogEntry Object" msgid "LogEntry Object"
msgstr "" msgstr ""
#: options.py:150 options.py:166 #: options.py:151 options.py:167
msgid "None" msgid "None"
msgstr "" msgstr ""
#: options.py:671 #: options.py:672
#, python-format #, python-format
msgid "Changed %s." msgid "Changed %s."
msgstr "" msgstr ""
#: options.py:671 options.py:681 #: options.py:672 options.py:682
msgid "and" msgid "and"
msgstr "" msgstr ""
#: options.py:676 #: options.py:677
#, python-format #, python-format
msgid "Added %(name)s \"%(object)s\"." msgid "Added %(name)s \"%(object)s\"."
msgstr "" msgstr ""
#: options.py:680 #: options.py:681
#, python-format #, python-format
msgid "Changed %(list)s for %(name)s \"%(object)s\"." msgid "Changed %(list)s for %(name)s \"%(object)s\"."
msgstr "" msgstr ""
#: options.py:685 #: options.py:686
#, python-format #, python-format
msgid "Deleted %(name)s \"%(object)s\"." msgid "Deleted %(name)s \"%(object)s\"."
msgstr "" msgstr ""
#: options.py:689 #: options.py:690
msgid "No fields changed." msgid "No fields changed."
msgstr "" msgstr ""
#: options.py:772 #: options.py:773
#, python-format #, python-format
msgid "The %(name)s \"%(obj)s\" was added successfully." msgid "The %(name)s \"%(obj)s\" was added successfully."
msgstr "" msgstr ""
#: options.py:776 options.py:824 #: options.py:777 options.py:825
msgid "You may edit it again below." msgid "You may edit it again below."
msgstr "" msgstr ""
#: options.py:788 options.py:837 #: options.py:789 options.py:838
#, python-format #, python-format
msgid "You may add another %s below." msgid "You may add another %s below."
msgstr "" msgstr ""
#: options.py:822 #: options.py:823
#, python-format #, python-format
msgid "The %(name)s \"%(obj)s\" was changed successfully." msgid "The %(name)s \"%(obj)s\" was changed successfully."
msgstr "" msgstr ""
#: options.py:830 #: options.py:831
#, python-format #, python-format
msgid "" msgid ""
"The %(name)s \"%(obj)s\" was added successfully. You may edit it again below." "The %(name)s \"%(obj)s\" was added successfully. You may edit it again below."
msgstr "" msgstr ""
#: options.py:899 options.py:1159 #: options.py:900 options.py:1159
msgid "" msgid ""
"Items must be selected in order to perform actions on them. No items have " "Items must be selected in order to perform actions on them. No items have "
"been changed." "been changed."
msgstr "" msgstr ""
#: options.py:918 #: options.py:919
msgid "No action selected." msgid "No action selected."
msgstr "" msgstr ""
#: options.py:998 #: options.py:999
#, python-format #, python-format
msgid "Add %s" msgid "Add %s"
msgstr "" msgstr ""
@ -249,34 +244,34 @@ msgstr ""
msgid "Change history: %s" msgid "Change history: %s"
msgstr "" msgstr ""
#: sites.py:315 tests.py:61 templates/admin/login.html:49 #: sites.py:322 tests.py:57 templates/admin/login.html:48
#: templates/registration/password_reset_complete.html:20 #: templates/registration/password_reset_complete.html:19
#: views/decorators.py:23 #: views/decorators.py:24
msgid "Log in" msgid "Log in"
msgstr "" msgstr ""
#: sites.py:380 #: sites.py:388
msgid "Site administration" msgid "Site administration"
msgstr "" msgstr ""
#: sites.py:432 #: sites.py:440
#, python-format #, python-format
msgid "%s administration" msgid "%s administration"
msgstr "" msgstr ""
#: widgets.py:87 #: widgets.py:90
msgid "Date:" msgid "Date:"
msgstr "" msgstr ""
#: widgets.py:87 #: widgets.py:91
msgid "Time:" msgid "Time:"
msgstr "" msgstr ""
#: widgets.py:161 #: widgets.py:165
msgid "Lookup" msgid "Lookup"
msgstr "" msgstr ""
#: widgets.py:267 #: widgets.py:271
msgid "Add Another" msgid "Add Another"
msgstr "" msgstr ""
@ -288,39 +283,39 @@ msgstr ""
msgid "We're sorry, but the requested page could not be found." msgid "We're sorry, but the requested page could not be found."
msgstr "" msgstr ""
#: templates/admin/500.html:7 templates/admin/app_index.html:8 #: templates/admin/500.html:6 templates/admin/app_index.html:7
#: templates/admin/base.html:45 templates/admin/change_form.html:21 #: templates/admin/base.html:47 templates/admin/change_form.html:19
#: templates/admin/change_list.html:43 #: templates/admin/change_list.html:41
#: templates/admin/delete_confirmation.html:8 #: templates/admin/delete_confirmation.html:7
#: templates/admin/delete_selected_confirmation.html:8 #: templates/admin/delete_selected_confirmation.html:7
#: templates/admin/invalid_setup.html:7 templates/admin/object_history.html:8 #: templates/admin/invalid_setup.html:6 templates/admin/object_history.html:7
#: templates/admin/auth/user/change_password.html:15 #: templates/admin/auth/user/change_password.html:13
#: templates/registration/logged_out.html:5 #: templates/registration/logged_out.html:4
#: templates/registration/password_change_done.html:7 #: templates/registration/password_change_done.html:6
#: templates/registration/password_change_form.html:8 #: templates/registration/password_change_form.html:7
#: templates/registration/password_reset_complete.html:7 #: templates/registration/password_reset_complete.html:6
#: templates/registration/password_reset_confirm.html:7 #: templates/registration/password_reset_confirm.html:6
#: templates/registration/password_reset_done.html:7 #: templates/registration/password_reset_done.html:6
#: templates/registration/password_reset_form.html:7 #: templates/registration/password_reset_form.html:6
msgid "Home" msgid "Home"
msgstr "" msgstr ""
#: templates/admin/500.html:8 #: templates/admin/500.html:7
msgid "Server error" msgid "Server error"
msgstr "" msgstr ""
#: templates/admin/500.html:12 #: templates/admin/500.html:11
msgid "Server error (500)" msgid "Server error (500)"
msgstr "" msgstr ""
#: templates/admin/500.html:15 #: templates/admin/500.html:14
msgid "Server Error <em>(500)</em>" msgid "Server Error <em>(500)</em>"
msgstr "" msgstr ""
#: templates/admin/500.html:16 #: templates/admin/500.html:15
msgid "" msgid ""
"There's been an error. It's been reported to the site administrators via e-" "There's been an error. It's been reported to the site administrators via "
"mail and should be fixed shortly. Thanks for your patience." "email and should be fixed shortly. Thanks for your patience."
msgstr "" msgstr ""
#: templates/admin/actions.html:4 #: templates/admin/actions.html:4
@ -344,7 +339,7 @@ msgstr ""
msgid "Clear selection" msgid "Clear selection"
msgstr "" 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 #, python-format
msgid "%(name)s" msgid "%(name)s"
msgstr "" msgstr ""
@ -354,22 +349,22 @@ msgid "Welcome,"
msgstr "" msgstr ""
#: templates/admin/base.html:33 #: templates/admin/base.html:33
#: templates/registration/password_change_done.html:4 #: templates/registration/password_change_done.html:3
#: templates/registration/password_change_form.html:5 #: templates/registration/password_change_form.html:4
msgid "Documentation" msgid "Documentation"
msgstr "" msgstr ""
#: templates/admin/base.html:35 #: templates/admin/base.html:36
#: templates/admin/auth/user/change_password.html:19 #: templates/admin/auth/user/change_password.html:17
#: templates/admin/auth/user/change_password.html:53 #: templates/admin/auth/user/change_password.html:51
#: templates/registration/password_change_done.html:4 #: templates/registration/password_change_done.html:3
#: templates/registration/password_change_form.html:5 #: templates/registration/password_change_form.html:4
msgid "Change password" msgid "Change password"
msgstr "" msgstr ""
#: templates/admin/base.html:36 #: templates/admin/base.html:38
#: templates/registration/password_change_done.html:4 #: templates/registration/password_change_done.html:3
#: templates/registration/password_change_form.html:5 #: templates/registration/password_change_form.html:4
msgid "Log out" msgid "Log out"
msgstr "" msgstr ""
@ -381,35 +376,35 @@ msgstr ""
msgid "Django administration" msgid "Django administration"
msgstr "" 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" msgid "Add"
msgstr "" 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" msgid "History"
msgstr "" msgstr ""
#: templates/admin/change_form.html:35 #: templates/admin/change_form.html:33
#: templates/admin/edit_inline/stacked.html:9 #: templates/admin/edit_inline/stacked.html:9
#: templates/admin/edit_inline/tabular.html:30 #: templates/admin/edit_inline/tabular.html:30
msgid "View on site" msgid "View on site"
msgstr "" msgstr ""
#: templates/admin/change_form.html:46 templates/admin/change_list.html:69 #: templates/admin/change_form.html:44 templates/admin/change_list.html:67
#: templates/admin/login.html:18 #: templates/admin/login.html:17
#: templates/admin/auth/user/change_password.html:29 #: templates/admin/auth/user/change_password.html:27
#: templates/registration/password_change_form.html:21 #: templates/registration/password_change_form.html:20
msgid "Please correct the error below." msgid "Please correct the error below."
msgid_plural "Please correct the errors below." msgid_plural "Please correct the errors below."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: templates/admin/change_list.html:60 #: templates/admin/change_list.html:58
#, python-format #, python-format
msgid "Add %(name)s" msgid "Add %(name)s"
msgstr "" msgstr ""
#: templates/admin/change_list.html:80 #: templates/admin/change_list.html:78
msgid "Filter" msgid "Filter"
msgstr "" msgstr ""
@ -426,12 +421,12 @@ msgstr ""
msgid "Toggle sorting" msgid "Toggle sorting"
msgstr "" msgstr ""
#: templates/admin/delete_confirmation.html:12 #: templates/admin/delete_confirmation.html:11
#: templates/admin/submit_line.html:4 #: templates/admin/submit_line.html:4
msgid "Delete" msgid "Delete"
msgstr "" msgstr ""
#: templates/admin/delete_confirmation.html:19 #: templates/admin/delete_confirmation.html:18
#, python-format #, python-format
msgid "" msgid ""
"Deleting the %(object_name)s '%(escaped_object)s' would result in deleting " "Deleting the %(object_name)s '%(escaped_object)s' would result in deleting "
@ -439,30 +434,30 @@ msgid ""
"following types of objects:" "following types of objects:"
msgstr "" msgstr ""
#: templates/admin/delete_confirmation.html:27 #: templates/admin/delete_confirmation.html:26
#, python-format #, python-format
msgid "" msgid ""
"Deleting the %(object_name)s '%(escaped_object)s' would require deleting the " "Deleting the %(object_name)s '%(escaped_object)s' would require deleting the "
"following protected related objects:" "following protected related objects:"
msgstr "" msgstr ""
#: templates/admin/delete_confirmation.html:35 #: templates/admin/delete_confirmation.html:34
#, python-format #, python-format
msgid "" msgid ""
"Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"? " "Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"? "
"All of the following related items will be deleted:" "All of the following related items will be deleted:"
msgstr "" msgstr ""
#: templates/admin/delete_confirmation.html:40 #: templates/admin/delete_confirmation.html:39
#: templates/admin/delete_selected_confirmation.html:45 #: templates/admin/delete_selected_confirmation.html:44
msgid "Yes, I'm sure" msgid "Yes, I'm sure"
msgstr "" msgstr ""
#: templates/admin/delete_selected_confirmation.html:11 #: templates/admin/delete_selected_confirmation.html:10
msgid "Delete multiple objects" msgid "Delete multiple objects"
msgstr "" msgstr ""
#: templates/admin/delete_selected_confirmation.html:18 #: templates/admin/delete_selected_confirmation.html:17
#, python-format #, python-format
msgid "" msgid ""
"Deleting the selected %(objects_name)s would result in deleting related " "Deleting the selected %(objects_name)s would result in deleting related "
@ -470,14 +465,14 @@ msgid ""
"types of objects:" "types of objects:"
msgstr "" msgstr ""
#: templates/admin/delete_selected_confirmation.html:26 #: templates/admin/delete_selected_confirmation.html:25
#, python-format #, python-format
msgid "" msgid ""
"Deleting the selected %(objects_name)s would require deleting the following " "Deleting the selected %(objects_name)s would require deleting the following "
"protected related objects:" "protected related objects:"
msgstr "" msgstr ""
#: templates/admin/delete_selected_confirmation.html:34 #: templates/admin/delete_selected_confirmation.html:33
#, python-format #, python-format
msgid "" msgid ""
"Are you sure you want to delete the selected %(objects_name)s? All of the " "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 " msgid " By %(filter_title)s "
msgstr "" msgstr ""
#: templates/admin/index.html:18 #: templates/admin/index.html:20
#, python-format #, python-format
msgid "Models available in the %(name)s application." msgid "Models in the %(name)s application"
msgstr "" msgstr ""
#: templates/admin/index.html:35 #: templates/admin/index.html:39
msgid "Change" msgid "Change"
msgstr "" msgstr ""
#: templates/admin/index.html:45 #: templates/admin/index.html:49
msgid "You don't have permission to edit anything." msgid "You don't have permission to edit anything."
msgstr "" msgstr ""
#: templates/admin/index.html:53 #: templates/admin/index.html:57
msgid "Recent Actions" msgid "Recent Actions"
msgstr "" msgstr ""
#: templates/admin/index.html:54 #: templates/admin/index.html:58
msgid "My Actions" msgid "My Actions"
msgstr "" msgstr ""
#: templates/admin/index.html:58 #: templates/admin/index.html:62
msgid "None available" msgid "None available"
msgstr "" msgstr ""
#: templates/admin/index.html:72 #: templates/admin/index.html:76
msgid "Unknown content" msgid "Unknown content"
msgstr "" msgstr ""
#: templates/admin/invalid_setup.html:13 #: templates/admin/invalid_setup.html:12
msgid "" msgid ""
"Something's wrong with your database installation. Make sure the appropriate " "Something's wrong with your database installation. Make sure the appropriate "
"database tables have been created, and make sure the database is readable by " "database tables have been created, and make sure the database is readable by "
"the appropriate user." "the appropriate user."
msgstr "" msgstr ""
#: templates/admin/login.html:34 #: templates/admin/login.html:37
msgid "Username:"
msgstr ""
#: templates/admin/login.html:38
msgid "Password:" msgid "Password:"
msgstr "" msgstr ""
#: templates/admin/login.html:45 #: templates/admin/login.html:44
msgid "Forgotten your password or username?" msgid "Forgotten your password or username?"
msgstr "" msgstr ""
#: templates/admin/object_history.html:24 #: templates/admin/object_history.html:23
msgid "Date/time" msgid "Date/time"
msgstr "" msgstr ""
#: templates/admin/object_history.html:25 #: templates/admin/object_history.html:24
msgid "User" msgid "User"
msgstr "" msgstr ""
#: templates/admin/object_history.html:26 #: templates/admin/object_history.html:25
msgid "Action" msgid "Action"
msgstr "" msgstr ""
#: templates/admin/object_history.html:40 #: templates/admin/object_history.html:39
msgid "" msgid ""
"This object doesn't have a change history. It probably wasn't added via this " "This object doesn't have a change history. It probably wasn't added via this "
"admin site." "admin site."
@ -601,147 +592,147 @@ msgstr ""
msgid "Enter a username and password." msgid "Enter a username and password."
msgstr "" msgstr ""
#: templates/admin/auth/user/change_password.html:33 #: templates/admin/auth/user/change_password.html:31
#, python-format #, python-format
msgid "Enter a new password for the user <strong>%(username)s</strong>." msgid "Enter a new password for the user <strong>%(username)s</strong>."
msgstr "" msgstr ""
#: templates/admin/auth/user/change_password.html:40 #: templates/admin/auth/user/change_password.html:38
msgid "Password" msgid "Password"
msgstr "" msgstr ""
#: templates/admin/auth/user/change_password.html:46 #: templates/admin/auth/user/change_password.html:44
#: templates/registration/password_change_form.html:43 #: templates/registration/password_change_form.html:42
msgid "Password (again)" msgid "Password (again)"
msgstr "" 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." msgid "Enter the same password as above, for verification."
msgstr "" msgstr ""
#: templates/admin/edit_inline/stacked.html:67 #: templates/admin/edit_inline/stacked.html:26
#: templates/admin/edit_inline/tabular.html:115 #: templates/admin/edit_inline/tabular.html:76
#, python-format msgid "Remove"
msgid "Add another %(verbose_name)s"
msgstr "" msgstr ""
#: templates/admin/edit_inline/stacked.html:70 #: templates/admin/edit_inline/stacked.html:27
#: templates/admin/edit_inline/tabular.html:118 #: templates/admin/edit_inline/tabular.html:75
msgid "Remove" #, python-format
msgid "Add another %(verbose_name)s"
msgstr "" msgstr ""
#: templates/admin/edit_inline/tabular.html:17 #: templates/admin/edit_inline/tabular.html:17
msgid "Delete?" msgid "Delete?"
msgstr "" 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." msgid "Thanks for spending some quality time with the Web site today."
msgstr "" msgstr ""
#: templates/registration/logged_out.html:11 #: templates/registration/logged_out.html:10
msgid "Log in again" msgid "Log in again"
msgstr "" msgstr ""
#: templates/registration/password_change_done.html:8 #: templates/registration/password_change_done.html:7
#: templates/registration/password_change_form.html:9 #: templates/registration/password_change_form.html:8
#: templates/registration/password_change_form.html:13 #: templates/registration/password_change_form.html:12
#: templates/registration/password_change_form.html:25 #: templates/registration/password_change_form.html:24
msgid "Password change" msgid "Password change"
msgstr "" msgstr ""
#: templates/registration/password_change_done.html:12 #: templates/registration/password_change_done.html:11
#: templates/registration/password_change_done.html:16 #: templates/registration/password_change_done.html:15
msgid "Password change successful" msgid "Password change successful"
msgstr "" msgstr ""
#: templates/registration/password_change_done.html:18 #: templates/registration/password_change_done.html:17
msgid "Your password was changed." msgid "Your password was changed."
msgstr "" msgstr ""
#: templates/registration/password_change_form.html:27 #: templates/registration/password_change_form.html:26
msgid "" msgid ""
"Please enter your old password, for security's sake, and then enter your new " "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." "password twice so we can verify you typed it in correctly."
msgstr "" msgstr ""
#: templates/registration/password_change_form.html:33 #: templates/registration/password_change_form.html:32
msgid "Old password" msgid "Old password"
msgstr "" msgstr ""
#: templates/registration/password_change_form.html:38 #: templates/registration/password_change_form.html:37
msgid "New password" msgid "New password"
msgstr "" msgstr ""
#: templates/registration/password_change_form.html:49 #: templates/registration/password_change_form.html:48
#: templates/registration/password_reset_confirm.html:27 #: templates/registration/password_reset_confirm.html:26
msgid "Change my password" msgid "Change my password"
msgstr "" msgstr ""
#: templates/registration/password_reset_complete.html:8 #: templates/registration/password_reset_complete.html:7
#: templates/registration/password_reset_confirm.html:12 #: templates/registration/password_reset_confirm.html:11
#: templates/registration/password_reset_done.html:8 #: templates/registration/password_reset_done.html:7
#: templates/registration/password_reset_form.html:8 #: templates/registration/password_reset_form.html:7
#: templates/registration/password_reset_form.html:12 #: templates/registration/password_reset_form.html:11
#: templates/registration/password_reset_form.html:16 #: templates/registration/password_reset_form.html:15
msgid "Password reset" msgid "Password reset"
msgstr "" msgstr ""
#: templates/registration/password_reset_complete.html:12 #: templates/registration/password_reset_complete.html:11
#: templates/registration/password_reset_complete.html:16 #: templates/registration/password_reset_complete.html:15
msgid "Password reset complete" msgid "Password reset complete"
msgstr "" 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." msgid "Your password has been set. You may go ahead and log in now."
msgstr "" msgstr ""
#: templates/registration/password_reset_confirm.html:8 #: templates/registration/password_reset_confirm.html:7
msgid "Password reset confirmation" msgid "Password reset confirmation"
msgstr "" msgstr ""
#: templates/registration/password_reset_confirm.html:18 #: templates/registration/password_reset_confirm.html:17
msgid "Enter new password" msgid "Enter new password"
msgstr "" msgstr ""
#: templates/registration/password_reset_confirm.html:20 #: templates/registration/password_reset_confirm.html:19
msgid "" msgid ""
"Please enter your new password twice so we can verify you typed it in " "Please enter your new password twice so we can verify you typed it in "
"correctly." "correctly."
msgstr "" msgstr ""
#: templates/registration/password_reset_confirm.html:24 #: templates/registration/password_reset_confirm.html:23
msgid "New password:" msgid "New password:"
msgstr "" msgstr ""
#: templates/registration/password_reset_confirm.html:26 #: templates/registration/password_reset_confirm.html:25
msgid "Confirm password:" msgid "Confirm password:"
msgstr "" msgstr ""
#: templates/registration/password_reset_confirm.html:32 #: templates/registration/password_reset_confirm.html:31
msgid "Password reset unsuccessful" msgid "Password reset unsuccessful"
msgstr "" msgstr ""
#: templates/registration/password_reset_confirm.html:34 #: templates/registration/password_reset_confirm.html:33
msgid "" msgid ""
"The password reset link was invalid, possibly because it has already been " "The password reset link was invalid, possibly because it has already been "
"used. Please request a new password reset." "used. Please request a new password reset."
msgstr "" msgstr ""
#: templates/registration/password_reset_done.html:12 #: templates/registration/password_reset_done.html:11
#: templates/registration/password_reset_done.html:16 #: templates/registration/password_reset_done.html:15
msgid "Password reset successful" msgid "Password reset successful"
msgstr "" msgstr ""
#: templates/registration/password_reset_done.html:18 #: templates/registration/password_reset_done.html:17
msgid "" 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." "address you submitted. You should be receiving it shortly."
msgstr "" msgstr ""
#: templates/registration/password_reset_email.html:2 #: templates/registration/password_reset_email.html:2
#, python-format #, python-format
msgid "" 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." "user account at %(site_name)s."
msgstr "" msgstr ""
@ -762,34 +753,34 @@ msgstr ""
msgid "The %(site_name)s team" msgid "The %(site_name)s team"
msgstr "" msgstr ""
#: templates/registration/password_reset_form.html:18 #: templates/registration/password_reset_form.html:17
msgid "" 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." "instructions for setting a new one."
msgstr "" msgstr ""
#: templates/registration/password_reset_form.html:22 #: templates/registration/password_reset_form.html:21
msgid "E-mail address:" msgid "Email address:"
msgstr "" msgstr ""
#: templates/registration/password_reset_form.html:22 #: templates/registration/password_reset_form.html:21
msgid "Reset my password" msgid "Reset my password"
msgstr "" msgstr ""
#: templatetags/admin_list.py:336 #: templatetags/admin_list.py:344
msgid "All dates" msgid "All dates"
msgstr "" msgstr ""
#: views/main.py:31 #: views/main.py:33
msgid "(None)" msgid "(None)"
msgstr "" msgstr ""
#: views/main.py:74
#, python-format
msgid "Select %s"
msgstr ""
#: views/main.py:76 #: views/main.py:76
#, python-format #, python-format
msgid "Select %s"
msgstr ""
#: views/main.py:78
#, python-format
msgid "Select %s to change" msgid "Select %s to change"
msgstr "" msgstr ""

View File

@ -1,8 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import models from django.db import models
from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User
from django.contrib.admin.util import quote from django.contrib.admin.util import quote
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_text from django.utils.encoding import smart_text
@ -12,15 +12,17 @@ ADDITION = 1
CHANGE = 2 CHANGE = 2
DELETION = 3 DELETION = 3
class LogEntryManager(models.Manager): class LogEntryManager(models.Manager):
def log_action(self, user_id, content_type_id, object_id, object_repr, action_flag, change_message=''): 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 = self.model(None, None, user_id, content_type_id, smart_text(object_id), object_repr[:200], action_flag, change_message)
e.save() e.save()
@python_2_unicode_compatible @python_2_unicode_compatible
class LogEntry(models.Model): class LogEntry(models.Model):
action_time = models.DateTimeField(_('action time'), auto_now=True) 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) content_type = models.ForeignKey(ContentType, blank=True, null=True)
object_id = models.TextField(_('object id'), blank=True, null=True) object_id = models.TextField(_('object id'), blank=True, null=True)
object_repr = models.CharField(_('object repr'), max_length=200) object_repr = models.CharField(_('object repr'), max_length=200)

View File

@ -1,4 +1,6 @@
from functools import update_wrapper, partial from functools import update_wrapper, partial
import warnings
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.forms.formsets import all_valid from django.forms.formsets import all_valid
@ -6,7 +8,7 @@ from django.forms.models import (modelform_factory, modelformset_factory,
inlineformset_factory, BaseInlineFormSet) inlineformset_factory, BaseInlineFormSet)
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.admin import widgets, helpers 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.admin.templatetags.admin_static import static
from django.contrib import messages from django.contrib import messages
from django.views.decorators.csrf import csrf_protect from django.views.decorators.csrf import csrf_protect
@ -344,14 +346,14 @@ class ModelAdmin(BaseModelAdmin):
self.admin_site = admin_site self.admin_site = admin_site
super(ModelAdmin, self).__init__() super(ModelAdmin, self).__init__()
def get_inline_instances(self, request): def get_inline_instances(self, request, obj=None):
inline_instances = [] inline_instances = []
for inline_class in self.inlines: for inline_class in self.inlines:
inline = inline_class(self.model, self.admin_site) inline = inline_class(self.model, self.admin_site)
if request: if request:
if not (inline.has_add_permission(request) or if not (inline.has_add_permission(request) or
inline.has_change_permission(request) or inline.has_change_permission(request, obj) or
inline.has_delete_permission(request)): inline.has_delete_permission(request, obj)):
continue continue
if not inline.has_add_permission(request): if not inline.has_add_permission(request):
inline.max_num = 0 inline.max_num = 0
@ -504,7 +506,7 @@ class ModelAdmin(BaseModelAdmin):
fields=self.list_editable, **defaults) fields=self.list_editable, **defaults)
def get_formsets(self, request, obj=None): 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) yield inline.get_formset(request, obj)
def get_paginator(self, request, queryset, per_page, orphans=0, allow_empty_first_page=True): 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" "admin/change_form.html"
], context, current_app=self.admin_site.name) ], 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. 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 # Here, we distinguish between different save types by checking for
# the presence of keys in request.POST. # the presence of keys in request.POST.
if "_continue" 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: if "_popup" in request.POST:
post_url_continue += "?_popup=1" url += "?_popup=1"
return HttpResponseRedirect(post_url_continue % pk_value) return HttpResponseRedirect(url)
if "_popup" in request.POST: if "_popup" in request.POST:
return HttpResponse( return HttpResponse(
@ -786,72 +816,104 @@ class ModelAdmin(BaseModelAdmin):
# escape() calls force_text. # escape() calls force_text.
(escape(pk_value), escapejs(obj))) (escape(pk_value), escapejs(obj)))
elif "_addanother" in request.POST: elif "_addanother" in request.POST:
self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_text(opts.verbose_name))) msg = _('The %(name)s "%(obj)s" was added successfully. You may add another %(name)s below.') % msg_dict
return HttpResponseRedirect(request.path) 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: else:
msg = _('The %(name)s "%(obj)s" was added successfully.') % msg_dict
self.message_user(request, msg) self.message_user(request, msg)
# Figure out where to redirect. If the user has change permission, # Figure out where to redirect. If the user has change permission,
# redirect to the change-list page for this object. Otherwise, # redirect to the change-list page for this object. Otherwise,
# redirect to the admin index. # redirect to the admin index.
if self.has_change_permission(request, None): if self.has_change_permission(request, None):
post_url = reverse('admin:%s_%s_changelist' % if hasperm_url is None:
(opts.app_label, opts.module_name), hasperm_url = 'admin:%s_%s_changelist' % (app_label, model_name)
current_app=self.admin_site.name) url = reverse(hasperm_url, current_app=site_name)
else: else:
post_url = reverse('admin:index', if noperm_url is None:
current_app=self.admin_site.name) noperm_url = 'admin:index'
return HttpResponseRedirect(post_url) 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. 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 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(). # Handle proxy models automatically created by .only() or .defer().
# Refs #14529 # Refs #14529
verbose_name = opts.verbose_name
module_name = opts.module_name
if obj._deferred: if obj._deferred:
opts_ = opts.proxy_for_model._meta opts_ = opts.proxy_for_model._meta
verbose_name = opts_.verbose_name 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: if "_continue" in request.POST:
self.message_user(request, msg + ' ' + _("You may edit it again below.")) msg = _('The %(name)s "%(obj)s" was changed successfully. You may edit it again below.') % msg_dict
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}
self.message_user(request, msg) self.message_user(request, msg)
return HttpResponseRedirect(reverse('admin:%s_%s_change' % if continue_editing_url is None:
(opts.app_label, module_name), continue_editing_url = 'admin:%s_%s_change' % (app_label, model_name)
args=(pk_value,), url = reverse(continue_editing_url, args=(quote(obj.pk),),
current_app=self.admin_site.name)) 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: elif "_addanother" in request.POST:
self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_text(verbose_name))) msg = _('The %(name)s "%(obj)s" was changed successfully. You may add another %(name)s below.') % msg_dict
return HttpResponseRedirect(reverse('admin:%s_%s_add' % self.message_user(request, msg)
(opts.app_label, module_name), if add_another_url is None:
current_app=self.admin_site.name)) add_another_url = 'admin:%s_%s_add' % (app_label, model_name)
url = reverse(add_another_url, current_app=site_name)
return HttpResponseRedirect(url)
else: else:
msg = _('The %(name)s "%(obj)s" was changed successfully.') % msg_dict
self.message_user(request, msg) self.message_user(request, msg)
# Figure out where to redirect. If the user has change permission, # Figure out where to redirect. If the user has change permission,
# redirect to the change-list page for this object. Otherwise, # redirect to the change-list page for this object. Otherwise,
# redirect to the admin index. # redirect to the admin index.
if self.has_change_permission(request, None): if self.has_change_permission(request, None):
post_url = reverse('admin:%s_%s_changelist' % if hasperm_url is None:
(opts.app_label, module_name), hasperm_url = 'admin:%s_%s_changelist' % (app_label,
current_app=self.admin_site.name) model_name)
url = reverse(hasperm_url, current_app=site_name)
else: else:
post_url = reverse('admin:index', if noperm_url is None:
current_app=self.admin_site.name) noperm_url = 'admin:index'
return HttpResponseRedirect(post_url) url = reverse(noperm_url, current_app=site_name)
return HttpResponseRedirect(url)
def response_action(self, request, queryset): def response_action(self, request, queryset):
""" """
@ -932,7 +994,7 @@ class ModelAdmin(BaseModelAdmin):
ModelForm = self.get_form(request) ModelForm = self.get_form(request)
formsets = [] formsets = []
inline_instances = self.get_inline_instances(request) inline_instances = self.get_inline_instances(request, None)
if request.method == 'POST': if request.method == 'POST':
form = ModelForm(request.POST, request.FILES) form = ModelForm(request.POST, request.FILES)
if form.is_valid(): if form.is_valid():
@ -1029,7 +1091,7 @@ class ModelAdmin(BaseModelAdmin):
ModelForm = self.get_form(request, obj) ModelForm = self.get_form(request, obj)
formsets = [] formsets = []
inline_instances = self.get_inline_instances(request) inline_instances = self.get_inline_instances(request, obj)
if request.method == 'POST': if request.method == 'POST':
form = ModelForm(request.POST, request.FILES, instance=obj) form = ModelForm(request.POST, request.FILES, instance=obj)
if form.is_valid(): if form.is_valid():

View File

@ -9,7 +9,6 @@ from django.db.models.base import ModelBase
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse, NoReverseMatch from django.core.urlresolvers import reverse, NoReverseMatch
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.safestring import mark_safe
from django.utils import six from django.utils import six
from django.utils.text import capfirst from django.utils.text import capfirst
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -18,12 +17,15 @@ from django.conf import settings
LOGIN_FORM_KEY = 'this_is_the_login_form' LOGIN_FORM_KEY = 'this_is_the_login_form'
class AlreadyRegistered(Exception): class AlreadyRegistered(Exception):
pass pass
class NotRegistered(Exception): class NotRegistered(Exception):
pass pass
class AdminSite(object): class AdminSite(object):
""" """
An AdminSite object encapsulates an instance of the Django admin application, ready An AdminSite object encapsulates an instance of the Django admin application, ready
@ -41,7 +43,7 @@ class AdminSite(object):
password_change_done_template = None password_change_done_template = None
def __init__(self, name='admin', app_name='admin'): 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.name = name
self.app_name = app_name self.app_name = app_name
self._actions = {'delete_selected': actions.delete_selected} self._actions = {'delete_selected': actions.delete_selected}
@ -80,20 +82,23 @@ class AdminSite(object):
if model in self._registry: if model in self._registry:
raise AlreadyRegistered('The model %s is already registered' % model.__name__) raise AlreadyRegistered('The model %s is already registered' % model.__name__)
# If we got **options then dynamically construct a subclass of # Ignore the registration if the model has been
# admin_class with those **options. # swapped out.
if options: if not model._meta.swapped:
# For reasons I don't quite understand, without a __module__ # If we got **options then dynamically construct a subclass of
# the created class appears to "live" in the wrong place, # admin_class with those **options.
# which causes issues later on. if options:
options['__module__'] = __name__ # For reasons I don't quite understand, without a __module__
admin_class = type("%sAdmin" % model.__name__, (admin_class,), options) # 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 (which might be a no-op)
validate(admin_class, model) validate(admin_class, model)
# Instantiate the admin class to save in the registry # Instantiate the admin class to save in the registry
self._registry[model] = admin_class(model, self) self._registry[model] = admin_class(model, self)
def unregister(self, model_or_iterable): def unregister(self, model_or_iterable):
""" """
@ -319,6 +324,7 @@ class AdminSite(object):
REDIRECT_FIELD_NAME: request.get_full_path(), REDIRECT_FIELD_NAME: request.get_full_path(),
} }
context.update(extra_context or {}) context.update(extra_context or {})
defaults = { defaults = {
'extra_context': context, 'extra_context': context,
'current_app': self.name, 'current_app': self.name,

View File

@ -12,6 +12,6 @@
{% block content %} {% block content %}
<h1>{% trans 'Server Error <em>(500)</em>' %}</h1> <h1>{% trans 'Server Error <em>(500)</em>' %}</h1>
<p>{% 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." %}</p> <p>{% 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." %}</p>
{% endblock %} {% endblock %}

View File

@ -28,7 +28,7 @@
</p> </p>
{% endif %} {% endif %}
<p>{% blocktrans with username=original.username %}Enter a new password for the user <strong>{{ username }}</strong>.{% endblocktrans %}</p> <p>{% blocktrans with username=original %}Enter a new password for the user <strong>{{ username }}</strong>.{% endblocktrans %}</p>
<fieldset class="module aligned"> <fieldset class="module aligned">

View File

@ -26,7 +26,7 @@
{% if user.is_active and user.is_staff %} {% if user.is_active and user.is_staff %}
<div id="user-tools"> <div id="user-tools">
{% trans 'Welcome,' %} {% trans 'Welcome,' %}
<strong>{% filter force_escape %}{% firstof user.first_name user.username %}{% endfilter %}</strong>. <strong>{% filter force_escape %}{% firstof user.get_short_name user.get_username %}{% endfilter %}</strong>.
{% block userlinks %} {% block userlinks %}
{% url 'django-admindocs-docroot' as docsroot %} {% url 'django-admindocs-docroot' as docsroot %}
{% if docsroot %} {% if docsroot %}

View File

@ -29,7 +29,7 @@
{% if change %}{% if not is_popup %} {% if change %}{% if not is_popup %}
<ul class="object-tools"> <ul class="object-tools">
{% block object-tools-items %} {% block object-tools-items %}
<li><a href="{% url opts|admin_urlname:'history' original.pk %}" class="historylink">{% trans "History" %}</a></li> <li><a href="{% url opts|admin_urlname:'history' original.pk|admin_urlquote %}" class="historylink">{% trans "History" %}</a></li>
{% if has_absolute_url %}<li><a href="{% url 'admin:view_on_site' content_type_id original.pk %}" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%} {% if has_absolute_url %}<li><a href="{% url 'admin:view_on_site' content_type_id original.pk %}" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%}
{% endblock %} {% endblock %}
</ul> </ul>

View File

@ -30,7 +30,7 @@
<form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %} <form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
<div class="form-row"> <div class="form-row">
{% if not form.this_is_the_login_form.errors %}{{ form.username.errors }}{% endif %} {% if not form.this_is_the_login_form.errors %}{{ form.username.errors }}{% endif %}
<label for="id_username" class="required">{% trans 'Username:' %}</label> {{ form.username }} <label for="id_username" class="required">{{ form.username.label }}:</label> {{ form.username }}
</div> </div>
<div class="form-row"> <div class="form-row">
{% if not form.this_is_the_login_form.errors %}{{ form.password.errors }}{% endif %} {% if not form.this_is_the_login_form.errors %}{{ form.password.errors }}{% endif %}

View File

@ -29,7 +29,7 @@
{% for action in action_list %} {% for action in action_list %}
<tr> <tr>
<th scope="row">{{ action.action_time|date:"DATETIME_FORMAT" }}</th> <th scope="row">{{ action.action_time|date:"DATETIME_FORMAT" }}</th>
<td>{{ action.user.username }}{% if action.user.get_full_name %} ({{ action.user.get_full_name }}){% endif %}</td> <td>{{ action.user.get_username }}{% if action.user.get_full_name %} ({{ action.user.get_full_name }}){% endif %}</td>
<td>{{ action.change_message }}</td> <td>{{ action.change_message }}</td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -1,8 +1,8 @@
{% load i18n %} {% load i18n admin_urls %}
<div class="submit-row"> <div class="submit-row">
{% if show_save %}<input type="submit" value="{% trans 'Save' %}" class="default" name="_save" {{ onclick_attrib }}/>{% endif %} {% if show_save %}<input type="submit" value="{% trans 'Save' %}" class="default" name="_save" {{ onclick_attrib }}/>{% endif %}
{% if show_delete_link %}<p class="deletelink-box"><a href="delete/" class="deletelink">{% trans "Delete" %}</a></p>{% endif %} {% if show_delete_link %}<p class="deletelink-box"><a href="{% url opts|admin_urlname:'delete' original.pk|admin_urlquote %}" class="deletelink">{% trans "Delete" %}</a></p>{% endif %}
{% if show_save_as_new %}<input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew" {{ onclick_attrib }}/>{%endif%} {% if show_save_as_new %}<input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew" {{ onclick_attrib }}/>{%endif%}
{% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother" {{ onclick_attrib }} />{% endif %} {% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother" {{ onclick_attrib }}/>{% endif %}
{% if show_save_and_continue %}<input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue" {{ onclick_attrib }}/>{% endif %} {% if show_save_and_continue %}<input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue" {{ onclick_attrib }}/>{% endif %}
</div> </div>

View File

@ -14,6 +14,6 @@
<h1>{% trans 'Password reset successful' %}</h1> <h1>{% trans 'Password reset successful' %}</h1>
<p>{% trans "We've e-mailed you instructions for setting your password to the e-mail address you submitted. You should be receiving it shortly." %}</p> <p>{% trans "We've emailed you instructions for setting your password to the email address you submitted. You should be receiving it shortly." %}</p>
{% endblock %} {% endblock %}

View File

@ -1,11 +1,11 @@
{% load i18n %}{% autoescape off %} {% 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:" %} {% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %} {% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %} {{ protocol }}://{{ domain }}{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %}
{% endblock %} {% 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!" %} {% trans "Thanks for using our site!" %}

View File

@ -14,11 +14,11 @@
<h1>{% trans "Password reset" %}</h1> <h1>{% trans "Password reset" %}</h1>
<p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll e-mail instructions for setting a new one." %}</p> <p>{% trans "Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one." %}</p>
<form action="" method="post">{% csrf_token %} <form action="" method="post">{% csrf_token %}
{{ form.email.errors }} {{ form.email.errors }}
<p><label for="id_email">{% trans 'E-mail address:' %}</label> {{ form.email }} <input type="submit" value="{% trans 'Reset my password' %}" /></p> <p><label for="id_email">{% trans 'Email address:' %}</label> {{ form.email }} <input type="submit" value="{% trans 'Reset my password' %}" /></p>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -28,7 +28,8 @@ def submit_row(context):
change = context['change'] change = context['change']
is_popup = context['is_popup'] is_popup = context['is_popup']
save_as = context['save_as'] save_as = context['save_as']
return { ctx = {
'opts': opts,
'onclick_attrib': (opts.get_ordered_objects() and change 'onclick_attrib': (opts.get_ordered_objects() and change
and 'onclick="submitOrderForm();"' or ''), and 'onclick="submitOrderForm();"' or ''),
'show_delete_link': (not is_popup and context['has_delete_permission'] 'show_delete_link': (not is_popup and context['has_delete_permission']
@ -40,6 +41,9 @@ def submit_row(context):
'is_popup': is_popup, 'is_popup': is_popup,
'show_save': True 'show_save': True
} }
if context.get('original') is not None:
ctx['original'] = context['original']
return ctx
@register.filter @register.filter
def cell_count(inline_admin_form): def cell_count(inline_admin_form):

View File

@ -21,9 +21,9 @@ class AdminSeleniumWebDriverTestCase(LiveServerTestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
super(AdminSeleniumWebDriverTestCase, cls).tearDownClass()
if hasattr(cls, 'selenium'): if hasattr(cls, 'selenium'):
cls.selenium.quit() cls.selenium.quit()
super(AdminSeleniumWebDriverTestCase, cls).tearDownClass()
def wait_until(self, callback, timeout=10): def wait_until(self, callback, timeout=10):
""" """
@ -98,4 +98,4 @@ class AdminSeleniumWebDriverTestCase(LiveServerTestCase):
`klass`. `klass`.
""" """
return (self.selenium.find_element_by_css_selector(selector) return (self.selenium.find_element_by_css_selector(selector)
.get_attribute('class').find(klass) != -1) .get_attribute('class').find(klass) != -1)

View File

@ -48,9 +48,9 @@ def prepare_lookup_value(key, value):
def quote(s): def quote(s):
""" """
Ensure that primary key values do not confuse the admin URLs by escaping Ensure that primary key values do not confuse the admin URLs by escaping
any '/', '_' and ':' characters. Similar to urllib.quote, except that the any '/', '_' and ':' and similarly problematic characters.
quoting is slightly different so that it doesn't get automatically Similar to urllib.quote, except that the quoting is slightly different so
unquoted by the Web browser. that it doesn't get automatically unquoted by the Web browser.
""" """
if not isinstance(s, six.string_types): if not isinstance(s, six.string_types):
return s return s
@ -191,6 +191,13 @@ class NestedObjects(Collector):
roots.extend(self._nested(root, seen, format_callback)) roots.extend(self._nested(root, seen, format_callback))
return roots 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): def model_format_dict(obj):
""" """

View File

@ -4,6 +4,7 @@ from django.contrib.admin.forms import AdminAuthenticationForm
from django.contrib.auth.views import login from django.contrib.auth.views import login
from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth import REDIRECT_FIELD_NAME
def staff_member_required(view_func): def staff_member_required(view_func):
""" """
Decorator for views that checks that the user is logged in and is a staff Decorator for views that checks that the user is logged in and is a staff

View File

@ -3,6 +3,7 @@ from functools import reduce
from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured
from django.core.paginator import InvalidPage from django.core.paginator import InvalidPage
from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from django.db.models.fields import FieldDoesNotExist from django.db.models.fields import FieldDoesNotExist
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
@ -376,4 +377,8 @@ class ChangeList(object):
return qs return qs
def url_for_result(self, result): 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)

View File

@ -4,7 +4,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Django\n" "Project-Id-Version: Django\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2010-05-13 15:35+0200\n"
"Last-Translator: Django team\n" "Last-Translator: Django team\n"
"Language-Team: English <en@li.org>\n" "Language-Team: English <en@li.org>\n"
@ -13,63 +13,75 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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:" msgid "tag:"
msgstr "" msgstr ""
#: views.py:92 views.py:94 views.py:96 #: views.py:93 views.py:95 views.py:97
msgid "filter:" msgid "filter:"
msgstr "" msgstr ""
#: views.py:155 views.py:157 views.py:159 #: views.py:156 views.py:158 views.py:160
msgid "view:" msgid "view:"
msgstr "" msgstr ""
#: views.py:187 #: views.py:188
#, python-format #, python-format
msgid "App %r not found" msgid "App %r not found"
msgstr "" msgstr ""
#: views.py:194 #: views.py:195
#, python-format #, python-format
msgid "Model %(model_name)r not found in app %(app_label)r" msgid "Model %(model_name)r not found in app %(app_label)r"
msgstr "" msgstr ""
#: views.py:206 #: views.py:207
#, python-format #, python-format
msgid "the related `%(app_label)s.%(data_type)s` object" msgid "the related `%(app_label)s.%(data_type)s` object"
msgstr "" msgstr ""
#: views.py:206 views.py:225 views.py:230 views.py:244 views.py:258 #: views.py:207 views.py:226 views.py:231 views.py:245 views.py:259
#: views.py:263 #: views.py:264
msgid "model:" msgid "model:"
msgstr "" msgstr ""
#: views.py:221 views.py:253 #: views.py:222 views.py:254
#, python-format #, python-format
msgid "related `%(app_label)s.%(object_name)s` objects" msgid "related `%(app_label)s.%(object_name)s` objects"
msgstr "" msgstr ""
#: views.py:225 views.py:258 #: views.py:226 views.py:259
#, python-format #, python-format
msgid "all %s" msgid "all %s"
msgstr "" msgstr ""
#: views.py:230 views.py:263 #: views.py:231 views.py:264
#, python-format #, python-format
msgid "number of %s" msgid "number of %s"
msgstr "" msgstr ""
#: views.py:268 #: views.py:269
#, python-format #, python-format
msgid "Fields on %s objects" msgid "Fields on %s objects"
msgstr "" msgstr ""
#: views.py:360 #: views.py:361
#, python-format #, python-format
msgid "%s does not appear to be a urlpattern object" msgid "%s does not appear to be a urlpattern object"
msgstr "" 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/bookmarklets.html:7 templates/admin_doc/index.html:7
#: templates/admin_doc/missing_docutils.html:7 #: templates/admin_doc/missing_docutils.html:7
#: templates/admin_doc/model_detail.html:15 #: templates/admin_doc/model_detail.html:15
@ -79,30 +91,18 @@ msgstr ""
#: templates/admin_doc/template_tag_index.html:8 #: templates/admin_doc/template_tag_index.html:8
#: templates/admin_doc/view_detail.html:7 #: templates/admin_doc/view_detail.html:7
#: templates/admin_doc/view_index.html:8 #: 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" msgid "Documentation"
msgstr "" msgstr ""
#: templates/admin_doc/bookmarklets.html:9 #: templates/admin_doc/bookmarklets.html:8
msgid "Bookmarklets" msgid "Bookmarklets"
msgstr "" msgstr ""
#: templates/admin_doc/bookmarklets.html:12 #: templates/admin_doc/bookmarklets.html:11
msgid "Documentation bookmarklets" msgid "Documentation bookmarklets"
msgstr "" msgstr ""
#: templates/admin_doc/bookmarklets.html:16 #: templates/admin_doc/bookmarklets.html:15
msgid "" msgid ""
"\n" "\n"
"<p class=\"help\">To install bookmarklets, drag the link to your bookmarks\n" "<p class=\"help\">To install bookmarklets, drag the link to your bookmarks\n"
@ -113,60 +113,69 @@ msgid ""
"your computer is \"internal\").</p>\n" "your computer is \"internal\").</p>\n"
msgstr "" msgstr ""
#: templates/admin_doc/bookmarklets.html:26 #: templates/admin_doc/bookmarklets.html:25
msgid "Documentation for this page" msgid "Documentation for this page"
msgstr "" msgstr ""
#: templates/admin_doc/bookmarklets.html:27 #: templates/admin_doc/bookmarklets.html:26
msgid "" msgid ""
"Jumps you from any page to the documentation for the view that generates " "Jumps you from any page to the documentation for the view that generates "
"that page." "that page."
msgstr "" msgstr ""
#: templates/admin_doc/bookmarklets.html:29 #: templates/admin_doc/bookmarklets.html:28
msgid "Show object ID" msgid "Show object ID"
msgstr "" msgstr ""
#: templates/admin_doc/bookmarklets.html:30 #: templates/admin_doc/bookmarklets.html:29
msgid "" msgid ""
"Shows the content-type and unique ID for pages that represent a single " "Shows the content-type and unique ID for pages that represent a single "
"object." "object."
msgstr "" msgstr ""
#: templates/admin_doc/bookmarklets.html:32 #: templates/admin_doc/bookmarklets.html:31
msgid "Edit this object (current window)" msgid "Edit this object (current window)"
msgstr "" 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." msgid "Jumps to the admin page for pages that represent a single object."
msgstr "" msgstr ""
#: templates/admin_doc/bookmarklets.html:35 #: templates/admin_doc/bookmarklets.html:34
msgid "Edit this object (new window)" msgid "Edit this object (new window)"
msgstr "" 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." msgid "As above, but opens the admin page in a new window."
msgstr "" msgstr ""
#: templates/admin_doc/model_detail.html:17 #: templates/admin_doc/model_detail.html:16
#: templates/admin_doc/model_index.html:11 #: templates/admin_doc/model_index.html:10
msgid "Models" msgid "Models"
msgstr "" msgstr ""
#: templates/admin_doc/template_detail.html:9 #: templates/admin_doc/template_detail.html:8
msgid "Templates" msgid "Templates"
msgstr "" msgstr ""
#: templates/admin_doc/template_filter_index.html:10 #: templates/admin_doc/template_filter_index.html:9
msgid "Filters" msgid "Filters"
msgstr "" msgstr ""
#: templates/admin_doc/template_tag_index.html:10 #: templates/admin_doc/template_tag_index.html:9
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
#: templates/admin_doc/view_detail.html:9 #: templates/admin_doc/view_detail.html:8
#: templates/admin_doc/view_index.html:10 #: templates/admin_doc/view_index.html:9
msgid "Views" msgid "Views"
msgstr "" 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 ""

View File

@ -26,7 +26,7 @@ class TestFieldType(unittest.TestCase):
def test_custom_fields(self): def test_custom_fields(self):
self.assertEqual( self.assertEqual(
views.get_readable_field_data_type(fields.CustomField()), views.get_readable_field_data_type(fields.CustomField()),
_('A custom field type') 'A custom field type'
) )
self.assertEqual( self.assertEqual(
views.get_readable_field_data_type(fields.DescriptionLackingField()), views.get_readable_field_data_type(fields.DescriptionLackingField()),

View File

@ -1,14 +1,17 @@
import re
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.utils.importlib import import_module 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' SESSION_KEY = '_auth_user_id'
BACKEND_SESSION_KEY = '_auth_user_backend' BACKEND_SESSION_KEY = '_auth_user_backend'
REDIRECT_FIELD_NAME = 'next' REDIRECT_FIELD_NAME = 'next'
def load_backend(path): def load_backend(path):
i = path.rfind('.') i = path.rfind('.')
module, attr = path[:i], path[i+1:] module, attr = path[:i], path[i + 1:]
try: try:
mod = import_module(module) mod = import_module(module)
except ImportError as e: 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)) raise ImproperlyConfigured('Module "%s" does not define a "%s" authentication backend' % (module, attr))
return cls() return cls()
def get_backends(): def get_backends():
from django.conf import settings from django.conf import settings
backends = [] backends = []
@ -30,6 +34,22 @@ def get_backends():
raise ImproperlyConfigured('No authentication backends have been defined. Does AUTHENTICATION_BACKENDS contain anything?') raise ImproperlyConfigured('No authentication backends have been defined. Does AUTHENTICATION_BACKENDS contain anything?')
return backends 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): def authenticate(**credentials):
""" """
If the given credentials are valid, return a User object. 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__) user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
return user 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): def login(request, user):
""" """
Persist a user id and a backend in the request. This way a user doesn't 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 request.user = user
user_logged_in.send(sender=user.__class__, request=request, user=user) user_logged_in.send(sender=user.__class__, request=request, user=user)
def logout(request): def logout(request):
""" """
Removes the authenticated user's ID from the request and flushes their 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 from django.contrib.auth.models import AnonymousUser
request.user = 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): def get_user(request):
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
try: try:

View File

@ -11,14 +11,13 @@ from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.html import escape from django.utils.html import escape
from django.utils.decorators import method_decorator 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.utils.translation import ugettext, ugettext_lazy as _
from django.views.decorators.csrf import csrf_protect from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters from django.views.decorators.debug import sensitive_post_parameters
csrf_protect_m = method_decorator(csrf_protect) csrf_protect_m = method_decorator(csrf_protect)
class GroupAdmin(admin.ModelAdmin): class GroupAdmin(admin.ModelAdmin):
search_fields = ('name',) search_fields = ('name',)
ordering = ('name',) ordering = ('name',)
@ -54,10 +53,10 @@ class UserAdmin(admin.ModelAdmin):
add_form = UserCreationForm add_form = UserCreationForm
change_password_form = AdminPasswordChangeForm change_password_form = AdminPasswordChangeForm
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') 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') search_fields = ('username', 'first_name', 'last_name', 'email')
ordering = ('username',) ordering = ('username',)
filter_horizontal = ('user_permissions',) filter_horizontal = ('groups', 'user_permissions',)
def get_fieldsets(self, request, obj=None): def get_fieldsets(self, request, obj=None):
if not obj: if not obj:
@ -106,9 +105,10 @@ class UserAdmin(admin.ModelAdmin):
raise PermissionDenied raise PermissionDenied
if extra_context is None: if extra_context is None:
extra_context = {} extra_context = {}
username_field = self.model._meta.get_field(self.model.USERNAME_FIELD)
defaults = { defaults = {
'auto_populated_fields': (), '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) extra_context.update(defaults)
return super(UserAdmin, self).add_view(request, form_url, return super(UserAdmin, self).add_view(request, form_url,
@ -153,7 +153,7 @@ class UserAdmin(admin.ModelAdmin):
'admin/auth/user/change_password.html' 'admin/auth/user/change_password.html'
], context, current_app=self.admin_site.name) ], 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 Determines the HttpResponse for the add_view stage. It mostly defers to
its superclass implementation but is customized because the User model 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 # * We are adding a user in a popup
if '_addanother' not in request.POST and '_popup' not in request.POST: if '_addanother' not in request.POST and '_popup' not in request.POST:
request.POST['_continue'] = 1 request.POST['_continue'] = 1
return super(UserAdmin, self).response_add(request, obj, return super(UserAdmin, self).response_add(request, obj, **kwargs)
post_url_continue)
admin.site.register(Group, GroupAdmin) admin.site.register(Group, GroupAdmin)
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)

View File

@ -1,6 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User, Permission from django.contrib.auth.models import Permission
class ModelBackend(object): class ModelBackend(object):
@ -12,10 +12,11 @@ class ModelBackend(object):
# configurable. # configurable.
def authenticate(self, username=None, password=None): def authenticate(self, username=None, password=None):
try: try:
user = User.objects.get(username=username) UserModel = get_user_model()
user = UserModel.objects.get_by_natural_key(username)
if user.check_password(password): if user.check_password(password):
return user return user
except User.DoesNotExist: except UserModel.DoesNotExist:
return None return None
def get_group_permissions(self, user_obj, obj=None): def get_group_permissions(self, user_obj, obj=None):
@ -29,7 +30,9 @@ class ModelBackend(object):
if user_obj.is_superuser: if user_obj.is_superuser:
perms = Permission.objects.all() perms = Permission.objects.all()
else: 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() 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]) user_obj._group_perm_cache = set(["%s.%s" % (ct, name) for ct, name in perms])
return user_obj._group_perm_cache return user_obj._group_perm_cache
@ -60,8 +63,9 @@ class ModelBackend(object):
def get_user(self, user_id): def get_user(self, user_id):
try: try:
return User.objects.get(pk=user_id) UserModel = get_user_model()
except User.DoesNotExist: return UserModel.objects.get(pk=user_id)
except UserModel.DoesNotExist:
return None return None
@ -94,17 +98,21 @@ class RemoteUserBackend(ModelBackend):
user = None user = None
username = self.clean_username(remote_user) username = self.clean_username(remote_user)
UserModel = get_user_model()
# Note that this could be accomplished in one try-except clause, but # 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 # instead we use get_or_create when creating unknown users since it has
# built-in safeguards for multiple threads. # built-in safeguards for multiple threads.
if self.create_unknown_user: 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: if created:
user = self.configure_user(user) user = self.configure_user(user)
else: else:
try: try:
user = User.objects.get(username=username) user = UserModel.objects.get_by_natural_key(username)
except User.DoesNotExist: except UserModel.DoesNotExist:
pass pass
return user return user

View File

@ -11,6 +11,11 @@ class PermLookupDict(object):
def __getitem__(self, perm_name): def __getitem__(self, perm_name):
return self.user.has_perm("%s.%s" % (self.module_name, 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): def __bool__(self):
return self.user.has_module_perms(self.module_name) return self.user.has_module_perms(self.module_name)
__nonzero__ = __bool__ # Python 2 __nonzero__ = __bool__ # Python 2
@ -27,6 +32,17 @@ class PermWrapper(object):
# I am large, I contain multitudes. # I am large, I contain multitudes.
raise TypeError("PermWrapper is not iterable.") 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): def auth(request):
""" """
Returns context variables required by apps that use Django's authentication Returns context variables required by apps that use Django's authentication

View File

@ -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"
}
}
]

View File

@ -7,9 +7,10 @@ from django.utils.datastructures import SortedDict
from django.utils.html import format_html, format_html_join from django.utils.html import format_html, format_html_join
from django.utils.http import int_to_base36 from django.utils.http import int_to_base36
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.text import capfirst
from django.utils.translation import ugettext, ugettext_lazy as _ 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.models import User
from django.contrib.auth.hashers import UNUSABLE_PASSWORD, identify_hasher from django.contrib.auth.hashers import UNUSABLE_PASSWORD, identify_hasher
from django.contrib.auth.tokens import default_token_generator 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 " "this user's password, but you can change the password "
"using <a href=\"password/\">this form</a>.")) "using <a href=\"password/\">this form</a>."))
def clean_password(self):
return self.initial["password"]
class Meta: class Meta:
model = User model = User
@ -129,13 +127,19 @@ class UserChangeForm(forms.ModelForm):
if f is not None: if f is not None:
f.queryset = f.queryset.select_related('content_type') 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): class AuthenticationForm(forms.Form):
""" """
Base class for authenticating users. Extend this to get a form that accepts Base class for authenticating users. Extend this to get a form that accepts
username/password logins. 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) password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
error_messages = { error_messages = {
@ -157,6 +161,11 @@ class AuthenticationForm(forms.Form):
self.user_cache = None self.user_cache = None
super(AuthenticationForm, self).__init__(*args, **kwargs) 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): def clean(self):
username = self.cleaned_data.get('username') username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password') password = self.cleaned_data.get('password')
@ -187,20 +196,21 @@ class AuthenticationForm(forms.Form):
class PasswordResetForm(forms.Form): class PasswordResetForm(forms.Form):
error_messages = { 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?"), "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."), "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): def clean_email(self):
""" """
Validates that an active user exists with the given email address. Validates that an active user exists with the given email address.
""" """
UserModel = get_user_model()
email = self.cleaned_data["email"] email = self.cleaned_data["email"]
self.users_cache = User.objects.filter(email__iexact=email, self.users_cache = UserModel.objects.filter(email__iexact=email,
is_active=True) is_active=True)
if not len(self.users_cache): if not len(self.users_cache):
raise forms.ValidationError(self.error_messages['unknown']) raise forms.ValidationError(self.error_messages['unknown'])
if any((user.password == UNUSABLE_PASSWORD) if any((user.password == UNUSABLE_PASSWORD)

View File

@ -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()

View File

@ -4,7 +4,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Django\n" "Project-Id-Version: Django\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2010-05-13 15:35+0200\n"
"Last-Translator: Django team\n" "Last-Translator: Django team\n"
"Language-Team: English <en@li.org>\n" "Language-Team: English <en@li.org>\n"
@ -25,48 +25,56 @@ msgstr ""
msgid "Important dates" msgid "Important dates"
msgstr "" msgstr ""
#: admin.py:125 #: admin.py:126
msgid "Password changed successfully." msgid "Password changed successfully."
msgstr "" msgstr ""
#: admin.py:135 #: admin.py:136
#, python-format #, python-format
msgid "Change password: %s" msgid "Change password: %s"
msgstr "" 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." msgid "A user with that username already exists."
msgstr "" 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." msgid "The two password fields didn't match."
msgstr "" msgstr ""
#: forms.py:65 forms.py:110 forms.py:139 #: forms.py:68 forms.py:113
msgid "Username" msgid "Username"
msgstr "" msgstr ""
#: forms.py:67 forms.py:111 #: forms.py:70 forms.py:114
msgid "Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only." msgid "Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only."
msgstr "" msgstr ""
#: forms.py:70 forms.py:114 #: forms.py:73 forms.py:117
msgid "This value may contain only letters, numbers and @/./+/-/_ characters." msgid "This value may contain only letters, numbers and @/./+/-/_ characters."
msgstr "" 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" msgid "Password"
msgstr "" msgstr ""
#: forms.py:74 #: forms.py:77
msgid "Password confirmation" msgid "Password confirmation"
msgstr "" msgstr ""
#: forms.py:76 #: forms.py:79
msgid "Enter the same password as above, for verification." msgid "Enter the same password as above, for verification."
msgstr "" msgstr ""
#: forms.py:117 #: forms.py:120
msgid "" msgid ""
"Raw passwords are not stored, so there is no way to see this user's " "Raw passwords are not stored, so there is no way to see this user's "
"password, but you can change the password using <a href=\"password/\">this " "password, but you can change the password using <a href=\"password/\">this "
@ -89,178 +97,178 @@ msgstr ""
msgid "This account is inactive." msgid "This account is inactive."
msgstr "" msgstr ""
#: forms.py:191 #: forms.py:196
msgid "" 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?" "you've registered?"
msgstr "" msgstr ""
#: forms.py:193 #: forms.py:198 tests/forms.py:347
msgid "" 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." "password."
msgstr "" msgstr ""
#: forms.py:196 #: forms.py:201
msgid "E-mail" msgid "Email"
msgstr "" msgstr ""
#: forms.py:253 #: forms.py:259
msgid "New password" msgid "New password"
msgstr "" msgstr ""
#: forms.py:255 #: forms.py:261
msgid "New password confirmation" msgid "New password confirmation"
msgstr "" msgstr ""
#: forms.py:284 #: forms.py:290
msgid "Your old password was entered incorrectly. Please enter it again." msgid "Your old password was entered incorrectly. Please enter it again."
msgstr "" msgstr ""
#: forms.py:287 #: forms.py:293
msgid "Old password" msgid "Old password"
msgstr "" msgstr ""
#: forms.py:312 #: forms.py:321
msgid "Password (again)" msgid "Password (again)"
msgstr "" msgstr ""
#: hashers.py:218 hashers.py:269 hashers.py:298 hashers.py:326 hashers.py:355 #: hashers.py:241 hashers.py:292 hashers.py:321 hashers.py:349 hashers.py:378
#: hashers.py:389 #: hashers.py:412
msgid "algorithm" msgid "algorithm"
msgstr "" msgstr ""
#: hashers.py:219 #: hashers.py:242
msgid "iterations" msgid "iterations"
msgstr "" 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" msgid "salt"
msgstr "" 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" msgid "hash"
msgstr "" msgstr ""
#: hashers.py:270 #: hashers.py:293
msgid "work factor" msgid "work factor"
msgstr "" msgstr ""
#: hashers.py:272 #: hashers.py:295
msgid "checksum" msgid "checksum"
msgstr "" msgstr ""
#: models.py:66 models.py:113 #: models.py:72 models.py:121
msgid "name" msgid "name"
msgstr "" msgstr ""
#: models.py:68 #: models.py:74
msgid "codename" msgid "codename"
msgstr "" msgstr ""
#: models.py:72 #: models.py:78
msgid "permission" msgid "permission"
msgstr "" msgstr ""
#: models.py:73 models.py:115 #: models.py:79 models.py:123
msgid "permissions" msgid "permissions"
msgstr "" msgstr ""
#: models.py:120 #: models.py:128
msgid "group" msgid "group"
msgstr "" msgstr ""
#: models.py:121 models.py:250 #: models.py:129 models.py:317
msgid "groups" msgid "groups"
msgstr "" msgstr ""
#: models.py:232 #: models.py:232
msgid "username" msgid "password"
msgstr "" msgstr ""
#: models.py:233 #: models.py:233
msgid "last login"
msgstr ""
#: models.py:298
msgid "username"
msgstr ""
#: models.py:299
msgid "" msgid ""
"Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters" "Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"
msgstr "" msgstr ""
#: models.py:235 #: models.py:302
msgid "Enter a valid username."
msgstr ""
#: models.py:304
msgid "first name" msgid "first name"
msgstr "" msgstr ""
#: models.py:236 #: models.py:305
msgid "last name" msgid "last name"
msgstr "" msgstr ""
#: models.py:237 #: models.py:306
msgid "e-mail address" msgid "email address"
msgstr "" msgstr ""
#: models.py:238 #: models.py:307
msgid "password"
msgstr ""
#: models.py:239
msgid "staff status" msgid "staff status"
msgstr "" msgstr ""
#: models.py:240 #: models.py:308
msgid "Designates whether the user can log into this admin site." msgid "Designates whether the user can log into this admin site."
msgstr "" msgstr ""
#: models.py:242 #: models.py:310
msgid "active" msgid "active"
msgstr "" msgstr ""
#: models.py:243 #: models.py:311
msgid "" msgid ""
"Designates whether this user should be treated as active. Unselect this " "Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts." "instead of deleting accounts."
msgstr "" msgstr ""
#: models.py:245 #: models.py:313
msgid "superuser status" msgid "superuser status"
msgstr "" msgstr ""
#: models.py:246 #: models.py:314
msgid "" msgid ""
"Designates that this user has all permissions without explicitly assigning " "Designates that this user has all permissions without explicitly assigning "
"them." "them."
msgstr "" msgstr ""
#: models.py:248 #: models.py:316
msgid "last login"
msgstr ""
#: models.py:249
msgid "date joined" msgid "date joined"
msgstr "" msgstr ""
#: models.py:251 #: models.py:318
msgid "" msgid ""
"The groups this user belongs to. A user will get all permissions granted to " "The groups this user belongs to. A user will get all permissions granted to "
"each of his/her group." "each of his/her group."
msgstr "" msgstr ""
#: models.py:255 #: models.py:322
msgid "user permissions" msgid "user permissions"
msgstr "" msgstr ""
#: models.py:260 #: models.py:331
msgid "user" msgid "user"
msgstr "" msgstr ""
#: models.py:261 #: models.py:332
msgid "users" msgid "users"
msgstr "" msgstr ""
#: views.py:93 #: views.py:97
msgid "Logged out" msgid "Logged out"
msgstr "" msgstr ""
#: management/commands/createsuperuser.py:27
msgid "Enter a valid e-mail address."
msgstr ""
#: templates/registration/password_reset_subject.txt:2 #: templates/registration/password_reset_subject.txt:2
#, python-format #, python-format
msgid "Password reset on %(site_name)s" msgid "Password reset on %(site_name)s"

View File

@ -6,9 +6,11 @@ from __future__ import unicode_literals
import getpass import getpass
import locale import locale
import unicodedata 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.db.models import get_models, signals
from django.contrib.auth.models import User
from django.utils import six from django.utils import six
from django.utils.six.moves import input 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()) return '%s_%s' % (action, opts.object_name.lower())
def _get_all_permissions(opts): def _get_all_permissions(opts, ctype):
"Returns (codename, name) for all permissions in the given opts." """
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 = [] perms = []
for action in ('add', 'change', 'delete'): for action in ('add', 'change', 'delete'):
perms.append((_get_permission_codename(action, opts), 'Can %s %s' % (action, opts.verbose_name_raw))) perms.append((_get_permission_codename(action, opts),
return perms + list(opts.permissions) '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): def create_permissions(app, created_models, verbosity, **kwargs):
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -38,7 +70,7 @@ def create_permissions(app, created_models, verbosity, **kwargs):
for klass in app_models: for klass in app_models:
ctype = ContentType.objects.get_for_model(klass) ctype = ContentType.objects.get_for_model(klass)
ctypes.add(ctype) 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)) searched_perms.append((ctype, perm))
# Find all the Permissions that have a context_type for a model we're # 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): def create_superuser(app, created_models, verbosity, db, **kwargs):
from django.core.management import call_command 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 " msg = ("\nYou just installed Django's auth system, which means you "
"don't have any superusers defined.\nWould you like to create one " "don't have any superusers defined.\nWould you like to create one "
"now? (yes/no): ") "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 :returns: The username, or an empty string if no username can be
determined. determined.
""" """
from django.contrib.auth.management.commands.createsuperuser import ( # If the User model has been swapped out, we can't make any assumptions
RE_VALID_USERNAME) # about the default user name.
if auth_app.User._meta.swapped:
return ''
default_username = get_system_username() default_username = get_system_username()
try: try:
default_username = unicodedata.normalize('NFKD', default_username)\ default_username = unicodedata.normalize('NFKD', default_username)\
.encode('ascii', 'ignore').decode('ascii').replace(' ', '').lower() .encode('ascii', 'ignore').decode('ascii').replace(' ', '').lower()
except UnicodeDecodeError: except UnicodeDecodeError:
return '' 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 '' return ''
# Don't return the default username if it is already taken. # Don't return the default username if it is already taken.
if check_db and default_username: if check_db and default_username:
try: try:
User.objects.get(username=default_username) auth_app.User.objects.get(username=default_username)
except User.DoesNotExist: except auth_app.User.DoesNotExist:
pass pass
else: else:
return '' return ''
return default_username return default_username
signals.post_syncdb.connect(create_permissions, 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, 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")

View File

@ -1,8 +1,8 @@
import getpass import getpass
from optparse import make_option from optparse import make_option
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import User
from django.db import DEFAULT_DB_ALIAS from django.db import DEFAULT_DB_ALIAS
@ -30,12 +30,16 @@ class Command(BaseCommand):
else: else:
username = getpass.getuser() username = getpass.getuser()
UserModel = get_user_model()
try: try:
u = User.objects.using(options.get('database')).get(username=username) u = UserModel.objects.using(options.get('database')).get(**{
except User.DoesNotExist: UserModel.USERNAME_FIELD: username
})
except UserModel.DoesNotExist:
raise CommandError("user '%s' does not exist" % username) 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 MAX_TRIES = 3
count = 0 count = 0
@ -48,9 +52,9 @@ class Command(BaseCommand):
count = count + 1 count = count + 1
if count == MAX_TRIES: 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.set_password(p1)
u.save() u.save()
return "Password changed successfully for user '%s'" % u.username return "Password changed successfully for user '%s'" % u

View File

@ -3,109 +3,119 @@ Management utility to create superusers.
""" """
import getpass import getpass
import re
import sys import sys
from optparse import make_option 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.contrib.auth.management import get_default_username
from django.core import exceptions from django.core import exceptions
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.db import DEFAULT_DB_ALIAS from django.db import DEFAULT_DB_ALIAS
from django.utils.six.moves import input from django.utils.six.moves import input
from django.utils.translation import ugettext as _ from django.utils.text import capfirst
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.'))
class Command(BaseCommand): class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option('--username', dest='username', default=None, def __init__(self, *args, **kwargs):
help='Specifies the username for the superuser.'), # Options are defined in an __init__ method to support swapping out
make_option('--email', dest='email', default=None, # custom user models in tests.
help='Specifies the email address for the superuser.'), super(Command, self).__init__(*args, **kwargs)
make_option('--noinput', action='store_false', dest='interactive', default=True, self.UserModel = get_user_model()
help=('Tells Django to NOT prompt the user for input of any kind. ' self.username_field = self.UserModel._meta.get_field(self.UserModel.USERNAME_FIELD)
'You must use --username and --email with --noinput, and '
'superusers created with --noinput will not be able to log ' self.option_list = BaseCommand.option_list + (
'in until they\'re given a valid password.')), make_option('--%s' % self.UserModel.USERNAME_FIELD, dest=self.UserModel.USERNAME_FIELD, default=None,
make_option('--database', action='store', dest='database', help='Specifies the login for the superuser.'),
default=DEFAULT_DB_ALIAS, help='Specifies the database to use. Default is "default".'), 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.' help = 'Used to create a superuser.'
def handle(self, *args, **options): def handle(self, *args, **options):
username = options.get('username', None) username = options.get(self.UserModel.USERNAME_FIELD, None)
email = options.get('email', None)
interactive = options.get('interactive') interactive = options.get('interactive')
verbosity = int(options.get('verbosity', 1)) verbosity = int(options.get('verbosity', 1))
database = options.get('database') 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 # If not provided, create the user with an unusable password
password = None password = None
user_data = {}
# Prompt for username/email/password. Enclose this whole thing in a # Do quick and dirty validation if --noinput
# try/except to trap for a keyboard interrupt and exit gracefully. if not interactive:
if 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() default_username = get_default_username()
try: try:
# Get a username # Get a username
while 1: while username is None:
if not username: if not username:
input_msg = 'Username' input_msg = capfirst(self.username_field.verbose_name)
if default_username: if default_username:
input_msg += ' (leave blank to use %r)' % default_username input_msg += " (leave blank to use '%s')" % default_username
username = input(input_msg + ': ') raw_value = input(input_msg + ': ')
if default_username and username == '':
username = default_username if default_username and raw_value == '':
if not RE_VALID_USERNAME.match(username): raw_value = default_username
self.stderr.write("Error: That username is invalid. Use only letters, digits and underscores.") try:
username = self.username_field.clean(raw_value, None)
except exceptions.ValidationError as e:
self.stderr.write("Error: %s" % '; '.join(e.messages))
username = None username = None
continue continue
try: try:
User.objects.using(database).get(username=username) self.UserModel.objects.db_manager(database).get_by_natural_key(username)
except User.DoesNotExist: except self.UserModel.DoesNotExist:
break pass
else: 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 username = None
# Get an email for field_name in self.UserModel.REQUIRED_FIELDS:
while 1: field = self.UserModel._meta.get_field(field_name)
if not email: user_data[field_name] = options.get(field_name)
email = input('E-mail address: ') while user_data[field_name] is None:
try: raw_value = input(capfirst(field.verbose_name + ': '))
is_valid_email(email) try:
except exceptions.ValidationError: user_data[field_name] = field.clean(raw_value, None)
self.stderr.write("Error: That e-mail address is invalid.") except exceptions.ValidationError as e:
email = None self.stderr.write("Error: %s" % '; '.join(e.messages))
else: user_data[field_name] = None
break
# Get a password # Get a password
while 1: while password is None:
if not password: if not password:
password = getpass.getpass() password = getpass.getpass()
password2 = getpass.getpass('Password (again): ') password2 = getpass.getpass('Password (again): ')
@ -117,12 +127,13 @@ class Command(BaseCommand):
self.stderr.write("Error: Blank passwords aren't allowed.") self.stderr.write("Error: Blank passwords aren't allowed.")
password = None password = None
continue continue
break
except KeyboardInterrupt: except KeyboardInterrupt:
self.stderr.write("\nOperation cancelled.") self.stderr.write("\nOperation cancelled.")
sys.exit(1) 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: if verbosity >= 1:
self.stdout.write("Superuser created successfully.") self.stdout.write("Superuser created successfully.")

View File

@ -55,7 +55,7 @@ class RemoteUserMiddleware(object):
# getting passed in the headers, then the correct user is already # getting passed in the headers, then the correct user is already
# persisted in the session and we don't need to continue. # persisted in the session and we don't need to continue.
if request.user.is_authenticated(): 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 return
# We are seeing this user for the first time in this session, attempt # We are seeing this user for the first time in this session, attempt
# to authenticate the user. # to authenticate the user.
@ -75,6 +75,6 @@ class RemoteUserMiddleware(object):
backend = auth.load_backend(backend_str) backend = auth.load_backend(backend_str)
try: try:
username = backend.clean_username(username) username = backend.clean_username(username)
except AttributeError: # Backend has no clean_username method. except AttributeError: # Backend has no clean_username method.
pass pass
return username return username

View File

@ -1,7 +1,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import re
import warnings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.mail import send_mail from django.core.mail import send_mail
from django.core import validators
from django.db import models from django.db import models
from django.db.models.manager import EmptyManager from django.db.models.manager import EmptyManager
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
@ -96,6 +99,7 @@ class GroupManager(models.Manager):
def get_by_natural_key(self, name): def get_by_natural_key(self, name):
return self.get(name=name) return self.get(name=name)
@python_2_unicode_compatible @python_2_unicode_compatible
class Group(models.Model): class Group(models.Model):
""" """
@ -131,7 +135,7 @@ class Group(models.Model):
return (self.name,) return (self.name,)
class UserManager(models.Manager): class BaseUserManager(models.Manager):
@classmethod @classmethod
def normalize_email(cls, email): def normalize_email(cls, email):
@ -148,30 +152,6 @@ class UserManager(models.Manager):
email = '@'.join([email_name, domain_part.lower()]) email = '@'.join([email_name, domain_part.lower()])
return email 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, def make_random_password(self, length=10,
allowed_chars='abcdefghjkmnpqrstuvwxyz' allowed_chars='abcdefghjkmnpqrstuvwxyz'
'ABCDEFGHJKLMNPQRSTUVWXYZ' 'ABCDEFGHJKLMNPQRSTUVWXYZ'
@ -185,7 +165,34 @@ class UserManager(models.Manager):
return get_random_string(length, allowed_chars) return get_random_string(length, allowed_chars)
def get_by_natural_key(self, username): 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. # 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): def _user_has_perm(user, perm, obj):
anon = user.is_anonymous()
active = user.is_active
for backend in auth.get_backends(): for backend in auth.get_backends():
if hasattr(backend, "has_perm"): if hasattr(backend, "has_perm"):
if obj is not None: if obj is not None:
@ -215,8 +220,6 @@ def _user_has_perm(user, perm, obj):
def _user_has_module_perms(user, app_label): def _user_has_module_perms(user, app_label):
anon = user.is_anonymous()
active = user.is_active
for backend in auth.get_backends(): for backend in auth.get_backends():
if hasattr(backend, "has_module_perms"): if hasattr(backend, "has_module_perms"):
if backend.has_module_perms(user, app_label): if backend.has_module_perms(user, app_label):
@ -225,52 +228,24 @@ def _user_has_module_perms(user, app_label):
@python_2_unicode_compatible @python_2_unicode_compatible
class User(models.Model): class AbstractBaseUser(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)
password = models.CharField(_('password'), max_length=128) 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) 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'), REQUIRED_FIELDS = []
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()
class Meta: class Meta:
verbose_name = _('user') abstract = True
verbose_name_plural = _('users')
def get_username(self):
"Return the identifying username for this User"
return getattr(self, self.USERNAME_FIELD)
def __str__(self): def __str__(self):
return self.username return self.get_username()
def natural_key(self): def natural_key(self):
return (self.username,) return (self.get_username(),)
def get_absolute_url(self):
return "/users/%s/" % urlquote(self.username)
def is_anonymous(self): def is_anonymous(self):
""" """
@ -286,13 +261,6 @@ class User(models.Model):
""" """
return True 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): def set_password(self, raw_password):
self.password = make_password(raw_password) self.password = make_password(raw_password)
@ -313,6 +281,71 @@ class User(models.Model):
def has_usable_password(self): def has_usable_password(self):
return is_password_usable(self.password) 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): def get_group_permissions(self, obj=None):
""" """
Returns a list of permission strings that this user has through his/her 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 Returns site-specific profile for this user. Raises
SiteProfileNotAvailable if this site does not allow profiles. 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'): if not hasattr(self, '_profile_cache'):
from django.conf import settings from django.conf import settings
if not getattr(settings, 'AUTH_PROFILE_MODULE', False): if not getattr(settings, 'AUTH_PROFILE_MODULE', False):
@ -407,6 +442,17 @@ class User(models.Model):
return self._profile_cache 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 @python_2_unicode_compatible
class AnonymousUser(object): class AnonymousUser(object):
id = None id = None
@ -431,7 +477,7 @@ class AnonymousUser(object):
return not self.__eq__(other) return not self.__eq__(other)
def __hash__(self): def __hash__(self):
return 1 # instances always return the same hash value return 1 # instances always return the same hash value
def save(self): def save(self):
raise NotImplementedError raise NotImplementedError

View File

@ -1,4 +1,5 @@
from django.dispatch import Signal from django.dispatch import Signal
user_logged_in = Signal(providing_args=['request', 'user']) user_logged_in = Signal(providing_args=['request', 'user'])
user_login_failed = Signal(providing_args=['credentials'])
user_logged_out = Signal(providing_args=['request', 'user']) user_logged_out = Signal(providing_args=['request', 'user'])

View File

@ -1,26 +1,16 @@
from django.contrib.auth.tests.auth_backends import (BackendTest, from django.contrib.auth.tests.custom_user import *
RowlevelBackendTest, AnonymousUserBackendTest, NoBackendsTest, from django.contrib.auth.tests.auth_backends import *
InActiveUserBackendTest) from django.contrib.auth.tests.basic import *
from django.contrib.auth.tests.basic import BasicTestCase from django.contrib.auth.tests.context_processors import *
from django.contrib.auth.tests.context_processors import AuthContextProcessorTests from django.contrib.auth.tests.decorators import *
from django.contrib.auth.tests.decorators import LoginRequiredTestCase from django.contrib.auth.tests.forms import *
from django.contrib.auth.tests.forms import (UserCreationFormTest, from django.contrib.auth.tests.remote_user import *
AuthenticationFormTest, SetPasswordFormTest, PasswordChangeFormTest, from django.contrib.auth.tests.management import *
UserChangeFormTest, PasswordResetFormTest) from django.contrib.auth.tests.models import *
from django.contrib.auth.tests.remote_user import (RemoteUserTest, from django.contrib.auth.tests.handlers import *
RemoteUserNoCreateTest, RemoteUserCustomTest) from django.contrib.auth.tests.hashers import *
from django.contrib.auth.tests.management import ( from django.contrib.auth.tests.signals import *
GetDefaultUsernameTestCase, from django.contrib.auth.tests.tokens import *
ChangepasswordManagementCommandTestCase, from django.contrib.auth.tests.views import *
)
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)
# The password for the fixture data users is 'password' # The password for the fixture data users is 'password'

View File

@ -1,22 +1,29 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import date
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User, Group, Permission, AnonymousUser 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.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings 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' backend = 'django.contrib.auth.backends.ModelBackend'
def setUp(self): def setUp(self):
self.curr_auth = settings.AUTHENTICATION_BACKENDS self.curr_auth = settings.AUTHENTICATION_BACKENDS
settings.AUTHENTICATION_BACKENDS = (self.backend,) settings.AUTHENTICATION_BACKENDS = (self.backend,)
User.objects.create_user('test', 'test@example.com', 'test') self.create_users()
User.objects.create_superuser('test2', 'test2@example.com', 'test')
def tearDown(self): def tearDown(self):
settings.AUTHENTICATION_BACKENDS = self.curr_auth settings.AUTHENTICATION_BACKENDS = self.curr_auth
@ -26,7 +33,7 @@ class BackendTest(TestCase):
ContentType.objects.clear_cache() ContentType.objects.clear_cache()
def test_has_perm(self): 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) self.assertEqual(user.has_perm('auth.test'), False)
user.is_staff = True user.is_staff = True
user.save() user.save()
@ -45,14 +52,14 @@ class BackendTest(TestCase):
self.assertEqual(user.has_perm('auth.test'), False) self.assertEqual(user.has_perm('auth.test'), False)
def test_custom_perms(self): def test_custom_perms(self):
user = User.objects.get(username='test') user = self.UserModel.objects.get(username='test')
content_type=ContentType.objects.get_for_model(Group) content_type = ContentType.objects.get_for_model(Group)
perm = Permission.objects.create(name='test', content_type=content_type, codename='test') perm = Permission.objects.create(name='test', content_type=content_type, codename='test')
user.user_permissions.add(perm) user.user_permissions.add(perm)
user.save() user.save()
# reloading user to purge the _perm_cache # 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_all_permissions() == set(['auth.test']), True)
self.assertEqual(user.get_group_permissions(), set([])) self.assertEqual(user.get_group_permissions(), set([]))
self.assertEqual(user.has_module_perms('Group'), False) 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') perm = Permission.objects.create(name='test3', content_type=content_type, codename='test3')
user.user_permissions.add(perm) user.user_permissions.add(perm)
user.save() 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.get_all_permissions(), set(['auth.test2', 'auth.test', 'auth.test3']))
self.assertEqual(user.has_perm('test'), False) self.assertEqual(user.has_perm('test'), False)
self.assertEqual(user.has_perm('auth.test'), True) self.assertEqual(user.has_perm('auth.test'), True)
@ -73,7 +80,7 @@ class BackendTest(TestCase):
group.permissions.add(perm) group.permissions.add(perm)
group.save() group.save()
user.groups.add(group) 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']) exp = set(['auth.test2', 'auth.test', 'auth.test3', 'auth.test_group'])
self.assertEqual(user.get_all_permissions(), exp) self.assertEqual(user.get_all_permissions(), exp)
self.assertEqual(user.get_group_permissions(), set(['auth.test_group'])) self.assertEqual(user.get_group_permissions(), set(['auth.test_group']))
@ -85,8 +92,8 @@ class BackendTest(TestCase):
def test_has_no_object_perm(self): def test_has_no_object_perm(self):
"""Regressiontest for #12462""" """Regressiontest for #12462"""
user = User.objects.get(username='test') user = self.UserModel.objects.get(username='test')
content_type=ContentType.objects.get_for_model(Group) content_type = ContentType.objects.get_for_model(Group)
perm = Permission.objects.create(name='test', content_type=content_type, codename='test') perm = Permission.objects.create(name='test', content_type=content_type, codename='test')
user.user_permissions.add(perm) user.user_permissions.add(perm)
user.save() user.save()
@ -98,9 +105,65 @@ class BackendTest(TestCase):
def test_get_all_superuser_permissions(self): def test_get_all_superuser_permissions(self):
"A superuser has all permissions. Refs #14795" "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())) 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): class TestObj(object):
pass pass
@ -108,7 +171,7 @@ class TestObj(object):
class SimpleRowlevelBackend(object): class SimpleRowlevelBackend(object):
def has_perm(self, user, perm, obj=None): def has_perm(self, user, perm, obj=None):
if not obj: if not obj:
return # We only support row level perms return # We only support row level perms
if isinstance(obj, TestObj): if isinstance(obj, TestObj):
if user.username == 'test2': if user.username == 'test2':
@ -126,7 +189,7 @@ class SimpleRowlevelBackend(object):
def get_all_permissions(self, user, obj=None): def get_all_permissions(self, user, obj=None):
if not obj: if not obj:
return [] # We only support row level perms return [] # We only support row level perms
if not isinstance(obj, TestObj): if not isinstance(obj, TestObj):
return ['none'] return ['none']
@ -140,7 +203,7 @@ class SimpleRowlevelBackend(object):
def get_group_permissions(self, user, obj=None): def get_group_permissions(self, user, obj=None):
if not obj: if not obj:
return # We only support row level perms return # We only support row level perms
if not isinstance(obj, TestObj): if not isinstance(obj, TestObj):
return ['none'] return ['none']
@ -151,6 +214,7 @@ class SimpleRowlevelBackend(object):
return ['none'] return ['none']
@skipIfCustomUser
class RowlevelBackendTest(TestCase): class RowlevelBackendTest(TestCase):
""" """
Tests for auth backend that supports object level permissions Tests for auth backend that supports object level permissions
@ -186,7 +250,6 @@ class RowlevelBackendTest(TestCase):
self.assertEqual(self.user2.get_all_permissions(), set([])) self.assertEqual(self.user2.get_all_permissions(), set([]))
def test_get_group_permissions(self): def test_get_group_permissions(self):
content_type=ContentType.objects.get_for_model(Group)
group = Group.objects.create(name='test_group') group = Group.objects.create(name='test_group')
self.user3.groups.add(group) self.user3.groups.add(group)
self.assertEqual(self.user3.get_group_permissions(TestObj()), set(['group_perm'])) 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'])) self.assertEqual(self.user1.get_all_permissions(TestObj()), set(['anon']))
@skipIfCustomUser
@override_settings(AUTHENTICATION_BACKENDS=[]) @override_settings(AUTHENTICATION_BACKENDS=[])
class NoBackendsTest(TestCase): class NoBackendsTest(TestCase):
""" """
@ -235,6 +299,7 @@ class NoBackendsTest(TestCase):
self.assertRaises(ImproperlyConfigured, self.user.has_perm, ('perm', TestObj(),)) self.assertRaises(ImproperlyConfigured, self.user.has_perm, ('perm', TestObj(),))
@skipIfCustomUser
class InActiveUserBackendTest(TestCase): class InActiveUserBackendTest(TestCase):
""" """
Tests for a inactive user Tests for a inactive user

View File

@ -1,13 +1,18 @@
import locale import locale
import traceback
from django.contrib.auth import get_user_model
from django.contrib.auth.management.commands import createsuperuser from django.contrib.auth.management.commands import createsuperuser
from django.contrib.auth.models import User, AnonymousUser 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.core.management import call_command
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings
from django.utils.six import StringIO from django.utils.six import StringIO
@skipIfCustomUser
class BasicTestCase(TestCase): class BasicTestCase(TestCase):
def test_user(self): def test_user(self):
"Check that users can be created and can set their password" "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 # Check API-based user creation with no password
u2 = User.objects.create_user('testuser2', 'test2@example.com') 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): def test_user_no_email(self):
"Check that users can be created without an email" "Check that users can be created without an email"
@ -98,7 +103,6 @@ class BasicTestCase(TestCase):
self.assertEqual(u.email, 'joe2@somewhere.org') self.assertEqual(u.email, 'joe2@somewhere.org')
self.assertFalse(u.has_usable_password()) self.assertFalse(u.has_usable_password())
new_io = StringIO() new_io = StringIO()
call_command("createsuperuser", call_command("createsuperuser",
interactive=False, interactive=False,
@ -124,15 +128,21 @@ class BasicTestCase(TestCase):
# Temporarily replace getpass to allow interactive code to be used # Temporarily replace getpass to allow interactive code to be used
# non-interactively # non-interactively
class mock_getpass: pass class mock_getpass:
pass
mock_getpass.getpass = staticmethod(lambda p=None: "nopasswd") mock_getpass.getpass = staticmethod(lambda p=None: "nopasswd")
createsuperuser.getpass = mock_getpass createsuperuser.getpass = mock_getpass
# Call the command in this new environment # Call the command in this new environment
new_io = StringIO() 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") self.fail("createsuperuser fails if the OS provides no information about the current locale")
finally: finally:
@ -143,3 +153,24 @@ class BasicTestCase(TestCase):
# If we were successful, a user should have been created # If we were successful, a user should have been created
u = User.objects.get(username="nolocale@somewhere.org") u = User.objects.get(username="nolocale@somewhere.org")
self.assertEqual(u.email, '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()

View File

@ -2,12 +2,65 @@ import os
from django.conf import global_settings from django.conf import global_settings
from django.contrib.auth import authenticate 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.db.models import Q
from django.template import context
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings 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( @override_settings(
TEMPLATE_DIRS=( TEMPLATE_DIRS=(
os.path.join(os.path.dirname(__file__), 'templates'), os.path.join(os.path.dirname(__file__), 'templates'),
@ -47,9 +100,28 @@ class AuthContextProcessorTests(TestCase):
self.assertContains(response, "Session accessed") self.assertContains(response, "Session accessed")
def test_perms_attrs(self): 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/') response = self.client.get('/auth_processor_perms/')
self.assertContains(response, "Has auth permissions") 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): def test_message_attrs(self):
self.client.login(username='super', password='secret') self.client.login(username='super', password='secret')

View File

@ -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'

View File

@ -1,7 +1,9 @@
from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.tests.views import AuthViewsTestCase from django.contrib.auth.tests.views import AuthViewsTestCase
from django.contrib.auth.tests.utils import skipIfCustomUser
@skipIfCustomUser
class LoginRequiredTestCase(AuthViewsTestCase): class LoginRequiredTestCase(AuthViewsTestCase):
""" """
Tests the login_required decorators Tests the login_required decorators

View File

@ -4,16 +4,17 @@ import os
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm, from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm,
PasswordChangeForm, SetPasswordForm, UserChangeForm, PasswordResetForm) PasswordChangeForm, SetPasswordForm, UserChangeForm, PasswordResetForm)
from django.contrib.auth.tests.utils import skipIfCustomUser
from django.core import mail from django.core import mail
from django.forms.fields import Field, EmailField from django.forms.fields import Field, EmailField
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils import six
from django.utils import translation from django.utils import translation
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@skipIfCustomUser
@override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
class UserCreationFormTest(TestCase): class UserCreationFormTest(TestCase):
@ -81,6 +82,7 @@ class UserCreationFormTest(TestCase):
self.assertEqual(repr(u), '<User: jsmith@example.com>') self.assertEqual(repr(u), '<User: jsmith@example.com>')
@skipIfCustomUser
@override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
class AuthenticationFormTest(TestCase): class AuthenticationFormTest(TestCase):
@ -133,6 +135,7 @@ class AuthenticationFormTest(TestCase):
self.assertEqual(form.non_field_errors(), []) self.assertEqual(form.non_field_errors(), [])
@skipIfCustomUser
@override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
class SetPasswordFormTest(TestCase): class SetPasswordFormTest(TestCase):
@ -160,6 +163,7 @@ class SetPasswordFormTest(TestCase):
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
@skipIfCustomUser
@override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
class PasswordChangeFormTest(TestCase): class PasswordChangeFormTest(TestCase):
@ -208,6 +212,7 @@ class PasswordChangeFormTest(TestCase):
['old_password', 'new_password1', 'new_password2']) ['old_password', 'new_password1', 'new_password2'])
@skipIfCustomUser
@override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
class UserChangeFormTest(TestCase): class UserChangeFormTest(TestCase):
@ -260,7 +265,25 @@ class UserChangeFormTest(TestCase):
self.assertIn(_("Invalid password format or unknown hashing algorithm."), self.assertIn(_("Invalid password format or unknown hashing algorithm."),
form.as_table()) 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',)) @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
class PasswordResetFormTest(TestCase): class PasswordResetFormTest(TestCase):
@ -338,4 +361,4 @@ class PasswordResetFormTest(TestCase):
form = PasswordResetForm(data) form = PasswordResetForm(data)
self.assertFalse(form.is_valid()) self.assertFalse(form.is_valid())
self.assertEqual(form["email"].errors, 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.")])

View File

@ -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'), [])

View File

@ -1,13 +1,21 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import date
from django.contrib.auth import models, management 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.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.core.management.base import CommandError
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings
from django.utils import six from django.utils import six
from django.utils.six import StringIO from django.utils.six import StringIO
@skipIfCustomUser
class GetDefaultUsernameTestCase(TestCase): class GetDefaultUsernameTestCase(TestCase):
def setUp(self): def setUp(self):
@ -36,6 +44,7 @@ class GetDefaultUsernameTestCase(TestCase):
self.assertEqual(management.get_default_username(), 'julia') self.assertEqual(management.get_default_username(), 'julia')
@skipIfCustomUser
class ChangepasswordManagementCommandTestCase(TestCase): class ChangepasswordManagementCommandTestCase(TestCase):
def setUp(self): def setUp(self):
@ -48,7 +57,7 @@ class ChangepasswordManagementCommandTestCase(TestCase):
self.stderr.close() self.stderr.close()
def test_that_changepassword_command_changes_joes_password(self): 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')) self.assertTrue(self.user.check_password('qwerty'))
command = changepassword.Command() command = changepassword.Command()
command._get_pass = lambda *args: 'not qwerty' command._get_pass = lambda *args: 'not qwerty'
@ -69,3 +78,133 @@ class ChangepasswordManagementCommandTestCase(TestCase):
with self.assertRaises(CommandError): with self.assertRaises(CommandError):
command.execute("joe", stdout=self.stdout, stderr=self.stderr) 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)

View File

@ -1,11 +1,13 @@
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import (Group, User, SiteProfileNotAvailable, from django.contrib.auth.models import (Group, User, SiteProfileNotAvailable,
UserManager) UserManager)
from django.contrib.auth.tests.utils import skipIfCustomUser
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.utils import six from django.utils import six
@skipIfCustomUser
@override_settings(USE_TZ=False, AUTH_PROFILE_MODULE='') @override_settings(USE_TZ=False, AUTH_PROFILE_MODULE='')
class ProfileTestCase(TestCase): class ProfileTestCase(TestCase):
@ -31,6 +33,7 @@ class ProfileTestCase(TestCase):
user.get_profile() user.get_profile()
@skipIfCustomUser
@override_settings(USE_TZ=False) @override_settings(USE_TZ=False)
class NaturalKeysTestCase(TestCase): class NaturalKeysTestCase(TestCase):
fixtures = ['authtestdata.json'] fixtures = ['authtestdata.json']
@ -45,6 +48,7 @@ class NaturalKeysTestCase(TestCase):
self.assertEqual(Group.objects.get_by_natural_key('users'), users_group) self.assertEqual(Group.objects.get_by_natural_key('users'), users_group)
@skipIfCustomUser
@override_settings(USE_TZ=False) @override_settings(USE_TZ=False)
class LoadDataWithoutNaturalKeysTestCase(TestCase): class LoadDataWithoutNaturalKeysTestCase(TestCase):
fixtures = ['regular.json'] fixtures = ['regular.json']
@ -55,6 +59,7 @@ class LoadDataWithoutNaturalKeysTestCase(TestCase):
self.assertEqual(group, user.groups.get()) self.assertEqual(group, user.groups.get())
@skipIfCustomUser
@override_settings(USE_TZ=False) @override_settings(USE_TZ=False)
class LoadDataWithNaturalKeysTestCase(TestCase): class LoadDataWithNaturalKeysTestCase(TestCase):
fixtures = ['natural.json'] fixtures = ['natural.json']
@ -65,6 +70,7 @@ class LoadDataWithNaturalKeysTestCase(TestCase):
self.assertEqual(group, user.groups.get()) self.assertEqual(group, user.groups.get())
@skipIfCustomUser
class UserManagerTestCase(TestCase): class UserManagerTestCase(TestCase):
def test_create_user(self): def test_create_user(self):

View File

@ -3,10 +3,12 @@ from datetime import datetime
from django.conf import settings from django.conf import settings
from django.contrib.auth.backends import RemoteUserBackend from django.contrib.auth.backends import RemoteUserBackend
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.tests.utils import skipIfCustomUser
from django.test import TestCase from django.test import TestCase
from django.utils import timezone from django.utils import timezone
@skipIfCustomUser
class RemoteUserTest(TestCase): class RemoteUserTest(TestCase):
urls = 'django.contrib.auth.tests.urls' urls = 'django.contrib.auth.tests.urls'
@ -106,6 +108,7 @@ class RemoteUserNoCreateBackend(RemoteUserBackend):
create_unknown_user = False create_unknown_user = False
@skipIfCustomUser
class RemoteUserNoCreateTest(RemoteUserTest): class RemoteUserNoCreateTest(RemoteUserTest):
""" """
Contains the same tests as RemoteUserTest, but using a custom auth backend Contains the same tests as RemoteUserTest, but using a custom auth backend
@ -142,6 +145,7 @@ class CustomRemoteUserBackend(RemoteUserBackend):
return user return user
@skipIfCustomUser
class RemoteUserCustomTest(RemoteUserTest): class RemoteUserCustomTest(RemoteUserTest):
""" """
Tests a custom RemoteUserBackend subclass that overrides the clean_username Tests a custom RemoteUserBackend subclass that overrides the clean_username

View File

@ -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 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',)) @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
class SignalTestCase(TestCase): class SignalTestCase(TestCase):
urls = 'django.contrib.auth.tests.urls' urls = 'django.contrib.auth.tests.urls'
@ -14,27 +18,41 @@ class SignalTestCase(TestCase):
def listener_logout(self, user, **kwargs): def listener_logout(self, user, **kwargs):
self.logged_out.append(user) self.logged_out.append(user)
def listener_login_failed(self, sender, credentials, **kwargs):
self.login_failed.append(credentials)
def setUp(self): def setUp(self):
"""Set up the listeners and reset the logged in/logged out counters""" """Set up the listeners and reset the logged in/logged out counters"""
self.logged_in = [] self.logged_in = []
self.logged_out = [] self.logged_out = []
self.login_failed = []
signals.user_logged_in.connect(self.listener_login) signals.user_logged_in.connect(self.listener_login)
signals.user_logged_out.connect(self.listener_logout) signals.user_logged_out.connect(self.listener_logout)
signals.user_login_failed.connect(self.listener_login_failed)
def tearDown(self): def tearDown(self):
"""Disconnect the listeners""" """Disconnect the listeners"""
signals.user_logged_in.disconnect(self.listener_login) signals.user_logged_in.disconnect(self.listener_login)
signals.user_logged_out.disconnect(self.listener_logout) signals.user_logged_out.disconnect(self.listener_logout)
signals.user_login_failed.disconnect(self.listener_login_failed)
def test_login(self): 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.client.login(username='testclient', password='bad')
self.assertEqual(len(self.logged_in), 0) 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: # Like this:
self.client.login(username='testclient', password='password') self.client.login(username='testclient', password='password')
self.assertEqual(len(self.logged_in), 1) self.assertEqual(len(self.logged_in), 1)
self.assertEqual(self.logged_in[0].username, 'testclient') 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): def test_logout_anonymous(self):
# The log_out function will still trigger the signal for anonymous # The log_out function will still trigger the signal for anonymous
# users. # users.
@ -47,3 +65,16 @@ class SignalTestCase(TestCase):
self.client.get('/logout/next_page/') self.client.get('/logout/next_page/')
self.assertEqual(len(self.logged_out), 1) self.assertEqual(len(self.logged_out), 1)
self.assertEqual(self.logged_out[0].username, 'testclient') 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)

View File

@ -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 %}

View File

@ -1 +1,4 @@
{% if perms.auth %}Has auth permissions{% endif %} {% 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 %}

View File

@ -1 +1 @@
E-mail sent Email sent

View File

@ -4,10 +4,12 @@ from datetime import date, timedelta
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.contrib.auth.tests.utils import skipIfCustomUser
from django.test import TestCase from django.test import TestCase
from django.utils import unittest from django.utils import unittest
@skipIfCustomUser
class TokenGeneratorTest(TestCase): class TokenGeneratorTest(TestCase):
def test_make_token(self): def test_make_token(self):

View File

@ -37,6 +37,10 @@ def auth_processor_perms(request):
return render_to_response('context_processors/auth_attrs_perms.html', return render_to_response('context_processors/auth_attrs_perms.html',
RequestContext(request, {}, processors=[context_processors.auth])) 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): def auth_processor_messages(request):
info(request, "Message 1") info(request, "Message 1")
return render_to_response('context_processors/auth_attrs_messages.html', 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'^logout/next_page/$', 'django.contrib.auth.views.logout', dict(next_page='/somewhere/')),
(r'^remote_user/$', remote_user_auth_view), (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'^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_required(password_reset)),
(r'^login_required_login_url/$', login_required(password_reset, login_url='/somewhere/')), (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_attr_access/$', auth_processor_attr_access),
(r'^auth_processor_user/$', auth_processor_user), (r'^auth_processor_user/$', auth_processor_user),
(r'^auth_processor_perms/$', auth_processor_perms), (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), (r'^auth_processor_messages/$', auth_processor_messages),
url(r'^userpage/(.+)/$', userpage, name="userpage"), url(r'^userpage/(.+)/$', userpage, name="userpage"),
) )

View File

@ -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)

View File

@ -5,6 +5,7 @@ from django.conf import global_settings, settings
from django.contrib.sites.models import Site, RequestSite from django.contrib.sites.models import Site, RequestSite
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core import mail from django.core import mail
from django.core.exceptions import SuspiciousOperation
from django.core.urlresolvers import reverse, NoReverseMatch from django.core.urlresolvers import reverse, NoReverseMatch
from django.http import QueryDict from django.http import QueryDict
from django.utils.encoding import force_text 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 import SESSION_KEY, REDIRECT_FIELD_NAME
from django.contrib.auth.forms import (AuthenticationForm, PasswordChangeForm, from django.contrib.auth.forms import (AuthenticationForm, PasswordChangeForm,
SetPasswordForm, PasswordResetForm) SetPasswordForm, PasswordResetForm)
from django.contrib.auth.tests.utils import skipIfCustomUser
@override_settings( @override_settings(
@ -50,6 +52,7 @@ class AuthViewsTestCase(TestCase):
return self.assertContains(response, escape(force_text(text)), **kwargs) return self.assertContains(response, escape(force_text(text)), **kwargs)
@skipIfCustomUser
class AuthViewNamedURLTests(AuthViewsTestCase): class AuthViewNamedURLTests(AuthViewsTestCase):
urls = 'django.contrib.auth.urls' urls = 'django.contrib.auth.urls'
@ -75,6 +78,7 @@ class AuthViewNamedURLTests(AuthViewsTestCase):
self.fail("Reversal of url named '%s' failed with NoReverseMatch" % name) self.fail("Reversal of url named '%s' failed with NoReverseMatch" % name)
@skipIfCustomUser
class PasswordResetTest(AuthViewsTestCase): class PasswordResetTest(AuthViewsTestCase):
def test_email_not_found(self): def test_email_not_found(self):
@ -100,6 +104,42 @@ class PasswordResetTest(AuthViewsTestCase):
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 1)
self.assertEqual("staffmember@example.com", mail.outbox[0].from_email) 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): def _test_confirm_start(self):
# Start by creating the email # Start by creating the email
response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'}) 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']) 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): class ChangePasswordTest(AuthViewsTestCase):
def fail_login(self, password='password'): def fail_login(self, password='password'):
@ -231,6 +295,7 @@ class ChangePasswordTest(AuthViewsTestCase):
self.assertTrue(response['Location'].endswith('/login/?next=/password_change/done/')) self.assertTrue(response['Location'].endswith('/login/?next=/password_change/done/'))
@skipIfCustomUser
class LoginTest(AuthViewsTestCase): class LoginTest(AuthViewsTestCase):
def test_current_site_in_context_after_login(self): def test_current_site_in_context_after_login(self):
@ -289,6 +354,7 @@ class LoginTest(AuthViewsTestCase):
"%s should be allowed" % good_url) "%s should be allowed" % good_url)
@skipIfCustomUser
class LoginURLSettings(AuthViewsTestCase): class LoginURLSettings(AuthViewsTestCase):
def setUp(self): def setUp(self):
@ -347,6 +413,7 @@ class LoginURLSettings(AuthViewsTestCase):
querystring.urlencode('/'))) querystring.urlencode('/')))
@skipIfCustomUser
class LogoutTest(AuthViewsTestCase): class LogoutTest(AuthViewsTestCase):
def confirm_logged_out(self): def confirm_logged_out(self):

View File

@ -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.crypto import constant_time_compare, salted_hmac
from django.utils import six from django.utils import six
class PasswordResetTokenGenerator(object): class PasswordResetTokenGenerator(object):
""" """
Strategy object used to generate and check tokens for the password Strategy object used to generate and check tokens for the password

View File

@ -15,10 +15,9 @@ from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect from django.views.decorators.csrf import csrf_protect
# Avoid shadowing the login() and logout() views below. # 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.decorators import login_required
from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm, SetPasswordForm, PasswordChangeForm 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.auth.tokens import default_token_generator
from django.contrib.sites.models import get_current_site 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, return TemplateResponse(request, template_name, context,
current_app=current_app) current_app=current_app)
def logout(request, next_page=None, def logout(request, next_page=None,
template_name='registration/logged_out.html', template_name='registration/logged_out.html',
redirect_field_name=REDIRECT_FIELD_NAME, 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. # Redirect to this page until the session has been cleared.
return HttpResponseRedirect(next_page or request.path) return HttpResponseRedirect(next_page or request.path)
def logout_then_login(request, login_url=None, current_app=None, extra_context=None): 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. 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) login_url = resolve_url(login_url)
return logout(request, login_url, current_app=current_app, extra_context=extra_context) return logout(request, login_url, current_app=current_app, extra_context=extra_context)
def redirect_to_login(next, login_url=None, def redirect_to_login(next, login_url=None,
redirect_field_name=REDIRECT_FIELD_NAME): redirect_field_name=REDIRECT_FIELD_NAME):
""" """
@ -128,6 +130,7 @@ def redirect_to_login(next, login_url=None,
return HttpResponseRedirect(urlunparse(login_url_parts)) return HttpResponseRedirect(urlunparse(login_url_parts))
# 4 views for password reset: # 4 views for password reset:
# - password_reset sends the mail # - password_reset sends the mail
# - password_reset_done shows a success message for the above # - password_reset_done shows a success message for the above
@ -160,7 +163,7 @@ def password_reset(request, is_admin_site=False,
'request': request, 'request': request,
} }
if is_admin_site: if is_admin_site:
opts = dict(opts, domain_override=request.META['HTTP_HOST']) opts = dict(opts, domain_override=request.get_host())
form.save(**opts) form.save(**opts)
return HttpResponseRedirect(post_reset_redirect) return HttpResponseRedirect(post_reset_redirect)
else: else:
@ -173,6 +176,7 @@ def password_reset(request, is_admin_site=False,
return TemplateResponse(request, template_name, context, return TemplateResponse(request, template_name, context,
current_app=current_app) current_app=current_app)
def password_reset_done(request, def password_reset_done(request,
template_name='registration/password_reset_done.html', template_name='registration/password_reset_done.html',
current_app=None, extra_context=None): current_app=None, extra_context=None):
@ -182,6 +186,7 @@ def password_reset_done(request,
return TemplateResponse(request, template_name, context, return TemplateResponse(request, template_name, context,
current_app=current_app) current_app=current_app)
# Doesn't need csrf_protect since no-one can guess the URL # Doesn't need csrf_protect since no-one can guess the URL
@sensitive_post_parameters() @sensitive_post_parameters()
@never_cache @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 View that checks the hash in a password reset link and presents a
form for entering a new password. 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: if post_reset_redirect is None:
post_reset_redirect = reverse('django.contrib.auth.views.password_reset_complete') post_reset_redirect = reverse('django.contrib.auth.views.password_reset_complete')
try: try:
uid_int = base36_to_int(uidb36) uid_int = base36_to_int(uidb36)
user = User.objects.get(id=uid_int) user = UserModel.objects.get(id=uid_int)
except (ValueError, OverflowError, User.DoesNotExist): except (ValueError, OverflowError, UserModel.DoesNotExist):
user = None user = None
if user is not None and token_generator.check_token(user, token): 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, return TemplateResponse(request, template_name, context,
current_app=current_app) current_app=current_app)
def password_reset_complete(request, def password_reset_complete(request,
template_name='registration/password_reset_complete.html', template_name='registration/password_reset_complete.html',
current_app=None, extra_context=None): current_app=None, extra_context=None):
@ -236,6 +243,7 @@ def password_reset_complete(request,
return TemplateResponse(request, template_name, context, return TemplateResponse(request, template_name, context,
current_app=current_app) current_app=current_app)
@sensitive_post_parameters() @sensitive_post_parameters()
@csrf_protect @csrf_protect
@login_required @login_required
@ -261,6 +269,7 @@ def password_change(request,
return TemplateResponse(request, template_name, context, return TemplateResponse(request, template_name, context,
current_app=current_app) current_app=current_app)
@login_required @login_required
def password_change_done(request, def password_change_done(request,
template_name='registration/password_change_done.html', template_name='registration/password_change_done.html',

View File

@ -1,11 +1,22 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib import admin from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.comments.models import Comment from django.contrib.comments.models import Comment
from django.utils.translation import ugettext_lazy as _, ungettext from django.utils.translation import ugettext_lazy as _, ungettext
from django.contrib.comments import get_model from django.contrib.comments import get_model
from django.contrib.comments.views.moderation import perform_flag, perform_approve, perform_delete 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): class CommentsAdmin(admin.ModelAdmin):
fieldsets = ( fieldsets = (
(None, (None,
@ -24,7 +35,7 @@ class CommentsAdmin(admin.ModelAdmin):
date_hierarchy = 'submit_date' date_hierarchy = 'submit_date'
ordering = ('-submit_date',) ordering = ('-submit_date',)
raw_id_fields = ('user',) 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"] actions = ["flag_comments", "approve_comments", "remove_comments"]
def get_actions(self, request): def get_actions(self, request):

View File

@ -1,30 +1,27 @@
from django.conf import settings
from django.contrib.syndication.views import Feed 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.contrib import comments
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
class LatestCommentFeed(Feed): class LatestCommentFeed(Feed):
"""Feed of latest comments on the current site.""" """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): def title(self):
if not hasattr(self, '_site'): return _("%(site_name)s comments") % dict(site_name=self.site.name)
self._site = Site.objects.get_current()
return _("%(site_name)s comments") % dict(site_name=self._site.name)
def link(self): def link(self):
if not hasattr(self, '_site'): return "http://%s/" % (self.site.domain)
self._site = Site.objects.get_current()
return "http://%s/" % (self._site.domain)
def description(self): def description(self):
if not hasattr(self, '_site'): return _("Latest comments on %(site_name)s") % dict(site_name=self.site.name)
self._site = Site.objects.get_current()
return _("Latest comments on %(site_name)s") % dict(site_name=self._site.name)
def items(self): def items(self):
qs = comments.get_model().objects.filter( qs = comments.get_model().objects.filter(
site__pk = settings.SITE_ID, site__pk = self.site.pk,
is_public = True, is_public = True,
is_removed = False, is_removed = False,
) )

View File

@ -4,7 +4,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Django\n" "Project-Id-Version: Django\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2010-05-13 15:35+0200\n"
"Last-Translator: Django team\n" "Last-Translator: Django team\n"
"Language-Team: English <en@li.org>\n" "Language-Team: English <en@li.org>\n"
@ -13,57 +13,57 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: admin.py:12 #: admin.py:25
msgid "Content" msgid "Content"
msgstr "" msgstr ""
#: admin.py:15 #: admin.py:28
msgid "Metadata" msgid "Metadata"
msgstr "" msgstr ""
#: admin.py:42 #: admin.py:55
msgid "flagged" msgid "flagged"
msgid_plural "flagged" msgid_plural "flagged"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: admin.py:43 #: admin.py:56
msgid "Flag selected comments" msgid "Flag selected comments"
msgstr "" msgstr ""
#: admin.py:47 #: admin.py:60
msgid "approved" msgid "approved"
msgid_plural "approved" msgid_plural "approved"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: admin.py:48 #: admin.py:61
msgid "Approve selected comments" msgid "Approve selected comments"
msgstr "" msgstr ""
#: admin.py:52 #: admin.py:65
msgid "removed" msgid "removed"
msgid_plural "removed" msgid_plural "removed"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: admin.py:53 #: admin.py:66
msgid "Remove selected comments" msgid "Remove selected comments"
msgstr "" msgstr ""
#: admin.py:65 #: admin.py:78
#, python-format #, python-format
msgid "1 comment was successfully %(action)s." msgid "1 comment was successfully %(action)s."
msgid_plural "%(count)s comments were successfully %(action)s." msgid_plural "%(count)s comments were successfully %(action)s."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: feeds.py:13 #: feeds.py:14
#, python-format #, python-format
msgid "%(site_name)s comments" msgid "%(site_name)s comments"
msgstr "" msgstr ""
#: feeds.py:23 #: feeds.py:20
#, python-format #, python-format
msgid "Latest comments on %(site_name)s" msgid "Latest comments on %(site_name)s"
msgstr "" msgstr ""
@ -100,78 +100,78 @@ msgid ""
"If you enter anything in this field your comment will be treated as spam" "If you enter anything in this field your comment will be treated as spam"
msgstr "" msgstr ""
#: models.py:22 #: models.py:23
msgid "content type" msgid "content type"
msgstr "" msgstr ""
#: models.py:24 #: models.py:25
msgid "object ID" msgid "object ID"
msgstr "" msgstr ""
#: models.py:50 models.py:168 #: models.py:53 models.py:177
msgid "user" msgid "user"
msgstr "" msgstr ""
#: models.py:52 #: models.py:55
msgid "user's name" msgid "user's name"
msgstr "" msgstr ""
#: models.py:53 #: models.py:56
msgid "user's email address" msgid "user's email address"
msgstr "" msgstr ""
#: models.py:54 #: models.py:57
msgid "user's URL" msgid "user's URL"
msgstr "" msgstr ""
#: models.py:56 models.py:76 models.py:169 #: models.py:59 models.py:79 models.py:178
msgid "comment" msgid "comment"
msgstr "" msgstr ""
#: models.py:59 #: models.py:62
msgid "date/time submitted" msgid "date/time submitted"
msgstr "" msgstr ""
#: models.py:60 #: models.py:63
msgid "IP address" msgid "IP address"
msgstr "" msgstr ""
#: models.py:61 #: models.py:64
msgid "is public" msgid "is public"
msgstr "" msgstr ""
#: models.py:62 #: models.py:65
msgid "" msgid ""
"Uncheck this box to make the comment effectively disappear from the site." "Uncheck this box to make the comment effectively disappear from the site."
msgstr "" msgstr ""
#: models.py:64 #: models.py:67
msgid "is removed" msgid "is removed"
msgstr "" msgstr ""
#: models.py:65 #: models.py:68
msgid "" msgid ""
"Check this box if the comment is inappropriate. A \"This comment has been " "Check this box if the comment is inappropriate. A \"This comment has been "
"removed\" message will be displayed instead." "removed\" message will be displayed instead."
msgstr "" msgstr ""
#: models.py:77 #: models.py:80
msgid "comments" msgid "comments"
msgstr "" msgstr ""
#: models.py:119 #: models.py:124
msgid "" msgid ""
"This comment was posted by an authenticated user and thus the name is read-" "This comment was posted by an authenticated user and thus the name is read-"
"only." "only."
msgstr "" msgstr ""
#: models.py:128 #: models.py:134
msgid "" msgid ""
"This comment was posted by an authenticated user and thus the email is read-" "This comment was posted by an authenticated user and thus the email is read-"
"only." "only."
msgstr "" msgstr ""
#: models.py:153 #: models.py:160
#, python-format #, python-format
msgid "" msgid ""
"Posted by %(user)s at %(date)s\n" "Posted by %(user)s at %(date)s\n"
@ -181,19 +181,19 @@ msgid ""
"http://%(domain)s%(url)s" "http://%(domain)s%(url)s"
msgstr "" msgstr ""
#: models.py:170 #: models.py:179
msgid "flag" msgid "flag"
msgstr "" msgstr ""
#: models.py:171 #: models.py:180
msgid "date" msgid "date"
msgstr "" msgstr ""
#: models.py:181 #: models.py:190
msgid "comment flag" msgid "comment flag"
msgstr "" msgstr ""
#: models.py:182 #: models.py:191
msgid "comment flags" msgid "comment flags"
msgstr "" msgstr ""

View File

@ -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.comments.managers import CommentManager
from django.contrib.contenttypes import generic from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.db import models
from django.core import urlresolvers from django.core import urlresolvers
from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone from django.utils import timezone
from django.conf import settings
from django.utils.encoding import python_2_unicode_compatible 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): class BaseCommentAbstractModel(models.Model):
""" """
@ -19,14 +19,14 @@ class BaseCommentAbstractModel(models.Model):
""" """
# Content-object field # Content-object field
content_type = models.ForeignKey(ContentType, content_type = models.ForeignKey(ContentType,
verbose_name=_('content type'), verbose_name=_('content type'),
related_name="content_type_set_for_%(class)s") 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") content_object = generic.GenericForeignKey(ct_field="content_type", fk_field="object_pk")
# Metadata about the comment # Metadata about the comment
site = models.ForeignKey(Site) site = models.ForeignKey(Site)
class Meta: class Meta:
abstract = True abstract = True
@ -40,6 +40,7 @@ class BaseCommentAbstractModel(models.Model):
args=(self.content_type_id, self.object_pk) args=(self.content_type_id, self.object_pk)
) )
@python_2_unicode_compatible @python_2_unicode_compatible
class Comment(BaseCommentAbstractModel): class Comment(BaseCommentAbstractModel):
""" """
@ -49,21 +50,21 @@ class Comment(BaseCommentAbstractModel):
# Who posted this comment? If ``user`` is set then it was an authenticated # 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 # user; otherwise at least user_name should have been set and the comment
# was posted by a non-authenticated user. # 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") blank=True, null=True, related_name="%(class)s_comments")
user_name = models.CharField(_("user's name"), max_length=50, 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_email = models.EmailField(_("user's email address"), blank=True)
user_url = models.URLField(_("user's URL"), blank=True) user_url = models.URLField(_("user's URL"), blank=True)
comment = models.TextField(_('comment'), max_length=COMMENT_MAX_LENGTH) comment = models.TextField(_('comment'), max_length=COMMENT_MAX_LENGTH)
# Metadata about the comment # Metadata about the comment
submit_date = models.DateTimeField(_('date/time submitted'), default=None) submit_date = models.DateTimeField(_('date/time submitted'), default=None)
ip_address = models.IPAddressField(_('IP address'), blank=True, null=True) ip_address = models.IPAddressField(_('IP address'), blank=True, null=True)
is_public = models.BooleanField(_('is public'), default=True, is_public = models.BooleanField(_('is public'), default=True,
help_text=_('Uncheck this box to make the comment effectively ' \ help_text=_('Uncheck this box to make the comment effectively ' \
'disappear from the site.')) '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. ' \ help_text=_('Check this box if the comment is inappropriate. ' \
'A "This comment has been removed" message will ' \ 'A "This comment has been removed" message will ' \
'be displayed instead.')) 'be displayed instead.'))
@ -95,9 +96,9 @@ class Comment(BaseCommentAbstractModel):
""" """
if not hasattr(self, "_userinfo"): if not hasattr(self, "_userinfo"):
userinfo = { userinfo = {
"name" : self.user_name, "name": self.user_name,
"email" : self.user_email, "email": self.user_email,
"url" : self.user_url "url": self.user_url
} }
if self.user_id: if self.user_id:
u = self.user u = self.user
@ -110,13 +111,14 @@ class Comment(BaseCommentAbstractModel):
if u.get_full_name(): if u.get_full_name():
userinfo["name"] = self.user.get_full_name() userinfo["name"] = self.user.get_full_name()
elif not self.user_name: elif not self.user_name:
userinfo["name"] = u.username userinfo["name"] = u.get_username()
self._userinfo = userinfo self._userinfo = userinfo
return self._userinfo return self._userinfo
userinfo = property(_get_userinfo, doc=_get_userinfo.__doc__) userinfo = property(_get_userinfo, doc=_get_userinfo.__doc__)
def _get_name(self): def _get_name(self):
return self.userinfo["name"] return self.userinfo["name"]
def _set_name(self, val): def _set_name(self, val):
if self.user_id: if self.user_id:
raise AttributeError(_("This comment was posted by an authenticated "\ raise AttributeError(_("This comment was posted by an authenticated "\
@ -126,6 +128,7 @@ class Comment(BaseCommentAbstractModel):
def _get_email(self): def _get_email(self):
return self.userinfo["email"] return self.userinfo["email"]
def _set_email(self, val): def _set_email(self, val):
if self.user_id: if self.user_id:
raise AttributeError(_("This comment was posted by an authenticated "\ raise AttributeError(_("This comment was posted by an authenticated "\
@ -135,6 +138,7 @@ class Comment(BaseCommentAbstractModel):
def _get_url(self): def _get_url(self):
return self.userinfo["url"] return self.userinfo["url"]
def _set_url(self, val): def _set_url(self, val):
self.user_url = val self.user_url = val
url = property(_get_url, _set_url, doc="The URL given by the user who posted this comment") 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 return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % d
@python_2_unicode_compatible @python_2_unicode_compatible
class CommentFlag(models.Model): 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; design users are only allowed to flag a comment with a given flag once;
if you want rating look elsewhere. if you want rating look elsewhere.
""" """
user = models.ForeignKey(User, verbose_name=_('user'), related_name="comment_flags") user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'), related_name="comment_flags")
comment = models.ForeignKey(Comment, verbose_name=_('comment'), related_name="flags") comment = models.ForeignKey(Comment, verbose_name=_('comment'), related_name="flags")
flag = models.CharField(_('flag'), max_length=30, db_index=True) flag = models.CharField(_('flag'), max_length=30, db_index=True)
flag_date = models.DateTimeField(_('date'), default=None) flag_date = models.DateTimeField(_('date'), default=None)
# Constants for flag types # Constants for flag types
@ -187,7 +192,7 @@ class CommentFlag(models.Model):
def __str__(self): def __str__(self):
return "%s flag of comment ID %s by %s" % \ 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): def save(self, *args, **kwargs):
if self.flag_date is None: if self.flag_date is None:

View File

@ -62,7 +62,7 @@ from django.contrib.comments import signals
from django.db.models.base import ModelBase from django.db.models.base import ModelBase
from django.template import Context, loader from django.template import Context, loader
from django.contrib import comments 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 from django.utils import timezone
class AlreadyModerated(Exception): class AlreadyModerated(Exception):
@ -240,7 +240,7 @@ class CommentModerator(object):
t = loader.get_template('comments/comment_notification_email.txt') t = loader.get_template('comments/comment_notification_email.txt')
c = Context({ 'comment': comment, c = Context({ 'comment': comment,
'content_object': content_object }) '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) content_object)
message = t.render(c) message = t.render(c)
send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipient_list, fail_silently=True) send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipient_list, fail_silently=True)

View File

@ -15,7 +15,6 @@ from django.views.decorators.csrf import csrf_protect
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
class CommentPostBadRequest(http.HttpResponseBadRequest): class CommentPostBadRequest(http.HttpResponseBadRequest):
""" """
Response returned when a comment post is invalid. If ``DEBUG`` is on a Response returned when a comment post is invalid. If ``DEBUG`` is on a
@ -27,6 +26,7 @@ class CommentPostBadRequest(http.HttpResponseBadRequest):
if settings.DEBUG: if settings.DEBUG:
self.content = render_to_string("comments/400-debug.html", {"why": why}) self.content = render_to_string("comments/400-debug.html", {"why": why})
@csrf_protect @csrf_protect
@require_POST @require_POST
def post_comment(request, next=None, using=None): def post_comment(request, next=None, using=None):
@ -40,7 +40,7 @@ def post_comment(request, next=None, using=None):
data = request.POST.copy() data = request.POST.copy()
if request.user.is_authenticated(): if request.user.is_authenticated():
if not data.get('name', ''): 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', ''): if not data.get('email', ''):
data["email"] = request.user.email data["email"] = request.user.email
@ -98,8 +98,8 @@ def post_comment(request, next=None, using=None):
] ]
return render_to_response( return render_to_response(
template_list, { template_list, {
"comment" : form.data.get("comment", ""), "comment": form.data.get("comment", ""),
"form" : form, "form": form,
"next": next, "next": next,
}, },
RequestContext(request, {}) RequestContext(request, {})
@ -113,9 +113,9 @@ def post_comment(request, next=None, using=None):
# Signal that the comment is about to be saved # Signal that the comment is about to be saved
responses = signals.comment_will_be_posted.send( responses = signals.comment_will_be_posted.send(
sender = comment.__class__, sender=comment.__class__,
comment = comment, comment=comment,
request = request request=request
) )
for (receiver, response) in responses: 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 # Save the comment and signal that it was saved
comment.save() comment.save()
signals.comment_was_posted.send( signals.comment_was_posted.send(
sender = comment.__class__, sender=comment.__class__,
comment = comment, comment=comment,
request = request request=request
) )
return next_redirect(data, next, comment_done, c=comment._get_pk_val()) return next_redirect(data, next, comment_done, c=comment._get_pk_val())
comment_done = confirmation_view( comment_done = confirmation_view(
template = "comments/posted.html", template="comments/posted.html",
doc = """Display a "comment was posted" success page.""" doc="""Display a "comment was posted" success page."""
) )

View File

@ -4,7 +4,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Django\n" "Project-Id-Version: Django\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2010-05-13 15:35+0200\n"
"Last-Translator: Django team\n" "Last-Translator: Django team\n"
"Language-Team: English <en@li.org>\n" "Language-Team: English <en@li.org>\n"
@ -13,29 +13,29 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: models.py:123 #: models.py:130
msgid "python model class name" msgid "python model class name"
msgstr "" msgstr ""
#: models.py:127 #: models.py:134
msgid "content type" msgid "content type"
msgstr "" msgstr ""
#: models.py:128 #: models.py:135
msgid "content types" msgid "content types"
msgstr "" msgstr ""
#: views.py:15 #: views.py:17
#, python-format #, python-format
msgid "Content type %(ct_id)s object has no associated model" msgid "Content type %(ct_id)s object has no associated model"
msgstr "" msgstr ""
#: views.py:19 #: views.py:21
#, python-format #, python-format
msgid "Content type %(ct_id)s object %(obj_id)s doesn't exist" msgid "Content type %(ct_id)s object %(obj_id)s doesn't exist"
msgstr "" msgstr ""
#: views.py:25 #: views.py:27
#, python-format #, python-format
msgid "%(ct_name)s objects don't have a get_absolute_url() method" msgid "%(ct_name)s objects don't have a get_absolute_url() method"
msgstr "" msgstr ""

View File

@ -3,7 +3,7 @@ from __future__ import unicode_literals
from django.db import models from django.db import models
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.views import shortcut 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.http import HttpRequest, Http404
from django.test import TestCase from django.test import TestCase
from django.utils.http import urlquote from django.utils.http import urlquote
@ -219,9 +219,8 @@ class ContentTypesTests(TestCase):
obj = FooWithUrl.objects.create(name="john") obj = FooWithUrl.objects.create(name="john")
if Site._meta.installed: if Site._meta.installed:
current_site = Site.objects.get_current()
response = shortcut(request, user_ct.id, obj.id) 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]) response._headers.get("location")[1])
Site._meta.installed = False Site._meta.installed = False

View File

@ -4,7 +4,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Django\n" "Project-Id-Version: Django\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2010-05-13 15:35+0200\n"
"Last-Translator: Django team\n" "Last-Translator: Django team\n"
"Language-Team: English <en@li.org>\n" "Language-Team: English <en@li.org>\n"
@ -17,7 +17,7 @@ msgstr ""
msgid "Advanced options" msgid "Advanced options"
msgstr "" msgstr ""
#: forms.py:7 models.py:7 #: forms.py:7 models.py:11
msgid "URL" msgid "URL"
msgstr "" msgstr ""
@ -45,40 +45,44 @@ msgstr ""
msgid "Flatpage with url %(url)s already exists for site %(site)s" msgid "Flatpage with url %(url)s already exists for site %(site)s"
msgstr "" msgstr ""
#: models.py:8 #: models.py:12
msgid "title" msgid "title"
msgstr "" msgstr ""
#: models.py:9 #: models.py:13
msgid "content" msgid "content"
msgstr "" msgstr ""
#: models.py:10 #: models.py:14
msgid "enable comments" msgid "enable comments"
msgstr "" msgstr ""
#: models.py:11 #: models.py:15
msgid "template name" msgid "template name"
msgstr "" msgstr ""
#: models.py:12 #: models.py:16
msgid "" msgid ""
"Example: 'flatpages/contact_page.html'. If this isn't provided, the system " "Example: 'flatpages/contact_page.html'. If this isn't provided, the system "
"will use 'flatpages/default.html'." "will use 'flatpages/default.html'."
msgstr "" msgstr ""
#: models.py:13 #: models.py:17
msgid "registration required" msgid "registration required"
msgstr "" msgstr ""
#: models.py:13 #: models.py:17
msgid "If this is checked, only logged-in users will be able to view the page." msgid "If this is checked, only logged-in users will be able to view the page."
msgstr "" msgstr ""
#: models.py:18 #: models.py:22
msgid "flat page" msgid "flat page"
msgstr "" msgstr ""
#: models.py:19 #: models.py:23
msgid "flat pages" msgid "flat pages"
msgstr "" msgstr ""
#: tests/forms.py:97
msgid "This field is required."
msgstr ""

View File

@ -1,6 +1,7 @@
from django import template from django import template
from django.conf import settings from django.conf import settings
from django.contrib.flatpages.models import FlatPage from django.contrib.flatpages.models import FlatPage
from django.contrib.sites.models import get_current_site
register = template.Library() register = template.Library()
@ -19,7 +20,11 @@ class FlatpageNode(template.Node):
self.user = None self.user = None
def render(self, context): 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 a prefix was specified, add a filter
if self.starts_with: if self.starts_with:
flatpages = flatpages.filter( flatpages = flatpages.filter(

View File

@ -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.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.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.utils.safestring import mark_safe
from django.views.decorators.csrf import csrf_protect from django.views.decorators.csrf import csrf_protect
@ -30,14 +31,15 @@ def flatpage(request, url):
""" """
if not url.startswith('/'): if not url.startswith('/'):
url = '/' + url url = '/' + url
site_id = get_current_site(request).id
try: try:
f = get_object_or_404(FlatPage, 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: except Http404:
if not url.endswith('/') and settings.APPEND_SLASH: if not url.endswith('/') and settings.APPEND_SLASH:
url += '/' url += '/'
f = get_object_or_404(FlatPage, 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) return HttpResponsePermanentRedirect('%s/' % request.path)
else: else:
raise raise

View File

@ -4,7 +4,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Django\n" "Project-Id-Version: Django\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2010-05-13 15:35+0200\n"
"Last-Translator: Django team\n" "Last-Translator: Django team\n"
"Language-Team: English <en@li.org>\n" "Language-Team: English <en@li.org>\n"

View File

@ -12,6 +12,7 @@ from django.conf import settings
from django.contrib.formtools import preview, utils from django.contrib.formtools import preview, utils
from django.contrib.formtools.wizard import FormWizard from django.contrib.formtools.wizard import FormWizard
from django.test import TestCase from django.test import TestCase
from django.test.html import parse_html
from django.test.utils import override_settings from django.test.utils import override_settings
from django.utils import unittest from django.utils import unittest
@ -218,7 +219,6 @@ class DummyRequest(http.HttpRequest):
) )
class WizardTests(TestCase): class WizardTests(TestCase):
urls = 'django.contrib.formtools.tests.urls' urls = 'django.contrib.formtools.tests.urls'
input_re = re.compile('name="([^"]+)" value="([^"]+)"')
wizard_step_data = ( wizard_step_data = (
{ {
'0-name': 'Pony', '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 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']} fields = {'wizard_step': response.context['step0']}
def grab(m): for input_field in previous_fields:
fields[m.group(1)] = m.group(2) input_attrs = dict(input_field.attributes)
return '' fields[input_attrs["name"]] = input_attrs["value"]
self.input_re.sub(grab, previous_fields)
return fields return fields
def check_wizard_step(self, response, step_no): def check_wizard_step(self, response, step_no):
@ -428,7 +427,6 @@ class WizardTests(TestCase):
""" """
step_count = len(self.wizard_step_data) step_count = len(self.wizard_step_data)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Step %d of %d' % (step_no, step_count)) self.assertContains(response, 'Step %d of %d' % (step_no, step_count))
data = self.grab_field_data(response) data = self.grab_field_data(response)

View File

@ -1,3 +1,5 @@
import json
from django.test import TestCase from django.test import TestCase
from django.core import signing from django.core import signing
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
@ -41,4 +43,5 @@ class TestCookieStorage(TestStorage, TestCase):
storage.init_data() storage.init_data()
storage.update_response(response) storage.update_response(response)
unsigned_cookie_data = cookie_signer.unsign(response.cookies[storage.prefix].value) 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": {}})

View File

@ -0,0 +1,6 @@
from django.utils import six
if six.PY3:
memoryview = memoryview
else:
memoryview = buffer

View File

@ -1,3 +1,5 @@
import logging
from django.forms.widgets import Textarea from django.forms.widgets import Textarea
from django.template import loader, Context from django.template import loader, Context
from django.templatetags.static import static 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 # Creating a template context that contains Django settings
# values needed by admin map templates. # values needed by admin map templates.
geo_context = Context({'LANGUAGE_BIDI' : translation.get_language_bidi()}) geo_context = Context({'LANGUAGE_BIDI' : translation.get_language_bidi()})
logger = logging.getLogger('django.contrib.gis')
class OpenLayersWidget(Textarea): class OpenLayersWidget(Textarea):
""" """
@ -29,7 +33,11 @@ class OpenLayersWidget(Textarea):
if isinstance(value, six.string_types): if isinstance(value, six.string_types):
try: try:
value = GEOSGeometry(value) value = GEOSGeometry(value)
except (GEOSException, ValueError): except (GEOSException, ValueError) as err:
logger.error(
"Error creating geometry from value '%s' (%s)" % (
value, err)
)
value = None value = None
if value and value.geom_type.upper() != self.geom_type: if value and value.geom_type.upper() != self.geom_type:
@ -56,7 +64,11 @@ class OpenLayersWidget(Textarea):
ogr = value.ogr ogr = value.ogr
ogr.transform(srid) ogr.transform(srid)
wkt = ogr.wkt 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 = '' wkt = ''
else: else:
wkt = value.wkt wkt = value.wkt

View File

@ -32,8 +32,9 @@ class BaseSpatialOperations(object):
# How the geometry column should be selected. # How the geometry column should be selected.
select = None select = None
# Does the spatial database have a geography type? # Does the spatial database have a geometry or geography type?
geography = False geography = False
geometry = False
area = False area = False
centroid = False centroid = False
@ -116,6 +117,16 @@ class BaseSpatialOperations(object):
""" """
raise NotImplementedError 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 # Spatial SQL Construction
def spatial_aggregate_sql(self, agg): def spatial_aggregate_sql(self, agg):
raise NotImplementedError('Aggregate support not implemented for this spatial backend.') raise NotImplementedError('Aggregate support not implemented for this spatial backend.')

View File

@ -44,7 +44,7 @@ class MySQLOperations(DatabaseOperations, BaseSpatialOperations):
modify the placeholder based on the contents of the given value. modify the placeholder based on the contents of the given value.
""" """
if hasattr(value, 'expression'): if hasattr(value, 'expression'):
placeholder = '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression])) placeholder = self.get_expression_column(value)
else: else:
placeholder = '%s(%%s)' % self.from_text placeholder = '%s(%%s)' % self.from_text
return placeholder return placeholder

View File

@ -213,7 +213,7 @@ class OracleOperations(DatabaseOperations, BaseSpatialOperations):
placeholder = '%s' placeholder = '%s'
# No geometry value used for F expression, substitue in # No geometry value used for F expression, substitue in
# the column name instead. # 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: else:
if transform_value(value, f.srid): if transform_value(value, f.srid):
return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (self.transform, value.srid, f.srid) return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (self.transform, value.srid, f.srid)

View File

@ -1,6 +1,7 @@
""" """
This object provides quoting for GEOS geometries into PostgreSQL/PostGIS. This object provides quoting for GEOS geometries into PostgreSQL/PostGIS.
""" """
from __future__ import unicode_literals
from psycopg2 import Binary from psycopg2 import Binary
from psycopg2.extensions import ISQLQuote from psycopg2.extensions import ISQLQuote
@ -10,7 +11,7 @@ class PostGISAdapter(object):
"Initializes on the geometry." "Initializes on the geometry."
# Getting the WKB (in string form, to allow easy pickling of # Getting the WKB (in string form, to allow easy pickling of
# the adaptor) and the SRID from the geometry. # the adaptor) and the SRID from the geometry.
self.ewkb = str(geom.ewkb) self.ewkb = bytes(geom.ewkb)
self.srid = geom.srid self.srid = geom.srid
self._adapter = Binary(self.ewkb) self._adapter = Binary(self.ewkb)
@ -39,7 +40,7 @@ class PostGISAdapter(object):
def getquoted(self): def getquoted(self):
"Returns a properly quoted string for use in PostgreSQL/PostGIS." "Returns a properly quoted string for use in PostgreSQL/PostGIS."
# psycopg will figure out whether to use E'\\000' or '\000' # 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): def prepare_database_save(self, unused):
return self return self

View File

@ -1,9 +1,11 @@
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation
class PostGISCreation(DatabaseCreation): class PostGISCreation(DatabaseCreation):
geom_index_type = 'GIST' 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): def sql_indexes_for_field(self, model, f, style):
"Return any spatial index creation SQL for the field." "Return any spatial index creation SQL for the field."
@ -16,8 +18,9 @@ class PostGISCreation(DatabaseCreation):
qn = self.connection.ops.quote_name qn = self.connection.ops.quote_name
db_table = model._meta.db_table db_table = model._meta.db_table
if f.geography: if f.geography or self.connection.ops.geometry:
# Geogrophy columns are created normally. # Geography and Geometry (PostGIS 2.0+) columns are
# created normally.
pass pass
else: else:
# Geometry columns are created by `AddGeometryColumn` # Geometry columns are created by `AddGeometryColumn`
@ -38,23 +41,31 @@ class PostGISCreation(DatabaseCreation):
style.SQL_FIELD(qn(f.column)) + style.SQL_FIELD(qn(f.column)) +
style.SQL_KEYWORD(' SET NOT NULL') + ';') style.SQL_KEYWORD(' SET NOT NULL') + ';')
if f.spatial_index: if f.spatial_index:
# Spatial indexes created the same way for both Geometry and # 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: 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: 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 ') + output.append(style.SQL_KEYWORD('CREATE INDEX ') +
style.SQL_TABLE(qn('%s_%s_id' % (db_table, f.column))) + style.SQL_TABLE(qn('%s_%s_id' % (db_table, f.column))) +
style.SQL_KEYWORD(' ON ') + style.SQL_KEYWORD(' ON ') +
style.SQL_TABLE(qn(db_table)) + style.SQL_TABLE(qn(db_table)) +
style.SQL_KEYWORD(' USING ') + style.SQL_KEYWORD(' USING ') +
style.SQL_COLTYPE(self.geom_index_type) + ' ( ' + 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 return output
def sql_table_creation_suffix(self): def sql_table_creation_suffix(self):
qn = self.connection.ops.quote_name postgis_template = getattr(settings, 'POSTGIS_TEMPLATE', 'template_postgis')
return ' TEMPLATE %s' % qn(getattr(settings, 'POSTGIS_TEMPLATE', 'template_postgis')) return ' TEMPLATE %s' % self.connection.ops.quote_name(postgis_template)

View File

@ -103,11 +103,12 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
self.geom_func_prefix = prefix self.geom_func_prefix = prefix
self.spatial_version = version self.spatial_version = version
except DatabaseError: except DatabaseError:
raise ImproperlyConfigured('Cannot determine PostGIS version for database "%s". ' raise ImproperlyConfigured(
'GeoDjango requires at least PostGIS version 1.3. ' 'Cannot determine PostGIS version for database "%s". '
'Was the database created from a spatial database ' 'GeoDjango requires at least PostGIS version 1.3. '
'template?' % self.connection.settings_dict['NAME'] 'Was the database created from a spatial database '
) 'template?' % self.connection.settings_dict['NAME']
)
# TODO: Raise helpful exceptions as they become known. # TODO: Raise helpful exceptions as they become known.
# PostGIS-specific operators. The commented descriptions of these # PostGIS-specific operators. The commented descriptions of these
@ -215,6 +216,10 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
'bboverlaps' : PostGISOperator('&&'), '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. # Creating a dictionary lookup of all GIS terms for PostGIS.
gis_terms = ['isnull'] gis_terms = ['isnull']
gis_terms += list(self.geometry_operators) gis_terms += list(self.geometry_operators)
@ -231,7 +236,6 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
self.distance_spheroid = prefix + 'distance_spheroid' self.distance_spheroid = prefix + 'distance_spheroid'
self.envelope = prefix + 'Envelope' self.envelope = prefix + 'Envelope'
self.extent = prefix + 'Extent' self.extent = prefix + 'Extent'
self.extent3d = prefix + 'Extent3D'
self.force_rhr = prefix + 'ForceRHR' self.force_rhr = prefix + 'ForceRHR'
self.geohash = GEOHASH self.geohash = GEOHASH
self.geojson = GEOJSON self.geojson = GEOJSON
@ -239,14 +243,12 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
self.intersection = prefix + 'Intersection' self.intersection = prefix + 'Intersection'
self.kml = prefix + 'AsKML' self.kml = prefix + 'AsKML'
self.length = prefix + 'Length' self.length = prefix + 'Length'
self.length3d = prefix + 'Length3D'
self.length_spheroid = prefix + 'length_spheroid' self.length_spheroid = prefix + 'length_spheroid'
self.makeline = prefix + 'MakeLine' self.makeline = prefix + 'MakeLine'
self.mem_size = prefix + 'mem_size' self.mem_size = prefix + 'mem_size'
self.num_geom = prefix + 'NumGeometries' self.num_geom = prefix + 'NumGeometries'
self.num_points =prefix + 'npoints' self.num_points =prefix + 'npoints'
self.perimeter = prefix + 'Perimeter' self.perimeter = prefix + 'Perimeter'
self.perimeter3d = prefix + 'Perimeter3D'
self.point_on_surface = prefix + 'PointOnSurface' self.point_on_surface = prefix + 'PointOnSurface'
self.polygonize = prefix + 'Polygonize' self.polygonize = prefix + 'Polygonize'
self.reverse = prefix + 'Reverse' self.reverse = prefix + 'Reverse'
@ -259,6 +261,15 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
self.union = prefix + 'Union' self.union = prefix + 'Union'
self.unionagg = 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): def check_aggregate_support(self, aggregate):
""" """
Checks if the given aggregate name is supported (that is, if it's 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.') 'only with an SRID of 4326.')
return 'geography(%s,%d)'% (f.geom_type, f.srid) 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: else:
return None return None
@ -375,7 +394,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
# If this is an F expression, then we don't really want # If this is an F expression, then we don't really want
# a placeholder and instead substitute in the column # a placeholder and instead substitute in the column
# of the expression. # 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 return placeholder

View File

@ -146,6 +146,8 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations):
except DatabaseError: except DatabaseError:
# we are using < 2.4.0-RC4 # we are using < 2.4.0-RC4
pass pass
if version >= (3, 0, 0):
self.geojson = 'AsGeoJSON'
def check_aggregate_support(self, aggregate): def check_aggregate_support(self, aggregate):
""" """
@ -208,7 +210,7 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations):
placeholder = '%s' placeholder = '%s'
# No geometry value used for F expression, substitue in # No geometry value used for F expression, substitue in
# the column name instead. # 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: else:
if transform_value(value, f.srid): if transform_value(value, f.srid):
# Adding Transform() to the SQL placeholder. # Adding Transform() to the SQL placeholder.

View File

@ -95,7 +95,7 @@ class GeometryField(Field):
# Is this a geography rather than a geometry column? # Is this a geography rather than a geometry column?
self.geography = geography 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` # `USER_SDO_GEOM_METADATA`
self._extent = kwargs.pop('extent', (-180.0, -90.0, 180.0, 90.0)) self._extent = kwargs.pop('extent', (-180.0, -90.0, 180.0, 90.0))
self._tolerance = kwargs.pop('tolerance', 0.05) self._tolerance = kwargs.pop('tolerance', 0.05)
@ -160,7 +160,7 @@ class GeometryField(Field):
# from the given string input. # from the given string input.
if isinstance(geom, Geometry): if isinstance(geom, Geometry):
pass 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: try:
geom = Geometry(geom) geom = Geometry(geom)
except GeometryException: except GeometryException:

View File

@ -5,6 +5,7 @@ corresponding to geographic model fields.
Thanks to Robert Coup for providing this functionality (see #4322). Thanks to Robert Coup for providing this functionality (see #4322).
""" """
from django.contrib.gis import memoryview
from django.utils import six from django.utils import six
class GeometryProxy(object): 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'): if isinstance(value, self._klass) and (str(value.geom_type).upper() == gtype or gtype == 'GEOMETRY'):
# Assigning the SRID to the geometry. # Assigning the SRID to the geometry.
if value.srid is None: value.srid = self._field.srid 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 # Set with None, WKT, HEX, or WKB
pass pass
else: else:

View File

@ -1,6 +1,7 @@
from django.db import connections from django.db import connections
from django.db.models.query import QuerySet, ValuesQuerySet, ValuesListQuerySet 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 import aggregates
from django.contrib.gis.db.models.fields import get_srid_info, PointField, LineStringField 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 from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery
@ -145,13 +146,14 @@ class GeoQuerySet(QuerySet):
""" """
backend = connections[self.db].ops backend = connections[self.db].ops
if not backend.geojson: 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): if not isinstance(precision, six.integer_types):
raise TypeError('Precision keyword must be set with an integer.') raise TypeError('Precision keyword must be set with an integer.')
# Setting the options flag -- which depends on which version of # 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): if backend.spatial_version >= (1, 4, 0):
options = 0 options = 0
if crs and bbox: options = 3 if crs and bbox: options = 3
@ -193,9 +195,9 @@ class GeoQuerySet(QuerySet):
# PostGIS AsGML() aggregate function parameter order depends on the # PostGIS AsGML() aggregate function parameter order depends on the
# version -- uggh. # version -- uggh.
if backend.spatial_version > (1, 3, 1): 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: 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} s['procedure_args'] = {'precision' : precision, 'version' : version}
return self._spatial_attribute('gml', s, **kwargs) return self._spatial_attribute('gml', s, **kwargs)
@ -676,7 +678,7 @@ class GeoQuerySet(QuerySet):
if not backend.geography: if not backend.geography:
if not isinstance(geo_field, PointField): if not isinstance(geo_field, PointField):
raise ValueError('Spherical distance calculation only supported on PointFields.') 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') raise ValueError('Spherical distance calculation only supported with Point Geometry parameters')
# The `function` procedure argument needs to be set differently for # The `function` procedure argument needs to be set differently for
# geodetic distance calculations. # geodetic distance calculations.

View File

@ -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.utils.six.moves import zip
from django.db.backends.util import truncate_name, typecast_timestamp from django.db.backends.util import truncate_name, typecast_timestamp
@ -114,10 +119,10 @@ class GeoSQLCompiler(compiler.SQLCompiler):
result = [] result = []
if opts is None: if opts is None:
opts = self.query.model._meta opts = self.query.model._meta
# Skip all proxy to the root proxied model
opts = opts.concrete_model._meta
aliases = set() aliases = set()
only_load = self.deferred_to_columns() only_load = self.deferred_to_columns()
# Skip all proxy to the root proxied model
proxied_model = opts.concrete_model
if start_alias: if start_alias:
seen = {None: start_alias} seen = {None: start_alias}
@ -128,12 +133,9 @@ class GeoSQLCompiler(compiler.SQLCompiler):
try: try:
alias = seen[model] alias = seen[model]
except KeyError: except KeyError:
if model is proxied_model: link_field = opts.get_ancestor_link(model)
alias = start_alias alias = self.query.join((start_alias, model._meta.db_table,
else: 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 seen[model] = alias
else: else:
# If we're starting from the base model of the queryset, the # 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): 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 # We resolve the rest of the columns if we're on Oracle or if
# the `geo_values` attribute is defined. # 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)) values.append(self.query.convert_values(value, field, self.connection))
else: else:
values.extend(row[index_start:]) values.extend(row[index_start:])

View File

@ -37,11 +37,11 @@
try: try:
from django.contrib.gis.gdal.driver import Driver from django.contrib.gis.gdal.driver import Driver
from django.contrib.gis.gdal.datasource import DataSource 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.srs import SpatialReference, CoordTransform
from django.contrib.gis.gdal.geometries import OGRGeometry from django.contrib.gis.gdal.geometries import OGRGeometry
HAS_GDAL = True HAS_GDAL = True
except: except Exception:
HAS_GDAL = False HAS_GDAL = False
try: try:

View File

@ -45,6 +45,7 @@ from django.contrib.gis.gdal.layer import Layer
# Getting the ctypes prototypes for the DataSource. # Getting the ctypes prototypes for the DataSource.
from django.contrib.gis.gdal.prototypes import ds as capi 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 import six
from django.utils.six.moves import xrange from django.utils.six.moves import xrange
@ -56,12 +57,14 @@ class DataSource(GDALBase):
"Wraps an OGR Data Source object." "Wraps an OGR Data Source object."
#### Python 'magic' routines #### #### 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. # The write flag.
if write: if write:
self._write = 1 self._write = 1
else: else:
self._write = 0 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 # Registering all the drivers, this needs to be done
# _before_ we try to open up a data source. # _before_ we try to open up a data source.
@ -73,7 +76,7 @@ class DataSource(GDALBase):
ds_driver = Driver.ptr_type() ds_driver = Driver.ptr_type()
try: try:
# OGROpen will auto-detect the data source type. # 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: except OGRException:
# Making the error message more clear rather than something # Making the error message more clear rather than something
# like "Invalid pointer returned from OGROpen". # like "Invalid pointer returned from OGROpen".
@ -102,7 +105,7 @@ class DataSource(GDALBase):
def __getitem__(self, index): def __getitem__(self, index):
"Allows use of the index [] operator to get a layer at the index." "Allows use of the index [] operator to get a layer at the index."
if isinstance(index, six.string_types): 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) if not l: raise OGRIndexError('invalid OGR Layer name given: "%s"' % index)
elif isinstance(index, int): elif isinstance(index, int):
if index < 0 or index >= self.layer_count: if index < 0 or index >= self.layer_count:
@ -128,4 +131,5 @@ class DataSource(GDALBase):
@property @property
def name(self): def name(self):
"Returns the name of the data source." "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)

View File

@ -5,6 +5,7 @@ from django.contrib.gis.gdal.error import OGRException
from django.contrib.gis.gdal.prototypes import ds as capi from django.contrib.gis.gdal.prototypes import ds as capi
from django.utils import six from django.utils import six
from django.utils.encoding import force_bytes
# For more information, see the OGR C API source code: # For more information, see the OGR C API source code:
# http://www.gdal.org/ogr/ogr__api_8h.html # http://www.gdal.org/ogr/ogr__api_8h.html
@ -36,7 +37,7 @@ class Driver(GDALBase):
name = dr_input name = dr_input
# Attempting to get the OGR driver by the string name. # 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): elif isinstance(dr_input, int):
self._register() self._register()
dr = capi.get_driver(dr_input) dr = capi.get_driver(dr_input)

View File

@ -52,7 +52,7 @@ class Envelope(object):
elif len(args) == 4: elif len(args) == 4:
# Individual parameters passed in. # Individual parameters passed in.
# Thanks to ww for the help # Thanks to ww for the help
self._from_sequence(map(float, args)) self._from_sequence([float(a) for a in args])
else: else:
raise OGRException('Incorrect number (%d) of arguments.' % len(args)) raise OGRException('Incorrect number (%d) of arguments.' % len(args))

Some files were not shown because too many files have changed in this diff Show More