From 3ebd048cf78048c7a6153701d182a2f683277ba3 Mon Sep 17 00:00:00 2001 From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:10:07 +0200 Subject: [PATCH 1/5] Bumped versions in Github actions configuration. --- .github/workflows/benchmark.yml | 4 ++-- .github/workflows/check-migrations.yml | 2 +- .github/workflows/check_commit_messages.yml | 4 ++-- .github/workflows/check_pr_quality.yml | 2 +- .github/workflows/coverage_comment.yml | 2 +- .github/workflows/coverage_tests.yml | 4 ++-- .github/workflows/docs.yml | 2 +- .github/workflows/linters.yml | 10 +++++----- .github/workflows/postgis.yml | 2 +- .github/workflows/python_matrix.yml | 4 ++-- .github/workflows/schedule_tests.yml | 12 ++++++------ .github/workflows/screenshots.yml | 6 +++--- .github/workflows/selenium.yml | 4 ++-- .github/workflows/tests.yml | 6 +++--- 14 files changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index e8107ca54470..d0afeaf0e7d4 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -14,7 +14,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout Benchmark Repo - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: repository: django/django-asv path: "." @@ -28,7 +28,7 @@ jobs: - name: Install Requirements run: pip install -r requirements.txt - name: Cache Django - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: Django/* key: Django diff --git a/.github/workflows/check-migrations.yml b/.github/workflows/check-migrations.yml index 8b29b2bb34f7..8c17f6d68939 100644 --- a/.github/workflows/check-migrations.yml +++ b/.github/workflows/check-migrations.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false diff --git a/.github/workflows/check_commit_messages.yml b/.github/workflows/check_commit_messages.yml index 0dbb36a9990e..2be113810101 100644 --- a/.github/workflows/check_commit_messages.yml +++ b/.github/workflows/check_commit_messages.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false @@ -76,7 +76,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false diff --git a/.github/workflows/check_pr_quality.yml b/.github/workflows/check_pr_quality.yml index 417ae5d20198..f7d853ed99dd 100644 --- a/.github/workflows/check_pr_quality.yml +++ b/.github/workflows/check_pr_quality.yml @@ -21,7 +21,7 @@ jobs: timeout-minutes: 5 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false # Checking out the default branch (not the PR head) is what makes diff --git a/.github/workflows/coverage_comment.yml b/.github/workflows/coverage_comment.yml index 00e34f09aaf1..c826b8a7257b 100644 --- a/.github/workflows/coverage_comment.yml +++ b/.github/workflows/coverage_comment.yml @@ -21,7 +21,7 @@ jobs: timeout-minutes: 60 steps: - name: Download coverage artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: coverage-artifacts github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/coverage_tests.yml b/.github/workflows/coverage_tests.yml index fa43897a2bdb..43d15b510ddc 100644 --- a/.github/workflows/coverage_tests.yml +++ b/.github/workflows/coverage_tests.yml @@ -35,7 +35,7 @@ jobs: --health-retries 5 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 persist-credentials: false @@ -93,7 +93,7 @@ jobs: - name: Upload artifacts if: success() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: coverage-artifacts path: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 04a93af0d710..a993fdca605d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -28,7 +28,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: Set up Python diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 5ad4ec881e5f..1416fd6d275d 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -24,7 +24,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: Set up Python @@ -44,7 +44,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: Set up Python @@ -64,7 +64,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: black @@ -75,7 +75,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: Run zizmor @@ -89,7 +89,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: Set up Node.js diff --git a/.github/workflows/postgis.yml b/.github/workflows/postgis.yml index 13e2af8552b2..7fb6d219f97a 100644 --- a/.github/workflows/postgis.yml +++ b/.github/workflows/postgis.yml @@ -32,7 +32,7 @@ jobs: name: PostgreSQL ${{ matrix.pg_major }} steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: Setup Miniforge diff --git a/.github/workflows/python_matrix.yml b/.github/workflows/python_matrix.yml index 921497698f3a..4d31af141a6e 100644 --- a/.github/workflows/python_matrix.yml +++ b/.github/workflows/python_matrix.yml @@ -23,7 +23,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - id: set-matrix @@ -40,7 +40,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/schedule_tests.yml b/.github/workflows/schedule_tests.yml index fa9081e95767..ae1f62022d58 100644 --- a/.github/workflows/schedule_tests.yml +++ b/.github/workflows/schedule_tests.yml @@ -25,7 +25,7 @@ jobs: continue-on-error: true steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: Set up Python @@ -46,7 +46,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: Set up Python @@ -75,7 +75,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: Set up Node.js @@ -95,7 +95,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: Set up Python @@ -134,7 +134,7 @@ jobs: --health-retries 5 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: Set up Python @@ -182,7 +182,7 @@ jobs: --health-retries 5 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: Set up Python diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index a3e587b30bbd..ae57ce71cf80 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -21,7 +21,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: Set up Python @@ -43,7 +43,7 @@ jobs: run: python -Wall runtests.py --verbosity=2 --noinput --selenium=chrome --headless --screenshots --settings=test_sqlite --parallel=1 - name: Cache oxipng - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ~/.cargo/ key: ${{ runner.os }}-cargo @@ -60,7 +60,7 @@ jobs: mv tests/screenshots/* "/tmp/screenshots/${{ github.event.pull_request.head.sha }}/" - name: Upload screenshots - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: screenshots-${{ github.event.pull_request.head.sha }} path: /tmp/screenshots/ diff --git a/.github/workflows/selenium.yml b/.github/workflows/selenium.yml index a42c0ea2c82e..c93808f92059 100644 --- a/.github/workflows/selenium.yml +++ b/.github/workflows/selenium.yml @@ -21,7 +21,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: Set up Python @@ -61,7 +61,7 @@ jobs: --health-retries 5 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: Set up Python diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 29010d64df37..d6e8af7fe118 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,7 +28,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: Set up Python @@ -49,7 +49,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: Set up Node.js @@ -69,7 +69,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false - name: Set up Python From 7b8504eaabea6c70195baa3c02155c24a6e270c6 Mon Sep 17 00:00:00 2001 From: Hanny Date: Fri, 4 Oct 2024 12:43:56 -0500 Subject: [PATCH 2/5] Fixed #35813 -- Made migrations track all changes to unmanaged models. Previously, only create and delete operations were generated. --- django/db/migrations/autodetector.py | 59 +++------ docs/releases/6.2.txt | 5 + tests/migrations/test_autodetector.py | 171 +++++++++++++++++++++++++- 3 files changed, 190 insertions(+), 45 deletions(-) diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py index 0c2e215fcd5a..0d0010f868b5 100644 --- a/django/db/migrations/autodetector.py +++ b/django/db/migrations/autodetector.py @@ -3,7 +3,6 @@ from collections import defaultdict, namedtuple from enum import Enum from graphlib import TopologicalSorter -from itertools import chain from django.conf import settings from django.db import models @@ -152,23 +151,17 @@ def _detect_changes(self, convert_apps=None, graph=None): # proxy models and ignoring unmigrated apps. self.old_model_keys = set() self.old_proxy_keys = set() - self.old_unmanaged_keys = set() self.new_model_keys = set() self.new_proxy_keys = set() - self.new_unmanaged_keys = set() for (app_label, model_name), model_state in self.from_state.models.items(): - if not model_state.options.get("managed", True): - self.old_unmanaged_keys.add((app_label, model_name)) - elif app_label not in self.from_state.real_apps: + if app_label not in self.from_state.real_apps: if model_state.options.get("proxy"): self.old_proxy_keys.add((app_label, model_name)) else: self.old_model_keys.add((app_label, model_name)) for (app_label, model_name), model_state in self.to_state.models.items(): - if not model_state.options.get("managed", True): - self.new_unmanaged_keys.add((app_label, model_name)) - elif app_label not in self.from_state.real_apps or ( + if app_label not in self.from_state.real_apps or ( convert_apps and app_label in convert_apps ): if model_state.options.get("proxy"): @@ -238,7 +231,6 @@ def _prepare_field_lists(self): """ self.kept_model_keys = self.old_model_keys & self.new_model_keys self.kept_proxy_keys = self.old_proxy_keys & self.new_proxy_keys - self.kept_unmanaged_keys = self.old_unmanaged_keys & self.new_unmanaged_keys self.through_users = {} self.old_field_keys = { (app_label, model_name, field_name) @@ -648,20 +640,17 @@ def generate_renamed_models(self): def generate_created_models(self): """ - Find all new models (both managed and unmanaged) and make create - operations for them as well as separate operations to create any - foreign key or M2M relationships (these are optimized later, if - possible). + Find all new models and make create operations for them as well as + separate operations to create any foreign key or M2M relationships + (these are optimized later, if possible). Defer any model options that refer to collections of fields that might be deferred (e.g. unique_together). """ - old_keys = self.old_model_keys | self.old_unmanaged_keys + old_keys = self.old_model_keys added_models = self.new_model_keys - old_keys - added_unmanaged_models = self.new_unmanaged_keys - old_keys - all_added_models = chain( - sorted(added_models, key=self.swappable_first_key, reverse=True), - sorted(added_unmanaged_models, key=self.swappable_first_key, reverse=True), + all_added_models = sorted( + added_models, key=self.swappable_first_key, reverse=True ) for app_label, model_name in all_added_models: model_state = self.to_state.models[app_label, model_name] @@ -755,11 +744,6 @@ def generate_created_models(self): beginning=True, ) - # Don't add operations which modify the database for unmanaged - # models - if not model_state.options.get("managed", True): - continue - # Generate operations for each related field for name, field in sorted(related_fields.items()): dependencies = self._get_dependencies_for_foreign_key( @@ -913,20 +897,16 @@ def generate_created_proxies(self): def generate_deleted_models(self): """ - Find all deleted models (managed and unmanaged) and make delete - operations for them as well as separate operations to delete any - foreign key or M2M relationships (these are optimized later, if - possible). + Find all deleted models and make delete operations for them as well + as separate operations to delete any foreign key or M2M relationships + (these are optimized later, if possible). Also bring forward removal of any model options that refer to collections of fields - the inverse of generate_created_models(). """ - new_keys = self.new_model_keys | self.new_unmanaged_keys + new_keys = self.new_model_keys deleted_models = self.old_model_keys - new_keys - deleted_unmanaged_models = self.old_unmanaged_keys - new_keys - all_deleted_models = chain( - sorted(deleted_models), sorted(deleted_unmanaged_models) - ) + all_deleted_models = sorted(deleted_models) for app_label, model_name in all_deleted_models: model_state = self.from_state.models[app_label, model_name] # Gather related fields @@ -1840,9 +1820,7 @@ def generate_altered_unique_together(self): self._generate_altered_foo_together(operations.AlterUniqueTogether) def generate_altered_db_table(self): - models_to_check = self.kept_model_keys.union( - self.kept_proxy_keys, self.kept_unmanaged_keys - ) + models_to_check = self.kept_model_keys.union(self.kept_proxy_keys) for app_label, model_name in sorted(models_to_check): old_model_name = self.renamed_models.get( (app_label, model_name), model_name @@ -1861,9 +1839,7 @@ def generate_altered_db_table(self): ) def generate_altered_db_table_comment(self): - models_to_check = self.kept_model_keys.union( - self.kept_proxy_keys, self.kept_unmanaged_keys - ) + models_to_check = self.kept_model_keys.union(self.kept_proxy_keys) for app_label, model_name in sorted(models_to_check): old_model_name = self.renamed_models.get( (app_label, model_name), model_name @@ -1890,11 +1866,6 @@ def generate_altered_options(self): """ models_to_check = self.kept_model_keys.union( self.kept_proxy_keys, - self.kept_unmanaged_keys, - # unmanaged converted to managed - self.old_unmanaged_keys & self.new_model_keys, - # managed converted to unmanaged - self.old_model_keys & self.new_unmanaged_keys, ) for app_label, model_name in sorted(models_to_check): diff --git a/docs/releases/6.2.txt b/docs/releases/6.2.txt index f25dd80d0894..a25c012a37fc 100644 --- a/docs/releases/6.2.txt +++ b/docs/releases/6.2.txt @@ -178,6 +178,11 @@ Management Commands * The new :djadmin:`listurls` command lists the URLs from the project's root URLconf, including the view class or function (and name, if present). +* The :djadmin:`makemigrations` command now tracks all changes to unmanaged + models, including field additions, removals, alterations, constraints, and + model renames. After upgrading, you will see new migrations detected for + unmanaged models that have changed since their creation. + Migrations ~~~~~~~~~~ diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py index 7a66e500cb89..f2bc7e48a1d0 100644 --- a/tests/migrations/test_autodetector.py +++ b/tests/migrations/test_autodetector.py @@ -712,6 +712,72 @@ class AutodetectorTests(BaseAutodetectorTests): author_unmanaged = ModelState( "testapp", "AuthorUnmanaged", [], {"managed": False}, ("testapp.author",) ) + author_unmanaged_empty = ModelState( + "testapp", + "Author", + [("id", models.AutoField(primary_key=True))], + {"managed": False}, + ("testapp.author",), + ) + author_unmanaged_name_check_constraint = ModelState( + "testapp", + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=200, default="Ada Lovelace")), + ], + { + "managed": False, + "constraints": [ + models.CheckConstraint( + condition=models.Q(name__contains="Bob"), name="name_contains_bob" + ) + ], + }, + ("testapp.author",), + ) + author_unmanaged_with_book = ModelState( + "testapp", + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=200)), + ("book", models.ForeignKey("otherapp.Book", models.CASCADE)), + ], + {"managed": False}, + ("testapp.author",), + ) + author_unmanaged_name_default = ModelState( + "testapp", + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=200, default="Ada Lovelace")), + ], + {"managed": False}, + ("testapp.author",), + ) + author_unmanaged_name_longer = ModelState( + "testapp", + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=400)), + ], + {"managed": False}, + ("testapp.author",), + ) + author_unmanaged_renamed_with_book = ModelState( + "testapp", + "Writer", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=200)), + ("book", models.ForeignKey("otherapp.Book", models.CASCADE)), + ], + {"managed": False}, + ("testapp.author",), + ) author_unmanaged_managed = ModelState( "testapp", "AuthorUnmanaged", [], {}, ("testapp.author",) ) @@ -829,6 +895,17 @@ class AutodetectorTests(BaseAutodetectorTests): ], {"db_table": "author_three"}, ) + book_unmanaged = ModelState( + "otherapp", + "Book", + [ + ("id", models.AutoField(primary_key=True)), + ("author", models.ForeignKey("testapp.Author", models.CASCADE)), + ("title", models.CharField(max_length=200)), + ], + {"managed": False}, + ("otherapp.book",), + ) contract = ModelState( "testapp", "Contract", @@ -968,6 +1045,17 @@ class AutodetectorTests(BaseAutodetectorTests): ("title", models.CharField(max_length=200)), ], ) + book_with_author_unmanaged_renamed = ModelState( + "otherapp", + "Book", + [ + ("id", models.AutoField(primary_key=True)), + ("author", models.ForeignKey("testapp.Writer", models.CASCADE)), + ("title", models.CharField(max_length=200)), + ], + {"managed": False}, + ("otherapp.book",), + ) book_with_field_and_author_renamed = ModelState( "otherapp", "Book", @@ -4019,8 +4107,45 @@ def test_proxy_to_mti_with_fk_to_proxy_proxy(self): ) self.assertEqual(fk_field.remote_field.model, "testapp.AAuthorProxyProxy") + def test_unmanaged_add_constraints(self): + """Test change detection of new constraints on an unmanaged model.""" + changes = self.get_changes( + [self.author_unmanaged_name_default], + [self.author_unmanaged_name_check_constraint], + ) + self.assertNumberMigrations(changes, "testapp", 1) + self.assertOperationTypes(changes, "testapp", 0, ["AddConstraint"]) + added_constraint = models.CheckConstraint( + condition=models.Q(name__contains="Bob"), name="name_contains_bob" + ) + self.assertOperationAttributes( + changes, "testapp", 0, 0, model_name="author", constraint=added_constraint + ) + + def test_unmanaged_add_field(self): + """Tests autodetection of new fields on an unmanaged model.""" + changes = self.get_changes( + [self.author_unmanaged_empty], [self.author_unmanaged_name_default] + ) + # Right number/type of migrations? + self.assertNumberMigrations(changes, "testapp", 1) + self.assertOperationTypes(changes, "testapp", 0, ["AddField"]) + self.assertOperationAttributes(changes, "testapp", 0, 0, name="name") + + def test_unmanaged_alter_field(self): + """Tests autodetection of altered fields on an unmanaged model.""" + changes = self.get_changes( + [self.author_unmanaged_name_default], [self.author_unmanaged_name_longer] + ) + # Right number/type of migrations? + self.assertNumberMigrations(changes, "testapp", 1) + self.assertOperationTypes(changes, "testapp", 0, ["AlterField"]) + self.assertOperationAttributes( + changes, "testapp", 0, 0, name="name", preserve_default=True + ) + def test_unmanaged_create(self): - """The autodetector correctly deals with managed models.""" + """The autodetector correctly deals with unmanaged models.""" # First, we test adding an unmanaged model changes = self.get_changes( [self.author_empty], [self.author_empty, self.author_unmanaged] @@ -4032,6 +4157,50 @@ def test_unmanaged_create(self): changes, "testapp", 0, 0, name="AuthorUnmanaged", options={"managed": False} ) + def test_unmanaged_remove_field(self): + """Tests autodetection of removed fields on an unmanaged model.""" + changes = self.get_changes( + [self.author_unmanaged_name_default], [self.author_unmanaged_empty] + ) + # Right number/type of migrations? + self.assertNumberMigrations(changes, "testapp", 1) + self.assertOperationTypes(changes, "testapp", 0, ["RemoveField"]) + self.assertOperationAttributes( + changes, "testapp", 0, 0, name="name", model_name="author" + ) + + def test_unmanaged_remove_constraints(self): + """Test change detection of new constraints on an umnanaged model.""" + changes = self.get_changes( + [self.author_unmanaged_name_check_constraint], + [self.author_unmanaged_name_default], + ) + self.assertNumberMigrations(changes, "testapp", 1) + self.assertOperationTypes(changes, "testapp", 0, ["RemoveConstraint"]) + self.assertOperationAttributes( + changes, "testapp", 0, 0, model_name="author", name="name_contains_bob" + ) + + def test_unmanaged_rename_model(self): + """Tests autodetection of renamed models on an unmanaged model.""" + changes = self.get_changes( + [self.author_unmanaged_with_book, self.book_unmanaged], + [ + self.author_unmanaged_renamed_with_book, + self.book_with_author_unmanaged_renamed, + ], + MigrationQuestioner({"ask_rename_model": True}), + ) + # Right number/type of migrations? + self.assertNumberMigrations(changes, "testapp", 1) + self.assertOperationTypes(changes, "testapp", 0, ["RenameModel"]) + self.assertOperationAttributes( + changes, "testapp", 0, 0, old_name="Author", new_name="Writer" + ) + # Now that RenameModel handles related fields too, there should be + # no AlterField for the related field. + self.assertNumberMigrations(changes, "otherapp", 0) + def test_unmanaged_delete(self): changes = self.get_changes( [self.author_empty, self.author_unmanaged], [self.author_empty] From ecc619703ad22d7707c14c11b14e3bea9684b3b6 Mon Sep 17 00:00:00 2001 From: Varun Kasyap Date: Fri, 10 Jul 2026 11:20:08 +0200 Subject: [PATCH 3/5] Fixed #37180 -- Fixed admin SelectBox height tests to verify available/chosen box heights. --- tests/admin_views/tests.py | 36 ++++++++---------------------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index eecd29536bdf..59606e1a11ea 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -6674,19 +6674,11 @@ def test_selectbox_height_collapsible_fieldset(self): url = self.live_server_url + reverse("admin7:admin_views_pizza_add") self.selenium.get(url) self.selenium.find_elements(By.TAG_NAME, "summary")[0].click() - from_filter_box = self.selenium.find_element(By.ID, "id_toppings_filter") - from_box = self.selenium.find_element(By.ID, "id_toppings_from") - to_filter_box = self.selenium.find_element(By.ID, "id_toppings_filter_selected") - to_box = self.selenium.find_element(By.ID, "id_toppings_to") + available_box = self.selenium.find_element(By.CLASS_NAME, "selector-available") + chosen_box = self.selenium.find_element(By.CLASS_NAME, "selector-chosen") self.assertEqual( - ( - to_filter_box.get_property("offsetHeight") - + to_box.get_property("offsetHeight") - ), - ( - from_filter_box.get_property("offsetHeight") - + from_box.get_property("offsetHeight") - ), + available_box.get_property("offsetHeight"), + chosen_box.get_property("offsetHeight"), ) self.take_screenshot("selectbox-collapsible") @@ -6701,23 +6693,11 @@ def test_selectbox_height_not_collapsible_fieldset(self): ) url = self.live_server_url + reverse("admin7:admin_views_question_add") self.selenium.get(url) - from_filter_box = self.selenium.find_element( - By.ID, "id_related_questions_filter" - ) - from_box = self.selenium.find_element(By.ID, "id_related_questions_from") - to_filter_box = self.selenium.find_element( - By.ID, "id_related_questions_filter_selected" - ) - to_box = self.selenium.find_element(By.ID, "id_related_questions_to") + available_box = self.selenium.find_element(By.CLASS_NAME, "selector-available") + chosen_box = self.selenium.find_element(By.CLASS_NAME, "selector-chosen") self.assertEqual( - ( - to_filter_box.get_property("offsetHeight") - + to_box.get_property("offsetHeight") - ), - ( - from_filter_box.get_property("offsetHeight") - + from_box.get_property("offsetHeight") - ), + available_box.get_property("offsetHeight"), + chosen_box.get_property("offsetHeight"), ) self.take_screenshot("selectbox-non-collapsible") From 4106acd141b614ca708c3d932ec8795aae45f5e2 Mon Sep 17 00:00:00 2001 From: Varun Kasyap Date: Fri, 10 Jul 2026 11:55:46 +0200 Subject: [PATCH 4/5] Fixed #37182 -- Corrected property typo in skip link to content test. --- tests/admin_views/test_skip_link_to_content.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/admin_views/test_skip_link_to_content.py b/tests/admin_views/test_skip_link_to_content.py index 5cec63307bf7..a32dd8108c82 100644 --- a/tests/admin_views/test_skip_link_to_content.py +++ b/tests/admin_views/test_skip_link_to_content.py @@ -123,7 +123,7 @@ def test_skip_link_with_RTL_language_doesnt_create_horizontal_scrolling(self): "return arguments[0].scrollHeight > arguments[0].offsetHeight;", body ) is_horizontal_scrolleable = self.selenium.execute_script( - "return arguments[0].scrollWeight > arguments[0].offsetWeight;", body + "return arguments[0].scrollWidth > arguments[0].offsetWidth;", body ) self.assertTrue(is_vertical_scrolleable) self.assertFalse(is_horizontal_scrolleable) From f51347964a85bd4881caabf3c736b2c54d75262f Mon Sep 17 00:00:00 2001 From: Varun Kasyap Date: Mon, 22 Jun 2026 21:41:14 +0530 Subject: [PATCH 5/5] Fixed #37181 -- Corrected assertion in SeleniumTests.test_pagination. --- tests/admin_views/test_history_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/admin_views/test_history_view.py b/tests/admin_views/test_history_view.py index 49a60aa306ed..4015f139f3b3 100644 --- a/tests/admin_views/test_history_view.py +++ b/tests/admin_views/test_history_view.py @@ -99,7 +99,7 @@ def test_pagination(self): self.assertEqual(current_page_link.text, "1") # The last page. last_page_link = self.selenium.find_element(By.XPATH, "//ul/li[last()]/a") - self.assertTrue(last_page_link.text, "20") + self.assertEqual(last_page_link.text, "11") # Select the second page. pages = paginator.find_elements(By.TAG_NAME, "a") second_page_link = pages[1]