From 1e8eadc94e3b27fe90ce9356b48e8543a1ff590e Mon Sep 17 00:00:00 2001
From: Claude Paroz <claude@2xlibre.net>
Date: Sat, 5 Jan 2013 23:43:01 +0100
Subject: [PATCH] Fixed #15888 -- Made tablename argument of createcachetable
 optional

Thanks Aymeric Augustin for the report and the documentation and
Tim Graham for the review.
---
 .../gis/db/backends/spatialite/creation.py    |  7 +--
 .../management/commands/createcachetable.py   | 40 +++++++++---
 django/db/backends/creation.py                |  8 +--
 docs/ref/django-admin.txt                     | 11 +++-
 docs/releases/1.7.txt                         |  5 ++
 docs/topics/cache.txt                         | 54 ++++++++++------
 tests/cache/tests.py                          | 63 ++++++++++++++-----
 7 files changed, 130 insertions(+), 58 deletions(-)

diff --git a/django/contrib/gis/db/backends/spatialite/creation.py b/django/contrib/gis/db/backends/spatialite/creation.py
index 2838ecd8e1..fb3606f7e9 100644
--- a/django/contrib/gis/db/backends/spatialite/creation.py
+++ b/django/contrib/gis/db/backends/spatialite/creation.py
@@ -1,8 +1,6 @@
 import os
 
 from django.conf import settings
-from django.core.cache import get_cache
-from django.core.cache.backends.db import BaseDatabaseCache
 from django.core.exceptions import ImproperlyConfigured
 from django.db.backends.sqlite3.creation import DatabaseCreation
 
@@ -55,10 +53,7 @@ class SpatiaLiteCreation(DatabaseCreation):
             interactive=False,
             database=self.connection.alias)
 
-        for cache_alias in settings.CACHES:
-            cache = get_cache(cache_alias)
-            if isinstance(cache, BaseDatabaseCache):
-                call_command('createcachetable', cache._table, database=self.connection.alias)
+        call_command('createcachetable', database=self.connection.alias)
 
         # Get a cursor (even though we don't need one yet). This has
         # the side effect of initializing the test database.
diff --git a/django/core/management/commands/createcachetable.py b/django/core/management/commands/createcachetable.py
index 27668f272d..253f5b5201 100644
--- a/django/core/management/commands/createcachetable.py
+++ b/django/core/management/commands/createcachetable.py
@@ -1,32 +1,50 @@
 from optparse import make_option
 
+from django.conf import settings
+from django.core.cache import get_cache
 from django.core.cache.backends.db import BaseDatabaseCache
-from django.core.management.base import LabelCommand, CommandError
+from django.core.management.base import BaseCommand, CommandError
 from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS
 from django.db.utils import DatabaseError
 from django.utils.encoding import force_text
 
 
-class Command(LabelCommand):
-    help = "Creates the table needed to use the SQL cache backend."
-    args = "<tablename>"
-    label = 'tablename'
+class Command(BaseCommand):
+    help = "Creates the tables needed to use the SQL cache backend."
 
-    option_list = LabelCommand.option_list + (
+    option_list = BaseCommand.option_list + (
         make_option('--database', action='store', dest='database',
             default=DEFAULT_DB_ALIAS, help='Nominates a database onto '
-                'which the cache table will be installed. '
+                'which the cache tables will be installed. '
                 'Defaults to the "default" database.'),
     )
 
     requires_model_validation = False
 
-    def handle_label(self, tablename, **options):
+    def handle(self, *tablenames, **options):
         db = options.get('database')
+        self.verbosity = int(options.get('verbosity'))
+        if len(tablenames):
+            # Legacy behavior, tablename specified as argument
+            for tablename in tablenames:
+                self.create_table(db, tablename)
+        else:
+            for cache_alias in settings.CACHES:
+                cache = get_cache(cache_alias)
+                if isinstance(cache, BaseDatabaseCache):
+                    self.create_table(db, cache._table)
+
+    def create_table(self, database, tablename):
         cache = BaseDatabaseCache(tablename, {})
-        if not router.allow_migrate(db, cache.cache_model_class):
+        if not router.allow_migrate(database, cache.cache_model_class):
             return
-        connection = connections[db]
+        connection = connections[database]
+
+        if tablename in connection.introspection.table_names():
+            if self.verbosity > 0:
+                self.stdout.write("Cache table '%s' already exists." % tablename)
+            return
+
         fields = (
             # "key" is a reserved word in MySQL, so use "cache_key" instead.
             models.CharField(name='cache_key', max_length=255, unique=True, primary_key=True),
@@ -63,3 +81,5 @@ class Command(LabelCommand):
                         (tablename, force_text(e)))
             for statement in index_output:
                 curs.execute(statement)
+        if self.verbosity > 1:
+            self.stdout.write("Cache table '%s' created." % tablename)
diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py
index 9c700c96b2..e4c870c01b 100644
--- a/django/db/backends/creation.py
+++ b/django/db/backends/creation.py
@@ -356,13 +356,7 @@ class BaseDatabaseCreation(object):
             interactive=False,
             database=self.connection.alias)
 
