diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index be5c0f7c9ab9..336b1571a5cd 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -1,8 +1,9 @@ +import datetime import hashlib from django.db.backends.utils import truncate_name from django.db.transaction import atomic -from django.utils import six +from django.utils import six, timezone from django.utils.encoding import force_bytes from django.utils.log import getLogger @@ -201,6 +202,13 @@ def effective_default(self, field): default = six.binary_type() else: default = six.text_type() + elif getattr(field, 'auto_now', False) or getattr(field, 'auto_now_add', False): + default = timezone.now() # default for DateTimeField + internal_type = field.get_internal_type() + if internal_type == 'DateField': + default = default.date() + elif internal_type == 'TimeField': + default = default.time() else: default = None # If it's a callable, call it diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py index 33a9e9e1663c..0c99207ba600 100644 --- a/django/db/migrations/autodetector.py +++ b/django/db/migrations/autodetector.py @@ -788,7 +788,11 @@ def _generate_added_field(self, app_label, model_name, field_name): preserve_default = True if (not field.null and not field.has_default() and not isinstance(field, models.ManyToManyField) and - not (field.blank and field.empty_strings_allowed)): + not (field.blank and field.empty_strings_allowed) and + not ( + isinstance(field, (models.DateField, models.DateTimeField, models.TimeField)) and + (field.auto_now or field.auto_now_add) + )): field = field.clone() field.default = self.questioner.ask_not_null_addition(field_name, model_name) preserve_default = False diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py index 2d45dac7f820..2720f6baeff6 100644 --- a/tests/migrations/test_autodetector.py +++ b/tests/migrations/test_autodetector.py @@ -47,6 +47,18 @@ class AutodetectorTests(TestCase): ("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=200, default='Ada Lovelace')), ]) + author_dates_of_birth_auto_now = ModelState("testapp", "Author", [ + ("id", models.AutoField(primary_key=True)), + ("date_of_birth", models.DateField(auto_now=True)), + ("date_time_of_birth", models.DateTimeField(auto_now=True)), + ("time_of_birth", models.TimeField(auto_now=True)), + ]) + author_dates_of_birth_auto_now_add = ModelState("testapp", "Author", [ + ("id", models.AutoField(primary_key=True)), + ("date_of_birth", models.DateField(auto_now_add=True)), + ("date_time_of_birth", models.DateTimeField(auto_now_add=True)), + ("time_of_birth", models.TimeField(auto_now_add=True)), + ]) author_name_deconstructable_1 = ModelState("testapp", "Author", [ ("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=200, default=DeconstructableObject())), @@ -522,6 +534,36 @@ def test_add_field(self): self.assertOperationTypes(changes, 'testapp', 0, ["AddField"]) self.assertOperationAttributes(changes, "testapp", 0, 0, name="name") + @mock.patch('django.db.migrations.questioner.MigrationQuestioner.ask_not_null_addition', + side_effect=Exception("Should not have prompted for not null addition")) + def test_add_date_fields_with_auto_now_not_asking_for_default(self, mocked_ask_method): + # Make state + before = self.make_project_state([self.author_empty]) + after = self.make_project_state([self.author_dates_of_birth_auto_now]) + autodetector = MigrationAutodetector(before, after) + changes = autodetector._detect_changes() + # Right number/type of migrations? + self.assertNumberMigrations(changes, 'testapp', 1) + self.assertOperationTypes(changes, 'testapp', 0, ["AddField", "AddField", "AddField"]) + self.assertOperationFieldAttributes(changes, "testapp", 0, 0, auto_now=True) + self.assertOperationFieldAttributes(changes, "testapp", 0, 1, auto_now=True) + self.assertOperationFieldAttributes(changes, "testapp", 0, 2, auto_now=True) + + @mock.patch('django.db.migrations.questioner.MigrationQuestioner.ask_not_null_addition', + side_effect=Exception("Should not have prompted for not null addition")) + def test_add_date_fields_with_auto_now_add_not_asking_for_default(self, mocked_ask_method): + # Make state + before = self.make_project_state([self.author_empty]) + after = self.make_project_state([self.author_dates_of_birth_auto_now_add]) + autodetector = MigrationAutodetector(before, after) + changes = autodetector._detect_changes() + # Right number/type of migrations? + self.assertNumberMigrations(changes, 'testapp', 1) + self.assertOperationTypes(changes, 'testapp', 0, ["AddField", "AddField", "AddField"]) + self.assertOperationFieldAttributes(changes, "testapp", 0, 0, auto_now_add=True) + self.assertOperationFieldAttributes(changes, "testapp", 0, 1, auto_now_add=True) + self.assertOperationFieldAttributes(changes, "testapp", 0, 2, auto_now_add=True) + def test_remove_field(self): """Tests autodetection of removed fields.""" # Make state diff --git a/tests/schema/tests.py b/tests/schema/tests.py index aa35bfddc92d..c57c81a495fe 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -3,13 +3,16 @@ import unittest from copy import copy +import mock + from django.db import ( DatabaseError, IntegrityError, OperationalError, connection, ) from django.db.models import Model from django.db.models.fields import ( AutoField, BigIntegerField, BinaryField, BooleanField, CharField, - DateTimeField, IntegerField, PositiveIntegerField, SlugField, TextField, + DateField, DateTimeField, IntegerField, PositiveIntegerField, SlugField, + TextField, TimeField, ) from django.db.models.fields.related import ( ForeignKey, ManyToManyField, OneToOneField, @@ -1535,3 +1538,81 @@ def test_add_field_use_effective_default(self): cursor.execute("SELECT surname FROM schema_author;") item = cursor.fetchall()[0] self.assertEqual(item[0], None if connection.features.interprets_empty_strings_as_nulls else '') + + @mock.patch('django.db.backends.base.schema.timezone') + def test_add_datefield_and_datetimefield_use_effective_default(self, mocked_tz): + """ + #25005 - effective_default() should be used for DateField, + DateTimeField and TimeField if auto_now or auto_add_now is set. + """ + now = datetime.datetime(month=1, day=1, year=2000, hour=1, minute=1) + mocked_tz.now = mock.MagicMock(return_value=now) + # Create the table + with connection.schema_editor() as editor: + editor.create_model(Author) + # Ensure there's no surname field + columns = self.column_classes(Author) + self.assertNotIn("date_of_birth_auto_now", columns) + self.assertNotIn("date_of_birth_auto_now_add", columns) + self.assertNotIn("date_time_of_birth_auto_now", columns) + self.assertNotIn("date_time_of_birth_auto_now_add", columns) + self.assertNotIn("time_of_birth_auto_now", columns) + self.assertNotIn("time_of_birth_auto_now_add", columns) + # Create a row + Author.objects.create(name='Anonymous1') + # Add new date and datetime fields to ensure default will be + # used from effective_default + date_of_birth_auto_now = DateField(auto_now=True) + date_of_birth_auto_now.set_attributes_from_name('date_of_birth_auto_now') + + date_of_birth_auto_now_add = DateField(auto_now_add=True) + date_of_birth_auto_now_add.set_attributes_from_name('date_of_birth_auto_now_add') + + date_time_of_birth_auto_now = DateTimeField(auto_now=True) + date_time_of_birth_auto_now.set_attributes_from_name('date_time_of_birth_auto_now') + + date_time_of_birth_auto_now_add = DateTimeField(auto_now_add=True) + date_time_of_birth_auto_now_add.set_attributes_from_name('date_time_of_birth_auto_now_add') + + time_of_birth_auto_now = TimeField(auto_now=True) + time_of_birth_auto_now.set_attributes_from_name('time_of_birth_auto_now') + + time_of_birth_auto_now_add = TimeField(auto_now_add=True) + time_of_birth_auto_now_add.set_attributes_from_name('time_of_birth_auto_now_add') + + # Ensure fields was added with the right defaults + with connection.schema_editor() as editor: + editor.add_field(Author, date_of_birth_auto_now) + with connection.cursor() as cursor: + cursor.execute("SELECT date_of_birth_auto_now FROM schema_author;") + self.assertEqual(cursor.fetchall()[0][0], now.date()) + + with connection.schema_editor() as editor: + editor.add_field(Author, date_of_birth_auto_now_add) + with connection.cursor() as cursor: + cursor.execute("SELECT date_of_birth_auto_now_add FROM schema_author;") + self.assertEqual(cursor.fetchall()[0][0], now.date()) + + with connection.schema_editor() as editor: + editor.add_field(Author, date_time_of_birth_auto_now) + with connection.cursor() as cursor: + cursor.execute("SELECT date_time_of_birth_auto_now FROM schema_author;") + self.assertEqual(cursor.fetchall()[0][0], now) + + with connection.schema_editor() as editor: + editor.add_field(Author, date_time_of_birth_auto_now_add) + with connection.cursor() as cursor: + cursor.execute("SELECT date_time_of_birth_auto_now_add FROM schema_author;") + self.assertEqual(cursor.fetchall()[0][0], now) + + with connection.schema_editor() as editor: + editor.add_field(Author, time_of_birth_auto_now) + with connection.cursor() as cursor: + cursor.execute("SELECT time_of_birth_auto_now FROM schema_author;") + self.assertEqual(cursor.fetchall()[0][0], now.time()) + + with connection.schema_editor() as editor: + editor.add_field(Author, time_of_birth_auto_now_add) + with connection.cursor() as cursor: + cursor.execute("SELECT time_of_birth_auto_now_add FROM schema_author;") + self.assertEqual(cursor.fetchall()[0][0], now.time())