From 43329af2e3f99e6dc39c6ae390b055aa180554ad Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Fri, 9 Sep 2011 22:34:23 +0000 Subject: [PATCH] Fixed #16408 -- Fixed conversion of dates, and other problems with the SpatiaLite backend. git-svn-id: http://code.djangoproject.com/svn/django/trunk@16749 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- .../gis/db/backends/spatialite/base.py | 9 ++-- .../gis/db/backends/spatialite/compiler.py | 32 +++++++++++++ .../gis/db/backends/spatialite/creation.py | 48 +++++++++++++++++-- .../gis/db/backends/spatialite/operations.py | 2 +- django/contrib/gis/db/models/sql/compiler.py | 2 +- django/contrib/gis/tests/geoapp/models.py | 1 + .../contrib/gis/tests/geoapp/test_regress.py | 14 ++++-- 7 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 django/contrib/gis/db/backends/spatialite/compiler.py diff --git a/django/contrib/gis/db/backends/spatialite/base.py b/django/contrib/gis/db/backends/spatialite/base.py index 12561747b3..f23ec53cf8 100644 --- a/django/contrib/gis/db/backends/spatialite/base.py +++ b/django/contrib/gis/db/backends/spatialite/base.py @@ -2,16 +2,16 @@ from ctypes.util import find_library from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from django.db.backends.sqlite3.base import * from django.db.backends.sqlite3.base import ( - _sqlite_extract, _sqlite_date_trunc, _sqlite_regexp, - DatabaseWrapper as SqliteDatabaseWrapper) + _sqlite_extract, _sqlite_date_trunc, _sqlite_regexp, _sqlite_format_dtdelta, + connection_created, Database, DatabaseWrapper as SQLiteDatabaseWrapper, + SQLiteCursorWrapper) from django.contrib.gis.db.backends.spatialite.client import SpatiaLiteClient from django.contrib.gis.db.backends.spatialite.creation import SpatiaLiteCreation from django.contrib.gis.db.backends.spatialite.introspection import SpatiaLiteIntrospection from django.contrib.gis.db.backends.spatialite.operations import SpatiaLiteOperations -class DatabaseWrapper(SqliteDatabaseWrapper): +class DatabaseWrapper(SQLiteDatabaseWrapper): def __init__(self, *args, **kwargs): # Before we get too far, make sure pysqlite 2.5+ is installed. if Database.version_info < (2, 5, 0): @@ -52,6 +52,7 @@ class DatabaseWrapper(SqliteDatabaseWrapper): self.connection.create_function("django_extract", 2, _sqlite_extract) self.connection.create_function("django_date_trunc", 2, _sqlite_date_trunc) self.connection.create_function("regexp", 2, _sqlite_regexp) + self.connection.create_function("django_format_dtdelta", 5, _sqlite_format_dtdelta) connection_created.send(sender=self.__class__, connection=self) ## From here on, customized for GeoDjango ## diff --git a/django/contrib/gis/db/backends/spatialite/compiler.py b/django/contrib/gis/db/backends/spatialite/compiler.py new file mode 100644 index 0000000000..3f81ae68a1 --- /dev/null +++ b/django/contrib/gis/db/backends/spatialite/compiler.py @@ -0,0 +1,32 @@ +from django.db.backends.util import typecast_timestamp +from django.db.models.sql import compiler +from django.db.models.sql.constants import MULTI +from django.contrib.gis.db.models.sql.compiler import GeoSQLCompiler as BaseGeoSQLCompiler + +SQLCompiler = compiler.SQLCompiler + +class GeoSQLCompiler(BaseGeoSQLCompiler, SQLCompiler): + pass + +class SQLInsertCompiler(compiler.SQLInsertCompiler, GeoSQLCompiler): + pass + +class SQLDeleteCompiler(compiler.SQLDeleteCompiler, GeoSQLCompiler): + pass + +class SQLUpdateCompiler(compiler.SQLUpdateCompiler, GeoSQLCompiler): + pass + +class SQLAggregateCompiler(compiler.SQLAggregateCompiler, GeoSQLCompiler): + pass + +class SQLDateCompiler(compiler.SQLDateCompiler, GeoSQLCompiler): + """ + This is overridden for GeoDjango to properly cast date columns, see #16757. + """ + def results_iter(self): + offset = len(self.query.extra_select) + for rows in self.execute_sql(MULTI): + for row in rows: + date = typecast_timestamp(str(row[offset])) + yield date diff --git a/django/contrib/gis/db/backends/spatialite/creation.py b/django/contrib/gis/db/backends/spatialite/creation.py index 41baf53e7f..db96f46ef0 100644 --- a/django/contrib/gis/db/backends/spatialite/creation.py +++ b/django/contrib/gis/db/backends/spatialite/creation.py @@ -3,7 +3,6 @@ from django.conf import settings from django.core.cache import get_cache from django.core.cache.backends.db import BaseDatabaseCache from django.core.exceptions import ImproperlyConfigured -from django.core.management import call_command from django.db.backends.sqlite3.creation import DatabaseCreation class SpatiaLiteCreation(DatabaseCreation): @@ -16,26 +15,65 @@ class SpatiaLiteCreation(DatabaseCreation): This method is overloaded to load up the SpatiaLite initialization SQL prior to calling the `syncdb` command. """ - if verbosity >= 1: - print "Creating test database '%s'..." % self.connection.alias + # Don't import django.core.management if it isn't needed. + from django.core.management import call_command - test_database_name = self._create_test_db(verbosity, autoclobber) + test_database_name = self._get_test_db_name() + + if verbosity >= 1: + test_db_repr = '' + if verbosity >= 2: + test_db_repr = " ('%s')" % test_database_name + print "Creating test database for alias '%s'%s..." % (self.connection.alias, test_db_repr) + + self._create_test_db(verbosity, autoclobber) self.connection.close() - self.connection.settings_dict["NAME"] = test_database_name + # Confirm the feature set of the test database self.connection.features.confirm() + # Need to load the SpatiaLite initialization SQL before running `syncdb`. self.load_spatialite_sql() call_command('syncdb', verbosity=verbosity, interactive=False, database=self.connection.alias) + # Report syncdb messages at one level lower than that requested. + # This ensures we don't get flooded with messages during testing + # (unless you really ask to be flooded) + call_command('syncdb', + verbosity=max(verbosity - 1, 0), + interactive=False, + database=self.connection.alias, + load_initial_data=False) + + # We need to then do a flush to ensure that any data installed by + # custom SQL has been removed. The only test data should come from + # test fixtures, or autogenerated from post_syncdb triggers. + # This has the side effect of loading initial data (which was + # intentionally skipped in the syncdb). + call_command('flush', + verbosity=max(verbosity - 1, 0), + interactive=False, + database=self.connection.alias) + + # One effect of calling syncdb followed by flush is that the id of the + # default site may or may not be 1, depending on how the sequence was + # reset. If the sites app is loaded, then we coerce it. + from django.db.models import get_model + Site = get_model('sites', 'Site') + if Site is not None and Site.objects.using(self.connection.alias).count() == 1: + Site.objects.using(self.connection.alias).update(id=settings.SITE_ID) + + from django.core.cache import get_cache + from django.core.cache.backends.db import BaseDatabaseCache for cache_alias in settings.CACHES: cache = get_cache(cache_alias) if isinstance(cache, BaseDatabaseCache): from django.db import router if router.allow_syncdb(self.connection.alias, cache.cache_model_class): call_command('createcachetable', cache._table, database=self.connection.alias) + # Get a cursor (even though we don't need one yet). This has # the side effect of initializing the test database. cursor = self.connection.cursor() diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py index 449c527187..2ba54e5228 100644 --- a/django/contrib/gis/db/backends/spatialite/operations.py +++ b/django/contrib/gis/db/backends/spatialite/operations.py @@ -48,7 +48,7 @@ def get_dist_ops(operator): return (SpatiaLiteDistance(operator),) class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): - compiler_module = 'django.contrib.gis.db.models.sql.compiler' + compiler_module = 'django.contrib.gis.db.backends.spatialite.compiler' name = 'spatialite' spatialite = True version_regex = re.compile(r'^(?P\d)\.(?P\d)\.(?P\d+)') diff --git a/django/contrib/gis/db/models/sql/compiler.py b/django/contrib/gis/db/models/sql/compiler.py index dea0fd3e83..782ce78368 100644 --- a/django/contrib/gis/db/models/sql/compiler.py +++ b/django/contrib/gis/db/models/sql/compiler.py @@ -202,7 +202,7 @@ class GeoSQLCompiler(compiler.SQLCompiler): #### Routines unique to GeoQuery #### def get_extra_select_format(self, alias): sel_fmt = '%s' - if alias in self.query.custom_select: + if hasattr(self.query, 'custom_select') and alias in self.query.custom_select: sel_fmt = sel_fmt % self.query.custom_select[alias] return sel_fmt diff --git a/django/contrib/gis/tests/geoapp/models.py b/django/contrib/gis/tests/geoapp/models.py index 89027eedfb..1ac830953a 100644 --- a/django/contrib/gis/tests/geoapp/models.py +++ b/django/contrib/gis/tests/geoapp/models.py @@ -19,6 +19,7 @@ class City(models.Model): # This is an inherited model from City class PennsylvaniaCity(City): county = models.CharField(max_length=30) + founded = models.DateTimeField(null=True) objects = models.GeoManager() # TODO: This should be implicitly inherited. class State(models.Model): diff --git a/django/contrib/gis/tests/geoapp/test_regress.py b/django/contrib/gis/tests/geoapp/test_regress.py index 6b914da019..02b1ff08eb 100644 --- a/django/contrib/gis/tests/geoapp/test_regress.py +++ b/django/contrib/gis/tests/geoapp/test_regress.py @@ -1,9 +1,10 @@ -import unittest +from datetime import datetime from django.contrib.gis.tests.utils import no_mysql, no_spatialite from django.contrib.gis.shortcuts import render_to_kmz -from models import City +from django.test import TestCase +from models import City, PennsylvaniaCity -class GeoRegressionTests(unittest.TestCase): +class GeoRegressionTests(TestCase): def test01_update(self): "Testing GeoQuerySet.update(), see #10411." @@ -35,3 +36,10 @@ class GeoRegressionTests(unittest.TestCase): extent = City.objects.filter(name='Pueblo').extent() for ref_val, val in zip(ref_ext, extent): self.assertAlmostEqual(ref_val, val, 4) + + def test04_unicode_date(self): + "Testing dates are converted properly, even on SpatiaLite, see #16408." + founded = datetime(1857, 5, 23) + mansfield = PennsylvaniaCity.objects.create(name='Mansfield', county='Tioga', point='POINT(-77.071445 41.823881)', + founded=founded) + self.assertEqual(founded, PennsylvaniaCity.objects.dates('founded', 'day')[0])