-        from django.core.cache import get_cache
-        from django.core.cache.backends.db import BaseDatabaseCache
-        for cache_alias in settings.CACHES:
-            cache = get_cache(cache_alias)
-            if isinstance(cache, BaseDatabaseCache):
-                call_command('createcachetable', cache._table,
-                             database=self.connection.alias)
+        call_command('createcachetable', database=self.connection.alias)
 
         # Get a cursor (even though we don't need one yet). This has
         # the side effect of initializing the test database.
diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt
index 39cbbc8dd5..9626606c01 100644
--- a/docs/ref/django-admin.txt
+++ b/docs/ref/django-admin.txt
@@ -131,12 +131,19 @@ createcachetable
 
 .. django-admin:: createcachetable
 
-Creates a cache table named ``tablename`` for use with the database cache
-backend. See :doc:`/topics/cache` for more information.
+Creates the cache tables for use with the database cache backend. See
+:doc:`/topics/cache` for more information.
 
 The :djadminopt:`--database` option can be used to specify the database
 onto which the cachetable will be installed.
 
+.. versionchanged:: 1.7
+
+    It is no longer necessary to provide the cache table name or the
+    :djadminopt:`--database` option. Django takes this information from your
+    settings file. If you have configured multiple caches or multiple databases,
+    all cache tables are created.
+
 dbshell
 -------
 
diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt
index 52f9eb0d43..7a49f13327 100644
--- a/docs/releases/1.7.txt
+++ b/docs/releases/1.7.txt
@@ -305,6 +305,11 @@ Management Commands
   ``use_natural_primary_keys`` arguments for ``serializers.serialize()``, allow
   the use of natural primary keys when serializing.
 
+* It is no longer necessary to provide the cache table name or the
+  :djadminopt:`--database` option for the :djadmin:`createcachetable` command.
+  Django takes this information from your settings file. If you have configured
+  multiple caches or multiple databases, all cache tables are created.
+
 Models
 ^^^^^^
 
diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt
index 513584dbbf..4e37eeb3f4 100644
--- a/docs/topics/cache.txt
+++ b/docs/topics/cache.txt
@@ -159,22 +159,18 @@ particularly temporary.
 Database caching
 ----------------
 
-To use a database table as your cache backend, first create a cache table in
-your database by running this command::
+Django can store its cached data in your database. This works best if you've
+got a fast, well-indexed database server.
 
-    $ python manage.py createcachetable [cache_table_name]
+To use a database table as your cache backend:
 
-...where ``[cache_table_name]`` is the name of the database table to create.
-(This name can be whatever you want, as long as it's a valid table name that's
-not already being used in your database.) This command creates a single table
-in your database that is in the proper format that Django's database-cache
-system expects.
+    * Set :setting:`BACKEND <CACHES-BACKEND>` to
+      ``django.core.cache.backends.db.DatabaseCache``
+    * Set :setting:`LOCATION <CACHES-LOCATION>` to ``tablename``, the name of
+      the database table. This name can be whatever you want, as long as it's
+      a valid table name that's not already being used in your database.
 
-Once you've created that database table, set your
-:setting:`BACKEND <CACHES-BACKEND>` setting to
-``"django.core.cache.backends.db.DatabaseCache"``, and
-:setting:`LOCATION <CACHES-LOCATION>` to ``tablename`` -- the name of the
-database table. In this example, the cache table's name is ``my_cache_table``::
+In this example, the cache table's name is ``my_cache_table``::
 
     CACHES = {
         'default': {
@@ -183,14 +179,36 @@ database table. In this example, the cache table's name is ``my_cache_table``::
         }
     }
 
+Creating the cache table
+~~~~~~~~~~~~~~~~~~~~~~~~
 
-The database caching backend uses the same database as specified in your
-settings file. You can't use a different database backend for your cache table.
+Before using the database cache, you must create the cache table with this
+command::
 
-Database caching works best if you've got a fast, well-indexed database server.
+    python manage.py createcachetable
 
-Database caching and multiple databases
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This creates a table in your database that is in the proper format that
+Django's database-cache system expects. The name of the table is taken from
+:setting:`LOCATION <CACHES-LOCATION>`.
+
+If you are using multiple database caches, :djadmin:`createcachetable` creates
+one table for each cache.
+
+If you are using multiple databases, :djadmin:`createcachetable` observes the
+``allow_migrate()`` method of your database routers (see below).
+
+Like :djadmin:`migrate`, :djadmin:`createcachetable` won't touch an existing
+table. It will only create missing tables.
+
+.. versionchanged:: 1.7
+
+    Before Django 1.7, :djadmin:`createcachetable` created one table at a time.
+    You had to pass the name of the table you wanted to create, and if you were
+    using multiple databases, you had to use the :djadminopt:`--database`
+    option. For backwards compatibility, this is still possible.
+
+Multiple databases
+~~~~~~~~~~~~~~~~~~
 
 If you use database caching with multiple databases, you'll also need
 to set up routing instructions for your database cache table. For the
