From e00af4418c929942822ecc207b9db94a8ed79f3d Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 2 Oct 2025 11:42:49 -0400 Subject: [PATCH 01/19] Try to run checks with Django 5.2 --- .github/workflows/ci.yml | 12 ++++++++++++ pyproject.toml | 26 +++++++++++++++++++++++++- requirements/requirements.in | 2 +- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d8574f27..53beba311 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,18 @@ jobs: python-version: "3.11" sonar: false junit-xml-upload: false + - env: py310-django5 + python-version: "3.10" + sonar: false + junit-xml-upload: false + - env: py311-django5 + python-version: "3.11" + sonar: false + junit-xml-upload: false + - env: py312-django5 + python-version: "3.12" + sonar: false + junit-xml-upload: false steps: - uses: actions/checkout@v4 with: diff --git a/pyproject.toml b/pyproject.toml index 639fc48ce..57bbf8978 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,8 +102,11 @@ legacy_tox_ini = """ py310 py311 py311sqlite + py310-django5 + py311-django5 + py312-django5 labels = - test = py312, py310, py311, py311sqlite, check + test = py312, py310, py311, py311sqlite, py310-django5, py311-django5, py312-django5, check lint = flake8, black, isort [testenv] @@ -128,6 +131,27 @@ legacy_tox_ini = """ setenv = DJANGO_SETTINGS_MODULE = test_app.sqlite3settings + [testenv:py310-django5] + deps = + Django>=5.2 + -r{toxinidir}/requirements/requirements_all.txt + -r{toxinidir}/requirements/requirements_dev.txt + docker = db + + [testenv:py311-django5] + deps = + Django>=5.2 + -r{toxinidir}/requirements/requirements_all.txt + -r{toxinidir}/requirements/requirements_dev.txt + docker = db + + [testenv:py312-django5] + deps = + Django>=5.2 + -r{toxinidir}/requirements/requirements_all.txt + -r{toxinidir}/requirements/requirements_dev.txt + docker = db + [docker:db] dockerfile = {toxinidir}/tools/dev_postgres/Dockerfile expose = diff --git a/requirements/requirements.in b/requirements/requirements.in index a9a6767b9..6072158d9 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -3,7 +3,7 @@ # if you are add a new feature which requires dependencies they should be in a separate requirements_.in file # cryptography -Django>=4.2.21,<4.3.0 # CVE-2024-45230, CVE-2024-56374 +Django>=5.2 djangorestframework django-crum inflection From 0ca3a15cc84a09eeb5bd7c82b8bf1f7a93a9df9a Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 2 Oct 2025 12:06:01 -0400 Subject: [PATCH 02/19] No need for this, question mark --- requirements/requirements_all.txt | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/requirements/requirements_all.txt b/requirements/requirements_all.txt index 6e5317289..924095d29 100644 --- a/requirements/requirements_all.txt +++ b/requirements/requirements_all.txt @@ -24,18 +24,6 @@ defusedxml==0.8.0rc2 # via # python3-openid # social-auth-core -django==4.2.21 - # via - # -r requirements/requirements.in - # channels - # django-auth-ldap - # django-crum - # django-flags - # django-oauth-toolkit - # django-redis - # djangorestframework - # drf-spectacular - # social-auth-app-django django-auth-ldap==5.1.0 # via -r requirements/requirements_authentication.in django-crum==0.7.9 From b2303457027b6697249c36df82f2b934b6a7c010 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 2 Oct 2025 12:21:27 -0400 Subject: [PATCH 03/19] Delete more --- requirements/requirements_dev.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt index 1b651c8e4..92bdee053 100644 --- a/requirements/requirements_dev.txt +++ b/requirements/requirements_dev.txt @@ -1,7 +1,6 @@ ansible # Used in build process to generate some configs black==25.1.0 # Linting tool, if changed update pyproject.toml as well build -django==4.2.21 django-debug-toolbar django-extensions djangorestframework From ab3e31e5bdc01bfccea7666dfaaf6429fb5a73bd Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 2 Oct 2025 14:49:41 -0400 Subject: [PATCH 04/19] Try to keep current working --- pyproject.toml | 9 ++++++--- requirements/requirements.in | 2 +- requirements/requirements_all.txt | 12 ++++++++++++ requirements/requirements_dev.txt | 1 + 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 57bbf8978..7f68c6c8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -133,23 +133,26 @@ legacy_tox_ini = """ [testenv:py310-django5] deps = - Django>=5.2 -r{toxinidir}/requirements/requirements_all.txt -r{toxinidir}/requirements/requirements_dev.txt + commands_pre = + python -m pip install --upgrade "Django==5.2.8" docker = db [testenv:py311-django5] deps = - Django>=5.2 -r{toxinidir}/requirements/requirements_all.txt -r{toxinidir}/requirements/requirements_dev.txt + commands_pre = + python -m pip install --upgrade "Django==5.2.8" docker = db [testenv:py312-django5] deps = - Django>=5.2 -r{toxinidir}/requirements/requirements_all.txt -r{toxinidir}/requirements/requirements_dev.txt + commands_pre = + python -m pip install --upgrade "Django==5.2.8" docker = db [docker:db] diff --git a/requirements/requirements.in b/requirements/requirements.in index 6072158d9..a9a6767b9 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -3,7 +3,7 @@ # if you are add a new feature which requires dependencies they should be in a separate requirements_.in file # cryptography -Django>=5.2 +Django>=4.2.21,<4.3.0 # CVE-2024-45230, CVE-2024-56374 djangorestframework django-crum inflection diff --git a/requirements/requirements_all.txt b/requirements/requirements_all.txt index 924095d29..6e5317289 100644 --- a/requirements/requirements_all.txt +++ b/requirements/requirements_all.txt @@ -24,6 +24,18 @@ defusedxml==0.8.0rc2 # via # python3-openid # social-auth-core +django==4.2.21 + # via + # -r requirements/requirements.in + # channels + # django-auth-ldap + # django-crum + # django-flags + # django-oauth-toolkit + # django-redis + # djangorestframework + # drf-spectacular + # social-auth-app-django django-auth-ldap==5.1.0 # via -r requirements/requirements_authentication.in django-crum==0.7.9 diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt index 92bdee053..1b651c8e4 100644 --- a/requirements/requirements_dev.txt +++ b/requirements/requirements_dev.txt @@ -1,6 +1,7 @@ ansible # Used in build process to generate some configs black==25.1.0 # Linting tool, if changed update pyproject.toml as well build +django==4.2.21 django-debug-toolbar django-extensions djangorestframework From 6e50ea30aeab92b570cf82930f6091f137496161 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 2 Oct 2025 16:19:43 -0400 Subject: [PATCH 05/19] dot-8 was too new --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7f68c6c8e..586b31035 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,7 +136,7 @@ legacy_tox_ini = """ -r{toxinidir}/requirements/requirements_all.txt -r{toxinidir}/requirements/requirements_dev.txt commands_pre = - python -m pip install --upgrade "Django==5.2.8" + python -m pip install --upgrade "Django==5.2.7" docker = db [testenv:py311-django5] @@ -144,7 +144,7 @@ legacy_tox_ini = """ -r{toxinidir}/requirements/requirements_all.txt -r{toxinidir}/requirements/requirements_dev.txt commands_pre = - python -m pip install --upgrade "Django==5.2.8" + python -m pip install --upgrade "Django==5.2.7" docker = db [testenv:py312-django5] @@ -152,7 +152,7 @@ legacy_tox_ini = """ -r{toxinidir}/requirements/requirements_all.txt -r{toxinidir}/requirements/requirements_dev.txt commands_pre = - python -m pip install --upgrade "Django==5.2.8" + python -m pip install --upgrade "Django==5.2.7" docker = db [docker:db] From 1f9ad80463139faa89c66a8f53e954f76c8ddad0 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 3 Oct 2025 08:45:52 -0400 Subject: [PATCH 06/19] apply the fix I guess --- ansible_base/resource_registry/fields.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ansible_base/resource_registry/fields.py b/ansible_base/resource_registry/fields.py index 6ffcbc994..91be00d32 100644 --- a/ansible_base/resource_registry/fields.py +++ b/ansible_base/resource_registry/fields.py @@ -47,6 +47,10 @@ def get_prefetch_queryset(self, instances, queryset=None): False, ) + def get_prefetch_querysets(self, instances, queryset=None): + # Django 5 compatibility: renamed from get_prefetch_queryset + return self.get_prefetch_queryset(instances, queryset) + class AnsibleResourceField(models.ForeignObject): """ From 16c02c07da12563fc33b5ec5cfe24d7d6ec747f1 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 6 Oct 2025 08:39:36 -0400 Subject: [PATCH 07/19] Make Django 5 the default --- pyproject.toml | 20 ++++++++++---------- requirements/requirements.in | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 586b31035..7246936a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,11 +102,11 @@ legacy_tox_ini = """ py310 py311 py311sqlite - py310-django5 - py311-django5 - py312-django5 + py310-django4 + py311-django4 + py312-django4 labels = - test = py312, py310, py311, py311sqlite, py310-django5, py311-django5, py312-django5, check + test = py312, py310, py311, py311sqlite, py310-django4, py311-django4, py312-django4, check lint = flake8, black, isort [testenv] @@ -131,28 +131,28 @@ legacy_tox_ini = """ setenv = DJANGO_SETTINGS_MODULE = test_app.sqlite3settings - [testenv:py310-django5] + [testenv:py310-django4] deps = -r{toxinidir}/requirements/requirements_all.txt -r{toxinidir}/requirements/requirements_dev.txt commands_pre = - python -m pip install --upgrade "Django==5.2.7" + python -m pip install --upgrade "Django>=4.2.21,<4.3.0" docker = db - [testenv:py311-django5] + [testenv:py311-django4] deps = -r{toxinidir}/requirements/requirements_all.txt -r{toxinidir}/requirements/requirements_dev.txt commands_pre = - python -m pip install --upgrade "Django==5.2.7" + python -m pip install --upgrade "Django>=4.2.21,<4.3.0" docker = db - [testenv:py312-django5] + [testenv:py312-django4] deps = -r{toxinidir}/requirements/requirements_all.txt -r{toxinidir}/requirements/requirements_dev.txt commands_pre = - python -m pip install --upgrade "Django==5.2.7" + python -m pip install --upgrade "Django>=4.2.21,<4.3.0" docker = db [docker:db] diff --git a/requirements/requirements.in b/requirements/requirements.in index a9a6767b9..3aa21e1bd 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -3,7 +3,7 @@ # if you are add a new feature which requires dependencies they should be in a separate requirements_.in file # cryptography -Django>=4.2.21,<4.3.0 # CVE-2024-45230, CVE-2024-56374 +Django<6.0 djangorestframework django-crum inflection From 6c14b88bacb1e7280d614bf8ded65a16fc1d0293 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 6 Oct 2025 09:11:30 -0400 Subject: [PATCH 08/19] Update the requirement the right way --- requirements/requirements.in | 2 +- requirements/requirements_all.txt | 2 +- requirements/requirements_dev.txt | 2 +- requirements/updater.sh | 8 ++++++++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/requirements/requirements.in b/requirements/requirements.in index 3aa21e1bd..f11e791fa 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -3,7 +3,7 @@ # if you are add a new feature which requires dependencies they should be in a separate requirements_.in file # cryptography -Django<6.0 +Django>5.2.0,<6.0 djangorestframework django-crum inflection diff --git a/requirements/requirements_all.txt b/requirements/requirements_all.txt index 6e5317289..ad8006457 100644 --- a/requirements/requirements_all.txt +++ b/requirements/requirements_all.txt @@ -24,7 +24,7 @@ defusedxml==0.8.0rc2 # via # python3-openid # social-auth-core -django==4.2.21 +django==5.2.7 # via # -r requirements/requirements.in # channels diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt index 1b651c8e4..7a8463633 100644 --- a/requirements/requirements_dev.txt +++ b/requirements/requirements_dev.txt @@ -1,7 +1,7 @@ ansible # Used in build process to generate some configs black==25.1.0 # Linting tool, if changed update pyproject.toml as well build -django==4.2.21 +django==5.2.7 django-debug-toolbar django-extensions djangorestframework diff --git a/requirements/updater.sh b/requirements/updater.sh index 7c48c7428..bf5e6557f 100755 --- a/requirements/updater.sh +++ b/requirements/updater.sh @@ -3,6 +3,14 @@ set -ue PYTHON=python3.11 +# Ensure script is run from the requirements/ directory +if [[ "$(basename $(pwd))" != "requirements" ]]; then + echo "ERROR: This script must be run from the requirements/ directory" + echo "Current directory: $(pwd)" + echo "Please run: cd requirements && ./updater.sh [run|upgrade]" + exit 1 +fi + for FILE in requirements.in requirements_all.txt ; do if [ ! -f ${FILE} ] ; then touch ${FILE} From e1eb71bf40103a9834faadef0ab3313009b56855 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 6 Oct 2025 09:20:01 -0400 Subject: [PATCH 09/19] Make Django 4 the exception --- ansible_base/resource_registry/fields.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible_base/resource_registry/fields.py b/ansible_base/resource_registry/fields.py index 91be00d32..c280c4678 100644 --- a/ansible_base/resource_registry/fields.py +++ b/ansible_base/resource_registry/fields.py @@ -13,7 +13,7 @@ class CustomForwardOneToOneDescriptor(ForwardOneToOneDescriptor): def get_queryset(self, **hints): return self.field.remote_field.model._base_manager.db_manager(hints=hints).filter(content_type=ContentType.objects.get_for_model(self.field.model)) - def get_prefetch_queryset(self, instances, queryset=None): + def get_prefetch_querysets(self, instances, queryset=None): if not queryset: queryset = self.get_queryset() queryset._add_hints(instance=instances[0]) @@ -47,9 +47,9 @@ def get_prefetch_queryset(self, instances, queryset=None): False, ) - def get_prefetch_querysets(self, instances, queryset=None): - # Django 5 compatibility: renamed from get_prefetch_queryset - return self.get_prefetch_queryset(instances, queryset) + def get_prefetch_queryset(self, instances, queryset=None): + # Django 4 compatibility: renamed to get_prefetch_querysets in Django 5 + return self.get_prefetch_querysets(instances, queryset) class AnsibleResourceField(models.ForeignObject): From bd668787b1d9c12925aa1c4b2eee9c90b35449a2 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 6 Oct 2025 09:33:36 -0400 Subject: [PATCH 10/19] Prevent unintended downgrade and fix the environment matrix --- .github/workflows/ci.yml | 6 +++--- requirements/requirements.in | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53beba311..66b77d383 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,15 +34,15 @@ jobs: python-version: "3.11" sonar: false junit-xml-upload: false - - env: py310-django5 + - env: py310-django4 python-version: "3.10" sonar: false junit-xml-upload: false - - env: py311-django5 + - env: py311-django4 python-version: "3.11" sonar: false junit-xml-upload: false - - env: py312-django5 + - env: py312-django4 python-version: "3.12" sonar: false junit-xml-upload: false diff --git a/requirements/requirements.in b/requirements/requirements.in index f11e791fa..7aba5641a 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -3,7 +3,7 @@ # if you are add a new feature which requires dependencies they should be in a separate requirements_.in file # cryptography -Django>5.2.0,<6.0 +Django>=4.2.21,<6.0 djangorestframework django-crum inflection From d8710cb3c3ce99623dade8f42d4056add23267d0 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 6 Oct 2025 10:05:33 -0400 Subject: [PATCH 11/19] Add migration to pick up help text change --- .../0009_objectrole_help_text_change.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 ansible_base/rbac/migrations/0009_objectrole_help_text_change.py diff --git a/ansible_base/rbac/migrations/0009_objectrole_help_text_change.py b/ansible_base/rbac/migrations/0009_objectrole_help_text_change.py new file mode 100644 index 000000000..bffb59782 --- /dev/null +++ b/ansible_base/rbac/migrations/0009_objectrole_help_text_change.py @@ -0,0 +1,42 @@ +# Generated by Django 5.2.7 on 2025-10-06 13:50 +# +# This migration updates the help_text for the `users` and `teams` ManyToManyField +# on the ObjectRole model to add trailing periods for consistency. +# +# WHY DJANGO 5 CREATES THIS BUT DJANGO 4 DOES NOT: +# +# Django 4's migration detector has a bug/limitation where it does not properly detect +# help_text changes on ManyToManyField instances that use custom `through` tables. +# The help_text was updated in the model code (ansible_base/rbac/models/role.py:518, 525) +# to add trailing periods, but Django 4's makemigrations did not detect this change. +# +# Django 5 improved its field state serialization and comparison logic for ManyToManyFields, +# particularly for fields with through tables, and now properly detects these help_text changes. +# +# This is a harmless migration that only updates field metadata (help_text) and does not +# modify the database schema or affect data. + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dab_rbac', '0008_remote_permissions_cleanup'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + migrations.swappable_dependency(settings.ANSIBLE_BASE_TEAM_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='objectrole', + name='teams', + field=models.ManyToManyField(help_text='Teams or groups who have access to the permissions defined by this object role.', related_name='has_roles', through='dab_rbac.RoleTeamAssignment', through_fields=('object_role', 'team'), to=settings.ANSIBLE_BASE_TEAM_MODEL), + ), + migrations.AlterField( + model_name='objectrole', + name='users', + field=models.ManyToManyField(help_text='Users who have access to the permissions defined by this object role.', related_name='has_roles', through='dab_rbac.RoleUserAssignment', through_fields=('object_role', 'user'), to=settings.AUTH_USER_MODEL), + ), + ] From d412f610ce89a0403595ab6c84b082bf9d157cee Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 6 Oct 2025 11:08:24 -0400 Subject: [PATCH 12/19] Correctly handle plural vs singular queryset --- ansible_base/resource_registry/fields.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ansible_base/resource_registry/fields.py b/ansible_base/resource_registry/fields.py index c280c4678..591279ea2 100644 --- a/ansible_base/resource_registry/fields.py +++ b/ansible_base/resource_registry/fields.py @@ -13,9 +13,11 @@ class CustomForwardOneToOneDescriptor(ForwardOneToOneDescriptor): def get_queryset(self, **hints): return self.field.remote_field.model._base_manager.db_manager(hints=hints).filter(content_type=ContentType.objects.get_for_model(self.field.model)) - def get_prefetch_querysets(self, instances, queryset=None): - if not queryset: + def get_prefetch_querysets(self, instances, querysets=None): + if querysets is None: queryset = self.get_queryset() + else: + queryset = querysets[0] queryset._add_hints(instance=instances[0]) query = models.Q.create( @@ -49,7 +51,9 @@ def get_prefetch_querysets(self, instances, queryset=None): def get_prefetch_queryset(self, instances, queryset=None): # Django 4 compatibility: renamed to get_prefetch_querysets in Django 5 - return self.get_prefetch_querysets(instances, queryset) + if queryset is None: + return self.get_prefetch_querysets(instances) + return self.get_prefetch_querysets(instances, [queryset]) class AnsibleResourceField(models.ForeignObject): From 42c42894b9e402039f4cac814005a962ef881380 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 6 Oct 2025 11:33:24 -0400 Subject: [PATCH 13/19] Falsy value handling --- ansible_base/resource_registry/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible_base/resource_registry/fields.py b/ansible_base/resource_registry/fields.py index 591279ea2..7247a0c81 100644 --- a/ansible_base/resource_registry/fields.py +++ b/ansible_base/resource_registry/fields.py @@ -14,7 +14,7 @@ def get_queryset(self, **hints): return self.field.remote_field.model._base_manager.db_manager(hints=hints).filter(content_type=ContentType.objects.get_for_model(self.field.model)) def get_prefetch_querysets(self, instances, querysets=None): - if querysets is None: + if not querysets: queryset = self.get_queryset() else: queryset = querysets[0] From 6b1c0cfe65c2c64c3f6fd56e3c2a95e9ccaac64f Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 6 Oct 2025 13:20:30 -0400 Subject: [PATCH 14/19] Copy standard Django practice --- ansible_base/resource_registry/fields.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ansible_base/resource_registry/fields.py b/ansible_base/resource_registry/fields.py index 7247a0c81..8f78591f6 100644 --- a/ansible_base/resource_registry/fields.py +++ b/ansible_base/resource_registry/fields.py @@ -14,10 +14,12 @@ def get_queryset(self, **hints): return self.field.remote_field.model._base_manager.db_manager(hints=hints).filter(content_type=ContentType.objects.get_for_model(self.field.model)) def get_prefetch_querysets(self, instances, querysets=None): - if not querysets: - queryset = self.get_queryset() - else: - queryset = querysets[0] + if querysets and len(querysets) != 1: + raise ValueError( + "querysets argument of get_prefetch_querysets() should have a length " + "of 1." + ) + queryset = querysets[0] if querysets else self.get_queryset() queryset._add_hints(instance=instances[0]) query = models.Q.create( From 4262a90d4f33a96bc6df0af2800ec3d426535126 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 6 Oct 2025 13:39:55 -0400 Subject: [PATCH 15/19] Add a test for multi querysets --- ansible_base/resource_registry/fields.py | 5 +- .../models/test_resource_field.py | 47 ++++++++++++++++++- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/ansible_base/resource_registry/fields.py b/ansible_base/resource_registry/fields.py index 8f78591f6..8970f1a6d 100644 --- a/ansible_base/resource_registry/fields.py +++ b/ansible_base/resource_registry/fields.py @@ -15,10 +15,7 @@ def get_queryset(self, **hints): def get_prefetch_querysets(self, instances, querysets=None): if querysets and len(querysets) != 1: - raise ValueError( - "querysets argument of get_prefetch_querysets() should have a length " - "of 1." - ) + raise ValueError("querysets argument of get_prefetch_querysets() should have a length " "of 1.") queryset = querysets[0] if querysets else self.get_queryset() queryset._add_hints(instance=instances[0]) diff --git a/test_app/tests/resource_registry/models/test_resource_field.py b/test_app/tests/resource_registry/models/test_resource_field.py index b3ac4bd3f..fbbedd1e6 100644 --- a/test_app/tests/resource_registry/models/test_resource_field.py +++ b/test_app/tests/resource_registry/models/test_resource_field.py @@ -2,7 +2,7 @@ from django.contrib.contenttypes.models import ContentType from ansible_base.resource_registry.models import Resource -from test_app.models import Organization +from test_app.models import Inventory, Organization @pytest.mark.django_db @@ -100,3 +100,48 @@ def test_resource_field_filtering(organization): org = Organization.objects.get(resource__name=organization.name) assert org.resource.pk == resource.pk + + +@pytest.mark.django_db +def test_resource_field_prefetch_related_across_foreign_key(organization, organization_1, organization_2, django_assert_num_queries): + """ + Generated by Claude Code (claude-sonnet-4-5@20250929) + Test that prefetch_related works across a ForeignKey to a model with a resource field. + This tests prefetching organization__resource on Inventory objects. + """ + # Create inventory objects linked to organizations + Inventory.objects.create(name="Inventory 1", organization=organization) + Inventory.objects.create(name="Inventory 2", organization=organization_1) + Inventory.objects.create(name="Inventory 3", organization=organization_2) + + org_ctype = ContentType.objects.get_for_model(Organization) + + # Prefetch organization__resource should result in 3 queries: + # 1. Fetch all Inventory objects + # 2. Fetch all related Organization objects + # 3. Fetch all related Resource objects + with django_assert_num_queries(3) as captured: + inventory_qs = list(Inventory.objects.prefetch_related("organization__resource").all()) + + # Verify the queries were as expected + assert "test_app_inventory" in captured[0]["sql"] + assert "test_app_organization" in captured[1]["sql"] + assert "dab_resource_registry_resource" in captured[2]["sql"] + + assert len(inventory_qs) == 3 + + # Collect resource pks for later verification + resource_pks = {} + with django_assert_num_queries(0): + for inv in inventory_qs: + assert inv.organization is not None + assert inv.organization.resource is not None + # Verify the resource data is correct + assert inv.organization.name == inv.organization.resource.name + assert str(inv.organization.pk) == inv.organization.resource.object_id + resource_pks[inv.organization.pk] = inv.organization.resource.pk + + # Verify the resources match what's in the database + for org_pk, resource_pk in resource_pks.items(): + expected_resource = Resource.objects.get(object_id=org_pk, content_type=org_ctype) + assert resource_pk == expected_resource.pk From cfdfa62515a447ce2bec308c88fc187e84be97ef Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 6 Oct 2025 13:55:45 -0400 Subject: [PATCH 16/19] Add test for hop in other direction --- .../models/test_resource_field.py | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/test_app/tests/resource_registry/models/test_resource_field.py b/test_app/tests/resource_registry/models/test_resource_field.py index fbbedd1e6..194bffbf6 100644 --- a/test_app/tests/resource_registry/models/test_resource_field.py +++ b/test_app/tests/resource_registry/models/test_resource_field.py @@ -1,7 +1,7 @@ import pytest from django.contrib.contenttypes.models import ContentType -from ansible_base.resource_registry.models import Resource +from ansible_base.resource_registry.models import Resource, ResourceType from test_app.models import Inventory, Organization @@ -145,3 +145,46 @@ def test_resource_field_prefetch_related_across_foreign_key(organization, organi for org_pk, resource_pk in resource_pks.items(): expected_resource = Resource.objects.get(object_id=org_pk, content_type=org_ctype) assert resource_pk == expected_resource.pk + + +@pytest.mark.django_db +def test_resource_field_prefetch_resource_type_from_organization(organization, organization_1, organization_2, django_assert_num_queries): + """ + Generated by Claude Code (claude-sonnet-4-5@20250929) + Test that prefetch_related works from Organization through resource to resource_type. + This tests prefetching resource__content_type__resource_type on Organization objects. + """ + org_ctype = ContentType.objects.get_for_model(Organization) + + # Prefetch resource__content_type__resource_type should result in 4 queries: + # 1. Fetch all Organization objects + # 2. Fetch all related Resource objects + # 3. Fetch all related ContentType objects + # 4. Fetch all related ResourceType objects + with django_assert_num_queries(4) as captured: + org_qs = list(Organization.objects.prefetch_related("resource__content_type__resource_type").all()) + + # Verify the queries were as expected + assert "test_app_organization" in captured[0]["sql"] + assert "dab_resource_registry_resource" in captured[1]["sql"] + assert "django_content_type" in captured[2]["sql"] + assert "dab_resource_registry_resourcetype" in captured[3]["sql"] + + assert len(org_qs) > 2 + + # Collect resource type data for later verification + resource_type_data = {} + with django_assert_num_queries(0): + for org in org_qs: + assert org.resource is not None + assert org.resource.content_type is not None + assert org.resource.content_type.resource_type is not None + # Verify the resource type is correct + resource_type = org.resource.content_type.resource_type + assert resource_type.content_type == org_ctype + resource_type_data[org.pk] = resource_type.pk + + # Verify the resource types match what's in the database + expected_resource_type = ResourceType.objects.get(content_type=org_ctype) + for org_pk, resource_type_pk in resource_type_data.items(): + assert resource_type_pk == expected_resource_type.pk From 59c8734e5cd7c9d43073d51b35e08d5a59df1b8a Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 6 Oct 2025 13:56:54 -0400 Subject: [PATCH 17/19] fix black issue --- ansible_base/resource_registry/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible_base/resource_registry/fields.py b/ansible_base/resource_registry/fields.py index 8970f1a6d..350a945df 100644 --- a/ansible_base/resource_registry/fields.py +++ b/ansible_base/resource_registry/fields.py @@ -15,7 +15,7 @@ def get_queryset(self, **hints): def get_prefetch_querysets(self, instances, querysets=None): if querysets and len(querysets) != 1: - raise ValueError("querysets argument of get_prefetch_querysets() should have a length " "of 1.") + raise ValueError("querysets argument of get_prefetch_querysets() should have a length of 1.") queryset = querysets[0] if querysets else self.get_queryset() queryset._add_hints(instance=instances[0]) From 7295064a95d6d22af66ccecdf10d3e240d5a762b Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 7 Oct 2025 08:16:41 -0400 Subject: [PATCH 18/19] Add no sonar --- ansible_base/resource_registry/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible_base/resource_registry/fields.py b/ansible_base/resource_registry/fields.py index 350a945df..113608137 100644 --- a/ansible_base/resource_registry/fields.py +++ b/ansible_base/resource_registry/fields.py @@ -48,7 +48,7 @@ def get_prefetch_querysets(self, instances, querysets=None): False, ) - def get_prefetch_queryset(self, instances, queryset=None): + def get_prefetch_queryset(self, instances, queryset=None): # NOSONAR # Django 4 compatibility: renamed to get_prefetch_querysets in Django 5 if queryset is None: return self.get_prefetch_querysets(instances) From 367c49fed224b120c25f3aa8cf61caa244bc183d Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 7 Oct 2025 08:17:34 -0400 Subject: [PATCH 19/19] Also exclude from coverage --- ansible_base/resource_registry/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible_base/resource_registry/fields.py b/ansible_base/resource_registry/fields.py index 113608137..7e5ee0c99 100644 --- a/ansible_base/resource_registry/fields.py +++ b/ansible_base/resource_registry/fields.py @@ -48,7 +48,7 @@ def get_prefetch_querysets(self, instances, querysets=None): False, ) - def get_prefetch_queryset(self, instances, queryset=None): # NOSONAR + def get_prefetch_queryset(self, instances, queryset=None): # NOSONAR # pragma: no cover # Django 4 compatibility: renamed to get_prefetch_querysets in Django 5 if queryset is None: return self.get_prefetch_querysets(instances)