|
29
|
1 |
import keyword |
|
|
2 |
from optparse import make_option |
|
|
3 |
|
|
0
|
4 |
from django.core.management.base import NoArgsCommand, CommandError |
|
29
|
5 |
from django.db import connections, DEFAULT_DB_ALIAS |
|
0
|
6 |
|
|
|
7 |
class Command(NoArgsCommand): |
|
|
8 |
help = "Introspects the database tables in the given database and outputs a Django model module." |
|
|
9 |
|
|
29
|
10 |
option_list = NoArgsCommand.option_list + ( |
|
|
11 |
make_option('--database', action='store', dest='database', |
|
|
12 |
default=DEFAULT_DB_ALIAS, help='Nominates a database to ' |
|
|
13 |
'introspect. Defaults to using the "default" database.'), |
|
|
14 |
) |
|
|
15 |
|
|
0
|
16 |
requires_model_validation = False |
|
|
17 |
|
|
29
|
18 |
db_module = 'django.db' |
|
|
19 |
|
|
0
|
20 |
def handle_noargs(self, **options): |
|
|
21 |
try: |
|
29
|
22 |
for line in self.handle_inspection(options): |
|
0
|
23 |
print line |
|
|
24 |
except NotImplementedError: |
|
|
25 |
raise CommandError("Database inspection isn't supported for the currently selected database backend.") |
|
|
26 |
|
|
29
|
27 |
def handle_inspection(self, options): |
|
|
28 |
connection = connections[options.get('database', DEFAULT_DB_ALIAS)] |
|
0
|
29 |
|
|
|
30 |
table2model = lambda table_name: table_name.title().replace('_', '').replace(' ', '').replace('-', '') |
|
|
31 |
|
|
|
32 |
cursor = connection.cursor() |
|
|
33 |
yield "# This is an auto-generated Django model module." |
|
|
34 |
yield "# You'll have to do the following manually to clean this up:" |
|
|
35 |
yield "# * Rearrange models' order" |
|
|
36 |
yield "# * Make sure each model has one field with primary_key=True" |
|
|
37 |
yield "# Feel free to rename the models, but don't rename db_table values or field names." |
|
|
38 |
yield "#" |
|
|
39 |
yield "# Also note: You'll have to insert the output of 'django-admin.py sqlcustom [appname]'" |
|
|
40 |
yield "# into your database." |
|
|
41 |
yield '' |
|
29
|
42 |
yield 'from %s import models' % self.db_module |
|
0
|
43 |
yield '' |
|
|
44 |
for table_name in connection.introspection.get_table_list(cursor): |
|
|
45 |
yield 'class %s(models.Model):' % table2model(table_name) |
|
|
46 |
try: |
|
|
47 |
relations = connection.introspection.get_relations(cursor, table_name) |
|
|
48 |
except NotImplementedError: |
|
|
49 |
relations = {} |
|
|
50 |
try: |
|
|
51 |
indexes = connection.introspection.get_indexes(cursor, table_name) |
|
|
52 |
except NotImplementedError: |
|
|
53 |
indexes = {} |
|
|
54 |
for i, row in enumerate(connection.introspection.get_table_description(cursor, table_name)): |
|
|
55 |
column_name = row[0] |
|
|
56 |
att_name = column_name.lower() |
|
|
57 |
comment_notes = [] # Holds Field notes, to be displayed in a Python comment. |
|
|
58 |
extra_params = {} # Holds Field parameters such as 'db_column'. |
|
|
59 |
|
|
|
60 |
# If the column name can't be used verbatim as a Python |
|
|
61 |
# attribute, set the "db_column" for this Field. |
|
|
62 |
if ' ' in att_name or '-' in att_name or keyword.iskeyword(att_name) or column_name != att_name: |
|
|
63 |
extra_params['db_column'] = column_name |
|
|
64 |
|
|
|
65 |
# Modify the field name to make it Python-compatible. |
|
|
66 |
if ' ' in att_name: |
|
|
67 |
att_name = att_name.replace(' ', '_') |
|
|
68 |
comment_notes.append('Field renamed to remove spaces.') |
|
|
69 |
if '-' in att_name: |
|
|
70 |
att_name = att_name.replace('-', '_') |
|
|
71 |
comment_notes.append('Field renamed to remove dashes.') |
|
|
72 |
if keyword.iskeyword(att_name): |
|
|
73 |
att_name += '_field' |
|
|
74 |
comment_notes.append('Field renamed because it was a Python reserved word.') |
|
|
75 |
if column_name != att_name: |
|
|
76 |
comment_notes.append('Field name made lowercase.') |
|
|
77 |
|
|
|
78 |
if i in relations: |
|
|
79 |
rel_to = relations[i][1] == table_name and "'self'" or table2model(relations[i][1]) |
|
|
80 |
field_type = 'ForeignKey(%s' % rel_to |
|
|
81 |
if att_name.endswith('_id'): |
|
|
82 |
att_name = att_name[:-3] |
|
|
83 |
else: |
|
|
84 |
extra_params['db_column'] = column_name |
|
|
85 |
else: |
|
29
|
86 |
# Calling `get_field_type` to get the field type string and any |
|
|
87 |
# additional paramters and notes. |
|
|
88 |
field_type, field_params, field_notes = self.get_field_type(connection, table_name, row) |
|
|
89 |
extra_params.update(field_params) |
|
|
90 |
comment_notes.extend(field_notes) |
|
0
|
91 |
|
|
|
92 |
# Add primary_key and unique, if necessary. |
|
|
93 |
if column_name in indexes: |
|
|
94 |
if indexes[column_name]['primary_key']: |
|
|
95 |
extra_params['primary_key'] = True |
|
|
96 |
elif indexes[column_name]['unique']: |
|
|
97 |
extra_params['unique'] = True |
|
|
98 |
|
|
|
99 |
field_type += '(' |
|
|
100 |
|
|
|
101 |
# Don't output 'id = meta.AutoField(primary_key=True)', because |
|
|
102 |
# that's assumed if it doesn't exist. |
|
|
103 |
if att_name == 'id' and field_type == 'AutoField(' and extra_params == {'primary_key': True}: |
|
|
104 |
continue |
|
|
105 |
|
|
|
106 |
# Add 'null' and 'blank', if the 'null_ok' flag was present in the |
|
|
107 |
# table description. |
|
|
108 |
if row[6]: # If it's NULL... |
|
|
109 |
extra_params['blank'] = True |
|
|
110 |
if not field_type in ('TextField(', 'CharField('): |
|
|
111 |
extra_params['null'] = True |
|
|
112 |
|
|
|
113 |
field_desc = '%s = models.%s' % (att_name, field_type) |
|
|
114 |
if extra_params: |
|
|
115 |
if not field_desc.endswith('('): |
|
|
116 |
field_desc += ', ' |
|
|
117 |
field_desc += ', '.join(['%s=%r' % (k, v) for k, v in extra_params.items()]) |
|
|
118 |
field_desc += ')' |
|
|
119 |
if comment_notes: |
|
|
120 |
field_desc += ' # ' + ' '.join(comment_notes) |
|
|
121 |
yield ' %s' % field_desc |
|
29
|
122 |
for meta_line in self.get_meta(table_name): |
|
|
123 |
yield meta_line |
|
|
124 |
|
|
|
125 |
def get_field_type(self, connection, table_name, row): |
|
|
126 |
""" |
|
|
127 |
Given the database connection, the table name, and the cursor row |
|
|
128 |
description, this routine will return the given field type name, as |
|
|
129 |
well as any additional keyword parameters and notes for the field. |
|
|
130 |
""" |
|
|
131 |
field_params = {} |
|
|
132 |
field_notes = [] |
|
|
133 |
|
|
|
134 |
try: |
|
|
135 |
field_type = connection.introspection.get_field_type(row[1], row) |
|
|
136 |
except KeyError: |
|
|
137 |
field_type = 'TextField' |
|
|
138 |
field_notes.append('This field type is a guess.') |
|
|
139 |
|
|
|
140 |
# This is a hook for DATA_TYPES_REVERSE to return a tuple of |
|
|
141 |
# (field_type, field_params_dict). |
|
|
142 |
if type(field_type) is tuple: |
|
|
143 |
field_type, new_params = field_type |
|
|
144 |
field_params.update(new_params) |
|
|
145 |
|
|
|
146 |
# Add max_length for all CharFields. |
|
|
147 |
if field_type == 'CharField' and row[3]: |
|
|
148 |
field_params['max_length'] = row[3] |
|
|
149 |
|
|
|
150 |
if field_type == 'DecimalField': |
|
|
151 |
field_params['max_digits'] = row[4] |
|
|
152 |
field_params['decimal_places'] = row[5] |
|
|
153 |
|
|
|
154 |
return field_type, field_params, field_notes |
|
|
155 |
|
|
|
156 |
def get_meta(self, table_name): |
|
|
157 |
""" |
|
|
158 |
Return a sequence comprising the lines of code necessary |
|
|
159 |
to construct the inner Meta class for the model corresponding |
|
|
160 |
to the given database table name. |
|
|
161 |
""" |
|
|
162 |
return [' class Meta:', |
|
|
163 |
' db_table = %r' % table_name, |
|
|
164 |
''] |