diff --git a/tests/cache/tests.py b/tests/cache/tests.py
index 6912e8f755..c04decbad8 100644
--- a/tests/cache/tests.py
+++ b/tests/cache/tests.py
@@ -20,7 +20,7 @@ from django.core import management
 from django.core.cache import get_cache
 from django.core.cache.backends.base import (CacheKeyWarning,
     InvalidCacheBackendError)
-from django.db import router, transaction
+from django.db import connection, router, transaction
 from django.core.cache.utils import make_template_fragment_key
 from django.http import (HttpResponse, HttpRequest, StreamingHttpResponse,
     QueryDict)
@@ -829,6 +829,14 @@ def custom_key_func(key, key_prefix, version):
     return 'CUSTOM-' + '-'.join([key_prefix, str(version), key])
 
 
+@override_settings(
+    CACHES={
+        'default': {
+            'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
+            'LOCATION': 'test cache table',
+        },
+    },
+)
 class DBCacheTests(BaseCacheTests, TransactionTestCase):
 
     available_apps = ['cache']
@@ -837,7 +845,7 @@ class DBCacheTests(BaseCacheTests, TransactionTestCase):
     def setUp(self):
         # Spaces are used in the table name to ensure quoting/escaping is working
         self._table_name = 'test cache table'
-        management.call_command('createcachetable', self._table_name, verbosity=0, interactive=False)
+        management.call_command('createcachetable', verbosity=0, interactive=False)
         self.cache = get_cache(self.backend_name, LOCATION=self._table_name, OPTIONS={'MAX_ENTRIES': 30})
         self.prefix_cache = get_cache(self.backend_name, LOCATION=self._table_name, KEY_PREFIX='cacheprefix')
         self.v2_cache = get_cache(self.backend_name, LOCATION=self._table_name, VERSION=2)
@@ -845,7 +853,6 @@ class DBCacheTests(BaseCacheTests, TransactionTestCase):
         self.custom_key_cache2 = get_cache(self.backend_name, LOCATION=self._table_name, KEY_FUNCTION='cache.tests.custom_key_func')
 
     def tearDown(self):
-        from django.db import connection
         cursor = connection.cursor()
         cursor.execute('DROP TABLE %s' % connection.ops.quote_name(self._table_name))
         connection.commit()
@@ -858,14 +865,29 @@ class DBCacheTests(BaseCacheTests, TransactionTestCase):
         self.perform_cull_test(50, 18)
 
     def test_second_call_doesnt_crash(self):
-        with six.assertRaisesRegex(self, management.CommandError,
-                "Cache table 'test cache table' could not be created"):
-            management.call_command(
-               'createcachetable',
-                self._table_name,
-                verbosity=0,
-                interactive=False
-            )
+        stdout = six.StringIO()
+        management.call_command(
+           'createcachetable',
+            stdout=stdout
+        )
+        self.assertEqual(stdout.getvalue(),
+            "Cache table '%s' already exists.\n" % self._table_name)
+
+    def test_createcachetable_with_table_argument(self):
+        """
+        Delete and recreate cache table with legacy behavior (explicitly
+        specifying the table name).
+        """
+        self.tearDown()
+        stdout = six.StringIO()
+        management.call_command(
+            'createcachetable',
+            self._table_name,
+            verbosity=2,
+            stdout=stdout
+        )
+        self.assertEqual(stdout.getvalue(),
+            "Cache table '%s' created.\n" % self._table_name)
 
     def test_clear_commits_transaction(self):
         # Ensure the database transaction is committed (#19896)
@@ -896,6 +918,14 @@ class DBCacheRouter(object):
             return db == 'other'
 
 
+@override_settings(
+    CACHES={
+        'default': {
+            'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
+            'LOCATION': 'my_cache_table',
+        },
+    },
+)
 class CreateCacheTableForDBCacheTests(TestCase):
     multi_db = True
 
@@ -905,13 +935,16 @@ class CreateCacheTableForDBCacheTests(TestCase):
             router.routers = [DBCacheRouter()]
             # cache table should not be created on 'default'
             with self.assertNumQueries(0, using='default'):
-                management.call_command('createcachetable', 'cache_table',
+                management.call_command('createcachetable',
                                         database='default',
                                         verbosity=0, interactive=False)
             # cache table should be created on 'other'
-            # one query is used to create the table and another one the index
-            with self.assertNumQueries(2, using='other'):
-                management.call_command('createcachetable', 'cache_table',
+            # Queries:
+            #   1: check table doesn't already exist
+            #   2: create the table
+            #   3: create the index
+            with self.assertNumQueries(3, using='other'):
+                management.call_command('createcachetable',
                                         database='other',
                                         verbosity=0, interactive=False)
         finally: