Code

Ticket #1561: inspectdb-ordering.diff

File inspectdb-ordering.diff, 9.6 KB (added by mir@…, 8 years ago)
Line 
1diff --git a/django/core/management.py b/django/core/management.py
2index 7851403..894ca47 100644
3--- a/django/core/management.py
4+++ b/django/core/management.py
5@@ -638,6 +638,162 @@ def startapp(app_name, directory):
6 startapp.help_doc = "Creates a Django app directory structure for the given app name in the current directory."
7 startapp.args = "[appname]"
8 
9+def createsuperuser(username=None, email=None, password=None):
10+    "Creates a superuser account."
11+    from django.core import validators
12+    from django.contrib.auth.models import User
13+    import getpass
14+
15+    try:
16+        import pwd
17+    except ImportError:
18+        default_username = ''
19+    else:
20+        # Determine the current system user's username, to use as a default.
21+        default_username = pwd.getpwuid(os.getuid())[0].replace(' ', '').lower()
22+
23+    # Determine whether the default username is taken, so we don't display
24+    # it as an option.
25+    if default_username:
26+        try:
27+            User.objects.get(username=default_username)
28+        except User.DoesNotExist:
29+            pass
30+        else:
31+            default_username = ''
32+
33+    try:
34+        while 1:
35+            if not username:
36+                input_msg = 'Username'
37+                if default_username:
38+                    input_msg += ' (Leave blank to use %r)' % default_username
39+                username = raw_input(input_msg + ': ')
40+            if default_username and username == '':
41+                username = default_username
42+            if not username.isalnum():
43+                sys.stderr.write("Error: That username is invalid. Use only letters, digits and underscores.\n")
44+                username = None
45+            try:
46+                User.objects.get(username=username)
47+            except User.DoesNotExist:
48+                break
49+            else:
50+                sys.stderr.write("Error: That username is already taken.\n")
51+                username = None
52+        while 1:
53+            if not email:
54+                email = raw_input('E-mail address: ')
55+            try:
56+                validators.isValidEmail(email, None)
57+            except validators.ValidationError:
58+                sys.stderr.write("Error: That e-mail address is invalid.\n")
59+                email = None
60+            else:
61+                break
62+        while 1:
63+            if not password:
64+                password = getpass.getpass()
65+                password2 = getpass.getpass('Password (again): ')
66+                if password != password2:
67+                    sys.stderr.write("Error: Your passwords didn't match.\n")
68+                    password = None
69+                    continue
70+            if password.strip() == '':
71+                sys.stderr.write("Error: Blank passwords aren't allowed.\n")
72+                password = None
73+                continue
74+            break
75+    except KeyboardInterrupt:
76+        sys.stderr.write("\nOperation cancelled.\n")
77+        sys.exit(1)
78+    u = User.objects.create_user(username, email, password)
79+    u.is_staff = True
80+    u.is_active = True
81+    u.is_superuser = True
82+    u.save()
83+    print "User created successfully."
84+createsuperuser.args = '[username] [email] [password] (Either all or none)'
85+
86+def _topological_sort(cursor, introspection_module, table_names):
87+    """
88+    sorts the table names in a way that there are no forward references.
89+    At least it tries.
90+    generates the pairs for the ordered list of table names: ( table_name, forward_refs )
91+    forward_refs is a set of table names that are referenced with a forward reference
92+    that could not be avoided
93+    """
94+    # tables_in is a list [ (table_name, [ referenced_table_name ] ], excluding self references
95+    tables_in = [ ( name, [ val[1]
96+                            for val in introspection_module.get_relations(cursor, name).values()
97+                            if val[1] != name ])
98+                  for name in table_names ]
99+    #
100+    # build the sorted list
101+    tables_out_set = set()
102+    # sort by number of references and lexically (to get a defined order that doesn't change from inspect to inspect)
103+    tables_in.sort(lambda (name1,refs1),(name2,refs2): cmp(len(refs1),len(refs2)) or cmp(name1,name2))
104+    # go through list until everything is handled
105+    while tables_in != [ ]:
106+        for (i, (table_name, refs)) in enumerate(tables_in):
107+            for ref in refs:
108+                if ref not in tables_out_set:
109+                    # forward reference found, try next element
110+                    break
111+            else:
112+                # bingo! take this element to the ordered tables
113+                # and start processing the list with the first element
114+                tables_out_set.add(table_name)
115+                del tables_in[i]
116+                yield (table_name, set())
117+                break
118+        else:
119+            # There is no element without forward references in tables_in
120+            # i.e. we have circular references
121+            # strategy: find the table that is referenced most often
122+            # and put it here. This will most probably break the cycle
123+            # with only one trouble maker.
124+            refcount = { }
125+            referrers = { }
126+            max_ref = 0
127+            max_reffered = None
128+            for (referrer_name, refs) in tables_in:
129+                for ref in refs:
130+                    if (not ref in tables_out_set
131+                        and not referrer_name in referrers.setdefault(ref,[ ])):
132+                        referrers[ref].append(referrer_name)
133+                        refcount[ref] = refcount.get(ref,0) + 1
134+                        if refcount[ref] > max_ref:
135+                            max_ref = refcount[ref]
136+                            max_referred = ref
137+            for (i,(table_name,refs)) in enumerate(tables_in):
138+                if table_name == max_referred:
139+                    candidate = i
140+                    break
141+            else:
142+                # paranoia, cannot really happen
143+                candidate = 0
144+            # removing the references from the first entry will
145+            # make sure that the first entry will be taken anyway
146+            # during the next while loop run.
147+            # possible improvement: reorder tables_in according to
148+            #                       number of remaining unresolved references
149+            bad_table_name = tables_in[candidate][0]
150+            forward_references = [ x for x in tables_in[candidate][1]
151+                                   if x not in tables_out_set ]
152+            print "# cyclic references detected at this point"
153+            print "# %s contains these forward references: %s" % (
154+                bad_table_name,
155+                ", ".join(forward_references))
156+            bad_referrers = referrers[bad_table_name]
157+            bad_referrers.sort()
158+            print "# and is referenced by %s" % ", ".join(bad_referrers)
159+            tables_out_set.add(bad_table_name)
160+            del tables_in[candidate]
161+            yield (bad_table_name, set(forward_references))
162+    return
163+
164+
165 def inspectdb(db_name):
166     "Generator that introspects the tables in the given database name and returns a Django model, one line at a time."
167     from django.db import connection, get_introspection_module
168@@ -663,12 +819,20 @@ def inspectdb(db_name):
169     yield ''
170     yield 'from django.db import models'
171     yield ''
172-    for table_name in introspection_module.get_table_list(cursor):
173+    for (table_name, forward_referenced_tables) in (
174+           _topological_sort(cursor, introspection_module,
175+                             introspection_module.get_table_list(cursor))):
176         yield 'class %s(models.Model):' % table2model(table_name)
177         try:
178             relations = introspection_module.get_relations(cursor, table_name)
179         except NotImplementedError:
180             relations = {}
181+        # relation_count :: {other_table_name: ref_count}
182+        # counts for each other table, how often it is referred by this table.
183+        # (needed for the decision whether we need related_name attributes)
184+        relation_count = {}
185+        for (field_index, (field_index_other_table, other_table)) in relations.iteritems():
186+            relation_count[other_table] = relation_count.setdefault(other_table,0) + 1
187         try:
188             indexes = introspection_module.get_indexes(cursor, table_name)
189         except NotImplementedError:
190@@ -683,14 +847,19 @@ def inspectdb(db_name):
191                 att_name += '_field'
192                 comment_notes.append('Field renamed because it was a Python reserved word.')
193 
194-            if relations.has_key(i):
195-                rel_to = relations[i][1] == table_name and "'self'" or table2model(relations[i][1])
196-                field_type = 'ForeignKey(%s' % rel_to
197+            if (relations.has_key(i)
198+                and relations[i][1] not in forward_referenced_tables):
199+                rel_to = relations[i][1] == table_name and "'self'" or "%s" % table2model(relations[i][1])
200+                field_type = "ForeignKey(%s" % rel_to
201                 if att_name.endswith('_id'):
202                     att_name = att_name[:-3]
203                 else:
204                     extra_params['db_column'] = att_name
205+                if relation_count[relations[i][1]] > 1:
206+                    extra_params['related_name'] = '%s_by_%s' % (table_name, att_name)
207             else:
208+                if relations.has_key(i):
209+                    comment_notes.append('This is a forward foreign key reference to %s.' % table2model(relations[i][1]))
210                 try:
211                     field_type = introspection_module.DATA_TYPES_REVERSE[row[1]]
212                 except KeyError: