From b4dd4d4bb7db6533e13be1455ccdc52c3d50cac3 Mon Sep 17 00:00:00 2001 From: Malcolm Tredinnick Date: Mon, 9 Mar 2009 03:35:02 +0000 Subject: [PATCH] Fixed #3163 -- Add a "Meta.managed" option to models. This allows a model to be defined which is not subject to database table creation and removal. Useful for models that sit over existing tables or database views. Thanks to Alexander Myodov, Wolfgang Kriesing and Ryan Kelly for the bulk of this patch. git-svn-id: http://code.djangoproject.com/svn/django/trunk@10008 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- AUTHORS | 1 + django/core/management/commands/syncdb.py | 2 +- django/db/backends/__init__.py | 4 + django/db/backends/creation.py | 10 ++ django/db/models/options.py | 3 +- docs/ref/django-admin.txt | 4 + docs/ref/models/options.txt | 25 ++++ tests/modeltests/unmanaged_models/__init__.py | 2 + tests/modeltests/unmanaged_models/models.py | 117 ++++++++++++++++++ 9 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 tests/modeltests/unmanaged_models/__init__.py create mode 100644 tests/modeltests/unmanaged_models/models.py diff --git a/AUTHORS b/AUTHORS index e49bb392aa6..1b2a0cfadb5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -297,6 +297,7 @@ answer newbie questions, and generally made Django that much better: James Murty msundstr Robert Myers + Alexander Myodov Nebojša Dorđević Doug Napoleone Gopal Narayanan diff --git a/django/core/management/commands/syncdb.py b/django/core/management/commands/syncdb.py index a41fd5725b3..dbef7a6a959 100644 --- a/django/core/management/commands/syncdb.py +++ b/django/core/management/commands/syncdb.py @@ -71,7 +71,7 @@ class Command(NoArgsCommand): if refto in seen_models: sql.extend(connection.creation.sql_for_pending_references(refto, self.style, pending_references)) sql.extend(connection.creation.sql_for_pending_references(model, self.style, pending_references)) - if verbosity >= 1: + if verbosity >= 1 and sql: print "Creating table %s" % model._meta.db_table for statement in sql: cursor.execute(statement) diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 6b027de1931..5a3cb53842e 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -450,6 +450,8 @@ class BaseDatabaseIntrospection(object): tables = set() for app in models.get_apps(): for model in models.get_models(app): + if not model._meta.managed: + continue tables.add(model._meta.db_table) tables.update([f.m2m_db_table() for f in model._meta.local_many_to_many]) if only_existing: @@ -476,6 +478,8 @@ class BaseDatabaseIntrospection(object): for app in apps: for model in models.get_models(app): + if not model._meta.managed: + continue for f in model._meta.local_fields: if isinstance(f, models.AutoField): sequence_list.append({'table': model._meta.db_table, 'column': f.column}) diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py index e6399874ef3..9bebce9065c 100644 --- a/django/db/backends/creation.py +++ b/django/db/backends/creation.py @@ -33,6 +33,8 @@ class BaseDatabaseCreation(object): from django.db import models opts = model._meta + if not opts.managed: + return [], {} final_output = [] table_output = [] pending_references = {} @@ -112,6 +114,8 @@ class BaseDatabaseCreation(object): "Returns any ALTER TABLE statements to add constraints after the fact." from django.db.backends.util import truncate_name + if not model._meta.managed: + return [] qn = self.connection.ops.quote_name final_output = [] opts = model._meta @@ -225,6 +229,8 @@ class BaseDatabaseCreation(object): def sql_indexes_for_model(self, model, style): "Returns the CREATE INDEX SQL statements for a single model" + if not model._meta.managed: + return [] output = [] for f in model._meta.local_fields: output.extend(self.sql_indexes_for_field(model, f, style)) @@ -255,6 +261,8 @@ class BaseDatabaseCreation(object): def sql_destroy_model(self, model, references_to_delete, style): "Return the DROP TABLE and restraint dropping statements for a single model" + if not model._meta.managed: + return [] # Drop the table now qn = self.connection.ops.quote_name output = ['%s %s;' % (style.SQL_KEYWORD('DROP TABLE'), @@ -271,6 +279,8 @@ class BaseDatabaseCreation(object): def sql_remove_table_constraints(self, model, references_to_delete, style): from django.db.backends.util import truncate_name + if not model._meta.managed: + return [] output = [] qn = self.connection.ops.quote_name for rel_class, f in references_to_delete[model]: diff --git a/django/db/models/options.py b/django/db/models/options.py index 06e17c61adb..2dd3d256a1f 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -21,7 +21,7 @@ get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]| DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering', 'unique_together', 'permissions', 'get_latest_by', 'order_with_respect_to', 'app_label', 'db_tablespace', - 'abstract') + 'abstract', 'managed') class Options(object): def __init__(self, meta, app_label=None): @@ -42,6 +42,7 @@ class Options(object): self.pk = None self.has_auto_field, self.auto_field = False, None self.abstract = False + self.managed = True self.parents = SortedDict() self.duplicate_targets = {} # Managers that have been inherited from abstract base classes. These diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index dff5e681f86..84ae4d34b57 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -431,6 +431,8 @@ Currently supported: * ``django`` for all ``*.py`` and ``*.html`` files (default) * ``djangojs`` for ``*.js`` files +.. _django-admin-reset: + reset --------------------------- @@ -634,6 +636,8 @@ This command is disabled when the ``--settings`` option to situations, either omit the ``--settings`` option or unset ``DJANGO_SETTINGS_MODULE``. +.. _django-admin-syncdb: + syncdb ------ diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt index e4e2d38543a..9525e58df9c 100644 --- a/docs/ref/models/options.txt +++ b/docs/ref/models/options.txt @@ -75,6 +75,30 @@ Example:: See the docs for :meth:`~django.db.models.QuerySet.latest` for more. +``managed`` +----------------------- + +.. attribute:: Options.managed + +.. versionadded:: 1.1 + +If ``False``, no database table creation or deletion operations will be +performed for this model. This is useful if the model represents an existing +table or a database view that has been created by some other means. + +The default value is ``True``, meaning Django will create the appropriate +database tables in :ref:`django-admin-syncdb` and remove them as part of a +:ref:`reset ` management command. + +If a model contains a :class:`~django.db.models.ManyToManyField` and has +``managed=False``, the intermediate table for the many-to-many join will also +not be created. Should you require the intermediate table to be created, set +it up as an explicit model and use the :attr:`ManyToManyField.through` +attribute. + +For tests involving models with ``managed=False``, it's up to you to ensure +the correct tables are created as part of the test setup. + ``order_with_respect_to`` ------------------------- @@ -181,3 +205,4 @@ The plural name for the object:: verbose_name_plural = "stories" If this isn't given, Django will use :attr:`~Options.verbose_name` + ``"s"``. + diff --git a/tests/modeltests/unmanaged_models/__init__.py b/tests/modeltests/unmanaged_models/__init__.py new file mode 100644 index 00000000000..139597f9cb0 --- /dev/null +++ b/tests/modeltests/unmanaged_models/__init__.py @@ -0,0 +1,2 @@ + + diff --git a/tests/modeltests/unmanaged_models/models.py b/tests/modeltests/unmanaged_models/models.py new file mode 100644 index 00000000000..717c8d5b3ac --- /dev/null +++ b/tests/modeltests/unmanaged_models/models.py @@ -0,0 +1,117 @@ +""" +Models can have a ``managed`` attribute, which specifies whether the SQL code +is generated for the table on various manage.py operations. +""" + +from django.db import models + +# All of these models are creatd in the database by Django. + +class A01(models.Model): + f_a = models.CharField(max_length=10, db_index=True) + f_b = models.IntegerField() + + class Meta: + db_table = 'A01' + + def __unicode__(self): + return self.f_a + +class B01(models.Model): + fk_a = models.ForeignKey(A01) + f_a = models.CharField(max_length=10, db_index=True) + f_b = models.IntegerField() + + class Meta: + db_table = 'B01' + # 'managed' is True by default. This tests we can set it explicitly. + managed = True + + def __unicode__(self): + return self.f_a + +class C01(models.Model): + mm_a = models.ManyToManyField(A01, db_table='D01') + f_a = models.CharField(max_length=10, db_index=True) + f_b = models.IntegerField() + + class Meta: + db_table = 'C01' + + def __unicode__(self): + return self.f_a + +# All of these models use the same tables as the previous set (they are shadows +# of possibly a subset of the columns). There should be no creation errors, +# since we have told Django they aren't managed by Django. + +class A02(models.Model): + f_a = models.CharField(max_length=10, db_index=True) + + class Meta: + db_table = 'A01' + managed = False + + def __unicode__(self): + return self.f_a + +class B02(models.Model): + class Meta: + db_table = 'B01' + managed = False + + fk_a = models.ForeignKey(A02) + f_a = models.CharField(max_length=10, db_index=True) + f_b = models.IntegerField() + + def __unicode__(self): + return self.f_a + +# To re-use the many-to-many intermediate table, we need to manually set up +# things up. +class C02(models.Model): + mm_a = models.ManyToManyField(A02, through="Intermediate") + f_a = models.CharField(max_length=10, db_index=True) + f_b = models.IntegerField() + + class Meta: + db_table = 'C01' + managed = False + + def __unicode__(self): + return self.f_a + +class Intermediate(models.Model): + a02 = models.ForeignKey(A02, db_column="a01_id") + c02 = models.ForeignKey(C02, db_column="c01_id") + + class Meta: + db_table = 'D01' + managed = False + +__test__ = {'API_TESTS':""" +The main test here is that the all the models can be created without any +database errors. We can also do some more simple insertion and lookup tests +whilst we're here to show that the second of models do refer to the tables from +the first set. + +# Insert some data into one set of models. +>>> a = A01.objects.create(f_a="foo", f_b=42) +>>> _ = B01.objects.create(fk_a=a, f_a="fred", f_b=1729) +>>> c = C01.objects.create(f_a="barney", f_b=1) +>>> c.mm_a = [a] + +# ... and pull it out via the other set. +>>> A02.objects.all() +[] +>>> b = B02.objects.all()[0] +>>> b + +>>> b.fk_a + +>>> C02.objects.filter(f_a=None) +[] +>>> C02.objects.filter(mm_a=a.id) +[] + +"""}