Merge remote-tracking branch 'core/master' into schema-alteration
Conflicts: django/db/models/loading.py django/db/models/options.py
This commit is contained in:
commit
b62e82365a
|
@ -0,0 +1,2 @@
|
|||
# Normalize line endings to avoid spurious failures in the core test suite on Windows.
|
||||
*html text eol=lf
|
6
AUTHORS
6
AUTHORS
|
@ -91,6 +91,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
James Bennett
|
||||
Danilo Bargen
|
||||
Shai Berger <shai@platonix.com>
|
||||
berto
|
||||
Julian Bez
|
||||
Arvis Bickovskis <viestards.lists@gmail.com>
|
||||
Natalia Bidart <nataliabidart@gmail.com>
|
||||
|
@ -231,6 +232,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Simon Greenhill <dev@simon.net.nz>
|
||||
Owen Griffiths
|
||||
Espen Grindhaug <http://grindhaug.org/>
|
||||
Mike Grouchy <http://mikegrouchy.com/>
|
||||
Janos Guljas
|
||||
Thomas Güttler <hv@tbz-pariv.de>
|
||||
Horst Gutmann <zerok@zerokspot.com>
|
||||
|
@ -380,6 +382,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Christian Metts
|
||||
michal@plovarna.cz
|
||||
Slawek Mikula <slawek dot mikula at gmail dot com>
|
||||
Katie Miller <katie@sub50.com>
|
||||
Shawn Milochik <shawn@milochik.com>
|
||||
mitakummaa@gmail.com
|
||||
Taylor Mitchell <taylor.mitchell@gmail.com>
|
||||
|
@ -510,6 +513,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Johan C. Stöver <johan@nilling.nl>
|
||||
Nowell Strite <http://nowell.strite.org/>
|
||||
Thomas Stromberg <tstromberg@google.com>
|
||||
Ben Sturmfels <ben@sturm.com.au>
|
||||
Travis Swicegood <travis@domain51.com>
|
||||
Pascal Varet
|
||||
SuperJared
|
||||
|
@ -528,6 +532,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Terry Huang <terryh.tp@gmail.com>
|
||||
Travis Terry <tdterry7@gmail.com>
|
||||
thebjorn <bp@datakortet.no>
|
||||
Lowe Thiderman <lowe.thiderman@gmail.com>
|
||||
Zach Thompson <zthompson47@gmail.com>
|
||||
Michael Thornhill <michael.thornhill@gmail.com>
|
||||
Deepak Thukral <deep.thukral@gmail.com>
|
||||
|
@ -585,6 +590,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Gasper Zejn <zejn@kiberpipa.org>
|
||||
Jarek Zgoda <jarek.zgoda@gmail.com>
|
||||
Cheng Zhang
|
||||
Hannes Struß <x@hannesstruss.de>
|
||||
|
||||
A big THANK YOU goes to:
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
VERSION = (1, 5, 0, 'alpha', 0)
|
||||
VERSION = (1, 6, 0, 'alpha', 0)
|
||||
|
||||
def get_version(*args, **kwargs):
|
||||
# Don't litter django/__init__.py with all the get_version stuff.
|
||||
|
|
|
@ -7,7 +7,13 @@ Can be run as a cronjob to clean out old data from the database (only expired
|
|||
sessions at the moment).
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
from django.core import management
|
||||
|
||||
if __name__ == "__main__":
|
||||
management.call_command('cleanup')
|
||||
warnings.warn(
|
||||
"The `daily_cleanup` script has been deprecated "
|
||||
"in favor of `django-admin.py clearsessions`.",
|
||||
PendingDeprecationWarning)
|
||||
management.call_command('clearsessions')
|
||||
|
|
|
@ -6,6 +6,7 @@ variable, and then from django.conf.global_settings; see the global settings fil
|
|||
a list of all possible variables.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time # Needed for Windows
|
||||
import warnings
|
||||
|
@ -55,6 +56,15 @@ class LazySettings(LazyObject):
|
|||
"""
|
||||
Setup logging from LOGGING_CONFIG and LOGGING settings.
|
||||
"""
|
||||
try:
|
||||
# Route warnings through python logging
|
||||
logging.captureWarnings(True)
|
||||
# Allow DeprecationWarnings through the warnings filters
|
||||
warnings.simplefilter("default", DeprecationWarning)
|
||||
except AttributeError:
|
||||
# No captureWarnings on Python 2.6, DeprecationWarnings are on anyway
|
||||
pass
|
||||
|
||||
if self.LOGGING_CONFIG:
|
||||
from django.utils.log import DEFAULT_LOGGING
|
||||
# First find the logging configuration function ...
|
||||
|
@ -83,6 +93,7 @@ class LazySettings(LazyObject):
|
|||
for name, value in options.items():
|
||||
setattr(holder, name, value)
|
||||
self._wrapped = holder
|
||||
self._configure_logging()
|
||||
|
||||
@property
|
||||
def configured(self):
|
||||
|
@ -99,9 +110,6 @@ class BaseSettings(object):
|
|||
def __setattr__(self, name, value):
|
||||
if name in ("MEDIA_URL", "STATIC_URL") and value and not value.endswith('/'):
|
||||
raise ImproperlyConfigured("If set, %s must end with a slash" % name)
|
||||
elif name == "ADMIN_MEDIA_PREFIX":
|
||||
warnings.warn("The ADMIN_MEDIA_PREFIX setting has been removed; "
|
||||
"use STATIC_URL instead.", DeprecationWarning)
|
||||
elif name == "ALLOWED_INCLUDE_ROOTS" and isinstance(value, six.string_types):
|
||||
raise ValueError("The ALLOWED_INCLUDE_ROOTS setting must be set "
|
||||
"to a tuple, not a string.")
|
||||
|
|
|
@ -150,12 +150,8 @@ SERVER_EMAIL = 'root@localhost'
|
|||
# Whether to send broken-link emails.
|
||||
SEND_BROKEN_LINK_EMAILS = False
|
||||
|
||||
# Database connection info.
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.dummy',
|
||||
},
|
||||
}
|
||||
# Database connection info. If left empty, will default to the dummy backend.
|
||||
DATABASES = {}
|
||||
|
||||
# Classes used to implement DB routing behavior.
|
||||
DATABASE_ROUTERS = []
|
||||
|
@ -449,6 +445,7 @@ MIDDLEWARE_CLASSES = (
|
|||
# SESSIONS #
|
||||
############
|
||||
|
||||
SESSION_CACHE_ALIAS = 'default' # Cache to store session data if using the cache session backend.
|
||||
SESSION_COOKIE_NAME = 'sessionid' # Cookie name. This can be whatever you want.
|
||||
SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2 # Age of cookie, in seconds (default: 2 weeks).
|
||||
SESSION_COOKIE_DOMAIN = None # A string like ".example.com", or None for standard domain cookie.
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
# About name_local: capitalize it as if your language name was appearing
|
||||
# inside a sentence in your language.
|
||||
|
||||
LANG_INFO = {
|
||||
'ar': {
|
||||
'bidi': True,
|
||||
|
@ -53,7 +56,7 @@ LANG_INFO = {
|
|||
'bidi': False,
|
||||
'code': 'da',
|
||||
'name': 'Danish',
|
||||
'name_local': 'Dansk',
|
||||
'name_local': 'dansk',
|
||||
},
|
||||
'de': {
|
||||
'bidi': False,
|
||||
|
@ -137,7 +140,7 @@ LANG_INFO = {
|
|||
'bidi': False,
|
||||
'code': 'fr',
|
||||
'name': 'French',
|
||||
'name_local': 'Fran\xe7ais',
|
||||
'name_local': 'fran\xe7ais',
|
||||
},
|
||||
'fy-nl': {
|
||||
'bidi': False,
|
||||
|
@ -269,7 +272,7 @@ LANG_INFO = {
|
|||
'bidi': False,
|
||||
'code': 'nb',
|
||||
'name': 'Norwegian Bokmal',
|
||||
'name_local': 'Norsk (bokm\xe5l)',
|
||||
'name_local': 'norsk (bokm\xe5l)',
|
||||
},
|
||||
'ne': {
|
||||
'bidi': False,
|
||||
|
@ -287,13 +290,13 @@ LANG_INFO = {
|
|||
'bidi': False,
|
||||
'code': 'nn',
|
||||
'name': 'Norwegian Nynorsk',
|
||||
'name_local': 'Norsk (nynorsk)',
|
||||
'name_local': 'norsk (nynorsk)',
|
||||
},
|
||||
'no': {
|
||||
'bidi': False,
|
||||
'code': 'no',
|
||||
'name': 'Norwegian',
|
||||
'name_local': 'Norsk',
|
||||
'name_local': 'norsk',
|
||||
},
|
||||
'pa': {
|
||||
'bidi': False,
|
||||
|
@ -365,7 +368,7 @@ LANG_INFO = {
|
|||
'bidi': False,
|
||||
'code': 'sv',
|
||||
'name': 'Swedish',
|
||||
'name_local': 'Svenska',
|
||||
'name_local': 'svenska',
|
||||
},
|
||||
'sw': {
|
||||
'bidi': False,
|
||||
|
|
|
@ -19,10 +19,6 @@ DATE_INPUT_FORMATS = (
|
|||
# '31/12/2009', '31/12/09'
|
||||
'%d/%m/%Y', '%d/%m/%y'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
# '14:30:59', '14:30'
|
||||
'%H:%M:%S', '%H:%M'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%d/%m/%Y %H:%M:%S',
|
||||
'%d/%m/%Y %H:%M',
|
||||
|
|
|
@ -17,21 +17,26 @@ FIRST_DAY_OF_WEEK = 1 # Monday
|
|||
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
|
||||
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
DATE_INPUT_FORMATS = (
|
||||
'%d.%m.%Y', '%d.%m.%y', # '25.10.2006', '25.10.06'
|
||||
'%Y-%m-%d', '%y-%m-%d', # '2006-10-25', '06-10-25'
|
||||
'%d.%m.%Y', '%d.%m.%y', # '05.01.2006', '05.01.06'
|
||||
'%d. %m. %Y', '%d. %m. %y', # '5. 1. 2006', '5. 1. 06'
|
||||
# '%d. %B %Y', '%d. %b. %Y', # '25. October 2006', '25. Oct. 2006'
|
||||
)
|
||||
# Kept ISO formats as one is in first position
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
'%H:%M:%S', # '04:30:59'
|
||||
'%H.%M', # '04.30'
|
||||
'%H:%M', # '04:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%d.%m.%Y %H:%M:%S', # '25.10.2006 14:30:59'
|
||||
'%d.%m.%Y %H:%M', # '25.10.2006 14:30'
|
||||
'%d.%m.%Y', # '25.10.2006'
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
'%d.%m.%Y %H:%M:%S', # '05.01.2006 04:30:59'
|
||||
'%d.%m.%Y %H.%M', # '05.01.2006 04.30'
|
||||
'%d.%m.%Y %H:%M', # '05.01.2006 04:30'
|
||||
'%d.%m.%Y', # '05.01.2006'
|
||||
'%d. %m. %Y %H:%M:%S', # '05. 01. 2006 04:30:59'
|
||||
'%d. %m. %Y %H.%M', # '05. 01. 2006 04.30'
|
||||
'%d. %m. %Y %H:%M', # '05. 01. 2006 04:30'
|
||||
'%d. %m. %Y', # '05. 01. 2006'
|
||||
'%Y-%m-%d %H.%M', # '2006-01-05 04.30'
|
||||
)
|
||||
DECIMAL_SEPARATOR = ','
|
||||
THOUSAND_SEPARATOR = '\xa0' # non-breaking space
|
||||
|
|
|
@ -18,10 +18,6 @@ FIRST_DAY_OF_WEEK = 1
|
|||
DATE_INPUT_FORMATS = (
|
||||
'%d.%m.%Y', # '25.10.2006'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%d.%m.%Y %H:%M:%S', # '25.10.2006 14:30:59'
|
||||
'%d.%m.%Y %H:%M', # '25.10.2006 14:30'
|
||||
|
|
|
@ -17,20 +17,12 @@ FIRST_DAY_OF_WEEK = 1 # Monday
|
|||
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
DATE_INPUT_FORMATS = (
|
||||
'%d.%m.%Y', '%d.%m.%y', # '25.10.2006', '25.10.06'
|
||||
'%Y-%m-%d', '%y-%m-%d', # '2006-10-25', '06-10-25'
|
||||
# '%d. %B %Y', '%d. %b. %Y', # '25. October 2006', '25. Oct. 2006'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%d.%m.%Y %H:%M:%S', # '25.10.2006 14:30:59'
|
||||
'%d.%m.%Y %H:%M', # '25.10.2006 14:30'
|
||||
'%d.%m.%Y', # '25.10.2006'
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
)
|
||||
DECIMAL_SEPARATOR = ','
|
||||
THOUSAND_SEPARATOR = '.'
|
||||
|
|
|
@ -19,20 +19,12 @@ FIRST_DAY_OF_WEEK = 1 # Monday
|
|||
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
DATE_INPUT_FORMATS = (
|
||||
'%d.%m.%Y', '%d.%m.%y', # '25.10.2006', '25.10.06'
|
||||
'%Y-%m-%d', '%y-%m-%d', # '2006-10-25', '06-10-25'
|
||||
# '%d. %B %Y', '%d. %b. %Y', # '25. October 2006', '25. Oct. 2006'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%d.%m.%Y %H:%M:%S', # '25.10.2006 14:30:59'
|
||||
'%d.%m.%Y %H:%M', # '25.10.2006 14:30'
|
||||
'%d.%m.%Y', # '25.10.2006'
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
)
|
||||
|
||||
# these are the separators for non-monetary numbers. For monetary numbers,
|
||||
|
|
|
@ -15,6 +15,7 @@ FIRST_DAY_OF_WEEK = 0 # Sunday
|
|||
|
||||
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
|
||||
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
# Kept ISO formats as they are in first position
|
||||
DATE_INPUT_FORMATS = (
|
||||
'%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06'
|
||||
# '%b %d %Y', '%b %d, %Y', # 'Oct 25 2006', 'Oct 25, 2006'
|
||||
|
@ -22,10 +23,6 @@ DATE_INPUT_FORMATS = (
|
|||
# '%B %d %Y', '%B %d, %Y', # 'October 25 2006', 'October 25, 2006'
|
||||
# '%d %B %Y', '%d %B, %Y', # '25 October 2006', '25 October, 2006'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
|
|
|
@ -17,16 +17,11 @@ FIRST_DAY_OF_WEEK = 0 # Sunday
|
|||
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
DATE_INPUT_FORMATS = (
|
||||
'%d/%m/%Y', '%d/%m/%y', # '25/10/2006', '25/10/06'
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
# '%b %d %Y', '%b %d, %Y', # 'Oct 25 2006', 'Oct 25, 2006'
|
||||
# '%d %b %Y', '%d %b, %Y', # '25 Oct 2006', '25 Oct, 2006'
|
||||
# '%B %d %Y', '%B %d, %Y', # 'October 25 2006', 'October 25, 2006'
|
||||
# '%d %B %Y', '%d %B, %Y', # '25 October 2006', '25 October, 2006'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
|
|
|
@ -19,10 +19,6 @@ DATE_INPUT_FORMATS = (
|
|||
# '31/12/2009', '31/12/09'
|
||||
'%d/%m/%Y', '%d/%m/%y'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
# '14:30:59', '14:30'
|
||||
'%H:%M:%S', '%H:%M'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%d/%m/%Y %H:%M:%S',
|
||||
'%d/%m/%Y %H:%M',
|
||||
|
|
|
@ -19,10 +19,6 @@ DATE_INPUT_FORMATS = (
|
|||
'%d/%m/%Y', # '31/12/2009'
|
||||
'%d/%m/%y', # '31/12/09'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%d/%m/%Y %H:%M:%S',
|
||||
'%d/%m/%Y %H:%M',
|
||||
|
|
|
@ -15,9 +15,6 @@ DATE_INPUT_FORMATS = (
|
|||
'%d/%m/%Y', '%d/%m/%y', # '25/10/2006', '25/10/06'
|
||||
'%Y%m%d', # '20061025'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', '%H:%M', # '14:30:59', '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%d/%m/%Y %H:%M:%S',
|
||||
'%d/%m/%Y %H:%M',
|
||||
|
|
|
@ -15,9 +15,6 @@ DATE_INPUT_FORMATS = (
|
|||
'%Y%m%d', # '20061025'
|
||||
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', '%H:%M', # '14:30:59', '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%d/%m/%Y %H:%M:%S',
|
||||
'%d/%m/%Y %H:%M',
|
||||
|
|
|
@ -19,13 +19,8 @@ FIRST_DAY_OF_WEEK = 1 # Monday
|
|||
DATE_INPUT_FORMATS = (
|
||||
'%d/%m/%Y', '%d/%m/%y', # '25/10/2006', '25/10/06'
|
||||
'%d.%m.%Y', '%d.%m.%y', # Swiss (fr_CH), '25.10.2006', '25.10.06'
|
||||
'%Y-%m-%d', '%y-%m-%d', # '2006-10-25', '06-10-25'
|
||||
# '%d %B %Y', '%d %b %Y', # '25 octobre 2006', '25 oct. 2006'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%d/%m/%Y %H:%M:%S', # '25/10/2006 14:30:59'
|
||||
'%d/%m/%Y %H:%M', # '25/10/2006 14:30'
|
||||
|
@ -33,9 +28,6 @@ DATETIME_INPUT_FORMATS = (
|
|||
'%d.%m.%Y %H:%M:%S', # Swiss (fr_CH), '25.10.2006 14:30:59'
|
||||
'%d.%m.%Y %H:%M', # Swiss (fr_CH), '25.10.2006 14:30'
|
||||
'%d.%m.%Y', # Swiss (fr_CH), '25.10.2006'
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
)
|
||||
DECIMAL_SEPARATOR = ','
|
||||
THOUSAND_SEPARATOR = '\xa0' # non-breaking space
|
||||
|
|
|
@ -15,15 +15,12 @@ FIRST_DAY_OF_WEEK = 1
|
|||
|
||||
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
|
||||
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
# Kept ISO formats as they are in first position
|
||||
DATE_INPUT_FORMATS = (
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
'%d.%m.%Y.', '%d.%m.%y.', # '25.10.2006.', '25.10.06.'
|
||||
'%d. %m. %Y.', '%d. %m. %y.', # '25. 10. 2006.', '25. 10. 06.'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
|
|
|
@ -20,10 +20,6 @@ DATE_INPUT_FORMATS = (
|
|||
'%d-%m-%Y', '%Y-%m-%d', # '25-10-2006', '2008-10-25'
|
||||
'%d-%m-%y', '%d/%m/%y', # '25-10-06', '25/10/06'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%d/%m/%Y %H:%M:%S', # '25/10/2006 14:30:59'
|
||||
'%d/%m/%Y %H:%M', # '25/10/2006 14:30'
|
||||
|
|
|
@ -15,16 +15,13 @@ FIRST_DAY_OF_WEEK = 1 # (Monday)
|
|||
|
||||
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
|
||||
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
# Kept ISO formats as they are in first position
|
||||
DATE_INPUT_FORMATS = (
|
||||
'%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06'
|
||||
# '%d %b %Y', '%d %b, %Y', '%d %b. %Y', # '25 Oct 2006', '25 Oct, 2006', '25 Oct. 2006'
|
||||
# '%d %B %Y', '%d %B, %Y', # '25 October 2006', '25 October, 2006'
|
||||
# '%d.%m.%Y', '%d.%m.%y', # '25.10.2006', '25.10.06'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
|
|
|
@ -16,6 +16,7 @@ SHORT_DATETIME_FORMAT = 'Y-n-j H:i'
|
|||
|
||||
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
|
||||
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
# Kept ISO formats as they are in first position
|
||||
DATE_INPUT_FORMATS = (
|
||||
'%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06'
|
||||
# '%b %d %Y', '%b %d, %Y', # 'Oct 25 2006', 'Oct 25, 2006'
|
||||
|
|
|
@ -16,6 +16,7 @@ FIRST_DAY_OF_WEEK = 1 #Monday
|
|||
|
||||
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
|
||||
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
# Kept ISO formats as they are in first position
|
||||
DATE_INPUT_FORMATS = (
|
||||
'%Y-%m-%d', '%d.%m.%Y', '%d.%m.%y', # '2006-10-25', '25.10.2006', '25.10.06'
|
||||
)
|
||||
|
|
|
@ -18,12 +18,6 @@ FIRST_DAY_OF_WEEK = 1
|
|||
DATE_INPUT_FORMATS = (
|
||||
'%d.%m.%Y.', '%d.%m.%y.', # '25.10.2006.', '25.10.06.'
|
||||
'%d. %m. %Y.', '%d. %m. %y.', # '25. 10. 2006.', '25. 10. 06.'
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
)
|
||||
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
|
@ -39,9 +33,6 @@ DATETIME_INPUT_FORMATS = (
|
|||
'%d. %m. %y. %H:%M:%S', # '25. 10. 06. 14:30:59'
|
||||
'%d. %m. %y. %H:%M', # '25. 10. 06. 14:30'
|
||||
'%d. %m. %y.', # '25. 10. 06.'
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
)
|
||||
|
||||
DECIMAL_SEPARATOR = ','
|
||||
|
|
|
@ -15,6 +15,7 @@ FIRST_DAY_OF_WEEK = 0 # Sunday
|
|||
|
||||
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
|
||||
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
# Kept ISO formats as they are in first position
|
||||
DATE_INPUT_FORMATS = (
|
||||
'%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06'
|
||||
# '%b %d %Y', '%b %d, %Y', # 'Oct 25 2006', 'Oct 25, 2006'
|
||||
|
@ -22,10 +23,6 @@ DATE_INPUT_FORMATS = (
|
|||
# '%B %d %Y', '%B %d, %Y', # 'October 25 2006', 'October 25, 2006'
|
||||
# '%d %B %Y', '%d %B, %Y', # '25 October 2006', '25 October, 2006'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
|
|
|
@ -16,22 +16,17 @@ FIRST_DAY_OF_WEEK = 1 # Monday
|
|||
|
||||
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
|
||||
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
# Kept ISO formats as they are in first position
|
||||
DATE_INPUT_FORMATS = (
|
||||
'%Y-%m-%d', '%d.%m.%Y', '%d.%m.%y', # '2006-10-25', '25.10.2006', '25.10.06'
|
||||
'%Y-%m-%d', # '2006-10-25',
|
||||
# '%d. %b %Y', '%d %b %Y', # '25. okt 2006', '25 okt 2006'
|
||||
# '%d. %b. %Y', '%d %b. %Y', # '25. okt. 2006', '25 okt. 2006'
|
||||
# '%d. %B %Y', '%d %B %Y', # '25. oktober 2006', '25 oktober 2006'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
'%d.%m.%Y %H:%M:%S', # '25.10.2006 14:30:59'
|
||||
'%d.%m.%Y %H:%M', # '25.10.2006 14:30'
|
||||
'%d.%m.%Y', # '25.10.2006'
|
||||
|
|
|
@ -16,10 +16,11 @@ FIRST_DAY_OF_WEEK = 1 # Monday (in Dutch 'maandag')
|
|||
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
|
||||
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
DATE_INPUT_FORMATS = (
|
||||
'%d-%m-%Y', '%d-%m-%y', '%Y-%m-%d', # '20-01-2009', '20-01-09', '2009-01-20'
|
||||
# '%d %b %Y', '%d %b %y', # '20 jan 2009', '20 jan 09'
|
||||
# '%d %B %Y', '%d %B %y', # '20 januari 2009', '20 januari 09'
|
||||
'%d-%m-%Y', '%d-%m-%y', # '20-01-2009', '20-01-09'
|
||||
# '%d %b %Y', '%d %b %y', # '20 jan 2009', '20 jan 09'
|
||||
# '%d %B %Y', '%d %B %y', # '20 januari 2009', '20 januari 09'
|
||||
)
|
||||
# Kept ISO formats as one is in first position
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '15:23:35'
|
||||
'%H.%M:%S', # '15.23:35'
|
||||
|
|
|
@ -16,17 +16,13 @@ FIRST_DAY_OF_WEEK = 1 # Monday
|
|||
|
||||
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
|
||||
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
# Kept ISO formats as they are in first position
|
||||
DATE_INPUT_FORMATS = (
|
||||
'%Y-%m-%d', '%d.%m.%Y', '%d.%m.%y', # '2006-10-25', '25.10.2006', '25.10.06'
|
||||
'%Y-%m-%d', # '2006-10-25',
|
||||
# '%d. %b %Y', '%d %b %Y', # '25. okt 2006', '25 okt 2006'
|
||||
# '%d. %b. %Y', '%d %b. %Y', # '25. okt. 2006', '25 okt. 2006'
|
||||
# '%d. %B %Y', '%d %B %Y', # '25. oktober 2006', '25 oktober 2006'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
|
|
|
@ -18,20 +18,13 @@ FIRST_DAY_OF_WEEK = 1 # Monday
|
|||
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
DATE_INPUT_FORMATS = (
|
||||
'%d.%m.%Y', '%d.%m.%y', # '25.10.2006', '25.10.06'
|
||||
'%Y-%m-%d', '%y-%m-%d', # '2006-10-25', '06-10-25'
|
||||
'%y-%m-%d', # '06-10-25'
|
||||
# '%d. %B %Y', '%d. %b. %Y', # '25. October 2006', '25. Oct. 2006'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%d.%m.%Y %H:%M:%S', # '25.10.2006 14:30:59'
|
||||
'%d.%m.%Y %H:%M', # '25.10.2006 14:30'
|
||||
'%d.%m.%Y', # '25.10.2006'
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
)
|
||||
DECIMAL_SEPARATOR = ','
|
||||
THOUSAND_SEPARATOR = ' '
|
||||
|
|
|
@ -15,15 +15,12 @@ FIRST_DAY_OF_WEEK = 0 # Sunday
|
|||
|
||||
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
|
||||
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
# Kept ISO formats as they are in first position
|
||||
DATE_INPUT_FORMATS = (
|
||||
'%Y-%m-%d', '%d/%m/%Y', '%d/%m/%y', # '2006-10-25', '25/10/2006', '25/10/06'
|
||||
# '%d de %b de %Y', '%d de %b, %Y', # '25 de Out de 2006', '25 Out, 2006'
|
||||
# '%d de %B de %Y', '%d de %B, %Y', # '25 de Outubro de 2006', '25 de Outubro, 2006'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
|
|
|
@ -17,14 +17,10 @@ FIRST_DAY_OF_WEEK = 0 # Sunday
|
|||
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
|
||||
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
DATE_INPUT_FORMATS = (
|
||||
'%d/%m/%Y', '%d/%m/%y', '%Y-%m-%d', # '25/10/2006', '25/10/06', '2006-10-25'
|
||||
'%d/%m/%Y', '%d/%m/%y', # '25/10/2006', '25/10/06'
|
||||
# '%d de %b de %Y', '%d de %b, %Y', # '25 de Out de 2006', '25 Out, 2006'
|
||||
# '%d de %B de %Y', '%d de %B, %Y', # '25 de Outubro de 2006', '25 de Outubro, 2006'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%d/%m/%Y %H:%M:%S', # '25/10/2006 14:30:59'
|
||||
'%d/%m/%Y %H:%M', # '25/10/2006 14:30'
|
||||
|
@ -32,9 +28,6 @@ DATETIME_INPUT_FORMATS = (
|
|||
'%d/%m/%y %H:%M:%S', # '25/10/06 14:30:59'
|
||||
'%d/%m/%y %H:%M', # '25/10/06 14:30'
|
||||
'%d/%m/%y', # '25/10/06'
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
)
|
||||
DECIMAL_SEPARATOR = ','
|
||||
THOUSAND_SEPARATOR = '.'
|
||||
|
|
|
@ -19,11 +19,6 @@ FIRST_DAY_OF_WEEK = 1 # Monday
|
|||
DATE_INPUT_FORMATS = (
|
||||
'%d.%m.%Y', # '25.10.2006'
|
||||
'%d.%m.%y', # '25.10.06'
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%d.%m.%Y %H:%M:%S', # '25.10.2006 14:30:59'
|
||||
|
@ -32,9 +27,6 @@ DATETIME_INPUT_FORMATS = (
|
|||
'%d.%m.%y %H:%M:%S', # '25.10.06 14:30:59'
|
||||
'%d.%m.%y %H:%M', # '25.10.06 14:30'
|
||||
'%d.%m.%y', # '25.10.06'
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
)
|
||||
DECIMAL_SEPARATOR = ','
|
||||
THOUSAND_SEPARATOR = '\xa0' # non-breaking space
|
||||
|
|
|
@ -18,20 +18,13 @@ FIRST_DAY_OF_WEEK = 1 # Monday
|
|||
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
DATE_INPUT_FORMATS = (
|
||||
'%d.%m.%Y', '%d.%m.%y', # '25.10.2006', '25.10.06'
|
||||
'%Y-%m-%d', '%y-%m-%d', # '2006-10-25', '06-10-25'
|
||||
'%y-%m-%d', # '06-10-25'
|
||||
# '%d. %B %Y', '%d. %b. %Y', # '25. October 2006', '25. Oct. 2006'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%d.%m.%Y %H:%M:%S', # '25.10.2006 14:30:59'
|
||||
'%d.%m.%Y %H:%M', # '25.10.2006 14:30'
|
||||
'%d.%m.%Y', # '25.10.2006'
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
)
|
||||
DECIMAL_SEPARATOR = ','
|
||||
THOUSAND_SEPARATOR = '\xa0' # non-breaking space
|
||||
|
|
|
@ -21,11 +21,6 @@ DATE_INPUT_FORMATS = (
|
|||
'%d. %m. %Y', '%d. %m. %y', # '25. 10. 2006', '25. 10. 06'
|
||||
)
|
||||
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%d.%m.%Y %H:%M:%S', # '25.10.2006 14:30:59'
|
||||
'%d.%m.%Y %H:%M', # '25.10.2006 14:30'
|
||||
|
|
|
@ -18,15 +18,10 @@ FIRST_DAY_OF_WEEK = 1
|
|||
DATE_INPUT_FORMATS = (
|
||||
'%d.%m.%Y.', '%d.%m.%y.', # '25.10.2006.', '25.10.06.'
|
||||
'%d. %m. %Y.', '%d. %m. %y.', # '25. 10. 2006.', '25. 10. 06.'
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
# '%d. %b %y.', '%d. %B %y.', # '25. Oct 06.', '25. October 06.'
|
||||
# '%d. %b \'%y.', '%d. %B \'%y.', # '25. Oct '06.', '25. October '06.'
|
||||
# '%d. %b %Y.', '%d. %B %Y.', # '25. Oct 2006.', '25. October 2006.'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%d.%m.%Y. %H:%M:%S', # '25.10.2006. 14:30:59'
|
||||
'%d.%m.%Y. %H:%M', # '25.10.2006. 14:30'
|
||||
|
@ -40,9 +35,6 @@ DATETIME_INPUT_FORMATS = (
|
|||
'%d. %m. %y. %H:%M:%S', # '25. 10. 06. 14:30:59'
|
||||
'%d. %m. %y. %H:%M', # '25. 10. 06. 14:30'
|
||||
'%d. %m. %y.', # '25. 10. 06.'
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
)
|
||||
DECIMAL_SEPARATOR = ','
|
||||
THOUSAND_SEPARATOR = '.'
|
||||
|
|
|
@ -18,15 +18,10 @@ FIRST_DAY_OF_WEEK = 1
|
|||
DATE_INPUT_FORMATS = (
|
||||
'%d.%m.%Y.', '%d.%m.%y.', # '25.10.2006.', '25.10.06.'
|
||||
'%d. %m. %Y.', '%d. %m. %y.', # '25. 10. 2006.', '25. 10. 06.'
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
# '%d. %b %y.', '%d. %B %y.', # '25. Oct 06.', '25. October 06.'
|
||||
# '%d. %b \'%y.', '%d. %B \'%y.', # '25. Oct '06.', '25. October '06.'
|
||||
# '%d. %b %Y.', '%d. %B %Y.', # '25. Oct 2006.', '25. October 2006.'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%d.%m.%Y. %H:%M:%S', # '25.10.2006. 14:30:59'
|
||||
'%d.%m.%Y. %H:%M', # '25.10.2006. 14:30'
|
||||
|
@ -40,9 +35,6 @@ DATETIME_INPUT_FORMATS = (
|
|||
'%d. %m. %y. %H:%M:%S', # '25. 10. 06. 14:30:59'
|
||||
'%d. %m. %y. %H:%M', # '25. 10. 06. 14:30'
|
||||
'%d. %m. %y.', # '25. 10. 06.'
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
)
|
||||
DECIMAL_SEPARATOR = ','
|
||||
THOUSAND_SEPARATOR = '.'
|
||||
|
|
|
@ -16,15 +16,12 @@ FIRST_DAY_OF_WEEK = 1
|
|||
|
||||
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
|
||||
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
# Kept ISO formats as they are in first position
|
||||
DATE_INPUT_FORMATS = (
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
'%m/%d/%Y', # '10/25/2006'
|
||||
'%m/%d/%y', # '10/25/06'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
|
|
|
@ -17,20 +17,13 @@ FIRST_DAY_OF_WEEK = 1 # Pazartesi
|
|||
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
|
||||
DATE_INPUT_FORMATS = (
|
||||
'%d/%m/%Y', '%d/%m/%y', # '25/10/2006', '25/10/06'
|
||||
'%Y-%m-%d', '%y-%m-%d', # '2006-10-25', '06-10-25'
|
||||
'%y-%m-%d', # '06-10-25'
|
||||
# '%d %B %Y', '%d %b. %Y', # '25 Ekim 2006', '25 Eki. 2006'
|
||||
)
|
||||
TIME_INPUT_FORMATS = (
|
||||
'%H:%M:%S', # '14:30:59'
|
||||
'%H:%M', # '14:30'
|
||||
)
|
||||
DATETIME_INPUT_FORMATS = (
|
||||
'%d/%m/%Y %H:%M:%S', # '25/10/2006 14:30:59'
|
||||
'%d/%m/%Y %H:%M', # '25/10/2006 14:30'
|
||||
'%d/%m/%Y', # '25/10/2006'
|
||||
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
|
||||
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
|
||||
'%Y-%m-%d', # '2006-10-25'
|
||||
)
|
||||
DECIMAL_SEPARATOR = ','
|
||||
THOUSAND_SEPARATOR = '.'
|
||||
|
|
|
@ -75,7 +75,7 @@ STATICFILES_DIRS = (
|
|||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
|
||||
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
|
||||
)
|
||||
|
||||
# Make this unique, and don't share it with anybody.
|
||||
|
@ -85,7 +85,7 @@ SECRET_KEY = '{{ secret_key }}'
|
|||
TEMPLATE_LOADERS = (
|
||||
'django.template.loaders.filesystem.Loader',
|
||||
'django.template.loaders.app_directories.Loader',
|
||||
# 'django.template.loaders.eggs.Loader',
|
||||
# 'django.template.loaders.eggs.Loader',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
|
|
|
@ -9,7 +9,7 @@ import datetime
|
|||
|
||||
from django.db import models
|
||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils.encoding import smart_text, force_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from django.contrib.admin.util import (get_model_from_relation,
|
||||
|
@ -102,7 +102,7 @@ class SimpleListFilter(ListFilter):
|
|||
}
|
||||
for lookup, title in self.lookup_choices:
|
||||
yield {
|
||||
'selected': self.value() == lookup,
|
||||
'selected': self.value() == force_text(lookup),
|
||||
'query_string': cl.get_query_string({
|
||||
self.parameter_name: lookup,
|
||||
}, []),
|
||||
|
|
|
@ -6,8 +6,8 @@ from django.contrib.auth import authenticate
|
|||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from django.utils.translation import ugettext_lazy
|
||||
|
||||
ERROR_MESSAGE = ugettext_lazy("Please enter the correct username and password "
|
||||
"for a staff account. Note that both fields are case-sensitive.")
|
||||
ERROR_MESSAGE = ugettext_lazy("Please enter the correct %(username)s and password "
|
||||
"for a staff account. Note that both fields may be case-sensitive.")
|
||||
|
||||
|
||||
class AdminAuthenticationForm(AuthenticationForm):
|
||||
|
@ -26,8 +26,12 @@ class AdminAuthenticationForm(AuthenticationForm):
|
|||
if username and password:
|
||||
self.user_cache = authenticate(username=username, password=password)
|
||||
if self.user_cache is None:
|
||||
raise forms.ValidationError(message)
|
||||
raise forms.ValidationError(message % {
|
||||
'username': self.username_field.verbose_name
|
||||
})
|
||||
elif not self.user_cache.is_active or not self.user_cache.is_staff:
|
||||
raise forms.ValidationError(message)
|
||||
raise forms.ValidationError(message % {
|
||||
'username': self.username_field.verbose_name
|
||||
})
|
||||
self.check_for_test_cookie()
|
||||
return self.cleaned_data
|
||||
|
|
|
@ -186,9 +186,7 @@ class AdminReadonlyField(object):
|
|||
if getattr(attr, "allow_tags", False):
|
||||
result_repr = mark_safe(result_repr)
|
||||
else:
|
||||
if value is None:
|
||||
result_repr = EMPTY_CHANGELIST_VALUE
|
||||
elif isinstance(f.rel, ManyToManyRel):
|
||||
if isinstance(f.rel, ManyToManyRel) and value is not None:
|
||||
result_repr = ", ".join(map(six.text_type, value.all()))
|
||||
else:
|
||||
result_repr = display_for_field(value, f)
|
||||
|
|
|
@ -4,7 +4,7 @@ from django.db import models
|
|||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.admin.util import quote
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
|
@ -42,13 +42,16 @@ class LogEntry(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
if self.action_flag == ADDITION:
|
||||
return _('Added "%(object)s".') % {'object': self.object_repr}
|
||||
return ugettext('Added "%(object)s".') % {'object': self.object_repr}
|
||||
elif self.action_flag == CHANGE:
|
||||
return _('Changed "%(object)s" - %(changes)s') % {'object': self.object_repr, 'changes': self.change_message}
|
||||
return ugettext('Changed "%(object)s" - %(changes)s') % {
|
||||
'object': self.object_repr,
|
||||
'changes': self.change_message,
|
||||
}
|
||||
elif self.action_flag == DELETION:
|
||||
return _('Deleted "%(object)s."') % {'object': self.object_repr}
|
||||
return ugettext('Deleted "%(object)s."') % {'object': self.object_repr}
|
||||
|
||||
return _('LogEntry Object')
|
||||
return ugettext('LogEntry Object')
|
||||
|
||||
def is_addition(self):
|
||||
return self.action_flag == ADDITION
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import copy
|
||||
from functools import update_wrapper, partial
|
||||
import warnings
|
||||
|
||||
|
@ -130,7 +131,7 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)):
|
|||
# passed to formfield_for_dbfield override the defaults.
|
||||
for klass in db_field.__class__.mro():
|
||||
if klass in self.formfield_overrides:
|
||||
kwargs = dict(self.formfield_overrides[klass], **kwargs)
|
||||
kwargs = dict(copy.deepcopy(self.formfield_overrides[klass]), **kwargs)
|
||||
return db_field.formfield(**kwargs)
|
||||
|
||||
# For any other type of field, just call its formfield() method.
|
||||
|
@ -407,8 +408,6 @@ class ModelAdmin(BaseModelAdmin):
|
|||
js.append('actions%s.js' % extra)
|
||||
if self.prepopulated_fields:
|
||||
js.extend(['urlify.js', 'prepopulate%s.js' % extra])
|
||||
if self.opts.get_ordered_objects():
|
||||
js.extend(['getElementsBySelector.js', 'dom-drag.js' , 'admin/ordering.js'])
|
||||
return forms.Media(js=[static('admin/js/%s' % url) for url in js])
|
||||
|
||||
def get_model_perms(self, request):
|
||||
|
@ -552,7 +551,7 @@ class ModelAdmin(BaseModelAdmin):
|
|||
"""
|
||||
from django.contrib.admin.models import LogEntry, DELETION
|
||||
LogEntry.objects.log_action(
|
||||
user_id = request.user.id,
|
||||
user_id = request.user.pk,
|
||||
content_type_id = ContentType.objects.get_for_model(self.model).pk,
|
||||
object_id = object.pk,
|
||||
object_repr = object_repr,
|
||||
|
@ -665,6 +664,13 @@ class ModelAdmin(BaseModelAdmin):
|
|||
# Use only the first item in list_display as link
|
||||
return list(list_display)[:1]
|
||||
|
||||
def get_list_filter(self, request):
|
||||
"""
|
||||
Returns a sequence containing the fields to be displayed as filters in
|
||||
the right sidebar of the changelist page.
|
||||
"""
|
||||
return self.list_filter
|
||||
|
||||
def construct_change_message(self, request, form, formsets):
|
||||
"""
|
||||
Construct a change message from a changed object.
|
||||
|
@ -691,12 +697,30 @@ class ModelAdmin(BaseModelAdmin):
|
|||
change_message = ' '.join(change_message)
|
||||
return change_message or _('No fields changed.')
|
||||
|
||||
def message_user(self, request, message):
|
||||
def message_user(self, request, message, level=messages.INFO, extra_tags='',
|
||||
fail_silently=False):
|
||||
"""
|
||||
Send a message to the user. The default implementation
|
||||
posts a message using the django.contrib.messages backend.
|
||||
|
||||
Exposes almost the same API as messages.add_message(), but accepts the
|
||||
positional arguments in a different order to maintain backwards
|
||||
compatibility. For convenience, it accepts the `level` argument as
|
||||
a string rather than the usual level number.
|
||||
"""
|
||||
messages.info(request, message)
|
||||
|
||||
if not isinstance(level, int):
|
||||
# attempt to get the level if passed a string
|
||||
try:
|
||||
level = getattr(messages.constants, level.upper())
|
||||
except AttributeError:
|
||||
levels = messages.constants.DEFAULT_TAGS.values()
|
||||
levels_repr = ', '.join('`%s`' % l for l in levels)
|
||||
raise ValueError('Bad message level string: `%s`. '
|
||||
'Possible values are: %s' % (level, levels_repr))
|
||||
|
||||
messages.add_message(request, level, message, extra_tags=extra_tags,
|
||||
fail_silently=fail_silently)
|
||||
|
||||
def save_form(self, request, form, change):
|
||||
"""
|
||||
|
@ -738,7 +762,6 @@ class ModelAdmin(BaseModelAdmin):
|
|||
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
|
||||
opts = self.model._meta
|
||||
app_label = opts.app_label
|
||||
ordered_objects = opts.get_ordered_objects()
|
||||
context.update({
|
||||
'add': add,
|
||||
'change': change,
|
||||
|
@ -747,7 +770,6 @@ class ModelAdmin(BaseModelAdmin):
|
|||
'has_delete_permission': self.has_delete_permission(request, obj),
|
||||
'has_file_field': True, # FIXME - this should check if form or formsets have a FileField,
|
||||
'has_absolute_url': hasattr(self.model, 'get_absolute_url'),
|
||||
'ordered_objects': ordered_objects,
|
||||
'form_url': form_url,
|
||||
'opts': opts,
|
||||
'content_type_id': ContentType.objects.get_for_model(self.model).id,
|
||||
|
@ -1174,6 +1196,7 @@ class ModelAdmin(BaseModelAdmin):
|
|||
|
||||
list_display = self.get_list_display(request)
|
||||
list_display_links = self.get_list_display_links(request, list_display)
|
||||
list_filter = self.get_list_filter(request)
|
||||
|
||||
# Check actions to see if any are available on this changelist
|
||||
actions = self.get_actions(request)
|
||||
|
@ -1184,7 +1207,7 @@ class ModelAdmin(BaseModelAdmin):
|
|||
ChangeList = self.get_changelist(request)
|
||||
try:
|
||||
cl = ChangeList(request, self.model, list_display,
|
||||
list_display_links, self.list_filter, self.date_hierarchy,
|
||||
list_display_links, list_filter, self.date_hierarchy,
|
||||
self.search_fields, self.list_select_related,
|
||||
self.list_per_page, self.list_max_show_all, self.list_editable,
|
||||
self)
|
||||
|
|
|
@ -354,6 +354,7 @@ class AdminSite(object):
|
|||
info = (app_label, model._meta.module_name)
|
||||
model_dict = {
|
||||
'name': capfirst(model._meta.verbose_name_plural),
|
||||
'object_name': model._meta.object_name,
|
||||
'perms': perms,
|
||||
}
|
||||
if perms.get('change', False):
|
||||
|
@ -371,6 +372,7 @@ class AdminSite(object):
|
|||
else:
|
||||
app_dict[app_label] = {
|
||||
'name': app_label.title(),
|
||||
'app_label': app_label,
|
||||
'app_url': reverse('admin:app_list', kwargs={'app_label': app_label}, current_app=self.name),
|
||||
'has_module_perms': has_module_perms,
|
||||
'models': [model_dict],
|
||||
|
@ -389,9 +391,9 @@ class AdminSite(object):
|
|||
'app_list': app_list,
|
||||
}
|
||||
context.update(extra_context or {})
|
||||
return TemplateResponse(request, [
|
||||
self.index_template or 'admin/index.html',
|
||||
], context, current_app=self.name)
|
||||
return TemplateResponse(request, self.index_template or
|
||||
'admin/index.html', context,
|
||||
current_app=self.name)
|
||||
|
||||
def app_index(self, request, app_label, extra_context=None):
|
||||
user = request.user
|
||||
|
@ -408,6 +410,7 @@ class AdminSite(object):
|
|||
info = (app_label, model._meta.module_name)
|
||||
model_dict = {
|
||||
'name': capfirst(model._meta.verbose_name_plural),
|
||||
'object_name': model._meta.object_name,
|
||||
'perms': perms,
|
||||
}
|
||||
if perms.get('change', False):
|
||||
|
@ -428,6 +431,7 @@ class AdminSite(object):
|
|||
# information.
|
||||
app_dict = {
|
||||
'name': app_label.title(),
|
||||
'app_label': app_label,
|
||||
'app_url': '',
|
||||
'has_module_perms': has_module_perms,
|
||||
'models': [model_dict],
|
||||
|
|
|
@ -322,6 +322,10 @@ thead th.sorted {
|
|||
background: #c5c5c5 url(../img/nav-bg-selected.gif) top left repeat-x;
|
||||
}
|
||||
|
||||
thead th.sorted .text {
|
||||
padding-right: 42px;
|
||||
}
|
||||
|
||||
table thead th .text span {
|
||||
padding: 2px 5px;
|
||||
display:block;
|
||||
|
|
|
@ -84,6 +84,11 @@ table thead th.sorted .sortoptions {
|
|||
float: left;
|
||||
}
|
||||
|
||||
thead th.sorted .text {
|
||||
padding-right: 0;
|
||||
padding-left: 42px;
|
||||
}
|
||||
|
||||
/* dashboard styles */
|
||||
|
||||
.dashboard .module table td a {
|
||||
|
|
|
@ -225,6 +225,21 @@ table p.datetime {
|
|||
padding-left: 0;
|
||||
}
|
||||
|
||||
/* URL */
|
||||
|
||||
p.url {
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #666;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.url a {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* FILE UPLOADS */
|
||||
|
||||
p.file-upload {
|
||||
|
|
|
@ -1,137 +0,0 @@
|
|||
addEvent(window, 'load', reorder_init);
|
||||
|
||||
var lis;
|
||||
var top = 0;
|
||||
var left = 0;
|
||||
var height = 30;
|
||||
|
||||
function reorder_init() {
|
||||
lis = document.getElementsBySelector('ul#orderthese li');
|
||||
var input = document.getElementsBySelector('input[name=order_]')[0];
|
||||
setOrder(input.value.split(','));
|
||||
input.disabled = true;
|
||||
draw();
|
||||
// Now initialize the dragging behavior
|
||||
var limit = (lis.length - 1) * height;
|
||||
for (var i = 0; i < lis.length; i++) {
|
||||
var li = lis[i];
|
||||
var img = document.getElementById('handle'+li.id);
|
||||
li.style.zIndex = 1;
|
||||
Drag.init(img, li, left + 10, left + 10, top + 10, top + 10 + limit);
|
||||
li.onDragStart = startDrag;
|
||||
li.onDragEnd = endDrag;
|
||||
img.style.cursor = 'move';
|
||||
}
|
||||
}
|
||||
|
||||
function submitOrderForm() {
|
||||
var inputOrder = document.getElementsBySelector('input[name=order_]')[0];
|
||||
inputOrder.value = getOrder();
|
||||
inputOrder.disabled=false;
|
||||
}
|
||||
|
||||
function startDrag() {
|
||||
this.style.zIndex = '10';
|
||||
this.className = 'dragging';
|
||||
}
|
||||
|
||||
function endDrag(x, y) {
|
||||
this.style.zIndex = '1';
|
||||
this.className = '';
|
||||
// Work out how far along it has been dropped, using x co-ordinate
|
||||
var oldIndex = this.index;
|
||||
var newIndex = Math.round((y - 10 - top) / height);
|
||||
// 'Snap' to the correct position
|
||||
this.style.top = (10 + top + newIndex * height) + 'px';
|
||||
this.index = newIndex;
|
||||
moveItem(oldIndex, newIndex);
|
||||
}
|
||||
|
||||
function moveItem(oldIndex, newIndex) {
|
||||
// Swaps two items, adjusts the index and left co-ord for all others
|
||||
if (oldIndex == newIndex) {
|
||||
return; // Nothing to swap;
|
||||
}
|
||||
var direction, lo, hi;
|
||||
if (newIndex > oldIndex) {
|
||||
lo = oldIndex;
|
||||
hi = newIndex;
|
||||
direction = -1;
|
||||
} else {
|
||||
direction = 1;
|
||||
hi = oldIndex;
|
||||
lo = newIndex;
|
||||
}
|
||||
var lis2 = new Array(); // We will build the new order in this array
|
||||
for (var i = 0; i < lis.length; i++) {
|
||||
if (i < lo || i > hi) {
|
||||
// Position of items not between the indexes is unaffected
|
||||
lis2[i] = lis[i];
|
||||
continue;
|
||||
} else if (i == newIndex) {
|
||||
lis2[i] = lis[oldIndex];
|
||||
continue;
|
||||
} else {
|
||||
// Item is between the two indexes - move it along 1
|
||||
lis2[i] = lis[i - direction];
|
||||
}
|
||||
}
|
||||
// Re-index everything
|
||||
reIndex(lis2);
|
||||
lis = lis2;
|
||||
draw();
|
||||
// document.getElementById('hiddenOrder').value = getOrder();
|
||||
document.getElementsBySelector('input[name=order_]')[0].value = getOrder();
|
||||
}
|
||||
|
||||
function reIndex(lis) {
|
||||
for (var i = 0; i < lis.length; i++) {
|
||||
lis[i].index = i;
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
for (var i = 0; i < lis.length; i++) {
|
||||
var li = lis[i];
|
||||
li.index = i;
|
||||
li.style.position = 'absolute';
|
||||
li.style.left = (10 + left) + 'px';
|
||||
li.style.top = (10 + top + (i * height)) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function getOrder() {
|
||||
var order = new Array(lis.length);
|
||||
for (var i = 0; i < lis.length; i++) {
|
||||
order[i] = lis[i].id.substring(1, 100);
|
||||
}
|
||||
return order.join(',');
|
||||
}
|
||||
|
||||
function setOrder(id_list) {
|
||||
/* Set the current order to match the lsit of IDs */
|
||||
var temp_lis = new Array();
|
||||
for (var i = 0; i < id_list.length; i++) {
|
||||
var id = 'p' + id_list[i];
|
||||
temp_lis[temp_lis.length] = document.getElementById(id);
|
||||
}
|
||||
reIndex(temp_lis);
|
||||
lis = temp_lis;
|
||||
draw();
|
||||
}
|
||||
|
||||
function addEvent(elm, evType, fn, useCapture)
|
||||
// addEvent and removeEvent
|
||||
// cross-browser event handling for IE5+, NS6 and Mozilla
|
||||
// By Scott Andrew
|
||||
{
|
||||
if (elm.addEventListener){
|
||||
elm.addEventListener(evType, fn, useCapture);
|
||||
return true;
|
||||
} else if (elm.attachEvent){
|
||||
var r = elm.attachEvent("on"+evType, fn);
|
||||
return r;
|
||||
} else {
|
||||
elm['on'+evType] = fn;
|
||||
}
|
||||
}
|
|
@ -1,167 +0,0 @@
|
|||
/* document.getElementsBySelector(selector)
|
||||
- returns an array of element objects from the current document
|
||||
matching the CSS selector. Selectors can contain element names,
|
||||
class names and ids and can be nested. For example:
|
||||
|
||||
elements = document.getElementsBySelect('div#main p a.external')
|
||||
|
||||
Will return an array of all 'a' elements with 'external' in their
|
||||
class attribute that are contained inside 'p' elements that are
|
||||
contained inside the 'div' element which has id="main"
|
||||
|
||||
New in version 0.4: Support for CSS2 and CSS3 attribute selectors:
|
||||
See http://www.w3.org/TR/css3-selectors/#attribute-selectors
|
||||
|
||||
Version 0.4 - Simon Willison, March 25th 2003
|
||||
-- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows
|
||||
-- Opera 7 fails
|
||||
*/
|
||||
|
||||
function getAllChildren(e) {
|
||||
// Returns all children of element. Workaround required for IE5/Windows. Ugh.
|
||||
return e.all ? e.all : e.getElementsByTagName('*');
|
||||
}
|
||||
|
||||
document.getElementsBySelector = function(selector) {
|
||||
// Attempt to fail gracefully in lesser browsers
|
||||
if (!document.getElementsByTagName) {
|
||||
return new Array();
|
||||
}
|
||||
// Split selector in to tokens
|
||||
var tokens = selector.split(' ');
|
||||
var currentContext = new Array(document);
|
||||
for (var i = 0; i < tokens.length; i++) {
|
||||
token = tokens[i].replace(/^\s+/,'').replace(/\s+$/,'');;
|
||||
if (token.indexOf('#') > -1) {
|
||||
// Token is an ID selector
|
||||
var bits = token.split('#');
|
||||
var tagName = bits[0];
|
||||
var id = bits[1];
|
||||
var element = document.getElementById(id);
|
||||
if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) {
|
||||
// ID not found or tag with that ID not found, return false.
|
||||
return new Array();
|
||||
}
|
||||
// Set currentContext to contain just this element
|
||||
currentContext = new Array(element);
|
||||
continue; // Skip to next token
|
||||
}
|
||||
if (token.indexOf('.') > -1) {
|
||||
// Token contains a class selector
|
||||
var bits = token.split('.');
|
||||
var tagName = bits[0];
|
||||
var className = bits[1];
|
||||
if (!tagName) {
|
||||
tagName = '*';
|
||||
}
|
||||
// Get elements matching tag, filter them for class selector
|
||||
var found = new Array;
|
||||
var foundCount = 0;
|
||||
for (var h = 0; h < currentContext.length; h++) {
|
||||
var elements;
|
||||
if (tagName == '*') {
|
||||
elements = getAllChildren(currentContext[h]);
|
||||
} else {
|
||||
try {
|
||||
elements = currentContext[h].getElementsByTagName(tagName);
|
||||
}
|
||||
catch(e) {
|
||||
elements = [];
|
||||
}
|
||||
}
|
||||
for (var j = 0; j < elements.length; j++) {
|
||||
found[foundCount++] = elements[j];
|
||||
}
|
||||
}
|
||||
currentContext = new Array;
|
||||
var currentContextIndex = 0;
|
||||
for (var k = 0; k < found.length; k++) {
|
||||
if (found[k].className && found[k].className.match(new RegExp('\\b'+className+'\\b'))) {
|
||||
currentContext[currentContextIndex++] = found[k];
|
||||
}
|
||||
}
|
||||
continue; // Skip to next token
|
||||
}
|
||||
// Code to deal with attribute selectors
|
||||
if (token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/)) {
|
||||
var tagName = RegExp.$1;
|
||||
var attrName = RegExp.$2;
|
||||
var attrOperator = RegExp.$3;
|
||||
var attrValue = RegExp.$4;
|
||||
if (!tagName) {
|
||||
tagName = '*';
|
||||
}
|
||||
// Grab all of the tagName elements within current context
|
||||
var found = new Array;
|
||||
var foundCount = 0;
|
||||
for (var h = 0; h < currentContext.length; h++) {
|
||||
var elements;
|
||||
if (tagName == '*') {
|
||||
elements = getAllChildren(currentContext[h]);
|
||||
} else {
|
||||
elements = currentContext[h].getElementsByTagName(tagName);
|
||||
}
|
||||
for (var j = 0; j < elements.length; j++) {
|
||||
found[foundCount++] = elements[j];
|
||||
}
|
||||
}
|
||||
currentContext = new Array;
|
||||
var currentContextIndex = 0;
|
||||
var checkFunction; // This function will be used to filter the elements
|
||||
switch (attrOperator) {
|
||||
case '=': // Equality
|
||||
checkFunction = function(e) { return (e.getAttribute(attrName) == attrValue); };
|
||||
break;
|
||||
case '~': // Match one of space seperated words
|
||||
checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('\\b'+attrValue+'\\b'))); };
|
||||
break;
|
||||
case '|': // Match start with value followed by optional hyphen
|
||||
checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('^'+attrValue+'-?'))); };
|
||||
break;
|
||||
case '^': // Match starts with value
|
||||
checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) == 0); };
|
||||
break;
|
||||
case '$': // Match ends with value - fails with "Warning" in Opera 7
|
||||
checkFunction = function(e) { return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); };
|
||||
break;
|
||||
case '*': // Match ends with value
|
||||
checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) > -1); };
|
||||
break;
|
||||
default :
|
||||
// Just test for existence of attribute
|
||||
checkFunction = function(e) { return e.getAttribute(attrName); };
|
||||
}
|
||||
currentContext = new Array;
|
||||
var currentContextIndex = 0;
|
||||
for (var k = 0; k < found.length; k++) {
|
||||
if (checkFunction(found[k])) {
|
||||
currentContext[currentContextIndex++] = found[k];
|
||||
}
|
||||
}
|
||||
// alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue);
|
||||
continue; // Skip to next token
|
||||
}
|
||||
// If we get here, token is JUST an element (not a class or ID selector)
|
||||
tagName = token;
|
||||
var found = new Array;
|
||||
var foundCount = 0;
|
||||
for (var h = 0; h < currentContext.length; h++) {
|
||||
var elements = currentContext[h].getElementsByTagName(tagName);
|
||||
for (var j = 0; j < elements.length; j++) {
|
||||
found[foundCount++] = elements[j];
|
||||
}
|
||||
}
|
||||
currentContext = found;
|
||||
}
|
||||
return currentContext;
|
||||
}
|
||||
|
||||
/* That revolting regular expression explained
|
||||
/^(\w+)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/
|
||||
\---/ \---/\-------------/ \-------/
|
||||
| | | |
|
||||
| | | The value
|
||||
| | ~,|,^,$,* or =
|
||||
| Attribute
|
||||
Tag
|
||||
*/
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n admin_static admin_modify %}
|
||||
{% load i18n admin_static %}
|
||||
{% load admin_urls %}
|
||||
|
||||
{% block extrahead %}{{ block.super }}
|
||||
|
@ -13,7 +13,7 @@
|
|||
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
|
||||
› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_label|capfirst|escape }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'changelist' %}{{ original.pk }}">{{ original|truncatewords:"18" }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'change' original.pk|admin_urlquote %}">{{ original|truncatewords:"18" }}</a>
|
||||
› {% trans 'Change password' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />{% endblock %}
|
||||
|
||||
{% block coltype %}{% if ordered_objects %}colMS{% else %}colM{% endif %}{% endblock %}
|
||||
{% block coltype %}colM{% endblock %}
|
||||
|
||||
{% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %}
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
{% for field in line %}
|
||||
<td{% if field.field.name %} class="field-{{ field.field.name }}"{% endif %}>
|
||||
{% if field.is_readonly %}
|
||||
<p>{{ field.contents }}</p>
|
||||
<p>{{ field.contents|linebreaksbr }}</p>
|
||||
{% else %}
|
||||
{{ field.field.errors.as_ul }}
|
||||
{{ field.field }}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
{% else %}
|
||||
{{ field.label_tag }}
|
||||
{% if field.is_readonly %}
|
||||
<p>{{ field.contents }}</p>
|
||||
<p>{{ field.contents|linebreaksbr }}</p>
|
||||
{% else %}
|
||||
{{ field.field }}
|
||||
{% endif %}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
{% if app_list %}
|
||||
{% for app in app_list %}
|
||||
<div class="module">
|
||||
<div class="app-{{ app.app_label }} module">
|
||||
<table>
|
||||
<caption>
|
||||
<a href="{{ app.app_url }}" class="section" title="{% blocktrans with name=app.name %}Models in the {{ name }} application{% endblocktrans %}">
|
||||
|
@ -22,7 +22,7 @@
|
|||
</a>
|
||||
</caption>
|
||||
{% for model in app.models %}
|
||||
<tr>
|
||||
<tr class="model-{{ model.object_name|lower }}">
|
||||
{% if model.admin_url %}
|
||||
<th scope="row"><a href="{{ model.admin_url }}">{{ model.name }}</a></th>
|
||||
{% else %}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% load i18n admin_urls %}
|
||||
<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" />{% 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_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_as_new %}<input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew" />{%endif%}
|
||||
{% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother" />{% endif %}
|
||||
{% if show_save_and_continue %}<input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue" />{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -30,8 +30,6 @@ def submit_row(context):
|
|||
save_as = context['save_as']
|
||||
ctx = {
|
||||
'opts': opts,
|
||||
'onclick_attrib': (opts.get_ordered_objects() and change
|
||||
and 'onclick="submitOrderForm();"' or ''),
|
||||
'show_delete_link': (not is_popup and context['has_delete_permission']
|
||||
and change and context.get('show_delete', True)),
|
||||
'show_save_as_new': not is_popup and change and save_as,
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import warnings
|
||||
from django.template import Library
|
||||
from django.templatetags.static import PrefixNode
|
||||
|
||||
register = Library()
|
||||
|
||||
@register.simple_tag
|
||||
def admin_media_prefix():
|
||||
"""
|
||||
Returns the string contained in the setting ADMIN_MEDIA_PREFIX.
|
||||
"""
|
||||
warnings.warn(
|
||||
"The admin_media_prefix template tag is deprecated. "
|
||||
"Use the static template tag instead.", DeprecationWarning)
|
||||
return PrefixNode.handle_simple("ADMIN_MEDIA_PREFIX")
|
|
@ -10,7 +10,7 @@ from django.contrib.admin.templatetags.admin_static import static
|
|||
from django.core.urlresolvers import reverse
|
||||
from django.forms.widgets import RadioFieldRenderer
|
||||
from django.forms.util import flatatt
|
||||
from django.utils.html import escape, format_html, format_html_join
|
||||
from django.utils.html import escape, format_html, format_html_join, smart_urlquote
|
||||
from django.utils.text import Truncator
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.safestring import mark_safe
|
||||
|
@ -306,6 +306,19 @@ class AdminURLFieldWidget(forms.TextInput):
|
|||
final_attrs.update(attrs)
|
||||
super(AdminURLFieldWidget, self).__init__(attrs=final_attrs)
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
html = super(AdminURLFieldWidget, self).render(name, value, attrs)
|
||||
if value:
|
||||
value = force_text(self._format_value(value))
|
||||
final_attrs = {'href': mark_safe(smart_urlquote(value))}
|
||||
html = format_html(
|
||||
'<p class="url">{0} <a {1}>{2}</a><br />{3} {4}</p>',
|
||||
_('Currently:'), flatatt(final_attrs), value,
|
||||
_('Change:'), html
|
||||
)
|
||||
return html
|
||||
|
||||
|
||||
class AdminIntegerFieldWidget(forms.TextInput):
|
||||
class_name = 'vIntegerField'
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ from django.core import urlresolvers
|
|||
from django.contrib.admindocs import utils
|
||||
from django.contrib.sites.models import Site
|
||||
from django.utils.importlib import import_module
|
||||
from django.utils._os import upath
|
||||
from django.utils import six
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.safestring import mark_safe
|
||||
|
@ -311,7 +312,7 @@ def load_all_installed_template_libraries():
|
|||
try:
|
||||
libraries = [
|
||||
os.path.splitext(p)[0]
|
||||
for p in os.listdir(os.path.dirname(mod.__file__))
|
||||
for p in os.listdir(os.path.dirname(upath(mod.__file__)))
|
||||
if p.endswith('.py') and p[0].isalpha()
|
||||
]
|
||||
except OSError:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import re
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
|
||||
from django.utils.importlib import import_module
|
||||
from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed
|
||||
|
||||
|
@ -60,6 +60,9 @@ def authenticate(**credentials):
|
|||
except TypeError:
|
||||
# This backend doesn't accept these credentials as arguments. Try the next one.
|
||||
continue
|
||||
except PermissionDenied:
|
||||
# This backend says to stop in our tracks - this user should not be allowed in at all.
|
||||
return None
|
||||
if user is None:
|
||||
continue
|
||||
# Annotate the user object with the path of the backend.
|
||||
|
@ -81,14 +84,14 @@ def login(request, user):
|
|||
user = request.user
|
||||
# TODO: It would be nice to support different login methods, like signed cookies.
|
||||
if SESSION_KEY in request.session:
|
||||
if request.session[SESSION_KEY] != user.id:
|
||||
if request.session[SESSION_KEY] != user.pk:
|
||||
# To avoid reusing another user's session, create a new, empty
|
||||
# session if the existing session corresponds to a different
|
||||
# authenticated user.
|
||||
request.session.flush()
|
||||
else:
|
||||
request.session.cycle_key()
|
||||
request.session[SESSION_KEY] = user.id
|
||||
request.session[SESSION_KEY] = user.pk
|
||||
request.session[BACKEND_SESSION_KEY] = user.backend
|
||||
if hasattr(request, 'user'):
|
||||
request.user = user
|
||||
|
|
|
@ -133,7 +133,7 @@ class UserAdmin(admin.ModelAdmin):
|
|||
adminForm = admin.helpers.AdminForm(form, fieldsets, {})
|
||||
|
||||
context = {
|
||||
'title': _('Change password: %s') % escape(user.username),
|
||||
'title': _('Change password: %s') % escape(user.get_username()),
|
||||
'adminForm': adminForm,
|
||||
'form_url': form_url,
|
||||
'form': form,
|
||||
|
@ -148,10 +148,10 @@ class UserAdmin(admin.ModelAdmin):
|
|||
'save_as': False,
|
||||
'show_save': True,
|
||||
}
|
||||
return TemplateResponse(request, [
|
||||
return TemplateResponse(request,
|
||||
self.change_user_password_template or
|
||||
'admin/auth/user/change_password.html'
|
||||
], context, current_app=self.admin_site.name)
|
||||
'admin/auth/user/change_password.html',
|
||||
context, current_app=self.admin_site.name)
|
||||
|
||||
def response_add(self, request, obj, **kwargs):
|
||||
"""
|
||||
|
|
|
@ -18,7 +18,9 @@ class PermLookupDict(object):
|
|||
|
||||
def __bool__(self):
|
||||
return self.user.has_module_perms(self.module_name)
|
||||
__nonzero__ = __bool__ # Python 2
|
||||
|
||||
def __nonzero__(self): # Python 2 compatibility
|
||||
return type(self).__bool__(self)
|
||||
|
||||
|
||||
class PermWrapper(object):
|
||||
|
|
|
@ -27,7 +27,7 @@ class ReadOnlyPasswordHashWidget(forms.Widget):
|
|||
encoded = value
|
||||
final_attrs = self.build_attrs(attrs)
|
||||
|
||||
if encoded == '' or encoded == UNUSABLE_PASSWORD:
|
||||
if not encoded or encoded == UNUSABLE_PASSWORD:
|
||||
summary = mark_safe("<strong>%s</strong>" % ugettext("No password set."))
|
||||
else:
|
||||
try:
|
||||
|
@ -52,6 +52,11 @@ class ReadOnlyPasswordHashField(forms.Field):
|
|||
kwargs.setdefault("required", False)
|
||||
super(ReadOnlyPasswordHashField, self).__init__(*args, **kwargs)
|
||||
|
||||
def bound_data(self, data, initial):
|
||||
# Always return initial because the widget doesn't
|
||||
# render an input field.
|
||||
return initial
|
||||
|
||||
|
||||
class UserCreationForm(forms.ModelForm):
|
||||
"""
|
||||
|
@ -143,8 +148,8 @@ class AuthenticationForm(forms.Form):
|
|||
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
|
||||
|
||||
error_messages = {
|
||||
'invalid_login': _("Please enter a correct username and password. "
|
||||
"Note that both fields are case-sensitive."),
|
||||
'invalid_login': _("Please enter a correct %(username)s and password. "
|
||||
"Note that both fields may be case-sensitive."),
|
||||
'no_cookies': _("Your Web browser doesn't appear to have cookies "
|
||||
"enabled. Cookies are required for logging in."),
|
||||
'inactive': _("This account is inactive."),
|
||||
|
@ -163,8 +168,8 @@ class AuthenticationForm(forms.Form):
|
|||
|
||||
# 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)
|
||||
self.username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
|
||||
self.fields['username'].label = capfirst(self.username_field.verbose_name)
|
||||
|
||||
def clean(self):
|
||||
username = self.cleaned_data.get('username')
|
||||
|
@ -175,7 +180,9 @@ class AuthenticationForm(forms.Form):
|
|||
password=password)
|
||||
if self.user_cache is None:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_login'])
|
||||
self.error_messages['invalid_login'] % {
|
||||
'username': self.username_field.verbose_name
|
||||
})
|
||||
elif not self.user_cache.is_active:
|
||||
raise forms.ValidationError(self.error_messages['inactive'])
|
||||
self.check_for_test_cookie()
|
||||
|
@ -209,10 +216,12 @@ class PasswordResetForm(forms.Form):
|
|||
"""
|
||||
UserModel = get_user_model()
|
||||
email = self.cleaned_data["email"]
|
||||
self.users_cache = UserModel.objects.filter(email__iexact=email,
|
||||
is_active=True)
|
||||
self.users_cache = UserModel.objects.filter(email__iexact=email)
|
||||
if not len(self.users_cache):
|
||||
raise forms.ValidationError(self.error_messages['unknown'])
|
||||
if not any(user.is_active for user in self.users_cache):
|
||||
# none of the filtered users are active
|
||||
raise forms.ValidationError(self.error_messages['unknown'])
|
||||
if any((user.password == UNUSABLE_PASSWORD)
|
||||
for user in self.users_cache):
|
||||
raise forms.ValidationError(self.error_messages['unusable'])
|
||||
|
@ -239,7 +248,7 @@ class PasswordResetForm(forms.Form):
|
|||
'email': user.email,
|
||||
'domain': domain,
|
||||
'site_name': site_name,
|
||||
'uid': int_to_base36(user.id),
|
||||
'uid': int_to_base36(user.pk),
|
||||
'user': user,
|
||||
'token': token_generator.make_token(user),
|
||||
'protocol': use_https and 'https' or 'http',
|
||||
|
|
|
@ -21,17 +21,12 @@ def check_password(environ, username, password):
|
|||
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
|
||||
if not user.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
|
||||
|
|
|
@ -10,6 +10,7 @@ import unicodedata
|
|||
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 import DEFAULT_DB_ALIAS, router
|
||||
from django.db.models import get_models, signals
|
||||
from django.utils import six
|
||||
from django.utils.six.moves import input
|
||||
|
@ -57,7 +58,10 @@ def _check_permission_clashing(custom, builtin, ctype):
|
|||
(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, db=DEFAULT_DB_ALIAS, **kwargs):
|
||||
if not router.allow_syncdb(db, auth_app.Permission):
|
||||
return
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
app_models = get_models(app)
|
||||
|
@ -68,7 +72,9 @@ def create_permissions(app, created_models, verbosity, **kwargs):
|
|||
# The codenames and ctypes that should exist.
|
||||
ctypes = set()
|
||||
for klass in app_models:
|
||||
ctype = ContentType.objects.get_for_model(klass)
|
||||
# Force looking up the content types in the current database
|
||||
# before creating foreign keys to them.
|
||||
ctype = ContentType.objects.db_manager(db).get_for_model(klass)
|
||||
ctypes.add(ctype)
|
||||
for perm in _get_all_permissions(klass._meta, ctype):
|
||||
searched_perms.append((ctype, perm))
|
||||
|
@ -76,21 +82,21 @@ def create_permissions(app, created_models, verbosity, **kwargs):
|
|||
# Find all the Permissions that have a context_type for a model we're
|
||||
# looking for. We don't need to check for codenames since we already have
|
||||
# a list of the ones we're going to create.
|
||||
all_perms = set(auth_app.Permission.objects.filter(
|
||||
all_perms = set(auth_app.Permission.objects.using(db).filter(
|
||||
content_type__in=ctypes,
|
||||
).values_list(
|
||||
"content_type", "codename"
|
||||
))
|
||||
|
||||
objs = [
|
||||
perms = [
|
||||
auth_app.Permission(codename=codename, name=name, content_type=ctype)
|
||||
for ctype, (codename, name) in searched_perms
|
||||
if (ctype.pk, codename) not in all_perms
|
||||
]
|
||||
auth_app.Permission.objects.bulk_create(objs)
|
||||
auth_app.Permission.objects.using(db).bulk_create(perms)
|
||||
if verbosity >= 2:
|
||||
for obj in objs:
|
||||
print("Adding permission '%s'" % obj)
|
||||
for perm in perms:
|
||||
print("Adding permission '%s'" % perm)
|
||||
|
||||
|
||||
def create_superuser(app, created_models, verbosity, db, **kwargs):
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
from django.contrib import auth
|
||||
from django.contrib.auth import load_backend
|
||||
from django.contrib.auth.backends import RemoteUserBackend
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
|
||||
|
@ -47,9 +49,18 @@ class RemoteUserMiddleware(object):
|
|||
try:
|
||||
username = request.META[self.header]
|
||||
except KeyError:
|
||||
# If specified header doesn't exist then return (leaving
|
||||
# request.user set to AnonymousUser by the
|
||||
# AuthenticationMiddleware).
|
||||
# If specified header doesn't exist then remove any existing
|
||||
# authenticated remote-user, or return (leaving request.user set to
|
||||
# AnonymousUser by the AuthenticationMiddleware).
|
||||
if request.user.is_authenticated():
|
||||
try:
|
||||
stored_backend = load_backend(request.session.get(
|
||||
auth.BACKEND_SESSION_KEY, ''))
|
||||
if isinstance(stored_backend, RemoteUserBackend):
|
||||
auth.logout(request)
|
||||
except ImproperlyConfigured as e:
|
||||
# backend failed to load
|
||||
auth.logout(request)
|
||||
return
|
||||
# If the user is already authenticated and that user is the user we are
|
||||
# getting passed in the headers, then the correct user is already
|
||||
|
|
|
@ -195,43 +195,13 @@ class UserManager(BaseUserManager):
|
|||
return u
|
||||
|
||||
|
||||
# A few helper functions for common logic between User and AnonymousUser.
|
||||
def _user_get_all_permissions(user, obj):
|
||||
permissions = set()
|
||||
for backend in auth.get_backends():
|
||||
if hasattr(backend, "get_all_permissions"):
|
||||
if obj is not None:
|
||||
permissions.update(backend.get_all_permissions(user, obj))
|
||||
else:
|
||||
permissions.update(backend.get_all_permissions(user))
|
||||
return permissions
|
||||
|
||||
|
||||
def _user_has_perm(user, perm, obj):
|
||||
for backend in auth.get_backends():
|
||||
if hasattr(backend, "has_perm"):
|
||||
if obj is not None:
|
||||
if backend.has_perm(user, perm, obj):
|
||||
return True
|
||||
else:
|
||||
if backend.has_perm(user, perm):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _user_has_module_perms(user, app_label):
|
||||
for backend in auth.get_backends():
|
||||
if hasattr(backend, "has_module_perms"):
|
||||
if backend.has_module_perms(user, app_label):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class AbstractBaseUser(models.Model):
|
||||
password = models.CharField(_('password'), max_length=128)
|
||||
last_login = models.DateTimeField(_('last login'), default=timezone.now)
|
||||
|
||||
is_active = True
|
||||
|
||||
REQUIRED_FIELDS = []
|
||||
|
||||
class Meta:
|
||||
|
@ -288,32 +258,46 @@ class AbstractBaseUser(models.Model):
|
|||
raise NotImplementedError()
|
||||
|
||||
|
||||
class AbstractUser(AbstractBaseUser):
|
||||
"""
|
||||
An abstract base class implementing a fully featured User model with
|
||||
admin-compliant permissions.
|
||||
# A few helper functions for common logic between User and AnonymousUser.
|
||||
def _user_get_all_permissions(user, obj):
|
||||
permissions = set()
|
||||
for backend in auth.get_backends():
|
||||
if hasattr(backend, "get_all_permissions"):
|
||||
if obj is not None:
|
||||
permissions.update(backend.get_all_permissions(user, obj))
|
||||
else:
|
||||
permissions.update(backend.get_all_permissions(user))
|
||||
return permissions
|
||||
|
||||
Username, password and email are required. Other fields are optional.
|
||||
|
||||
def _user_has_perm(user, perm, obj):
|
||||
for backend in auth.get_backends():
|
||||
if hasattr(backend, "has_perm"):
|
||||
if obj is not None:
|
||||
if backend.has_perm(user, perm, obj):
|
||||
return True
|
||||
else:
|
||||
if backend.has_perm(user, perm):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _user_has_module_perms(user, app_label):
|
||||
for backend in auth.get_backends():
|
||||
if hasattr(backend, "has_module_perms"):
|
||||
if backend.has_module_perms(user, app_label):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class PermissionsMixin(models.Model):
|
||||
"""
|
||||
A mixin class that adds the fields and methods necessary to support
|
||||
Django's Group and Permission model using the ModelBackend.
|
||||
"""
|
||||
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 '
|
||||
|
@ -322,30 +306,9 @@ class AbstractUser(AbstractBaseUser):
|
|||
verbose_name=_('user permissions'), blank=True,
|
||||
help_text='Specific permissions for this user.')
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
USERNAME_FIELD = 'username'
|
||||
REQUIRED_FIELDS = ['email']
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('user')
|
||||
verbose_name_plural = _('users')
|
||||
abstract = True
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "/users/%s/" % urlquote(self.username)
|
||||
|
||||
def get_full_name(self):
|
||||
"""
|
||||
Returns the first_name plus the last_name, with a space in between.
|
||||
"""
|
||||
full_name = '%s %s' % (self.first_name, self.last_name)
|
||||
return full_name.strip()
|
||||
|
||||
def get_short_name(self):
|
||||
"Returns the short name for the user."
|
||||
return self.first_name
|
||||
|
||||
def get_group_permissions(self, obj=None):
|
||||
"""
|
||||
Returns a list of permission strings that this user has through his/her
|
||||
|
@ -403,6 +366,55 @@ class AbstractUser(AbstractBaseUser):
|
|||
|
||||
return _user_has_module_perms(self, app_label)
|
||||
|
||||
|
||||
class AbstractUser(AbstractBaseUser, PermissionsMixin):
|
||||
"""
|
||||
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.'))
|
||||
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
|
||||
|
||||
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 email_user(self, subject, message, from_email=None):
|
||||
"""
|
||||
Sends an email to this User.
|
||||
|
|
|
@ -14,3 +14,16 @@ from django.contrib.auth.tests.tokens import *
|
|||
from django.contrib.auth.tests.views import *
|
||||
|
||||
# The password for the fixture data users is 'password'
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.test.signals import setting_changed
|
||||
|
||||
|
||||
@receiver(setting_changed)
|
||||
def user_model_swapped(**kwargs):
|
||||
if kwargs['setting'] == 'AUTH_USER_MODEL':
|
||||
from django.db.models.manager import ensure_default_manager
|
||||
from django.contrib.auth.models import User
|
||||
# Reset User manager
|
||||
setattr(User, 'objects', User._default_manager)
|
||||
ensure_default_manager(User)
|
||||
|
|
|
@ -4,9 +4,10 @@ from datetime import date
|
|||
from django.conf import settings
|
||||
from django.contrib.auth.models import User, Group, Permission, AnonymousUser
|
||||
from django.contrib.auth.tests.utils import skipIfCustomUser
|
||||
from django.contrib.auth.tests.custom_user import ExtensionUser
|
||||
from django.contrib.auth.tests.custom_user import ExtensionUser, CustomPermissionsUser
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
|
||||
from django.contrib.auth import authenticate
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
|
@ -33,7 +34,7 @@ class BaseModelBackendTest(object):
|
|||
ContentType.objects.clear_cache()
|
||||
|
||||
def test_has_perm(self):
|
||||
user = self.UserModel.objects.get(username='test')
|
||||
user = self.UserModel.objects.get(pk=self.user.pk)
|
||||
self.assertEqual(user.has_perm('auth.test'), False)
|
||||
user.is_staff = True
|
||||
user.save()
|
||||
|
@ -52,14 +53,14 @@ class BaseModelBackendTest(object):
|
|||
self.assertEqual(user.has_perm('auth.test'), False)
|
||||
|
||||
def test_custom_perms(self):
|
||||
user = self.UserModel.objects.get(username='test')
|
||||
user = self.UserModel.objects.get(pk=self.user.pk)
|
||||
content_type = ContentType.objects.get_for_model(Group)
|
||||
perm = Permission.objects.create(name='test', content_type=content_type, codename='test')
|
||||
user.user_permissions.add(perm)
|
||||
user.save()
|
||||
|
||||
# reloading user to purge the _perm_cache
|
||||
user = self.UserModel.objects.get(username='test')
|
||||
user = self.UserModel.objects.get(pk=self.user.pk)
|
||||
self.assertEqual(user.get_all_permissions() == set(['auth.test']), True)
|
||||
self.assertEqual(user.get_group_permissions(), set([]))
|
||||
self.assertEqual(user.has_module_perms('Group'), False)
|
||||
|
@ -70,7 +71,7 @@ class BaseModelBackendTest(object):
|
|||
perm = Permission.objects.create(name='test3', content_type=content_type, codename='test3')
|
||||
user.user_permissions.add(perm)
|
||||
user.save()
|
||||
user = self.UserModel.objects.get(username='test')
|
||||
user = self.UserModel.objects.get(pk=self.user.pk)
|
||||
self.assertEqual(user.get_all_permissions(), set(['auth.test2', 'auth.test', 'auth.test3']))
|
||||
self.assertEqual(user.has_perm('test'), False)
|
||||
self.assertEqual(user.has_perm('auth.test'), True)
|
||||
|
@ -80,7 +81,7 @@ class BaseModelBackendTest(object):
|
|||
group.permissions.add(perm)
|
||||
group.save()
|
||||
user.groups.add(group)
|
||||
user = self.UserModel.objects.get(username='test')
|
||||
user = self.UserModel.objects.get(pk=self.user.pk)
|
||||
exp = set(['auth.test2', 'auth.test', 'auth.test3', 'auth.test_group'])
|
||||
self.assertEqual(user.get_all_permissions(), exp)
|
||||
self.assertEqual(user.get_group_permissions(), set(['auth.test_group']))
|
||||
|
@ -92,7 +93,7 @@ class BaseModelBackendTest(object):
|
|||
|
||||
def test_has_no_object_perm(self):
|
||||
"""Regressiontest for #12462"""
|
||||
user = self.UserModel.objects.get(username='test')
|
||||
user = self.UserModel.objects.get(pk=self.user.pk)
|
||||
content_type = ContentType.objects.get_for_model(Group)
|
||||
perm = Permission.objects.create(name='test', content_type=content_type, codename='test')
|
||||
user.user_permissions.add(perm)
|
||||
|
@ -105,7 +106,7 @@ class BaseModelBackendTest(object):
|
|||
|
||||
def test_get_all_superuser_permissions(self):
|
||||
"A superuser has all permissions. Refs #14795"
|
||||
user = self.UserModel.objects.get(username='test2')
|
||||
user = self.UserModel.objects.get(pk=self.superuser.pk)
|
||||
self.assertEqual(len(user.get_all_permissions()), len(Permission.objects.all()))
|
||||
|
||||
|
||||
|
@ -117,12 +118,12 @@ class ModelBackendTest(BaseModelBackendTest, TestCase):
|
|||
UserModel = User
|
||||
|
||||
def create_users(self):
|
||||
User.objects.create_user(
|
||||
self.user = User.objects.create_user(
|
||||
username='test',
|
||||
email='test@example.com',
|
||||
password='test',
|
||||
)
|
||||
User.objects.create_superuser(
|
||||
self.superuser = User.objects.create_superuser(
|
||||
username='test2',
|
||||
email='test2@example.com',
|
||||
password='test',
|
||||
|
@ -150,13 +151,13 @@ class ExtensionUserModelBackendTest(BaseModelBackendTest, TestCase):
|
|||
UserModel = ExtensionUser
|
||||
|
||||
def create_users(self):
|
||||
ExtensionUser.objects.create_user(
|
||||
self.user = ExtensionUser.objects.create_user(
|
||||
username='test',
|
||||
email='test@example.com',
|
||||
password='test',
|
||||
date_of_birth=date(2006, 4, 25)
|
||||
)
|
||||
ExtensionUser.objects.create_superuser(
|
||||
self.superuser = ExtensionUser.objects.create_superuser(
|
||||
username='test2',
|
||||
email='test2@example.com',
|
||||
password='test',
|
||||
|
@ -164,6 +165,31 @@ class ExtensionUserModelBackendTest(BaseModelBackendTest, TestCase):
|
|||
)
|
||||
|
||||
|
||||
@override_settings(AUTH_USER_MODEL='auth.CustomPermissionsUser')
|
||||
class CustomPermissionsUserModelBackendTest(BaseModelBackendTest, TestCase):
|
||||
"""
|
||||
Tests for the ModelBackend using the CustomPermissionsUser model.
|
||||
|
||||
As with the ExtensionUser test, this isn't a perfect test, because both
|
||||
the User and CustomPermissionsUser are synchronized to the database,
|
||||
which wouldn't ordinary happen in production.
|
||||
"""
|
||||
|
||||
UserModel = CustomPermissionsUser
|
||||
|
||||
def create_users(self):
|
||||
self.user = CustomPermissionsUser.objects.create_user(
|
||||
email='test@example.com',
|
||||
password='test',
|
||||
date_of_birth=date(2006, 4, 25)
|
||||
)
|
||||
self.superuser = CustomPermissionsUser.objects.create_superuser(
|
||||
email='test2@example.com',
|
||||
password='test',
|
||||
date_of_birth=date(1976, 11, 8)
|
||||
)
|
||||
|
||||
|
||||
class TestObj(object):
|
||||
pass
|
||||
|
||||
|
@ -323,3 +349,38 @@ class InActiveUserBackendTest(TestCase):
|
|||
def test_has_module_perms(self):
|
||||
self.assertEqual(self.user1.has_module_perms("app1"), False)
|
||||
self.assertEqual(self.user1.has_module_perms("app2"), False)
|
||||
|
||||
|
||||
class PermissionDeniedBackend(object):
|
||||
"""
|
||||
Always raises PermissionDenied.
|
||||
"""
|
||||
supports_object_permissions = True
|
||||
supports_anonymous_user = True
|
||||
supports_inactive_user = True
|
||||
|
||||
def authenticate(self, username=None, password=None):
|
||||
raise PermissionDenied
|
||||
|
||||
|
||||
@skipIfCustomUser
|
||||
class PermissionDeniedBackendTest(TestCase):
|
||||
"""
|
||||
Tests that other backends are not checked once a backend raises PermissionDenied
|
||||
"""
|
||||
backend = 'django.contrib.auth.tests.auth_backends.PermissionDeniedBackend'
|
||||
|
||||
def setUp(self):
|
||||
self.user1 = User.objects.create_user('test', 'test@example.com', 'test')
|
||||
self.user1.save()
|
||||
|
||||
@override_settings(AUTHENTICATION_BACKENDS=(backend, ) +
|
||||
tuple(settings.AUTHENTICATION_BACKENDS))
|
||||
def test_permission_denied(self):
|
||||
"user is not authenticated after a backend raises permission denied #2550"
|
||||
self.assertEqual(authenticate(username='test', password='test'), None)
|
||||
|
||||
@override_settings(AUTHENTICATION_BACKENDS=tuple(
|
||||
settings.AUTHENTICATION_BACKENDS) + (backend, ))
|
||||
def test_authenticates(self):
|
||||
self.assertEqual(authenticate(username='test', password='test'), self.user1)
|
||||
|
|
|
@ -162,6 +162,8 @@ class BasicTestCase(TestCase):
|
|||
def test_swappable_user(self):
|
||||
"The current user model can be swapped out for another"
|
||||
self.assertEqual(get_user_model(), CustomUser)
|
||||
with self.assertRaises(AttributeError):
|
||||
User.objects.all()
|
||||
|
||||
@override_settings(AUTH_USER_MODEL='badsetting')
|
||||
def test_swappable_user_bad_setting(self):
|
||||
|
|
|
@ -9,6 +9,7 @@ from django.contrib.auth.context_processors import PermWrapper, PermLookupDict
|
|||
from django.db.models import Q
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.utils._os import upath
|
||||
|
||||
|
||||
class MockUser(object):
|
||||
|
@ -63,7 +64,7 @@ class PermWrapperTests(TestCase):
|
|||
@skipIfCustomUser
|
||||
@override_settings(
|
||||
TEMPLATE_DIRS=(
|
||||
os.path.join(os.path.dirname(__file__), 'templates'),
|
||||
os.path.join(os.path.dirname(upath(__file__)), 'templates'),
|
||||
),
|
||||
USE_TZ=False, # required for loading the fixture
|
||||
PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',),
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser, AbstractUser, UserManager
|
||||
from django.contrib.auth.models import (
|
||||
BaseUserManager,
|
||||
AbstractBaseUser,
|
||||
AbstractUser,
|
||||
UserManager,
|
||||
PermissionsMixin
|
||||
)
|
||||
|
||||
|
||||
# The custom User uses email as the unique identifier, and requires
|
||||
|
@ -88,3 +94,53 @@ class ExtensionUser(AbstractUser):
|
|||
|
||||
class Meta:
|
||||
app_label = 'auth'
|
||||
|
||||
|
||||
# The CustomPermissionsUser users email as the identifier, but uses the normal
|
||||
# Django permissions model. This allows us to check that the PermissionsMixin
|
||||
# includes everything that is needed to interact with the ModelBackend.
|
||||
|
||||
class CustomPermissionsUserManager(CustomUserManager):
|
||||
def create_superuser(self, email, password, date_of_birth):
|
||||
u = self.create_user(email, password=password, date_of_birth=date_of_birth)
|
||||
u.is_superuser = True
|
||||
u.save(using=self._db)
|
||||
return u
|
||||
|
||||
|
||||
class CustomPermissionsUser(AbstractBaseUser, PermissionsMixin):
|
||||
email = models.EmailField(verbose_name='email address', max_length=255, unique=True)
|
||||
date_of_birth = models.DateField()
|
||||
|
||||
objects = CustomPermissionsUserManager()
|
||||
|
||||
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
|
||||
|
||||
|
||||
class IsActiveTestUser1(AbstractBaseUser):
|
||||
"""
|
||||
This test user class and derivatives test the default is_active behavior
|
||||
"""
|
||||
username = models.CharField(max_length=30, unique=True)
|
||||
|
||||
objects = BaseUserManager()
|
||||
|
||||
USERNAME_FIELD = 'username'
|
||||
|
||||
class Meta:
|
||||
app_label = 'auth'
|
||||
|
||||
# the is_active attr is provided by AbstractBaseUser
|
||||
|
|
|
@ -3,13 +3,15 @@ from __future__ import unicode_literals
|
|||
import os
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm,
|
||||
PasswordChangeForm, SetPasswordForm, UserChangeForm, PasswordResetForm)
|
||||
PasswordChangeForm, SetPasswordForm, UserChangeForm, PasswordResetForm,
|
||||
ReadOnlyPasswordHashWidget)
|
||||
from django.contrib.auth.tests.utils import skipIfCustomUser
|
||||
from django.core import mail
|
||||
from django.forms.fields import Field, EmailField
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils._os import upath
|
||||
from django.utils import translation
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
|
@ -98,7 +100,9 @@ class AuthenticationFormTest(TestCase):
|
|||
form = AuthenticationForm(None, data)
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertEqual(form.non_field_errors(),
|
||||
[force_text(form.error_messages['invalid_login'])])
|
||||
[force_text(form.error_messages['invalid_login'] % {
|
||||
'username': User._meta.get_field('username').verbose_name
|
||||
})])
|
||||
|
||||
def test_inactive_user(self):
|
||||
# The user is inactive.
|
||||
|
@ -282,6 +286,14 @@ class UserChangeFormTest(TestCase):
|
|||
self.assertTrue(form.is_valid())
|
||||
self.assertEqual(form.cleaned_data['password'], 'sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161')
|
||||
|
||||
def test_bug_19349_bound_password_field(self):
|
||||
user = User.objects.get(username='testclient')
|
||||
form = UserChangeForm(data={}, instance=user)
|
||||
# When rendering the bound password field,
|
||||
# ReadOnlyPasswordHashWidget needs the initial
|
||||
# value to render correctly
|
||||
self.assertEqual(form.initial['password'], form['password'].value())
|
||||
|
||||
|
||||
@skipIfCustomUser
|
||||
@override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
|
||||
|
@ -322,7 +334,7 @@ class PasswordResetFormTest(TestCase):
|
|||
self.assertEqual(form.cleaned_data['email'], email)
|
||||
|
||||
def test_custom_email_subject(self):
|
||||
template_path = os.path.join(os.path.dirname(__file__), 'templates')
|
||||
template_path = os.path.join(os.path.dirname(upath(__file__)), 'templates')
|
||||
with self.settings(TEMPLATE_DIRS=(template_path,)):
|
||||
data = {'email': 'testclient@example.com'}
|
||||
form = PasswordResetForm(data)
|
||||
|
@ -362,3 +374,13 @@ class PasswordResetFormTest(TestCase):
|
|||
self.assertFalse(form.is_valid())
|
||||
self.assertEqual(form["email"].errors,
|
||||
[_("The user account associated with this email address cannot reset the password.")])
|
||||
|
||||
|
||||
class ReadOnlyPasswordHashWidgetTest(TestCase):
|
||||
|
||||
def test_bug_19349_render_with_none_value(self):
|
||||
# Rendering the widget with value set to None
|
||||
# mustn't raise an exception.
|
||||
widget = ReadOnlyPasswordHashWidget()
|
||||
html = widget.render(name='password', value=None, attrs={})
|
||||
self.assertIn(_("No password set."), html)
|
||||
|
|
|
@ -2,30 +2,23 @@ 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 import CustomUser
|
||||
from django.contrib.auth.tests.utils import skipIfCustomUser
|
||||
from django.test import TransactionTestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@skipIfCustomUser
|
||||
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.objects.create_user('test', 'test@example.com', 'test')
|
||||
|
||||
# User not in database
|
||||
self.assertTrue(check_password({}, 'unknown', '') is None)
|
||||
|
@ -33,15 +26,43 @@ class ModWsgiHandlerTestCase(TransactionTestCase):
|
|||
# Valid user with correct password
|
||||
self.assertTrue(check_password({}, 'test', 'test'))
|
||||
|
||||
# correct password, but user is inactive
|
||||
User.objects.filter(username='test').update(is_active=False)
|
||||
self.assertFalse(check_password({}, 'test', 'test'))
|
||||
|
||||
# Valid user with incorrect password
|
||||
self.assertFalse(check_password({}, 'test', 'incorrect'))
|
||||
|
||||
@override_settings(AUTH_USER_MODEL='auth.CustomUser')
|
||||
def test_check_password_custom_user(self):
|
||||
"""
|
||||
Verify that check_password returns the correct values as per
|
||||
http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms#Apache_Authentication_Provider
|
||||
|
||||
with custom user installed
|
||||
"""
|
||||
|
||||
CustomUser.objects.create_user('test@example.com', '1990-01-01', 'test')
|
||||
|
||||
# User not in database
|
||||
self.assertTrue(check_password({}, 'unknown', '') is None)
|
||||
|
||||
# Valid user with correct password'
|
||||
self.assertTrue(check_password({}, 'test@example.com', 'test'))
|
||||
|
||||
# Valid user with incorrect password
|
||||
self.assertFalse(check_password({}, 'test@example.com', '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
|
||||
"""
|
||||
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)
|
||||
|
||||
# User not in database
|
||||
self.assertEqual(groups_for_user({}, 'unknown'), [])
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import (Group, User, SiteProfileNotAvailable,
|
||||
UserManager)
|
||||
from django.contrib.auth.tests.utils import skipIfCustomUser
|
||||
|
@ -98,3 +99,36 @@ class UserManagerTestCase(TestCase):
|
|||
self.assertRaisesMessage(ValueError,
|
||||
'The given username must be set',
|
||||
User.objects.create_user, username='')
|
||||
|
||||
|
||||
class IsActiveTestCase(TestCase):
|
||||
"""
|
||||
Tests the behavior of the guaranteed is_active attribute
|
||||
"""
|
||||
|
||||
@skipIfCustomUser
|
||||
def test_builtin_user_isactive(self):
|
||||
user = User.objects.create(username='foo', email='foo@bar.com')
|
||||
# is_active is true by default
|
||||
self.assertEqual(user.is_active, True)
|
||||
user.is_active = False
|
||||
user.save()
|
||||
user_fetched = User.objects.get(pk=user.pk)
|
||||
# the is_active flag is saved
|
||||
self.assertFalse(user_fetched.is_active)
|
||||
|
||||
@override_settings(AUTH_USER_MODEL='auth.IsActiveTestUser1')
|
||||
def test_is_active_field_default(self):
|
||||
"""
|
||||
tests that the default value for is_active is provided
|
||||
"""
|
||||
UserModel = get_user_model()
|
||||
user = UserModel(username='foo')
|
||||
self.assertEqual(user.is_active, True)
|
||||
# you can set the attribute - but it will not save
|
||||
user.is_active = False
|
||||
# there should be no problem saving - but the attribute is not saved
|
||||
user.save()
|
||||
user_fetched = UserModel.objects.get(pk=user.pk)
|
||||
# the attribute is always true for newly retrieved instance
|
||||
self.assertEqual(user_fetched.is_active, True)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
from datetime import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.backends import RemoteUserBackend
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.contrib.auth.tests.utils import skipIfCustomUser
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
@ -23,7 +24,7 @@ class RemoteUserTest(TestCase):
|
|||
self.curr_middleware = settings.MIDDLEWARE_CLASSES
|
||||
self.curr_auth = settings.AUTHENTICATION_BACKENDS
|
||||
settings.MIDDLEWARE_CLASSES += (self.middleware,)
|
||||
settings.AUTHENTICATION_BACKENDS = (self.backend,)
|
||||
settings.AUTHENTICATION_BACKENDS += (self.backend,)
|
||||
|
||||
def test_no_remote_user(self):
|
||||
"""
|
||||
|
@ -97,6 +98,26 @@ class RemoteUserTest(TestCase):
|
|||
response = self.client.get('/remote_user/', REMOTE_USER=self.known_user)
|
||||
self.assertEqual(default_login, response.context['user'].last_login)
|
||||
|
||||
def test_header_disappears(self):
|
||||
"""
|
||||
Tests that a logged in user is logged out automatically when
|
||||
the REMOTE_USER header disappears during the same browser session.
|
||||
"""
|
||||
User.objects.create(username='knownuser')
|
||||
# Known user authenticates
|
||||
response = self.client.get('/remote_user/', REMOTE_USER=self.known_user)
|
||||
self.assertEqual(response.context['user'].username, 'knownuser')
|
||||
# During the session, the REMOTE_USER header disappears. Should trigger logout.
|
||||
response = self.client.get('/remote_user/')
|
||||
self.assertEqual(response.context['user'].is_anonymous(), True)
|
||||
# verify the remoteuser middleware will not remove a user
|
||||
# authenticated via another backend
|
||||
User.objects.create_user(username='modeluser', password='foo')
|
||||
self.client.login(username='modeluser', password='foo')
|
||||
authenticate(username='modeluser', password='foo')
|
||||
response = self.client.get('/remote_user/')
|
||||
self.assertEqual(response.context['user'].username, 'modeluser')
|
||||
|
||||
def tearDown(self):
|
||||
"""Restores settings to avoid breaking other tests."""
|
||||
settings.MIDDLEWARE_CLASSES = self.curr_middleware
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
unicode: {{ user }}
|
||||
id: {{ user.id }}
|
||||
id: {{ user.pk }}
|
||||
username: {{ user.username }}
|
||||
url: {% url 'userpage' user %}
|
||||
|
|
|
@ -11,6 +11,7 @@ from django.http import QueryDict
|
|||
from django.utils.encoding import force_text
|
||||
from django.utils.html import escape
|
||||
from django.utils.http import urlquote
|
||||
from django.utils._os import upath
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
|
@ -27,7 +28,7 @@ from django.contrib.auth.tests.utils import skipIfCustomUser
|
|||
LANGUAGE_CODE='en',
|
||||
TEMPLATE_LOADERS=global_settings.TEMPLATE_LOADERS,
|
||||
TEMPLATE_DIRS=(
|
||||
os.path.join(os.path.dirname(__file__), 'templates'),
|
||||
os.path.join(os.path.dirname(upath(__file__)), 'templates'),
|
||||
),
|
||||
USE_TZ=False,
|
||||
PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',),
|
||||
|
@ -115,6 +116,8 @@ class PasswordResetTest(AuthViewsTestCase):
|
|||
self.assertTrue("http://adminsite.com" in mail.outbox[0].body)
|
||||
self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email)
|
||||
|
||||
# Skip any 500 handler action (like sending more mail...)
|
||||
@override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True)
|
||||
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
|
||||
|
@ -131,6 +134,8 @@ class PasswordResetTest(AuthViewsTestCase):
|
|||
)
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
||||
# Skip any 500 handler action (like sending more mail...)
|
||||
@override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True)
|
||||
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):
|
||||
|
@ -243,7 +248,9 @@ class ChangePasswordTest(AuthViewsTestCase):
|
|||
'username': 'testclient',
|
||||
'password': password,
|
||||
})
|
||||
self.assertContainsEscaped(response, AuthenticationForm.error_messages['invalid_login'])
|
||||
self.assertContainsEscaped(response, AuthenticationForm.error_messages['invalid_login'] % {
|
||||
'username': User._meta.get_field('username').verbose_name
|
||||
})
|
||||
|
||||
def logout(self):
|
||||
response = self.client.get('/logout/')
|
||||
|
|
|
@ -58,7 +58,7 @@ class PasswordResetTokenGenerator(object):
|
|||
# Ensure results are consistent across DB backends
|
||||
login_timestamp = user.last_login.replace(microsecond=0, tzinfo=None)
|
||||
|
||||
value = (six.text_type(user.id) + user.password +
|
||||
value = (six.text_type(user.pk) + user.password +
|
||||
six.text_type(login_timestamp) + six.text_type(timestamp))
|
||||
hash = salted_hmac(key_salt, value).hexdigest()[::2]
|
||||
return "%s-%s" % (ts_b36, hash)
|
||||
|
|
|
@ -7,7 +7,7 @@ from django.conf import settings
|
|||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponseRedirect, QueryDict
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.http import base36_to_int
|
||||
from django.utils.http import base36_to_int, is_safe_url
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.shortcuts import resolve_url
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
|
@ -37,18 +37,12 @@ def login(request, template_name='registration/login.html',
|
|||
if request.method == "POST":
|
||||
form = authentication_form(data=request.POST)
|
||||
if form.is_valid():
|
||||
# Use default setting if redirect_to is empty
|
||||
if not redirect_to:
|
||||
redirect_to = settings.LOGIN_REDIRECT_URL
|
||||
redirect_to = resolve_url(redirect_to)
|
||||
|
||||
netloc = urlparse(redirect_to)[1]
|
||||
# Heavier security check -- don't allow redirection to a different
|
||||
# host.
|
||||
if netloc and netloc != request.get_host():
|
||||
# Ensure the user-originating redirection url is safe.
|
||||
if not is_safe_url(url=redirect_to, host=request.get_host()):
|
||||
redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL)
|
||||
|
||||
# Okay, security checks complete. Log the user in.
|
||||
# Okay, security check complete. Log the user in.
|
||||
auth_login(request, form.get_user())
|
||||
|
||||
if request.session.test_cookie_worked():
|
||||
|
@ -82,27 +76,27 @@ def logout(request, next_page=None,
|
|||
Logs out the user and displays 'You are logged out' message.
|
||||
"""
|
||||
auth_logout(request)
|
||||
redirect_to = request.REQUEST.get(redirect_field_name, '')
|
||||
if redirect_to:
|
||||
netloc = urlparse(redirect_to)[1]
|
||||
# Security check -- don't allow redirection to a different host.
|
||||
if not (netloc and netloc != request.get_host()):
|
||||
return HttpResponseRedirect(redirect_to)
|
||||
|
||||
if next_page is None:
|
||||
current_site = get_current_site(request)
|
||||
context = {
|
||||
'site': current_site,
|
||||
'site_name': current_site.name,
|
||||
'title': _('Logged out')
|
||||
}
|
||||
if extra_context is not None:
|
||||
context.update(extra_context)
|
||||
return TemplateResponse(request, template_name, context,
|
||||
current_app=current_app)
|
||||
else:
|
||||
if redirect_field_name in request.REQUEST:
|
||||
next_page = request.REQUEST[redirect_field_name]
|
||||
# Security check -- don't allow redirection to a different host.
|
||||
if not is_safe_url(url=next_page, host=request.get_host()):
|
||||
next_page = request.path
|
||||
|
||||
if next_page:
|
||||
# Redirect to this page until the session has been cleared.
|
||||
return HttpResponseRedirect(next_page or request.path)
|
||||
return HttpResponseRedirect(next_page)
|
||||
|
||||
current_site = get_current_site(request)
|
||||
context = {
|
||||
'site': current_site,
|
||||
'site_name': current_site.name,
|
||||
'title': _('Logged out')
|
||||
}
|
||||
if extra_context is not None:
|
||||
context.update(extra_context)
|
||||
return TemplateResponse(request, template_name, context,
|
||||
current_app=current_app)
|
||||
|
||||
|
||||
def logout_then_login(request, login_url=None, current_app=None, extra_context=None):
|
||||
|
@ -206,7 +200,7 @@ def password_reset_confirm(request, uidb36=None, token=None,
|
|||
post_reset_redirect = reverse('django.contrib.auth.views.password_reset_complete')
|
||||
try:
|
||||
uid_int = base36_to_int(uidb36)
|
||||
user = UserModel.objects.get(id=uid_int)
|
||||
user = UserModel.objects.get(pk=uid_int)
|
||||
except (ValueError, OverflowError, UserModel.DoesNotExist):
|
||||
user = None
|
||||
|
||||
|
|
|
@ -20,9 +20,9 @@ def get_comment_app():
|
|||
# Try to import the package
|
||||
try:
|
||||
package = import_module(comments_app)
|
||||
except ImportError:
|
||||
except ImportError as e:
|
||||
raise ImproperlyConfigured("The COMMENTS_APP setting refers to "\
|
||||
"a non-existing package.")
|
||||
"a non-existing package. (%s)" % e)
|
||||
|
||||
return package
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from django.dispatch import Signal
|
|||
# Sent just before a comment will be posted (after it's been approved and
|
||||
# moderated; this can be used to modify the comment (in place) with posting
|
||||
# details or other such actions. If any receiver returns False the comment will be
|
||||
# discarded and a 403 (not allowed) response. This signal is sent at more or less
|
||||
# discarded and a 400 response. This signal is sent at more or less
|
||||
# the same time (just before, actually) as the Comment object's pre-save signal,
|
||||
# except that the HTTP request is sent along with this signal.
|
||||
comment_will_be_posted = Signal(providing_args=["comment", "request"])
|
||||
|
|
|
@ -44,9 +44,6 @@ def post_comment(request, next=None, using=None):
|
|||
if not data.get('email', ''):
|
||||
data["email"] = request.user.email
|
||||
|
||||
# Check to see if the POST data overrides the view's next argument.
|
||||
next = data.get("next", next)
|
||||
|
||||
# Look up the object we're trying to comment about
|
||||
ctype = data.get("content_type")
|
||||
object_pk = data.get("object_pk")
|
||||
|
@ -100,7 +97,7 @@ def post_comment(request, next=None, using=None):
|
|||
template_list, {
|
||||
"comment": form.data.get("comment", ""),
|
||||
"form": form,
|
||||
"next": next,
|
||||
"next": data.get("next", next),
|
||||
},
|
||||
RequestContext(request, {})
|
||||
)
|
||||
|
@ -131,7 +128,8 @@ def post_comment(request, next=None, using=None):
|
|||
request=request
|
||||
)
|
||||
|
||||
return next_redirect(data, next, comment_done, c=comment._get_pk_val())
|
||||
return next_redirect(request, fallback=next or 'comments-comment-done',
|
||||
c=comment._get_pk_val())
|
||||
|
||||
comment_done = confirmation_view(
|
||||
template="comments/posted.html",
|
||||
|
|
|
@ -10,7 +10,6 @@ from django.shortcuts import get_object_or_404, render_to_response
|
|||
from django.views.decorators.csrf import csrf_protect
|
||||
|
||||
|
||||
|
||||
@csrf_protect
|
||||
@login_required
|
||||
def flag(request, comment_id, next=None):
|
||||
|
@ -27,7 +26,8 @@ def flag(request, comment_id, next=None):
|
|||
# Flag on POST
|
||||
if request.method == 'POST':
|
||||
perform_flag(request, comment)
|
||||
return next_redirect(request.POST.copy(), next, flag_done, c=comment.pk)
|
||||
return next_redirect(request, fallback=next or 'comments-flag-done',
|
||||
c=comment.pk)
|
||||
|
||||
# Render a form on GET
|
||||
else:
|
||||
|
@ -54,7 +54,8 @@ def delete(request, comment_id, next=None):
|
|||
if request.method == 'POST':
|
||||
# Flag the comment as deleted instead of actually deleting it.
|
||||
perform_delete(request, comment)
|
||||
return next_redirect(request.POST.copy(), next, delete_done, c=comment.pk)
|
||||
return next_redirect(request, fallback=next or 'comments-delete-done',
|
||||
c=comment.pk)
|
||||
|
||||
# Render a form on GET
|
||||
else:
|
||||
|
@ -81,7 +82,8 @@ def approve(request, comment_id, next=None):
|
|||
if request.method == 'POST':
|
||||
# Flag the comment as approved.
|
||||
perform_approve(request, comment)
|
||||
return next_redirect(request.POST.copy(), next, approve_done, c=comment.pk)
|
||||
return next_redirect(request, fallback=next or 'comments-approve-done',
|
||||
c=comment.pk)
|
||||
|
||||
# Render a form on GET
|
||||
else:
|
||||
|
|
|
@ -9,25 +9,26 @@ except ImportError: # Python 2
|
|||
from urllib import urlencode
|
||||
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.core import urlresolvers
|
||||
from django.shortcuts import render_to_response
|
||||
from django.shortcuts import render_to_response, resolve_url
|
||||
from django.template import RequestContext
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.contrib import comments
|
||||
from django.utils.http import is_safe_url
|
||||
|
||||
def next_redirect(data, default, default_view, **get_kwargs):
|
||||
def next_redirect(request, fallback, **get_kwargs):
|
||||
"""
|
||||
Handle the "where should I go next?" part of comment views.
|
||||
|
||||
The next value could be a kwarg to the function (``default``), or a
|
||||
``?next=...`` GET arg, or the URL of a given view (``default_view``). See
|
||||
The next value could be a
|
||||
``?next=...`` GET arg or the URL of a given view (``fallback``). See
|
||||
the view modules for examples.
|
||||
|
||||
Returns an ``HttpResponseRedirect``.
|
||||
"""
|
||||
next = data.get("next", default)
|
||||
if next is None:
|
||||
next = urlresolvers.reverse(default_view)
|
||||
next = request.POST.get('next')
|
||||
if not is_safe_url(url=next, host=request.get_host()):
|
||||
next = resolve_url(fallback)
|
||||
|
||||
if get_kwargs:
|
||||
if '#' in next:
|
||||
tmp = next.rsplit('#', 1)
|
||||
|
|
|
@ -5,20 +5,19 @@ from __future__ import unicode_literals
|
|||
|
||||
from collections import defaultdict
|
||||
from functools import partial
|
||||
from operator import attrgetter
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import connection
|
||||
from django.db.models import signals
|
||||
from django.db import models, router, DEFAULT_DB_ALIAS
|
||||
from django.db.models.fields.related import RelatedField, Field, ManyToManyRel
|
||||
from django.db.models.loading import get_model
|
||||
from django.forms import ModelForm
|
||||
from django.forms.models import BaseModelFormSet, modelformset_factory, save_instance
|
||||
from django.contrib.admin.options import InlineModelAdmin, flatten_fieldsets
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.encoding import smart_text
|
||||
|
||||
|
||||
class GenericForeignKey(object):
|
||||
"""
|
||||
Provides a generic relation to any object through content-type/object-id
|
||||
|
@ -52,9 +51,6 @@ class GenericForeignKey(object):
|
|||
kwargs[self.fk_field] = value._get_pk_val()
|
||||
|
||||
def get_content_type(self, obj=None, id=None, using=None):
|
||||
# Convenience function using get_model avoids a circular import when
|
||||
# using this model
|
||||
ContentType = get_model("contenttypes", "contenttype")
|
||||
if obj:
|
||||
return ContentType.objects.db_manager(obj._state.db).get_for_model(obj)
|
||||
elif id:
|
||||
|
@ -209,18 +205,16 @@ class GenericRelation(RelatedField, Field):
|
|||
# same db_type as well.
|
||||
return None
|
||||
|
||||
def extra_filters(self, pieces, pos, negate):
|
||||
def get_content_type(self):
|
||||
"""
|
||||
Return an extra filter to the queryset so that the results are filtered
|
||||
on the appropriate content type.
|
||||
Returns the content type associated with this field's model.
|
||||
"""
|
||||
if negate:
|
||||
return []
|
||||
ContentType = get_model("contenttypes", "contenttype")
|
||||
content_type = ContentType.objects.get_for_model(self.model)
|
||||
prefix = "__".join(pieces[:pos + 1])
|
||||
return [("%s__%s" % (prefix, self.content_type_field_name),
|
||||
content_type)]
|
||||
return ContentType.objects.get_for_model(self.model)
|
||||
|
||||
def get_extra_join_sql(self, connection, qn, lhs_alias, rhs_alias):
|
||||
extra_col = self.rel.to._meta.get_field_by_name(self.content_type_field_name)[0].column
|
||||
contenttype = self.get_content_type().pk
|
||||
return " AND %s.%s = %%s" % (qn(rhs_alias), qn(extra_col)), [contenttype]
|
||||
|
||||
def bulk_related_objects(self, objs, using=DEFAULT_DB_ALIAS):
|
||||
"""
|
||||
|
@ -251,9 +245,6 @@ class ReverseGenericRelatedObjectsDescriptor(object):
|
|||
if instance is None:
|
||||
return self
|
||||
|
||||
# This import is done here to avoid circular import importing this module
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
# Dynamically create a class that subclasses the related model's
|
||||
# default manager.
|
||||
rel_model = self.field.rel.to
|
||||
|
@ -329,8 +320,11 @@ def create_generic_related_manager(superclass):
|
|||
set(obj._get_pk_val() for obj in instances)
|
||||
}
|
||||
qs = super(GenericRelatedObjectManager, self).get_query_set().using(db).filter(**query)
|
||||
# We (possibly) need to convert object IDs to the type of the
|
||||
# instances' PK in order to match up instances:
|
||||
object_id_converter = instances[0]._meta.pk.to_python
|
||||
return (qs,
|
||||
attrgetter(self.object_id_field_name),
|
||||
lambda relobj: object_id_converter(getattr(relobj, self.object_id_field_name)),
|
||||
lambda obj: obj._get_pk_val(),
|
||||
False,
|
||||
self.prefetch_cache_name)
|
||||
|
@ -381,8 +375,6 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
|
|||
|
||||
def __init__(self, data=None, files=None, instance=None, save_as_new=None,
|
||||
prefix=None, queryset=None):
|
||||
# Avoid a circular import.
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
opts = self.model._meta
|
||||
self.instance = instance
|
||||
self.rel_name = '-'.join((
|
||||
|
@ -411,8 +403,6 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
|
|||
))
|
||||
|
||||
def save_new(self, form, commit=True):
|
||||
# Avoid a circular import.
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
kwargs = {
|
||||
self.ct_field.get_attname(): ContentType.objects.get_for_model(self.instance).pk,
|
||||
self.ct_fk_field.get_attname(): self.instance.pk,
|
||||
|
@ -434,8 +424,6 @@ def generic_inlineformset_factory(model, form=ModelForm,
|
|||
defaults ``content_type`` and ``object_id`` respectively.
|
||||
"""
|
||||
opts = model._meta
|
||||
# Avoid a circular import.
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
# if there is no field called `ct_field` let the exception propagate
|
||||
ct_field = opts.get_field(ct_field)
|
||||
if not isinstance(ct_field, models.ForeignKey) or ct_field.rel.to != ContentType:
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import DEFAULT_DB_ALIAS, router
|
||||
from django.db.models import get_apps, get_models, signals
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils import six
|
||||
from django.utils.six.moves import input
|
||||
|
||||
def update_contenttypes(app, created_models, verbosity=2, **kwargs):
|
||||
|
||||
def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, **kwargs):
|
||||
"""
|
||||
Creates content types for models in the given app, removing any model
|
||||
entries that no longer have a matching model class.
|
||||
"""
|
||||
if not router.allow_syncdb(db, ContentType):
|
||||
return
|
||||
|
||||
ContentType.objects.clear_cache()
|
||||
app_models = get_models(app)
|
||||
if not app_models:
|
||||
|
@ -19,10 +24,11 @@ def update_contenttypes(app, created_models, verbosity=2, **kwargs):
|
|||
(model._meta.object_name.lower(), model)
|
||||
for model in app_models
|
||||
)
|
||||
|
||||
# Get all the content types
|
||||
content_types = dict(
|
||||
(ct.model, ct)
|
||||
for ct in ContentType.objects.filter(app_label=app_label)
|
||||
for ct in ContentType.objects.using(db).filter(app_label=app_label)
|
||||
)
|
||||
to_remove = [
|
||||
ct
|
||||
|
@ -30,7 +36,7 @@ def update_contenttypes(app, created_models, verbosity=2, **kwargs):
|
|||
if model_name not in app_models
|
||||
]
|
||||
|
||||
cts = ContentType.objects.bulk_create([
|
||||
cts = [
|
||||
ContentType(
|
||||
name=smart_text(model._meta.verbose_name_raw),
|
||||
app_label=app_label,
|
||||
|
@ -38,7 +44,8 @@ def update_contenttypes(app, created_models, verbosity=2, **kwargs):
|
|||
)
|
||||
for (model_name, model) in six.iteritems(app_models)
|
||||
if model_name not in content_types
|
||||
])
|
||||
]
|
||||
ContentType.objects.using(db).bulk_create(cts)
|
||||
if verbosity >= 2:
|
||||
for ct in cts:
|
||||
print("Adding content type '%s | %s'" % (ct.app_label, ct.model))
|
||||
|
@ -71,6 +78,7 @@ If you're unsure, answer 'no'.
|
|||
if verbosity >= 2:
|
||||
print("Stale content types remain.")
|
||||
|
||||
|
||||
def update_all_contenttypes(verbosity=2, **kwargs):
|
||||
for app in get_apps():
|
||||
update_contenttypes(app, None, verbosity, **kwargs)
|
||||
|
|
|
@ -35,7 +35,7 @@ class FlatpageForm(forms.ModelForm):
|
|||
for site in sites:
|
||||
if same_url.filter(sites=site).exists():
|
||||
raise forms.ValidationError(
|
||||
_('Flatpage with url %(url)s already exists for site %(site)s' %
|
||||
{'url': url, 'site': site}))
|
||||
_('Flatpage with url %(url)s already exists for site %(site)s') %
|
||||
{'url': url, 'site': site})
|
||||
|
||||
return super(FlatpageForm, self).clean()
|
||||
|
|
|
@ -14,6 +14,7 @@ from django.contrib.formtools.wizard import FormWizard
|
|||
from django.test import TestCase
|
||||
from django.test.html import parse_html
|
||||
from django.test.utils import override_settings
|
||||
from django.utils._os import upath
|
||||
from django.utils import unittest
|
||||
|
||||
from django.contrib.formtools.tests.wizard import *
|
||||
|
@ -36,7 +37,7 @@ class TestFormPreview(preview.FormPreview):
|
|||
|
||||
@override_settings(
|
||||
TEMPLATE_DIRS=(
|
||||
os.path.join(os.path.dirname(__file__), 'templates'),
|
||||
os.path.join(os.path.dirname(upath(__file__)), 'templates'),
|
||||
),
|
||||
)
|
||||
class PreviewTests(TestCase):
|
||||
|
@ -214,7 +215,7 @@ class DummyRequest(http.HttpRequest):
|
|||
@override_settings(
|
||||
SECRET_KEY="123",
|
||||
TEMPLATE_DIRS=(
|
||||
os.path.join(os.path.dirname(__file__), 'templates'),
|
||||
os.path.join(os.path.dirname(upath(__file__)), 'templates'),
|
||||
),
|
||||
)
|
||||
class WizardTests(TestCase):
|
||||
|
|
|
@ -9,6 +9,7 @@ from django.conf import settings
|
|||
from django.contrib.auth.models import User
|
||||
from django.contrib.formtools.wizard.views import CookieWizardView
|
||||
from django.contrib.formtools.tests.wizard.forms import UserForm, UserFormSet
|
||||
from django.utils._os import upath
|
||||
|
||||
|
||||
class WizardTests(object):
|
||||
|
@ -72,6 +73,10 @@ class WizardTests(object):
|
|||
self.assertEqual(response.context['wizard']['steps'].current, 'form2')
|
||||
self.assertEqual(response.context.get('another_var', None), True)
|
||||
|
||||
# ticket #19025: `form` should be included in context
|
||||
form = response.context_data['wizard']['form']
|
||||
self.assertEqual(response.context_data['form'], form)
|
||||
|
||||
def test_form_finish(self):
|
||||
response = self.client.get(self.wizard_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -82,7 +87,7 @@ class WizardTests(object):
|
|||
self.assertEqual(response.context['wizard']['steps'].current, 'form2')
|
||||
|
||||
post_data = self.wizard_step_data[1]
|
||||
post_data['form2-file1'] = open(__file__, 'rb')
|
||||
post_data['form2-file1'] = open(upath(__file__), 'rb')
|
||||
response = self.client.post(self.wizard_url, post_data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.context['wizard']['steps'].current, 'form3')
|
||||
|
@ -95,7 +100,7 @@ class WizardTests(object):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
all_data = response.context['form_list']
|
||||
with open(__file__, 'rb') as f:
|
||||
with open(upath(__file__), 'rb') as f:
|
||||
self.assertEqual(all_data[1]['file1'].read(), f.read())
|
||||
all_data[1]['file1'].close()
|
||||
del all_data[1]['file1']
|
||||
|
@ -114,7 +119,7 @@ class WizardTests(object):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
post_data = self.wizard_step_data[1]
|
||||
with open(__file__, 'rb') as post_file:
|
||||
with open(upath(__file__), 'rb') as post_file:
|
||||
post_data['form2-file1'] = post_file
|
||||
response = self.client.post(self.wizard_url, post_data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -126,7 +131,7 @@ class WizardTests(object):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
all_data = response.context['all_cleaned_data']
|
||||
with open(__file__, 'rb') as f:
|
||||
with open(upath(__file__), 'rb') as f:
|
||||
self.assertEqual(all_data['file1'].read(), f.read())
|
||||
all_data['file1'].close()
|
||||
del all_data['file1']
|
||||
|
@ -146,7 +151,7 @@ class WizardTests(object):
|
|||
|
||||
post_data = self.wizard_step_data[1]
|
||||
post_data['form2-file1'].close()
|
||||
post_data['form2-file1'] = open(__file__, 'rb')
|
||||
post_data['form2-file1'] = open(upath(__file__), 'rb')
|
||||
response = self.client.post(self.wizard_url, post_data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
@ -174,7 +179,7 @@ class WizardTests(object):
|
|||
|
||||
post_data = self.wizard_step_data[1]
|
||||
post_data['form2-file1'].close()
|
||||
post_data['form2-file1'] = open(__file__, 'rb')
|
||||
post_data['form2-file1'] = open(upath(__file__), 'rb')
|
||||
response = self.client.post(self.wizard_url, post_data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.context['wizard']['steps'].current, 'form3')
|
||||
|
@ -287,7 +292,7 @@ class WizardTestKwargs(TestCase):
|
|||
self.wizard_step_data[0]['form1-user'] = self.testuser.pk
|
||||
|
||||
def test_template(self):
|
||||
templates = os.path.join(os.path.dirname(__file__), 'templates')
|
||||
templates = os.path.join(os.path.dirname(upath(__file__)), 'templates')
|
||||
with self.settings(
|
||||
TEMPLATE_DIRS=list(settings.TEMPLATE_DIRS) + [templates]):
|
||||
response = self.client.get(self.wizard_url)
|
||||
|
|
|
@ -69,7 +69,9 @@ class BaseStorage(object):
|
|||
wizard_files = self.data[self.step_files_key].get(step, {})
|
||||
|
||||
if wizard_files and not self.file_storage:
|
||||
raise NoFileStorageConfigured
|
||||
raise NoFileStorageConfigured(
|
||||
"You need to define 'file_storage' in your "
|
||||
"wizard view in order to handle file uploads.")
|
||||
|
||||
files = {}
|
||||
for field, field_dict in six.iteritems(wizard_files):
|
||||
|
@ -81,7 +83,9 @@ class BaseStorage(object):
|
|||
|
||||
def set_step_files(self, step, files):
|
||||
if files and not self.file_storage:
|
||||
raise NoFileStorageConfigured
|
||||
raise NoFileStorageConfigured(
|
||||
"You need to define 'file_storage' in your "
|
||||
"wizard view in order to handle file uploads.")
|
||||
|
||||
if step not in self.data[self.step_files_key]:
|
||||
self.data[self.step_files_key][step] = {}
|
||||
|
|
|
@ -174,7 +174,9 @@ class WizardView(TemplateView):
|
|||
for field in six.itervalues(form.base_fields):
|
||||
if (isinstance(field, forms.FileField) and
|
||||
not hasattr(cls, 'file_storage')):
|
||||
raise NoFileStorageConfigured
|
||||
raise NoFileStorageConfigured(
|
||||
"You need to define 'file_storage' in your "
|
||||
"wizard view in order to handle file uploads.")
|
||||
|
||||
# build the kwargs for the wizardview instances
|
||||
kwargs['form_list'] = init_form_list
|
||||
|
@ -436,8 +438,8 @@ class WizardView(TemplateView):
|
|||
def get_all_cleaned_data(self):
|
||||
"""
|
||||
Returns a merged dictionary of all step cleaned_data dictionaries.
|
||||
If a step contains a `FormSet`, the key will be prefixed with formset
|
||||
and contain a list of the formset cleaned_data dictionaries.
|
||||
If a step contains a `FormSet`, the key will be prefixed with
|
||||
'formset-' and contain a list of the formset cleaned_data dictionaries.
|
||||
"""
|
||||
cleaned_data = {}
|
||||
for form_key in self.get_form_list():
|
||||
|
@ -458,8 +460,8 @@ class WizardView(TemplateView):
|
|||
def get_cleaned_data_for_step(self, step):
|
||||
"""
|
||||
Returns the cleaned data for a given `step`. Before returning the
|
||||
cleaned data, the stored values are being revalidated through the
|
||||
form. If the data doesn't validate, None will be returned.
|
||||
cleaned data, the stored values are revalidated through the form.
|
||||
If the data doesn't validate, None will be returned.
|
||||
"""
|
||||
if step in self.form_list:
|
||||
form_obj = self.get_form(step=step,
|
||||
|
@ -528,7 +530,7 @@ class WizardView(TemplateView):
|
|||
context.update({'another_var': True})
|
||||
return context
|
||||
"""
|
||||
context = super(WizardView, self).get_context_data(**kwargs)
|
||||
context = super(WizardView, self).get_context_data(form=form, **kwargs)
|
||||
context.update(self.storage.extra_data)
|
||||
context['wizard'] = {
|
||||
'form': form,
|
||||
|
|
|
@ -7,29 +7,7 @@ class GeoSQLCompiler(BaseGeoSQLCompiler, SQLCompiler):
|
|||
pass
|
||||
|
||||
class SQLInsertCompiler(compiler.SQLInsertCompiler, GeoSQLCompiler):
|
||||
def placeholder(self, field, val):
|
||||
if field is None:
|
||||
# A field value of None means the value is raw.
|
||||
return val
|
||||
elif hasattr(field, 'get_placeholder'):
|
||||
# Some fields (e.g. geo fields) need special munging before
|
||||
# they can be inserted.
|
||||
ph = field.get_placeholder(val, self.connection)
|
||||
if ph == 'NULL':
|
||||
# If the placeholder returned is 'NULL', then we need to
|
||||
# to remove None from the Query parameters. Specifically,
|
||||
# cx_Oracle will assume a CHAR type when a placeholder ('%s')
|
||||
# is used for columns of MDSYS.SDO_GEOMETRY. Thus, we use
|
||||
# 'NULL' for the value, and remove None from the query params.
|
||||
# See also #10888.
|
||||
param_idx = self.query.columns.index(field.column)
|
||||
params = list(self.query.params)
|
||||
params.pop(param_idx)
|
||||
self.query.params = tuple(params)
|
||||
return ph
|
||||
else:
|
||||
# Return the common case for the placeholder
|
||||
return '%s'
|
||||
pass
|
||||
|
||||
class SQLDeleteCompiler(compiler.SQLDeleteCompiler, GeoSQLCompiler):
|
||||
pass
|
||||
|
|
|
@ -288,3 +288,12 @@ class OracleOperations(DatabaseOperations, BaseSpatialOperations):
|
|||
def spatial_ref_sys(self):
|
||||
from django.contrib.gis.db.backends.oracle.models import SpatialRefSys
|
||||
return SpatialRefSys
|
||||
|
||||
def modify_insert_params(self, placeholders, params):
|
||||
"""Drop out insert parameters for NULL placeholder. Needed for Oracle Spatial
|
||||
backend due to #10888
|
||||
"""
|
||||
# This code doesn't work for bulk insert cases.
|
||||
assert len(placeholders) == 1
|
||||
return [[param for pholder,param
|
||||
in six.moves.zip(placeholders[0], params[0]) if pholder != 'NULL'], ]
|
||||
|
|
|
@ -1,12 +1,23 @@
|
|||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class PostGISCreation(DatabaseCreation):
|
||||
geom_index_type = 'GIST'
|
||||
geom_index_ops = 'GIST_GEOMETRY_OPS'
|
||||
geom_index_ops_nd = 'GIST_GEOMETRY_OPS_ND'
|
||||
|
||||
@cached_property
|
||||
def template_postgis(self):
|
||||
template_postgis = getattr(settings, 'POSTGIS_TEMPLATE', 'template_postgis')
|
||||
cursor = self.connection.cursor()
|
||||
cursor.execute('SELECT 1 FROM pg_database WHERE datname = %s LIMIT 1;', (template_postgis,))
|
||||
if cursor.fetchone():
|
||||
return template_postgis
|
||||
return None
|
||||
|
||||
def sql_indexes_for_field(self, model, f, style):
|
||||
"Return any spatial index creation SQL for the field."
|
||||
from django.contrib.gis.db.models.fields import GeometryField
|
||||
|
@ -67,5 +78,19 @@ class PostGISCreation(DatabaseCreation):
|
|||
return output
|
||||
|
||||
def sql_table_creation_suffix(self):
|
||||
postgis_template = getattr(settings, 'POSTGIS_TEMPLATE', 'template_postgis')
|
||||
return ' TEMPLATE %s' % self.connection.ops.quote_name(postgis_template)
|
||||
if self.template_postgis is not None:
|
||||
return ' TEMPLATE %s' % (
|
||||
self.connection.ops.quote_name(self.template_postgis),)
|
||||
return ''
|
||||
|
||||
def _create_test_db(self, verbosity, autoclobber):
|
||||
test_database_name = super(PostGISCreation, self)._create_test_db(verbosity, autoclobber)
|
||||
if self.template_postgis is None:
|
||||
# Connect to the test database in order to create the postgis extension
|
||||
self.connection.close()
|
||||
self.connection.settings_dict["NAME"] = test_database_name
|
||||
cursor = self.connection.cursor()
|
||||
cursor.execute("CREATE EXTENSION postgis")
|
||||
cursor.connection.commit()
|
||||
|
||||
return test_database_name
|
||||
|
|
|
@ -36,29 +36,23 @@ class DatabaseWrapper(SQLiteDatabaseWrapper):
|
|||
self.creation = SpatiaLiteCreation(self)
|
||||
self.introspection = SpatiaLiteIntrospection(self)
|
||||
|
||||
def _cursor(self):
|
||||
if self.connection is None:
|
||||
self._sqlite_create_connection()
|
||||
|
||||
## From here on, customized for GeoDjango ##
|
||||
|
||||
# Enabling extension loading on the SQLite connection.
|
||||
try:
|
||||
self.connection.enable_load_extension(True)
|
||||
except AttributeError:
|
||||
raise ImproperlyConfigured('The pysqlite library does not support C extension loading. '
|
||||
'Both SQLite and pysqlite must be configured to allow '
|
||||
'the loading of extensions to use SpatiaLite.'
|
||||
)
|
||||
|
||||
# Loading the SpatiaLite library extension on the connection, and returning
|
||||
# the created cursor.
|
||||
cur = self.connection.cursor(factory=SQLiteCursorWrapper)
|
||||
try:
|
||||
cur.execute("SELECT load_extension(%s)", (self.spatialite_lib,))
|
||||
except Exception as msg:
|
||||
raise ImproperlyConfigured('Unable to load the SpatiaLite library extension '
|
||||
'"%s" because: %s' % (self.spatialite_lib, msg))
|
||||
return cur
|
||||
else:
|
||||
return self.connection.cursor(factory=SQLiteCursorWrapper)
|
||||
def get_new_connection(self, conn_params):
|
||||
conn = super(DatabaseWrapper, self).get_new_connection(conn_params)
|
||||
# Enabling extension loading on the SQLite connection.
|
||||
try:
|
||||
conn.enable_load_extension(True)
|
||||
except AttributeError:
|
||||
raise ImproperlyConfigured(
|
||||
'The pysqlite library does not support C extension loading. '
|
||||
'Both SQLite and pysqlite must be configured to allow '
|
||||
'the loading of extensions to use SpatiaLite.')
|
||||
# Loading the SpatiaLite library extension on the connection, and returning
|
||||
# the created cursor.
|
||||
cur = conn.cursor(factory=SQLiteCursorWrapper)
|
||||
try:
|
||||
cur.execute("SELECT load_extension(%s)", (self.spatialite_lib,))
|
||||
except Exception as msg:
|
||||
raise ImproperlyConfigured('Unable to load the SpatiaLite library extension '
|
||||
'"%s" because: %s' % (self.spatialite_lib, msg))
|
||||
cur.close()
|
||||
return conn
|
||||
|
|
|
@ -760,8 +760,10 @@ class GeoQuerySet(QuerySet):
|
|||
self.query.add_select_related([field_name])
|
||||
compiler = self.query.get_compiler(self.db)
|
||||
compiler.pre_sql_setup()
|
||||
rel_table, rel_col = self.query.related_select_cols[self.query.related_select_fields.index(geo_field)]
|
||||
return compiler._field_column(geo_field, rel_table)
|
||||
for (rel_table, rel_col), field in self.query.related_select_cols:
|
||||
if field == geo_field:
|
||||
return compiler._field_column(geo_field, rel_table)
|
||||
raise ValueError("%r not in self.query.related_select_cols" % geo_field)
|
||||
elif not geo_field in opts.local_fields:
|
||||
# This geographic field is inherited from another model, so we have to
|
||||
# use the db table for the _parent_ model instead.
|
||||
|
|
|
@ -39,7 +39,7 @@ class GeoSQLCompiler(compiler.SQLCompiler):
|
|||
if self.query.select:
|
||||
only_load = self.deferred_to_columns()
|
||||
# This loop customized for GeoQuery.
|
||||
for col, field in zip(self.query.select, self.query.select_fields):
|
||||
for col, field in self.query.select:
|
||||
if isinstance(col, (list, tuple)):
|
||||
alias, column = col
|
||||
table = self.query.alias_map[alias].table_name
|
||||
|
@ -85,7 +85,7 @@ class GeoSQLCompiler(compiler.SQLCompiler):
|
|||
])
|
||||
|
||||
# This loop customized for GeoQuery.
|
||||
for (table, col), field in zip(self.query.related_select_cols, self.query.related_select_fields):
|
||||
for (table, col), field in self.query.related_select_cols:
|
||||
r = self.get_field_select(field, table, col)
|
||||
if with_aliases and col in col_aliases:
|
||||
c_alias = 'Col%d' % len(col_aliases)
|
||||
|
@ -101,7 +101,7 @@ class GeoSQLCompiler(compiler.SQLCompiler):
|
|||
return result
|
||||
|
||||
def get_default_columns(self, with_aliases=False, col_aliases=None,
|
||||
start_alias=None, opts=None, as_pairs=False, local_only=False):
|
||||
start_alias=None, opts=None, as_pairs=False, from_parent=None):
|
||||
"""
|
||||
Computes the default columns for selecting every field in the base
|
||||
model. Will sometimes be called to pull in related models (e.g. via
|
||||
|
@ -127,7 +127,7 @@ class GeoSQLCompiler(compiler.SQLCompiler):
|
|||
if start_alias:
|
||||
seen = {None: start_alias}
|
||||
for field, model in opts.get_fields_with_model():
|
||||
if local_only and model is not None:
|
||||
if from_parent and model is not None and issubclass(from_parent, model):
|
||||
continue
|
||||
if start_alias:
|
||||
try:
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue