Repository: just-work/django-admin-smoke (language: python)

Issue:
Incorrect assert while checking created
Current implementation looks for last added object using `order_by`, but this does not help with manual PK and UUIDs.

* Get object by fetching redirect url and looking to response template context
* If model has `created` `DateTime` field with `auto_now_add`, use `order_by('created')` to fetch created object and compare admin changeform url for it with redirect url


Discussion / hints:

Fix patch:
diff --git a/pyproject.toml b/pyproject.toml
index 27ee648..2b09e7c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -17,7 +17,7 @@ authors = [
 
 dependencies = [
     "Django>=3.2,<5.0",
-    "django-testing-utils~=0.5.0",
+    "django-testing-utils~=0.6.0",
 ]
 license = { text = "MIT" }
 keywords = [


Test patch:
diff --git a/src/admin_smoke/tests.py b/src/admin_smoke/tests.py
index eda93ea..d04620f 100644
--- a/src/admin_smoke/tests.py
+++ b/src/admin_smoke/tests.py
@@ -1,6 +1,6 @@
 from datetime import timedelta
 from typing import (Any, TypeVar, Type, Union, Optional, Iterable,
-                    cast, Dict, List, TYPE_CHECKING, ClassVar, Generic)
+                    Dict, List, TYPE_CHECKING, ClassVar, Generic)
 
 from django.contrib.admin import ModelAdmin, site
 from django.contrib.auth import get_user_model
@@ -157,7 +157,9 @@ def get_form_data(form: ModelForm) -> Dict[str, Any]:
             data[key] = v
         return data
 
-    def get_form_data_from_response(self, r: HttpResponseBase) -> Dict[str, Any]:
+    def get_form_data_from_response(
+        self, r: HttpResponseBase
+    ) -> Dict[str, Any]:
         """ Get form data from response context."""
         data = {'_continue': 'save and continue'}
         cd = getattr(r, 'context_data')
@@ -206,6 +208,8 @@ def get_errors_from_response(r: HttpResponseBase) -> Dict[str, ErrorList]:
 # noinspection PyAbstractClass
 class CommonAdminTests(CommonAdminTestsTarget):
     """ Common smoke tests for django admin."""
+    created_field = 'created'
+    modified_field = 'modified'
 
     def test_changelist(self) -> None:
         """ Object list ist rendered correctly."""
@@ -230,6 +234,14 @@ def assert_row_count(self, url: str,
         marker = f'<p class="paginator">{count} {n}{save}</p>'
         self.assertInHTML(marker, text)
 
+    def assert_auto_now_add_created(self, obj: M) -> None:
+        # checks TimeStampModel.created timestamp
+        self.assert_object_fields(obj, **{self.created_field: self.now})
+
+    def assert_auto_now_modified(self, obj: M) -> None:
+        # checks TimeStampModel.modified timestamp
+        self.assert_object_fields(obj, **{self.modified_field: self.now})
+
     def post_changeform(self, create: bool = False,
                         erase: Union[None, str, Iterable[str]] = None,
                         fields: Optional[Dict[str, Any]] = None
@@ -277,25 +289,23 @@ def test_changeform_save(self) -> None:
         self.assertEqual(r.status_code, 302)
 
         obj = self.get_object()
-        if hasattr(obj, 'modified'):
-            # check whether TimeStampedModel.modified timestamp has changed
-            self.assert_object_fields(obj, modified=self.now)
+        if self.modified_field and hasattr(obj, self.modified_field):
+            self.assert_auto_now_modified(obj)
 
     def test_changeform_create(self) -> None:
         """ New object created correctly."""
         c = self.model.objects.count()
         self.now += second
+        existing = list(self.model.objects.values_list('pk', flat=True))
 
         r = self.post_changeform(create=True)
         self.assertFalse(self.get_errors_from_response(r))
         self.assertEqual(r.status_code, 302)
         self.assertEqual(self.model.objects.count(), c + 1)
         assert self.opts.pk is not None
-        obj = cast(models.Model,
-                   self.model.objects.order_by(self.opts.pk.name).last())
-        if hasattr(obj, 'created'):
-            # checks TimeStampModel.created timestamp
-            self.assertEqual(obj.created, self.now)  # type: ignore
+        obj = self.model.objects.exclude(pk__in=existing).get()
+        if self.created_field and hasattr(obj, self.created_field):
+            self.assert_auto_now_add_created(obj)
 
     def test_all_fields_present(self) -> None:
         """ All not excluded fields are present on the form. """
diff --git a/tests/testproject/testapp/migrations/0001_initial.py b/tests/testproject/testapp/migrations/0001_initial.py
index a21f296..de0ef4f 100644
--- a/tests/testproject/testapp/migrations/0001_initial.py
+++ b/tests/testproject/testapp/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 3.0.5 on 2020-04-10 16:54
+# Generated by Django 4.1.13 on 2024-03-26 09:58
 
 from django.conf import settings
 from django.db import migrations, models
@@ -27,7 +27,9 @@ class Migration(migrations.Migration):
             fields=[
                 ('pid', models.AutoField(primary_key=True, serialize=False)),
                 ('name', models.CharField(max_length=10, unique=True)),
-                ('client', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='testapp.Client')),
+                ('created', models.DateTimeField(auto_now_add=True)),
+                ('modified', models.DateTimeField(auto_now=True)),
+                ('client', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='testapp.client')),
                 ('members', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)),
             ],
         ),
@@ -38,7 +40,7 @@ class Migration(migrations.Migration):
                 ('name', models.CharField(max_length=10, unique=True)),
                 ('attachment', models.FileField(upload_to='')),
                 ('visible', models.BooleanField(default=True)),
-                ('project', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='testapp.Project')),
+                ('project', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='testapp.project')),
             ],
         ),
         migrations.CreateModel(
@@ -47,7 +49,7 @@ class Migration(migrations.Migration):
                 ('tid', models.AutoField(primary_key=True, serialize=False)),
                 ('name', models.CharField(blank=True, max_length=32)),
                 ('object_id', models.IntegerField()),
-                ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
+                ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
             ],
         ),
     ]
diff --git a/tests/testproject/testapp/migrations/0002_project_created.py b/tests/testproject/testapp/migrations/0002_project_created.py
deleted file mode 100644
index 618dd6e..0000000
--- a/tests/testproject/testapp/migrations/0002_project_created.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Generated by Django 3.0.4 on 2020-04-13 13:16
-
-from django.db import migrations, models
-import django.utils.timezone
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('testapp', '0001_initial'),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name='project',
-            name='created',
-            field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
-            preserve_default=False,
-        ),
-    ]
diff --git a/tests/testproject/testapp/models.py b/tests/testproject/testapp/models.py
index add1742..a73b8d6 100644
--- a/tests/testproject/testapp/models.py
+++ b/tests/testproject/testapp/models.py
@@ -1,6 +1,3 @@
-from typing import Type
-
-from django.contrib.auth import get_user_model
 from django.contrib.auth.models import User
 from django.contrib.contenttypes import fields
 from django.contrib.contenttypes.models import ContentType
@@ -27,6 +24,7 @@ class Project(models.Model):
     members = models.ManyToManyField(User, blank=True)
     tags = fields.GenericRelation(Tag)
     created = models.DateTimeField(auto_now_add=True, editable=False)
+    modified = models.DateTimeField(auto_now=True, editable=False)
 
 
 class Task(models.Model):