2009-12-22 23:18:51 +08:00
|
|
|
import keyword
|
|
|
|
from optparse import make_option
|
|
|
|
|
2007-08-16 22:34:01 +08:00
|
|
|
from django.core.management.base import NoArgsCommand, CommandError
|
2009-12-22 23:18:51 +08:00
|
|
|
from django.db import connections, DEFAULT_DB_ALIAS
|
2007-08-16 14:06:55 +08:00
|
|
|
|
2007-08-16 22:34:01 +08:00
|
|
|
class Command(NoArgsCommand):
|
2007-08-16 14:06:55 +08:00
|
|
|
help = "Introspects the database tables in the given database and outputs a Django model module."
|
|
|
|
|
2009-12-22 23:18:51 +08:00
|
|
|
option_list = NoArgsCommand.option_list + (
|
|
|
|
make_option('--database', action='store', dest='database',
|
|
|
|
default=DEFAULT_DB_ALIAS, help='Nominates a database to '
|
|
|
|
'introspect. Defaults to using the "default" database.'),
|
|
|
|
)
|
|
|
|
|
2007-08-16 14:06:55 +08:00
|
|
|
requires_model_validation = False
|
|
|
|
|
2010-01-19 05:02:47 +08:00
|
|
|
db_module = 'django.db'
|
|
|
|
|
2007-08-16 22:34:01 +08:00
|
|
|
def handle_noargs(self, **options):
|
2007-08-16 14:06:55 +08:00
|
|
|
try:
|
2009-12-22 23:18:51 +08:00
|
|
|
for line in self.handle_inspection(options):
|
2011-01-24 22:58:05 +08:00
|
|
|
self.stdout.write("%s\n" % line)
|
2007-08-16 14:06:55 +08:00
|
|
|
except NotImplementedError:
|
|
|
|
raise CommandError("Database inspection isn't supported for the currently selected database backend.")
|
|
|
|
|
2009-12-22 23:18:51 +08:00
|
|
|
def handle_inspection(self, options):
|
2011-10-23 11:43:43 +08:00
|
|
|
connection = connections[options.get('database')]
|
2012-06-02 07:58:53 +08:00
|
|
|
# 'table_name_filter' is a stealth option
|
|
|
|
table_name_filter = options.get('table_name_filter')
|
2007-08-16 14:06:55 +08:00
|
|
|
|
2008-08-16 13:03:40 +08:00
|
|
|
table2model = lambda table_name: table_name.title().replace('_', '').replace(' ', '').replace('-', '')
|
2007-08-16 14:06:55 +08:00
|
|
|
|
|
|
|
cursor = connection.cursor()
|
|
|
|
yield "# This is an auto-generated Django model module."
|
|
|
|
yield "# You'll have to do the following manually to clean this up:"
|
|
|
|
yield "# * Rearrange models' order"
|
|
|
|
yield "# * Make sure each model has one field with primary_key=True"
|
|
|
|
yield "# Feel free to rename the models, but don't rename db_table values or field names."
|
|
|
|
yield "#"
|
|
|
|
yield "# Also note: You'll have to insert the output of 'django-admin.py sqlcustom [appname]'"
|
|
|
|
yield "# into your database."
|
|
|
|
yield ''
|
2010-01-19 05:02:47 +08:00
|
|
|
yield 'from %s import models' % self.db_module
|
2007-08-16 14:06:55 +08:00
|
|
|
yield ''
|
2012-04-27 14:56:31 +08:00
|
|
|
known_models = []
|
2012-04-29 07:11:55 +08:00
|
|
|
for table_name in connection.introspection.table_names(cursor):
|
2012-06-02 07:58:53 +08:00
|
|
|
if table_name_filter is not None and callable(table_name_filter):
|
|
|
|
if not table_name_filter(table_name):
|
|
|
|
continue
|
2007-08-16 14:06:55 +08:00
|
|
|
yield 'class %s(models.Model):' % table2model(table_name)
|
2012-04-27 14:56:31 +08:00
|
|
|
known_models.append(table2model(table_name))
|
2007-08-16 14:06:55 +08:00
|
|
|
try:
|
2008-08-11 20:11:25 +08:00
|
|
|
relations = connection.introspection.get_relations(cursor, table_name)
|
2007-08-16 14:06:55 +08:00
|
|
|
except NotImplementedError:
|
|
|
|
relations = {}
|
|
|
|
try:
|
2008-08-11 20:11:25 +08:00
|
|
|
indexes = connection.introspection.get_indexes(cursor, table_name)
|
2007-08-16 14:06:55 +08:00
|
|
|
except NotImplementedError:
|
|
|
|
indexes = {}
|
2008-08-11 20:11:25 +08:00
|
|
|
for i, row in enumerate(connection.introspection.get_table_description(cursor, table_name)):
|
2008-09-17 13:12:53 +08:00
|
|
|
column_name = row[0]
|
|
|
|
att_name = column_name.lower()
|
2007-08-16 14:06:55 +08:00
|
|
|
comment_notes = [] # Holds Field notes, to be displayed in a Python comment.
|
|
|
|
extra_params = {} # Holds Field parameters such as 'db_column'.
|
|
|
|
|
2008-09-17 13:12:53 +08:00
|
|
|
# If the column name can't be used verbatim as a Python
|
|
|
|
# attribute, set the "db_column" for this Field.
|
|
|
|
if ' ' in att_name or '-' in att_name or keyword.iskeyword(att_name) or column_name != att_name:
|
|
|
|
extra_params['db_column'] = column_name
|
|
|
|
|
2012-02-05 15:51:37 +08:00
|
|
|
# Add primary_key and unique, if necessary.
|
|
|
|
if column_name in indexes:
|
|
|
|
if indexes[column_name]['primary_key']:
|
|
|
|
extra_params['primary_key'] = True
|
|
|
|
elif indexes[column_name]['unique']:
|
|
|
|
extra_params['unique'] = True
|
|
|
|
|
2008-09-17 13:12:53 +08:00
|
|
|
# Modify the field name to make it Python-compatible.
|
2008-08-16 13:03:40 +08:00
|
|
|
if ' ' in att_name:
|
|
|
|
att_name = att_name.replace(' ', '_')
|
2007-08-16 14:06:55 +08:00
|
|
|
comment_notes.append('Field renamed to remove spaces.')
|
2012-02-05 15:51:37 +08:00
|
|
|
|
2008-08-16 13:03:40 +08:00
|
|
|
if '-' in att_name:
|
|
|
|
att_name = att_name.replace('-', '_')
|
|
|
|
comment_notes.append('Field renamed to remove dashes.')
|
2012-02-05 15:51:37 +08:00
|
|
|
|
2008-09-17 13:12:53 +08:00
|
|
|
if column_name != att_name:
|
|
|
|
comment_notes.append('Field name made lowercase.')
|
2007-08-16 14:06:55 +08:00
|
|
|
|
|
|
|
if i in relations:
|
|
|
|
rel_to = relations[i][1] == table_name and "'self'" or table2model(relations[i][1])
|
2012-04-27 14:56:31 +08:00
|
|
|
|
|
|
|
if rel_to in known_models:
|
|
|
|
field_type = 'ForeignKey(%s' % rel_to
|
|
|
|
else:
|
|
|
|
field_type = "ForeignKey('%s'" % rel_to
|
|
|
|
|
2007-08-16 14:06:55 +08:00
|
|
|
if att_name.endswith('_id'):
|
|
|
|
att_name = att_name[:-3]
|
|
|
|
else:
|
2008-09-17 13:12:53 +08:00
|
|
|
extra_params['db_column'] = column_name
|
2007-08-16 14:06:55 +08:00
|
|
|
else:
|
2010-01-19 05:02:47 +08:00
|
|
|
# Calling `get_field_type` to get the field type string and any
|
|
|
|
# additional paramters and notes.
|
|
|
|
field_type, field_params, field_notes = self.get_field_type(connection, table_name, row)
|
|
|
|
extra_params.update(field_params)
|
|
|
|
comment_notes.extend(field_notes)
|
2007-08-16 14:06:55 +08:00
|
|
|
|
|
|
|
field_type += '('
|
2012-02-05 15:51:37 +08:00
|
|
|
|
2011-01-24 22:58:05 +08:00
|
|
|
if keyword.iskeyword(att_name):
|
|
|
|
att_name += '_field'
|
|
|
|
comment_notes.append('Field renamed because it was a Python reserved word.')
|
2007-08-16 14:06:55 +08:00
|
|
|
|
2012-02-12 04:53:48 +08:00
|
|
|
if att_name[0].isdigit():
|
|
|
|
att_name = 'number_%s' % att_name
|
2011-08-22 10:46:59 +08:00
|
|
|
extra_params['db_column'] = unicode(column_name)
|
|
|
|
comment_notes.append("Field renamed because it wasn't a "
|
|
|
|
"valid Python identifier.")
|
|
|
|
|
2007-08-16 14:06:55 +08:00
|
|
|
# Don't output 'id = meta.AutoField(primary_key=True)', because
|
|
|
|
# that's assumed if it doesn't exist.
|
|
|
|
if att_name == 'id' and field_type == 'AutoField(' and extra_params == {'primary_key': True}:
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Add 'null' and 'blank', if the 'null_ok' flag was present in the
|
|
|
|
# table description.
|
|
|
|
if row[6]: # If it's NULL...
|
|
|
|
extra_params['blank'] = True
|
|
|
|
if not field_type in ('TextField(', 'CharField('):
|
|
|
|
extra_params['null'] = True
|
|
|
|
|
|
|
|
field_desc = '%s = models.%s' % (att_name, field_type)
|
|
|
|
if extra_params:
|
|
|
|
if not field_desc.endswith('('):
|
|
|
|
field_desc += ', '
|
|
|
|
field_desc += ', '.join(['%s=%r' % (k, v) for k, v in extra_params.items()])
|
|
|
|
field_desc += ')'
|
|
|
|
if comment_notes:
|
|
|
|
field_desc += ' # ' + ' '.join(comment_notes)
|
|
|
|
yield ' %s' % field_desc
|
2010-01-19 05:02:47 +08:00
|
|
|
for meta_line in self.get_meta(table_name):
|
|
|
|
yield meta_line
|
|
|
|
|
|
|
|
def get_field_type(self, connection, table_name, row):
|
|
|
|
"""
|
|
|
|
Given the database connection, the table name, and the cursor row
|
|
|
|
description, this routine will return the given field type name, as
|
|
|
|
well as any additional keyword parameters and notes for the field.
|
|
|
|
"""
|
|
|
|
field_params = {}
|
|
|
|
field_notes = []
|
|
|
|
|
|
|
|
try:
|
|
|
|
field_type = connection.introspection.get_field_type(row[1], row)
|
|
|
|
except KeyError:
|
|
|
|
field_type = 'TextField'
|
|
|
|
field_notes.append('This field type is a guess.')
|
|
|
|
|
|
|
|
# This is a hook for DATA_TYPES_REVERSE to return a tuple of
|
|
|
|
# (field_type, field_params_dict).
|
|
|
|
if type(field_type) is tuple:
|
|
|
|
field_type, new_params = field_type
|
|
|
|
field_params.update(new_params)
|
|
|
|
|
|
|
|
# Add max_length for all CharFields.
|
|
|
|
if field_type == 'CharField' and row[3]:
|
|
|
|
field_params['max_length'] = row[3]
|
|
|
|
|
|
|
|
if field_type == 'DecimalField':
|
|
|
|
field_params['max_digits'] = row[4]
|
|
|
|
field_params['decimal_places'] = row[5]
|
|
|
|
|
|
|
|
return field_type, field_params, field_notes
|
|
|
|
|
|
|
|
def get_meta(self, table_name):
|
|
|
|
"""
|
|
|
|
Return a sequence comprising the lines of code necessary
|
|
|
|
to construct the inner Meta class for the model corresponding
|
|
|
|
to the given database table name.
|
|
|
|
"""
|
|
|
|
return [' class Meta:',
|
|
|
|
' db_table = %r' % table_name,
|
|
|
|
'']
|