diff --git a/AUTHORS b/AUTHORS index e49bb392aa..1b2a0cfadb 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 a41fd5725b..dbef7a6a95 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 6b027de193..5a3cb53842 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 e6399874ef..9bebce9065 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 06e17c61ad..2dd3d256a1 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 dff5e681f8..84ae4d34b5 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 e4e2d38543..9525e58df9 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 0000000000..139597f9cb --- /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 0000000000..717c8d5b3a --- /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) +[] + +"""}