Code

Ticket #1561: inspectdb-ordering.2.diff

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