|
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 '' |