Code

Ticket #3163: unmanaged_models.diff

File unmanaged_models.diff, 12.0 KB (added by rfk, 5 years ago)

patch for unmanaged models, including tests and doc update

Line 
1Index: django/db/models/options.py
2===================================================================
3--- django/db/models/options.py (revision 9904)
4+++ django/db/models/options.py (working copy)
5@@ -21,7 +21,7 @@
6 DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering',
7                  'unique_together', 'permissions', 'get_latest_by',
8                  'order_with_respect_to', 'app_label', 'db_tablespace',
9-                 'abstract')
10+                 'abstract', 'managed')
11 
12 class Options(object):
13     def __init__(self, meta, app_label=None):
14@@ -42,6 +42,7 @@
15         self.pk = None
16         self.has_auto_field, self.auto_field = False, None
17         self.abstract = False
18+        self.managed = True
19         self.parents = SortedDict()
20         self.duplicate_targets = {}
21         # Managers that have been inherited from abstract base classes. These
22Index: django/db/backends/__init__.py
23===================================================================
24--- django/db/backends/__init__.py      (revision 9904)
25+++ django/db/backends/__init__.py      (working copy)
26@@ -450,8 +450,9 @@
27         tables = set()
28         for app in models.get_apps():
29             for model in models.get_models(app):
30-                tables.add(model._meta.db_table)
31-                tables.update([f.m2m_db_table() for f in model._meta.local_many_to_many])
32+                if model._meta.managed:
33+                    tables.add(model._meta.db_table)
34+                    tables.update([f.m2m_db_table() for f in model._meta.local_many_to_many])
35         if only_existing:
36             tables = [t for t in tables if self.table_name_converter(t) in self.table_names()]
37         return tables
38@@ -476,6 +477,8 @@
39 
40         for app in apps:
41             for model in models.get_models(app):
42+                if not model._meta.managed:
43+                    continue
44                 for f in model._meta.local_fields:
45                     if isinstance(f, models.AutoField):
46                         sequence_list.append({'table': model._meta.db_table, 'column': f.column})
47Index: django/db/backends/creation.py
48===================================================================
49--- django/db/backends/creation.py      (revision 9904)
50+++ django/db/backends/creation.py      (working copy)
51@@ -33,6 +33,8 @@
52         from django.db import models
53 
54         opts = model._meta
55+        if not opts.managed:
56+            return [], {}         
57         final_output = []
58         table_output = []
59         pending_references = {}
60@@ -112,6 +114,8 @@
61         "Returns any ALTER TABLE statements to add constraints after the fact."
62         from django.db.backends.util import truncate_name
63 
64+        if not model._meta.managed:
65+            return []
66         qn = self.connection.ops.quote_name
67         final_output = []
68         opts = model._meta
69@@ -225,6 +229,8 @@
70 
71     def sql_indexes_for_model(self, model, style):
72         "Returns the CREATE INDEX SQL statements for a single model"
73+        if not model._meta.managed:
74+            return []
75         output = []
76         for f in model._meta.local_fields:
77             output.extend(self.sql_indexes_for_field(model, f, style))
78@@ -255,6 +261,8 @@
79 
80     def sql_destroy_model(self, model, references_to_delete, style):
81         "Return the DROP TABLE and restraint dropping statements for a single model"
82+        if not model._meta.managed:
83+            return []
84         # Drop the table now
85         qn = self.connection.ops.quote_name
86         output = ['%s %s;' % (style.SQL_KEYWORD('DROP TABLE'),
87@@ -271,6 +279,8 @@
88     def sql_remove_table_constraints(self, model, references_to_delete, style):
89         from django.db.backends.util import truncate_name
90 
91+        if not model._meta.managed:
92+            return []
93         output = []
94         qn = self.connection.ops.quote_name
95         for rel_class, f in references_to_delete[model]:
96Index: django/core/management/commands/syncdb.py
97===================================================================
98--- django/core/management/commands/syncdb.py   (revision 9904)
99+++ django/core/management/commands/syncdb.py   (working copy)
100@@ -71,7 +71,7 @@
101                     if refto in seen_models:
102                         sql.extend(connection.creation.sql_for_pending_references(refto, self.style, pending_references))
103                 sql.extend(connection.creation.sql_for_pending_references(model, self.style, pending_references))
104-                if verbosity >= 1:
105+                if verbosity >= 1 and sql:
106                     print "Creating table %s" % model._meta.db_table
107                 for statement in sql:
108                     cursor.execute(statement)
109Index: tests/modeltests/unmanaged_models/__init__.py
110===================================================================
111--- tests/modeltests/unmanaged_models/__init__.py       (revision 0)
112+++ tests/modeltests/unmanaged_models/__init__.py       (revision 0)
113@@ -0,0 +1,2 @@
114+
115+
116Index: tests/modeltests/unmanaged_models/models.py
117===================================================================
118--- tests/modeltests/unmanaged_models/models.py (revision 0)
119+++ tests/modeltests/unmanaged_models/models.py (revision 0)
120@@ -0,0 +1,200 @@
121+"""
122+xx. unmanaged_models
123+
124+Models can have a ``managed`` attribute, which specifies whether the
125+SQL code is generated for the table on various manage.py operations.
126+The default is True.
127+"""
128+
129+from django.db import models
130+
131+"""
132+General test strategy:
133+* All tests are numbered (01, 02, 03... etc).
134+* Each test contains three models (A, B, C, followed with the number of test),
135+  containing both indexed and non-indexed fields (to verify sql_index), usual
136+  fields (model A), foreign keys (model B) and many-to-many fields (model C).
137+  D table is generated automatically as intermediate M2M one.
138+* The normal (default; managed = True) behaviour during the manage.py
139+  operations is not thoroughly checked; it is the duty of the appropriate tests
140+  for the primary functionality of these operations.
141+  The most attention is paid to whether the managed = False disables the SQL
142+  generation properly.
143+* The intermediate table for M2M relations is not ever verified explicitly,
144+  because it is not ever marked with managed explicitly.
145+"""
146+
147+# This dictionary maps the name of the model/SQL table (like 'A01')
148+# to the boolean specifying whether this name should appear in the final SQL
149+checks = {}
150+
151+
152+"""
153+01: managed is not set.
154+    In such case, it should be equal (by default) to True,
155+    and SQL is generated for all three models.
156+"""
157+checks['A01'] = True
158+checks['B01'] = True
159+checks['C01'] = True
160+
161+class A01(models.Model):
162+    class Meta: db_table = 'A01'
163+
164+    f_a = models.TextField(db_index = True)
165+    f_b = models.IntegerField()
166+
167+class B01(models.Model):
168+    class Meta: db_table = 'B01'
169+
170+    fk_a = models.ForeignKey(A01)
171+    f_a = models.TextField(db_index = True)
172+    f_b = models.IntegerField()
173+
174+class C01(models.Model):
175+    class Meta: db_table = 'C01'
176+
177+    mm_a = models.ManyToManyField(A01, db_table = 'D01')
178+    f_a = models.TextField(db_index = True)
179+    f_b = models.IntegerField()
180+
181+"""
182+02: managed is set to True.
183+    SQL is generated for all three models.
184+"""
185+checks['A02'] = True
186+checks['B02'] = True
187+checks['C02'] = True
188+
189+class A02(models.Model):
190+    class Meta:
191+        db_table = 'A02'
192+        managed = True
193+
194+    f_a = models.TextField(db_index = True)
195+    f_b = models.IntegerField()
196+
197+class B02(models.Model):
198+    class Meta:
199+        db_table = 'B02'
200+        managed = True
201+
202+    fk_a = models.ForeignKey(A02)
203+    f_a = models.TextField(db_index = True)
204+    f_b = models.IntegerField()
205+
206+class C02(models.Model):
207+    class Meta:
208+        db_table = 'C02'
209+        managed = True
210+
211+    mm_a = models.ManyToManyField(A02, db_table = 'D02')
212+    f_a = models.TextField(db_index = True)
213+    f_b = models.IntegerField()
214+
215+
216+"""
217+03: managed is set to False.
218+    SQL is NOT generated for any of the three models.
219+"""
220+checks['A03'] = False
221+checks['B03'] = False
222+checks['C03'] = False
223+
224+class A03(models.Model):
225+    class Meta:
226+        db_table = 'A03'
227+        managed = False
228+
229+    f_a = models.TextField(db_index = True)
230+    f_b = models.IntegerField()
231+
232+class B03(models.Model):
233+    class Meta:
234+        db_table = 'B03'
235+        managed = False
236+
237+    fk_a = models.ForeignKey(A03)
238+    f_a = models.TextField(db_index = True)
239+    f_b = models.IntegerField()
240+
241+class C03(models.Model):
242+    class Meta:
243+        db_table = 'C03'
244+        managed = False
245+
246+    mm_a = models.ManyToManyField(A03, db_table = 'D03')
247+    f_a = models.TextField(db_index = True)
248+    f_b = models.IntegerField()
249+
250+
251+# We will use short names for these templates
252+sql_templates = {
253+        'create table': 'CREATE TABLE "%s"',
254+        'create index': 'CREATE INDEX "%s_f_a"',
255+        'drop table': 'DROP TABLE "%s"',
256+        'delete from': 'DELETE FROM "%s"'
257+}
258+
259+def get_failed_models(arr_sql, sql_template_names):
260+    """
261+    Find the models which should not be in the SQL but they are present,
262+    or they should be in the SQL but they are missing.
263+    """
264+    txt_sql = ' '.join(arr_sql)
265+    for (model, should_be_present) in checks.iteritems():
266+        # Do we expect to see the model name in the SQL text?
267+        for sql_template_name in sql_template_names:
268+            # We are interested not in just the model name like "A01",
269+            # but in the whole string like 'CREATE TABLE "A01"'
270+            # so we apply the model name to the template
271+            # to find out the expected string
272+            expected = (sql_templates[sql_template_name])%model
273+            if ((expected in txt_sql) != should_be_present):
274+                # Our expectations failed!
275+                yield 'The string %s %s present in SQL but it %s.'%(
276+                    expected,
277+                    {False: 'is not', True: 'is'}[expected in txt_sql],
278+                    {False: 'should not be', True: 'should be'}[should_be_present]
279+                    )
280+
281+
282+__test__ = {'API_TESTS':"""
283+>>> from django.db.models import get_app
284+>>> from django.core.management.sql import *
285+>>> from django.core.management.color import no_style
286+>>> import sys
287+
288+>>> myapp = get_app('unmanaged_models')
289+>>> mystyle = no_style()
290+
291+# a. Verify sql_create
292+>>> list(get_failed_models( sql_create(myapp, mystyle), ['create table'] ))
293+[]
294+
295+# b. Verify sql_delete
296+>>> list(get_failed_models( sql_delete(myapp, mystyle), ['drop table'] ))
297+[]
298+
299+# c. Verify sql_reset
300+>>> list(get_failed_models( sql_reset(myapp, mystyle), ['drop table', 'create table', 'create index'] ))
301+[]
302+
303+# d. Verify sql_flush
304+>>> # sql_flush(mystyle)
305+>>> list(get_failed_models( sql_flush(mystyle), ['delete from'] ))
306+[]
307+
308+# e. Verify sql_custom
309+# No custom data provided, should not be no output.
310+>>> sql_custom(myapp,mystyle)
311+[]
312+
313+# f. Verify sql_indexes
314+>>> list(get_failed_models( sql_indexes(myapp, mystyle), ['create index'] ))
315+[]
316+
317+# g. Verify sql_all
318+>>> list(get_failed_models( sql_all(myapp, mystyle), ['create table', 'create index'] ))
319+[]
320+"""}
321Index: AUTHORS
322===================================================================
323--- AUTHORS     (revision 9904)
324+++ AUTHORS     (working copy)
325@@ -443,6 +443,7 @@
326     Mykola Zamkovoi <nickzam@gmail.com>
327     Jarek Zgoda <jarek.zgoda@gmail.com>
328     Cheng Zhang
329+    Alexander Myodov <alex@myodov.com>
330 
331 A big THANK YOU goes to:
332 
333Index: docs/ref/models/options.txt
334===================================================================
335--- docs/ref/models/options.txt (revision 9904)
336+++ docs/ref/models/options.txt (working copy)
337@@ -181,3 +181,15 @@
338     verbose_name_plural = "stories"
339 
340 If this isn't given, Django will use :attr:`~Options.verbose_name` + ``"s"``.
341+
342+``managed``
343+-----------------------
344+
345+.. attribute:: Options.managed
346+
347+.. versionadded:: 1.1
348+
349+If ``False``, Django's database-management facilities (such as syncdb) will
350+avoid processing this model.  Set this option on models that use
351+manually-created database tables or views.
352+