Merge remote-tracking branch 'django/master' into t3011
This commit is contained in:
commit
57ac6e3d32
3
AUTHORS
3
AUTHORS
|
@ -31,6 +31,8 @@ The PRIMARY AUTHORS are (and/or have been):
|
||||||
* Claude Paroz
|
* Claude Paroz
|
||||||
* Anssi Kääriäinen
|
* Anssi Kääriäinen
|
||||||
* Florian Apolloner
|
* Florian Apolloner
|
||||||
|
* Jeremy Dunck
|
||||||
|
* Bryan Veloso
|
||||||
|
|
||||||
More information on the main contributors to Django can be found in
|
More information on the main contributors to Django can be found in
|
||||||
docs/internals/committers.txt.
|
docs/internals/committers.txt.
|
||||||
|
@ -167,7 +169,6 @@ answer newbie questions, and generally made Django that much better:
|
||||||
dready <wil@mojipage.com>
|
dready <wil@mojipage.com>
|
||||||
Maximillian Dornseif <md@hudora.de>
|
Maximillian Dornseif <md@hudora.de>
|
||||||
Daniel Duan <DaNmarner@gmail.com>
|
Daniel Duan <DaNmarner@gmail.com>
|
||||||
Jeremy Dunck <http://dunck.us/>
|
|
||||||
Andrew Durdin <adurdin@gmail.com>
|
Andrew Durdin <adurdin@gmail.com>
|
||||||
dusk@woofle.net
|
dusk@woofle.net
|
||||||
Andy Dustman <farcepest@gmail.com>
|
Andy Dustman <farcepest@gmail.com>
|
||||||
|
|
|
@ -24,7 +24,9 @@ def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIE
|
||||||
if test_func(request.user):
|
if test_func(request.user):
|
||||||
return view_func(request, *args, **kwargs)
|
return view_func(request, *args, **kwargs)
|
||||||
path = request.build_absolute_uri()
|
path = request.build_absolute_uri()
|
||||||
resolved_login_url = resolve_url(login_url or settings.LOGIN_URL)
|
# urlparse chokes on lazy objects in Python 3, force to str
|
||||||
|
resolved_login_url = force_str(
|
||||||
|
resolve_url(login_url or settings.LOGIN_URL))
|
||||||
# If the login url is the same scheme and net location then just
|
# If the login url is the same scheme and net location then just
|
||||||
# use the path as the "next" url.
|
# use the path as the "next" url.
|
||||||
login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
|
login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
|
||||||
|
@ -33,7 +35,8 @@ def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIE
|
||||||
(not login_netloc or login_netloc == current_netloc)):
|
(not login_netloc or login_netloc == current_netloc)):
|
||||||
path = request.get_full_path()
|
path = request.get_full_path()
|
||||||
from django.contrib.auth.views import redirect_to_login
|
from django.contrib.auth.views import redirect_to_login
|
||||||
return redirect_to_login(path, login_url, redirect_field_name)
|
return redirect_to_login(
|
||||||
|
path, resolved_login_url, redirect_field_name)
|
||||||
return _wrapped_view
|
return _wrapped_view
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ from django.utils.translation import ugettext, ugettext_lazy as _
|
||||||
|
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth.hashers import UNUSABLE_PASSWORD, is_password_usable, identify_hasher
|
from django.contrib.auth.hashers import UNUSABLE_PASSWORD, identify_hasher
|
||||||
from django.contrib.auth.tokens import default_token_generator
|
from django.contrib.auth.tokens import default_token_generator
|
||||||
from django.contrib.sites.models import get_current_site
|
from django.contrib.sites.models import get_current_site
|
||||||
|
|
||||||
|
@ -24,22 +24,22 @@ mask_password = lambda p: "%s%s" % (p[:UNMASKED_DIGITS_TO_SHOW], "*" * max(len(p
|
||||||
class ReadOnlyPasswordHashWidget(forms.Widget):
|
class ReadOnlyPasswordHashWidget(forms.Widget):
|
||||||
def render(self, name, value, attrs):
|
def render(self, name, value, attrs):
|
||||||
encoded = value
|
encoded = value
|
||||||
|
|
||||||
if not is_password_usable(encoded):
|
|
||||||
return "None"
|
|
||||||
|
|
||||||
final_attrs = self.build_attrs(attrs)
|
final_attrs = self.build_attrs(attrs)
|
||||||
|
|
||||||
try:
|
if encoded == '' or encoded == UNUSABLE_PASSWORD:
|
||||||
hasher = identify_hasher(encoded)
|
summary = mark_safe("<strong>%s</strong>" % ugettext("No password set."))
|
||||||
except ValueError:
|
|
||||||
summary = mark_safe("<strong>Invalid password format or unknown hashing algorithm.</strong>")
|
|
||||||
else:
|
else:
|
||||||
summary = format_html_join('',
|
try:
|
||||||
"<strong>{0}</strong>: {1} ",
|
hasher = identify_hasher(encoded)
|
||||||
((ugettext(key), value)
|
except ValueError:
|
||||||
for key, value in hasher.safe_summary(encoded).items())
|
summary = mark_safe("<strong>%s</strong>" % ugettext(
|
||||||
)
|
"Invalid password format or unknown hashing algorithm."))
|
||||||
|
else:
|
||||||
|
summary = format_html_join('',
|
||||||
|
"<strong>{0}</strong>: {1} ",
|
||||||
|
((ugettext(key), value)
|
||||||
|
for key, value in hasher.safe_summary(encoded).items())
|
||||||
|
)
|
||||||
|
|
||||||
return format_html("<div{0}>{1}</div>", flatatt(final_attrs), summary)
|
return format_html("<div{0}>{1}</div>", flatatt(final_attrs), summary)
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,13 @@ def reset_hashers(**kwargs):
|
||||||
|
|
||||||
|
|
||||||
def is_password_usable(encoded):
|
def is_password_usable(encoded):
|
||||||
return (encoded is not None and encoded != UNUSABLE_PASSWORD)
|
if encoded is None or encoded == UNUSABLE_PASSWORD:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
hasher = identify_hasher(encoded)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def check_password(password, encoded, setter=None, preferred='default'):
|
def check_password(password, encoded, setter=None, preferred='default'):
|
||||||
|
|
|
@ -240,23 +240,29 @@ class UserChangeFormTest(TestCase):
|
||||||
# Just check we can create it
|
# Just check we can create it
|
||||||
form = MyUserForm({})
|
form = MyUserForm({})
|
||||||
|
|
||||||
|
def test_unsuable_password(self):
|
||||||
|
user = User.objects.get(username='empty_password')
|
||||||
|
user.set_unusable_password()
|
||||||
|
user.save()
|
||||||
|
form = UserChangeForm(instance=user)
|
||||||
|
self.assertIn(_("No password set."), form.as_table())
|
||||||
|
|
||||||
def test_bug_17944_empty_password(self):
|
def test_bug_17944_empty_password(self):
|
||||||
user = User.objects.get(username='empty_password')
|
user = User.objects.get(username='empty_password')
|
||||||
form = UserChangeForm(instance=user)
|
form = UserChangeForm(instance=user)
|
||||||
# Just check that no error is raised.
|
self.assertIn(_("No password set."), form.as_table())
|
||||||
form.as_table()
|
|
||||||
|
|
||||||
def test_bug_17944_unmanageable_password(self):
|
def test_bug_17944_unmanageable_password(self):
|
||||||
user = User.objects.get(username='unmanageable_password')
|
user = User.objects.get(username='unmanageable_password')
|
||||||
form = UserChangeForm(instance=user)
|
form = UserChangeForm(instance=user)
|
||||||
# Just check that no error is raised.
|
self.assertIn(_("Invalid password format or unknown hashing algorithm."),
|
||||||
form.as_table()
|
form.as_table())
|
||||||
|
|
||||||
def test_bug_17944_unknown_password_algorithm(self):
|
def test_bug_17944_unknown_password_algorithm(self):
|
||||||
user = User.objects.get(username='unknown_password')
|
user = User.objects.get(username='unknown_password')
|
||||||
form = UserChangeForm(instance=user)
|
form = UserChangeForm(instance=user)
|
||||||
# Just check that no error is raised.
|
self.assertIn(_("Invalid password format or unknown hashing algorithm."),
|
||||||
form.as_table()
|
form.as_table())
|
||||||
|
|
||||||
|
|
||||||
@skipIfCustomUser
|
@skipIfCustomUser
|
||||||
|
|
|
@ -100,6 +100,10 @@ class TestUtilsHashPass(unittest.TestCase):
|
||||||
self.assertRaises(ValueError, doit)
|
self.assertRaises(ValueError, doit)
|
||||||
self.assertRaises(ValueError, identify_hasher, "lolcat$salt$hash")
|
self.assertRaises(ValueError, identify_hasher, "lolcat$salt$hash")
|
||||||
|
|
||||||
|
def test_bad_encoded(self):
|
||||||
|
self.assertFalse(is_password_usable('letmein_badencoded'))
|
||||||
|
self.assertFalse(is_password_usable(''))
|
||||||
|
|
||||||
def test_low_level_pkbdf2(self):
|
def test_low_level_pkbdf2(self):
|
||||||
hasher = PBKDF2PasswordHasher()
|
hasher = PBKDF2PasswordHasher()
|
||||||
encoded = hasher.encode('letmein', 'seasalt')
|
encoded = hasher.encode('letmein', 'seasalt')
|
||||||
|
|
|
@ -90,8 +90,6 @@ class BaseSpatialOperations(object):
|
||||||
|
|
||||||
# For quoting column values, rather than columns.
|
# For quoting column values, rather than columns.
|
||||||
def geo_quote_name(self, name):
|
def geo_quote_name(self, name):
|
||||||
if isinstance(name, six.text_type):
|
|
||||||
name = name.encode('ascii')
|
|
||||||
return "'%s'" % name
|
return "'%s'" % name
|
||||||
|
|
||||||
# GeometryField operations
|
# GeometryField operations
|
||||||
|
|
|
@ -3,20 +3,6 @@ A collection of utility routines and classes used by the spatial
|
||||||
backends.
|
backends.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.utils import six
|
|
||||||
|
|
||||||
def gqn(val):
|
|
||||||
"""
|
|
||||||
The geographic quote name function; used for quoting tables and
|
|
||||||
geometries (they use single rather than the double quotes of the
|
|
||||||
backend quotename function).
|
|
||||||
"""
|
|
||||||
if isinstance(val, six.string_types):
|
|
||||||
if isinstance(val, six.text_type): val = val.encode('ascii')
|
|
||||||
return "'%s'" % val
|
|
||||||
else:
|
|
||||||
return str(val)
|
|
||||||
|
|
||||||
class SpatialOperation(object):
|
class SpatialOperation(object):
|
||||||
"""
|
"""
|
||||||
Base class for generating spatial SQL.
|
Base class for generating spatial SQL.
|
||||||
|
|
|
@ -181,7 +181,11 @@ class DataSourceTest(unittest.TestCase):
|
||||||
|
|
||||||
# Making sure the SpatialReference is as expected.
|
# Making sure the SpatialReference is as expected.
|
||||||
if hasattr(source, 'srs_wkt'):
|
if hasattr(source, 'srs_wkt'):
|
||||||
self.assertEqual(source.srs_wkt, g.srs.wkt)
|
self.assertEqual(
|
||||||
|
source.srs_wkt,
|
||||||
|
# Depending on lib versions, WGS_84 might be WGS_1984
|
||||||
|
g.srs.wkt.replace('SPHEROID["WGS_84"', 'SPHEROID["WGS_1984"')
|
||||||
|
)
|
||||||
|
|
||||||
def test06_spatial_filter(self):
|
def test06_spatial_filter(self):
|
||||||
"Testing the Layer.spatial_filter property."
|
"Testing the Layer.spatial_filter property."
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import json
|
||||||
from binascii import b2a_hex
|
from binascii import b2a_hex
|
||||||
try:
|
try:
|
||||||
from django.utils.six.moves import cPickle as pickle
|
from django.utils.six.moves import cPickle as pickle
|
||||||
|
@ -111,8 +112,9 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin):
|
||||||
for g in self.geometries.json_geoms:
|
for g in self.geometries.json_geoms:
|
||||||
geom = OGRGeometry(g.wkt)
|
geom = OGRGeometry(g.wkt)
|
||||||
if not hasattr(g, 'not_equal'):
|
if not hasattr(g, 'not_equal'):
|
||||||
self.assertEqual(g.json, geom.json)
|
# Loading jsons to prevent decimal differences
|
||||||
self.assertEqual(g.json, geom.geojson)
|
self.assertEqual(json.loads(g.json), json.loads(geom.json))
|
||||||
|
self.assertEqual(json.loads(g.json), json.loads(geom.geojson))
|
||||||
self.assertEqual(OGRGeometry(g.wkt), OGRGeometry(geom.json))
|
self.assertEqual(OGRGeometry(g.wkt), OGRGeometry(geom.json))
|
||||||
|
|
||||||
def test02_points(self):
|
def test02_points(self):
|
||||||
|
|
|
@ -110,7 +110,7 @@ def geos_version_info():
|
||||||
is a release candidate (and what number release candidate), and the C API
|
is a release candidate (and what number release candidate), and the C API
|
||||||
version.
|
version.
|
||||||
"""
|
"""
|
||||||
ver = geos_version()
|
ver = geos_version().decode()
|
||||||
m = version_regex.match(ver)
|
m = version_regex.match(ver)
|
||||||
if not m: raise GEOSException('Could not parse version info string "%s"' % ver)
|
if not m: raise GEOSException('Could not parse version info string "%s"' % ver)
|
||||||
return dict((key, m.group(key)) for key in ('version', 'release_candidate', 'capi_version', 'major', 'minor', 'subminor'))
|
return dict((key, m.group(key)) for key in ('version', 'release_candidate', 'capi_version', 'major', 'minor', 'subminor'))
|
||||||
|
|
|
@ -215,15 +215,18 @@ class ListMixin(object):
|
||||||
"Standard list reverse method"
|
"Standard list reverse method"
|
||||||
self[:] = self[-1::-1]
|
self[:] = self[-1::-1]
|
||||||
|
|
||||||
def sort(self, cmp=cmp, key=None, reverse=False):
|
def sort(self, cmp=None, key=None, reverse=False):
|
||||||
"Standard list sort method"
|
"Standard list sort method"
|
||||||
if key:
|
if key:
|
||||||
temp = [(key(v),v) for v in self]
|
temp = [(key(v),v) for v in self]
|
||||||
temp.sort(cmp=cmp, key=lambda x: x[0], reverse=reverse)
|
temp.sort(key=lambda x: x[0], reverse=reverse)
|
||||||
self[:] = [v[1] for v in temp]
|
self[:] = [v[1] for v in temp]
|
||||||
else:
|
else:
|
||||||
temp = list(self)
|
temp = list(self)
|
||||||
temp.sort(cmp=cmp, reverse=reverse)
|
if cmp is not None:
|
||||||
|
temp.sort(cmp=cmp, reverse=reverse)
|
||||||
|
else:
|
||||||
|
temp.sort(reverse=reverse)
|
||||||
self[:] = temp
|
self[:] = temp
|
||||||
|
|
||||||
### Private routines ###
|
### Private routines ###
|
||||||
|
|
|
@ -16,7 +16,8 @@ test_suites = [
|
||||||
def suite():
|
def suite():
|
||||||
"Builds a test suite for the GEOS tests."
|
"Builds a test suite for the GEOS tests."
|
||||||
s = TestSuite()
|
s = TestSuite()
|
||||||
map(s.addTest, test_suites)
|
for suite in test_suites:
|
||||||
|
s.addTest(suite)
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def run(verbosity=1):
|
def run(verbosity=1):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import ctypes
|
import ctypes
|
||||||
|
import json
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from django.contrib.gis.geos import (GEOSException, GEOSIndexError, GEOSGeometry,
|
from django.contrib.gis.geos import (GEOSException, GEOSIndexError, GEOSGeometry,
|
||||||
|
@ -204,8 +205,9 @@ class GEOSTest(unittest.TestCase, TestDataMixin):
|
||||||
for g in self.geometries.json_geoms:
|
for g in self.geometries.json_geoms:
|
||||||
geom = GEOSGeometry(g.wkt)
|
geom = GEOSGeometry(g.wkt)
|
||||||
if not hasattr(g, 'not_equal'):
|
if not hasattr(g, 'not_equal'):
|
||||||
self.assertEqual(g.json, geom.json)
|
# Loading jsons to prevent decimal differences
|
||||||
self.assertEqual(g.json, geom.geojson)
|
self.assertEqual(json.loads(g.json), json.loads(geom.json))
|
||||||
|
self.assertEqual(json.loads(g.json), json.loads(geom.geojson))
|
||||||
self.assertEqual(GEOSGeometry(g.wkt), GEOSGeometry(geom.json))
|
self.assertEqual(GEOSGeometry(g.wkt), GEOSGeometry(geom.json))
|
||||||
|
|
||||||
def test_fromfile(self):
|
def test_fromfile(self):
|
||||||
|
|
|
@ -55,14 +55,14 @@ class ListMixinTest(unittest.TestCase):
|
||||||
|
|
||||||
def lists_of_len(self, length=None):
|
def lists_of_len(self, length=None):
|
||||||
if length is None: length = self.limit
|
if length is None: length = self.limit
|
||||||
pl = range(length)
|
pl = list(range(length))
|
||||||
return pl, self.listType(pl)
|
return pl, self.listType(pl)
|
||||||
|
|
||||||
def limits_plus(self, b):
|
def limits_plus(self, b):
|
||||||
return range(-self.limit - b, self.limit + b)
|
return range(-self.limit - b, self.limit + b)
|
||||||
|
|
||||||
def step_range(self):
|
def step_range(self):
|
||||||
return range(-1 - self.limit, 0) + range(1, 1 + self.limit)
|
return list(range(-1 - self.limit, 0)) + list(range(1, 1 + self.limit))
|
||||||
|
|
||||||
def test01_getslice(self):
|
def test01_getslice(self):
|
||||||
'Slice retrieval'
|
'Slice retrieval'
|
||||||
|
@ -160,13 +160,13 @@ class ListMixinTest(unittest.TestCase):
|
||||||
del pl[i:j]
|
del pl[i:j]
|
||||||
del ul[i:j]
|
del ul[i:j]
|
||||||
self.assertEqual(pl[:], ul[:], 'del slice [%d:%d]' % (i,j))
|
self.assertEqual(pl[:], ul[:], 'del slice [%d:%d]' % (i,j))
|
||||||
for k in range(-Len - 1,0) + range(1,Len):
|
for k in list(range(-Len - 1, 0)) + list(range(1, Len)):
|
||||||
pl, ul = self.lists_of_len(Len)
|
pl, ul = self.lists_of_len(Len)
|
||||||
del pl[i:j:k]
|
del pl[i:j:k]
|
||||||
del ul[i:j:k]
|
del ul[i:j:k]
|
||||||
self.assertEqual(pl[:], ul[:], 'del slice [%d:%d:%d]' % (i,j,k))
|
self.assertEqual(pl[:], ul[:], 'del slice [%d:%d:%d]' % (i,j,k))
|
||||||
|
|
||||||
for k in range(-Len - 1,0) + range(1,Len):
|
for k in list(range(-Len - 1, 0)) + list(range(1, Len)):
|
||||||
pl, ul = self.lists_of_len(Len)
|
pl, ul = self.lists_of_len(Len)
|
||||||
del pl[:i:k]
|
del pl[:i:k]
|
||||||
del ul[:i:k]
|
del ul[:i:k]
|
||||||
|
@ -177,7 +177,7 @@ class ListMixinTest(unittest.TestCase):
|
||||||
del ul[i::k]
|
del ul[i::k]
|
||||||
self.assertEqual(pl[:], ul[:], 'del slice [%d::%d]' % (i,k))
|
self.assertEqual(pl[:], ul[:], 'del slice [%d::%d]' % (i,k))
|
||||||
|
|
||||||
for k in range(-Len - 1,0) + range(1,Len):
|
for k in list(range(-Len - 1, 0)) + list(range(1, Len)):
|
||||||
pl, ul = self.lists_of_len(Len)
|
pl, ul = self.lists_of_len(Len)
|
||||||
del pl[::k]
|
del pl[::k]
|
||||||
del ul[::k]
|
del ul[::k]
|
||||||
|
@ -320,7 +320,7 @@ class ListMixinTest(unittest.TestCase):
|
||||||
pl.sort()
|
pl.sort()
|
||||||
ul.sort()
|
ul.sort()
|
||||||
self.assertEqual(pl[:], ul[:], 'sort')
|
self.assertEqual(pl[:], ul[:], 'sort')
|
||||||
mid = pl[len(pl) / 2]
|
mid = pl[len(pl) // 2]
|
||||||
pl.sort(key=lambda x: (mid-x)**2)
|
pl.sort(key=lambda x: (mid-x)**2)
|
||||||
ul.sort(key=lambda x: (mid-x)**2)
|
ul.sort(key=lambda x: (mid-x)**2)
|
||||||
self.assertEqual(pl[:], ul[:], 'sort w/ key')
|
self.assertEqual(pl[:], ul[:], 'sort w/ key')
|
||||||
|
@ -330,7 +330,7 @@ class ListMixinTest(unittest.TestCase):
|
||||||
pl.sort(reverse=True)
|
pl.sort(reverse=True)
|
||||||
ul.sort(reverse=True)
|
ul.sort(reverse=True)
|
||||||
self.assertEqual(pl[:], ul[:], 'sort w/ reverse')
|
self.assertEqual(pl[:], ul[:], 'sort w/ reverse')
|
||||||
mid = pl[len(pl) / 2]
|
mid = pl[len(pl) // 2]
|
||||||
pl.sort(key=lambda x: (mid-x)**2)
|
pl.sort(key=lambda x: (mid-x)**2)
|
||||||
ul.sort(key=lambda x: (mid-x)**2)
|
ul.sort(key=lambda x: (mid-x)**2)
|
||||||
self.assertEqual(pl[:], ul[:], 'sort w/ key')
|
self.assertEqual(pl[:], ul[:], 'sort w/ key')
|
||||||
|
@ -338,7 +338,7 @@ class ListMixinTest(unittest.TestCase):
|
||||||
def test_12_arithmetic(self):
|
def test_12_arithmetic(self):
|
||||||
'Arithmetic'
|
'Arithmetic'
|
||||||
pl, ul = self.lists_of_len()
|
pl, ul = self.lists_of_len()
|
||||||
al = range(10,14)
|
al = list(range(10,14))
|
||||||
self.assertEqual(list(pl + al), list(ul + al), 'add')
|
self.assertEqual(list(pl + al), list(ul + al), 'add')
|
||||||
self.assertEqual(type(ul), type(ul + al), 'type of add result')
|
self.assertEqual(type(ul), type(ul + al), 'type of add result')
|
||||||
self.assertEqual(list(al + pl), list(al + ul), 'radd')
|
self.assertEqual(list(al + pl), list(al + ul), 'radd')
|
||||||
|
|
|
@ -8,9 +8,11 @@ from django.utils import unittest
|
||||||
test_srs = ({'srid' : 4326,
|
test_srs = ({'srid' : 4326,
|
||||||
'auth_name' : ('EPSG', True),
|
'auth_name' : ('EPSG', True),
|
||||||
'auth_srid' : 4326,
|
'auth_srid' : 4326,
|
||||||
'srtext' : 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],TOWGS84[0,0,0,0,0,0,0],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]',
|
# Only the beginning, because there are differences depending on installed libs
|
||||||
'srtext14' : 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]',
|
'srtext' : 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84"',
|
||||||
'proj4' : '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs ',
|
'proj4' : ['+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs ',
|
||||||
|
# +ellps=WGS84 has been removed in the 4326 proj string in proj-4.8
|
||||||
|
'+proj=longlat +datum=WGS84 +no_defs '],
|
||||||
'spheroid' : 'WGS 84', 'name' : 'WGS 84',
|
'spheroid' : 'WGS 84', 'name' : 'WGS 84',
|
||||||
'geographic' : True, 'projected' : False, 'spatialite' : True,
|
'geographic' : True, 'projected' : False, 'spatialite' : True,
|
||||||
'ellipsoid' : (6378137.0, 6356752.3, 298.257223563), # From proj's "cs2cs -le" and Wikipedia (semi-minor only)
|
'ellipsoid' : (6378137.0, 6356752.3, 298.257223563), # From proj's "cs2cs -le" and Wikipedia (semi-minor only)
|
||||||
|
@ -19,9 +21,9 @@ test_srs = ({'srid' : 4326,
|
||||||
{'srid' : 32140,
|
{'srid' : 32140,
|
||||||
'auth_name' : ('EPSG', False),
|
'auth_name' : ('EPSG', False),
|
||||||
'auth_srid' : 32140,
|
'auth_srid' : 32140,
|
||||||
'srtext' : 'PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",30.28333333333333],PARAMETER["standard_parallel_2",28.38333333333333],PARAMETER["latitude_of_origin",27.83333333333333],PARAMETER["central_meridian",-99],PARAMETER["false_easting",600000],PARAMETER["false_northing",4000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AUTHORITY["EPSG","32140"]]',
|
'srtext' : 'PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980"',
|
||||||
'srtext14': 'PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",30.28333333333333],PARAMETER["standard_parallel_2",28.38333333333333],PARAMETER["latitude_of_origin",27.83333333333333],PARAMETER["central_meridian",-99],PARAMETER["false_easting",600000],PARAMETER["false_northing",4000000],AUTHORITY["EPSG","32140"],AXIS["X",EAST],AXIS["Y",NORTH]]',
|
'proj4' : ['+proj=lcc +lat_1=30.28333333333333 +lat_2=28.38333333333333 +lat_0=27.83333333333333 +lon_0=-99 +x_0=600000 +y_0=4000000 +ellps=GRS80 +datum=NAD83 +units=m +no_defs ',
|
||||||
'proj4' : '+proj=lcc +lat_1=30.28333333333333 +lat_2=28.38333333333333 +lat_0=27.83333333333333 +lon_0=-99 +x_0=600000 +y_0=4000000 +ellps=GRS80 +datum=NAD83 +units=m +no_defs ',
|
'+proj=lcc +lat_1=30.28333333333333 +lat_2=28.38333333333333 +lat_0=27.83333333333333 +lon_0=-99 +x_0=600000 +y_0=4000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '],
|
||||||
'spheroid' : 'GRS 1980', 'name' : 'NAD83 / Texas South Central',
|
'spheroid' : 'GRS 1980', 'name' : 'NAD83 / Texas South Central',
|
||||||
'geographic' : False, 'projected' : True, 'spatialite' : False,
|
'geographic' : False, 'projected' : True, 'spatialite' : False,
|
||||||
'ellipsoid' : (6378137.0, 6356752.31414, 298.257222101), # From proj's "cs2cs -le" and Wikipedia (semi-minor only)
|
'ellipsoid' : (6378137.0, 6356752.31414, 298.257222101), # From proj's "cs2cs -le" and Wikipedia (semi-minor only)
|
||||||
|
@ -51,17 +53,12 @@ class SpatialRefSysTest(unittest.TestCase):
|
||||||
|
|
||||||
# No proj.4 and different srtext on oracle backends :(
|
# No proj.4 and different srtext on oracle backends :(
|
||||||
if postgis:
|
if postgis:
|
||||||
if connection.ops.spatial_version >= (1, 4, 0):
|
self.assertTrue(srs.wkt.startswith(sd['srtext']))
|
||||||
srtext = sd['srtext14']
|
self.assertTrue(srs.proj4text in sd['proj4'])
|
||||||
else:
|
|
||||||
srtext = sd['srtext']
|
|
||||||
self.assertEqual(srtext, srs.wkt)
|
|
||||||
self.assertEqual(sd['proj4'], srs.proj4text)
|
|
||||||
|
|
||||||
@no_mysql
|
@no_mysql
|
||||||
def test02_osr(self):
|
def test02_osr(self):
|
||||||
"Testing getting OSR objects from SpatialRefSys model objects."
|
"Testing getting OSR objects from SpatialRefSys model objects."
|
||||||
from django.contrib.gis.gdal import GDAL_VERSION
|
|
||||||
for sd in test_srs:
|
for sd in test_srs:
|
||||||
sr = SpatialRefSys.objects.get(srid=sd['srid'])
|
sr = SpatialRefSys.objects.get(srid=sd['srid'])
|
||||||
self.assertEqual(True, sr.spheroid.startswith(sd['spheroid']))
|
self.assertEqual(True, sr.spheroid.startswith(sd['spheroid']))
|
||||||
|
@ -76,15 +73,10 @@ class SpatialRefSysTest(unittest.TestCase):
|
||||||
# Testing the SpatialReference object directly.
|
# Testing the SpatialReference object directly.
|
||||||
if postgis or spatialite:
|
if postgis or spatialite:
|
||||||
srs = sr.srs
|
srs = sr.srs
|
||||||
if GDAL_VERSION <= (1, 8):
|
self.assertTrue(srs.proj4 in sd['proj4'])
|
||||||
self.assertEqual(sd['proj4'], srs.proj4)
|
|
||||||
# No `srtext` field in the `spatial_ref_sys` table in SpatiaLite
|
# No `srtext` field in the `spatial_ref_sys` table in SpatiaLite
|
||||||
if not spatialite:
|
if not spatialite:
|
||||||
if connection.ops.spatial_version >= (1, 4, 0):
|
self.assertTrue(srs.wkt.startswith(sd['srtext']))
|
||||||
srtext = sd['srtext14']
|
|
||||||
else:
|
|
||||||
srtext = sd['srtext']
|
|
||||||
self.assertEqual(srtext, srs.wkt)
|
|
||||||
|
|
||||||
@no_mysql
|
@no_mysql
|
||||||
def test03_ellipsoid(self):
|
def test03_ellipsoid(self):
|
||||||
|
|
|
@ -223,18 +223,17 @@ class WSGIHandler(base.BaseHandler):
|
||||||
set_script_prefix(base.get_script_name(environ))
|
set_script_prefix(base.get_script_name(environ))
|
||||||
signals.request_started.send(sender=self.__class__)
|
signals.request_started.send(sender=self.__class__)
|
||||||
try:
|
try:
|
||||||
try:
|
request = self.request_class(environ)
|
||||||
request = self.request_class(environ)
|
except UnicodeDecodeError:
|
||||||
except UnicodeDecodeError:
|
logger.warning('Bad Request (UnicodeDecodeError)',
|
||||||
logger.warning('Bad Request (UnicodeDecodeError)',
|
exc_info=sys.exc_info(),
|
||||||
exc_info=sys.exc_info(),
|
extra={
|
||||||
extra={
|
'status_code': 400,
|
||||||
'status_code': 400,
|
}
|
||||||
}
|
)
|
||||||
)
|
response = http.HttpResponseBadRequest()
|
||||||
response = http.HttpResponseBadRequest()
|
else:
|
||||||
else:
|
response = self.get_response(request)
|
||||||
response = self.get_response(request)
|
|
||||||
finally:
|
finally:
|
||||||
signals.request_finished.send(sender=self.__class__)
|
signals.request_finished.send(sender=self.__class__)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from optparse import make_option
|
from optparse import make_option
|
||||||
|
from datetime import datetime
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
@ -90,10 +91,12 @@ class Command(BaseCommand):
|
||||||
self.stdout.write("Validating models...\n\n")
|
self.stdout.write("Validating models...\n\n")
|
||||||
self.validate(display_num_errors=True)
|
self.validate(display_num_errors=True)
|
||||||
self.stdout.write((
|
self.stdout.write((
|
||||||
|
"%(started_at)s\n"
|
||||||
"Django version %(version)s, using settings %(settings)r\n"
|
"Django version %(version)s, using settings %(settings)r\n"
|
||||||
"Development server is running at http://%(addr)s:%(port)s/\n"
|
"Development server is running at http://%(addr)s:%(port)s/\n"
|
||||||
"Quit the server with %(quit_command)s.\n"
|
"Quit the server with %(quit_command)s.\n"
|
||||||
) % {
|
) % {
|
||||||
|
"started_at": datetime.now().strftime('%B %d, %Y - %X'),
|
||||||
"version": self.get_version(),
|
"version": self.get_version(),
|
||||||
"settings": settings.SETTINGS_MODULE,
|
"settings": settings.SETTINGS_MODULE,
|
||||||
"addr": self._raw_ipv6 and '[%s]' % self.addr or self.addr,
|
"addr": self._raw_ipv6 and '[%s]' % self.addr or self.addr,
|
||||||
|
|
|
@ -37,6 +37,7 @@ from django.db.backends.mysql.client import DatabaseClient
|
||||||
from django.db.backends.mysql.creation import DatabaseCreation
|
from django.db.backends.mysql.creation import DatabaseCreation
|
||||||
from django.db.backends.mysql.introspection import DatabaseIntrospection
|
from django.db.backends.mysql.introspection import DatabaseIntrospection
|
||||||
from django.db.backends.mysql.validation import DatabaseValidation
|
from django.db.backends.mysql.validation import DatabaseValidation
|
||||||
|
from django.utils.encoding import force_str
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.safestring import SafeBytes, SafeText
|
from django.utils.safestring import SafeBytes, SafeText
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
@ -390,7 +391,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
if settings_dict['NAME']:
|
if settings_dict['NAME']:
|
||||||
kwargs['db'] = settings_dict['NAME']
|
kwargs['db'] = settings_dict['NAME']
|
||||||
if settings_dict['PASSWORD']:
|
if settings_dict['PASSWORD']:
|
||||||
kwargs['passwd'] = settings_dict['PASSWORD']
|
kwargs['passwd'] = force_str(settings_dict['PASSWORD'])
|
||||||
if settings_dict['HOST'].startswith('/'):
|
if settings_dict['HOST'].startswith('/'):
|
||||||
kwargs['unix_socket'] = settings_dict['HOST']
|
kwargs['unix_socket'] = settings_dict['HOST']
|
||||||
elif settings_dict['HOST']:
|
elif settings_dict['HOST']:
|
||||||
|
|
|
@ -13,6 +13,7 @@ from django.db.backends.postgresql_psycopg2.client import DatabaseClient
|
||||||
from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation
|
from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation
|
||||||
from django.db.backends.postgresql_psycopg2.version import get_version
|
from django.db.backends.postgresql_psycopg2.version import get_version
|
||||||
from django.db.backends.postgresql_psycopg2.introspection import DatabaseIntrospection
|
from django.db.backends.postgresql_psycopg2.introspection import DatabaseIntrospection
|
||||||
|
from django.utils.encoding import force_str
|
||||||
from django.utils.log import getLogger
|
from django.utils.log import getLogger
|
||||||
from django.utils.safestring import SafeText, SafeBytes
|
from django.utils.safestring import SafeText, SafeBytes
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
@ -172,7 +173,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
if settings_dict['USER']:
|
if settings_dict['USER']:
|
||||||
conn_params['user'] = settings_dict['USER']
|
conn_params['user'] = settings_dict['USER']
|
||||||
if settings_dict['PASSWORD']:
|
if settings_dict['PASSWORD']:
|
||||||
conn_params['password'] = settings_dict['PASSWORD']
|
conn_params['password'] = force_str(settings_dict['PASSWORD'])
|
||||||
if settings_dict['HOST']:
|
if settings_dict['HOST']:
|
||||||
conn_params['host'] = settings_dict['HOST']
|
conn_params['host'] = settings_dict['HOST']
|
||||||
if settings_dict['PORT']:
|
if settings_dict['PORT']:
|
||||||
|
|
|
@ -199,7 +199,7 @@ class CharField(Field):
|
||||||
|
|
||||||
def widget_attrs(self, widget):
|
def widget_attrs(self, widget):
|
||||||
attrs = super(CharField, self).widget_attrs(widget)
|
attrs = super(CharField, self).widget_attrs(widget)
|
||||||
if self.max_length is not None and isinstance(widget, (TextInput, PasswordInput)):
|
if self.max_length is not None and isinstance(widget, TextInput):
|
||||||
# The HTML attribute is maxlength, not max_length.
|
# The HTML attribute is maxlength, not max_length.
|
||||||
attrs.update({'maxlength': str(self.max_length)})
|
attrs.update({'maxlength': str(self.max_length)})
|
||||||
return attrs
|
return attrs
|
||||||
|
|
|
@ -260,10 +260,17 @@ class Input(Widget):
|
||||||
final_attrs['value'] = force_text(self._format_value(value))
|
final_attrs['value'] = force_text(self._format_value(value))
|
||||||
return format_html('<input{0} />', flatatt(final_attrs))
|
return format_html('<input{0} />', flatatt(final_attrs))
|
||||||
|
|
||||||
|
|
||||||
class TextInput(Input):
|
class TextInput(Input):
|
||||||
input_type = 'text'
|
input_type = 'text'
|
||||||
|
|
||||||
class PasswordInput(Input):
|
def __init__(self, attrs=None):
|
||||||
|
if attrs is not None:
|
||||||
|
self.input_type = attrs.pop('type', self.input_type)
|
||||||
|
super(TextInput, self).__init__(attrs)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordInput(TextInput):
|
||||||
input_type = 'password'
|
input_type = 'password'
|
||||||
|
|
||||||
def __init__(self, attrs=None, render_value=False):
|
def __init__(self, attrs=None, render_value=False):
|
||||||
|
@ -400,9 +407,8 @@ class Textarea(Widget):
|
||||||
flatatt(final_attrs),
|
flatatt(final_attrs),
|
||||||
force_text(value))
|
force_text(value))
|
||||||
|
|
||||||
class DateInput(Input):
|
|
||||||
input_type = 'text'
|
|
||||||
|
|
||||||
|
class DateInput(TextInput):
|
||||||
def __init__(self, attrs=None, format=None):
|
def __init__(self, attrs=None, format=None):
|
||||||
super(DateInput, self).__init__(attrs)
|
super(DateInput, self).__init__(attrs)
|
||||||
if format:
|
if format:
|
||||||
|
@ -431,9 +437,8 @@ class DateInput(Input):
|
||||||
pass
|
pass
|
||||||
return super(DateInput, self)._has_changed(self._format_value(initial), data)
|
return super(DateInput, self)._has_changed(self._format_value(initial), data)
|
||||||
|
|
||||||
class DateTimeInput(Input):
|
|
||||||
input_type = 'text'
|
|
||||||
|
|
||||||
|
class DateTimeInput(TextInput):
|
||||||
def __init__(self, attrs=None, format=None):
|
def __init__(self, attrs=None, format=None):
|
||||||
super(DateTimeInput, self).__init__(attrs)
|
super(DateTimeInput, self).__init__(attrs)
|
||||||
if format:
|
if format:
|
||||||
|
@ -462,9 +467,8 @@ class DateTimeInput(Input):
|
||||||
pass
|
pass
|
||||||
return super(DateTimeInput, self)._has_changed(self._format_value(initial), data)
|
return super(DateTimeInput, self)._has_changed(self._format_value(initial), data)
|
||||||
|
|
||||||
class TimeInput(Input):
|
|
||||||
input_type = 'text'
|
|
||||||
|
|
||||||
|
class TimeInput(TextInput):
|
||||||
def __init__(self, attrs=None, format=None):
|
def __init__(self, attrs=None, format=None):
|
||||||
super(TimeInput, self).__init__(attrs)
|
super(TimeInput, self).__init__(attrs)
|
||||||
if format:
|
if format:
|
||||||
|
|
|
@ -105,7 +105,7 @@ class CsrfViewMiddleware(object):
|
||||||
if getattr(callback, 'csrf_exempt', False):
|
if getattr(callback, 'csrf_exempt', False):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Assume that anything not defined as 'safe' by RC2616 needs protection
|
# Assume that anything not defined as 'safe' by RFC2616 needs protection
|
||||||
if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
|
if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
|
||||||
if getattr(request, '_dont_enforce_csrf_checks', False):
|
if getattr(request, '_dont_enforce_csrf_checks', False):
|
||||||
# Mechanism to turn off CSRF checks for test suite.
|
# Mechanism to turn off CSRF checks for test suite.
|
||||||
|
|
|
@ -531,11 +531,9 @@ def cycle(parser, token):
|
||||||
The optional flag "silent" can be used to prevent the cycle declaration
|
The optional flag "silent" can be used to prevent the cycle declaration
|
||||||
from returning any value::
|
from returning any value::
|
||||||
|
|
||||||
{% cycle 'row1' 'row2' as rowcolors silent %}{# no value here #}
|
|
||||||
{% for o in some_list %}
|
{% for o in some_list %}
|
||||||
<tr class="{% cycle rowcolors %}">{# first value will be "row1" #}
|
{% cycle 'row1' 'row2' as rowcolors silent %}
|
||||||
...
|
<tr class="{{ rowcolors }}">{% include "subtemplate.html " %}</tr>
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -5,8 +5,7 @@ import sys
|
||||||
current_version = sys.version_info
|
current_version = sys.version_info
|
||||||
|
|
||||||
use_workaround = (
|
use_workaround = (
|
||||||
(current_version < (2, 6, 8)) or
|
(current_version < (2, 7, 3)) or
|
||||||
(current_version >= (2, 7) and current_version < (2, 7, 3)) or
|
|
||||||
(current_version >= (3, 0) and current_version < (3, 2, 3))
|
(current_version >= (3, 0) and current_version < (3, 2, 3))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -28,14 +28,3 @@ Indices, glossary and tables
|
||||||
* :ref:`genindex`
|
* :ref:`genindex`
|
||||||
* :ref:`modindex`
|
* :ref:`modindex`
|
||||||
* :ref:`glossary`
|
* :ref:`glossary`
|
||||||
|
|
||||||
Deprecated/obsolete documentation
|
|
||||||
=================================
|
|
||||||
|
|
||||||
The following documentation covers features that have been deprecated or that
|
|
||||||
have been replaced in newer versions of Django.
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
obsolete/index
|
|
||||||
|
|
|
@ -91,8 +91,7 @@ The dynamically-generated admin site is ugly! How can I change it?
|
||||||
We like it, but if you don't agree, you can modify the admin site's
|
We like it, but if you don't agree, you can modify the admin site's
|
||||||
presentation by editing the CSS stylesheet and/or associated image files. The
|
presentation by editing the CSS stylesheet and/or associated image files. The
|
||||||
site is built using semantic HTML and plenty of CSS hooks, so any changes you'd
|
site is built using semantic HTML and plenty of CSS hooks, so any changes you'd
|
||||||
like to make should be possible by editing the stylesheet. We've got a
|
like to make should be possible by editing the stylesheet.
|
||||||
:doc:`guide to the CSS used in the admin </obsolete/admin-css>` to get you started.
|
|
||||||
|
|
||||||
What browsers are supported for using the admin?
|
What browsers are supported for using the admin?
|
||||||
------------------------------------------------
|
------------------------------------------------
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
Django documentation
|
Django documentation
|
||||||
====================
|
====================
|
||||||
|
|
||||||
.. rubric:: Everything you need to know about Django (and then some).
|
.. rubric:: Everything you need to know about Django.
|
||||||
|
|
||||||
Getting help
|
Getting help
|
||||||
============
|
============
|
||||||
|
|
|
@ -379,6 +379,34 @@ Florian Apolloner
|
||||||
.. _Graz University of Technology: http://tugraz.at/
|
.. _Graz University of Technology: http://tugraz.at/
|
||||||
.. _Ubuntuusers webteam: http://wiki.ubuntuusers.de/ubuntuusers/Webteam
|
.. _Ubuntuusers webteam: http://wiki.ubuntuusers.de/ubuntuusers/Webteam
|
||||||
|
|
||||||
|
Jeremy Dunck
|
||||||
|
Jeremy was rescued from corporate IT drudgery by Free Software and, in part,
|
||||||
|
Django. Many of Jeremy's interests center around access to information.
|
||||||
|
|
||||||
|
Jeremy was the lead developer of Pegasus News, one of the first uses of
|
||||||
|
Django outside World Online, and has since joined Votizen, a startup intent
|
||||||
|
on reducing the influence of money in politics.
|
||||||
|
|
||||||
|
He serves as DSF Secretary, organizes and helps organize sprints, cares
|
||||||
|
about the health and equity of the Django community. He has gone an
|
||||||
|
embarrassingly long time without a working blog.
|
||||||
|
|
||||||
|
Jeremy lives in Mountain View, CA, USA.
|
||||||
|
|
||||||
|
`Bryan Veloso`_
|
||||||
|
Bryan found Django 0.96 through a fellow designer who was evangelizing
|
||||||
|
its use. It was his first foray outside of the land that was PHP-based
|
||||||
|
templating. Although he has only ever used Django for personal projects,
|
||||||
|
it is the very reason he considers himself a designer/developer
|
||||||
|
hybrid and is working to further design within the Django community.
|
||||||
|
|
||||||
|
Bryan works as a designer at GitHub by day, and masquerades as a `vlogger`_
|
||||||
|
and `shoutcaster`_ in the after-hours. Bryan lives in Los Angeles, CA, USA.
|
||||||
|
|
||||||
|
.. _bryan veloso: http://avalonstar.com/
|
||||||
|
.. _vlogger: http://youtube.com/bryanveloso/
|
||||||
|
.. _shoutcaster: http://twitch.tv/vlogalonstar/
|
||||||
|
|
||||||
Specialists
|
Specialists
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
@ -403,16 +431,6 @@ Ian Kelly
|
||||||
Matt Boersma
|
Matt Boersma
|
||||||
Matt is also responsible for Django's Oracle support.
|
Matt is also responsible for Django's Oracle support.
|
||||||
|
|
||||||
Jeremy Dunck
|
|
||||||
Jeremy is the lead developer of Pegasus News, a personalized local site based
|
|
||||||
in Dallas, Texas. An early contributor to Greasemonkey and Django, he sees
|
|
||||||
technology as a tool for communication and access to knowledge.
|
|
||||||
|
|
||||||
Jeremy helped kick off GeoDjango development, and is mostly responsible for
|
|
||||||
the serious speed improvements that signals received in Django 1.0.
|
|
||||||
|
|
||||||
Jeremy lives in Dallas, Texas, USA.
|
|
||||||
|
|
||||||
`Simon Meers`_
|
`Simon Meers`_
|
||||||
Simon discovered Django 0.96 during his Computer Science PhD research and
|
Simon discovered Django 0.96 during his Computer Science PhD research and
|
||||||
has been developing with it full-time ever since. His core code
|
has been developing with it full-time ever since. His core code
|
||||||
|
|
|
@ -67,8 +67,7 @@ different needs:
|
||||||
whathaveyou.
|
whathaveyou.
|
||||||
|
|
||||||
* Finally, there's some "specialized" documentation not usually relevant to
|
* Finally, there's some "specialized" documentation not usually relevant to
|
||||||
most developers. This includes the :doc:`release notes </releases/index>`,
|
most developers. This includes the :doc:`release notes </releases/index>` and
|
||||||
:doc:`documentation of obsolete features </obsolete/index>`,
|
|
||||||
:doc:`internals documentation </internals/index>` for those who want to add
|
:doc:`internals documentation </internals/index>` for those who want to add
|
||||||
code to Django itself, and a :doc:`few other things that simply don't fit
|
code to Django itself, and a :doc:`few other things that simply don't fit
|
||||||
elsewhere </misc/index>`.
|
elsewhere </misc/index>`.
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.2 KiB |
|
@ -1,186 +0,0 @@
|
||||||
======================================
|
|
||||||
Customizing the Django admin interface
|
|
||||||
======================================
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
The design of the admin has changed somewhat since this document was
|
|
||||||
written, and parts may not apply any more. This document is no longer
|
|
||||||
maintained since an official API for customizing the Django admin interface
|
|
||||||
is in development.
|
|
||||||
|
|
||||||
Django's dynamic admin interface gives you a fully-functional admin for free
|
|
||||||
with no hand-coding required. The dynamic admin is designed to be
|
|
||||||
production-ready, not just a starting point, so you can use it as-is on a real
|
|
||||||
site. While the underlying format of the admin pages is built in to Django, you
|
|
||||||
can customize the look and feel by editing the admin stylesheet and images.
|
|
||||||
|
|
||||||
Here's a quick and dirty overview some of the main styles and classes used in
|
|
||||||
the Django admin CSS.
|
|
||||||
|
|
||||||
Modules
|
|
||||||
=======
|
|
||||||
|
|
||||||
The ``.module`` class is a basic building block for grouping content in the
|
|
||||||
admin. It's generally applied to a ``div`` or a ``fieldset``. It wraps the content
|
|
||||||
group in a box and applies certain styles to the elements within. An ``h2``
|
|
||||||
within a ``div.module`` will align to the top of the ``div`` as a header for the
|
|
||||||
whole group.
|
|
||||||
|
|
||||||
.. image:: _images/module.png
|
|
||||||
:alt: Example use of module class on admin homepage
|
|
||||||
|
|
||||||
Column Types
|
|
||||||
============
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
All admin pages (except the dashboard) are fluid-width. All fixed-width
|
|
||||||
classes from previous Django versions have been removed.
|
|
||||||
|
|
||||||
The base template for each admin page has a block that defines the column
|
|
||||||
structure for the page. This sets a class on the page content area
|
|
||||||
(``div#content``) so everything on the page knows how wide it should be. There
|
|
||||||
are three column types available.
|
|
||||||
|
|
||||||
colM
|
|
||||||
This is the default column setting for all pages. The "M" stands for "main".
|
|
||||||
Assumes that all content on the page is in one main column
|
|
||||||
(``div#content-main``).
|
|
||||||
colMS
|
|
||||||
This is for pages with one main column and a sidebar on the right. The "S"
|
|
||||||
stands for "sidebar". Assumes that main content is in ``div#content-main``
|
|
||||||
and sidebar content is in ``div#content-related``. This is used on the main
|
|
||||||
admin page.
|
|
||||||
colSM
|
|
||||||
Same as above, with the sidebar on the left. The source order of the columns
|
|
||||||
doesn't matter.
|
|
||||||
|
|
||||||
For instance, you could stick this in a template to make a two-column page with
|
|
||||||
the sidebar on the right:
|
|
||||||
|
|
||||||
.. code-block:: html+django
|
|
||||||
|
|
||||||
{% block coltype %}colMS{% endblock %}
|
|
||||||
|
|
||||||
Text Styles
|
|
||||||
===========
|
|
||||||
|
|
||||||
Font Sizes
|
|
||||||
----------
|
|
||||||
|
|
||||||
Most HTML elements (headers, lists, etc.) have base font sizes in the stylesheet
|
|
||||||
based on context. There are three classes are available for forcing text to a
|
|
||||||
certain size in any context.
|
|
||||||
|
|
||||||
small
|
|
||||||
11px
|
|
||||||
tiny
|
|
||||||
10px
|
|
||||||
mini
|
|
||||||
9px (use sparingly)
|
|
||||||
|
|
||||||
Font Styles and Alignment
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
There are also a few styles for styling text.
|
|
||||||
|
|
||||||
.quiet
|
|
||||||
Sets font color to light gray. Good for side notes in instructions. Combine
|
|
||||||
with ``.small`` or ``.tiny`` for sheer excitement.
|
|
||||||
.help
|
|
||||||
This is a custom class for blocks of inline help text explaining the
|
|
||||||
function of form elements. It makes text smaller and gray, and when applied
|
|
||||||
to ``p`` elements within ``.form-row`` elements (see Form Styles below),
|
|
||||||
it will offset the text to align with the form field. Use this for help
|
|
||||||
text, instead of ``small quiet``. It works on other elements, but try to
|
|
||||||
put the class on a ``p`` whenever you can.
|
|
||||||
.align-left
|
|
||||||
It aligns the text left. Only works on block elements containing inline
|
|
||||||
elements.
|
|
||||||
.align-right
|
|
||||||
Are you paying attention?
|
|
||||||
.nowrap
|
|
||||||
Keeps text and inline objects from wrapping. Comes in handy for table
|
|
||||||
headers you want to stay on one line.
|
|
||||||
|
|
||||||
Floats and Clears
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
float-left
|
|
||||||
floats left
|
|
||||||
float-right
|
|
||||||
floats right
|
|
||||||
clear
|
|
||||||
clears all
|
|
||||||
|
|
||||||
Object Tools
|
|
||||||
============
|
|
||||||
|
|
||||||
Certain actions which apply directly to an object are used in form and
|
|
||||||
changelist pages. These appear in a "toolbar" row above the form or changelist,
|
|
||||||
to the right of the page. The tools are wrapped in a ``ul`` with the class
|
|
||||||
``object-tools``. There are two custom tool types which can be defined with an
|
|
||||||
additional class on the ``a`` for that tool. These are ``.addlink`` and
|
|
||||||
``.viewsitelink``.
|
|
||||||
|
|
||||||
Example from a changelist page:
|
|
||||||
|
|
||||||
.. code-block:: html+django
|
|
||||||
|
|
||||||
<ul class="object-tools">
|
|
||||||
<li><a href="/stories/add/" class="addlink">Add redirect</a></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
.. image:: _images/objecttools_01.png
|
|
||||||
:alt: Object tools on a changelist page
|
|
||||||
|
|
||||||
and from a form page:
|
|
||||||
|
|
||||||
.. code-block:: html+django
|
|
||||||
|
|
||||||
<ul class="object-tools">
|
|
||||||
<li><a href="/history/303/152383/">History</a></li>
|
|
||||||
<li><a href="/r/303/152383/" class="viewsitelink">View on site</a></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
.. image:: _images/objecttools_02.png
|
|
||||||
:alt: Object tools on a form page
|
|
||||||
|
|
||||||
Form Styles
|
|
||||||
===========
|
|
||||||
|
|
||||||
Fieldsets
|
|
||||||
---------
|
|
||||||
|
|
||||||
Admin forms are broken up into groups by ``fieldset`` elements. Each form fieldset
|
|
||||||
should have a class ``.module``. Each fieldset should have a header ``h2`` within the
|
|
||||||
fieldset at the top (except the first group in the form, and in some cases where the
|
|
||||||
group of fields doesn't have a logical label).
|
|
||||||
|
|
||||||
Each fieldset can also take extra classes in addition to ``.module`` to apply
|
|
||||||
appropriate formatting to the group of fields.
|
|
||||||
|
|
||||||
.aligned
|
|
||||||
This will align the labels and inputs side by side on the same line.
|
|
||||||
.wide
|
|
||||||
Used in combination with ``.aligned`` to widen the space available for the
|
|
||||||
labels.
|
|
||||||
|
|
||||||
Form Rows
|
|
||||||
---------
|
|
||||||
|
|
||||||
Each row of the form (within the ``fieldset``) should be enclosed in a ``div``
|
|
||||||
with class ``form-row``. If the field in the row is required, a class of
|
|
||||||
``required`` should also be added to the ``div.form-row``.
|
|
||||||
|
|
||||||
.. image:: _images/formrow.png
|
|
||||||
:alt: Example use of form-row class
|
|
||||||
|
|
||||||
Labels
|
|
||||||
------
|
|
||||||
|
|
||||||
Form labels should always precede the field, except in the case
|
|
||||||
of checkboxes and radio buttons, where the ``input`` should come first. Any
|
|
||||||
explanation or help text should follow the ``label`` in a ``p`` with class
|
|
||||||
``.help``.
|
|
|
@ -1,12 +0,0 @@
|
||||||
Deprecated/obsolete documentation
|
|
||||||
=================================
|
|
||||||
|
|
||||||
These documents cover features that have been deprecated or that have been
|
|
||||||
replaced in newer versions of Django. They're preserved here for folks using old
|
|
||||||
versions of Django or those still using deprecated APIs. No new code based on
|
|
||||||
these APIs should be written.
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 1
|
|
||||||
|
|
||||||
admin-css
|
|
|
@ -126,8 +126,9 @@ provided for each widget will be rendered exactly the same::
|
||||||
|
|
||||||
On a real Web page, you probably don't want every widget to look the same. You
|
On a real Web page, you probably don't want every widget to look the same. You
|
||||||
might want a larger input element for the comment, and you might want the
|
might want a larger input element for the comment, and you might want the
|
||||||
'name' widget to have some special CSS class. To do this, you use the
|
'name' widget to have some special CSS class. It is also possible to specify
|
||||||
:attr:`Widget.attrs` argument when creating the widget:
|
the 'type' attribute to take advantage of the new HTML5 input types. To do
|
||||||
|
this, you use the :attr:`Widget.attrs` argument when creating the widget:
|
||||||
|
|
||||||
For example::
|
For example::
|
||||||
|
|
||||||
|
@ -245,7 +246,7 @@ commonly used groups of widgets:
|
||||||
|
|
||||||
Date input as a simple text box: ``<input type='text' ...>``
|
Date input as a simple text box: ``<input type='text' ...>``
|
||||||
|
|
||||||
Takes one optional argument:
|
Takes same arguments as :class:`TextInput`, with one more optional argument:
|
||||||
|
|
||||||
.. attribute:: DateInput.format
|
.. attribute:: DateInput.format
|
||||||
|
|
||||||
|
@ -262,7 +263,7 @@ commonly used groups of widgets:
|
||||||
|
|
||||||
Date/time input as a simple text box: ``<input type='text' ...>``
|
Date/time input as a simple text box: ``<input type='text' ...>``
|
||||||
|
|
||||||
Takes one optional argument:
|
Takes same arguments as :class:`TextInput`, with one more optional argument:
|
||||||
|
|
||||||
.. attribute:: DateTimeInput.format
|
.. attribute:: DateTimeInput.format
|
||||||
|
|
||||||
|
@ -279,7 +280,7 @@ commonly used groups of widgets:
|
||||||
|
|
||||||
Time input as a simple text box: ``<input type='text' ...>``
|
Time input as a simple text box: ``<input type='text' ...>``
|
||||||
|
|
||||||
Takes one optional argument:
|
Takes same arguments as :class:`TextInput`, with one more optional argument:
|
||||||
|
|
||||||
.. attribute:: TimeInput.format
|
.. attribute:: TimeInput.format
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ up to and including the new version.
|
||||||
Final releases
|
Final releases
|
||||||
==============
|
==============
|
||||||
|
|
||||||
|
.. _development_release_notes:
|
||||||
|
|
||||||
1.5 release
|
1.5 release
|
||||||
-----------
|
-----------
|
||||||
|
|
|
@ -257,15 +257,14 @@ Installing the development version
|
||||||
|
|
||||||
If you decide to use the latest development version of Django,
|
If you decide to use the latest development version of Django,
|
||||||
you'll want to pay close attention to `the development timeline`_,
|
you'll want to pay close attention to `the development timeline`_,
|
||||||
and you'll want to keep an eye on `the list of
|
and you'll want to keep an eye on the :ref:`release notes for the
|
||||||
backwards-incompatible changes`_. This will help you stay on top
|
upcoming release <development_release_notes>`. This will help you stay
|
||||||
of any new features you might want to use, as well as any changes
|
on top of any new features you might want to use, as well as any changes
|
||||||
you'll need to make to your code when updating your copy of Django.
|
you'll need to make to your code when updating your copy of Django.
|
||||||
(For stable releases, any necessary changes are documented in the
|
(For stable releases, any necessary changes are documented in the
|
||||||
release notes.)
|
release notes.)
|
||||||
|
|
||||||
.. _the development timeline: https://code.djangoproject.com/timeline
|
.. _the development timeline: https://code.djangoproject.com/timeline
|
||||||
.. _the list of backwards-incompatible changes: https://code.djangoproject.com/wiki/BackwardsIncompatibleChanges
|
|
||||||
|
|
||||||
If you'd like to be able to update your Django code occasionally with the
|
If you'd like to be able to update your Django code occasionally with the
|
||||||
latest bug fixes and improvements, follow these instructions:
|
latest bug fixes and improvements, follow these instructions:
|
||||||
|
|
|
@ -401,6 +401,19 @@ class BackendTestCase(TestCase):
|
||||||
self.assertEqual(list(cursor.fetchmany(2)), [('Jane', 'Doe'), ('John', 'Doe')])
|
self.assertEqual(list(cursor.fetchmany(2)), [('Jane', 'Doe'), ('John', 'Doe')])
|
||||||
self.assertEqual(list(cursor.fetchall()), [('Mary', 'Agnelline'), ('Peter', 'Parker')])
|
self.assertEqual(list(cursor.fetchall()), [('Mary', 'Agnelline'), ('Peter', 'Parker')])
|
||||||
|
|
||||||
|
def test_unicode_password(self):
|
||||||
|
old_password = connection.settings_dict['PASSWORD']
|
||||||
|
connection.settings_dict['PASSWORD'] = "françois"
|
||||||
|
try:
|
||||||
|
cursor = connection.cursor()
|
||||||
|
except backend.Database.DatabaseError:
|
||||||
|
# As password is probably wrong, a database exception is expected
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
self.fail("Unexpected error raised with unicode password: %s" % e)
|
||||||
|
finally:
|
||||||
|
connection.settings_dict['PASSWORD'] = old_password
|
||||||
|
|
||||||
def test_database_operations_helper_class(self):
|
def test_database_operations_helper_class(self):
|
||||||
# Ticket #13630
|
# Ticket #13630
|
||||||
self.assertTrue(hasattr(connection, 'ops'))
|
self.assertTrue(hasattr(connection, 'ops'))
|
||||||
|
|
|
@ -31,9 +31,9 @@ class FormsWidgetTestCase(TestCase):
|
||||||
self.assertHTMLEqual(w.render('email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'}), '<input type="text" name="email" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" class="fun" />')
|
self.assertHTMLEqual(w.render('email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'}), '<input type="text" name="email" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" class="fun" />')
|
||||||
|
|
||||||
# You can also pass 'attrs' to the constructor:
|
# You can also pass 'attrs' to the constructor:
|
||||||
w = TextInput(attrs={'class': 'fun'})
|
w = TextInput(attrs={'class': 'fun', 'type': 'email'})
|
||||||
self.assertHTMLEqual(w.render('email', ''), '<input type="text" class="fun" name="email" />')
|
self.assertHTMLEqual(w.render('email', ''), '<input type="email" class="fun" name="email" />')
|
||||||
self.assertHTMLEqual(w.render('email', 'foo@example.com'), '<input type="text" class="fun" value="foo@example.com" name="email" />')
|
self.assertHTMLEqual(w.render('email', 'foo@example.com'), '<input type="email" class="fun" value="foo@example.com" name="email" />')
|
||||||
|
|
||||||
# 'attrs' passed to render() get precedence over those passed to the constructor:
|
# 'attrs' passed to render() get precedence over those passed to the constructor:
|
||||||
w = TextInput(attrs={'class': 'pretty'})
|
w = TextInput(attrs={'class': 'pretty'})
|
||||||
|
@ -915,8 +915,8 @@ beatle J R Ringo False""")
|
||||||
self.assertHTMLEqual(w.render('date', datetime.datetime(2007, 9, 17, 12, 51)), '<input type="text" name="date" value="2007-09-17 12:51:00" />')
|
self.assertHTMLEqual(w.render('date', datetime.datetime(2007, 9, 17, 12, 51)), '<input type="text" name="date" value="2007-09-17 12:51:00" />')
|
||||||
|
|
||||||
# Use 'format' to change the way a value is displayed.
|
# Use 'format' to change the way a value is displayed.
|
||||||
w = DateTimeInput(format='%d/%m/%Y %H:%M')
|
w = DateTimeInput(format='%d/%m/%Y %H:%M', attrs={'type': 'datetime'})
|
||||||
self.assertHTMLEqual(w.render('date', d), '<input type="text" name="date" value="17/09/2007 12:51" />')
|
self.assertHTMLEqual(w.render('date', d), '<input type="datetime" name="date" value="17/09/2007 12:51" />')
|
||||||
self.assertFalse(w._has_changed(d, '17/09/2007 12:51'))
|
self.assertFalse(w._has_changed(d, '17/09/2007 12:51'))
|
||||||
|
|
||||||
# Make sure a custom format works with _has_changed. The hidden input will use
|
# Make sure a custom format works with _has_changed. The hidden input will use
|
||||||
|
@ -938,8 +938,8 @@ beatle J R Ringo False""")
|
||||||
self.assertHTMLEqual(w.render('date', '2007-09-17'), '<input type="text" name="date" value="2007-09-17" />')
|
self.assertHTMLEqual(w.render('date', '2007-09-17'), '<input type="text" name="date" value="2007-09-17" />')
|
||||||
|
|
||||||
# Use 'format' to change the way a value is displayed.
|
# Use 'format' to change the way a value is displayed.
|
||||||
w = DateInput(format='%d/%m/%Y')
|
w = DateInput(format='%d/%m/%Y', attrs={'type': 'date'})
|
||||||
self.assertHTMLEqual(w.render('date', d), '<input type="text" name="date" value="17/09/2007" />')
|
self.assertHTMLEqual(w.render('date', d), '<input type="date" name="date" value="17/09/2007" />')
|
||||||
self.assertFalse(w._has_changed(d, '17/09/2007'))
|
self.assertFalse(w._has_changed(d, '17/09/2007'))
|
||||||
|
|
||||||
# Make sure a custom format works with _has_changed. The hidden input will use
|
# Make sure a custom format works with _has_changed. The hidden input will use
|
||||||
|
@ -963,8 +963,8 @@ beatle J R Ringo False""")
|
||||||
self.assertHTMLEqual(w.render('time', '13:12:11'), '<input type="text" name="time" value="13:12:11" />')
|
self.assertHTMLEqual(w.render('time', '13:12:11'), '<input type="text" name="time" value="13:12:11" />')
|
||||||
|
|
||||||
# Use 'format' to change the way a value is displayed.
|
# Use 'format' to change the way a value is displayed.
|
||||||
w = TimeInput(format='%H:%M')
|
w = TimeInput(format='%H:%M', attrs={'type': 'time'})
|
||||||
self.assertHTMLEqual(w.render('time', t), '<input type="text" name="time" value="12:51" />')
|
self.assertHTMLEqual(w.render('time', t), '<input type="time" name="time" value="12:51" />')
|
||||||
self.assertFalse(w._has_changed(t, '12:51'))
|
self.assertFalse(w._has_changed(t, '12:51'))
|
||||||
|
|
||||||
# Make sure a custom format works with _has_changed. The hidden input will use
|
# Make sure a custom format works with _has_changed. The hidden input will use
|
||||||
|
|
|
@ -1,21 +1,26 @@
|
||||||
|
import os
|
||||||
|
|
||||||
from django.utils import unittest
|
from django.utils import unittest
|
||||||
from django.utils._os import safe_join
|
from django.utils._os import safe_join
|
||||||
|
|
||||||
|
|
||||||
class SafeJoinTests(unittest.TestCase):
|
class SafeJoinTests(unittest.TestCase):
|
||||||
def test_base_path_ends_with_sep(self):
|
def test_base_path_ends_with_sep(self):
|
||||||
|
drive, path = os.path.splitdrive(safe_join("/abc/", "abc"))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
safe_join("/abc/", "abc"),
|
path,
|
||||||
"/abc/abc",
|
"{0}abc{0}abc".format(os.path.sep)
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_root_path(self):
|
def test_root_path(self):
|
||||||
|
drive, path = os.path.splitdrive(safe_join("/", "path"))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
safe_join("/", "path"),
|
path,
|
||||||
"/path",
|
"{0}path".format(os.path.sep),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
drive, path = os.path.splitdrive(safe_join("/", ""))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
safe_join("/", ""),
|
path,
|
||||||
"/",
|
os.path.sep,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue