Fixed #15277 -- Cleaned up `ogrinspect` command, added tests and extended support beyond file-based OGR data sources. Thanks, willinoed for bug report and jpaulett for initial patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16845 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Justin Bronn 2011-09-17 19:54:52 +00:00
parent f97a574196
commit 3ac877840a
7 changed files with 155 additions and 40 deletions

View File

@ -1,15 +0,0 @@
from django.core.management.base import BaseCommand, CommandError
class ArgsCommand(BaseCommand):
"""
Command class for commands that take multiple arguments.
"""
args = '<arg arg ...>'
def handle(self, *args, **options):
if not args:
raise CommandError('Must provide the following arguments: %s' % self.args)
return self.handle_args(*args, **options)
def handle_args(self, *args, **options):
raise NotImplementedError()

View File

@ -1,7 +1,7 @@
import os import os
from optparse import make_option from optparse import make_option
from django.contrib.gis import gdal from django.contrib.gis import gdal
from django.contrib.gis.management.base import ArgsCommand, CommandError from django.core.management.base import LabelCommand, CommandError
def layer_option(option, opt, value, parser): def layer_option(option, opt, value, parser):
""" """
@ -17,7 +17,7 @@ def layer_option(option, opt, value, parser):
def list_option(option, opt, value, parser): def list_option(option, opt, value, parser):
""" """
Callback for `make_option` for `ogrinspect` keywords that require Callback for `make_option` for `ogrinspect` keywords that require
a string list. If the string is 'True'/'true' then the option a string list. If the string is 'True'/'true' then the option
value will be a boolean instead. value will be a boolean instead.
""" """
if value.lower() == 'true': if value.lower() == 'true':
@ -25,20 +25,20 @@ def list_option(option, opt, value, parser):
else: else:
dest = [s for s in value.split(',')] dest = [s for s in value.split(',')]
setattr(parser.values, option.dest, dest) setattr(parser.values, option.dest, dest)
class Command(ArgsCommand): class Command(LabelCommand):
help = ('Inspects the given OGR-compatible data source (e.g., a shapefile) and outputs\n' help = ('Inspects the given OGR-compatible data source (e.g., a shapefile) and outputs\n'
'a GeoDjango model with the given model name. For example:\n' 'a GeoDjango model with the given model name. For example:\n'
' ./manage.py ogrinspect zipcode.shp Zipcode') ' ./manage.py ogrinspect zipcode.shp Zipcode')
args = '[data_source] [model_name]' args = '[data_source] [model_name]'
option_list = ArgsCommand.option_list + ( option_list = LabelCommand.option_list + (
make_option('--blank', dest='blank', type='string', action='callback', make_option('--blank', dest='blank', type='string', action='callback',
callback=list_option, default=False, callback=list_option, default=False,
help='Use a comma separated list of OGR field names to add ' help='Use a comma separated list of OGR field names to add '
'the `blank=True` option to the field definition. Set with' 'the `blank=True` option to the field definition. Set with'
'`true` to apply to all applicable fields.'), '`true` to apply to all applicable fields.'),
make_option('--decimal', dest='decimal', type='string', action='callback', make_option('--decimal', dest='decimal', type='string', action='callback',
callback=list_option, default=False, callback=list_option, default=False,
help='Use a comma separated list of OGR float fields to ' help='Use a comma separated list of OGR float fields to '
'generate `DecimalField` instead of the default ' 'generate `DecimalField` instead of the default '
@ -46,7 +46,7 @@ class Command(ArgsCommand):
make_option('--geom-name', dest='geom_name', type='string', default='geom', make_option('--geom-name', dest='geom_name', type='string', default='geom',
help='Specifies the model name for the Geometry Field ' help='Specifies the model name for the Geometry Field '
'(defaults to `geom`)'), '(defaults to `geom`)'),
make_option('--layer', dest='layer_key', type='string', action='callback', make_option('--layer', dest='layer_key', type='string', action='callback',
callback=layer_option, default=0, callback=layer_option, default=0,
help='The key for specifying which layer in the OGR data ' help='The key for specifying which layer in the OGR data '
'source to use. Defaults to 0 (the first layer). May be ' 'source to use. Defaults to 0 (the first layer). May be '
@ -58,7 +58,7 @@ class Command(ArgsCommand):
make_option('--no-imports', action='store_false', dest='imports', default=True, make_option('--no-imports', action='store_false', dest='imports', default=True,
help='Do not include `from django.contrib.gis.db import models` ' help='Do not include `from django.contrib.gis.db import models` '
'statement.'), 'statement.'),
make_option('--null', dest='null', type='string', action='callback', make_option('--null', dest='null', type='string', action='callback',
callback=list_option, default=False, callback=list_option, default=False,
help='Use a comma separated list of OGR field names to add ' help='Use a comma separated list of OGR field names to add '
'the `null=True` option to the field definition. Set with' 'the `null=True` option to the field definition. Set with'
@ -72,7 +72,7 @@ class Command(ArgsCommand):
requires_model_validation = False requires_model_validation = False
def handle_args(self, *args, **options): def handle(self, *args, **options):
try: try:
data_source, model_name = args data_source, model_name = args
except ValueError: except ValueError:
@ -81,10 +81,6 @@ class Command(ArgsCommand):
if not gdal.HAS_GDAL: if not gdal.HAS_GDAL:
raise CommandError('GDAL is required to inspect geospatial data sources.') raise CommandError('GDAL is required to inspect geospatial data sources.')
# TODO: Support non file-based OGR datasources.
if not os.path.isfile(data_source):
raise CommandError('The given data source cannot be found: "%s"' % data_source)
# Removing options with `None` values. # Removing options with `None` values.
options = dict([(k, v) for k, v in options.items() if not v is None]) options = dict([(k, v) for k, v in options.items() if not v is None])
@ -97,8 +93,9 @@ class Command(ArgsCommand):
# Whether the user wants to generate the LayerMapping dictionary as well. # Whether the user wants to generate the LayerMapping dictionary as well.
show_mapping = options.pop('mapping', False) show_mapping = options.pop('mapping', False)
# Popping the verbosity global option, as it's not accepted by `_ogrinspect`. # Getting rid of settings that `_ogrinspect` doesn't like.
verbosity = options.pop('verbosity', False) verbosity = options.pop('verbosity', False)
settings = options.pop('settings', False)
# Returning the output of ogrinspect with the given arguments # Returning the output of ogrinspect with the given arguments
# and options. # and options.
@ -115,8 +112,8 @@ class Command(ArgsCommand):
# This extra legwork is so that the dictionary definition comes # This extra legwork is so that the dictionary definition comes
# out in the same order as the fields in the model definition. # out in the same order as the fields in the model definition.
rev_mapping = dict([(v, k) for k, v in mapping_dict.items()]) rev_mapping = dict([(v, k) for k, v in mapping_dict.items()])
output.extend(['', '# Auto-generated `LayerMapping` dictionary for %s model' % model_name, output.extend(['', '# Auto-generated `LayerMapping` dictionary for %s model' % model_name,
'%s_mapping = {' % model_name.lower()]) '%s_mapping = {' % model_name.lower()])
output.extend([" '%s' : '%s'," % (rev_mapping[ogr_fld], ogr_fld) for ogr_fld in ds[options['layer_key']].fields]) output.extend([" '%s' : '%s'," % (rev_mapping[ogr_fld], ogr_fld) for ogr_fld in ds[options['layer_key']].fields])
output.extend([" '%s' : '%s'," % (options['geom_name'], mapping_dict[options['geom_name']]), '}']) output.extend([" '%s' : '%s'," % (options['geom_name'], mapping_dict[options['geom_name']]), '}'])
return '\n'.join(output) return '\n'.join(output) + '\n'

View File

@ -29,15 +29,13 @@ def geo_apps(namespace=True, runtests=False):
# The following GeoDjango test apps depend on GDAL support. # The following GeoDjango test apps depend on GDAL support.
if HAS_GDAL: if HAS_GDAL:
# Geographic admin requires GDAL # Geographic admin, LayerMapping, and ogrinspect test apps
apps.append('geoadmin') # all require GDAL.
apps.extend(['geoadmin', 'layermap', 'inspectapp'])
# 3D apps use LayerMapping, which uses GDAL. # 3D apps use LayerMapping, which uses GDAL and require GEOS 3.1+.
if connection.ops.postgis and GEOS_PREPARE: if connection.ops.postgis and GEOS_PREPARE:
apps.append('geo3d') apps.append('geo3d')
apps.append('layermap')
if runtests: if runtests:
return [('django.contrib.gis.tests', app) for app in apps] return [('django.contrib.gis.tests', app) for app in apps]
elif namespace: elif namespace:

View File

@ -0,0 +1,13 @@
from django.contrib.gis.db import models
class AllOGRFields(models.Model):
f_decimal = models.FloatField()
f_float = models.FloatField()
f_int = models.IntegerField()
f_char = models.CharField(max_length=10)
f_date = models.DateField()
f_datetime = models.DateTimeField()
f_time = models.TimeField()
geom = models.PolygonField()
objects = models.GeoManager()

View File

@ -0,0 +1,122 @@
import os
from django.db import connections
from django.test import TestCase
from django.contrib.gis.gdal import Driver
from django.contrib.gis.geometry.test_data import TEST_DATA
from django.contrib.gis.utils.ogrinspect import ogrinspect
from models import AllOGRFields
class OGRInspectTest(TestCase):
def test_poly(self):
shp_file = os.path.join(TEST_DATA, 'test_poly', 'test_poly.shp')
model_def = ogrinspect(shp_file, 'MyModel')
expected = [
'# This is an auto-generated Django model module created by ogrinspect.',
'from django.contrib.gis.db import models',
'',
'class MyModel(models.Model):',
' float = models.FloatField()',
' int = models.FloatField()',
' str = models.CharField(max_length=80)',
' geom = models.PolygonField(srid=-1)',
' objects = models.GeoManager()',
]
self.assertEqual(model_def, '\n'.join(expected))
def test_date_field(self):
shp_file = os.path.join(TEST_DATA, 'cities', 'cities.shp')
model_def = ogrinspect(shp_file, 'City')
expected = [
'# This is an auto-generated Django model module created by ogrinspect.',
'from django.contrib.gis.db import models',
'',
'class City(models.Model):',
' name = models.CharField(max_length=80)',
' population = models.FloatField()',
' density = models.FloatField()',
' created = models.DateField()',
' geom = models.PointField(srid=-1)',
' objects = models.GeoManager()',
]
self.assertEqual(model_def, '\n'.join(expected))
def test_time_field(self):
# Only possible to test this on PostGIS at the momemnt. MySQL
# complains about permissions, and SpatiaLite/Oracle are
# insanely difficult to get support compiled in for in GDAL.
if not connections['default'].ops.postgis:
return
# Getting the database identifier used by OGR, if None returned
# GDAL does not have the support compiled in.
ogr_db = get_ogr_db_string()
if not ogr_db:
return
# writing shapefules via GDAL currently does not support writing OGRTime
# fields, so we need to actually use a database
model_def = ogrinspect(ogr_db, 'Measurement',
layer_key=AllOGRFields._meta.db_table,
decimal=['f_decimal'])
expected = [
'# This is an auto-generated Django model module created by ogrinspect.',
'from django.contrib.gis.db import models',
'',
'class Measurement(models.Model):',
' f_decimal = models.DecimalField(max_digits=0, decimal_places=0)',
' f_int = models.IntegerField()',
' f_datetime = models.DateTimeField()',
' f_time = models.TimeField()',
' f_float = models.FloatField()',
' f_char = models.CharField(max_length=10)',
' f_date = models.DateField()',
' geom = models.PolygonField()',
' objects = models.GeoManager()',
]
self.assertEqual(model_def, '\n'.join(expected))
def get_ogr_db_string():
# Construct the DB string that GDAL will use to inspect the database.
# GDAL will create its own connection to the database, so we re-use the
# connection settings from the Django test. This approach is a bit fragile
# and cannot work on any other database other than PostgreSQL at the moment.
db = connections.databases['default']
# Map from the django backend into the OGR driver name and database identifier
# http://www.gdal.org/ogr/ogr_formats.html
#
# TODO: Support Oracle (OCI), MySQL, and SpatiaLite.
drivers = {
'django.contrib.gis.db.backends.postgis': ('PostgreSQL', 'PG'),
}
drv_name, db_str = drivers[db['ENGINE']]
# Ensure that GDAL library has driver support for the database.
try:
Driver(drv_name)
except:
return None
# Build the params of the OGR database connection string
# TODO: connection strings are database-dependent, thus if
# we ever test other backends, this will need to change.
params = ["dbname='%s'" % db['NAME']]
def add(key, template):
value = db.get(key, None)
# Don't add the parameter if it is not in django's settings
if value:
params.append(template % value)
add('HOST', "host='%s'")
add('PORT', "port='%s'")
add('USER', "user='%s'")
add('PASSWORD', "password='%s'")
return '%s:%s' % (db_str, ' '.join(params))

View File

@ -8,7 +8,7 @@ Author: Travis Pinney, Dane Springmeyer, & Justin Bronn
from itertools import izip from itertools import izip
# Requires GDAL to use. # Requires GDAL to use.
from django.contrib.gis.gdal import DataSource from django.contrib.gis.gdal import DataSource
from django.contrib.gis.gdal.field import OFTDate, OFTDateTime, OFTInteger, OFTReal, OFTString from django.contrib.gis.gdal.field import OFTDate, OFTDateTime, OFTInteger, OFTReal, OFTString, OFTTime
def mapping(data_source, geom_name='geom', layer_key=0, multi_geom=False): def mapping(data_source, geom_name='geom', layer_key=0, multi_geom=False):
""" """
@ -189,7 +189,7 @@ def _ogrinspect(data_source, model_name, geom_name='geom', layer_key=0, srid=Non
yield ' %s = models.DateField(%s)' % (mfield, kwargs_str[2:]) yield ' %s = models.DateField(%s)' % (mfield, kwargs_str[2:])
elif field_type is OFTDateTime: elif field_type is OFTDateTime:
yield ' %s = models.DateTimeField(%s)' % (mfield, kwargs_str[2:]) yield ' %s = models.DateTimeField(%s)' % (mfield, kwargs_str[2:])
elif field_type is OFTDate: elif field_type is OFTTime:
yield ' %s = models.TimeField(%s)' % (mfield, kwargs_str[2:]) yield ' %s = models.TimeField(%s)' % (mfield, kwargs_str[2:])
else: else:
raise TypeError('Unknown field type %s in %s' % (field_type, mfield)) raise TypeError('Unknown field type %s in %s' % (field_type, mfield))