diff --git a/django/contrib/gis/management/base.py b/django/contrib/gis/management/base.py new file mode 100644 index 0000000000..c998063af8 --- /dev/null +++ b/django/contrib/gis/management/base.py @@ -0,0 +1,15 @@ +from django.core.management.base import BaseCommand, CommandError + +class ArgsCommand(BaseCommand): + """ + Command class for commands that take multiple arguments. + """ + args = '' + + 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() diff --git a/django/contrib/gis/management/commands/ogrinspect.py b/django/contrib/gis/management/commands/ogrinspect.py new file mode 100644 index 0000000000..aa4f41831e --- /dev/null +++ b/django/contrib/gis/management/commands/ogrinspect.py @@ -0,0 +1,119 @@ +import os, sys +from optparse import make_option +from django.contrib.gis import gdal +from django.contrib.gis.management.base import ArgsCommand, CommandError + +def layer_option(option, opt, value, parser): + """ + Callback for `make_option` for the `ogrinspect` `layer_key` + keyword option which may be an integer or a string. + """ + try: + dest = int(value) + except ValueError: + dest = value + setattr(parser.values, option.dest, dest) + +def list_option(option, opt, value, parser): + """ + Callback for `make_option` for `ogrinspect` keywords that require + a string list. If the string is 'True'/'true' then the option + value will be a boolean instead. + """ + if value.lower() == 'true': + dest = True + else: + dest = [s for s in value.split(',')] + setattr(parser.values, option.dest, dest) + +class Command(ArgsCommand): + 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' + ' ./manage.py ogrinspect zipcode.shp Zipcode') + args = '[data_source] [model_name]' + + option_list = ArgsCommand.option_list + ( + make_option('--blank', dest='blank', type='string', action='callback', + callback=list_option, default=False, + help='Use a comma separated list of OGR field names to add ' + 'the `blank=True` option to the field definition. Set with' + '`true` to apply to all applicable fields.'), + make_option('--decimal', dest='decimal', type='string', action='callback', + callback=list_option, default=False, + help='Use a comma separated list of OGR float fields to ' + 'generate `DecimalField` instead of the default ' + '`FloatField`. Set to `true` to apply to all OGR float fields.'), + make_option('--geom-name', dest='geom_name', type='string', default='geom', + help='Specifies the model name for the Geometry Field ' + '(defaults to `geom`)'), + make_option('--layer', dest='layer_key', type='string', action='callback', + callback=layer_option, default=0, + help='The key for specifying which layer in the OGR data ' + 'source to use. Defaults to 0 (the first layer). May be ' + 'an integer or a string identifier for the layer.'), + make_option('--multi-geom', action='store_true', dest='multi_geom', default=False, + help='Treat the geometry in the data source as a geometry collection.'), + make_option('--name-field', dest='name_field', + help='Specifies a field name to return for the `__unicode__` function.'), + make_option('--no-imports', action='store_false', dest='imports', default=True, + help='Do not include `from django.contrib.gis.db import models` ' + 'statement.'), + make_option('--null', dest='null', type='string', action='callback', + callback=list_option, default=False, + help='Use a comma separated list of OGR field names to add ' + 'the `null=True` option to the field definition. Set with' + '`true` to apply to all applicable fields.'), + make_option('--srid', dest='srid', + help='The SRID to use for the Geometry Field. If it can be ' + 'determined, the SRID of the data source is used.'), + make_option('--mapping', action='store_true', dest='mapping', + help='Generate mapping dictionary for use with `LayerMapping`.') + ) + + requires_model_validation = False + + def handle_args(self, *args, **options): + try: + data_source, model_name = args + except ValueError: + raise CommandError('Invalid arguments, must provide: %s' % self.args) + + if not gdal.HAS_GDAL: + 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. + options = dict([(k, v) for k, v in options.items() if not v is None]) + + # Getting the OGR DataSource from the string parameter. + try: + ds = gdal.DataSource(data_source) + except gdal.OGRException, msg: + raise CommandError(msg) + + # Whether the user wants to generate the LayerMapping dictionary as well. + show_mapping = options.pop('mapping', False) + + # Returning the output of ogrinspect with the given arguments + # and options. + from django.contrib.gis.utils.ogrinspect import _ogrinspect, mapping + output = [s for s in _ogrinspect(ds, model_name, **options)] + if show_mapping: + # Constructing the keyword arguments for `mapping`, and + # calling it on the data source. + kwargs = {'geom_name' : options['geom_name'], + 'layer_key' : options['layer_key'], + 'multi_geom' : options['multi_geom'], + } + mapping_dict = mapping(ds, **kwargs) + # This extra legwork is so that the dictionary definition comes + # out in the same order as the fields in the model definition. + rev_mapping = dict([(v, k) for k, v in mapping_dict.items()]) + output.extend(['', '# Auto-generated `LayerMapping` dictionary for %s model' % model_name, + '%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'," % (options['geom_name'], mapping_dict[options['geom_name']]), '}']) + return '\n'.join(output) diff --git a/django/contrib/gis/utils/ogrinspect.py b/django/contrib/gis/utils/ogrinspect.py index 2eca58da40..c0c3c40a69 100644 --- a/django/contrib/gis/utils/ogrinspect.py +++ b/django/contrib/gis/utils/ogrinspect.py @@ -10,7 +10,7 @@ from itertools import izip from django.contrib.gis.gdal import DataSource from django.contrib.gis.gdal.field import OFTDate, OFTDateTime, OFTInteger, OFTReal, OFTString, OFTTime -def mapping(data_source, geom_name='geom', layer_key=0): +def mapping(data_source, geom_name='geom', layer_key=0, multi_geom=False): """ Given a DataSource, generates a dictionary that may be used for invoking the LayerMapping utility. @@ -21,6 +21,8 @@ def mapping(data_source, geom_name='geom', layer_key=0): `layer_key` => The key for specifying which layer in the DataSource to use; defaults to 0 (the first layer). May be an integer index or a string identifier for the layer. + + `multi_geom` => Boolean (default: False) - specify as multigeometry. """ if isinstance(data_source, basestring): # Instantiating the DataSource from the string. @@ -39,8 +41,9 @@ def mapping(data_source, geom_name='geom', layer_key=0): if mfield[-1:] == '_': mfield += 'field' _mapping[mfield] = field gtype = data_source[layer_key].geom_type - _mapping[geom_name] = str(gtype).upper() - + if multi_geom and gtype.num in (1, 2, 3): prefix = 'MULTI' + else: prefix = '' + _mapping[geom_name] = prefix + str(gtype).upper() return _mapping def ogrinspect(*args, **kwargs):