diff options
| author | Andrew Godwin <andrew@aeracode.org> | 2013-05-18 10:21:31 +0200 |
|---|---|---|
| committer | Andrew Godwin <andrew@aeracode.org> | 2013-05-18 10:21:31 +0200 |
| commit | b31eea069cf9bc801c82a975307a9173527d78f2 (patch) | |
| tree | d3fb5b6e5ae4e6d4293786b94a193ed4f9fc4959 | |
| parent | 76d93a52cd56be23104f824e6755ecc8d3a34d94 (diff) | |
| parent | f54a8880d78f4b0b37371b0b295b2b1e73c8b67f (diff) | |
Merge branch 'master' into schema-alteration
162 files changed, 2456 insertions, 1342 deletions
diff --git a/.gitignore b/.gitignore index 2d028c7287..0ad4e34e23 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ MANIFEST dist/ docs/_build/ tests/coverage_html/ -tests/.coverage
\ No newline at end of file +tests/.coverage @@ -36,6 +36,7 @@ The PRIMARY AUTHORS are (and/or have been): * Preston Holmes * Simon Charette * Donald Stufft + * Marc Tamlyn More information on the main contributors to Django can be found in docs/internals/committers.txt. @@ -121,6 +122,7 @@ answer newbie questions, and generally made Django that much better: Chris Cahoon <chris.cahoon@gmail.com> Juan Manuel Caicedo <juan.manuel.caicedo@gmail.com> Trevor Caira <trevor@caira.com> + Aaron Cannon <cannona@fireantproductions.com> Brett Cannon <brett@python.org> Ricardo Javier Cárdenes Medina <ricardo.cardenes@gmail.com> Jeremy Carbaugh <jcarbaugh@gmail.com> @@ -539,7 +541,6 @@ answer newbie questions, and generally made Django that much better: Aaron Swartz <http://www.aaronsw.com/> Ville Säävuori <http://www.unessa.net/> Mart Sõmermaa <http://mrts.pri.ee/> - Marc Tamlyn Christian Tanzer <tanzer@swing.co.at> Tyler Tarabula <tyler.tarabula@gmail.com> Tyson Tate <tyson@fallingbullets.com> diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index a9af4baa59..53aef351c0 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -576,7 +576,7 @@ DEFAULT_EXCEPTION_REPORTER_FILTER = 'django.views.debug.SafeExceptionReporterFil ########### # The name of the class to use to run the test suite -TEST_RUNNER = 'django.test.simple.DjangoTestSuiteRunner' +TEST_RUNNER = 'django.test.runner.DiscoverRunner' ############ # FIXTURES # diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 9bc3d55454..7373837bb0 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -37,7 +37,7 @@ from django.utils.encoding import force_text HORIZONTAL, VERTICAL = 1, 2 # returns the <ul> class for a given radio_admin field -get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '') +get_ul_class = lambda x: 'radiolist%s' % (' inline' if x == HORIZONTAL else '') class IncorrectLookupParameters(Exception): @@ -189,7 +189,7 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): kwargs['widget'] = widgets.AdminRadioSelect(attrs={ 'class': get_ul_class(self.radio_fields[db_field.name]), }) - kwargs['empty_label'] = db_field.blank and _('None') or None + kwargs['empty_label'] = _('None') if db_field.blank else None queryset = self.get_field_queryset(db, db_field, request) if queryset is not None: diff --git a/django/contrib/admindocs/tests/__init__.py b/django/contrib/admindocs/tests/__init__.py index 2a67f83bcb..e69de29bb2 100644 --- a/django/contrib/admindocs/tests/__init__.py +++ b/django/contrib/admindocs/tests/__init__.py @@ -1 +0,0 @@ -from .test_fields import TestFieldType diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index 6b1bea15f3..348727eb21 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -267,7 +267,7 @@ def model_detail(request, app_label, model_name): return render_to_response('admin_doc/model_detail.html', { 'root_path': urlresolvers.reverse('admin:index'), 'name': '%s.%s' % (opts.app_label, opts.object_name), - 'summary': _("Fields on %s objects") % opts.object_name, + 'summary': _("Attributes on %s objects") % opts.object_name, 'description': model.__doc__, 'fields': fields, }, context_instance=RequestContext(request)) diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index e44e7a703e..0e08d8ef31 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -171,7 +171,7 @@ class AuthenticationForm(forms.Form): # Set the label for the "username" field. UserModel = get_user_model() self.username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD) - if not self.fields['username'].label: + if self.fields['username'].label is None: self.fields['username'].label = capfirst(self.username_field.verbose_name) def clean(self): diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py index 092cccedde..6abdb5f476 100644 --- a/django/contrib/auth/hashers.py +++ b/django/contrib/auth/hashers.py @@ -9,7 +9,7 @@ from django.conf import settings from django.test.signals import setting_changed from django.utils import importlib from django.utils.datastructures import SortedDict -from django.utils.encoding import force_bytes, force_str +from django.utils.encoding import force_bytes, force_str, force_text from django.core.exceptions import ImproperlyConfigured from django.utils.crypto import ( pbkdf2, constant_time_compare, get_random_string) @@ -263,13 +263,13 @@ class BCryptSHA256PasswordHasher(BasePasswordHasher): Secure password hashing using the bcrypt algorithm (recommended) This is considered by many to be the most secure algorithm but you - must first install the py-bcrypt library. Please be warned that + must first install the bcrypt library. Please be warned that this library depends on native C code and might cause portability issues. """ algorithm = "bcrypt_sha256" digest = hashlib.sha256 - library = ("py-bcrypt", "bcrypt") + library = ("bcrypt", "bcrypt") rounds = 12 def salt(self): @@ -291,7 +291,7 @@ class BCryptSHA256PasswordHasher(BasePasswordHasher): password = force_bytes(password) data = bcrypt.hashpw(password, salt) - return "%s$%s" % (self.algorithm, data) + return "%s$%s" % (self.algorithm, force_text(data)) def verify(self, password, encoded): algorithm, data = encoded.split('$', 1) @@ -307,6 +307,9 @@ class BCryptSHA256PasswordHasher(BasePasswordHasher): else: password = force_bytes(password) + # Ensure that our data is a bytestring + data = force_bytes(data) + return constant_time_compare(data, bcrypt.hashpw(password, data)) def safe_summary(self, encoded): @@ -326,7 +329,7 @@ class BCryptPasswordHasher(BCryptSHA256PasswordHasher): Secure password hashing using the bcrypt algorithm This is considered by many to be the most secure algorithm but you - must first install the py-bcrypt library. Please be warned that + must first install the bcrypt library. Please be warned that this library depends on native C code and might cause portability issues. diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py index 2e35e33d7f..2c308642e5 100644 --- a/django/contrib/auth/tests/__init__.py +++ b/django/contrib/auth/tests/__init__.py @@ -1,16 +1 @@ -from django.contrib.auth.tests.test_custom_user import * -from django.contrib.auth.tests.test_auth_backends import * -from django.contrib.auth.tests.test_basic import * -from django.contrib.auth.tests.test_context_processors import * -from django.contrib.auth.tests.test_decorators import * -from django.contrib.auth.tests.test_forms import * -from django.contrib.auth.tests.test_remote_user import * -from django.contrib.auth.tests.test_management import * -from django.contrib.auth.tests.test_models import * -from django.contrib.auth.tests.test_handlers import * -from django.contrib.auth.tests.test_hashers import * -from django.contrib.auth.tests.test_signals import * -from django.contrib.auth.tests.test_tokens import * -from django.contrib.auth.tests.test_views import * - # The password for the fixture data users is 'password' diff --git a/django/contrib/auth/tests/test_forms.py b/django/contrib/auth/tests/test_forms.py index 2c8f6e4faf..0b998105af 100644 --- a/django/contrib/auth/tests/test_forms.py +++ b/django/contrib/auth/tests/test_forms.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals import os + +from django.contrib.auth import get_user_model from django.contrib.auth.models import User from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm, PasswordChangeForm, SetPasswordForm, UserChangeForm, PasswordResetForm, @@ -13,6 +15,7 @@ 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.text import capfirst from django.utils.translation import ugettext as _ @@ -146,6 +149,24 @@ class AuthenticationFormTest(TestCase): form = CustomAuthenticationForm() self.assertEqual(form['username'].label, "Name") + def test_username_field_label_not_set(self): + + class CustomAuthenticationForm(AuthenticationForm): + username = CharField() + + form = CustomAuthenticationForm() + UserModel = get_user_model() + username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD) + self.assertEqual(form.fields['username'].label, capfirst(username_field.verbose_name)) + + def test_username_field_label_empty_string(self): + + class CustomAuthenticationForm(AuthenticationForm): + username = CharField(label='') + + form = CustomAuthenticationForm() + self.assertEqual(form.fields['username'].label, "") + @skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) diff --git a/django/contrib/auth/tests/test_handlers.py b/django/contrib/auth/tests/test_handlers.py index 41063aaf4a..e0d2fa2200 100644 --- a/django/contrib/auth/tests/test_handlers.py +++ b/django/contrib/auth/tests/test_handlers.py @@ -2,7 +2,7 @@ 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.test_custom_user import CustomUser from django.contrib.auth.tests.utils import skipIfCustomUser from django.test import TransactionTestCase from django.test.utils import override_settings diff --git a/django/contrib/auth/tests/test_hashers.py b/django/contrib/auth/tests/test_hashers.py index 9253fcbc43..d49fdc412e 100644 --- a/django/contrib/auth/tests/test_hashers.py +++ b/django/contrib/auth/tests/test_hashers.py @@ -92,7 +92,7 @@ class TestUtilsHashPass(unittest.TestCase): self.assertFalse(check_password('lètmeiz', encoded)) self.assertEqual(identify_hasher(encoded).algorithm, "crypt") - @skipUnless(bcrypt, "py-bcrypt not installed") + @skipUnless(bcrypt, "bcrypt not installed") def test_bcrypt_sha256(self): encoded = make_password('lètmein', hasher='bcrypt_sha256') self.assertTrue(is_password_usable(encoded)) @@ -108,7 +108,7 @@ class TestUtilsHashPass(unittest.TestCase): self.assertTrue(check_password(password, encoded)) self.assertFalse(check_password(password[:72], encoded)) - @skipUnless(bcrypt, "py-bcrypt not installed") + @skipUnless(bcrypt, "bcrypt not installed") def test_bcrypt(self): encoded = make_password('lètmein', hasher='bcrypt') self.assertTrue(is_password_usable(encoded)) diff --git a/django/contrib/auth/tests/test_management.py b/django/contrib/auth/tests/test_management.py index 687a5c31cb..04fd4941ab 100644 --- a/django/contrib/auth/tests/test_management.py +++ b/django/contrib/auth/tests/test_management.py @@ -5,7 +5,7 @@ from django.contrib.auth import models, management from django.contrib.auth.management import create_permissions from django.contrib.auth.management.commands import changepassword from django.contrib.auth.models import User -from django.contrib.auth.tests import CustomUser +from django.contrib.auth.tests.test_custom_user import CustomUser from django.contrib.auth.tests.utils import skipIfCustomUser from django.core.management import call_command from django.core.management.base import CommandError diff --git a/django/contrib/comments/views/utils.py b/django/contrib/comments/views/utils.py index 79f6376232..da70272282 100644 --- a/django/contrib/comments/views/utils.py +++ b/django/contrib/comments/views/utils.py @@ -37,7 +37,7 @@ def next_redirect(request, fallback, **get_kwargs): else: anchor = '' - joiner = ('?' in next) and '&' or '?' + joiner = '&' if '?' in next else '?' next += joiner + urlencode(get_kwargs) + anchor return HttpResponseRedirect(next) diff --git a/django/contrib/contenttypes/views.py b/django/contrib/contenttypes/views.py index 5c22cb6672..6d4a719289 100644 --- a/django/contrib/contenttypes/views.py +++ b/django/contrib/contenttypes/views.py @@ -75,7 +75,7 @@ def shortcut(request, content_type_id, object_id): # If all that malarkey found an object domain, use it. Otherwise, fall back # to whatever get_absolute_url() returned. if object_domain is not None: - protocol = request.is_secure() and 'https' or 'http' + protocol = 'https' if request.is_secure() else 'http' return http.HttpResponseRedirect('%s://%s%s' % (protocol, object_domain, absurl)) else: diff --git a/django/contrib/flatpages/tests/__init__.py b/django/contrib/flatpages/tests/__init__.py index 3703e23cbf..e69de29bb2 100644 --- a/django/contrib/flatpages/tests/__init__.py +++ b/django/contrib/flatpages/tests/__init__.py @@ -1,6 +0,0 @@ -from django.contrib.flatpages.tests.test_csrf import * -from django.contrib.flatpages.tests.test_forms import * -from django.contrib.flatpages.tests.test_models import * -from django.contrib.flatpages.tests.test_middleware import * -from django.contrib.flatpages.tests.test_templatetags import * -from django.contrib.flatpages.tests.test_views import * diff --git a/django/contrib/formtools/tests/__init__.py b/django/contrib/formtools/tests/__init__.py index 2bbeea8c5a..e69de29bb2 100644 --- a/django/contrib/formtools/tests/__init__.py +++ b/django/contrib/formtools/tests/__init__.py @@ -1,2 +0,0 @@ -from django.contrib.formtools.tests.tests import * -from django.contrib.formtools.tests.wizard import * diff --git a/django/contrib/formtools/tests/urls.py b/django/contrib/formtools/tests/urls.py index 739095d69f..f96f89ecdf 100644 --- a/django/contrib/formtools/tests/urls.py +++ b/django/contrib/formtools/tests/urls.py @@ -5,7 +5,7 @@ This is a URLconf to be loaded by tests.py. Add any URLs needed for tests only. from __future__ import absolute_import from django.conf.urls import patterns, url -from django.contrib.formtools.tests import TestFormPreview +from django.contrib.formtools.tests.tests import TestFormPreview from django.contrib.formtools.tests.forms import TestForm diff --git a/django/contrib/formtools/tests/wizard/test_forms.py b/django/contrib/formtools/tests/wizard/test_forms.py index 7425755cf5..21917822a7 100644 --- a/django/contrib/formtools/tests/wizard/test_forms.py +++ b/django/contrib/formtools/tests/wizard/test_forms.py @@ -17,7 +17,7 @@ from django.contrib.formtools.wizard.views import (WizardView, class DummyRequest(http.HttpRequest): def __init__(self, POST=None): super(DummyRequest, self).__init__() - self.method = POST and "POST" or "GET" + self.method = "POST" if POST else "GET" if POST is not None: self.POST.update(POST) self.session = {} diff --git a/django/contrib/gis/db/models/fields.py b/django/contrib/gis/db/models/fields.py index 249617f771..2e221b7477 100644 --- a/django/contrib/gis/db/models/fields.py +++ b/django/contrib/gis/db/models/fields.py @@ -44,6 +44,7 @@ class GeometryField(Field): # The OpenGIS Geometry name. geom_type = 'GEOMETRY' + form_class = forms.GeometryField # Geodetic units. geodetic_units = ('Decimal Degree', 'degree') @@ -201,11 +202,14 @@ class GeometryField(Field): return connection.ops.geo_db_type(self) def formfield(self, **kwargs): - defaults = {'form_class' : forms.GeometryField, + defaults = {'form_class' : self.form_class, 'geom_type' : self.geom_type, 'srid' : self.srid, } defaults.update(kwargs) + if (self.dim > 2 and not 'widget' in kwargs and + not getattr(defaults['form_class'].widget, 'supports_3d', False)): + defaults['widget'] = forms.Textarea return super(GeometryField, self).formfield(**defaults) def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False): @@ -267,28 +271,35 @@ class GeometryField(Field): # The OpenGIS Geometry Type Fields class PointField(GeometryField): geom_type = 'POINT' + form_class = forms.PointField description = _("Point") class LineStringField(GeometryField): geom_type = 'LINESTRING' + form_class = forms.LineStringField description = _("Line string") class PolygonField(GeometryField): geom_type = 'POLYGON' + form_class = forms.PolygonField description = _("Polygon") class MultiPointField(GeometryField): geom_type = 'MULTIPOINT' + form_class = forms.MultiPointField description = _("Multi-point") class MultiLineStringField(GeometryField): geom_type = 'MULTILINESTRING' + form_class = forms.MultiLineStringField description = _("Multi-line string") class MultiPolygonField(GeometryField): geom_type = 'MULTIPOLYGON' + form_class = forms.MultiPolygonField description = _("Multi polygon") class GeometryCollectionField(GeometryField): geom_type = 'GEOMETRYCOLLECTION' + form_class = forms.GeometryCollectionField description = _("Geometry collection") diff --git a/django/contrib/gis/db/models/sql/compiler.py b/django/contrib/gis/db/models/sql/compiler.py index b488f59362..b2befd44b2 100644 --- a/django/contrib/gis/db/models/sql/compiler.py +++ b/django/contrib/gis/db/models/sql/compiler.py @@ -121,7 +121,7 @@ class GeoSQLCompiler(compiler.SQLCompiler): """ result = [] if opts is None: - opts = self.query.model._meta + opts = self.query.get_meta() aliases = set() only_load = self.deferred_to_columns() seen = self.query.included_inherited_models.copy() @@ -247,7 +247,7 @@ class GeoSQLCompiler(compiler.SQLCompiler): used. If `column` is specified, it will be used instead of the value in `field.column`. """ - if table_alias is None: table_alias = self.query.model._meta.db_table + if table_alias is None: table_alias = self.query.get_meta().db_table return "%s.%s" % (self.quote_name_unless_alias(table_alias), self.connection.ops.quote_name(column or field.column)) diff --git a/django/contrib/gis/forms/__init__.py b/django/contrib/gis/forms/__init__.py index 82971da6be..93a2d3847b 100644 --- a/django/contrib/gis/forms/__init__.py +++ b/django/contrib/gis/forms/__init__.py @@ -1,2 +1,5 @@ from django.forms import * -from django.contrib.gis.forms.fields import GeometryField +from .fields import (GeometryField, GeometryCollectionField, PointField, + MultiPointField, LineStringField, MultiLineStringField, PolygonField, + MultiPolygonField) +from .widgets import BaseGeometryWidget, OpenLayersWidget, OSMWidget diff --git a/django/contrib/gis/forms/fields.py b/django/contrib/gis/forms/fields.py index d3feac83e7..6e2cbd59f5 100644 --- a/django/contrib/gis/forms/fields.py +++ b/django/contrib/gis/forms/fields.py @@ -9,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _ # While this couples the geographic forms to the GEOS library, # it decouples from database (by not importing SpatialBackend). from django.contrib.gis.geos import GEOSException, GEOSGeometry, fromstr +from .widgets import OpenLayersWidget class GeometryField(forms.Field): @@ -17,7 +18,8 @@ class GeometryField(forms.Field): accepted by GEOSGeometry is accepted by this form. By default, this includes WKT, HEXEWKB, WKB (in a buffer), and GeoJSON. """ - widget = forms.Textarea + widget = OpenLayersWidget + geom_type = 'GEOMETRY' default_error_messages = { 'required' : _('No geometry value provided.'), @@ -31,12 +33,13 @@ class GeometryField(forms.Field): # Pop out attributes from the database field, or use sensible # defaults (e.g., allow None). self.srid = kwargs.pop('srid', None) - self.geom_type = kwargs.pop('geom_type', 'GEOMETRY') + self.geom_type = kwargs.pop('geom_type', self.geom_type) if 'null' in kwargs: kwargs.pop('null', True) warnings.warn("Passing 'null' keyword argument to GeometryField is deprecated.", DeprecationWarning, stacklevel=2) super(GeometryField, self).__init__(**kwargs) + self.widget.attrs['geom_type'] = self.geom_type def to_python(self, value): """ @@ -98,3 +101,31 @@ class GeometryField(forms.Field): else: # Check for change of state of existence return bool(initial) != bool(data) + + +class GeometryCollectionField(GeometryField): + geom_type = 'GEOMETRYCOLLECTION' + + +class PointField(GeometryField): + geom_type = 'POINT' + + +class MultiPointField(GeometryField): + geom_type = 'MULTIPOINT' + + +class LineStringField(GeometryField): + geom_type = 'LINESTRING' + + +class MultiLineStringField(GeometryField): + geom_type = 'MULTILINESTRING' + + +class PolygonField(GeometryField): + geom_type = 'POLYGON' + + +class MultiPolygonField(GeometryField): + geom_type = 'MULTIPOLYGON' diff --git a/django/contrib/gis/forms/widgets.py b/django/contrib/gis/forms/widgets.py new file mode 100644 index 0000000000..d50c7c005a --- /dev/null +++ b/django/contrib/gis/forms/widgets.py @@ -0,0 +1,112 @@ +from __future__ import unicode_literals + +import logging + +from django.conf import settings +from django.contrib.gis import gdal +from django.contrib.gis.geos import GEOSGeometry, GEOSException +from django.forms.widgets import Widget +from django.template import loader +from django.utils import six +from django.utils import translation + +logger = logging.getLogger('django.contrib.gis') + + +class BaseGeometryWidget(Widget): + """ + The base class for rich geometry widgets. + Renders a map using the WKT of the geometry. + """ + geom_type = 'GEOMETRY' + map_srid = 4326 + map_width = 600 + map_height = 400 + display_wkt = False + + supports_3d = False + template_name = '' # set on subclasses + + def __init__(self, attrs=None): + self.attrs = {} + for key in ('geom_type', 'map_srid', 'map_width', 'map_height', 'display_wkt'): + self.attrs[key] = getattr(self, key) + if attrs: + self.attrs.update(attrs) + + def render(self, name, value, attrs=None): + # If a string reaches here (via a validation error on another + # field) then just reconstruct the Geometry. + if isinstance(value, six.string_types): + try: + value = GEOSGeometry(value) + except (GEOSException, ValueError) as err: + logger.error( + "Error creating geometry from value '%s' (%s)" % ( + value, err) + ) + value = None + + wkt = '' + if value: + # Check that srid of value and map match + if value.srid != self.map_srid: + try: + ogr = value.ogr + ogr.transform(self.map_srid) + wkt = ogr.wkt + except gdal.OGRException as err: + logger.error( + "Error transforming geometry from srid '%s' to srid '%s' (%s)" % ( + value.srid, self.map_srid, err) + ) + else: + wkt = value.wkt + + context = self.build_attrs(attrs, + name=name, + module='geodjango_%s' % name.replace('-','_'), # JS-safe + wkt=wkt, + geom_type=gdal.OGRGeomType(self.attrs['geom_type']), + STATIC_URL=settings.STATIC_URL, + LANGUAGE_BIDI=translation.get_language_bidi(), + ) + return loader.render_to_string(self.template_name, context) + + +class OpenLayersWidget(BaseGeometryWidget): + template_name = 'gis/openlayers.html' + class Media: + js = ( + 'http://openlayers.org/api/2.11/OpenLayers.js', + 'gis/js/OLMapWidget.js', + ) + + +class OSMWidget(BaseGeometryWidget): + """ + An OpenLayers/OpenStreetMap-based widget. + """ + template_name = 'gis/openlayers-osm.html' + default_lon = 5 + default_lat = 47 + + class Media: + js = ( + 'http://openlayers.org/api/2.11/OpenLayers.js', + 'http://www.openstreetmap.org/openlayers/OpenStreetMap.js', + 'gis/js/OLMapWidget.js', + ) + + @property + def map_srid(self): + # Use the official spherical mercator projection SRID on versions + # of GDAL that support it; otherwise, fallback to 900913. + if gdal.HAS_GDAL and gdal.GDAL_VERSION >= (1, 7): + return 3857 + else: + return 900913 + + def render(self, name, value, attrs=None): + return super(self, OSMWidget).render(name, value, + {'default_lon': self.default_lon, 'default_lat': self.default_lat}) diff --git a/django/contrib/gis/gdal/__init__.py b/django/contrib/gis/gdal/__init__.py index c33fbcb97a..2aa867bb69 100644 --- a/django/contrib/gis/gdal/__init__.py +++ b/django/contrib/gis/gdal/__init__.py @@ -31,6 +31,9 @@ to a non-existant file location (e.g., `GDAL_LIBRARY_PATH='/null/path'`; setting to None/False/'' will not work as a string must be given). """ +from django.contrib.gis.gdal.error import check_err, OGRException, OGRIndexError, SRSException +from django.contrib.gis.gdal.geomtype import OGRGeomType + # Attempting to import objects that depend on the GDAL library. The # HAS_GDAL flag will be set to True if the library is present on # the system. @@ -41,7 +44,7 @@ try: from django.contrib.gis.gdal.srs import SpatialReference, CoordTransform from django.contrib.gis.gdal.geometries import OGRGeometry HAS_GDAL = True -except Exception: +except OGRException: HAS_GDAL = False try: @@ -50,5 +53,3 @@ except ImportError: # No ctypes, but don't raise an exception. pass -from django.contrib.gis.gdal.error import check_err, OGRException, OGRIndexError, SRSException -from django.contrib.gis.gdal.geomtype import OGRGeomType diff --git a/django/contrib/gis/gdal/tests/__init__.py b/django/contrib/gis/gdal/tests/__init__.py index 262d294a43..e69de29bb2 100644 --- a/django/contrib/gis/gdal/tests/__init__.py +++ b/django/contrib/gis/gdal/tests/__init__.py @@ -1,28 +0,0 @@ -""" -Module for executing all of the GDAL tests. None -of these tests require the use of the database. -""" -from __future__ import absolute_import - -from django.utils.unittest import TestSuite, TextTestRunner - -# Importing the GDAL test modules. -from . import test_driver, test_ds, test_envelope, test_geom, test_srs - -test_suites = [test_driver.suite(), - test_ds.suite(), - test_envelope.suite(), - test_geom.suite(), - test_srs.suite(), - ] - -def suite(): - "Builds a test suite for the GDAL tests." - s = TestSuite() - for test_suite in test_suites: - s.addTest(test_suite) - return s - -def run(verbosity=1): - "Runs the GDAL tests." - TextTestRunner(verbosity=verbosity).run(suite()) diff --git a/django/contrib/gis/gdal/tests/test_driver.py b/django/contrib/gis/gdal/tests/test_driver.py index 06ec93f936..c27302da72 100644 --- a/django/contrib/gis/gdal/tests/test_driver.py +++ b/django/contrib/gis/gdal/tests/test_driver.py @@ -1,5 +1,10 @@ -import unittest -from django.contrib.gis.gdal import Driver, OGRException +from django.contrib.gis.gdal import HAS_GDAL +from django.utils import unittest +from django.utils.unittest import skipUnless + +if HAS_GDAL: + from django.contrib.gis.gdal import Driver, OGRException + valid_drivers = ('ESRI Shapefile', 'MapInfo File', 'TIGER', 'S57', 'DGN', 'Memory', 'CSV', 'GML', 'KML') @@ -12,6 +17,8 @@ aliases = {'eSrI' : 'ESRI Shapefile', 'sHp' : 'ESRI Shapefile', } + +@skipUnless(HAS_GDAL, "GDAL is required") class DriverTest(unittest.TestCase): def test01_valid_driver(self): @@ -30,11 +37,3 @@ class DriverTest(unittest.TestCase): for alias, full_name in aliases.items(): dr = Driver(alias) self.assertEqual(full_name, str(dr)) - -def suite(): - s = unittest.TestSuite() - s.addTest(unittest.makeSuite(DriverTest)) - return s - -def run(verbosity=2): - unittest.TextTestRunner(verbosity=verbosity).run(suite()) diff --git a/django/contrib/gis/gdal/tests/test_ds.py b/django/contrib/gis/gdal/tests/test_ds.py index a87a1c6c35..3664f055f2 100644 --- a/django/contrib/gis/gdal/tests/test_ds.py +++ b/django/contrib/gis/gdal/tests/test_ds.py @@ -1,32 +1,38 @@ import os -import unittest -from django.contrib.gis.gdal import DataSource, Envelope, OGRGeometry, OGRException, OGRIndexError, GDAL_VERSION -from django.contrib.gis.gdal.field import OFTReal, OFTInteger, OFTString + +from django.contrib.gis.gdal import HAS_GDAL from django.contrib.gis.geometry.test_data import get_ds_file, TestDS, TEST_DATA +from django.utils import unittest +from django.utils.unittest import skipUnless + +if HAS_GDAL: + from django.contrib.gis.gdal import DataSource, Envelope, OGRGeometry, OGRException, OGRIndexError, GDAL_VERSION + from django.contrib.gis.gdal.field import OFTReal, OFTInteger, OFTString + # List of acceptable data sources. + ds_list = ( + TestDS('test_point', nfeat=5, nfld=3, geom='POINT', gtype=1, driver='ESRI Shapefile', + fields={'dbl' : OFTReal, 'int' : OFTInteger, 'str' : OFTString,}, + extent=(-1.35011,0.166623,-0.524093,0.824508), # Got extent from QGIS + srs_wkt='GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]', + field_values={'dbl' : [float(i) for i in range(1, 6)], 'int' : list(range(1, 6)), 'str' : [str(i) for i in range(1, 6)]}, + fids=range(5)), + TestDS('test_vrt', ext='vrt', nfeat=3, nfld=3, geom='POINT', gtype='Point25D', driver='VRT', + fields={'POINT_X' : OFTString, 'POINT_Y' : OFTString, 'NUM' : OFTString}, # VRT uses CSV, which all types are OFTString. + extent=(1.0, 2.0, 100.0, 523.5), # Min/Max from CSV + field_values={'POINT_X' : ['1.0', '5.0', '100.0'], 'POINT_Y' : ['2.0', '23.0', '523.5'], 'NUM' : ['5', '17', '23']}, + fids=range(1,4)), + TestDS('test_poly', nfeat=3, nfld=3, geom='POLYGON', gtype=3, + driver='ESRI Shapefile', + fields={'float' : OFTReal, 'int' : OFTInteger, 'str' : OFTString,}, + extent=(-1.01513,-0.558245,0.161876,0.839637), # Got extent from QGIS + srs_wkt='GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]'), + ) -# List of acceptable data sources. -ds_list = (TestDS('test_point', nfeat=5, nfld=3, geom='POINT', gtype=1, driver='ESRI Shapefile', - fields={'dbl' : OFTReal, 'int' : OFTInteger, 'str' : OFTString,}, - extent=(-1.35011,0.166623,-0.524093,0.824508), # Got extent from QGIS - srs_wkt='GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]', - field_values={'dbl' : [float(i) for i in range(1, 6)], 'int' : list(range(1, 6)), 'str' : [str(i) for i in range(1, 6)]}, - fids=range(5)), - TestDS('test_vrt', ext='vrt', nfeat=3, nfld=3, geom='POINT', gtype='Point25D', driver='VRT', - fields={'POINT_X' : OFTString, 'POINT_Y' : OFTString, 'NUM' : OFTString}, # VRT uses CSV, which all types are OFTString. - extent=(1.0, 2.0, 100.0, 523.5), # Min/Max from CSV - field_values={'POINT_X' : ['1.0', '5.0', '100.0'], 'POINT_Y' : ['2.0', '23.0', '523.5'], 'NUM' : ['5', '17', '23']}, - fids=range(1,4)), - TestDS('test_poly', nfeat=3, nfld=3, geom='POLYGON', gtype=3, - driver='ESRI Shapefile', - fields={'float' : OFTReal, 'int' : OFTInteger, 'str' : OFTString,}, - extent=(-1.01513,-0.558245,0.161876,0.839637), # Got extent from QGIS - srs_wkt='GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]'), - ) +bad_ds = (TestDS('foo'),) -bad_ds = (TestDS('foo'), - ) +@skipUnless(HAS_GDAL, "GDAL is required") class DataSourceTest(unittest.TestCase): def test01_valid_shp(self): @@ -236,11 +242,3 @@ class DataSourceTest(unittest.TestCase): feat = ds[0][0] # Reference value obtained using `ogrinfo`. self.assertEqual(676586997978, feat.get('ALAND10')) - -def suite(): - s = unittest.TestSuite() - s.addTest(unittest.makeSuite(DataSourceTest)) - return s - -def run(verbosity=2): - unittest.TextTestRunner(verbosity=verbosity).run(suite()) diff --git a/django/contrib/gis/gdal/tests/test_envelope.py b/django/contrib/gis/gdal/tests/test_envelope.py index 07e12c0ca7..7518dc69aa 100644 --- a/django/contrib/gis/gdal/tests/test_envelope.py +++ b/django/contrib/gis/gdal/tests/test_envelope.py @@ -1,5 +1,9 @@ -from django.contrib.gis.gdal import Envelope, OGRException +from django.contrib.gis.gdal import HAS_GDAL from django.utils import unittest +from django.utils.unittest import skipUnless + +if HAS_GDAL: + from django.contrib.gis.gdal import Envelope, OGRException class TestPoint(object): @@ -7,11 +11,13 @@ class TestPoint(object): self.x = x self.y = y + +@skipUnless(HAS_GDAL, "GDAL is required") class EnvelopeTest(unittest.TestCase): def setUp(self): self.e = Envelope(0, 0, 5, 5) - + def test01_init(self): "Testing Envelope initilization." e1 = Envelope((0, 0, 5, 5)) @@ -85,11 +91,3 @@ class EnvelopeTest(unittest.TestCase): self.assertEqual((-1, 0, 5, 5), self.e) self.e.expand_to_include(TestPoint(10, 10)) self.assertEqual((-1, 0, 10, 10), self.e) - -def suite(): - s = unittest.TestSuite() - s.addTest(unittest.makeSuite(EnvelopeTest)) - return s - -def run(verbosity=2): - unittest.TextTestRunner(verbosity=verbosity).run(suite()) diff --git a/django/contrib/gis/gdal/tests/test_geom.py b/django/contrib/gis/gdal/tests/test_geom.py index 9b8ae6a26b..c048d2bb82 100644 --- a/django/contrib/gis/gdal/tests/test_geom.py +++ b/django/contrib/gis/gdal/tests/test_geom.py @@ -5,12 +5,19 @@ try: except ImportError: import pickle -from django.contrib.gis.gdal import (OGRGeometry, OGRGeomType, OGRException, - OGRIndexError, SpatialReference, CoordTransform, GDAL_VERSION) +from django.contrib.gis.gdal import HAS_GDAL from django.contrib.gis.geometry.test_data import TestDataMixin from django.utils.six.moves import xrange from django.utils import unittest +from django.utils.unittest import skipUnless +if HAS_GDAL: + from django.contrib.gis.gdal import (OGRGeometry, OGRGeomType, + OGRException, OGRIndexError, SpatialReference, CoordTransform, + GDAL_VERSION) + + +@skipUnless(HAS_GDAL, "GDAL is required") class OGRGeomTest(unittest.TestCase, TestDataMixin): "This tests the OGR Geometry." @@ -476,11 +483,3 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin): "Testing equivalence methods with non-OGRGeometry instances." self.assertNotEqual(None, OGRGeometry('POINT(0 0)')) self.assertEqual(False, OGRGeometry('LINESTRING(0 0, 1 1)') == 3) - -def suite(): - s = unittest.TestSuite() - s.addTest(unittest.makeSuite(OGRGeomTest)) - return s - -def run(verbosity=2): - unittest.TextTestRunner(verbosity=verbosity).run(suite()) diff --git a/django/contrib/gis/gdal/tests/test_srs.py b/django/contrib/gis/gdal/tests/test_srs.py index 3d3bba7939..363b597dae 100644 --- a/django/contrib/gis/gdal/tests/test_srs.py +++ b/django/contrib/gis/gdal/tests/test_srs.py @@ -1,5 +1,9 @@ -from django.contrib.gis.gdal import SpatialReference, CoordTransform, OGRException, SRSException +from django.contrib.gis.gdal import HAS_GDAL from django.utils import unittest +from django.utils.unittest import skipUnless + +if HAS_GDAL: + from django.contrib.gis.gdal import SpatialReference, CoordTransform, OGRException, SRSException class TestSRS: @@ -46,6 +50,8 @@ well_known = (TestSRS('GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",637813 bad_srlist = ('Foobar', 'OOJCS["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"]]',) + +@skipUnless(HAS_GDAL, "GDAL is required") class SpatialRefTest(unittest.TestCase): def test01_wkt(self): @@ -155,11 +161,3 @@ class SpatialRefTest(unittest.TestCase): self.assertEqual('EPSG', s1['AUTHORITY']) self.assertEqual(4326, int(s1['AUTHORITY', 1])) self.assertEqual(None, s1['FOOBAR']) - -def suite(): - s = unittest.TestSuite() - s.addTest(unittest.makeSuite(SpatialRefTest)) - return s - -def run(verbosity=2): - unittest.TextTestRunner(verbosity=verbosity).run(suite()) diff --git a/django/contrib/gis/geoip/tests.py b/django/contrib/gis/geoip/tests.py index bb4a3e7e23..3fa64bf6be 100644 --- a/django/contrib/gis/geoip/tests.py +++ b/django/contrib/gis/geoip/tests.py @@ -3,16 +3,28 @@ from __future__ import unicode_literals import os from django.conf import settings -from django.contrib.gis.geos import GEOSGeometry -from django.contrib.gis.geoip import GeoIP, GeoIPException +from django.contrib.gis.geos import HAS_GEOS +from django.contrib.gis.geoip import HAS_GEOIP from django.utils import unittest +from django.utils.unittest import skipUnless from django.utils import six +if HAS_GEOIP: + from . import GeoIP, GeoIPException + +if HAS_GEOS: + from ..geos import GEOSGeometry + + # Note: Requires use of both the GeoIP country and city datasets. # The GEOIP_DATA path should be the only setting set (the directory # should contain links or the actual database files 'GeoIP.dat' and # 'GeoLiteCity.dat'. + + +@skipUnless(HAS_GEOIP and getattr(settings, "GEOIP_PATH", None), + "GeoIP is required along with the GEOIP_DATA setting.") class GeoIPTest(unittest.TestCase): def test01_init(self): @@ -70,6 +82,7 @@ class GeoIPTest(unittest.TestCase): self.assertEqual({'country_code' : 'US', 'country_name' : 'United States'}, g.country(query)) + @skipUnless(HAS_GEOS, "Geos is required") def test04_city(self): "Testing GeoIP city querying methods." g = GeoIP(country='<foo>') @@ -105,12 +118,3 @@ class GeoIPTest(unittest.TestCase): g = GeoIP() d = g.city("www.osnabrueck.de") self.assertEqual('Osnabrück', d['city']) - - -def suite(): - s = unittest.TestSuite() - s.addTest(unittest.makeSuite(GeoIPTest)) - return s - -def run(verbosity=1): - unittest.TextTestRunner(verbosity=verbosity).run(suite()) diff --git a/django/contrib/gis/geos/__init__.py b/django/contrib/gis/geos/__init__.py index 5885a30bef..945f561fb7 100644 --- a/django/contrib/gis/geos/__init__.py +++ b/django/contrib/gis/geos/__init__.py @@ -3,12 +3,18 @@ The GeoDjango GEOS module. Please consult the GeoDjango documentation for more details: http://geodjango.org/docs/geos.html """ -from django.contrib.gis.geos.geometry import GEOSGeometry, wkt_regex, hex_regex -from django.contrib.gis.geos.point import Point -from django.contrib.gis.geos.linestring import LineString, LinearRing -from django.contrib.gis.geos.polygon import Polygon -from django.contrib.gis.geos.collections import GeometryCollection, MultiPoint, MultiLineString, MultiPolygon -from django.contrib.gis.geos.error import GEOSException, GEOSIndexError -from django.contrib.gis.geos.io import WKTReader, WKTWriter, WKBReader, WKBWriter -from django.contrib.gis.geos.factory import fromfile, fromstr -from django.contrib.gis.geos.libgeos import geos_version, geos_version_info, GEOS_PREPARE +try: + from .libgeos import geos_version, geos_version_info, GEOS_PREPARE + HAS_GEOS = True +except ImportError: + HAS_GEOS = False + +if HAS_GEOS: + from .geometry import GEOSGeometry, wkt_regex, hex_regex + from .point import Point + from .linestring import LineString, LinearRing + from .polygon import Polygon + from .collections import GeometryCollection, MultiPoint, MultiLineString, MultiPolygon + from .error import GEOSException, GEOSIndexError + from .io import WKTReader, WKTWriter, WKBReader, WKBWriter + from .factory import fromfile, fromstr diff --git a/django/contrib/gis/geos/tests/__init__.py b/django/contrib/gis/geos/tests/__init__.py index 6b715d8c59..e69de29bb2 100644 --- a/django/contrib/gis/geos/tests/__init__.py +++ b/django/contrib/gis/geos/tests/__init__.py @@ -1,28 +0,0 @@ -""" -GEOS Testing module. -""" -from __future__ import absolute_import - -from django.utils.unittest import TestSuite, TextTestRunner -from . import test_geos, test_io, test_geos_mutation, test_mutable_list - -test_suites = [ - test_geos.suite(), - test_io.suite(), - test_geos_mutation.suite(), - test_mutable_list.suite(), - ] - -def suite(): - "Builds a test suite for the GEOS tests." - s = TestSuite() - for suite in test_suites: - s.addTest(suite) - return s - -def run(verbosity=1): - "Runs the GEOS tests." - TextTestRunner(verbosity=verbosity).run(suite()) - -if __name__ == '__main__': - run(2) diff --git a/django/contrib/gis/geos/tests/test_geos.py b/django/contrib/gis/geos/tests/test_geos.py index 66d890d8cb..c7fe5b2321 100644 --- a/django/contrib/gis/geos/tests/test_geos.py +++ b/django/contrib/gis/geos/tests/test_geos.py @@ -6,20 +6,28 @@ import random from binascii import a2b_hex, b2a_hex from io import BytesIO +from django.contrib.gis.gdal import HAS_GDAL + from django.contrib.gis import memoryview -from django.contrib.gis.geos import (GEOSException, GEOSIndexError, GEOSGeometry, - GeometryCollection, Point, MultiPoint, Polygon, MultiPolygon, LinearRing, - LineString, MultiLineString, fromfile, fromstr, geos_version_info) -from django.contrib.gis.geos.base import gdal, numpy, GEOSBase -from django.contrib.gis.geos.libgeos import GEOS_PREPARE from django.contrib.gis.geometry.test_data import TestDataMixin from django.utils.encoding import force_bytes from django.utils import six from django.utils.six.moves import xrange from django.utils import unittest +from django.utils.unittest import skipUnless + +from .. import HAS_GEOS + +if HAS_GEOS: + from .. import (GEOSException, GEOSIndexError, GEOSGeometry, + GeometryCollection, Point, MultiPoint, Polygon, MultiPolygon, LinearRing, + LineString, MultiLineString, fromfile, fromstr, geos_version_info, + GEOS_PREPARE) + from ..base import gdal, numpy, GEOSBase +@skipUnless(HAS_GEOS, "Geos is required.") class GEOSTest(unittest.TestCase, TestDataMixin): @property @@ -198,7 +206,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertEqual(srid, poly.shell.srid) self.assertEqual(srid, fromstr(poly.ewkt).srid) # Checking export - @unittest.skipUnless(gdal.HAS_GDAL, "gdal is required") + @skipUnless(HAS_GDAL, "GDAL is required.") def test_json(self): "Testing GeoJSON input/output (via GDAL)." for g in self.geometries.json_geoms: @@ -662,6 +670,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): p3 = fromstr(p1.hex, srid=-1) # -1 is intended. self.assertEqual(-1, p3.srid) + @skipUnless(HAS_GDAL, "GDAL is required.") def test_custom_srid(self): """ Test with a srid unknown from GDAL """ pnt = Point(111200, 220900, srid=999999) @@ -851,7 +860,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): # And, they should be equal. self.assertEqual(gc1, gc2) - @unittest.skipUnless(gdal.HAS_GDAL, "gdal is required") + @skipUnless(HAS_GDAL, "GDAL is required.") def test_gdal(self): "Testing `ogr` and `srs` properties." g1 = fromstr('POINT(5 23)') @@ -878,7 +887,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertNotEqual(poly._ptr, cpy1._ptr) self.assertNotEqual(poly._ptr, cpy2._ptr) - @unittest.skipUnless(gdal.HAS_GDAL, "gdal is required to transform geometries") + @skipUnless(HAS_GDAL, "GDAL is required to transform geometries") def test_transform(self): "Testing `transform` method." orig = GEOSGeometry('POINT (-104.609 38.255)', 4326) @@ -903,7 +912,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertAlmostEqual(trans.x, p.x, prec) self.assertAlmostEqual(trans.y, p.y, prec) - @unittest.skipUnless(gdal.HAS_GDAL, "gdal is required to transform geometries") + @skipUnless(HAS_GDAL, "GDAL is required to transform geometries") def test_transform_3d(self): p3d = GEOSGeometry('POINT (5 23 100)', 4326) p3d.transform(2774) @@ -912,6 +921,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): else: self.assertIsNone(p3d.z) + @skipUnless(HAS_GDAL, "GDAL is required.") def test_transform_noop(self): """ Testing `transform` method (SRID match) """ # transform() should no-op if source & dest SRIDs match, @@ -962,6 +972,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): g = GEOSGeometry('POINT (-104.609 38.255)', srid=-1) self.assertRaises(GEOSException, g.transform, 2774, clone=True) + @skipUnless(HAS_GDAL, "GDAL is required.") def test_transform_nogdal(self): """ Testing `transform` method (GDAL not available) """ old_has_gdal = gdal.HAS_GDAL @@ -1016,7 +1027,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertEqual(geom, tmpg) if not no_srid: self.assertEqual(geom.srid, tmpg.srid) - @unittest.skipUnless(GEOS_PREPARE, "geos >= 3.1.0 is required") + @skipUnless(HAS_GEOS and GEOS_PREPARE, "geos >= 3.1.0 is required") def test_prepared(self): "Testing PreparedGeometry support." # Creating a simple multipolygon and getting a prepared version. @@ -1043,7 +1054,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): for geom, merged in zip(ref_geoms, ref_merged): self.assertEqual(merged, geom.merged) - @unittest.skipUnless(GEOS_PREPARE, "geos >= 3.1.0 is required") + @skipUnless(HAS_GEOS and GEOS_PREPARE, "geos >= 3.1.0 is required") def test_valid_reason(self): "Testing IsValidReason support" @@ -1058,7 +1069,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertIsInstance(g.valid_reason, six.string_types) self.assertTrue(g.valid_reason.startswith("Too few points in geometry component")) - @unittest.skipUnless(geos_version_info()['version'] >= '3.2.0', "geos >= 3.2.0 is required") + @skipUnless(HAS_GEOS and geos_version_info()['version'] >= '3.2.0', "geos >= 3.2.0 is required") def test_linearref(self): "Testing linear referencing" @@ -1091,12 +1102,3 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertTrue(m, msg="Unable to parse the version string '%s'" % v_init) self.assertEqual(m.group('version'), v_geos) self.assertEqual(m.group('capi_version'), v_capi) - - -def suite(): - s = unittest.TestSuite() - s.addTest(unittest.makeSuite(GEOSTest)) - return s - -def run(verbosity=2): - unittest.TextTestRunner(verbosity=verbosity).run(suite()) diff --git a/django/contrib/gis/geos/tests/test_geos_mutation.py b/django/contrib/gis/geos/tests/test_geos_mutation.py index 0c69b2bf4f..40b708a0ef 100644 --- a/django/contrib/gis/geos/tests/test_geos_mutation.py +++ b/django/contrib/gis/geos/tests/test_geos_mutation.py @@ -2,15 +2,23 @@ # Modified from original contribution by Aryeh Leib Taurog, which was # released under the New BSD license. -from django.contrib.gis.geos import * -from django.contrib.gis.geos.error import GEOSIndexError from django.utils import unittest +from django.utils.unittest import skipUnless + +from .. import HAS_GEOS + +if HAS_GEOS: + from .. import * + from ..error import GEOSIndexError + def getItem(o,i): return o[i] def delItem(o,i): del o[i] def setItem(o,i,v): o[i] = v -def api_get_distance(x): return x.distance(Point(-200,-200)) +if HAS_GEOS: + def api_get_distance(x): return x.distance(Point(-200,-200)) + def api_get_buffer(x): return x.buffer(10) def api_get_geom_typeid(x): return x.geom_typeid def api_get_num_coords(x): return x.num_coords @@ -29,6 +37,8 @@ geos_function_tests = [ val for name, val in vars().items() if hasattr(val, '__call__') and name.startswith('api_get_') ] + +@skipUnless(HAS_GEOS, "Geos is required.") class GEOSMutationTest(unittest.TestCase): """ Tests Pythonic Mutability of Python GEOS geometry wrappers @@ -122,14 +132,3 @@ class GEOSMutationTest(unittest.TestCase): lsa = MultiPoint(*map(Point,((5,5),(3,-2),(8,1)))) for f in geos_function_tests: self.assertEqual(f(lsa), f(mp), 'MultiPoint ' + f.__name__) - -def suite(): - s = unittest.TestSuite() - s.addTest(unittest.makeSuite(GEOSMutationTest)) - return s - -def run(verbosity=2): - unittest.TextTestRunner(verbosity=verbosity).run(suite()) - -if __name__ == '__main__': - run() diff --git a/django/contrib/gis/geos/tests/test_io.py b/django/contrib/gis/geos/tests/test_io.py index 45a9a220b1..38ca2e0923 100644 --- a/django/contrib/gis/geos/tests/test_io.py +++ b/django/contrib/gis/geos/tests/test_io.py @@ -4,10 +4,16 @@ import binascii import unittest from django.contrib.gis import memoryview -from django.contrib.gis.geos import GEOSGeometry, WKTReader, WKTWriter, WKBReader, WKBWriter, geos_version_info from django.utils import six +from django.utils.unittest import skipUnless +from ..import HAS_GEOS +if HAS_GEOS: + from .. import GEOSGeometry, WKTReader, WKTWriter, WKBReader, WKBWriter, geos_version_info + + +@skipUnless(HAS_GEOS, "Geos is required.") class GEOSIOTest(unittest.TestCase): def test01_wktreader(self): @@ -109,11 +115,3 @@ class GEOSIOTest(unittest.TestCase): wkb_w.srid = True self.assertEqual(hex3d_srid, wkb_w.write_hex(g)) self.assertEqual(wkb3d_srid, wkb_w.write(g)) - -def suite(): - s = unittest.TestSuite() - s.addTest(unittest.makeSuite(GEOSIOTest)) - return s - -def run(verbosity=2): - unittest.TextTestRunner(verbosity=verbosity).run(suite()) diff --git a/django/contrib/gis/geos/tests/test_mutable_list.py b/django/contrib/gis/geos/tests/test_mutable_list.py index 988d8417a2..a4a56f2e5f 100644 --- a/django/contrib/gis/geos/tests/test_mutable_list.py +++ b/django/contrib/gis/geos/tests/test_mutable_list.py @@ -395,15 +395,3 @@ class ListMixinTest(unittest.TestCase): class ListMixinTestSingle(ListMixinTest): listType = UserListB - -def suite(): - s = unittest.TestSuite() - s.addTest(unittest.makeSuite(ListMixinTest)) - s.addTest(unittest.makeSuite(ListMixinTestSingle)) - return s - -def run(verbosity=2): - unittest.TextTestRunner(verbosity=verbosity).run(suite()) - -if __name__ == '__main__': - run() diff --git a/django/contrib/gis/sitemaps/views.py b/django/contrib/gis/sitemaps/views.py index 36e48f7d20..9682c125d2 100644 --- a/django/contrib/gis/sitemaps/views.py +++ b/django/contrib/gis/sitemaps/views.py @@ -20,7 +20,7 @@ def index(request, sitemaps): """ current_site = get_current_site(request) sites = [] - protocol = request.is_secure() and 'https' or 'http' + protocol = 'https' if request.is_secure() else 'http' for section, site in sitemaps.items(): if callable(site): pages = site().paginator.num_pages diff --git a/django/contrib/gis/static/gis/js/OLMapWidget.js b/django/contrib/gis/static/gis/js/OLMapWidget.js new file mode 100644 index 0000000000..252196b369 --- /dev/null +++ b/django/contrib/gis/static/gis/js/OLMapWidget.js @@ -0,0 +1,371 @@ +(function() { +/** + * Transforms an array of features to a single feature with the merged + * geometry of geom_type + */ +OpenLayers.Util.properFeatures = function(features, geom_type) { + if (features.constructor == Array) { + var geoms = []; + for (var i=0; i<features.length; i++) { + geoms.push(features[i].geometry); + } + var geom = new geom_type(geoms); + features = new OpenLayers.Feature.Vector(geom); + } + return features; +} + +/** + * @requires OpenLayers/Format/WKT.js + */ + +/** + * Class: OpenLayers.Format.DjangoWKT + * Class for reading Well-Known Text, with workarounds to successfully parse + * geometries and collections as returnes by django.contrib.gis.geos. + * + * Inherits from: + * - <OpenLayers.Format.WKT> + */ + +OpenLayers.Format.DjangoWKT = OpenLayers.Class(OpenLayers.Format.WKT, { + initialize: function(options) { + OpenLayers.Format.WKT.prototype.initialize.apply(this, [options]); + this.regExes.justComma = /\s*,\s*/; + }, + + parse: { + 'point': function(str) { + var coords = OpenLayers.String.trim(str).split(this.regExes.spaces); + return new OpenLayers.Feature.Vector( + new OpenLayers.Geometry.Point(coords[0], coords[1]) + ); + }, + + 'multipoint': function(str) { + var point; + var points = OpenLayers.String.trim(str).split(this.regExes.justComma); + var components = []; + for(var i=0, len=points.length; i<len; ++i) { + point = points[i].replace(this.regExes.trimParens, '$1'); + components.push(this.parse.point.apply(this, [point]).geometry); + } + return new OpenLayers.Feature.Vector( + new OpenLayers.Geometry.MultiPoint(components) + ); + }, + + 'linestring': function(str) { + var points = OpenLayers.String.trim(str).split(','); + var components = []; + for(var i=0, len=points.length; i<len; ++i) { + components.push(this.parse.point.apply(this, [points[i]]).geometry); + } + return new OpenLayers.Feature.Vector( + new OpenLayers.Geometry.LineString(components) + ); + }, + + 'multilinestring': function(str) { + var line; + var lines = OpenLayers.String.trim(str).split(this.regExes.parenComma); + var components = []; + for(var i=0, len=lines.length; i<len; ++i) { + line = lines[i].replace(this.regExes.trimParens, '$1'); + components.push(this.parse.linestring.apply(this, [line]).geometry); + } + return new OpenLayers.Feature.Vector( + new OpenLayers.Geometry.MultiLineString(components) + ); + }, + + 'polygon': function(str) { + var ring, linestring, linearring; + var rings = OpenLayers.String.trim(str).split(this.regExes.parenComma); + var components = []; + for(var i=0, len=rings.length; i<len; ++i) { + ring = rings[i].replace(this.regExes.trimParens, '$1'); + linestring = this.parse.linestring.apply(this, [ring]).geometry; + linearring = new OpenLayers.Geometry.LinearRing(linestring.components); + components.push(linearring); + } + return new OpenLayers.Feature.Vector( + new OpenLayers.Geometry.Polygon(components) + ); + }, + + 'multipolygon': function(str) { + var polygon; + var polygons = OpenLayers.String.trim(str).split(this.regExes.doubleParenComma); + var components = []; + for(var i=0, len=polygons.length; i<len; ++i) { + polygon = polygons[i].replace(this.regExes.trimParens, '$1'); + components.push(this.parse.polygon.apply(this, [polygon]).geometry); + } + return new OpenLayers.Feature.Vector( + new OpenLayers.Geometry.MultiPolygon(components) + ); + }, + + 'geometrycollection': function(str) { + // separate components of the collection with | + str = str.replace(/,\s*([A-Za-z])/g, '|$1'); + var wktArray = OpenLayers.String.trim(str).split('|'); + var components = []; + for(var i=0, len=wktArray.length; i<len; ++i) { + components.push(OpenLayers.Format.WKT.prototype.read.apply(this,[wktArray[i]])); + } + return components; + } + }, + + extractGeometry: function(geometry) { + var type = geometry.CLASS_NAME.split('.')[2].toLowerCase(); + if (!this.extract[type]) { + return null; + } + if (this.internalProjection && this.externalProjection) { + geometry = geometry.clone(); + geometry.transform(this.internalProjection, this.externalProjection); + } + var wktType = type == 'collection' ? 'GEOMETRYCOLLECTION' : type.toUpperCase(); + var data = wktType + '(' + this.extract[type].apply(this, [geometry]) + ')'; + return data; + }, + + /** + * Patched write: successfully writes WKT for geometries and + * geometrycollections. + */ + write: function(features) { + var collection, geometry, type, data, isCollection; + isCollection = features.geometry.CLASS_NAME == "OpenLayers.Geometry.Collection"; + var pieces = []; + if (isCollection) { + collection = features.geometry.components; + pieces.push('GEOMETRYCOLLECTION('); + for (var i=0, len=collection.length; i<len; ++i) { + if (i>0) { + pieces.push(','); + } + pieces.push(this.extractGeometry(collection[i])); + } + pieces.push(')'); + } else { + pieces.push(this.extractGeometry(features.geometry)); + } + return pieces.join(''); + }, + + CLASS_NAME: "OpenLayers.Format.DjangoWKT" +}); + +function MapWidget(options) { + this.map = null; + this.controls = null; + this.panel = null; + this.layers = {}; + this.wkt_f = new OpenLayers.Format.DjangoWKT(); + + // Mapping from OGRGeomType name to OpenLayers.Geometry name + if (options['geom_name'] == 'Unknown') options['geom_type'] = OpenLayers.Geometry; + else if (options['geom_name'] == 'GeometryCollection') options['geom_type'] = OpenLayers.Geometry.Collection; + else options['geom_type'] = eval('OpenLayers.Geometry' + options['geom_name']); + + // Default options + this.options = { + color: 'ee9900', + default_lat: 0, + default_lon: 0, + default_zoom: 4, + is_collection: options['geom_type'] instanceof OpenLayers.Geometry.Collection, + layerswitcher: false, + map_options: {}, + map_srid: 4326, + modifiable: true, + mouse_position: false, + opacity: 0.4, + point_zoom: 12, + scale_text: false, + scrollable: true + }; + + // Altering using user-provied options + for (var property in options) { + if (options.hasOwnProperty(property)) { + this.options[property] = options[property]; + } + } + + this.map = new OpenLayers.Map(this.options.map_id, this.options.map_options); + if (this.options.base_layer) this.layers.base = this.options.base_layer; + else this.layers.base = new OpenLayers.Layer.WMS('OpenLayers WMS', 'http://vmap0.tiles.osgeo.org/wms/vmap0', {layers: 'basic'}); + this.map.addLayer(this.layers.base); + + var defaults_style = { + 'fillColor': '#' + this.options.color, + 'fillOpacity': this.options.opacity, + 'strokeColor': '#' + this.options.color, + }; + if (this.options.geom_name == 'LineString') { + defaults_style['strokeWidth'] = 3; + } + var styleMap = new OpenLayers.StyleMap({'default': OpenLayers.Util.applyDefaults(defaults_style, OpenLayers.Feature.Vector.style['default'])}); + this.layers.vector = new OpenLayers.Layer.Vector(" " + this.options.name, {styleMap: styleMap}); + this.map.addLayer(this.layers.vector); + wkt = document.getElementById(this.options.id).value; + if (wkt) { + var feat = OpenLayers.Util.properFeatures(this.read_wkt(wkt), this.options.geom_type); + this.write_wkt(feat); + if (this.options.is_collection) { + for (var i=0; i<this.num_geom; i++) { + this.layers.vector.addFeatures([new OpenLayers.Feature.Vector(feat.geometry.components[i].clone())]); + } + } else { + this.layers.vector.addFeatures([feat]); + } + this.map.zoomToExtent(feat.geometry.getBounds()); + if (this.options.geom_name == 'Point') { + this.map.zoomTo(this.options.point_zoom); + } + } else { + this.map.setCenter(this.defaultCenter(), this.options.default_zoom); + } + this.layers.vector.events.on({'featuremodified': this.modify_wkt, scope: this}); + this.layers.vector.events.on({'featureadded': this.add_wkt, scope: this}); + + this.getControls(this.layers.vector); + this.panel.addControls(this.controls); + this.map.addControl(this.panel); + this.addSelectControl(); + + if (this.options.mouse_position) { + this.map.addControl(new OpenLayers.Control.MousePosition()); + } + if (this.options.scale_text) { + this.map.addControl(new OpenLayers.Control.Scale()); + } + if (this.options.layerswitcher) { + this.map.addControl(new OpenLayers.Control.LayerSwitcher()); + } + if (!this.options.scrollable) { + this.map.getControlsByClass('OpenLayers.Control.Navigation')[0].disableZoomWheel(); + } + if (wkt) { + if (this.options.modifiable) { + this.enableEditing(); + } + } else { + this.enableDrawing(); + } +} + +MapWidget.prototype.get_ewkt = function(feat) { + return "SRID=" + this.options.map_srid + ";" + this.wkt_f.write(feat); +}; + +MapWidget.prototype.read_wkt = function(wkt) { + var prefix = 'SRID=' + this.options.map_srid + ';' + if (wkt.indexOf(prefix) === 0) { + wkt = wkt.slice(prefix.length); + } + return this.wkt_f.read(wkt); +}; + +MapWidget.prototype.write_wkt = function(feat) { + feat = OpenLayers.Util.properFeatures(feat, this.options.geom_type); + if (this.options.is_collection) { + this.num_geom = feat.geometry.components.length; + } else { + this.num_geom = 1; + } + document.getElementById(this.options.id).value = this.get_ewkt(feat); +}; + +MapWidget.prototype.add_wkt = function(event) { + if (this.options.is_collection) { + var feat = new OpenLayers.Feature.Vector(new this.options.geom_type()); + for (var i=0; i<this.layers.vector.features.length; i++) { + feat.geometry.addComponents([this.layers.vector.features[i].geometry]); + } + this.write_wkt(feat); + } else { + if (this.layers.vector.features.length > 1) { + old_feats = [this.layers.vector.features[0]]; + this.layers.vector.removeFeatures(old_feats); + this.layers.vector.destroyFeatures(old_feats); + } + this.write_wkt(event.feature); + } +}; + +MapWidget.prototype.modify_wkt = function(event) { + if (this.options.is_collection) { + if (this.options.geom_name == 'MultiPoint') { + this.add_wkt(event); + return; + } else { + var feat = new OpenLayers.Feature.Vector(new this.options.geom_type()); + for (var i=0; i<this.num_geom; i++) { + feat.geometry.addComponents([this.layers.vector.features[i].geometry]); + } + this.write_wkt(feat); + } + } else { + this.write_wkt(event.feature); + } +}; + +MapWidget.prototype.deleteFeatures = function() { + this.layers.vector.removeFeatures(this.layers.vector.features); + this.layers.vector.destroyFeatures(); +}; + +MapWidget.prototype.clearFeatures = function() { + this.deleteFeatures(); + document.getElementById(this.options.id).value = ''; + this.map.setCenter(this.defaultCenter(), this.options.default_zoom); +}; + +MapWidget.prototype.defaultCenter = function() { + var center = new OpenLayers.LonLat(this.options.default_lon, this.options.default_lat); + if (this.options.map_srid) { + return center.transform(new OpenLayers.Projection("EPSG:4326"), this.map.getProjectionObject()); + } + return center; +}; + +MapWidget.prototype.addSelectControl = function() { + var select = new OpenLayers.Control.SelectFeature(this.layers.vector, {'toggle': true, 'clickout': true}); + this.map.addControl(select); + select.activate(); +}; + +MapWidget.prototype.enableDrawing = function () { + this.map.getControlsByClass('OpenLayers.Control.DrawFeature')[0].activate(); +}; + +MapWidget.prototype.enableEditing = function () { + this.map.getControlsByClass('OpenLayers.Control.ModifyFeature')[0].activate(); +}; + +MapWidget.prototype.getControls = function(layer) { + this.panel = new OpenLayers.Control.Panel({'displayClass': 'olControlEditingToolbar'}); + this.controls = [new OpenLayers.Control.Navigation()]; + if (!this.options.modifiable && layer.features.length) + return; + if (this.options.geom_name == 'LineString' || this.options.geom_name == 'Unknown') { + this.controls.push(new OpenLayers.Control.DrawFeature(layer, OpenLayers.Handler.Path, {'displayClass': 'olControlDrawFeaturePath'})); + } + if (this.options.geom_name == 'Polygon' || this.options.geom_name == 'Unknown') { + this.controls.push(new OpenLayers.Control.DrawFeature(layer, OpenLayers.Handler.Polygon, {'displayClass': 'olControlDrawFeaturePolygon'})); + } + if (this.options.geom_name == 'Point' || this.options.geom_name == 'Unknown') { + this.controls.push(new OpenLayers.Control.DrawFeature(layer, OpenLayers.Handler.Point, {'displayClass': 'olControlDrawFeaturePoint'})); + } + if (this.options.modifiable) { + this.controls.push(new OpenLayers.Control.ModifyFeature(layer, {'displayClass': 'olControlModifyFeature'})); + } +}; +window.MapWidget = MapWidget; +})(); diff --git a/django/contrib/gis/templates/gis/admin/openlayers.js b/django/contrib/gis/templates/gis/admin/openlayers.js index 924621ea49..4425fee27e 100644 --- a/django/contrib/gis/templates/gis/admin/openlayers.js +++ b/django/contrib/gis/templates/gis/admin/openlayers.js @@ -12,7 +12,7 @@ OpenLayers.Projection.addTransform("EPSG:4326", "EPSG:3857", OpenLayers.Layer.Sp {{ module }}.is_point = {{ is_point|yesno:"true,false" }}; {% endblock %} {{ module }}.get_ewkt = function(feat){ - return 'SRID={{ srid }};' + {{ module }}.wkt_f.write(feat); + return 'SRID={{ srid|unlocalize }};' + {{ module }}.wkt_f.write(feat); }; {{ module }}.read_wkt = function(wkt){ // OpenLayers cannot handle EWKT -- we make sure to strip it out. diff --git a/django/contrib/gis/templates/gis/openlayers-osm.html b/django/contrib/gis/templates/gis/openlayers-osm.html new file mode 100644 index 0000000000..7b644aaa8d --- /dev/null +++ b/django/contrib/gis/templates/gis/openlayers-osm.html @@ -0,0 +1,17 @@ +{% extends "gis/openlayers.html" %} +{% load l10n %} + +{% block map_options %}var map_options = { + maxExtend: new OpenLayers.Bounds(-20037508,-20037508,20037508,20037508), + maxResolution: 156543.0339, + numZoomLevels: 20, + units: 'm' +};{% endblock %} + +{% block options %}{{ block.super }} +options['scale_text'] = true; +options['mouse_position'] = true; +options['default_lon'] = {{ default_lon|unlocalize }}; +options['default_lat'] = {{ default_lat|unlocalize }}; +options['base_layer'] = new OpenLayers.Layer.OSM.Mapnik("OpenStreetMap (Mapnik)"); +{% endblock %} diff --git a/django/contrib/gis/templates/gis/openlayers.html b/django/contrib/gis/templates/gis/openlayers.html new file mode 100644 index 0000000000..281c0badd6 --- /dev/null +++ b/django/contrib/gis/templates/gis/openlayers.html @@ -0,0 +1,34 @@ +<style type="text/css">{% block map_css %} + #{{ id }}_map { width: {{ map_width }}px; height: {{ map_height }}px; } + #{{ id }}_map .aligned label { float: inherit; } + #{{ id }}_div_map { position: relative; vertical-align: top; float: {{ LANGUAGE_BIDI|yesno:"right,left" }}; } + {% if not display_wkt %}#{{ id }} { display: none; }{% endif %} + .olControlEditingToolbar .olControlModifyFeatureItemActive { + background-image: url("{{ STATIC_URL }}admin/img/gis/move_vertex_on.png"); + background-repeat: no-repeat; + } + .olControlEditingToolbar .olControlModifyFeatureItemInactive { + background-image: url("{{ STATIC_URL }}admin/img/gis/move_vertex_off.png"); + background-repeat: no-repeat; + }{% endblock %} +</style> + +<div id="{{ id }}_div_map"> + <div id="{{ id }}_map"></div> + <span class="clear_features"><a href="javascript:{{ module }}.clearFeatures()">Delete all Features</a></span> + {% if display_wkt %}<p> WKT debugging window:</p>{% endif %} + <textarea id="{{ id }}" class="vWKTField required" cols="150" rows="10" name="{{ name }}">{{ wkt }}</textarea> + <script type="text/javascript"> + {% block map_options %}var map_options = {};{% endblock %} + {% block options %}var options = { + geom_name: '{{ geom_type }}', + id: '{{ id }}', + map_id: '{{ id }}_map', + map_options: map_options, + map_srid: {{ map_srid }}, + name: '{{ name }}' + }; + {% endblock %} + var {{ module }} = new MapWidget(options); + </script> +</div> diff --git a/django/contrib/gis/tests/__init__.py b/django/contrib/gis/tests/__init__.py index 765c03018b..1703f3f1b3 100644 --- a/django/contrib/gis/tests/__init__.py +++ b/django/contrib/gis/tests/__init__.py @@ -1,13 +1,4 @@ -from django.conf import settings -from django.test.simple import build_suite, DjangoTestSuiteRunner -from django.utils import unittest - -from .test_geoforms import GeometryFieldTest -from .test_measure import DistanceTest, AreaTest -from .test_spatialrefsys import SpatialRefSysTest - - -def geo_apps(namespace=True, runtests=False): +def geo_apps(): """ Returns a list of GeoDjango test applications that reside in `django.contrib.gis.tests` that can be used with the current @@ -36,88 +27,4 @@ def geo_apps(namespace=True, runtests=False): # 3D apps use LayerMapping, which uses GDAL and require GEOS 3.1+. if connection.ops.postgis and GEOS_PREPARE: apps.append('geo3d') - if runtests: - return [('django.contrib.gis.tests', app) for app in apps] - elif namespace: - return ['django.contrib.gis.tests.%s' % app - for app in apps] - else: - return apps - - -def geodjango_suite(apps=True): - """ - Returns a TestSuite consisting only of GeoDjango tests that can be run. - """ - import sys - from django.db.models import get_app - - suite = unittest.TestSuite() - - # Adding the GEOS tests. - from django.contrib.gis.geos import tests as geos_tests - suite.addTest(geos_tests.suite()) - - # Adding GDAL tests, and any test suite that depends on GDAL, to the - # suite if GDAL is available. - from django.contrib.gis.gdal import HAS_GDAL - if HAS_GDAL: - from django.contrib.gis.gdal import tests as gdal_tests - suite.addTest(gdal_tests.suite()) - else: - sys.stderr.write('GDAL not available - no tests requiring GDAL will be run.\n') - - # Add GeoIP tests to the suite, if the library and data is available. - from django.contrib.gis.geoip import HAS_GEOIP - if HAS_GEOIP and hasattr(settings, 'GEOIP_PATH'): - from django.contrib.gis.geoip import tests as geoip_tests - suite.addTest(geoip_tests.suite()) - - # Finally, adding the suites for each of the GeoDjango test apps. - if apps: - for app_name in geo_apps(namespace=False): - suite.addTest(build_suite(get_app(app_name))) - - return suite - - -class GeoDjangoTestSuiteRunner(DjangoTestSuiteRunner): - - def setup_test_environment(self, **kwargs): - super(GeoDjangoTestSuiteRunner, self).setup_test_environment(**kwargs) - - # Saving original values of INSTALLED_APPS, ROOT_URLCONF, and SITE_ID. - self.old_installed = getattr(settings, 'INSTALLED_APPS', None) - self.old_root_urlconf = getattr(settings, 'ROOT_URLCONF', '') - self.old_site_id = getattr(settings, 'SITE_ID', None) - - # Constructing the new INSTALLED_APPS, and including applications - # within the GeoDjango test namespace. - new_installed = [ - 'django.contrib.sites', - 'django.contrib.sitemaps', - 'django.contrib.gis', - ] - - # Calling out to `geo_apps` to get GeoDjango applications supported - # for testing. - new_installed.extend(geo_apps()) - settings.INSTALLED_APPS = list(self.old_installed) + new_installed - - # SITE_ID needs to be set - settings.SITE_ID = 1 - - # ROOT_URLCONF needs to be set, else `AttributeErrors` are raised - # when TestCases are torn down that have `urls` defined. - settings.ROOT_URLCONF = '' - - - def teardown_test_environment(self, **kwargs): - super(GeoDjangoTestSuiteRunner, self).teardown_test_environment(**kwargs) - settings.INSTALLED_APPS = self.old_installed - settings.ROOT_URLCONF = self.old_root_urlconf - settings.SITE_ID = self.old_site_id - - - def build_suite(self, test_labels, extra_tests=None, **kwargs): - return geodjango_suite() + return [('django.contrib.gis.tests', app) for app in apps] diff --git a/django/contrib/gis/tests/distapp/tests.py b/django/contrib/gis/tests/distapp/tests.py index 5574b42738..2ed17a03bd 100644 --- a/django/contrib/gis/tests/distapp/tests.py +++ b/django/contrib/gis/tests/distapp/tests.py @@ -2,24 +2,33 @@ from __future__ import absolute_import from django.db import connection from django.db.models import Q -from django.contrib.gis.geos import GEOSGeometry, LineString +from django.contrib.gis.geos import HAS_GEOS from django.contrib.gis.measure import D # alias for Distance -from django.contrib.gis.tests.utils import oracle, postgis, spatialite, no_oracle, no_spatialite +from django.contrib.gis.tests.utils import ( + HAS_SPATIAL_DB, mysql, oracle, postgis, spatialite, no_oracle, no_spatialite +) from django.test import TestCase +from django.utils.unittest import skipUnless -from .models import (AustraliaCity, Interstate, SouthTexasInterstate, - SouthTexasCity, SouthTexasCityFt, CensusZipcode, SouthTexasZipcode) +if HAS_GEOS and HAS_SPATIAL_DB: + from django.contrib.gis.geos import GEOSGeometry, LineString + from .models import (AustraliaCity, Interstate, SouthTexasInterstate, + SouthTexasCity, SouthTexasCityFt, CensusZipcode, SouthTexasZipcode) + +@skipUnless(HAS_GEOS and HAS_SPATIAL_DB and not mysql, + "Geos and spatial db (not mysql) are required.") class DistanceTest(TestCase): - # A point we are testing distances with -- using a WGS84 - # coordinate that'll be implicitly transormed to that to - # the coordinate system of the field, EPSG:32140 (Texas South Central - # w/units in meters) - stx_pnt = GEOSGeometry('POINT (-95.370401017314293 29.704867409475465)', 4326) - # Another one for Australia - au_pnt = GEOSGeometry('POINT (150.791 -34.4919)', 4326) + if HAS_GEOS and HAS_SPATIAL_DB: + # A point we are testing distances with -- using a WGS84 + # coordinate that'll be implicitly transormed to that to + # the coordinate system of the field, EPSG:32140 (Texas South Central + # w/units in meters) + stx_pnt = GEOSGeometry('POINT (-95.370401017314293 29.704867409475465)', 4326) + # Another one for Australia + au_pnt = GEOSGeometry('POINT (150.791 -34.4919)', 4326) def get_names(self, qs): cities = [c.name for c in qs] diff --git a/django/contrib/gis/tests/geo3d/tests.py b/django/contrib/gis/tests/geo3d/tests.py index 6b40164422..df9f35690b 100644 --- a/django/contrib/gis/tests/geo3d/tests.py +++ b/django/contrib/gis/tests/geo3d/tests.py @@ -3,14 +3,22 @@ from __future__ import absolute_import, unicode_literals import os import re -from django.contrib.gis.db.models import Union, Extent3D -from django.contrib.gis.geos import GEOSGeometry, LineString, Point, Polygon -from django.contrib.gis.utils import LayerMapping, LayerMapError +from django.contrib.gis.gdal import HAS_GDAL +from django.contrib.gis.geos import HAS_GEOS +from django.contrib.gis.tests.utils import postgis from django.test import TestCase from django.utils._os import upath +from django.utils.unittest import skipUnless -from .models import (City3D, Interstate2D, Interstate3D, InterstateProj2D, - InterstateProj3D, Point2D, Point3D, MultiPoint3D, Polygon2D, Polygon3D) +if HAS_GEOS: + from django.contrib.gis.db.models import Union, Extent3D + from django.contrib.gis.geos import GEOSGeometry, LineString, Point, Polygon + + from .models import (City3D, Interstate2D, Interstate3D, InterstateProj2D, + InterstateProj3D, Point2D, Point3D, MultiPoint3D, Polygon2D, Polygon3D) + +if HAS_GDAL: + from django.contrib.gis.utils import LayerMapping, LayerMapError data_path = os.path.realpath(os.path.join(os.path.dirname(upath(__file__)), '..', 'data')) @@ -54,6 +62,7 @@ bbox_data = ( ) +@skipUnless(HAS_GEOS and HAS_GDAL and postgis, "Geos, GDAL and postgis are required.") class Geo3DTest(TestCase): """ Only a subset of the PostGIS routines are 3D-enabled, and this TestCase diff --git a/django/contrib/gis/tests/geoadmin/tests.py b/django/contrib/gis/tests/geoadmin/tests.py index 669914bdea..15874758be 100644 --- a/django/contrib/gis/tests/geoadmin/tests.py +++ b/django/contrib/gis/tests/geoadmin/tests.py @@ -1,12 +1,18 @@ from __future__ import absolute_import from django.test import TestCase -from django.contrib.gis import admin -from django.contrib.gis.geos import GEOSGeometry, Point +from django.contrib.gis.geos import HAS_GEOS +from django.contrib.gis.tests.utils import HAS_SPATIAL_DB +from django.utils.unittest import skipUnless -from .models import City +if HAS_GEOS and HAS_SPATIAL_DB: + from django.contrib.gis import admin + from django.contrib.gis.geos import Point + from .models import City + +@skipUnless(HAS_GEOS and HAS_SPATIAL_DB, "Geos and spatial db are required.") class GeoAdminTest(TestCase): urls = 'django.contrib.gis.tests.geoadmin.urls' @@ -24,10 +30,7 @@ class GeoAdminTest(TestCase): result) def test_olmap_WMS_rendering(self): - admin.site.unregister(City) - admin.site.register(City, admin.GeoModelAdmin) - - geoadmin = admin.site._registry[City] + geoadmin = admin.GeoModelAdmin(City, admin.site) result = geoadmin.get_map_widget(City._meta.get_field('point'))( ).render('point', Point(-79.460734, 40.18476)) self.assertIn( diff --git a/django/contrib/gis/tests/geoapp/test_feeds.py b/django/contrib/gis/tests/geoapp/test_feeds.py index c9cf6362c1..778cadc9d4 100644 --- a/django/contrib/gis/tests/geoapp/test_feeds.py +++ b/django/contrib/gis/tests/geoapp/test_feeds.py @@ -4,11 +4,16 @@ from xml.dom import minidom from django.conf import settings from django.contrib.sites.models import Site +from django.contrib.gis.geos import HAS_GEOS +from django.contrib.gis.tests.utils import HAS_SPATIAL_DB from django.test import TestCase +from django.utils.unittest import skipUnless -from .models import City +if HAS_GEOS: + from .models import City +@skipUnless(HAS_GEOS and HAS_SPATIAL_DB, "Geos and spatial db are required.") class GeoFeedTest(TestCase): urls = 'django.contrib.gis.tests.geoapp.urls' diff --git a/django/contrib/gis/tests/geoapp/test_regress.py b/django/contrib/gis/tests/geoapp/test_regress.py index a27b2d40f6..43dbcfd852 100644 --- a/django/contrib/gis/tests/geoapp/test_regress.py +++ b/django/contrib/gis/tests/geoapp/test_regress.py @@ -3,14 +3,19 @@ from __future__ import absolute_import, unicode_literals from datetime import datetime +from django.contrib.gis.geos import HAS_GEOS from django.contrib.gis.tests.utils import no_mysql, no_spatialite from django.contrib.gis.shortcuts import render_to_kmz +from django.contrib.gis.tests.utils import HAS_SPATIAL_DB from django.db.models import Count, Min from django.test import TestCase +from django.utils.unittest import skipUnless -from .models import City, PennsylvaniaCity, State, Truth +if HAS_GEOS: + from .models import City, PennsylvaniaCity, State, Truth +@skipUnless(HAS_GEOS and HAS_SPATIAL_DB, "Geos and spatial db are required.") class GeoRegressionTests(TestCase): def test_update(self): @@ -72,8 +77,8 @@ class GeoRegressionTests(TestCase): t1 = Truth.objects.create(val=True) t2 = Truth.objects.create(val=False) - val1 = Truth.objects.get(pk=1).val - val2 = Truth.objects.get(pk=2).val + val1 = Truth.objects.get(pk=t1.pk).val + val2 = Truth.objects.get(pk=t2.pk).val # verify types -- should't be 0/1 self.assertIsInstance(val1, bool) self.assertIsInstance(val2, bool) diff --git a/django/contrib/gis/tests/geoapp/test_sitemaps.py b/django/contrib/gis/tests/geoapp/test_sitemaps.py index aa2d97032c..337b4b75c6 100644 --- a/django/contrib/gis/tests/geoapp/test_sitemaps.py +++ b/django/contrib/gis/tests/geoapp/test_sitemaps.py @@ -5,12 +5,17 @@ from xml.dom import minidom import zipfile from django.conf import settings +from django.contrib.gis.geos import HAS_GEOS +from django.contrib.gis.tests.utils import HAS_SPATIAL_DB from django.contrib.sites.models import Site from django.test import TestCase +from django.utils.unittest import skipUnless -from .models import City, Country +if HAS_GEOS: + from .models import City, Country +@skipUnless(HAS_GEOS and HAS_SPATIAL_DB, "Geos and spatial db are required.") class GeoSitemapTest(TestCase): urls = 'django.contrib.gis.tests.geoapp.urls' diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index 672d78c1dd..cf6e316919 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -3,26 +3,31 @@ from __future__ import absolute_import import re from django.db import connection -from django.db.utils import DatabaseError from django.contrib.gis import gdal -from django.contrib.gis.geos import (fromstr, GEOSGeometry, - Point, LineString, LinearRing, Polygon, GeometryCollection) +from django.contrib.gis.geos import HAS_GEOS from django.contrib.gis.tests.utils import ( no_mysql, no_oracle, no_spatialite, mysql, oracle, postgis, spatialite) from django.test import TestCase from django.utils import six, unittest +from django.utils.unittest import skipUnless -from .models import Country, City, PennsylvaniaCity, State, Track +if HAS_GEOS: + from django.contrib.gis.geos import (fromstr, GEOSGeometry, + Point, LineString, LinearRing, Polygon, GeometryCollection) -from .test_feeds import GeoFeedTest -from .test_regress import GeoRegressionTests -from .test_sitemaps import GeoSitemapTest + from .models import Country, City, PennsylvaniaCity, State, Track - -if not spatialite: +if HAS_GEOS and not spatialite: from .models import Feature, MinusOneSRID + +def postgis_bug_version(): + spatial_version = getattr(connection.ops, "spatial_version", (0,0,0)) + return spatial_version and (2, 0, 0) <= spatial_version <= (2, 0, 1) + + +@skipUnless(HAS_GEOS and postgis, "Geos and postgis are required.") class GeoModelTest(TestCase): def test_fixtures(self): @@ -197,6 +202,7 @@ class GeoModelTest(TestCase): self.assertTrue(isinstance(cities2[0].point, Point)) +@skipUnless(HAS_GEOS and postgis, "Geos and postgis are required.") class GeoLookupTest(TestCase): @no_mysql @@ -297,7 +303,7 @@ class GeoLookupTest(TestCase): # The left/right lookup tests are known failures on PostGIS 2.0/2.0.1 # http://trac.osgeo.org/postgis/ticket/2035 - if connection.ops.postgis and (2, 0, 0) <= connection.ops.spatial_version <= (2, 0, 1): + if postgis_bug_version(): test_left_right_lookups = unittest.expectedFailure(test_left_right_lookups) def test_equals_lookups(self): @@ -382,6 +388,7 @@ class GeoLookupTest(TestCase): self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, intersects_mask)).name) +@skipUnless(HAS_GEOS and postgis, "Geos and postgis are required.") class GeoQuerySetTest(TestCase): # Please keep the tests in GeoQuerySet method's alphabetic order diff --git a/django/contrib/gis/tests/geogapp/tests.py b/django/contrib/gis/tests/geogapp/tests.py index a8c607c502..ed54999f90 100644 --- a/django/contrib/gis/tests/geogapp/tests.py +++ b/django/contrib/gis/tests/geogapp/tests.py @@ -5,14 +5,19 @@ from __future__ import absolute_import import os -from django.contrib.gis import gdal +from django.contrib.gis.gdal import HAS_GDAL +from django.contrib.gis.geos import HAS_GEOS from django.contrib.gis.measure import D +from django.contrib.gis.tests.utils import postgis from django.test import TestCase from django.utils._os import upath +from django.utils.unittest import skipUnless -from .models import City, County, Zipcode +if HAS_GEOS: + from .models import City, County, Zipcode +@skipUnless(HAS_GEOS and postgis, "Geos and postgis are required.") class GeographyTest(TestCase): def test01_fixture_load(self): @@ -54,11 +59,11 @@ class GeographyTest(TestCase): htown = City.objects.get(name='Houston') self.assertRaises(ValueError, City.objects.get, point__exact=htown.point) + @skipUnless(HAS_GDAL, "GDAL is required.") def test05_geography_layermapping(self): "Testing LayerMapping support on models with geography fields." # There is a similar test in `layermap` that uses the same data set, # but the County model here is a bit different. - if not gdal.HAS_GDAL: return from django.contrib.gis.utils import LayerMapping # Getting the shapefile and mapping dictionary. diff --git a/django/contrib/gis/tests/inspectapp/tests.py b/django/contrib/gis/tests/inspectapp/tests.py index d28bdf0290..668b87ba86 100644 --- a/django/contrib/gis/tests/inspectapp/tests.py +++ b/django/contrib/gis/tests/inspectapp/tests.py @@ -4,13 +4,19 @@ import os from django.db import connections from django.test import TestCase -from django.contrib.gis.gdal import Driver +from django.contrib.gis.gdal import HAS_GDAL from django.contrib.gis.geometry.test_data import TEST_DATA -from django.contrib.gis.utils.ogrinspect import ogrinspect +from django.contrib.gis.tests.utils import HAS_SPATIAL_DB +from django.utils.unittest import skipUnless -from .models import AllOGRFields +if HAS_GDAL: + from django.contrib.gis.gdal import Driver + from django.contrib.gis.utils.ogrinspect import ogrinspect + from .models import AllOGRFields + +@skipUnless(HAS_GDAL and HAS_SPATIAL_DB, "GDAL and spatial db are required.") class OGRInspectTest(TestCase): maxDiff = 1024 diff --git a/django/contrib/gis/tests/layermap/tests.py b/django/contrib/gis/tests/layermap/tests.py index 470e5be216..8379311a2b 100644 --- a/django/contrib/gis/tests/layermap/tests.py +++ b/django/contrib/gis/tests/layermap/tests.py @@ -5,19 +5,23 @@ import os from copy import copy from decimal import Decimal -from django.contrib.gis.gdal import DataSource -from django.contrib.gis.tests.utils import mysql -from django.contrib.gis.utils.layermapping import (LayerMapping, LayerMapError, - InvalidDecimal, MissingForeignKey) +from django.contrib.gis.gdal import HAS_GDAL +from django.contrib.gis.tests.utils import HAS_SPATIAL_DB, mysql from django.db import router from django.conf import settings from django.test import TestCase from django.utils import unittest +from django.utils.unittest import skipUnless from django.utils._os import upath -from .models import ( - City, County, CountyFeat, Interstate, ICity1, ICity2, Invalid, State, - city_mapping, co_mapping, cofeat_mapping, inter_mapping) +if HAS_GDAL: + from django.contrib.gis.utils.layermapping import (LayerMapping, + LayerMapError, InvalidDecimal, MissingForeignKey) + from django.contrib.gis.gdal import DataSource + + from .models import ( + City, County, CountyFeat, Interstate, ICity1, ICity2, Invalid, State, + city_mapping, co_mapping, cofeat_mapping, inter_mapping) shp_path = os.path.realpath(os.path.join(os.path.dirname(upath(__file__)), os.pardir, 'data')) @@ -32,6 +36,7 @@ NUMS = [1, 2, 1, 19, 1] # Number of polygons for each. STATES = ['Texas', 'Texas', 'Texas', 'Hawaii', 'Colorado'] +@skipUnless(HAS_GDAL and HAS_SPATIAL_DB, "GDAL and spatial db are required.") class LayerMapTest(TestCase): def test_init(self): @@ -310,6 +315,7 @@ class OtherRouter(object): return True +@skipUnless(HAS_GDAL and HAS_SPATIAL_DB, "GDAL and spatial db are required.") class LayerMapRouterTest(TestCase): def setUp(self): diff --git a/django/contrib/gis/tests/relatedapp/tests.py b/django/contrib/gis/tests/relatedapp/tests.py index 93a8ff4efd..6320edcff5 100644 --- a/django/contrib/gis/tests/relatedapp/tests.py +++ b/django/contrib/gis/tests/relatedapp/tests.py @@ -2,15 +2,20 @@ from __future__ import absolute_import from datetime import date -from django.contrib.gis.geos import GEOSGeometry, Point, MultiPoint -from django.contrib.gis.db.models import Collect, Count, Extent, F, Union -from django.contrib.gis.geometry.backend import Geometry -from django.contrib.gis.tests.utils import mysql, oracle, no_mysql, no_oracle, no_spatialite +from django.contrib.gis.geos import HAS_GEOS +from django.contrib.gis.tests.utils import HAS_SPATIAL_DB, mysql, oracle, no_mysql, no_oracle, no_spatialite from django.test import TestCase +from django.utils.unittest import skipUnless -from .models import City, Location, DirectoryEntry, Parcel, Book, Author, Article +if HAS_GEOS: + from django.contrib.gis.db.models import Collect, Count, Extent, F, Union + from django.contrib.gis.geometry.backend import Geometry + from django.contrib.gis.geos import GEOSGeometry, Point, MultiPoint + from .models import City, Location, DirectoryEntry, Parcel, Book, Author, Article + +@skipUnless(HAS_GEOS and HAS_SPATIAL_DB, "Geos and spatial db are required.") class RelatedGeoModelTest(TestCase): def test02_select_related(self): diff --git a/django/contrib/gis/tests/test_geoforms.py b/django/contrib/gis/tests/test_geoforms.py index 24bb50c6bc..402d9b944b 100644 --- a/django/contrib/gis/tests/test_geoforms.py +++ b/django/contrib/gis/tests/test_geoforms.py @@ -1,24 +1,25 @@ from django.forms import ValidationError from django.contrib.gis.gdal import HAS_GDAL from django.contrib.gis.tests.utils import HAS_SPATIALREFSYS +from django.test import SimpleTestCase from django.utils import six -from django.utils import unittest +from django.utils.unittest import skipUnless if HAS_SPATIALREFSYS: from django.contrib.gis import forms from django.contrib.gis.geos import GEOSGeometry -@unittest.skipUnless(HAS_GDAL and HAS_SPATIALREFSYS, "GeometryFieldTest needs gdal support and a spatial database") -class GeometryFieldTest(unittest.TestCase): +@skipUnless(HAS_GDAL and HAS_SPATIALREFSYS, "GeometryFieldTest needs gdal support and a spatial database") +class GeometryFieldTest(SimpleTestCase): - def test00_init(self): + def test_init(self): "Testing GeometryField initialization with defaults." fld = forms.GeometryField() for bad_default in ('blah', 3, 'FoO', None, 0): self.assertRaises(ValidationError, fld.clean, bad_default) - def test01_srid(self): + def test_srid(self): "Testing GeometryField with a SRID set." # Input that doesn't specify the SRID is assumed to be in the SRID # of the input field. @@ -34,7 +35,7 @@ class GeometryFieldTest(unittest.TestCase): cleaned_geom = fld.clean('SRID=4326;POINT (-95.363151 29.763374)') self.assertTrue(xform_geom.equals_exact(cleaned_geom, tol)) - def test02_null(self): + def test_null(self): "Testing GeometryField's handling of null (None) geometries." # Form fields, by default, are required (`required=True`) fld = forms.GeometryField() @@ -46,7 +47,7 @@ class GeometryFieldTest(unittest.TestCase): fld = forms.GeometryField(required=False) self.assertIsNone(fld.clean(None)) - def test03_geom_type(self): + def test_geom_type(self): "Testing GeometryField's handling of different geometry types." # By default, all geometry types are allowed. fld = forms.GeometryField() @@ -60,7 +61,7 @@ class GeometryFieldTest(unittest.TestCase): # but rejected by `clean` self.assertRaises(forms.ValidationError, pnt_fld.clean, 'LINESTRING(0 0, 1 1)') - def test04_to_python(self): + def test_to_python(self): """ Testing to_python returns a correct GEOSGeometry object or a ValidationError @@ -74,13 +75,169 @@ class GeometryFieldTest(unittest.TestCase): self.assertRaises(forms.ValidationError, fld.to_python, wkt) -def suite(): - s = unittest.TestSuite() - s.addTest(unittest.makeSuite(GeometryFieldTest)) - return s +@skipUnless(HAS_GDAL and HAS_SPATIALREFSYS, + "SpecializedFieldTest needs gdal support and a spatial database") +class SpecializedFieldTest(SimpleTestCase): + def setUp(self): + self.geometries = { + 'point': GEOSGeometry("SRID=4326;POINT(9.052734375 42.451171875)"), + 'multipoint': GEOSGeometry("SRID=4326;MULTIPOINT(" + "(13.18634033203125 14.504356384277344)," + "(13.207969665527 14.490966796875)," + "(13.177070617675 14.454917907714))"), + 'linestring': GEOSGeometry("SRID=4326;LINESTRING(" + "-8.26171875 -0.52734375," + "-7.734375 4.21875," + "6.85546875 3.779296875," + "5.44921875 -3.515625)"), + 'multilinestring': GEOSGeometry("SRID=4326;MULTILINESTRING(" + "(-16.435546875 -2.98828125," + "-17.2265625 2.98828125," + "-0.703125 3.515625," + "-1.494140625 -3.33984375)," + "(-8.0859375 -5.9765625," + "8.525390625 -8.7890625," + "12.392578125 -0.87890625," + "10.01953125 7.646484375))"), + 'polygon': GEOSGeometry("SRID=4326;POLYGON(" + "(-1.669921875 6.240234375," + "-3.8671875 -0.615234375," + "5.9765625 -3.955078125," + "18.193359375 3.955078125," + "9.84375 9.4921875," + "-1.669921875 6.240234375))"), + 'multipolygon': GEOSGeometry("SRID=4326;MULTIPOLYGON(" + "((-17.578125 13.095703125," + "-17.2265625 10.8984375," + "-13.974609375 10.1953125," + "-13.359375 12.744140625," + "-15.732421875 13.7109375," + "-17.578125 13.095703125))," + "((-8.525390625 5.537109375," + "-8.876953125 2.548828125," + "-5.888671875 1.93359375," + "-5.09765625 4.21875," + "-6.064453125 6.240234375," + "-8.525390625 5.537109375)))"), + 'geometrycollection': GEOSGeometry("SRID=4326;GEOMETRYCOLLECTION(" + "POINT(5.625 -0.263671875)," + "POINT(6.767578125 -3.603515625)," + "POINT(8.525390625 0.087890625)," + "POINT(8.0859375 -2.13134765625)," + "LINESTRING(" + "6.273193359375 -1.175537109375," + "5.77880859375 -1.812744140625," + "7.27294921875 -2.230224609375," + "7.657470703125 -1.25244140625))"), + } -def run(verbosity=2): - unittest.TextTestRunner(verbosity=verbosity).run(suite()) + def assertMapWidget(self, form_instance): + """ + Make sure the MapWidget js is passed in the form media and a MapWidget + is actually created + """ + self.assertTrue(form_instance.is_valid()) + rendered = form_instance.as_p() + self.assertIn('new MapWidget(options);', rendered) + self.assertIn('gis/js/OLMapWidget.js', str(form_instance.media)) + + def assertTextarea(self, geom, rendered): + """Makes sure the wkt and a textarea are in the content""" + + self.assertIn('<textarea ', rendered) + self.assertIn('required', rendered) + self.assertIn(geom.wkt, rendered) + + def test_pointfield(self): + class PointForm(forms.Form): + p = forms.PointField() + + geom = self.geometries['point'] + form = PointForm(data={'p': geom}) + self.assertTextarea(geom, form.as_p()) + self.assertMapWidget(form) + self.assertFalse(PointForm().is_valid()) + invalid = PointForm(data={'p': 'some invalid geom'}) + self.assertFalse(invalid.is_valid()) + self.assertTrue('Invalid geometry value' in str(invalid.errors)) + + for invalid in [geom for key, geom in self.geometries.items() if key!='point']: + self.assertFalse(PointForm(data={'p': invalid.wkt}).is_valid()) + + def test_multipointfield(self): + class PointForm(forms.Form): + p = forms.MultiPointField() + + geom = self.geometries['multipoint'] + form = PointForm(data={'p': geom}) + self.assertTextarea(geom, form.as_p()) + self.assertMapWidget(form) + self.assertFalse(PointForm().is_valid()) + + for invalid in [geom for key, geom in self.geometries.items() if key!='multipoint']: + self.assertFalse(PointForm(data={'p': invalid.wkt}).is_valid()) + + def test_linestringfield(self): + class LineStringForm(forms.Form): + l = forms.LineStringField() + + geom = self.geometries['linestring'] + form = LineStringForm(data={'l': geom}) + self.assertTextarea(geom, form.as_p()) + self.assertMapWidget(form) + self.assertFalse(LineStringForm().is_valid()) + + for invalid in [geom for key, geom in self.geometries.items() if key!='linestring']: + self.assertFalse(LineStringForm(data={'p': invalid.wkt}).is_valid()) + + def test_multilinestringfield(self): + class LineStringForm(forms.Form): + l = forms.MultiLineStringField() + + geom = self.geometries['multilinestring'] + form = LineStringForm(data={'l': geom}) + self.assertTextarea(geom, form.as_p()) + self.assertMapWidget(form) + self.assertFalse(LineStringForm().is_valid()) + + for invalid in [geom for key, geom in self.geometries.items() if key!='multilinestring']: + self.assertFalse(LineStringForm(data={'p': invalid.wkt}).is_valid()) + + def test_polygonfield(self): + class PolygonForm(forms.Form): + p = forms.PolygonField() + + geom = self.geometries['polygon'] + form = PolygonForm(data={'p': geom}) + self.assertTextarea(geom, form.as_p()) + self.assertMapWidget(form) + self.assertFalse(PolygonForm().is_valid()) + + for invalid in [geom for key, geom in self.geometries.items() if key!='polygon']: + self.assertFalse(PolygonForm(data={'p': invalid.wkt}).is_valid()) + + def test_multipolygonfield(self): + class PolygonForm(forms.Form): + p = forms.MultiPolygonField() + + geom = self.geometries['multipolygon'] + form = PolygonForm(data={'p': geom}) + self.assertTextarea(geom, form.as_p()) + self.assertMapWidget(form) + self.assertFalse(PolygonForm().is_valid()) + + for invalid in [geom for key, geom in self.geometries.items() if key!='multipolygon']: + self.assertFalse(PolygonForm(data={'p': invalid.wkt}).is_valid()) + + def test_geometrycollectionfield(self): + class GeometryForm(forms.Form): + g = forms.GeometryCollectionField() + + geom = self.geometries['geometrycollection'] + form = GeometryForm(data={'g': geom}) + self.assertTextarea(geom, form.as_p()) + self.assertMapWidget(form) + self.assertFalse(GeometryForm().is_valid()) -if __name__=="__main__": - run() + for invalid in [geom for key, geom in self.geometries.items() if key!='geometrycollection']: + self.assertFalse(GeometryForm(data={'g': invalid.wkt}).is_valid()) diff --git a/django/contrib/gis/tests/test_spatialrefsys.py b/django/contrib/gis/tests/test_spatialrefsys.py index 257e06a3f8..f33883e9f1 100644 --- a/django/contrib/gis/tests/test_spatialrefsys.py +++ b/django/contrib/gis/tests/test_spatialrefsys.py @@ -1,4 +1,3 @@ -from django.db import connection from django.contrib.gis.gdal import HAS_GDAL from django.contrib.gis.tests.utils import (no_mysql, oracle, postgis, spatialite, HAS_SPATIALREFSYS, SpatialRefSys) diff --git a/django/contrib/gis/tests/utils.py b/django/contrib/gis/tests/utils.py index 8355b27fd7..a09a3ab531 100644 --- a/django/contrib/gis/tests/utils.py +++ b/django/contrib/gis/tests/utils.py @@ -35,3 +35,12 @@ elif spatialite: else: HAS_SPATIALREFSYS = False SpatialRefSys = None + + +def has_spatial_db(): + # All databases must have spatial backends to run GeoDjango tests. + spatial_dbs = [name for name, db_dict in settings.DATABASES.items() + if db_dict['ENGINE'].startswith('django.contrib.gis')] + return len(spatial_dbs) == len(settings.DATABASES) + +HAS_SPATIAL_DB = has_spatial_db() diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py index 282dbc7450..cb56b8bce2 100644 --- a/django/contrib/gis/utils/layermapping.py +++ b/django/contrib/gis/utils/layermapping.py @@ -201,7 +201,7 @@ class LayerMapping(object): if not (ltype.name.startswith(gtype.name) or self.make_multi(ltype, model_field)): raise LayerMapError('Invalid mapping geometry; model has %s%s, ' 'layer geometry type is %s.' % - (fld_name, (coord_dim == 3 and '(dim=3)') or '', ltype)) + (fld_name, '(dim=3)' if coord_dim == 3 else '', ltype)) # Setting the `geom_field` attribute w/the name of the model field # that is a Geometry. Also setting the coordinate dimension diff --git a/django/contrib/messages/tests/__init__.py b/django/contrib/messages/tests/__init__.py index e57594dec9..e69de29bb2 100644 --- a/django/contrib/messages/tests/__init__.py +++ b/django/contrib/messages/tests/__init__.py @@ -1,5 +0,0 @@ -from django.contrib.messages.tests.test_cookie import CookieTest -from django.contrib.messages.tests.test_fallback import FallbackTest -from django.contrib.messages.tests.test_middleware import MiddlewareTest -from django.contrib.messages.tests.test_session import SessionTest -from django.contrib.messages.tests.test_mixins import SuccessMessageMixinTests diff --git a/django/contrib/messages/views.py b/django/contrib/messages/views.py index d08804aa1d..3c2ca355ba 100644 --- a/django/contrib/messages/views.py +++ b/django/contrib/messages/views.py @@ -1,8 +1,7 @@ -from django.views.generic.edit import FormMixin from django.contrib import messages -class SuccessMessageMixin(FormMixin): +class SuccessMessageMixin(object): """ Adds a success message on successful form submission. """ diff --git a/django/contrib/sitemaps/tests/__init__.py b/django/contrib/sitemaps/tests/__init__.py index fdbb9696d5..e69de29bb2 100644 --- a/django/contrib/sitemaps/tests/__init__.py +++ b/django/contrib/sitemaps/tests/__init__.py @@ -1,4 +0,0 @@ -from .test_flatpages import FlatpagesSitemapTests -from .test_generic import GenericViewsSitemapTests -from .test_http import HTTPSitemapTests -from .test_https import HTTPSSitemapTests, HTTPSDetectionSitemapTests diff --git a/django/contrib/staticfiles/management/commands/collectstatic.py b/django/contrib/staticfiles/management/commands/collectstatic.py index 22fdac7c76..c620993f33 100644 --- a/django/contrib/staticfiles/management/commands/collectstatic.py +++ b/django/contrib/staticfiles/management/commands/collectstatic.py @@ -174,7 +174,7 @@ Type 'yes' to continue, or 'no' to cancel: """ "%(destination)s%(unmodified)s%(post_processed)s.\n") summary = template % { 'modified_count': modified_count, - 'identifier': 'static file' + (modified_count != 1 and 's' or ''), + 'identifier': 'static file' + ('' if modified_count == 1 else 's'), 'action': self.symlink and 'symlinked' or 'copied', 'destination': (destination_path and " to '%s'" % destination_path or ''), diff --git a/django/core/files/images.py b/django/core/files/images.py index 0d87ae853e..e1d6091658 100644 --- a/django/core/files/images.py +++ b/django/core/files/images.py @@ -1,7 +1,7 @@ """ Utility functions for handling images. -Requires PIL, as you might imagine. +Requires Pillow (or PIL), as you might imagine. """ import zlib @@ -35,11 +35,7 @@ def get_image_dimensions(file_or_path, close=False): 'close' to True to close the file at the end if it is initially in an open state. """ - # Try to import PIL in either of the two ways it can end up installed. - try: - from PIL import ImageFile as PILImageFile - except ImportError: - import ImageFile as PILImageFile + from django.utils.image import ImageFile as PILImageFile p = PILImageFile.Parser() if hasattr(file_or_path, 'read'): diff --git a/django/core/files/storage.py b/django/core/files/storage.py index 89caf3675d..18d15e1ab6 100644 --- a/django/core/files/storage.py +++ b/django/core/files/storage.py @@ -200,9 +200,9 @@ class FileSystemStorage(Storage): getattr(os, 'O_BINARY', 0)) # The current umask value is masked out by os.open! fd = os.open(full_path, flags, 0o666) + _file = None try: locks.lock(fd, locks.LOCK_EX) - _file = None for chunk in content.chunks(): if _file is None: mode = 'wb' if isinstance(chunk, bytes) else 'wt' diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index 7638937c94..77d5e1b264 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -63,7 +63,7 @@ def find_management_module(app_name): while parts: part = parts.pop() - f, path, descr = imp.find_module(part, path and [path] or None) + f, path, descr = imp.find_module(part, [path] if path else None) if f: f.close() return path diff --git a/django/core/management/base.py b/django/core/management/base.py index e6a968bbad..ba6ad8f4c0 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -60,7 +60,7 @@ class OutputWrapper(object): return getattr(self._out, name) def write(self, msg, style_func=None, ending=None): - ending = ending is None and self.ending or ending + ending = self.ending if ending is None else ending if ending and not msg.endswith(ending): msg += ending style_func = [f for f in (style_func, self.style_func, lambda x:x) @@ -311,7 +311,7 @@ class BaseCommand(object): error_text = s.read() raise CommandError("One or more models did not validate:\n%s" % error_text) if display_num_errors: - self.stdout.write("%s error%s found" % (num_errors, num_errors != 1 and 's' or '')) + self.stdout.write("%s error%s found" % (num_errors, '' if num_errors == 1 else 's')) def handle(self, *args, **options): """ diff --git a/django/core/management/commands/createcachetable.py b/django/core/management/commands/createcachetable.py index 94b6b09400..4d1bc0403e 100644 --- a/django/core/management/commands/createcachetable.py +++ b/django/core/management/commands/createcachetable.py @@ -44,7 +44,7 @@ class Command(LabelCommand): elif f.unique: field_output.append("UNIQUE") if f.db_index: - unique = f.unique and "UNIQUE " or "" + unique = "UNIQUE " if f.unique else "" index_output.append("CREATE %sINDEX %s ON %s (%s);" % \ (unique, qn('%s_%s' % (tablename, f.name)), qn(tablename), qn(f.name))) diff --git a/django/core/management/commands/runserver.py b/django/core/management/commands/runserver.py index 0ca15fdb6f..4fe03216d5 100644 --- a/django/core/management/commands/runserver.py +++ b/django/core/management/commands/runserver.py @@ -65,7 +65,7 @@ class Command(BaseCommand): elif self.use_ipv6 and not _fqdn: raise CommandError('"%s" is not a valid IPv6 address.' % self.addr) if not self.addr: - self.addr = self.use_ipv6 and '::1' or '127.0.0.1' + self.addr = '::1' if self.use_ipv6 else '127.0.0.1' self._raw_ipv6 = bool(self.use_ipv6) self.run(*args, **options) @@ -86,7 +86,7 @@ class Command(BaseCommand): threading = options.get('use_threading') shutdown_message = options.get('shutdown_message', '') - quit_command = (sys.platform == 'win32') and 'CTRL-BREAK' or 'CONTROL-C' + quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C' self.stdout.write("Validating models...\n\n") self.validate(display_num_errors=True) diff --git a/django/core/management/validation.py b/django/core/management/validation.py index 94d604346b..0f0eade569 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -105,14 +105,10 @@ def get_validation_errors(outfile, app=None): if isinstance(f, models.FileField) and not f.upload_to: e.add(opts, '"%s": FileFields require an "upload_to" attribute.' % f.name) if isinstance(f, models.ImageField): - # Try to import PIL in either of the two ways it can end up installed. try: - from PIL import Image + from django.utils.image import Image except ImportError: - try: - import Image - except ImportError: - e.add(opts, '"%s": To use ImageFields, you need to install the Python Imaging Library. Get it at http://www.pythonware.com/products/pil/ .' % f.name) + e.add(opts, '"%s": To use ImageFields, you need to install Pillow. Get it at https://pypi.python.org/pypi/Pillow.' % f.name) if isinstance(f, models.BooleanField) and getattr(f, 'null', False): e.add(opts, '"%s": BooleanFields do not accept null values. Use a NullBooleanField instead.' % f.name) if isinstance(f, models.FilePathField) and not (f.allow_files or f.allow_folders): diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py index 294934a04a..1e78026c40 100644 --- a/django/core/serializers/base.py +++ b/django/core/serializers/base.py @@ -161,9 +161,7 @@ class DeserializedObject(object): def save(self, save_m2m=True, using=None): # Call save on the Model baseclass directly. This bypasses any # model-defined save. The save is also forced to be raw. - # This ensures that the data that is deserialized is literally - # what came from the file, not post-processed by pre_save/save - # methods. + # raw=True is passed to any pre/post_save signals. models.Model.save_base(self.object, using=using, raw=True) if self.m2m_data and save_m2m: for accessor_name, object_list in self.m2m_data.items(): diff --git a/django/db/models/base.py b/django/db/models/base.py index d6870b561a..556249fa54 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -630,7 +630,7 @@ class Model(six.with_metaclass(ModelBase)): # If possible, try an UPDATE. If that doesn't update anything, do an INSERT. if pk_set and not force_insert: base_qs = cls._base_manager.using(using) - values = [(f, None, (raw and getattr(self, f.attname) or f.pre_save(self, False))) + values = [(f, None, (getattr(self, f.attname) if raw else f.pre_save(self, False))) for f in non_pks] if not values: # We can end up here when saving a model in inheritance chain where @@ -697,8 +697,8 @@ class Model(six.with_metaclass(ModelBase)): def _get_next_or_previous_by_FIELD(self, field, is_next, **kwargs): if not self.pk: raise ValueError("get_next/get_previous cannot be used on unsaved objects.") - op = is_next and 'gt' or 'lt' - order = not is_next and '-' or '' + op = 'gt' if is_next else 'lt' + order = '' if is_next else '-' param = force_text(getattr(self, field.attname)) q = Q(**{'%s__%s' % (field.name, op): param}) q = q | Q(**{field.name: param, 'pk__%s' % op: self.pk}) @@ -711,8 +711,8 @@ class Model(six.with_metaclass(ModelBase)): def _get_next_or_previous_in_order(self, is_next): cachename = "__%s_order_cache" % is_next if not hasattr(self, cachename): - op = is_next and 'gt' or 'lt' - order = not is_next and '-_order' or '_order' + op = 'gt' if is_next else 'lt' + order = '_order' if is_next else '-_order' order_field = self._meta.order_with_respect_to obj = self._default_manager.filter(**{ order_field.name: getattr(self, order_field.attname) diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 13174796c1..b1c1601300 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -468,7 +468,7 @@ class Field(object): def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH): """Returns choices with a default blank choices included, for use as SelectField choices for this field.""" - first_choice = include_blank and blank_choice or [] + first_choice = blank_choice if include_blank else [] if self.choices: return first_choice + list(self.choices) rel_model = self.rel.to @@ -491,7 +491,7 @@ class Field(object): """ Returns flattened choices with a default blank choice included. """ - first_choice = include_blank and blank_choice or [] + first_choice = blank_choice if include_blank else [] return first_choice + list(self.flatchoices) def _get_val_from_obj(self, obj): diff --git a/django/db/models/options.py b/django/db/models/options.py index 7ca2f1c321..46c18a64c6 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -349,7 +349,7 @@ class Options(object): """ Returns the requested field by name. Raises FieldDoesNotExist on error. """ - to_search = many_to_many and (self.fields + self.many_to_many) or self.fields + to_search = (self.fields + self.many_to_many) if many_to_many else self.fields for f in to_search: if f.name == name: return f diff --git a/django/db/models/query.py b/django/db/models/query.py index 337049e2ff..d3763d3934 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -869,7 +869,7 @@ class QuerySet(object): """ if self.query.extra_order_by or self.query.order_by: return True - elif self.query.default_ordering and self.query.model._meta.ordering: + elif self.query.default_ordering and self.query.get_meta().ordering: return True else: return False diff --git a/django/db/models/related.py b/django/db/models/related.py index 6c93076c48..4b00dd343b 100644 --- a/django/db/models/related.py +++ b/django/db/models/related.py @@ -27,7 +27,7 @@ class RelatedObject(object): Analogue of django.db.models.fields.Field.get_choices, provided initially for utilisation by RelatedFieldListFilter. """ - first_choice = include_blank and blank_choice or [] + first_choice = blank_choice if include_blank else [] queryset = self.model._default_manager.all() if limit_to_currently_related: queryset = queryset.complex_filter( diff --git a/django/db/models/sql/aggregates.py b/django/db/models/sql/aggregates.py index 1b65847b7f..23b79923d1 100644 --- a/django/db/models/sql/aggregates.py +++ b/django/db/models/sql/aggregates.py @@ -112,7 +112,7 @@ class StdDev(Aggregate): def __init__(self, col, sample=False, **extra): super(StdDev, self).__init__(col, **extra) - self.sql_function = sample and 'STDDEV_SAMP' or 'STDDEV_POP' + self.sql_function = 'STDDEV_SAMP' if sample else 'STDDEV_POP' class Sum(Aggregate): sql_function = 'SUM' @@ -122,4 +122,4 @@ class Variance(Aggregate): def __init__(self, col, sample=False, **extra): super(Variance, self).__init__(col, **extra) - self.sql_function = sample and 'VAR_SAMP' or 'VAR_POP' + self.sql_function = 'VAR_SAMP' if sample else 'VAR_POP' diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 018fc098ea..bbe310c8c3 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -32,7 +32,7 @@ class SQLCompiler(object): # cleaned. We are not using a clone() of the query here. """ if not self.query.tables: - self.query.join((None, self.query.model._meta.db_table, None)) + self.query.join((None, self.query.get_meta().db_table, None)) if (not self.query.select and self.query.default_cols and not self.query.included_inherited_models): self.query.setup_inherited_models() @@ -260,7 +260,7 @@ class SQLCompiler(object): """ result = [] if opts is None: - opts = self.query.model._meta + opts = self.query.get_meta() qn = self.quote_name_unless_alias qn2 = self.connection.ops.quote_name aliases = set() @@ -309,7 +309,7 @@ class SQLCompiler(object): qn = self.quote_name_unless_alias qn2 = self.connection.ops.quote_name result = [] - opts = self.query.model._meta + opts = self.query.get_meta() for name in self.query.distinct_fields: parts = name.split(LOOKUP_SEP) @@ -338,7 +338,7 @@ class SQLCompiler(object): ordering = self.query.order_by else: ordering = (self.query.order_by - or self.query.model._meta.ordering + or self.query.get_meta().ordering or []) qn = self.quote_name_unless_alias qn2 = self.connection.ops.quote_name @@ -388,7 +388,7 @@ class SQLCompiler(object): # 'col' is of the form 'field' or 'field1__field2' or # '-field1__field2__field', etc. for table, cols, order in self.find_ordering_name(field, - self.query.model._meta, default_order=asc): + self.query.get_meta(), default_order=asc): for col in cols: if (table, col) not in processed_pairs: elt = '%s.%s' % (qn(table), qn2(col)) @@ -512,7 +512,7 @@ class SQLCompiler(object): # Extra tables can end up in self.tables, but not in the # alias_map if they aren't in a join. That's OK. We skip them. continue - alias_str = (alias != name and ' %s' % alias or '') + alias_str = '' if alias == name else (' %s' % alias) if join_type and not first: extra_cond = join_field.get_extra_restriction( self.query.where_class, alias, lhs) @@ -532,7 +532,7 @@ class SQLCompiler(object): (qn(lhs), qn2(lhs_col), qn(alias), qn2(rhs_col))) result.append('%s)' % extra_sql) else: - connector = not first and ', ' or '' + connector = '' if first else ', ' result.append('%s%s%s' % (connector, qn(name), alias_str)) first = False for t in self.query.extra_tables: @@ -541,7 +541,7 @@ class SQLCompiler(object): # calls increments the refcount, so an alias refcount of one means # this is the only reference. if alias not in self.query.alias_map or self.query.alias_refcount[alias] == 1: - connector = not first and ', ' or '' + connector = '' if first else ', ' result.append('%s%s' % (connector, qn(alias))) first = False return result, from_params @@ -556,10 +556,10 @@ class SQLCompiler(object): select_cols = self.query.select + self.query.related_select_cols # Just the column, not the fields. select_cols = [s[0] for s in select_cols] - if (len(self.query.model._meta.concrete_fields) == len(self.query.select) + if (len(self.query.get_meta().concrete_fields) == len(self.query.select) and self.connection.features.allows_group_by_pk): self.query.group_by = [ - (self.query.model._meta.db_table, self.query.model._meta.pk.column) + (self.query.get_meta().db_table, self.query.get_meta().pk.column) ] select_cols = [] seen = set() @@ -716,14 +716,14 @@ class SQLCompiler(object): if self.query.select: fields = [f.field for f in self.query.select] else: - fields = self.query.model._meta.concrete_fields + fields = self.query.get_meta().concrete_fields fields = fields + [f.field for f in self.query.related_select_cols] # If the field was deferred, exclude it from being passed # into `resolve_columns` because it wasn't selected. only_load = self.deferred_to_columns() if only_load: - db_table = self.query.model._meta.db_table + db_table = self.query.get_meta().db_table fields = [f for f in fields if db_table in only_load and f.column in only_load[db_table]] row = self.resolve_columns(row, fields) @@ -825,7 +825,7 @@ class SQLInsertCompiler(SQLCompiler): # We don't need quote_name_unless_alias() here, since these are all # going to be column names (so we can avoid the extra overhead). qn = self.connection.ops.quote_name - opts = self.query.model._meta + opts = self.query.get_meta() result = ['INSERT INTO %s' % qn(opts.db_table)] has_fields = bool(self.query.fields) @@ -887,7 +887,7 @@ class SQLInsertCompiler(SQLCompiler): if self.connection.features.can_return_id_from_insert: return self.connection.ops.fetch_returned_insert_id(cursor) return self.connection.ops.last_insert_id(cursor, - self.query.model._meta.db_table, self.query.model._meta.pk.column) + self.query.get_meta().db_table, self.query.get_meta().pk.column) class SQLDeleteCompiler(SQLCompiler): @@ -959,7 +959,7 @@ class SQLUpdateCompiler(SQLCompiler): related queries are not available. """ cursor = super(SQLUpdateCompiler, self).execute_sql(result_type) - rows = cursor and cursor.rowcount or 0 + rows = cursor.rowcount if cursor else 0 is_empty = cursor is None del cursor for query in self.query.get_related_updates(): @@ -992,7 +992,7 @@ class SQLUpdateCompiler(SQLCompiler): query.bump_prefix() query.extra = {} query.select = [] - query.add_fields([query.model._meta.pk.name]) + query.add_fields([query.get_meta().pk.name]) # Recheck the count - it is possible that fiddling with the select # fields above removes tables from the query. Refs #18304. count = query.count_active_tables() diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index acb9f248d6..0a4152587d 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -532,7 +532,7 @@ class Query(object): # Ordering uses the 'rhs' ordering, unless it has none, in which case # the current ordering is used. - self.order_by = rhs.order_by and rhs.order_by[:] or self.order_by + self.order_by = rhs.order_by[:] if rhs.order_by else self.order_by self.extra_order_by = rhs.extra_order_by or self.extra_order_by def deferred_to_data(self, target, callback): @@ -552,7 +552,7 @@ class Query(object): field_names, defer = self.deferred_loading if not field_names: return - orig_opts = self.model._meta + orig_opts = self.get_meta() seen = {} must_include = {orig_opts.concrete_model: set([orig_opts.pk])} for field_name in field_names: @@ -818,7 +818,7 @@ class Query(object): alias = self.tables[0] self.ref_alias(alias) else: - alias = self.join((None, self.model._meta.db_table, None)) + alias = self.join((None, self.get_meta().db_table, None)) return alias def count_active_tables(self): @@ -906,7 +906,7 @@ class Query(object): whereas column determination is a later part, and side-effect, of as_sql()). """ - opts = self.model._meta + opts = self.get_meta() root_alias = self.tables[0] seen = {None: root_alias} @@ -1624,7 +1624,7 @@ class Query(object): "Cannot add count col with multiple cols in 'select': %r" % self.select count = self.aggregates_module.Count(self.select[0].col) else: - opts = self.model._meta + opts = self.get_meta() if not self.select: count = self.aggregates_module.Count( (self.join((None, opts.db_table, None)), opts.pk.column), @@ -1732,7 +1732,7 @@ class Query(object): field_names = set(field_names) if 'pk' in field_names: field_names.remove('pk') - field_names.add(self.model._meta.pk.name) + field_names.add(self.get_meta().pk.name) if defer: # Remove any existing deferred names from the current set before diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index 78727e394a..20770162c9 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -41,12 +41,12 @@ class DeleteQuery(Query): lot of values in pk_list. """ if not field: - field = self.model._meta.pk + field = self.get_meta().pk for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): where = self.where_class() where.add((Constraint(None, field.column, field), 'in', pk_list[offset:offset + GET_ITERATOR_CHUNK_SIZE]), AND) - self.do_query(self.model._meta.db_table, where, using=using) + self.do_query(self.get_meta().db_table, where, using=using) def delete_qs(self, query, using): """ @@ -112,7 +112,7 @@ class UpdateQuery(Query): related_updates=self.related_updates.copy(), **kwargs) def update_batch(self, pk_list, values, using): - pk_field = self.model._meta.pk + pk_field = self.get_meta().pk self.add_update_values(values) for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): self.where = self.where_class() @@ -129,7 +129,7 @@ class UpdateQuery(Query): """ values_seq = [] for name, val in six.iteritems(values): - field, model, direct, m2m = self.model._meta.get_field_by_name(name) + field, model, direct, m2m = self.get_meta().get_field_by_name(name) if not direct or m2m: raise FieldError('Cannot update model field %r (only non-relations and foreign keys permitted).' % field) if model: @@ -236,7 +236,7 @@ class DateQuery(Query): ) except FieldError: raise FieldDoesNotExist("%s has no field named '%s'" % ( - self.model._meta.object_name, field_name + self.get_meta().object_name, field_name )) field = result[0] self._check_field(field) # overridden in DateTimeQuery @@ -245,7 +245,7 @@ class DateQuery(Query): self.clear_select_clause() self.select = [SelectInfo(select, None)] self.distinct = True - self.order_by = order == 'ASC' and [1] or [-1] + self.order_by = [1] if order == 'ASC' else [-1] if field.null: self.add_filter(("%s__isnull" % field_name, False)) diff --git a/django/forms/fields.py b/django/forms/fields.py index 146a10d635..4ce57d34a3 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -602,13 +602,9 @@ class ImageField(FileField): if f is None: return None - # Try to import PIL in either of the two ways it can end up installed. - try: - from PIL import Image - except ImportError: - import Image + from django.utils.image import Image - # We need to get a file object for PIL. We might have a path or we might + # We need to get a file object for Pillow. We might have a path or we might # have to read the data into memory. if hasattr(data, 'temporary_file_path'): file = data.temporary_file_path() @@ -623,12 +619,8 @@ class ImageField(FileField): # image in memory, which is a DoS vector. See #3848 and #18520. # verify() must be called immediately after the constructor. Image.open(file).verify() - except ImportError: - # Under PyPy, it is possible to import PIL. However, the underlying - # _imaging C module isn't available, so an ImportError will be - # raised. Catch and re-raise. - raise - except Exception: # Python Imaging Library doesn't recognize it as an image + except Exception: + # Pillow (or PIL) doesn't recognize it as an image. six.reraise(ValidationError, ValidationError(self.error_messages['invalid_image']), sys.exc_info()[2]) if hasattr(f, 'seek') and callable(f.seek): f.seek(0) diff --git a/django/forms/forms.py b/django/forms/forms.py index f3a0fe6b38..b231de421a 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -523,7 +523,7 @@ class BoundField(object): widget = self.field.widget id_ = widget.attrs.get('id') or self.auto_id if id_: - attrs = attrs and flatatt(attrs) or '' + attrs = flatatt(attrs) if attrs else '' contents = format_html('<label for="{0}"{1}>{2}</label>', widget.id_for_label(id_), attrs, contents ) diff --git a/django/forms/formsets.py b/django/forms/formsets.py index 2bd11d9f53..d421770093 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -119,7 +119,7 @@ class BaseFormSet(object): return self.management_form.cleaned_data[INITIAL_FORM_COUNT] else: # Use the length of the initial data if it's there, 0 otherwise. - initial_forms = self.initial and len(self.initial) or 0 + initial_forms = len(self.initial) if self.initial else 0 return initial_forms def _construct_forms(self): diff --git a/django/forms/widgets.py b/django/forms/widgets.py index e72ab014b8..aca4a457af 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -775,7 +775,7 @@ class MultiWidget(Widget): You'll probably want to use this class with MultiValueField. """ def __init__(self, widgets, attrs=None): - self.widgets = [isinstance(w, type) and w() or w for w in widgets] + self.widgets = [w() if isinstance(w, type) else w for w in widgets] super(MultiWidget, self).__init__(attrs) def render(self, name, value, attrs=None): diff --git a/django/http/multipartparser.py b/django/http/multipartparser.py index 0c07335f27..0e999f2ded 100644 --- a/django/http/multipartparser.py +++ b/django/http/multipartparser.py @@ -292,7 +292,7 @@ class LazyStream(six.Iterator): def read(self, size=None): def parts(): - remaining = (size is not None and [size] or [self._remaining])[0] + remaining = self._remaining if size is None else size # do the whole thing in one shot if no limit was provided. if remaining is None: yield b''.join(self) diff --git a/django/template/base.py b/django/template/base.py index c5bddaf63e..dc6b0c366c 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -641,7 +641,7 @@ class FilterExpression(object): (name, len(nondefs), plen)) # Defaults can be overridden. - defaults = defaults and list(defaults) or [] + defaults = list(defaults) if defaults else [] try: for parg in provided: defaults.pop(0) diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index b272322e1b..04e7a37d8e 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -127,7 +127,7 @@ class ForNode(Node): self.nodelist_empty = nodelist_empty def __repr__(self): - reversed_text = self.is_reversed and ' reversed' or '' + reversed_text = ' reversed' if self.is_reversed else '' return "<For Node: for %s in %s, tail_len: %d%s>" % \ (', '.join(self.loopvars), self.sequence, len(self.nodelist_loop), reversed_text) @@ -788,7 +788,7 @@ def do_for(parser, token): " words: %s" % token.contents) is_reversed = bits[-1] == 'reversed' - in_index = is_reversed and -3 or -2 + in_index = -3 if is_reversed else -2 if bits[in_index] != 'in': raise TemplateSyntaxError("'for' statements should use the format" " 'for x in y': %s" % token.contents) diff --git a/django/test/_doctest.py b/django/test/_doctest.py index 9b8ddc9696..5381cff2f0 100644 --- a/django/test/_doctest.py +++ b/django/test/_doctest.py @@ -49,6 +49,13 @@ files containing doctests. There are also many ways to override parts of doctest's default behaviors. See the Library Reference Manual for details. """ +import warnings + +warnings.warn( + "The django.test._doctest module is deprecated; " + "use the doctest module from the Python standard library instead.", + PendingDeprecationWarning) + __docformat__ = 'reStructuredText en' diff --git a/django/test/runner.py b/django/test/runner.py new file mode 100644 index 0000000000..e753e365fa --- /dev/null +++ b/django/test/runner.py @@ -0,0 +1,289 @@ +import os +from optparse import make_option + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.test import TestCase +from django.test.utils import setup_test_environment, teardown_test_environment +from django.utils import unittest +from django.utils.unittest import TestSuite, defaultTestLoader + + +class DiscoverRunner(object): + """ + A Django test runner that uses unittest2 test discovery. + """ + + test_loader = defaultTestLoader + reorder_by = (TestCase, ) + option_list = ( + make_option('-t', '--top-level-directory', + action='store', dest='top_level', default=None, + help='Top level of project for unittest discovery.'), + make_option('-p', '--pattern', action='store', dest='pattern', + default="test*.py", + help='The test matching pattern. Defaults to test*.py.'), + ) + + def __init__(self, pattern=None, top_level=None, + verbosity=1, interactive=True, failfast=False, + **kwargs): + + self.pattern = pattern + self.top_level = top_level + + self.verbosity = verbosity + self.interactive = interactive + self.failfast = failfast + + def setup_test_environment(self, **kwargs): + setup_test_environment() + settings.DEBUG = False + unittest.installHandler() + + def build_suite(self, test_labels=None, extra_tests=None, **kwargs): + suite = TestSuite() + test_labels = test_labels or ['.'] + extra_tests = extra_tests or [] + + discover_kwargs = {} + if self.pattern is not None: + discover_kwargs['pattern'] = self.pattern + if self.top_level is not None: + discover_kwargs['top_level_dir'] = self.top_level + + for label in test_labels: + kwargs = discover_kwargs.copy() + tests = None + + label_as_path = os.path.abspath(label) + + # if a module, or "module.ClassName[.method_name]", just run those + if not os.path.exists(label_as_path): + tests = self.test_loader.loadTestsFromName(label) + elif os.path.isdir(label_as_path) and not self.top_level: + # Try to be a bit smarter than unittest about finding the + # default top-level for a given directory path, to avoid + # breaking relative imports. (Unittest's default is to set + # top-level equal to the path, which means relative imports + # will result in "Attempted relative import in non-package."). + + # We'd be happy to skip this and require dotted module paths + # (which don't cause this problem) instead of file paths (which + # do), but in the case of a directory in the cwd, which would + # be equally valid if considered as a top-level module or as a + # directory path, unittest unfortunately prefers the latter. + + top_level = label_as_path + while True: + init_py = os.path.join(top_level, '__init__.py') + if os.path.exists(init_py): + try_next = os.path.dirname(top_level) + if try_next == top_level: + # __init__.py all the way down? give up. + break + top_level = try_next + continue + break + kwargs['top_level_dir'] = top_level + + + if not (tests and tests.countTestCases()): + # if no tests found, it's probably a package; try discovery + tests = self.test_loader.discover(start_dir=label, **kwargs) + + # make unittest forget the top-level dir it calculated from this + # run, to support running tests from two different top-levels. + self.test_loader._top_level_dir = None + + suite.addTests(tests) + + for test in extra_tests: + suite.addTest(test) + + return reorder_suite(suite, self.reorder_by) + + def setup_databases(self, **kwargs): + return setup_databases(self.verbosity, self.interactive, **kwargs) + + def run_suite(self, suite, **kwargs): + return unittest.TextTestRunner( + verbosity=self.verbosity, + failfast=self.failfast, + ).run(suite) + + def teardown_databases(self, old_config, **kwargs): + """ + Destroys all the non-mirror databases. + """ + old_names, mirrors = old_config + for connection, old_name, destroy in old_names: + if destroy: + connection.creation.destroy_test_db(old_name, self.verbosity) + + def teardown_test_environment(self, **kwargs): + unittest.removeHandler() + teardown_test_environment() + + def suite_result(self, suite, result, **kwargs): + return len(result.failures) + len(result.errors) + + def run_tests(self, test_labels, extra_tests=None, **kwargs): + """ + Run the unit tests for all the test labels in the provided list. + + Test labels should be dotted Python paths to test modules, test + classes, or test methods. + + A list of 'extra' tests may also be provided; these tests + will be added to the test suite. + + Returns the number of tests that failed. + """ + self.setup_test_environment() + suite = self.build_suite(test_labels, extra_tests) + old_config = self.setup_databases() + result = self.run_suite(suite) + self.teardown_databases(old_config) + self.teardown_test_environment() + return self.suite_result(suite, result) + + +def dependency_ordered(test_databases, dependencies): + """ + Reorder test_databases into an order that honors the dependencies + described in TEST_DEPENDENCIES. + """ + ordered_test_databases = [] + resolved_databases = set() + + # Maps db signature to dependencies of all it's aliases + dependencies_map = {} + + # sanity check - no DB can depend on it's own alias + for sig, (_, aliases) in test_databases: + all_deps = set() + for alias in aliases: + all_deps.update(dependencies.get(alias, [])) + if not all_deps.isdisjoint(aliases): + raise ImproperlyConfigured( + "Circular dependency: databases %r depend on each other, " + "but are aliases." % aliases) + dependencies_map[sig] = all_deps + + while test_databases: + changed = False + deferred = [] + + # Try to find a DB that has all it's dependencies met + for signature, (db_name, aliases) in test_databases: + if dependencies_map[signature].issubset(resolved_databases): + resolved_databases.update(aliases) + ordered_test_databases.append((signature, (db_name, aliases))) + changed = True + else: + deferred.append((signature, (db_name, aliases))) + + if not changed: + raise ImproperlyConfigured( + "Circular dependency in TEST_DEPENDENCIES") + test_databases = deferred + return ordered_test_databases + + +def reorder_suite(suite, classes): + """ + Reorders a test suite by test type. + + `classes` is a sequence of types + + All tests of type classes[0] are placed first, then tests of type + classes[1], etc. Tests with no match in classes are placed last. + """ + class_count = len(classes) + bins = [unittest.TestSuite() for i in range(class_count+1)] + partition_suite(suite, classes, bins) + for i in range(class_count): + bins[0].addTests(bins[i+1]) + return bins[0] + + +def partition_suite(suite, classes, bins): + """ + Partitions a test suite by test type. + + classes is a sequence of types + bins is a sequence of TestSuites, one more than classes + + Tests of type classes[i] are added to bins[i], + tests with no match found in classes are place in bins[-1] + """ + for test in suite: + if isinstance(test, unittest.TestSuite): + partition_suite(test, classes, bins) + else: + for i in range(len(classes)): + if isinstance(test, classes[i]): + bins[i].addTest(test) + break + else: + bins[-1].addTest(test) + + +def setup_databases(verbosity, interactive, **kwargs): + from django.db import connections, DEFAULT_DB_ALIAS + + # First pass -- work out which databases actually need to be created, + # and which ones are test mirrors or duplicate entries in DATABASES + mirrored_aliases = {} + test_databases = {} + dependencies = {} + for alias in connections: + connection = connections[alias] + if connection.settings_dict['TEST_MIRROR']: + # If the database is marked as a test mirror, save + # the alias. + mirrored_aliases[alias] = ( + connection.settings_dict['TEST_MIRROR']) + else: + # Store a tuple with DB parameters that uniquely identify it. + # If we have two aliases with the same values for that tuple, + # we only need to create the test database once. + item = test_databases.setdefault( + connection.creation.test_db_signature(), + (connection.settings_dict['NAME'], set()) + ) + item[1].add(alias) + + if 'TEST_DEPENDENCIES' in connection.settings_dict: + dependencies[alias] = ( + connection.settings_dict['TEST_DEPENDENCIES']) + else: + if alias != DEFAULT_DB_ALIAS: + dependencies[alias] = connection.settings_dict.get( + 'TEST_DEPENDENCIES', [DEFAULT_DB_ALIAS]) + + # Second pass -- actually create the databases. + old_names = [] + mirrors = [] + + for signature, (db_name, aliases) in dependency_ordered( + test_databases.items(), dependencies): + test_db_name = None + # Actually create the database for the first connection + + for alias in aliases: + connection = connections[alias] + old_names.append((connection, db_name, True)) + if test_db_name is None: + test_db_name = connection.creation.create_test_db( + verbosity, autoclobber=not interactive) + else: + connection.settings_dict['NAME'] = test_db_name + + for alias, mirror_alias in mirrored_aliases.items(): + mirrors.append((alias, connections[alias].settings_dict['NAME'])) + connections[alias].settings_dict['NAME'] = ( + connections[mirror_alias].settings_dict['NAME']) + + return old_names, mirrors diff --git a/django/test/simple.py b/django/test/simple.py index 6804b4958c..5117c6452f 100644 --- a/django/test/simple.py +++ b/django/test/simple.py @@ -1,10 +1,15 @@ +""" +This module is pending deprecation as of Django 1.6 and will be removed in +version 1.8. + +""" + import unittest as real_unittest +import warnings -from django.conf import settings -from django.core.exceptions import ImproperlyConfigured from django.db.models import get_app, get_apps from django.test import _doctest as doctest -from django.test.utils import setup_test_environment, teardown_test_environment +from django.test import runner from django.test.testcases import OutputChecker, DocTestRunner from django.utils import unittest from django.utils.importlib import import_module @@ -12,6 +17,11 @@ from django.utils.module_loading import module_has_submodule __all__ = ('DjangoTestSuiteRunner',) +warnings.warn( + "The django.test.simple module and DjangoTestSuiteRunner are deprecated; " + "use django.test.runner.DiscoverRunner instead.", + PendingDeprecationWarning) + # The module name for tests outside models.py TEST_MODULE = 'tests' @@ -154,97 +164,7 @@ def build_test(label): return unittest.TestSuite(tests) -def partition_suite(suite, classes, bins): - """ - Partitions a test suite by test type. - - classes is a sequence of types - bins is a sequence of TestSuites, one more than classes - - Tests of type classes[i] are added to bins[i], - tests with no match found in classes are place in bins[-1] - """ - for test in suite: - if isinstance(test, unittest.TestSuite): - partition_suite(test, classes, bins) - else: - for i in range(len(classes)): - if isinstance(test, classes[i]): - bins[i].addTest(test) - break - else: - bins[-1].addTest(test) - - -def reorder_suite(suite, classes): - """ - Reorders a test suite by test type. - - `classes` is a sequence of types - - All tests of type classes[0] are placed first, then tests of type - classes[1], etc. Tests with no match in classes are placed last. - """ - class_count = len(classes) - bins = [unittest.TestSuite() for i in range(class_count+1)] - partition_suite(suite, classes, bins) - for i in range(class_count): - bins[0].addTests(bins[i+1]) - return bins[0] - - -def dependency_ordered(test_databases, dependencies): - """ - Reorder test_databases into an order that honors the dependencies - described in TEST_DEPENDENCIES. - """ - ordered_test_databases = [] - resolved_databases = set() - - # Maps db signature to dependencies of all it's aliases - dependencies_map = {} - - # sanity check - no DB can depend on it's own alias - for sig, (_, aliases) in test_databases: - all_deps = set() - for alias in aliases: - all_deps.update(dependencies.get(alias, [])) - if not all_deps.isdisjoint(aliases): - raise ImproperlyConfigured( - "Circular dependency: databases %r depend on each other, " - "but are aliases." % aliases) - dependencies_map[sig] = all_deps - - while test_databases: - changed = False - deferred = [] - - # Try to find a DB that has all it's dependencies met - for signature, (db_name, aliases) in test_databases: - if dependencies_map[signature].issubset(resolved_databases): - resolved_databases.update(aliases) - ordered_test_databases.append((signature, (db_name, aliases))) - changed = True - else: - deferred.append((signature, (db_name, aliases))) - - if not changed: - raise ImproperlyConfigured( - "Circular dependency in TEST_DEPENDENCIES") - test_databases = deferred - return ordered_test_databases - - -class DjangoTestSuiteRunner(object): - def __init__(self, verbosity=1, interactive=True, failfast=True, **kwargs): - self.verbosity = verbosity - self.interactive = interactive - self.failfast = failfast - - def setup_test_environment(self, **kwargs): - setup_test_environment() - settings.DEBUG = False - unittest.installHandler() +class DjangoTestSuiteRunner(runner.DiscoverRunner): def build_suite(self, test_labels, extra_tests=None, **kwargs): suite = unittest.TestSuite() @@ -264,109 +184,4 @@ class DjangoTestSuiteRunner(object): for test in extra_tests: suite.addTest(test) - return reorder_suite(suite, (unittest.TestCase,)) - - def setup_databases(self, **kwargs): - from django.db import connections, DEFAULT_DB_ALIAS - - # First pass -- work out which databases actually need to be created, - # and which ones are test mirrors or duplicate entries in DATABASES - mirrored_aliases = {} - test_databases = {} - dependencies = {} - for alias in connections: - connection = connections[alias] - if connection.settings_dict['TEST_MIRROR']: - # If the database is marked as a test mirror, save - # the alias. - mirrored_aliases[alias] = ( - connection.settings_dict['TEST_MIRROR']) - else: - # Store a tuple with DB parameters that uniquely identify it. - # If we have two aliases with the same values for that tuple, - # we only need to create the test database once. - item = test_databases.setdefault( - connection.creation.test_db_signature(), - (connection.settings_dict['NAME'], set()) - ) - item[1].add(alias) - - if 'TEST_DEPENDENCIES' in connection.settings_dict: - dependencies[alias] = ( - connection.settings_dict['TEST_DEPENDENCIES']) - else: - if alias != DEFAULT_DB_ALIAS: - dependencies[alias] = connection.settings_dict.get( - 'TEST_DEPENDENCIES', [DEFAULT_DB_ALIAS]) - - # Second pass -- actually create the databases. - old_names = [] - mirrors = [] - - for signature, (db_name, aliases) in dependency_ordered( - test_databases.items(), dependencies): - test_db_name = None - # Actually create the database for the first connection - - for alias in aliases: - connection = connections[alias] - old_names.append((connection, db_name, True)) - if test_db_name is None: - test_db_name = connection.creation.create_test_db( - self.verbosity, autoclobber=not self.interactive) - else: - connection.settings_dict['NAME'] = test_db_name - - for alias, mirror_alias in mirrored_aliases.items(): - mirrors.append((alias, connections[alias].settings_dict['NAME'])) - connections[alias].settings_dict['NAME'] = ( - connections[mirror_alias].settings_dict['NAME']) - - return old_names, mirrors - - def run_suite(self, suite, **kwargs): - return unittest.TextTestRunner( - verbosity=self.verbosity, failfast=self.failfast).run(suite) - - def teardown_databases(self, old_config, **kwargs): - """ - Destroys all the non-mirror databases. - """ - old_names, mirrors = old_config - for connection, old_name, destroy in old_names: - if destroy: - connection.creation.destroy_test_db(old_name, self.verbosity) - - def teardown_test_environment(self, **kwargs): - unittest.removeHandler() - teardown_test_environment() - - def suite_result(self, suite, result, **kwargs): - return len(result.failures) + len(result.errors) - - def run_tests(self, test_labels, extra_tests=None, **kwargs): - """ - Run the unit tests for all the test labels in the provided list. - Labels must be of the form: - - app.TestClass.test_method - Run a single specific test method - - app.TestClass - Run all the test methods in a given class - - app - Search for doctests and unittests in the named application. - - When looking for tests, the test runner will look in the models and - tests modules for the application. - - A list of 'extra' tests may also be provided; these tests - will be added to the test suite. - - Returns the number of tests that failed. - """ - self.setup_test_environment() - suite = self.build_suite(test_labels, extra_tests) - old_config = self.setup_databases() - result = self.run_suite(suite) - self.teardown_databases(old_config) - self.teardown_test_environment() - return self.suite_result(suite, result) + return runner.reorder_suite(suite, (unittest.TestCase,)) diff --git a/django/test/testcases.py b/django/test/testcases.py index a9fcc2bab6..6fe6b9c397 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -97,6 +97,12 @@ def assert_and_parse_html(self, html, user_msg, msg): class OutputChecker(doctest.OutputChecker): + def __init__(self): + warnings.warn( + "The django.test.testcases.OutputChecker class is deprecated; " + "use the doctest module from the Python standard library instead.", + PendingDeprecationWarning) + def check_output(self, want, got, optionflags): """ The entry method for doctest output checking. Defers to a sequence of @@ -151,6 +157,10 @@ class OutputChecker(doctest.OutputChecker): class DocTestRunner(doctest.DocTestRunner): def __init__(self, *args, **kwargs): + warnings.warn( + "The django.test.testcases.DocTestRunner class is deprecated; " + "use the doctest module from the Python standard library instead.", + PendingDeprecationWarning) doctest.DocTestRunner.__init__(self, *args, **kwargs) self.optionflags = doctest.ELLIPSIS diff --git a/django/utils/dateformat.py b/django/utils/dateformat.py index b2586ba1ff..6d0a7b63f7 100644 --- a/django/utils/dateformat.py +++ b/django/utils/dateformat.py @@ -234,7 +234,7 @@ class DateFormat(TimeFormat): def T(self): "Time zone of this machine; e.g. 'EST' or 'MDT'" - name = self.timezone and self.timezone.tzname(self.data) or None + name = self.timezone.tzname(self.data) if self.timezone else None if name is None: name = self.format('O') return six.text_type(name) diff --git a/django/utils/html.py b/django/utils/html.py index 650e8485ab..8b28d97d13 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -187,7 +187,10 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False): If autoescape is True, the link text and URLs will get autoescaped. """ - trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x + def trim_url(x, limit=trim_url_limit): + if limit is None or len(x) <= limit: + return x + return '%s...' % x[:max(0, limit - 3)] safe_input = isinstance(text, SafeData) words = word_split_re.split(force_text(text)) for i, word in enumerate(words): diff --git a/django/utils/image.py b/django/utils/image.py new file mode 100644 index 0000000000..54c11adfee --- /dev/null +++ b/django/utils/image.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +""" +To provide a shim layer over Pillow/PIL situation until the PIL support is +removed. + + +Combinations To Account For +=========================== + +* Pillow: + + * never has ``_imaging`` under any Python + * has the ``Image.alpha_composite``, which may aid in detection + +* PIL + + * CPython 2.x may have _imaging (& work) + * CPython 2.x may *NOT* have _imaging (broken & needs a error message) + * CPython 3.x doesn't work + * PyPy will *NOT* have _imaging (but works?) + +Restated, that looks like: + +* If we're on Python 2.x, it could be either Pillow or PIL: + + * If ``import _imaging`` results in ``ImportError``, either they have a + working Pillow installation or a broken PIL installation, so we need to + detect further: + + * To detect, we first ``import Image``. + * If ``Image`` has a ``alpha_composite`` attribute present, only Pillow + has this, so we assume it's working. + * If ``Image`` DOES NOT have a ``alpha_composite``attribute, it must be + PIL & is a broken (likely C compiler-less) install, which we need to + warn the user about. + + * If ``import _imaging`` works, it must be PIL & is a working install. + +* Python 3.x + + * If ``import Image`` works, it must be Pillow, since PIL isn't Python 3.x + compatible. + +* PyPy + + * If ``import _imaging`` results in ``ImportError``, it could be either + Pillow or PIL, both of which work without it on PyPy, so we're fine. + + +Approach +======== + +* Attempt to import ``Image`` + + * ``ImportError`` - nothing is installed, toss an exception + * Either Pillow or the PIL is installed, so continue detecting + +* Attempt to ``hasattr(Image, 'alpha_composite')`` + + * If it works, it's Pillow & working + * If it fails, we've got a PIL install, continue detecting + + * The only option here is that we're on Python 2.x or PyPy, of which + we only care about if we're on CPython. + * If we're on CPython, attempt to ``import _imaging`` + + * ``ImportError`` - Bad install, toss an exception + +""" +from __future__ import unicode_literals + +import warnings + +from django.core.exceptions import ImproperlyConfigured +from django.utils.translation import ugettext_lazy as _ + + +Image = None +_imaging = None +ImageFile = None + + +def _detect_image_library(): + global Image + global _imaging + global ImageFile + + # Skip re-attempting to import if we've already run detection. + if Image is not None: + return Image, _imaging, ImageFile + + # Assume it's not there. + PIL_imaging = False + + try: + # Try from the Pillow (or one variant of PIL) install location first. + from PIL import Image as PILImage + except ImportError as err: + try: + # If that failed, try the alternate import syntax for PIL. + import Image as PILImage + except ImportError as err: + # Neither worked, so it's likely not installed. + raise ImproperlyConfigured( + _("Neither Pillow nor PIL could be imported: %s" % err) + ) + + # ``Image.alpha_composite`` was added to Pillow in SHA: e414c6 & is not + # available in any version of the PIL. + if hasattr(PILImage, 'alpha_composite'): + PIL_imaging = False + else: + # We're dealing with the PIL. Determine if we're on CPython & if + # ``_imaging`` is available. + import platform + + # This is the Alex Approved™ way. + # See http://mail.python.org/pipermail//pypy-dev/2011-November/008739.html + if platform.python_implementation().lower() == 'cpython': + # We're on CPython (likely 2.x). Since a C compiler is needed to + # produce a fully-working PIL & will create a ``_imaging`` module, + # we'll attempt to import it to verify their kit works. + try: + import _imaging as PIL_imaging + except ImportError as err: + raise ImproperlyConfigured( + _("The '_imaging' module for the PIL could not be " + + "imported: %s" % err) + ) + + # Try to import ImageFile as well. + try: + from PIL import ImageFile as PILImageFile + except ImportError: + import ImageFile as PILImageFile + + # Finally, warn about deprecation... + if PIL_imaging is not False: + warnings.warn( + "Support for the PIL will be removed in Django 1.8. Please " + + "uninstall it & install Pillow instead.", + PendingDeprecationWarning + ) + + return PILImage, PIL_imaging, PILImageFile + + +Image, _imaging, ImageFile = _detect_image_library() diff --git a/django/utils/log.py b/django/utils/log.py index b291b86706..a9b62caae1 100644 --- a/django/utils/log.py +++ b/django/utils/log.py @@ -111,7 +111,7 @@ class AdminEmailHandler(logging.Handler): message = "%s\n\n%s" % (stack_trace, request_repr) reporter = ExceptionReporter(request, is_email=True, *exc_info) - html_message = self.include_html and reporter.get_traceback_html() or None + html_message = reporter.get_traceback_html() if self.include_html else None mail.mail_admins(subject, message, fail_silently=True, html_message=html_message, connection=self.connection()) diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index ad172407de..07353c35ee 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -651,7 +651,10 @@ def parse_accept_lang_header(lang_string): first, lang, priority = pieces[i : i + 3] if first: return [] - priority = priority and float(priority) or 1.0 + if priority: + priority = float(priority) + if not priority: # if priority is 0.0 at this point make it 1.0 + priority = 1.0 result.append((lang, priority)) result.sort(key=lambda k: k[1], reverse=True) return result diff --git a/django/utils/tree.py b/django/utils/tree.py index 4152b4600b..3f93738b4f 100644 --- a/django/utils/tree.py +++ b/django/utils/tree.py @@ -20,7 +20,7 @@ class Node(object): Constructs a new Node. If no connector is given, the default will be used. """ - self.children = children and children[:] or [] + self.children = children[:] if children else [] self.connector = connector or self.default self.negated = negated diff --git a/django/utils/unittest/loader.py b/django/utils/unittest/loader.py index 0bdd106d39..695bac40ef 100644 --- a/django/utils/unittest/loader.py +++ b/django/utils/unittest/loader.py @@ -125,7 +125,7 @@ class TestLoader(unittest.TestLoader): return self.loadTestsFromTestCase(obj) elif (isinstance(obj, types.UnboundMethodType) and isinstance(parent, type) and - issubclass(parent, case.TestCase)): + issubclass(parent, unittest.TestCase)): return self.suiteClass([parent(obj.__name__)]) elif isinstance(obj, unittest.TestSuite): return obj diff --git a/docs/faq/contributing.txt b/docs/faq/contributing.txt index 6f2dfd906f..20950e88c5 100644 --- a/docs/faq/contributing.txt +++ b/docs/faq/contributing.txt @@ -27,7 +27,7 @@ to make it dead easy, even for someone who may not be intimately familiar with that area of the code, to understand the problem and verify the fix: * Are there clear instructions on how to reproduce the bug? If this - touches a dependency (such as PIL), a contrib module, or a specific + touches a dependency (such as Pillow/PIL), a contrib module, or a specific database, are those instructions clear enough even for someone not familiar with it? diff --git a/docs/index.txt b/docs/index.txt index 6473aa3168..7f9d1bd032 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -185,8 +185,7 @@ testing of Django applications: * **Testing:** :doc:`Introduction <topics/testing/index>` | :doc:`Writing and running tests <topics/testing/overview>` | - :doc:`Advanced topics <topics/testing/advanced>` | - :doc:`Doctests <topics/testing/doctests>` + :doc:`Advanced topics <topics/testing/advanced>` * **Deployment:** :doc:`Overview <howto/deployment/index>` | diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index f891bc4eb7..6b9c7df14a 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -30,13 +30,10 @@ Journal-World`_ of Lawrence, Kansas, USA. Simon lives in Brighton, England. `Jacob Kaplan-Moss`_ - Jacob is a partner at `Revolution Systems`_ which provides support services - around Django and related open source technologies. A good deal of Jacob's - work time is devoted to working on Django. Jacob previously worked at World - Online, where Django was invented, where he was the lead developer of - Ellington, a commercial Web publishing platform for media companies. - - Jacob lives in Lawrence, Kansas, USA. + Jacob is Director of Platform Security at Heroku_. He worked at World + Online for four years, where he helped open source Django and found + the Django Software Foundation. Jacob lives on a hobby farm outside of + Lawrence where he spends his weekends playing with dirt and power tools. `Wilson Miner`_ Wilson's design-fu is what makes Django look so nice. He designed the @@ -55,6 +52,7 @@ Journal-World`_ of Lawrence, Kansas, USA. .. _jacob kaplan-moss: http://jacobian.org/ .. _revolution systems: http://revsys.com/ .. _wilson miner: http://wilsonminer.com/ +.. _heroku: http://heroku.com/ Current developers ================== @@ -200,7 +198,7 @@ Karen Tracey He has worked on Django's auth, admin and staticfiles apps as well as the form, core, internationalization and test systems. He currently works - as the lead engineer at Gidsy_. + at Mozilla_. Jannis lives in Berlin, Germany. @@ -208,7 +206,7 @@ Karen Tracey .. _Bauhaus-University Weimar: http://www.uni-weimar.de/ .. _virtualenv: http://www.virtualenv.org/ .. _pip: http://www.pip-installer.org/ -.. _Gidsy: http://gidsy.com/ +.. _Mozilla: http://www.mozilla.org/ `James Tauber`_ James is the lead developer of Pinax_ and the CEO and founder of @@ -455,9 +453,6 @@ Jeremy Dunck .. _`Daniel Lindsley`: http://toastdriven.com/ .. _`Amazon Web Services`: https://aws.amazon.com/ -Specialists ------------ - `James Bennett`_ James is Django's release manager, and also contributes to the documentation and provide the occasional bugfix. @@ -508,6 +503,30 @@ Tim Graham Tim works as a software engineer and lives in Philadelphia, PA, USA. +Marc Tamlyn + Marc started life on the web using Django 1.2 back in 2010, and has never + looked back. He was involved with rewriting the class based view + documentation at DjangoCon EU 2012, and also helped to develop `CCBV`_, an + additional class based view reference tool. + + Marc currently works at `Incuna Ltd`_, a digital healthcare agency in + Oxford, UK. + +.. _CCBV: http://ccbv.co.uk/ +.. _Incuna Ltd: http://incuna.com/ + +Donald Stufft + Donald found Python and Django in 2007 while trying to find a language, + and web framework that he really enjoyed using after many years of PHP. He + fell in love with the beauty of Python and the way Django made tasks simple + and easy. His contributions to Django focus primarily on ensuring that it + is and remains a secure web framework. + + Donald currently works at `Nebula Inc`_ as a Software Engineer for their + security team and lives in the Greater Philadelphia Area. + +.. _Nebula Inc: https://www.nebula.com/ + Developers Emeritus =================== diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 1533e25dc8..774de2a2fd 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -73,7 +73,7 @@ these changes. ``django.utils.formats.get_format()`` to get the appropriate formats. -* The ability to use a function-based test runners will be removed, +* The ability to use a function-based test runner will be removed, along with the ``django.test.simple.run_tests()`` test runner. * The ``views.feed()`` view and ``feeds.Feed`` class in @@ -365,6 +365,12 @@ these changes. * ``django.conf.urls.shortcut`` and ``django.views.defaults.shortcut`` will be removed. +* Support for the Python Imaging Library (PIL) module will be removed, as it + no longer appears to be actively maintained & does not work on Python 3. + You are advised to install `Pillow`_, which should be used instead. + +.. _`Pillow`: https://pypi.python.org/pypi/Pillow + * The following private APIs will be removed: - ``django.db.close_connection()`` @@ -375,6 +381,15 @@ these changes. * ``django.forms.widgets.RadioInput`` will be removed in favor of ``django.forms.widgets.RadioChoiceInput``. +* The module ``django.test.simple`` and the class + ``django.test.simple.DjangoTestSuiteRunner`` will be removed. Instead use + ``django.test.runner.DiscoverRunner``. + +* The module ``django.test._doctest`` and the classes + ``django.test.testcases.DocTestRunner`` and + ``django.test.testcases.OutputChecker`` will be removed. Instead use the + doctest module from the Python standard library. + 2.0 --- diff --git a/docs/intro/tutorial04.txt b/docs/intro/tutorial04.txt index 87d8e584ad..9f54243a3e 100644 --- a/docs/intro/tutorial04.txt +++ b/docs/intro/tutorial04.txt @@ -185,7 +185,7 @@ conversion. We will: 2. Delete some of the old, unneeded views. -3. Fix up URL handling for the new views. +3. Introduce new views based on Django's generic views. Read on for details. @@ -205,32 +205,51 @@ Amend URLconf First, open the ``polls/urls.py`` URLconf and change it like so:: from django.conf.urls import patterns, url - from django.views.generic import DetailView, ListView - from polls.models import Poll + + from polls import views urlpatterns = patterns('', - url(r'^$', - ListView.as_view( - queryset=Poll.objects.order_by('-pub_date')[:5], - context_object_name='latest_poll_list', - template_name='polls/index.html'), - name='index'), - url(r'^(?P<pk>\d+)/$', - DetailView.as_view( - model=Poll, - template_name='polls/detail.html'), - name='detail'), - url(r'^(?P<pk>\d+)/results/$', - DetailView.as_view( - model=Poll, - template_name='polls/results.html'), - name='results'), - url(r'^(?P<poll_id>\d+)/vote/$', 'polls.views.vote', name='vote'), + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^(?P<pk>\d+)/$', views.DetailView.as_view(), name='detail'), + url(r'^(?P<pk>\d+)/results/$', views.ResultsView.as_view(), name='results'), + url(r'^(?P<poll_id>\d+)/vote/$', views.vote, name='vote'), ) Amend views ----------- +Next, we're going to remove our old ``index``, ``detail``, and ``results`` +views and use Django's generic views instead. To do so, open the +``polls/views.py`` file and change it like so:: + + from django.shortcuts import get_object_or_404, render + from django.http import HttpResponseRedirect + from django.core.urlresolvers import reverse + from django.views import generic + + from polls.models import Choice, Poll + + class IndexView(generic.ListView): + template_name = 'polls/index.html' + context_object_name = 'latest_poll_list' + + def get_queryset(self): + """Return the last five published polls.""" + return Poll.objects.order_by('-pub_date')[:5] + + + class DetailView(generic.DetailView): + model = Poll + template_name = 'polls/detail.html' + + + class ResultsView(generic.DetailView): + model = Poll + template_name = 'polls/results.html' + + def vote(request, poll_id): + .... + We're using two generic views here: :class:`~django.views.generic.list.ListView` and :class:`~django.views.generic.detail.DetailView`. Respectively, those @@ -238,7 +257,7 @@ two views abstract the concepts of "display a list of objects" and "display a detail page for a particular type of object." * Each generic view needs to know what model it will be acting - upon. This is provided using the ``model`` parameter. + upon. This is provided using the ``model`` attribute. * The :class:`~django.views.generic.detail.DetailView` generic view expects the primary key value captured from the URL to be called @@ -248,7 +267,7 @@ two views abstract the concepts of "display a list of objects" and By default, the :class:`~django.views.generic.detail.DetailView` generic view uses a template called ``<app name>/<model name>_detail.html``. In our case, it'll use the template ``"polls/poll_detail.html"``. The -``template_name`` argument is used to tell Django to use a specific +``template_name`` attribute is used to tell Django to use a specific template name instead of the autogenerated default template name. We also specify the ``template_name`` for the ``results`` list view -- this ensures that the results view and the detail view have a @@ -268,16 +287,11 @@ automatically -- since we're using a Django model (``Poll``), Django is able to determine an appropriate name for the context variable. However, for ListView, the automatically generated context variable is ``poll_list``. To override this we provide the ``context_object_name`` -option, specifying that we want to use ``latest_poll_list`` instead. +attribute, specifying that we want to use ``latest_poll_list`` instead. As an alternative approach, you could change your templates to match the new default context variables -- but it's a lot easier to just tell Django to use the variable you want. -You can now delete the ``index()``, ``detail()`` and ``results()`` views from -``polls/views.py``. We don't need them anymore -- they have been replaced by -generic views. You can also delete the import for ``HttpResponse``, which is no -longer required. - Run the server, and use your new polling app based on generic views. For full details on generic views, see the :doc:`generic views documentation diff --git a/docs/intro/tutorial05.txt b/docs/intro/tutorial05.txt index 97d1d96ad7..a276763d67 100644 --- a/docs/intro/tutorial05.txt +++ b/docs/intro/tutorial05.txt @@ -156,8 +156,9 @@ Create a test to expose the bug What we've just done in the shell to test for the problem is exactly what we can do in an automated test, so let's turn that into an automated test. -The best place for an application's tests is in the application's ``tests.py`` -file - the testing system will look there for tests automatically. +A conventional place for an application's tests is in the application's +``tests.py`` file; the testing system will automatically find tests in any file +whose name begins with ``test``. Put the following in the ``tests.py`` file in the ``polls`` application:: @@ -377,45 +378,40 @@ Improving our view The list of polls shows polls that aren't published yet (i.e. those that have a ``pub_date`` in the future). Let's fix that. -In :doc:`Tutorial 4 </intro/tutorial04>` we deleted the view functions from -``views.py`` in favor of a :class:`~django.views.generic.list.ListView` in -``urls.py``:: +In :doc:`Tutorial 4 </intro/tutorial04>` we introduced a class-based view, +based on :class:`~django.views.generic.list.ListView`:: - url(r'^$', - ListView.as_view( - queryset=Poll.objects.order_by('-pub_date')[:5], - context_object_name='latest_poll_list', - template_name='polls/index.html'), - name='index'), + class IndexView(generic.ListView): + template_name = 'polls/index.html' + context_object_name = 'latest_poll_list' + + def get_queryset(self): + """Return the last five published polls.""" + return Poll.objects.order_by('-pub_date')[:5] ``response.context_data['latest_poll_list']`` extracts the data this view places into the context. -We need to amend the line that gives us the ``queryset``:: - - queryset=Poll.objects.order_by('-pub_date')[:5], - -Let's change the queryset so that it also checks the date by comparing it with -``timezone.now()``. First we need to add an import:: +We need to amend the ``get_queryset`` method and change it so that it also +checks the date by comparing it with ``timezone.now()``. First we need to add +an import:: from django.utils import timezone -and then we must amend the existing ``url`` function to:: +and then we must amend the ``get_queryset`` method like so:: - url(r'^$', - ListView.as_view( - queryset=Poll.objects.filter(pub_date__lte=timezone.now) \ - .order_by('-pub_date')[:5], - context_object_name='latest_poll_list', - template_name='polls/index.html'), - name='index'), + def get_queryset(self): + """ + Return the last five published polls (not including those set to be + published in the future). + """ + return Poll.objects.filter( + pub_date__lte=timezone.now() + ).order_by('-pub_date')[:5] -``Poll.objects.filter(pub_date__lte=timezone.now)`` returns a queryset +``Poll.objects.filter(pub_date__lte=timezone.now())`` returns a queryset containing Polls whose ``pub_date`` is less than or equal to - that is, earlier -than or equal to - ``timezone.now``. Notice that we use a callable queryset -argument, ``timezone.now``, which will be evaluated at request time. If we had -included the parentheses, ``timezone.now()`` would be evaluated just once when -the web server is started. +than or equal to - ``timezone.now``. Testing our new view -------------------- @@ -526,20 +522,18 @@ Testing the ``DetailView`` What we have works well; however, even though future polls don't appear in the *index*, users can still reach them if they know or guess the right URL. So we -need similar constraints in the ``DetailViews``, by adding:: - - queryset=Poll.objects.filter(pub_date__lte=timezone.now) +need to add a similar constraint to ``DetailView``:: -to them - for example:: - url(r'^(?P<pk>\d+)/$', - DetailView.as_view( - queryset=Poll.objects.filter(pub_date__lte=timezone.now), - model=Poll, - template_name='polls/detail.html'), - name='detail'), + class DetailView(generic.DetailView): + ... + def get_queryset(self): + """ + Excludes any polls that aren't published yet. + """ + return Poll.objects.filter(pub_date__lte=timezone.now()) -and of course, we will add some tests, to check that a ``Poll`` whose +And of course, we will add some tests, to check that a ``Poll`` whose ``pub_date`` is in the past can be displayed, and that one with a ``pub_date`` in the future is not:: @@ -565,9 +559,9 @@ in the future is not:: Ideas for more tests -------------------- -We ought to add similar ``queryset`` arguments to the other ``DetailView`` -URLs, and create a new test class for each view. They'll be very similar to -what we have just created; in fact there will be a lot of repetition. +We ought to add a similar ``get_queryset`` method to ``ResultsView`` and +create a new test class for that view. It'll be very similar to what we have +just created; in fact there will be a lot of repetition. We could also improve our application in other ways, adding tests along the way. For example, it's silly that ``Polls`` can be published on the site that diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index 3ba7c38c43..ee0bf0f225 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -9,7 +9,7 @@ required for projects, in which case there are Mixins and Generic class-based views. Many of Django's built-in class-based views inherit from other class-based -views or various mixins. Because this inheritence chain is very important, the +views or various mixins. Because this inheritance chain is very important, the ancestor classes are documented under the section title of **Ancestors (MRO)**. MRO is an acronym for Method Resolution Order. diff --git a/docs/ref/contrib/admin/admindocs.txt b/docs/ref/contrib/admin/admindocs.txt index b3e26eca48..394d078e5b 100644 --- a/docs/ref/contrib/admin/admindocs.txt +++ b/docs/ref/contrib/admin/admindocs.txt @@ -57,9 +57,10 @@ Model reference =============== The **models** section of the ``admindocs`` page describes each model in the -system along with all the fields and methods available on it. Relationships to -other models appear as hyperlinks. Descriptions are pulled from ``help_text`` -attributes on fields or from docstrings on model methods. +system along with all the fields and methods (without any arguments) available +on it. While model properties don't have any arguments, they are not listed. +Relationships to other models appear as hyperlinks. Descriptions are pulled +from ``help_text`` attributes on fields or from docstrings on model methods. A model with useful documentation might look like this:: diff --git a/docs/ref/contrib/gis/forms-api.txt b/docs/ref/contrib/gis/forms-api.txt new file mode 100644 index 0000000000..d0c671958f --- /dev/null +++ b/docs/ref/contrib/gis/forms-api.txt @@ -0,0 +1,165 @@ +.. _ref-gis-forms-api: + +=================== +GeoDjango Forms API +=================== + +.. module:: django.contrib.gis.forms + :synopsis: GeoDjango forms API. + +.. versionadded:: 1.6 + +GeoDjango provides some specialized form fields and widgets in order to visually +display and edit geolocalized data on a map. By default, they use +`OpenLayers`_-powered maps, with a base WMS layer provided by `Metacarta`_. + +.. _OpenLayers: http://openlayers.org/ +.. _Metacarta: http://metacarta.com/ + +Field arguments +=============== +In addition to the regular :ref:`form field arguments <core-field-arguments>`, +GeoDjango form fields take the following optional arguments. + +``srid`` +~~~~~~~~ + +.. attribute:: Field.srid + + This is the SRID code that the field value should be transformed to. For + example, if the map widget SRID is different from the SRID more generally + used by your application or database, the field will automatically convert + input values into that SRID. + +``geom_type`` +~~~~~~~~~~~~~ + +.. attribute:: Field.geom_type + + You generally shouldn't have to set or change that attribute which should + be setup depending on the field class. It matches the OpenGIS standard + geometry name. + +Form field classes +================== + +``GeometryField`` +~~~~~~~~~~~~~~~~~ + +.. class:: GeometryField + +``PointField`` +~~~~~~~~~~~~~~ + +.. class:: PointField + +``LineStringField`` +~~~~~~~~~~~~~~~~~~~ + +.. class:: LineStringField + +``PolygonField`` +~~~~~~~~~~~~~~~~ + +.. class:: PolygonField + +``MultiPointField`` +~~~~~~~~~~~~~~~~~~~ + +.. class:: MultiPointField + +``MultiLineStringField`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: MultiLineStringField + +``MultiPolygonField`` +~~~~~~~~~~~~~~~~~~~~~ + +.. class:: MultiPolygonField + +``GeometryCollectionField`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: GeometryCollectionField + +Form widgets +============ + +.. module:: django.contrib.gis.widgets + :synopsis: GeoDjango widgets API. + +GeoDjango form widgets allow you to display and edit geographic data on a +visual map. +Note that none of the currently available widgets supports 3D geometries, hence +geometry fields will fallback using a simple ``Textarea`` widget for such data. + +Widget attributes +~~~~~~~~~~~~~~~~~ + +GeoDjango widgets are template-based, so their attributes are mostly different +from other Django widget attributes. + + +.. attribute:: BaseGeometryWidget.geom_type + + The OpenGIS geometry type, generally set by the form field. + +.. attribute:: BaseGeometryWidget.map_height +.. attribute:: BaseGeometryWidget.map_width + + Height and width of the widget map (default is 400x600). + +.. attribute:: BaseGeometryWidget.map_srid + + SRID code used by the map (default is 4326). + +.. attribute:: BaseGeometryWidget.display_wkt + + Boolean value specifying if a textarea input showing the WKT representation + of the current geometry is visible, mainly for debugging purposes (default + is ``False``). + +.. attribute:: BaseGeometryWidget.supports_3d + + Indicates if the widget supports edition of 3D data (default is ``False``). + +.. attribute:: BaseGeometryWidget.template_name + + The template used to render the map widget. + +You can pass widget attributes in the same manner that for any other Django +widget. For example:: + + from django.contrib.gis import forms + + class MyGeoForm(forms.Form): + point = forms.PointField(widget= + forms.OSMWidget(attrs={'map_width': 800, 'map_height': 500})) + +Widget classes +~~~~~~~~~~~~~~ + +``BaseGeometryWidget`` + +.. class:: BaseGeometryWidget + + This is an abstract base widget containing the logic needed by subclasses. + You cannot directly use this widget for a geometry field. + Note that the rendering of GeoDjango widgets is based on a template, + identified by the :attr:`template_name` class attribute. + +``OpenLayersWidget`` + +.. class:: OpenLayersWidget + + This is the default widget used by all GeoDjango form fields. + ``template_name`` is ``gis/openlayers.html``. + +``OSMWidget`` + +.. class:: OSMWidget + + This widget uses an OpenStreetMap base layer (Mapnik) to display geographic + objects on. + ``template_name`` is ``gis/openlayers-osm.html``. diff --git a/docs/ref/contrib/gis/index.txt b/docs/ref/contrib/gis/index.txt index 6a1402bfab..c533aa459d 100644 --- a/docs/ref/contrib/gis/index.txt +++ b/docs/ref/contrib/gis/index.txt @@ -18,6 +18,7 @@ of spatially enabled data. install/index model-api db-api + forms-api geoquerysets measure geos diff --git a/docs/ref/contrib/gis/testing.txt b/docs/ref/contrib/gis/testing.txt index 2a6dcef46f..fca6675345 100644 --- a/docs/ref/contrib/gis/testing.txt +++ b/docs/ref/contrib/gis/testing.txt @@ -134,57 +134,14 @@ your settings:: GeoDjango tests =============== -GeoDjango's test suite may be run in one of two ways, either by itself or -with the rest of :ref:`Django's unit tests <running-unit-tests>`. +To have the GeoDjango tests executed when :ref:`running the Django test suite +<running-unit-tests>` with ``runtests.py`` all of the databases in the settings +file must be using one of the :ref:`spatial database backends +<spatial-backends>`. -Run only GeoDjango tests ------------------------- - -.. class:: django.contrib.gis.tests.GeoDjangoTestSuiteRunner - -To run *only* the tests for GeoDjango, the :setting:`TEST_RUNNER` -setting must be changed to use the -:class:`~django.contrib.gis.tests.GeoDjangoTestSuiteRunner`:: - - TEST_RUNNER = 'django.contrib.gis.tests.GeoDjangoTestSuiteRunner' Example -^^^^^^^ - -First, you'll need a bare-bones settings file, like below, that is -customized with your spatial database name and user:: - - TEST_RUNNER = 'django.contrib.gis.tests.GeoDjangoTestSuiteRunner' - - DATABASES = { - 'default': { - 'ENGINE': 'django.contrib.gis.db.backends.postgis', - 'NAME': 'a_spatial_database', - 'USER': 'db_user' - } - } - -Assuming the above is in a file called ``postgis.py`` that is in the -the same directory as ``manage.py`` of your Django project, then -you may run the tests with the following command:: - - $ python manage.py test --settings=postgis - -Run with ``runtests.py`` ------------------------- - -To have the GeoDjango tests executed when -:ref:`running the Django test suite <running-unit-tests>` with ``runtests.py`` -all of the databases in the settings file must be using one of the -:ref:`spatial database backends <spatial-backends>`. - -.. warning:: - - Do not change the :setting:`TEST_RUNNER` setting - when running the GeoDjango tests with ``runtests.py``. - -Example -^^^^^^^ +------- The following is an example bare-bones settings file with spatial backends that can be used to run the entire Django test suite, including those @@ -208,3 +165,7 @@ directory as ``runtests.py``, then all Django and GeoDjango tests would be performed when executing the command:: $ ./runtests.py --settings=postgis + +To run only the GeoDjango test suite, specify ``django.contrib.gis``:: + + $ ./runtests.py --settings=postgis django.contrib.gis diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index ec49705add..2f2880679c 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -373,7 +373,46 @@ application, ``<dirname>/foo/bar/mydata.json`` for each directory in :setting:`FIXTURE_DIRS`, and the literal path ``foo/bar/mydata.json``. When fixture files are processed, the data is saved to the database as is. -Model defined ``save`` methods and ``pre_save`` signals are not called. +Model defined :meth:`~django.db.models.Model.save` methods are not called, and +any :data:`~django.db.models.signals.pre_save` or +:data:`~django.db.models.signals.post_save` signals will be called with +``raw=True`` since the instance only contains attributes that are local to the +model. You may, for example, want to disable handlers that access +related fields that aren't present during fixture loading and would otherwise +raise an exception:: + + from django.db.models.signals import post_save + from .models import MyModel + + def my_handler(**kwargs): + # disable the handler during fixture loading + if kwargs['raw']: + return + ... + + post_save.connect(my_handler, sender=MyModel) + +You could also write a simple decorator to encapsulate this logic:: + + from functools import wraps + + def disable_for_loaddata(signal_handler): + """ + Decorator that turns off signal handlers when loading fixture data. + """ + @wraps(signal_handler) + def wrapper(*args, **kwargs): + if kwargs['raw']: + return + signal_handler(*args, **kwargs) + return wrapper + + @disable_for_loaddata + def my_handler(**kwargs): + ... + +Just be aware that this logic will disable the signals whenever fixtures are +deserialized, not just during ``loaddata``. Note that the order in which fixture files are processed is undefined. However, all fixture data is installed as a single transaction, so data in diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 29f889445d..8e1a4b34d1 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -30,6 +30,8 @@ exception or returns the clean value:: ... ValidationError: [u'Enter a valid email address.'] +.. _core-field-arguments: + Core field arguments -------------------- @@ -608,19 +610,21 @@ For each field, we describe the default widget used if you don't specify * Normalizes to: An ``UploadedFile`` object that wraps the file content and file name into a single object. * Validates that file data has been bound to the form, and that the - file is of an image format understood by PIL. + file is of an image format understood by Pillow/PIL. * Error message keys: ``required``, ``invalid``, ``missing``, ``empty``, ``invalid_image`` - Using an ``ImageField`` requires that the `Python Imaging Library`_ (PIL) - is installed and supports the image formats you use. If you encounter a - ``corrupt image`` error when you upload an image, it usually means PIL + Using an ``ImageField`` requires that either `Pillow`_ (recommended) or the + `Python Imaging Library`_ (PIL) are installed and supports the image + formats you use. If you encounter a ``corrupt image`` error when you + upload an image, it usually means either Pillow or PIL doesn't understand its format. To fix this, install the appropriate - library and reinstall PIL. + library and reinstall Pillow or PIL. When you use an ``ImageField`` on a form, you must also remember to :ref:`bind the file data to the form <binding-uploaded-files>`. +.. _Pillow: http://python-imaging.github.io/Pillow/ .. _Python Imaging Library: http://www.pythonware.com/products/pil/ ``IntegerField`` diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index d322904ec9..99ba78cb09 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -287,6 +287,9 @@ For example, if you have a field ``title`` that has ``unique_for_date="pub_date"``, then Django wouldn't allow the entry of two records with the same ``title`` and ``pub_date``. +Note that if you set this to point to a :class:`DateTimeField`, only the date +portion of the field will be considered. + This is enforced by model validation but not at the database level. ``unique_for_month`` diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index d27214a66c..ffada19082 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -328,7 +328,7 @@ Use the ``reverse()`` method to reverse the order in which a queryset's elements are returned. Calling ``reverse()`` a second time restores the ordering back to the normal direction. -To retrieve the ''last'' five items in a queryset, you could do this:: +To retrieve the "last" five items in a queryset, you could do this:: my_queryset.reverse()[:5] @@ -1486,7 +1486,7 @@ internally so that repeated evaluations do not result in additional queries. In contrast, ``iterator()`` will read results directly, without doing any caching at the ``QuerySet`` level (internally, the default iterator calls ``iterator()`` and caches the return value). For a ``QuerySet`` which returns a large number of -objects that you only need to access once, this can results in better +objects that you only need to access once, this can result in better performance and a significant reduction in memory. Note that using ``iterator()`` on a ``QuerySet`` which has already been diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 04b42aeeb2..eb470cdd14 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1725,11 +1725,16 @@ misspelled) variables. See :ref:`invalid-template-variables`.. TEST_RUNNER ----------- -Default: ``'django.test.simple.DjangoTestSuiteRunner'`` +Default: ``'django.test.runner.DiscoverRunner'`` The name of the class to use for starting the test suite. See :ref:`other-testing-frameworks`. +.. versionchanged:: 1.6 + + Previously the default ``TEST_RUNNER`` was + ``django.test.simple.DjangoTestSuiteRunner``. + .. setting:: THOUSAND_SEPARATOR THOUSAND_SEPARATOR diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index a7d5b6690e..14ae9aa9b8 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -659,11 +659,11 @@ Functions for working with Python modules. wrong. For example:: from django.utils.module_loading import import_by_path - import_by_path = import_by_path('django.utils.module_loading.import_by_path') + ImproperlyConfigured = import_by_path('django.core.exceptions.ImproperlyConfigured') is equivalent to:: - from django.utils.module_loading import import_by_path + from django.core.exceptions import ImproperlyConfigured ``django.utils.safestring`` =========================== diff --git a/docs/releases/1.2.4.txt b/docs/releases/1.2.4.txt index b74ea9aef2..ae15ea6f7c 100644 --- a/docs/releases/1.2.4.txt +++ b/docs/releases/1.2.4.txt @@ -78,7 +78,7 @@ GeoDjango The function-based :setting:`TEST_RUNNER` previously used to execute the GeoDjango test suite, ``django.contrib.gis.tests.run_gis_tests``, was finally deprecated in favor of a class-based test runner, -:class:`django.contrib.gis.tests.GeoDjangoTestSuiteRunner`, added in this +``django.contrib.gis.tests.GeoDjangoTestSuiteRunner``, added in this release. In addition, the GeoDjango test suite is now included when diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt index 9a41f903f8..89cece941b 100644 --- a/docs/releases/1.3.txt +++ b/docs/releases/1.3.txt @@ -799,7 +799,7 @@ GeoDjango * The function-based :setting:`TEST_RUNNER` previously used to execute the GeoDjango test suite, ``django.contrib.gis.tests.run_gis_tests``, was deprecated for the class-based runner, - :class:`django.contrib.gis.tests.GeoDjangoTestSuiteRunner`. + ``django.contrib.gis.tests.GeoDjangoTestSuiteRunner``. * Previously, calling :meth:`~django.contrib.gis.geos.GEOSGeometry.transform` would diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 9cce36aac3..98889254cd 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -69,6 +69,29 @@ This avoids the overhead of re-establishing a connection at the beginning of each request. For backwards compatibility, this feature is disabled by default. See :ref:`persistent-database-connections` for details. +Discovery of tests in any test module +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Django 1.6 ships with a new test runner that allows more flexibility in the +location of tests. The previous runner +(``django.test.simple.DjangoTestSuiteRunner``) found tests only in the +``models.py`` and ``tests.py`` modules of a Python package in +:setting:`INSTALLED_APPS`. + +The new runner (``django.test.runner.DjangoTestDiscoverRunner``) uses the test +discovery features built into unittest2 (the version of unittest in the Python +2.7+ standard library, and bundled with Django). With test discovery, tests can +be located in any module whose name matches the pattern ``test*.py``. + +In addition, the test labels provided to ``./manage.py test`` to nominate +specific tests to run must now be full Python dotted paths (or directory +paths), rather than ``applabel.TestCase.test_method_name`` pseudo-paths. This +allows running tests located anywhere in your codebase, rather than only in +:setting:`INSTALLED_APPS`. For more details, see :doc:`/topics/testing/index`. + +This change is backwards-incompatible; see the :ref:`backwards-incompatibility +notes<new-test-runner>`. + Time zone aware aggregation ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -91,6 +114,13 @@ Django 1.6 adds support for savepoints in SQLite, with some :ref:`limitations A new :class:`django.db.models.BinaryField` model field allows to store raw binary data in the database. +GeoDjango form widgets +~~~~~~~~~~~~~~~~~~~~~~ + +GeoDjango now provides :ref:`form fields and widgets <ref-gis-forms-api>` for +its geo-specialized fields. They are OpenLayers-based by default, but they can +be customized to use any other JS framework. + Minor features ~~~~~~~~~~~~~~ @@ -197,6 +227,13 @@ Minor features * Added ``BCryptSHA256PasswordHasher`` to resolve the password truncation issue with bcrypt. +* `Pillow`_ is now the preferred image manipulation library to use with Django. + `PIL`_ is pending deprecation (support to be removed in Django 1.8). + To upgrade, you should **first** uninstall PIL, **then** install Pillow. + +.. _`Pillow`: https://pypi.python.org/pypi/Pillow +.. _`PIL`: https://pypi.python.org/pypi/PIL + Backwards incompatible changes in 1.6 ===================================== @@ -238,6 +275,40 @@ In previous versions, database-level autocommit was only an option for PostgreSQL, and it was disabled by default. This option is now :ref:`ignored <postgresql-autocommit-mode>` and can be removed. +.. _new-test-runner: + +New test runner +~~~~~~~~~~~~~~~ + +In order to maintain greater consistency with Python's unittest module, the new +test runner (``django.test.runner.DiscoverRunner``) does not automatically +support some types of tests that were supported by the previous runner: + +* Tests in ``models.py`` and ``tests/__init__.py`` files will no longer be + found and run. Move them to a file whose name begins with ``test``. + +* Doctests will no longer be automatically discovered. To integrate doctests in + your test suite, follow the `recommendations in the Python documentation`_. + +Django bundles a modified version of the :mod:`doctest` module from the Python +standard library (in ``django.test._doctest``) in order to allow passing in a +custom ``DocTestRunner`` when instantiating a ``DocTestSuite``, and includes +some additional doctest utilities (``django.test.testcases.DocTestRunner`` +turns on the ``ELLIPSIS`` option by default, and +``django.test.testcases.OutputChecker`` provides better matching of XML, JSON, +and numeric data types). + +These utilities are deprecated and will be removed in Django 1.8; doctest +suites should be updated to work with the standard library's doctest module (or +converted to unittest-compatible tests). + +If you wish to delay updates to your test suite, you can set your +:setting:`TEST_RUNNER` setting to ``django.test.simple.DjangoTestSuiteRunner`` +to fully restore the old test behavior. ``DjangoTestSuiteRunner`` is +deprecated but will not be removed from Django until version 1.8. + +.. _recommendations in the Python documentation: http://docs.python.org/2/library/doctest.html#unittest-api + Addition of ``QuerySet.datetimes()`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index b53bbe8211..56f3e60350 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -95,7 +95,8 @@ An authentication backend is a class that implements two required methods: optional permission related :ref:`authorization methods <authorization_methods>`. The ``get_user`` method takes a ``user_id`` -- which could be a username, -database ID or whatever -- and returns a ``User`` object. +database ID or whatever, but has to be the primary key of your ``User`` object +-- and returns a ``User`` object. The ``authenticate`` method takes credentials as keyword arguments. Most of the time, it'll just look like this:: diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index e666cded75..8849520b11 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -939,10 +939,15 @@ provides several built-in forms located in :mod:`django.contrib.auth.forms`: A form used in the admin interface to change a user's password. + Takes the ``user`` as the first positional argument. + .. class:: AuthenticationForm A form for logging a user in. + Takes ``request`` as its first positional argument, which is stored on the + form instance for use by sub-classes. + .. class:: PasswordChangeForm A form for allowing a user to change their password. diff --git a/docs/topics/auth/passwords.txt b/docs/topics/auth/passwords.txt index 2193e6a3c7..206e7d856c 100644 --- a/docs/topics/auth/passwords.txt +++ b/docs/topics/auth/passwords.txt @@ -76,8 +76,8 @@ use it Django supports bcrypt with minimal effort. To use Bcrypt as your default storage algorithm, do the following: -1. Install the `py-bcrypt`_ library (probably by running ``sudo pip install - py-bcrypt``, or downloading the library and installing it with ``python +1. Install the `bcrypt library`_ (probably by running ``sudo pip install + bcrypt``, or downloading the library and installing it with ``python setup.py install``). 2. Modify :setting:`PASSWORD_HASHERS` to list ``BCryptSHA256PasswordHasher`` @@ -185,7 +185,7 @@ mentioned algorithms won't be able to upgrade. .. _pbkdf2: http://en.wikipedia.org/wiki/PBKDF2 .. _nist: http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf .. _bcrypt: http://en.wikipedia.org/wiki/Bcrypt -.. _py-bcrypt: http://pypi.python.org/pypi/py-bcrypt/ +.. _`bcrypt library`: https://pypi.python.org/pypi/bcrypt/ Manually managing a user's password diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index 255584c68b..78786996cd 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -92,19 +92,15 @@ Django provides a single API to control database transactions. .. function:: atomic(using=None, savepoint=True) - This function creates an atomic block for writes to the database. - (Atomicity is the defining property of database transactions.) + Atomicity is the defining property of database transactions. ``atomic`` + allows us to create a block of code within which the atomicity on the + database is guaranteed. If the block of code is successfully completed, the + changes are committed to the database. If there is an exception, the + changes are rolled back. - When the block completes successfully, the changes are committed to the - database. When it raises an exception, the changes are rolled back. - - ``atomic`` can be nested. In this case, when an inner block completes - successfully, its effects can still be rolled back if an exception is - raised in the outer block at a later point. - - ``atomic`` takes a ``using`` argument which should be the name of a - database. If this argument isn't provided, Django uses the ``"default"`` - database. + ``atomic`` blocks can be nested. In this case, when an inner block + completes successfully, its effects can still be rolled back if an + exception is raised in the outer block at a later point. ``atomic`` is usable both as a `decorator`_:: @@ -137,24 +133,32 @@ Django provides a single API to control database transactions. @transaction.atomic def viewfunc(request): - do_stuff() + create_parent() try: with transaction.atomic(): - do_stuff_that_could_fail() + generate_relationships() except IntegrityError: handle_exception() - do_more_stuff() + add_children() - In this example, even if ``do_stuff_that_could_fail()`` causes a database + In this example, even if ``generate_relationships()`` causes a database error by breaking an integrity constraint, you can execute queries in - ``do_more_stuff()``, and the changes from ``do_stuff()`` are still there. + ``add_children()``, and the changes from ``create_parent()`` are still + there. Note that any operations attempted in ``generate_relationships()`` + will already have been rolled back safely when ``handle_exception()`` is + called, so the exception handler can also operate on the database if + necessary. In order to guarantee atomicity, ``atomic`` disables some APIs. Attempting to commit, roll back, or change the autocommit state of the database connection within an ``atomic`` block will raise an exception. + ``atomic`` takes a ``using`` argument which should be the name of a + database. If this argument isn't provided, Django uses the ``"default"`` + database. + Under the hood, Django's transaction management code: - opens a transaction when entering the outermost ``atomic`` block; @@ -516,7 +520,7 @@ Transaction states The three functions described above relied on a concept called "transaction states". This mechanisme was deprecated in Django 1.6, but it's still -available until Django 1.8.. +available until Django 1.8. At any time, each database connection is in one of these two states: diff --git a/docs/topics/i18n/timezones.txt b/docs/topics/i18n/timezones.txt index 22a0edb073..e4a043b08f 100644 --- a/docs/topics/i18n/timezones.txt +++ b/docs/topics/i18n/timezones.txt @@ -189,6 +189,8 @@ Add the following middleware to :setting:`MIDDLEWARE_CLASSES`:: tz = request.session.get('django_timezone') if tz: timezone.activate(tz) + else: + timezone.deactivate() Create a view that can set the current timezone:: diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 2ce9d8d2bc..5b4ffea528 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -1437,7 +1437,9 @@ Here's example HTML template code: <select name="language"> {% get_language_info_list for LANGUAGES as languages %} {% for language in languages %} - <option value="{{ language.code }}">{{ language.name_local }} ({{ language.code }})</option> + <option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}> + {{ language.name_local }} ({{ language.code }}) + </option> {% endfor %} </select> <input type="submit" value="Go" /> diff --git a/docs/topics/serialization.txt b/docs/topics/serialization.txt index cb34117997..e88e16029e 100644 --- a/docs/topics/serialization.txt +++ b/docs/topics/serialization.txt @@ -203,7 +203,7 @@ Foreign keys and other relational fields are treated a little bit differently:: <!-- ... --> </object> -In this example we specify that the auth.Permission object with the PK 24 has +In this example we specify that the auth.Permission object with the PK 27 has a foreign key to the contenttypes.ContentType instance with the PK 9. ManyToMany-relations are exported for the model that binds them. For instance, diff --git a/docs/topics/testing/advanced.txt b/docs/topics/testing/advanced.txt index 5f2fa65bed..cefb770469 100644 --- a/docs/topics/testing/advanced.txt +++ b/docs/topics/testing/advanced.txt @@ -165,7 +165,7 @@ environment first. Django provides a convenience method to do this:: :func:`~django.test.utils.setup_test_environment` puts several Django features into modes that allow for repeatable testing, but does not create the test -databases; :func:`django.test.simple.DjangoTestSuiteRunner.setup_databases` +databases; :func:`django.test.runner.DiscoverRunner.setup_databases` takes care of that. The call to :func:`~django.test.utils.setup_test_environment` is made @@ -178,27 +178,27 @@ tests via Django's test runner. Using different testing frameworks ================================== -Clearly, :mod:`doctest` and :mod:`unittest` are not the only Python testing -frameworks. While Django doesn't provide explicit support for alternative -frameworks, it does provide a way to invoke tests constructed for an -alternative framework as if they were normal Django tests. +Clearly, :mod:`unittest` is not the only Python testing framework. While Django +doesn't provide explicit support for alternative frameworks, it does provide a +way to invoke tests constructed for an alternative framework as if they were +normal Django tests. When you run ``./manage.py test``, Django looks at the :setting:`TEST_RUNNER` setting to determine what to do. By default, :setting:`TEST_RUNNER` points to -``'django.test.simple.DjangoTestSuiteRunner'``. This class defines the default Django +``'django.test.runner.DiscoverRunner'``. This class defines the default Django testing behavior. This behavior involves: #. Performing global pre-test setup. -#. Looking for unit tests and doctests in the ``models.py`` and - ``tests.py`` files in each installed application. +#. Looking for tests in any file below the current directory whose name matches + the pattern ``test*.py``. #. Creating the test databases. #. Running ``syncdb`` to install models and initial data into the test databases. -#. Running the unit tests and doctests that are found. +#. Running the tests that were found. #. Destroying the test databases. @@ -215,15 +215,22 @@ process to satisfy whatever testing requirements you may have. Defining a test runner ---------------------- -.. currentmodule:: django.test.simple +.. currentmodule:: django.test.runner A test runner is a class defining a ``run_tests()`` method. Django ships -with a ``DjangoTestSuiteRunner`` class that defines the default Django +with a ``DiscoverRunner`` class that defines the default Django testing behavior. This class defines the ``run_tests()`` entry point, plus a selection of other methods that are used to by ``run_tests()`` to set up, execute and tear down the test suite. -.. class:: DjangoTestSuiteRunner(verbosity=1, interactive=True, failfast=True, **kwargs) +.. class:: DiscoverRunner(pattern='test*.py', top_level=None, verbosity=1, interactive=True, failfast=True, **kwargs) + + ``DiscoverRunner`` will search for tests in any file matching ``pattern``. + + ``top_level`` can be used to specify the directory containing your + top-level Python modules. Usually Django can figure this out automatically, + so it's not necessary to specify this option. If specified, it should + generally be the directory containing your ``manage.py`` file. ``verbosity`` determines the amount of notification and debug information that will be printed to the console; ``0`` is no output, ``1`` is normal @@ -238,11 +245,10 @@ set up, execute and tear down the test suite. If ``failfast`` is ``True``, the test suite will stop running after the first test failure is detected. - Django will, from time to time, extend the capabilities of - the test runner by adding new arguments. The ``**kwargs`` declaration - allows for this expansion. If you subclass ``DjangoTestSuiteRunner`` or - write your own test runner, ensure accept and handle the ``**kwargs`` - parameter. + Django may, from time to time, extend the capabilities of the test runner + by adding new arguments. The ``**kwargs`` declaration allows for this + expansion. If you subclass ``DiscoverRunner`` or write your own test + runner, ensure it accepts ``**kwargs``. Your test runner may also define additional command-line options. If you add an ``option_list`` attribute to a subclassed test runner, @@ -252,7 +258,7 @@ set up, execute and tear down the test suite. Attributes ~~~~~~~~~~ -.. attribute:: DjangoTestSuiteRunner.option_list +.. attribute:: DiscoverRunner.option_list This is the tuple of ``optparse`` options which will be fed into the management command's ``OptionParser`` for parsing arguments. See the @@ -261,20 +267,25 @@ Attributes Methods ~~~~~~~ -.. method:: DjangoTestSuiteRunner.run_tests(test_labels, extra_tests=None, **kwargs) +.. method:: DiscoverRunner.run_tests(test_labels, extra_tests=None, **kwargs) Run the test suite. ``test_labels`` is a list of strings describing the tests to be run. A test - label can take one of three forms: + label can take one of four forms: - * ``app.TestCase.test_method`` -- Run a single test method in a test + * ``path.to.test_module.TestCase.test_method`` -- Run a single test method + in a test case. + * ``path.to.test_module.TestCase`` -- Run all the test methods in a test case. - * ``app.TestCase`` -- Run all the test methods in a test case. - * ``app`` -- Search for and run all tests in the named application. + * ``path.to.module`` -- Search for and run all tests in the named Python + package or module. + * ``path/to/directory`` -- Search for and run all tests below the named + directory. - If ``test_labels`` has a value of ``None``, the test runner should run - search for tests in all the applications in :setting:`INSTALLED_APPS`. + If ``test_labels`` has a value of ``None``, the test runner will search for + tests in all files below the current directory whose names match its + ``pattern`` (see above). ``extra_tests`` is a list of extra ``TestCase`` instances to add to the suite that is executed by the test runner. These extra tests are run @@ -282,13 +293,13 @@ Methods This method should return the number of tests that failed. -.. method:: DjangoTestSuiteRunner.setup_test_environment(**kwargs) +.. method:: DiscoverRunner.setup_test_environment(**kwargs) Sets up the test environment by calling :func:`~django.test.utils.setup_test_environment` and setting :setting:`DEBUG` to ``False``. -.. method:: DjangoTestSuiteRunner.build_suite(test_labels, extra_tests=None, **kwargs) +.. method:: DiscoverRunner.build_suite(test_labels, extra_tests=None, **kwargs) Constructs a test suite that matches the test labels provided. @@ -309,7 +320,7 @@ Methods Returns a ``TestSuite`` instance ready to be run. -.. method:: DjangoTestSuiteRunner.setup_databases(**kwargs) +.. method:: DiscoverRunner.setup_databases(**kwargs) Creates the test databases. @@ -317,13 +328,13 @@ Methods that have been made. This data will be provided to the ``teardown_databases()`` function at the conclusion of testing. -.. method:: DjangoTestSuiteRunner.run_suite(suite, **kwargs) +.. method:: DiscoverRunner.run_suite(suite, **kwargs) Runs the test suite. Returns the result produced by the running the test suite. -.. method:: DjangoTestSuiteRunner.teardown_databases(old_config, **kwargs) +.. method:: DiscoverRunner.teardown_databases(old_config, **kwargs) Destroys the test databases, restoring pre-test conditions. @@ -331,11 +342,11 @@ Methods database configuration that need to be reversed. It is the return value of the ``setup_databases()`` method. -.. method:: DjangoTestSuiteRunner.teardown_test_environment(**kwargs) +.. method:: DiscoverRunner.teardown_test_environment(**kwargs) Restores the pre-test environment. -.. method:: DjangoTestSuiteRunner.suite_result(suite, result, **kwargs) +.. method:: DiscoverRunner.suite_result(suite, result, **kwargs) Computes and returns a return code based on a test suite, and the result from that test suite. @@ -402,7 +413,7 @@ can be useful during testing. ``old_database_name``. The ``verbosity`` argument has the same behavior as for - :class:`~django.test.simple.DjangoTestSuiteRunner`. + :class:`~django.test.runner.DiscoverRunner`. .. _topics-testing-code-coverage: diff --git a/docs/topics/testing/doctests.txt b/docs/topics/testing/doctests.txt deleted file mode 100644 index 5036e946a9..0000000000 --- a/docs/topics/testing/doctests.txt +++ /dev/null @@ -1,81 +0,0 @@ -=================== -Django and doctests -=================== - -Doctests use Python's standard :mod:`doctest` module, which searches your -docstrings for statements that resemble a session of the Python interactive -interpreter. A full explanation of how :mod:`doctest` works is out of the scope -of this document; read Python's official documentation for the details. - -.. admonition:: What's a **docstring**? - - A good explanation of docstrings (and some guidelines for using them - effectively) can be found in :pep:`257`: - - A docstring is a string literal that occurs as the first statement in - a module, function, class, or method definition. Such a docstring - becomes the ``__doc__`` special attribute of that object. - - For example, this function has a docstring that describes what it does:: - - def add_two(num): - "Return the result of adding two to the provided number." - return num + 2 - - Because tests often make great documentation, putting tests directly in - your docstrings is an effective way to document *and* test your code. - -As with unit tests, for a given Django application, the test runner looks for -doctests in two places: - -* The ``models.py`` file. You can define module-level doctests and/or a - doctest for individual models. It's common practice to put - application-level doctests in the module docstring and model-level - doctests in the model docstrings. - -* A file called ``tests.py`` in the application directory -- i.e., the - directory that holds ``models.py``. This file is a hook for any and all - doctests you want to write that aren't necessarily related to models. - -This example doctest is equivalent to the example given in the unittest section -above:: - - # models.py - - from django.db import models - - class Animal(models.Model): - """ - An animal that knows how to make noise - - # Create some animals - >>> lion = Animal.objects.create(name="lion", sound="roar") - >>> cat = Animal.objects.create(name="cat", sound="meow") - - # Make 'em speak - >>> lion.speak() - 'The lion says "roar"' - >>> cat.speak() - 'The cat says "meow"' - """ - name = models.CharField(max_length=20) - sound = models.CharField(max_length=20) - - def speak(self): - return 'The %s says "%s"' % (self.name, self.sound) - -When you :ref:`run your tests <running-tests>`, the test runner will find this -docstring, notice that portions of it look like an interactive Python session, -and execute those lines while checking that the results match. - -In the case of model tests, note that the test runner takes care of creating -its own test database. That is, any test that accesses a database -- by -creating and saving model instances, for example -- will not affect your -production database. However, the database is not refreshed between doctests, -so if your doctest requires a certain state you should consider flushing the -database or loading a fixture. (See the section on :ref:`fixtures -<topics-testing-fixtures>` for more on this.) Note that to use this feature, -the database user Django is connecting as must have ``CREATE DATABASE`` -rights. - -For more details about :mod:`doctest`, see the Python documentation. diff --git a/docs/topics/testing/index.txt b/docs/topics/testing/index.txt index 94e88bdf04..1a99a399b4 100644 --- a/docs/topics/testing/index.txt +++ b/docs/topics/testing/index.txt @@ -6,7 +6,6 @@ Testing in Django :hidden: overview - doctests advanced Automated testing is an extremely useful bug-killing tool for the modern @@ -29,83 +28,13 @@ it should be doing. The best part is, it's really easy. -Unit tests v. doctests -====================== - -There are two primary ways to write tests with Django, corresponding to the -two test frameworks that ship in the Python standard library. The two -frameworks are: - -* **Unit tests** -- tests that are expressed as methods on a Python class - that subclasses :class:`unittest.TestCase` or Django's customized - :class:`~django.test.TestCase`. For example:: - - import unittest - - class MyFuncTestCase(unittest.TestCase): - def testBasic(self): - a = ['larry', 'curly', 'moe'] - self.assertEqual(my_func(a, 0), 'larry') - self.assertEqual(my_func(a, 1), 'curly') - -* **Doctests** -- tests that are embedded in your functions' docstrings and - are written in a way that emulates a session of the Python interactive - interpreter. For example:: - - def my_func(a_list, idx): - """ - >>> a = ['larry', 'curly', 'moe'] - >>> my_func(a, 0) - 'larry' - >>> my_func(a, 1) - 'curly' - """ - return a_list[idx] - -Which should I use? -------------------- - -Because Django supports both of the standard Python test frameworks, it's up to -you and your tastes to decide which one to use. You can even decide to use -*both*. - -For developers new to testing, however, this choice can seem confusing. Here, -then, are a few key differences to help you decide which approach is right for -you: - -* If you've been using Python for a while, :mod:`doctest` will probably feel - more "pythonic". It's designed to make writing tests as easy as possible, - so it requires no overhead of writing classes or methods. You simply put - tests in docstrings. This has the added advantage of serving as - documentation (and correct documentation, at that!). However, while - doctests are good for some simple example code, they are not very good if - you want to produce either high quality, comprehensive tests or high - quality documentation. Test failures are often difficult to debug - as it can be unclear exactly why the test failed. Thus, doctests should - generally be avoided and used primarily for documentation examples only. - -* The :mod:`unittest` framework will probably feel very familiar to - developers coming from Java. :mod:`unittest` is inspired by Java's JUnit, - so you'll feel at home with this method if you've used JUnit or any test - framework inspired by JUnit. - -* If you need to write a bunch of tests that share similar code, then - you'll appreciate the :mod:`unittest` framework's organization around - classes and methods. This makes it easy to abstract common tasks into - common methods. The framework also supports explicit setup and/or cleanup - routines, which give you a high level of control over the environment - in which your test cases are run. - -* If you're writing tests for Django itself, you should use :mod:`unittest`. - Where to go from here ===================== -As unit tests are preferred in Django, we treat them in detail in the +The preferred way to write tests in Django is using the :mod:`unittest` module +built in to the Python standard library. This is covered in detail in the :doc:`overview` document. -:doc:`doctests` describes Django-specific features when using doctests. - -You can also use any *other* Python test framework, Django provides an API and +You can also use any *other* Python test framework; Django provides an API and tools for that kind of integration. They are described in the :ref:`other-testing-frameworks` section of :doc:`advanced`. diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index 9228a07b31..fc2b393898 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -17,7 +17,7 @@ Writing tests ============= Django's unit tests use a Python standard library module: :mod:`unittest`. This -module defines tests in class-based approach. +module defines tests using a class-based approach. .. admonition:: unittest2 @@ -46,46 +46,37 @@ module defines tests in class-based approach. .. _unittest2: http://pypi.python.org/pypi/unittest2 -For a given Django application, the test runner looks for unit tests in two -places: +Here is an example which subclasses from :class:`django.test.TestCase`, +which is a subclass of :class:`unittest.TestCase` that runs each test inside a +transaction to provide isolation:: -* The ``models.py`` file. The test runner looks for any subclass of - :class:`unittest.TestCase` in this module. - -* A file called ``tests.py`` in the application directory -- i.e., the - directory that holds ``models.py``. Again, the test runner looks for any - subclass of :class:`unittest.TestCase` in this module. - -Here is an example :class:`unittest.TestCase` subclass:: - - from django.utils import unittest + from django.test import TestCase from myapp.models import Animal - class AnimalTestCase(unittest.TestCase): + class AnimalTestCase(TestCase): def setUp(self): - self.lion = Animal(name="lion", sound="roar") - self.cat = Animal(name="cat", sound="meow") + Animal.objects.create(name="lion", sound="roar") + Animal.objects.create(name="cat", sound="meow") def test_animals_can_speak(self): """Animals that can speak are correctly identified""" - self.assertEqual(self.lion.speak(), 'The lion says "roar"') - self.assertEqual(self.cat.speak(), 'The cat says "meow"') + lion = Animal.objects.get(name="lion") + cat = Animal.objects.get(name="cat") + self.assertEqual(lion.speak(), 'The lion says "roar"') + self.assertEqual(cat.speak(), 'The cat says "meow"') -When you :ref:`run your tests <running-tests>`, the default behavior of the test -utility is to find all the test cases (that is, subclasses of -:class:`unittest.TestCase`) in ``models.py`` and ``tests.py``, automatically -build a test suite out of those test cases, and run that suite. +When you :ref:`run your tests <running-tests>`, the default behavior of the +test utility is to find all the test cases (that is, subclasses of +:class:`unittest.TestCase`) in any file whose name begins with ``test``, +automatically build a test suite out of those test cases, and run that suite. -There is a second way to define the test suite for a module: if you define a -function called ``suite()`` in either ``models.py`` or ``tests.py``, the -Django test runner will use that function to construct the test suite for that -module. This follows the `suggested organization`_ for unit tests. See the -Python documentation for more details on how to construct a complex test -suite. +.. versionchanged:: 1.6 -For more details about :mod:`unittest`, see the Python documentation. + Previously, Django's default test runner only discovered tests in + ``tests.py`` and ``models.py`` files within a Python package listed in + :setting:`INSTALLED_APPS`. -.. _suggested organization: http://docs.python.org/library/unittest.html#organizing-tests +For more details about :mod:`unittest`, see the Python documentation. .. warning:: @@ -93,14 +84,15 @@ For more details about :mod:`unittest`, see the Python documentation. be sure to create your test classes as subclasses of :class:`django.test.TestCase` rather than :class:`unittest.TestCase`. - In the example above, we instantiate some models but do not save them to - the database. Using :class:`unittest.TestCase` avoids the cost of running - each test in a transaction and flushing the database, but for most - applications the scope of tests you will be able to write this way will - be fairly limited, so it's easiest to use :class:`django.test.TestCase`. + Using :class:`unittest.TestCase` avoids the cost of running each test in a + transaction and flushing the database, but if your tests interact with + the database their behavior will vary based on the order that the test + runner executes them. This can lead to unit tests that pass when run in + isolation but fail when run in a suite. .. _running-tests: + Running tests ============= @@ -109,46 +101,47 @@ your project's ``manage.py`` utility:: $ ./manage.py test -By default, this will run every test in every application in -:setting:`INSTALLED_APPS`. If you only want to run tests for a particular -application, add the application name to the command line. For example, if your -:setting:`INSTALLED_APPS` contains ``'myproject.polls'`` and -``'myproject.animals'``, you can run the ``myproject.animals`` unit tests alone -with this command:: +Test discovery is based on the unittest module's `built-in test discovery`. By +default, this will discover tests in any file named "test*.py" under the +current working directory. - $ ./manage.py test animals +.. _built-in test discovery: http://docs.python.org/2/library/unittest.html#test-discovery -Note that we used ``animals``, not ``myproject.animals``. +You can specify particular tests to run by supplying any number of "test +labels" to ``./manage.py test``. Each test label can be a full Python dotted +path to a package, module, ``TestCase`` subclass, or test method. For instance:: -You can be even *more* specific by naming an individual test case. To -run a single test case in an application (for example, the -``AnimalTestCase`` described in the "Writing unit tests" section), add -the name of the test case to the label on the command line:: + # Run all the tests in the animals.tests module + $ ./manage.py test animals.tests + + # Run all the tests found within the 'animals' package + $ ./manage.py test animals - $ ./manage.py test animals.AnimalTestCase + # Run just one test case + $ ./manage.py test animals.tests.AnimalTestCase -And it gets even more granular than that! To run a *single* test -method inside a test case, add the name of the test method to the -label:: + # Run just one test method + $ ./manage.py test animals.tests.AnimalTestCase.test_animals_can_speak - $ ./manage.py test animals.AnimalTestCase.test_animals_can_speak +You can also provide a path to a directory to discover tests below that +directory:: -You can use the same rules if you're using doctests. Django will use the -test label as a path to the test method or class that you want to run. -If your ``models.py`` or ``tests.py`` has a function with a doctest, or -class with a class-level doctest, you can invoke that test by appending the -name of the test method or class to the label:: + $ ./manage.py test animals/ - $ ./manage.py test animals.classify +You can specify a custom filename pattern match using the ``-p`` (or +``--pattern``) option, if your test files are named differently from the +``test*.py`` pattern:: -If you want to run the doctest for a specific method in a class, add the -name of the method to the label:: + $ ./manage.py test --pattern="tests_*.py" - $ ./manage.py test animals.Classifier.run +.. versionchanged:: 1.6 -If you're using a ``__test__`` dictionary to specify doctests for a -module, Django will use the label as a key in the ``__test__`` dictionary -for defined in ``models.py`` and ``tests.py``. + Previously, test labels were in the form ``applabel``, + ``applabel.TestCase``, or ``applabel.TestCase.test_method``, rather than + being true Python dotted paths, and tests could only be found within + ``tests.py`` or ``models.py`` files within a Python package listed in + :setting:`INSTALLED_APPS`. The ``--pattern`` option and file paths as test + labels are new in 1.6. If you press ``Ctrl-C`` while the tests are running, the test runner will wait for the currently running test to complete and then exit gracefully. @@ -173,6 +166,7 @@ be reported, and any test databases created by the run will not be destroyed. flag areas in your code that aren't strictly wrong but could benefit from a better implementation. + .. _the-test-database: The test database @@ -290,25 +284,15 @@ If there are test failures, however, you'll see full details about which tests failed:: ====================================================================== - FAIL: Doctest: ellington.core.throttle.models + FAIL: test_was_published_recently_with_future_poll (polls.tests.PollMethodTests) ---------------------------------------------------------------------- Traceback (most recent call last): - File "/dev/django/test/doctest.py", line 2153, in runTest - raise self.failureException(self.format_failure(new.getvalue())) - AssertionError: Failed doctest test for myapp.models - File "/dev/myapp/models.py", line 0, in models - - ---------------------------------------------------------------------- - File "/dev/myapp/models.py", line 14, in myapp.models - Failed example: - throttle.check("actor A", "action one", limit=2, hours=1) - Expected: - True - Got: - False + File "/dev/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_poll + self.assertEqual(future_poll.was_published_recently(), False) + AssertionError: True != False ---------------------------------------------------------------------- - Ran 2 tests in 0.048s + Ran 1 test in 0.003s FAILED (failures=1) diff --git a/extras/csrf_migration_helper.py b/extras/csrf_migration_helper.py index 62a8fb92af..2a8853494c 100755 --- a/extras/csrf_migration_helper.py +++ b/extras/csrf_migration_helper.py @@ -143,7 +143,7 @@ def get_template_dirs(): """ from django.conf import settings dirs = set() - if ('django.template.loaders.filesystem.load_template_source' in settings.TEMPLATE_LOADERS + if ('django.template.loaders.filesystem.load_template_source' in settings.TEMPLATE_LOADERS or 'django.template.loaders.filesystem.Loader' in settings.TEMPLATE_LOADERS): dirs.update(map(unicode, settings.TEMPLATE_DIRS)) @@ -281,12 +281,10 @@ def search_python_list(python_code, template_names): Returns a list of tuples, each one being: (filename, line number) """ - retval = [] + retval = set() for tn in template_names: - retval.extend(search_python(python_code, tn)) - retval = list(set(retval)) - retval.sort() - return retval + retval.update(search_python(python_code, tn)) + return sorted(retval) def search_python(python_code, template_name): """ diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index 5071977b2d..c8986770f3 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -19,9 +19,9 @@ from django import conf, get_version from django.conf import settings from django.core.management import BaseCommand, CommandError from django.db import connection -from django.test.simple import DjangoTestSuiteRunner +from django.test.runner import DiscoverRunner from django.utils import unittest -from django.utils.encoding import force_str, force_text +from django.utils.encoding import force_text from django.utils._os import upath from django.utils.six import StringIO from django.test import LiveServerTestCase @@ -1090,7 +1090,7 @@ class ManageValidate(AdminScriptTestCase): self.assertOutput(out, '0 errors found') -class CustomTestRunner(DjangoTestSuiteRunner): +class CustomTestRunner(DiscoverRunner): def __init__(self, *args, **kwargs): assert 'liveserver' not in kwargs diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index e0000ff6b5..8e678a72b3 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -26,6 +26,7 @@ from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.models import Group, User, Permission, UNUSABLE_PASSWORD from django.contrib.contenttypes.models import ContentType from django.core.urlresolvers import reverse +from django.db import connection from django.forms.util import ErrorList from django.template.response import TemplateResponse from django.test import TestCase @@ -3605,7 +3606,13 @@ class UserAdminTest(TestCase): # Don't depend on a warm cache, see #17377. ContentType.objects.clear_cache() - with self.assertNumQueries(10): + + expected_queries = 10 + # Oracle doesn't implement "RELEASE SAVPOINT", see #20387. + if connection.vendor == 'oracle': + expected_queries -= 1 + + with self.assertNumQueries(expected_queries): response = self.client.get('/test_admin/admin/auth/user/%s/' % u.pk) self.assertEqual(response.status_code, 200) @@ -3643,7 +3650,12 @@ class GroupAdminTest(TestCase): def test_group_permission_performance(self): g = Group.objects.create(name="test_group") - with self.assertNumQueries(8): # instead of 259! + expected_queries = 8 + # Oracle doesn't implement "RELEASE SAVPOINT", see #20387. + if connection.vendor == 'oracle': + expected_queries -= 1 + + with self.assertNumQueries(expected_queries): response = self.client.get('/test_admin/admin/auth/group/%s/' % g.pk) self.assertEqual(response.status_code, 200) diff --git a/tests/backends/tests.py b/tests/backends/tests.py index a426926cef..81b2851403 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -367,14 +367,19 @@ class EscapingChecks(TestCase): All tests in this test case are also run with settings.DEBUG=True in EscapingChecksDebug test case, to also test CursorDebugWrapper. """ + + # For Oracle, when you want to select a value, you need to specify the + # special pseudo-table 'dual'; a select with no from clause is invalid. + bare_select_suffix = " FROM DUAL" if connection.vendor == 'oracle' else "" + def test_paramless_no_escaping(self): cursor = connection.cursor() - cursor.execute("SELECT '%s'") + cursor.execute("SELECT '%s'" + self.bare_select_suffix) self.assertEqual(cursor.fetchall()[0][0], '%s') def test_parameter_escaping(self): cursor = connection.cursor() - cursor.execute("SELECT '%%', %s", ('%d',)) + cursor.execute("SELECT '%%', %s" + self.bare_select_suffix, ('%d',)) self.assertEqual(cursor.fetchall()[0], ('%', '%d')) @unittest.skipUnless(connection.vendor == 'sqlite', diff --git a/tests/file_storage/tests.py b/tests/file_storage/tests.py index 5e6adee894..e4b71dba82 100644 --- a/tests/file_storage/tests.py +++ b/tests/file_storage/tests.py @@ -29,16 +29,10 @@ from django.utils._os import upath from django.test.utils import override_settings from servers.tests import LiveServerBase -# Try to import PIL in either of the two ways it can end up installed. -# Checking for the existence of Image is enough for CPython, but -# for PyPy, you need to check for the underlying modules try: - from PIL import Image, _imaging -except ImportError: - try: - import Image, _imaging - except ImportError: - Image = None + from django.utils.image import Image +except ImproperlyConfigured: + Image = None class GetStorageClassTests(SimpleTestCase): @@ -494,7 +488,7 @@ class DimensionClosingBug(unittest.TestCase): """ Test that get_image_dimensions() properly closes files (#8817) """ - @unittest.skipUnless(Image, "PIL not installed") + @unittest.skipUnless(Image, "Pillow/PIL not installed") def test_not_closing_of_files(self): """ Open files passed into get_image_dimensions() should stay opened. @@ -505,7 +499,7 @@ class DimensionClosingBug(unittest.TestCase): finally: self.assertTrue(not empty_io.closed) - @unittest.skipUnless(Image, "PIL not installed") + @unittest.skipUnless(Image, "Pillow/PIL not installed") def test_closing_of_filenames(self): """ get_image_dimensions() called with a filename should closed the file. @@ -542,7 +536,7 @@ class InconsistentGetImageDimensionsBug(unittest.TestCase): Test that get_image_dimensions() works properly after various calls using a file handler (#11158) """ - @unittest.skipUnless(Image, "PIL not installed") + @unittest.skipUnless(Image, "Pillow/PIL not installed") def test_multiple_calls(self): """ Multiple calls of get_image_dimensions() should return the same size. @@ -556,7 +550,7 @@ class InconsistentGetImageDimensionsBug(unittest.TestCase): self.assertEqual(image_pil.size, size_1) self.assertEqual(size_1, size_2) - @unittest.skipUnless(Image, "PIL not installed") + @unittest.skipUnless(Image, "Pillow/PIL not installed") def test_bug_19457(self): """ Regression test for #19457 diff --git a/tests/generic_relations/models.py b/tests/generic_relations/models.py index 34dc8d3a7d..2acb982b9a 100644 --- a/tests/generic_relations/models.py +++ b/tests/generic_relations/models.py @@ -98,3 +98,7 @@ class Gecko(models.Model): # To test fix for #11263 class Rock(Mineral): tags = generic.GenericRelation(TaggedItem) + +class ManualPK(models.Model): + id = models.IntegerField(primary_key=True) + tags = generic.GenericRelation(TaggedItem) diff --git a/tests/generic_relations/tests.py b/tests/generic_relations/tests.py index dd9dc506ca..79c7bc6184 100644 --- a/tests/generic_relations/tests.py +++ b/tests/generic_relations/tests.py @@ -6,7 +6,7 @@ from django.contrib.contenttypes.models import ContentType from django.test import TestCase from .models import (TaggedItem, ValuableTaggedItem, Comparison, Animal, - Vegetable, Mineral, Gecko, Rock) + Vegetable, Mineral, Gecko, Rock, ManualPK) class GenericRelationsTests(TestCase): @@ -75,12 +75,17 @@ class GenericRelationsTests(TestCase): "<Animal: Lion>", "<Animal: Platypus>" ]) + # Create another fatty tagged instance with different PK to ensure + # there is a content type restriction in the generated queries below. + mpk = ManualPK.objects.create(id=lion.pk) + mpk.tags.create(tag="fatty") self.assertQuerysetEqual(Animal.objects.filter(tags__tag='fatty'), [ "<Animal: Platypus>" ]) self.assertQuerysetEqual(Animal.objects.exclude(tags__tag='fatty'), [ "<Animal: Lion>" ]) + mpk.delete() # If you delete an object with an explicit Generic relation, the related # objects are deleted when the source object is deleted. diff --git a/tests/generic_views/tests.py b/tests/generic_views/tests.py deleted file mode 100644 index 244aa3c701..0000000000 --- a/tests/generic_views/tests.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import absolute_import - -from .test_base import (ViewTest, TemplateViewTest, RedirectViewTest, - GetContextDataTest) -from .test_dates import (ArchiveIndexViewTests, YearArchiveViewTests, - MonthArchiveViewTests, WeekArchiveViewTests, DayArchiveViewTests, - DateDetailViewTests) -from .test_detail import DetailViewTest -from .test_edit import (FormMixinTests, BasicFormTests, ModelFormMixinTests, - CreateViewTests, UpdateViewTests, DeleteViewTests) -from .test_list import ListViewTests diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index d26a201efa..1022c8d2f1 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -43,14 +43,8 @@ if can_run_compilation_tests: from .commands.compilation import (PoFileTests, PoFileContentsTests, PercentRenderingTests, MultipleLocaleCompilationTests, CompilationErrorHandling) -from .contenttypes.tests import ContentTypeTests from .forms import I18nForm, SelectDateForm, SelectDateWidget, CompanyForm from .models import Company, TestModel -from .patterns.tests import (URLRedirectWithoutTrailingSlashTests, - URLTranslationTests, URLDisabledTests, URLTagTests, URLTestCaseBase, - URLRedirectWithoutTrailingSlashSettingTests, URLNamespaceTests, - URLPrefixTests, URLResponseTests, URLRedirectTests, PathUnusedTests, - URLVaryAcceptLanguageTests) here = os.path.dirname(os.path.abspath(upath(__file__))) diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py index c3b2f7fccb..2d602d6412 100644 --- a/tests/model_fields/models.py +++ b/tests/model_fields/models.py @@ -1,17 +1,12 @@ import os import tempfile -# Try to import PIL in either of the two ways it can end up installed. -# Checking for the existence of Image is enough for CPython, but for PyPy, -# you need to check for the underlying modules. +from django.core.exceptions import ImproperlyConfigured try: - from PIL import Image, _imaging -except ImportError: - try: - import Image, _imaging - except ImportError: - Image = None + from django.utils.image import Image +except ImproperlyConfigured: + Image = None from django.core.files.storage import FileSystemStorage from django.db import models @@ -87,7 +82,7 @@ class VerboseNameField(models.Model): field9 = models.FileField("verbose field9", upload_to="unused") field10 = models.FilePathField("verbose field10") field11 = models.FloatField("verbose field11") - # Don't want to depend on PIL in this test + # Don't want to depend on Pillow/PIL in this test #field_image = models.ImageField("verbose field") field12 = models.IntegerField("verbose field12") field13 = models.IPAddressField("verbose field13") @@ -119,7 +114,7 @@ class Document(models.Model): ############################################################################### # ImageField -# If PIL available, do these tests. +# If Pillow/PIL available, do these tests. if Image: class TestImageFieldFile(ImageFieldFile): """ diff --git a/tests/model_fields/test_imagefield.py b/tests/model_fields/test_imagefield.py index df0215db3d..457892ddb8 100644 --- a/tests/model_fields/test_imagefield.py +++ b/tests/model_fields/test_imagefield.py @@ -3,20 +3,24 @@ from __future__ import absolute_import import os import shutil +from django.core.exceptions import ImproperlyConfigured from django.core.files import File from django.core.files.images import ImageFile from django.test import TestCase from django.utils._os import upath from django.utils.unittest import skipIf -from .models import Image +try: + from .models import Image +except ImproperlyConfigured: + Image = None if Image: from .models import (Person, PersonWithHeight, PersonWithHeightAndWidth, PersonDimensionsFirst, PersonTwoImages, TestImageFieldFile) from .models import temp_storage_dir else: - # PIL not available, create dummy classes (tests will be skipped anyway) + # Pillow not available, create dummy classes (tests will be skipped anyway) class Person(): pass PersonWithHeight = PersonWithHeightAndWidth = PersonDimensionsFirst = Person diff --git a/tests/model_fields/tests.py b/tests/model_fields/tests.py index 035a5c2ae3..e1e38d0ec7 100644 --- a/tests/model_fields/tests.py +++ b/tests/model_fields/tests.py @@ -15,11 +15,6 @@ from .models import (Foo, Bar, Whiz, BigD, BigS, Image, BigInt, Post, NullBooleanModel, BooleanModel, DataModel, Document, RenamedField, VerboseNameField, FksToBooleans) -from .test_imagefield import (ImageFieldTests, ImageFieldTwoDimensionsTests, - TwoImageFieldTests, ImageFieldNoDimensionsTests, - ImageFieldOneDimensionTests, ImageFieldDimensionsFirstTests, - ImageFieldUsingFileTests) - class BasicFieldTests(test.TestCase): def test_show_hidden_initial(self): diff --git a/tests/model_forms/models.py b/tests/model_forms/models.py index 25c780f1c2..a79d9b8c5b 100644 --- a/tests/model_forms/models.py +++ b/tests/model_forms/models.py @@ -11,6 +11,7 @@ from __future__ import unicode_literals import os import tempfile +from django.core.exceptions import ImproperlyConfigured from django.core.files.storage import FileSystemStorage from django.db import models from django.utils import six @@ -91,14 +92,7 @@ class TextFile(models.Model): return self.description try: - # If PIL is available, try testing ImageFields. Checking for the existence - # of Image is enough for CPython, but for PyPy, you need to check for the - # underlying modules If PIL is not available, ImageField tests are omitted. - # Try to import PIL in either of the two ways it can end up installed. - try: - from PIL import Image, _imaging - except ImportError: - import Image, _imaging + from django.utils.image import Image test_images = True @@ -137,7 +131,7 @@ try: def __str__(self): return self.description -except ImportError: +except ImproperlyConfigured: test_images = False @python_2_unicode_compatible diff --git a/tests/runtests.py b/tests/runtests.py index ee2488c4a1..a18324f676 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -10,16 +10,23 @@ from django import contrib from django.utils._os import upath from django.utils import six -CONTRIB_DIR_NAME = 'django.contrib' +CONTRIB_MODULE_PATH = 'django.contrib' TEST_TEMPLATE_DIR = 'templates' RUNTESTS_DIR = os.path.abspath(os.path.dirname(upath(__file__))) CONTRIB_DIR = os.path.dirname(upath(contrib.__file__)) + TEMP_DIR = tempfile.mkdtemp(prefix='django_') os.environ['DJANGO_TEST_TEMP_DIR'] = TEMP_DIR -SUBDIRS_TO_SKIP = ['templates'] +SUBDIRS_TO_SKIP = [ + 'templates', + 'test_discovery_sample', + 'test_discovery_sample2', + 'test_runner_deprecation_app', + 'test_runner_invalid_app', +] ALWAYS_INSTALLED_APPS = [ 'shared_models', @@ -40,17 +47,12 @@ ALWAYS_INSTALLED_APPS = [ 'staticfiles_tests.apps.no_label', ] -def geodjango(settings): - # All databases must have spatial backends to run GeoDjango tests. - spatial_dbs = [name for name, db_dict in settings.DATABASES.items() - if db_dict['ENGINE'].startswith('django.contrib.gis')] - return len(spatial_dbs) == len(settings.DATABASES) def get_test_modules(): modules = [] - for loc, dirpath in ( + for modpath, dirpath in ( (None, RUNTESTS_DIR), - (CONTRIB_DIR_NAME, CONTRIB_DIR)): + (CONTRIB_MODULE_PATH, CONTRIB_DIR)): for f in os.listdir(dirpath): if ('.' in f or # Python 3 byte code dirs (PEP 3147) @@ -59,9 +61,14 @@ def get_test_modules(): os.path.basename(f) in SUBDIRS_TO_SKIP or os.path.isfile(f)): continue - modules.append((loc, f)) + modules.append((modpath, f)) return modules +def get_installed(): + from django.db.models.loading import get_apps + return [app.__name__.rsplit('.', 1)[0] for app in get_apps()] + + def setup(verbosity, test_labels): from django.conf import settings from django.db.models.loading import get_apps, load_app @@ -95,25 +102,45 @@ def setup(verbosity, test_labels): get_apps() # Load all the test model apps. - test_labels_set = set([label.split('.')[0] for label in test_labels]) test_modules = get_test_modules() + # Reduce given test labels to just the app module path + test_labels_set = set() + for label in test_labels: + bits = label.split('.') + if bits[:2] == ['django', 'contrib']: + bits = bits[:3] + else: + bits = bits[:1] + test_labels_set.add('.'.join(bits)) + # If GeoDjango, then we'll want to add in the test applications # that are a part of its test suite. - if geodjango(settings): + from django.contrib.gis.tests.utils import HAS_SPATIAL_DB + if HAS_SPATIAL_DB: from django.contrib.gis.tests import geo_apps - test_modules.extend(geo_apps(runtests=True)) + test_modules.extend(geo_apps()) settings.INSTALLED_APPS.extend(['django.contrib.gis', 'django.contrib.sitemaps']) - for module_dir, module_name in test_modules: - if module_dir: - module_label = '.'.join([module_dir, module_name]) + for modpath, module_name in test_modules: + if modpath: + module_label = '.'.join([modpath, module_name]) else: module_label = module_name - # if the module was named on the command line, or + # if the module (or an ancestor) was named on the command line, or # no modules were named (i.e., run all), import - # this module and add it to the list to test. - if not test_labels or module_name in test_labels_set: + # this module and add it to INSTALLED_APPS. + if not test_labels: + module_found_in_labels = True + else: + match = lambda label: ( + module_label == label or # exact match + module_label.startswith(label + '.') # ancestor match + ) + + module_found_in_labels = any(match(l) for l in test_labels_set) + + if module_found_in_labels: if verbosity >= 2: print("Importing application %s" % module_name) mod = load_app(module_label) @@ -139,21 +166,19 @@ def django_tests(verbosity, interactive, failfast, test_labels): state = setup(verbosity, test_labels) extra_tests = [] - # If GeoDjango is used, add it's tests that aren't a part of - # an application (e.g., GEOS, GDAL, Distance objects). - if geodjango(settings) and (not test_labels or 'gis' in test_labels): - from django.contrib.gis.tests import geodjango_suite - extra_tests.append(geodjango_suite(apps=False)) - # Run the test suite, including the extra validation tests. from django.test.utils import get_runner if not hasattr(settings, 'TEST_RUNNER'): - settings.TEST_RUNNER = 'django.test.simple.DjangoTestSuiteRunner' + settings.TEST_RUNNER = 'django.test.runner.DiscoverRunner' TestRunner = get_runner(settings) - test_runner = TestRunner(verbosity=verbosity, interactive=interactive, - failfast=failfast) - failures = test_runner.run_tests(test_labels, extra_tests=extra_tests) + test_runner = TestRunner( + verbosity=verbosity, + interactive=interactive, + failfast=failfast, + ) + failures = test_runner.run_tests( + test_labels or get_installed(), extra_tests=extra_tests) teardown(state) return failures @@ -162,10 +187,7 @@ def django_tests(verbosity, interactive, failfast, test_labels): def bisect_tests(bisection_label, options, test_labels): state = setup(int(options.verbosity), test_labels) - if not test_labels: - # Get the full list of test labels to use for bisection - from django.db.models.loading import get_apps - test_labels = [app.__name__.split('.')[-2] for app in get_apps()] + test_labels = test_labels or get_installed() print('***** Bisecting test suite: %s' % ' '.join(test_labels)) @@ -222,11 +244,7 @@ def bisect_tests(bisection_label, options, test_labels): def paired_tests(paired_test, options, test_labels): state = setup(int(options.verbosity), test_labels) - if not test_labels: - print("") - # Get the full list of test labels to use for bisection - from django.db.models.loading import get_apps - test_labels = [app.__name__.split('.')[-2] for app in get_apps()] + test_labels = test_labels or get_installed() print('***** Trying paired execution') diff --git a/tests/serializers_regress/models.py b/tests/serializers_regress/models.py index 21fe073122..21a3448a8e 100644 --- a/tests/serializers_regress/models.py +++ b/tests/serializers_regress/models.py @@ -2,7 +2,7 @@ A test spanning all the capabilities of all the serializers. This class sets up a model for each model field type -(except for image types, because of the PIL dependency). +(except for image types, because of the Pillow/PIL dependency). """ from django.db import models diff --git a/tests/template_tests/tests.py b/tests/template_tests/tests.py index da1562d2d5..2aeaee9464 100644 --- a/tests/template_tests/tests.py +++ b/tests/template_tests/tests.py @@ -36,15 +36,6 @@ from django.utils.safestring import mark_safe from django.utils import six from django.utils.tzinfo import LocalTimezone -from .test_callables import CallableVariablesTests -from .test_context import ContextTests -from .test_custom import CustomTagTests, CustomFilterTests -from .test_parser import ParserTests -from .test_unicode import UnicodeTests -from .test_nodelist import NodelistTest, ErrorIndexTest -from .test_smartif import SmartIfTests -from .test_response import (TemplateResponseTest, CacheMiddlewareTest, - SimpleTemplateResponseTest, CustomURLConfTest) try: from .loaders import RenderToStringTest, EggLoaderTest diff --git a/tests/test_runner/deprecation_app/__init__.py b/tests/test_discovery_sample/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/tests/test_runner/deprecation_app/__init__.py +++ b/tests/test_discovery_sample/__init__.py diff --git a/tests/test_discovery_sample/pattern_tests.py b/tests/test_discovery_sample/pattern_tests.py new file mode 100644 index 0000000000..f62a233d75 --- /dev/null +++ b/tests/test_discovery_sample/pattern_tests.py @@ -0,0 +1,7 @@ +from unittest import TestCase + + +class Test(TestCase): + + def test_sample(self): + self.assertEqual(1, 1) diff --git a/tests/test_runner/invalid_app/models/__init__.py b/tests/test_discovery_sample/tests/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/tests/test_runner/invalid_app/models/__init__.py +++ b/tests/test_discovery_sample/tests/__init__.py diff --git a/tests/test_discovery_sample/tests/tests.py b/tests/test_discovery_sample/tests/tests.py new file mode 100644 index 0000000000..58bd4e7f1e --- /dev/null +++ b/tests/test_discovery_sample/tests/tests.py @@ -0,0 +1,7 @@ +from unittest import TestCase + + +class Test(TestCase): + + def test_sample(self): + pass diff --git a/tests/test_discovery_sample/tests_sample.py b/tests/test_discovery_sample/tests_sample.py new file mode 100644 index 0000000000..c541bc4cd6 --- /dev/null +++ b/tests/test_discovery_sample/tests_sample.py @@ -0,0 +1,22 @@ +from unittest import TestCase as UnitTestCase + +from django.test import TestCase as DjangoTestCase +from django.utils.unittest import TestCase as UT2TestCase + + +class TestVanillaUnittest(UnitTestCase): + + def test_sample(self): + self.assertEqual(1, 1) + + +class TestUnittest2(UT2TestCase): + + def test_sample(self): + self.assertEqual(1, 1) + + +class TestDjangoTestCase(DjangoTestCase): + + def test_sample(self): + self.assertEqual(1, 1) diff --git a/tests/test_discovery_sample2/__init__.py b/tests/test_discovery_sample2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/test_discovery_sample2/__init__.py diff --git a/tests/test_discovery_sample2/tests.py b/tests/test_discovery_sample2/tests.py new file mode 100644 index 0000000000..232054a9f1 --- /dev/null +++ b/tests/test_discovery_sample2/tests.py @@ -0,0 +1,7 @@ +from django.test import TestCase + + +class Test(TestCase): + + def test_sample(self): + pass diff --git a/tests/test_runner/test_discover_runner.py b/tests/test_runner/test_discover_runner.py new file mode 100644 index 0000000000..3dc364b351 --- /dev/null +++ b/tests/test_runner/test_discover_runner.py @@ -0,0 +1,68 @@ +from django.test import TestCase +from django.test.runner import DiscoverRunner + + +class DiscoverRunnerTest(TestCase): + + def test_dotted_test_module(self): + count = DiscoverRunner().build_suite( + ["test_discovery_sample.tests_sample"], + ).countTestCases() + + self.assertEqual(count, 3) + + def test_dotted_test_class_vanilla_unittest(self): + count = DiscoverRunner().build_suite( + ["test_discovery_sample.tests_sample.TestVanillaUnittest"], + ).countTestCases() + + self.assertEqual(count, 1) + + def test_dotted_test_class_unittest2(self): + count = DiscoverRunner().build_suite( + ["test_discovery_sample.tests_sample.TestUnittest2"], + ).countTestCases() + + self.assertEqual(count, 1) + + def test_dotted_test_class_django_testcase(self): + count = DiscoverRunner().build_suite( + ["test_discovery_sample.tests_sample.TestDjangoTestCase"], + ).countTestCases() + + self.assertEqual(count, 1) + + def test_dotted_test_method_vanilla_unittest(self): + count = DiscoverRunner().build_suite( + ["test_discovery_sample.tests_sample.TestVanillaUnittest.test_sample"], + ).countTestCases() + + self.assertEqual(count, 1) + + def test_dotted_test_method_unittest2(self): + count = DiscoverRunner().build_suite( + ["test_discovery_sample.tests_sample.TestUnittest2.test_sample"], + ).countTestCases() + + self.assertEqual(count, 1) + + def test_dotted_test_method_django_testcase(self): + count = DiscoverRunner().build_suite( + ["test_discovery_sample.tests_sample.TestDjangoTestCase.test_sample"], + ).countTestCases() + + self.assertEqual(count, 1) + + def test_pattern(self): + count = DiscoverRunner( + pattern="*_tests.py", + ).build_suite(["test_discovery_sample"]).countTestCases() + + self.assertEqual(count, 1) + + def test_file_path(self): + count = DiscoverRunner().build_suite( + ["test_discovery_sample/"], + ).countTestCases() + + self.assertEqual(count, 4) diff --git a/tests/test_runner/tests.py b/tests/test_runner/tests.py index 9f0cc794b7..95ccf0242a 100644 --- a/tests/test_runner/tests.py +++ b/tests/test_runner/tests.py @@ -9,7 +9,7 @@ from optparse import make_option from django.core.exceptions import ImproperlyConfigured from django.core.management import call_command from django import db -from django.test import simple, TransactionTestCase, skipUnlessDBFeature +from django.test import runner, TransactionTestCase, skipUnlessDBFeature from django.test.simple import DjangoTestSuiteRunner, get_tests from django.test.testcases import connections_support_transactions from django.utils import unittest @@ -20,7 +20,7 @@ from .models import Person TEST_APP_OK = 'test_runner.valid_app.models' -TEST_APP_ERROR = 'test_runner.invalid_app.models' +TEST_APP_ERROR = 'test_runner_invalid_app.models' class DependencyOrderingTests(unittest.TestCase): @@ -36,7 +36,7 @@ class DependencyOrderingTests(unittest.TestCase): 'bravo': ['charlie'], } - ordered = simple.dependency_ordered(raw, dependencies=dependencies) + ordered = runner.dependency_ordered(raw, dependencies=dependencies) ordered_sigs = [sig for sig,value in ordered] self.assertIn('s1', ordered_sigs) @@ -56,7 +56,7 @@ class DependencyOrderingTests(unittest.TestCase): 'bravo': ['charlie'], } - ordered = simple.dependency_ordered(raw, dependencies=dependencies) + ordered = runner.dependency_ordered(raw, dependencies=dependencies) ordered_sigs = [sig for sig,value in ordered] self.assertIn('s1', ordered_sigs) @@ -83,7 +83,7 @@ class DependencyOrderingTests(unittest.TestCase): 'delta': ['charlie'], } - ordered = simple.dependency_ordered(raw, dependencies=dependencies) + ordered = runner.dependency_ordered(raw, dependencies=dependencies) ordered_sigs = [sig for sig,aliases in ordered] self.assertIn('s1', ordered_sigs) @@ -110,7 +110,7 @@ class DependencyOrderingTests(unittest.TestCase): 'alpha': ['bravo'], } - self.assertRaises(ImproperlyConfigured, simple.dependency_ordered, raw, dependencies=dependencies) + self.assertRaises(ImproperlyConfigured, runner.dependency_ordered, raw, dependencies=dependencies) def test_own_alias_dependency(self): raw = [ @@ -121,7 +121,7 @@ class DependencyOrderingTests(unittest.TestCase): } with self.assertRaises(ImproperlyConfigured): - simple.dependency_ordered(raw, dependencies=dependencies) + runner.dependency_ordered(raw, dependencies=dependencies) # reordering aliases shouldn't matter raw = [ @@ -129,7 +129,7 @@ class DependencyOrderingTests(unittest.TestCase): ] with self.assertRaises(ImproperlyConfigured): - simple.dependency_ordered(raw, dependencies=dependencies) + runner.dependency_ordered(raw, dependencies=dependencies) class MockTestRunner(object): @@ -156,7 +156,7 @@ class ManageCommandTests(unittest.TestCase): testrunner='test_runner.NonExistentRunner') -class CustomOptionsTestRunner(simple.DjangoTestSuiteRunner): +class CustomOptionsTestRunner(runner.DiscoverRunner): option_list = ( make_option('--option_a','-a', action='store', dest='option_a', default='1'), make_option('--option_b','-b', action='store', dest='option_b', default='2'), @@ -289,15 +289,16 @@ class DummyBackendTest(unittest.TestCase): class DeprecationDisplayTest(AdminScriptTestCase): # tests for 19546 def setUp(self): - settings = {'INSTALLED_APPS': '("test_runner.deprecation_app",)', - 'DATABASES': '{"default": {"ENGINE":"django.db.backends.sqlite3", "NAME":":memory:"}}' } + settings = { + 'DATABASES': '{"default": {"ENGINE":"django.db.backends.sqlite3", "NAME":":memory:"}}' + } self.write_settings('settings.py', sdict=settings) def tearDown(self): self.remove_settings('settings.py') def test_runner_deprecation_verbosity_default(self): - args = ['test', '--settings=test_project.settings'] + args = ['test', '--settings=test_project.settings', 'test_runner_deprecation_app'] out, err = self.run_django_admin(args) self.assertIn("DeprecationWarning: warning from test", err) self.assertIn("DeprecationWarning: module-level warning from deprecation_app", err) diff --git a/tests/test_runner_deprecation_app/__init__.py b/tests/test_runner_deprecation_app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/test_runner_deprecation_app/__init__.py diff --git a/tests/test_runner/deprecation_app/models.py b/tests/test_runner_deprecation_app/models.py index 71a8362390..71a8362390 100644 --- a/tests/test_runner/deprecation_app/models.py +++ b/tests/test_runner_deprecation_app/models.py diff --git a/tests/test_runner/deprecation_app/tests.py b/tests/test_runner_deprecation_app/tests.py index 6dee0888ec..24d716f2bb 100644 --- a/tests/test_runner/deprecation_app/tests.py +++ b/tests/test_runner_deprecation_app/tests.py @@ -7,5 +7,3 @@ warnings.warn("module-level warning from deprecation_app", DeprecationWarning) class DummyTest(TestCase): def test_warn(self): warnings.warn("warning from test", DeprecationWarning) - - diff --git a/tests/test_runner/invalid_app/__init__.py b/tests/test_runner_invalid_app/__init__.py index 45efd37d3e..45efd37d3e 100644 --- a/tests/test_runner/invalid_app/__init__.py +++ b/tests/test_runner_invalid_app/__init__.py diff --git a/tests/test_runner_invalid_app/models/__init__.py b/tests/test_runner_invalid_app/models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/test_runner_invalid_app/models/__init__.py diff --git a/tests/test_runner/invalid_app/tests/__init__.py b/tests/test_runner_invalid_app/tests/__init__.py index b4ed71824b..b4ed71824b 100644 --- a/tests/test_runner/invalid_app/tests/__init__.py +++ b/tests/test_runner_invalid_app/tests/__init__.py diff --git a/tests/utils_tests/tests.py b/tests/utils_tests/tests.py deleted file mode 100644 index 8d7fda4800..0000000000 --- a/tests/utils_tests/tests.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -Tests for django.utils. -""" -from __future__ import absolute_import - -from .test_archive import TestBzip2Tar, TestGzipTar, TestTar, TestZip -from .test_baseconv import TestBaseConv -from .test_checksums import TestUtilsChecksums -from .test_crypto import TestUtilsCryptoMisc, TestUtilsCryptoPBKDF2 -from .test_datastructures import (DictWrapperTests, ImmutableListTests, - MergeDictTests, MultiValueDictTests, SortedDictTests) -from .test_dateformat import DateFormatTests -from .test_dateparse import DateParseTests -from .test_datetime_safe import DatetimeTests -from .test_decorators import DecoratorFromMiddlewareTests -from .test_encoding import TestEncodingUtils -from .test_feedgenerator import FeedgeneratorTest -from .test_functional import FunctionalTestCase -from .test_html import TestUtilsHtml -from .test_http import TestUtilsHttp, ETagProcessingTests, HttpDateProcessingTests -from .test_itercompat import TestIsIterator -from .test_ipv6 import TestUtilsIPv6 -from .test_jslex import JsToCForGettextTest, JsTokensTest -from .test_module_loading import (CustomLoader, DefaultLoader, EggLoader, - ModuleImportTestCase) -from .test_numberformat import TestNumberFormat -from .test_os_utils import SafeJoinTests -from .test_regex_helper import NormalizeTests -from .test_simplelazyobject import TestUtilsSimpleLazyObject -from .test_termcolors import TermColorTests -from .test_text import TestUtilsText -from .test_timesince import TimesinceTests -from .test_timezone import TimezoneTests -from .test_tzinfo import TzinfoTests diff --git a/tests/validation/tests.py b/tests/validation/tests.py index ead1b57981..b571e0c298 100644 --- a/tests/validation/tests.py +++ b/tests/validation/tests.py @@ -8,12 +8,6 @@ from . import ValidationTestCase from .models import (Author, Article, ModelToValidate, GenericIPAddressTestModel, GenericIPAddrUnpackUniqueTest) -# Import other tests for this package. -from .test_custom_messages import CustomMessagesTest -from .test_error_messages import ValidationMessagesTest -from .test_unique import GetUniqueCheckTests, PerformUniqueChecksTest -from .test_validators import TestModelsWithValidators - class BaseModelValidationTests(ValidationTestCase): |
