From c52cbb52a24a3e9e8f39e903767f2464fa6947d9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:54:32 +0000 Subject: [PATCH 1/7] Update dependency Django to v5 [SECURITY] --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2ba9fd2dfc..5d7ecbfb27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] dependencies = [ - "Django==4.2.30", + "Django==5.2.16", "attrs>=25.0.0,<26", "base36>=0.1.1,<0.2", "beautifulsoup4>=4.8.2,<5", diff --git a/uv.lock b/uv.lock index 1acf9c2102..bac0627132 100644 --- a/uv.lock +++ b/uv.lock @@ -765,16 +765,16 @@ sdist = { url = "https://files.pythonhosted.org/packages/2b/8f/77a4b8ec50c821193 [[package]] name = "django" -version = "4.2.30" +version = "5.2.16" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, { name = "sqlparse" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/b5/f1a53dc68da6429d6e0345bb848161e2381a2e9f02700148911e8582c2b3/django-4.2.30.tar.gz", hash = "sha256:4ebc7a434e3819db6cf4b399fb5b3f536310a30e8486f08b66886840be84b37c", size = 10468707, upload-time = "2026-04-07T14:05:45.57Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/26/889449d521ae508b26de715954faecd8bcf3f740affb81b2d146a83b42a5/django-5.2.16.tar.gz", hash = "sha256:59ea02020c3136fce14bef0bbece21a10a4febef5eed1c51c22ae468efa22200", size = 10890894, upload-time = "2026-07-07T13:52:17.005Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/b7/a7c96f239cf91313a6589233fed55111c7063b26683b226802732c455dbc/django-4.2.30-py3-none-any.whl", hash = "sha256:4d07aaf1c62f9984842b67c2874ebbf7056a17be253860299b93ae1881faad65", size = 7997231, upload-time = "2026-04-07T14:05:38.241Z" }, + { url = "https://files.pythonhosted.org/packages/4e/13/1e5e3e4c15dcecb04281b3cb2a46a4670e1cef131068e202f6040df19224/django-5.2.16-py3-none-any.whl", hash = "sha256:04f354bf9d807a86ad1a8392fe3808d362358a8eafc322848e0e43e59b24371d", size = 8311943, upload-time = "2026-07-07T13:52:11.223Z" }, ] [[package]] @@ -2734,7 +2734,7 @@ requires-dist = [ { name = "deepmerge", specifier = ">=2.0,<3" }, { name = "dj-database-url", specifier = ">=3.0.0,<4" }, { name = "dj-static", specifier = ">=0.0.6,<0.0.7" }, - { name = "django", specifier = "==4.2.30" }, + { name = "django", specifier = "==5.2.16" }, { name = "django-anymail", extras = ["mailgun"], specifier = ">=13.0,<14" }, { name = "django-bitfield", specifier = ">=2.2.0,<3" }, { name = "django-cache-memoize", specifier = ">=0.2.0,<0.3" }, From ea0199e079f074f8b4641eb374fc88663e7bca89 Mon Sep 17 00:00:00 2001 From: Matt Bertrand Date: Thu, 13 Aug 2026 09:51:28 -0400 Subject: [PATCH 2/7] Fix Django 5.2 incompatibilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Django 5 gives RemoteUserMiddleware its own __call__ that discards the return value of process_request, so ApisixUserMiddleware returning self.get_response(request) made every request run its downstream chain twice — doubled writes, doubled side effects, throttle limits consumed at 2x. Returning None and letting __call__ drive the chain fixes it, and behaves identically on 4.2. Same fix as learn-ai 2fe143b. The rest: - index_together was removed in 5.1; Meta.indexes replaces it and the migration renames the existing index in place rather than rebuilding it - USE_L10N was removed in 5.0 - django-filter 2.4.0 calls ChoiceField._set_choices, which 5.0 removed. The >=2.4.0,<3 pin could never reach a fixed release because upstream switched to CalVer (21.1 ... 26.1), so the upper bound comes off - djangorestframework 3.17.2 fixes RawPostDataException when DRF reads request.data under 5.2 - django-safedelete 1.4.1 still calls the deprecated log_action(); ignore the warning until upstream moves to log_actions() - renovate was capped at Django <5, which is why 4.2 sat here through its EOL; raise it to <6 so 5.2.x patches come through Full suite passes apart from the ordering flakes tracked in mitodl/hq#12841. Co-Authored-By: Claude Opus 5 (1M context) --- .../migrations/0017_channelgrouprole_index.py | 17 +++++++++++++++++ channels/models.py | 2 +- fixtures/common.py | 7 +++++++ main/middleware/apisix_user.py | 7 +++---- main/middleware/apisix_user_test.py | 2 +- main/settings.py | 2 -- pyproject.toml | 2 +- renovate.json | 2 +- uv.lock | 14 +++++++------- 9 files changed, 38 insertions(+), 17 deletions(-) create mode 100644 channels/migrations/0017_channelgrouprole_index.py diff --git a/channels/migrations/0017_channelgrouprole_index.py b/channels/migrations/0017_channelgrouprole_index.py new file mode 100644 index 0000000000..6f0f18cb21 --- /dev/null +++ b/channels/migrations/0017_channelgrouprole_index.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.16 on 2026-08-13 12:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("channels", "0016_alter_channel_featured_list"), + ] + + operations = [ + migrations.RenameIndex( + model_name="channelgrouprole", + new_name="channels_ch_channel_ae5fc6_idx", + old_fields=("channel", "role"), + ), + ] diff --git a/channels/models.py b/channels/models.py index 7ae3121164..3a78af4953 100644 --- a/channels/models.py +++ b/channels/models.py @@ -279,7 +279,7 @@ class ChannelGroupRole(TimestampedModel): class Meta: unique_together = (("channel", "group", "role"),) - index_together = (("channel", "role"),) + indexes = [models.Index(fields=["channel", "role"])] def __str__(self): return ( diff --git a/fixtures/common.py b/fixtures/common.py index cc28765a80..e852b83d04 100644 --- a/fixtures/common.py +++ b/fixtures/common.py @@ -50,6 +50,13 @@ def warnings_as_errors(): module=".*(api_jwt|api_jws|rest_framework_jwt|astroid|bs4|celery|factory|botocore|posthog|pydantic).*", category=DeprecationWarning, ) + # django-safedelete 1.4.1 (latest) still calls LogEntry.objects.log_action(), + # deprecated in Django 5.2. Drop once upstream moves to log_actions(). + warnings.filterwarnings( + "ignore", + message=r"LogEntryManager\.log_action\(\) is deprecated.*", + category=DeprecationWarning, + ) yield finally: warnings.resetwarnings() diff --git a/main/middleware/apisix_user.py b/main/middleware/apisix_user.py index df8a42181b..9bdf2c8105 100644 --- a/main/middleware/apisix_user.py +++ b/main/middleware/apisix_user.py @@ -12,7 +12,6 @@ from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from django.http import HttpRequest -from django.http.response import HttpResponseBase from posthog import Posthog from authentication.api import user_created_actions @@ -215,7 +214,7 @@ class ApisixUserMiddleware(RemoteUserMiddleware): header = "HTTP_X_USERINFO" - def process_request(self, request: HttpRequest) -> HttpResponseBase | None: + def process_request(self, request: HttpRequest) -> None: """ Modify the header to contain username, pass off to RemoteUserMiddleware """ @@ -242,7 +241,7 @@ def process_request(self, request: HttpRequest) -> HttpResponseBase | None: # Already logged in as this user: skip login() so we don't cycle # the session and write last_login on every request. request.user = apisix_user - return self.get_response(request) + return None if request.user.is_authenticated and request.user != apisix_user: # The user is authenticated, but doesn't match the user we got @@ -266,4 +265,4 @@ def process_request(self, request: HttpRequest) -> HttpResponseBase | None: log.debug("Forcing user logout because no APISIX user was found") logout(request) - return self.get_response(request) + return None diff --git a/main/middleware/apisix_user_test.py b/main/middleware/apisix_user_test.py index ac32fda7b9..6ed27e3361 100644 --- a/main/middleware/apisix_user_test.py +++ b/main/middleware/apisix_user_test.py @@ -158,7 +158,7 @@ def test_get_request_ambiguous_identity_fails_closed(mocker, mock_login): mock_get_response = mocker.Mock(return_value="response") apisix_middleware = ApisixUserMiddleware(mock_get_response) - assert apisix_middleware.process_request(mock_request) == "response" + assert apisix_middleware(mock_request) == "response" mock_login.assert_not_called() mock_get_response.assert_called_once_with(mock_request) diff --git a/main/settings.py b/main/settings.py index df5afa6c74..49f7a85b36 100644 --- a/main/settings.py +++ b/main/settings.py @@ -305,8 +305,6 @@ USE_I18N = True -USE_L10N = True - USE_TZ = True AUTHENTICATION_BACKENDS = ( diff --git a/pyproject.toml b/pyproject.toml index 5d7ecbfb27..e1b6303191 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "django-bitfield>=2.2.0,<3", "django-cache-memoize>=0.2.0,<0.3", "django-cors-headers>=4.0.0,<5", - "django-filter>=2.4.0,<3", + "django-filter>=26.1", "django-guardian>=3.0.0,<4", "django-health-check>=3.24.0,<4", "django-imagekit>=6.0.0,<7", diff --git a/renovate.json b/renovate.json index b20aea85bc..9d25ea524d 100644 --- a/renovate.json +++ b/renovate.json @@ -3,7 +3,7 @@ "extends": ["local>mitodl/.github:renovate-config"], "packageRules": [ { - "allowedVersions": "<5", + "allowedVersions": "<6", "matchPackageNames": ["django", "Django"] } ] diff --git a/uv.lock b/uv.lock index bac0627132..9eef158c0b 100644 --- a/uv.lock +++ b/uv.lock @@ -837,14 +837,14 @@ wheels = [ [[package]] name = "django-filter" -version = "2.4.0" +version = "26.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/cf/adae3e55995ea27e1dceb493e0226557d4207d8819ddb99591df5204a471/django-filter-2.4.0.tar.gz", hash = "sha256:84e9d5bb93f237e451db814ed422a3a625751cbc9968b484ecc74964a8696b06", size = 146904, upload-time = "2020-09-27T09:08:58.079Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/3e/563965173d4cbb5fc308087e7b3d11a115b7b67273d093622480b1e31f78/django_filter-26.1.tar.gz", hash = "sha256:66ea04031b068c77c86e1ac26ced7a3f8f13ce797f5795751707e3deefc58054", size = 144299, upload-time = "2026-07-11T09:27:02.767Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/2b/b2fe483c3095b6222725dd05f9ad9e6ed6cb7347c154fdbd80238d36f1a8/django_filter-2.4.0-py3-none-any.whl", hash = "sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1", size = 73156, upload-time = "2020-09-27T09:08:52.69Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/afffed1e3c4540fb75bf550a18b6176a9f6371b5f3e52b69a28995b6480c/django_filter-26.1-py3-none-any.whl", hash = "sha256:7d98ef2899218e6242619b532cb1b95af14e09dfcf74844aecb550ad27b59ff2", size = 94069, upload-time = "2026-07-11T09:27:01.012Z" }, ] [[package]] @@ -1033,14 +1033,14 @@ wheels = [ [[package]] name = "djangorestframework" -version = "3.17.0" +version = "3.17.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/46/615ad5425c7b941e6fd3f382d25b1aca680022200d8e5b87bfc665c4425d/djangorestframework-3.17.0.tar.gz", hash = "sha256:456fd992a33f9e64c9d0f47e85d9787db0efb44f894c1e513315b5e74765bd4c", size = 905671, upload-time = "2026-03-18T20:33:42.861Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/35/c96055e700fdff25da3a7b7756cfd1d4dc54f38b9bc6d6c5e19e3a0fdc20/djangorestframework-3.17.2.tar.gz", hash = "sha256:89ed713b6dc83e1539f214b7d10808ae19bb8511004beba886225da6d5c9dafa", size = 906683, upload-time = "2026-08-05T07:47:22.5Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/ee/01dfe0e712d37007a4417bf7ba518a9e6830d4de009ed4bfdfebd45b99cf/djangorestframework-3.17.0-py3-none-any.whl", hash = "sha256:d84fe85f30b7ac6e8c0076ce9ff635e4eaedca5912f8d7d2926ce448c08533ba", size = 898818, upload-time = "2026-03-18T20:33:41.335Z" }, + { url = "https://files.pythonhosted.org/packages/a2/46/c14108e400b208c394325eb63fbae06c81341b6447fa1a6f9da718b17fe7/djangorestframework-3.17.2-py3-none-any.whl", hash = "sha256:cb0546a7415d5b46c04e0f4fe0a54b2109f4fdd5e83ca773c8c6183a6493d042", size = 899109, upload-time = "2026-08-05T07:47:20.853Z" }, ] [[package]] @@ -2739,7 +2739,7 @@ requires-dist = [ { name = "django-bitfield", specifier = ">=2.2.0,<3" }, { name = "django-cache-memoize", specifier = ">=0.2.0,<0.3" }, { name = "django-cors-headers", specifier = ">=4.0.0,<5" }, - { name = "django-filter", specifier = ">=2.4.0,<3" }, + { name = "django-filter", specifier = ">=26.1" }, { name = "django-guardian", specifier = ">=3.0.0,<4" }, { name = "django-health-check", specifier = ">=3.24.0,<4" }, { name = "django-imagekit", specifier = ">=6.0.0,<7" }, From 032b999c12f03e57675b306edfc0816920e9504d Mon Sep 17 00:00:00 2001 From: Matt Bertrand Date: Thu, 13 Aug 2026 10:03:16 -0400 Subject: [PATCH 3/7] Bump CI postgres to 16 Django 5.2 requires PostgreSQL 14+. CI was still on 12.22; local dev already runs 16 via docker-compose.services.yml. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ae468c785..0b253824d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: # Label used to access the service container db: # Docker Hub image - image: postgres:12.22 + image: postgres:16 # Set health checks to wait until postgres has started options: >- --health-cmd pg_isready From b484b75f559c638cbd42423e273f0c257af87f0c Mon Sep 17 00:00:00 2001 From: Matt Bertrand Date: Thu, 13 Aug 2026 10:11:13 -0400 Subject: [PATCH 4/7] Give the ChannelGroupRole index an explicit name Matches taskbatch_job_status_idx and content_fb_course_time_idx rather than Django's auto-generated hash name. Co-Authored-By: Claude Opus 5 (1M context) --- channels/migrations/0017_channelgrouprole_index.py | 4 ++-- channels/models.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/channels/migrations/0017_channelgrouprole_index.py b/channels/migrations/0017_channelgrouprole_index.py index 6f0f18cb21..a5216f9693 100644 --- a/channels/migrations/0017_channelgrouprole_index.py +++ b/channels/migrations/0017_channelgrouprole_index.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.16 on 2026-08-13 12:41 +# Generated by Django 5.2.16 on 2026-08-13 14:09 from django.db import migrations @@ -11,7 +11,7 @@ class Migration(migrations.Migration): operations = [ migrations.RenameIndex( model_name="channelgrouprole", - new_name="channels_ch_channel_ae5fc6_idx", + new_name="channelgrouprole_ch_role_idx", old_fields=("channel", "role"), ), ] diff --git a/channels/models.py b/channels/models.py index 3a78af4953..d44c159f9b 100644 --- a/channels/models.py +++ b/channels/models.py @@ -279,7 +279,11 @@ class ChannelGroupRole(TimestampedModel): class Meta: unique_together = (("channel", "group", "role"),) - indexes = [models.Index(fields=["channel", "role"])] + indexes = [ + models.Index( + fields=["channel", "role"], name="channelgrouprole_ch_role_idx" + ) + ] def __str__(self): return ( From c2967d7e12e569fabb71e6ff00330bcf6bf40816 Mon Sep 17 00:00:00 2001 From: Matt Bertrand Date: Thu, 13 Aug 2026 12:41:23 -0400 Subject: [PATCH 5/7] Exercise the same-user branch through the middleware call The same_user case called process_request() directly, so it couldn't catch the duplicate get_response() run this branch fixes. Co-Authored-By: Claude Opus 5 (1M context) --- main/middleware/apisix_user_test.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/main/middleware/apisix_user_test.py b/main/middleware/apisix_user_test.py index 6ed27e3361..040b089cc0 100644 --- a/main/middleware/apisix_user_test.py +++ b/main/middleware/apisix_user_test.py @@ -187,9 +187,11 @@ def test_get_request_different_user_logout(mocker, client, same_user): ) mocker.patch("main.middleware.apisix_user.login") mock_logout = mocker.patch("main.middleware.apisix_user.logout") - apisix_middleware = ApisixUserMiddleware(mocker.Mock()) - apisix_middleware.process_request(mock_request) + mock_get_response = mocker.Mock(return_value="response") + apisix_middleware = ApisixUserMiddleware(mock_get_response) + assert apisix_middleware(mock_request) == "response" assert mock_logout.call_count == (0 if same_user else 1) + mock_get_response.assert_called_once_with(mock_request) @pytest.mark.django_db(transaction=True) From 0d91b410bd8e63abc3d043f327b5b16fa44da965 Mon Sep 17 00:00:00 2001 From: Matt Bertrand Date: Thu, 13 Aug 2026 12:54:34 -0400 Subject: [PATCH 6/7] Scope the log_action() deprecation filter to safedelete.admin Matching on message alone would also swallow the warning for any first-party log_action() call. Co-Authored-By: Claude Opus 5 (1M context) --- fixtures/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fixtures/common.py b/fixtures/common.py index e852b83d04..9e1b1e0843 100644 --- a/fixtures/common.py +++ b/fixtures/common.py @@ -55,6 +55,7 @@ def warnings_as_errors(): warnings.filterwarnings( "ignore", message=r"LogEntryManager\.log_action\(\) is deprecated.*", + module=r"safedelete\.admin", category=DeprecationWarning, ) yield From 1581b438004e4910f7b7a8f7f6814ccc0b77b368 Mon Sep 17 00:00:00 2001 From: Matt Bertrand Date: Thu, 13 Aug 2026 14:18:14 -0400 Subject: [PATCH 7/7] Use postgres 15 in CI to match deployed RDS (15.17 on CI/RC/production) Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b253824d1..8fc5ee7734 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: # Label used to access the service container db: # Docker Hub image - image: postgres:16 + image: postgres:15 # Set health checks to wait until postgres has started options: >- --health-cmd pg_isready