app/django/contrib/gis/management/commands/inspectdb.py
changeset 323 ff1a9aa48cfd
equal deleted inserted replaced
322:6641e941ef1e 323:ff1a9aa48cfd
       
     1 """
       
     2  This overrides the traditional `inspectdb` command so that geographic databases
       
     3  may be introspected.
       
     4 """
       
     5 
       
     6 from django.core.management.commands.inspectdb import Command as InspectCommand
       
     7 from django.contrib.gis.db.backend import SpatialBackend
       
     8 
       
     9 class Command(InspectCommand):
       
    10 
       
    11     # Mapping from lower-case OGC type to the corresponding GeoDjango field.
       
    12     geofield_mapping = {'point' : 'PointField',
       
    13                         'linestring' : 'LineStringField',
       
    14                         'polygon' : 'PolygonField',
       
    15                         'multipoint' : 'MultiPointField',
       
    16                         'multilinestring' : 'MultiLineStringField',
       
    17                         'multipolygon' : 'MultiPolygonField',
       
    18                         'geometrycollection' : 'GeometryCollectionField',
       
    19                         'geometry' : 'GeometryField',
       
    20                         }
       
    21 
       
    22     def geometry_columns(self):
       
    23         """
       
    24         Returns a datastructure of metadata information associated with the
       
    25         `geometry_columns` (or equivalent) table.
       
    26         """
       
    27         # The `geo_cols` is a dictionary data structure that holds information
       
    28         # about any geographic columns in the database.
       
    29         geo_cols = {}
       
    30         def add_col(table, column, coldata):
       
    31             if table in geo_cols:
       
    32                 # If table already has a geometry column.
       
    33                 geo_cols[table][column] = coldata
       
    34             else:
       
    35                 # Otherwise, create a dictionary indexed by column.
       
    36                 geo_cols[table] = { column : coldata }
       
    37 
       
    38         if SpatialBackend.name == 'postgis':
       
    39             # PostGIS holds all geographic column information in the `geometry_columns` table.
       
    40             from django.contrib.gis.models import GeometryColumns
       
    41             for geo_col in GeometryColumns.objects.all():
       
    42                 table = geo_col.f_table_name
       
    43                 column = geo_col.f_geometry_column
       
    44                 coldata = {'type' : geo_col.type, 'srid' : geo_col.srid, 'dim' : geo_col.coord_dimension}
       
    45                 add_col(table, column, coldata)
       
    46             return geo_cols
       
    47         elif SpatialBackend.name == 'mysql':
       
    48             # On MySQL have to get all table metadata before hand; this means walking through
       
    49             # each table and seeing if any column types are spatial.  Can't detect this with
       
    50             # `cursor.description` (what the introspection module does) because all spatial types
       
    51             # have the same integer type (255 for GEOMETRY).
       
    52             from django.db import connection
       
    53             cursor = connection.cursor()
       
    54             cursor.execute('SHOW TABLES')
       
    55             tables = cursor.fetchall();
       
    56             for table_tup in tables:
       
    57                 table = table_tup[0]
       
    58                 table_desc = cursor.execute('DESCRIBE `%s`' % table)
       
    59                 col_info = cursor.fetchall()
       
    60                 for column, typ, null, key, default, extra in col_info:
       
    61                     if typ in self.geofield_mapping: add_col(table, column, {'type' : typ})
       
    62             return geo_cols
       
    63         else:
       
    64             # TODO: Oracle (has incomplete `geometry_columns` -- have to parse
       
    65             #  SDO SQL to get specific type, SRID, and other information).
       
    66             raise NotImplementedError('Geographic database inspection not available.')
       
    67 
       
    68     def handle_inspection(self):
       
    69         "Overloaded from Django's version to handle geographic database tables."
       
    70         from django.db import connection
       
    71         import keyword
       
    72 
       
    73         geo_cols = self.geometry_columns()
       
    74 
       
    75         table2model = lambda table_name: table_name.title().replace('_', '')
       
    76 
       
    77         cursor = connection.cursor()
       
    78         yield "# This is an auto-generated Django model module."
       
    79         yield "# You'll have to do the following manually to clean this up:"
       
    80         yield "#     * Rearrange models' order"
       
    81         yield "#     * Make sure each model has one field with primary_key=True"
       
    82         yield "# Feel free to rename the models, but don't rename db_table values or field names."
       
    83         yield "#"
       
    84         yield "# Also note: You'll have to insert the output of 'django-admin.py sqlcustom [appname]'"
       
    85         yield "# into your database."
       
    86         yield ''
       
    87         yield 'from django.contrib.gis.db import models'
       
    88         yield ''
       
    89         for table_name in connection.introspection.get_table_list(cursor):
       
    90             # Getting the geographic table dictionary.
       
    91             geo_table = geo_cols.get(table_name, {})
       
    92 
       
    93             yield 'class %s(models.Model):' % table2model(table_name)
       
    94             try:
       
    95                 relations = connection.introspection.get_relations(cursor, table_name)
       
    96             except NotImplementedError:
       
    97                 relations = {}
       
    98             try:
       
    99                 indexes = connection.introspection.get_indexes(cursor, table_name)
       
   100             except NotImplementedError:
       
   101                 indexes = {}
       
   102             for i, row in enumerate(connection.introspection.get_table_description(cursor, table_name)):
       
   103                 att_name, iatt_name = row[0].lower(), row[0]
       
   104                 comment_notes = [] # Holds Field notes, to be displayed in a Python comment.
       
   105                 extra_params = {}  # Holds Field parameters such as 'db_column'.
       
   106 
       
   107                 if ' ' in att_name:
       
   108                     extra_params['db_column'] = att_name
       
   109                     att_name = att_name.replace(' ', '')
       
   110                     comment_notes.append('Field renamed to remove spaces.')
       
   111                 if keyword.iskeyword(att_name):
       
   112                     extra_params['db_column'] = att_name
       
   113                     att_name += '_field'
       
   114                     comment_notes.append('Field renamed because it was a Python reserved word.')
       
   115 
       
   116                 if i in relations:
       
   117                     rel_to = relations[i][1] == table_name and "'self'" or table2model(relations[i][1])
       
   118                     field_type = 'ForeignKey(%s' % rel_to
       
   119                     if att_name.endswith('_id'):
       
   120                         att_name = att_name[:-3]
       
   121                     else:
       
   122                         extra_params['db_column'] = att_name
       
   123                 else:
       
   124                     if iatt_name in geo_table:
       
   125                         ## Customization for Geographic Columns ##
       
   126                         geo_col = geo_table[iatt_name]
       
   127                         field_type = self.geofield_mapping[geo_col['type'].lower()]
       
   128                         # Adding extra keyword arguments for the SRID and dimension (if not defaults).
       
   129                         dim, srid = geo_col.get('dim', 2), geo_col.get('srid', 4326)
       
   130                         if dim != 2: extra_params['dim'] = dim
       
   131                         if srid != 4326: extra_params['srid'] = srid
       
   132                     else:
       
   133                         try:
       
   134                             field_type = connection.introspection.data_types_reverse[row[1]]
       
   135                         except KeyError:
       
   136                             field_type = 'TextField'
       
   137                             comment_notes.append('This field type is a guess.')
       
   138 
       
   139                     # This is a hook for data_types_reverse to return a tuple of
       
   140                     # (field_type, extra_params_dict).
       
   141                     if type(field_type) is tuple:
       
   142                         field_type, new_params = field_type
       
   143                         extra_params.update(new_params)
       
   144 
       
   145                     # Add max_length for all CharFields.
       
   146                     if field_type == 'CharField' and row[3]:
       
   147                         extra_params['max_length'] = row[3]
       
   148 
       
   149                     if field_type == 'DecimalField':
       
   150                         extra_params['max_digits'] = row[4]
       
   151                         extra_params['decimal_places'] = row[5]
       
   152 
       
   153                     # Add primary_key and unique, if necessary.
       
   154                     column_name = extra_params.get('db_column', att_name)
       
   155                     if column_name in indexes:
       
   156                         if indexes[column_name]['primary_key']:
       
   157                             extra_params['primary_key'] = True
       
   158                         elif indexes[column_name]['unique']:
       
   159                             extra_params['unique'] = True
       
   160 
       
   161                     field_type += '('
       
   162 
       
   163                 # Don't output 'id = meta.AutoField(primary_key=True)', because
       
   164                 # that's assumed if it doesn't exist.
       
   165                 if att_name == 'id' and field_type == 'AutoField(' and extra_params == {'primary_key': True}:
       
   166                     continue
       
   167 
       
   168                 # Add 'null' and 'blank', if the 'null_ok' flag was present in the
       
   169                 # table description.
       
   170                 if row[6]: # If it's NULL...
       
   171                     extra_params['blank'] = True
       
   172                     if not field_type in ('TextField(', 'CharField('):
       
   173                         extra_params['null'] = True
       
   174 
       
   175                 field_desc = '%s = models.%s' % (att_name, field_type)
       
   176                 if extra_params:
       
   177                     if not field_desc.endswith('('):
       
   178                         field_desc += ', '
       
   179                     field_desc += ', '.join(['%s=%r' % (k, v) for k, v in extra_params.items()])
       
   180                 field_desc += ')'
       
   181                 if comment_notes:
       
   182                     field_desc += ' # ' + ' '.join(comment_notes)
       
   183                 yield '    %s' % field_desc
       
   184             if table_name in geo_cols:
       
   185                 yield '    objects = models.GeoManager()'
       
   186             yield '    class Meta:'
       
   187             yield '        db_table = %r' % table_name
       
   188             yield ''