Skip to content

Upgrade to django 5.2 - #5497

Draft
yakky wants to merge 28 commits into
openlibhums:r-v1.9.xfrom
sissamedialab:feature/janeway-1.9
Draft

Upgrade to django 5.2#5497
yakky wants to merge 28 commits into
openlibhums:r-v1.9.xfrom
sissamedialab:feature/janeway-1.9

Conversation

@yakky

@yakky yakky commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Add support for Django 5.2

  • rewrite deprecated imports
  • rewrite obsolete code patterns
  • rewrite header access
  • rewrite boto code for boto3

yakky and others added 28 commits August 20, 2026 20:11
utils/management/commands/backup.py had zero dedicated test coverage
(its only prior 'backup' hit in the test suite was an unrelated code
comment). This is Phase 1 item openlibhums#1 of the Django 5.2 migration plan: a
regression oracle for the boto (v2) -> boto3 migration, run and confirmed
green against the current boto 2.46.1 dependency.

Adds BackupCommandS3Test, covering:
- the S3 connection is opened with the configured region/host/credentials
  and bucket, without making a real network call (boto.s3.connect_to_region
  and the boto.s3.key.Key class are mocked)
- a completion email is sent to superuser accounts when BACKUP_TYPE=s3 and
  BACKUP_EMAIL=True
- an error email is sent (and the command does not raise) when the S3
  connection fails

The command's internal call_command('dumpdata', ...) step is faked out:
under this repo's sqlite test settings, a real dumpdata run fails with
'no such table: core_pgfiletext' because core.models.PGFileText declares
required_db_vendor='postgresql' and has no table on sqlite, yet dumpdata
still tries to serialize it. That is a pre-existing sqlite/postgres model
incompatibility unrelated to the S3 path under test here, so it is faked
out rather than worked around.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
review/logic.py:serve_review_file() builds a .docx review form with
python-docx and had zero test coverage. This is Phase 1 item openlibhums#2 of the
Django 5.2 migration plan: a regression oracle for the python-docx
0.8.11 -> 1.2.0 upgrade, run and confirmed green against the current
python-docx 0.8.11 dependency.

Adds ServeReviewFileTests.test_serve_review_file_produces_valid_docx,
which builds a real ReviewAssignment with a ReviewForm carrying two
ReviewFormElements, calls serve_review_file(), reads the generated docx
bytes back out of the StreamingHttpResponse, and asserts:
- the document's first heading is 'Review #<pk>'
- a heading contains the article title
- each ReviewFormElement's name appears as a level-2 heading

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Re-applied by hand after rebasing this branch onto the correct
r-v1.9.x base: the original commit targeted utils/tests/__init__.py
because the branch had been accidentally built on top of origin/master,
where utils/tests.py had not yet been split into a package the same
way r-v1.9.x already has (r-v1.9.x keeps the real content in
test_utils.py, with an empty __init__.py). Ported the two new test
methods and their imports (retrieve_tokens, HTTPError) into the
existing TestORCiDRecord class in test_utils.py instead.
Removes packages verified via git grep to have zero real usage in
src/, per the Phase 2 migration plan's Verified unused packages page:

- ipaddress: Python 2 stdlib backport, zero imports
- kitchen: zero imports
- python-wordpress-xmlrpc (dev-requirements.txt): zero imports repo-wide
- bs4: redundant meta-package; beautifulsoup4 already provides the
  same 'bs4' importable namespace used across 27 files
- six: only needed transitively via ebooklib's own install_requires,
  zero direct imports in src/
- pycparser: only needed transitively via cffi, zero direct imports
- ua-parser: only needed transitively via user-agents, zero direct
  imports (user-agents itself is still a direct dependency, kept)
- django-dynamicsites (openlibhums git-installed fork): zero imports,
  no INSTALLED_APPS/MIDDLEWARE entry; also already broken under
  today's Django 4.2 (render_to_response(context_instance=...) and
  django.utils.http.urlquote were both removed years ago). Also
  removes its now-stale entry from pyproject.toml's ruff exclude list.

Full test suite re-run after removal: 1295 tests, all pass, no change
from baseline.

Part of Phase 2 of the Django 5.2 migration plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mechanical import-source swap only, no behavior change:
- 'import mock' -> 'from unittest import mock'
- 'from mock import X' -> 'from unittest.mock import X'

Touches the 13 files found via git grep for '^import mock|from mock
import' (src/api/tests/test_preprints.py, src/core/tests/test_app.py,
src/core/tests/test_logic.py, src/core/tests/test_views.py,
src/cron/management/commands/send_publication_notifications.py,
src/identifiers/tests/test_logic.py, src/repository/tests/test_models.py,
src/security/test_security.py, src/submission/tests/test_logic.py,
src/submission/tests/test_workflow.py, src/typesetting/tests.py,
src/utils/management/commands/test_fire_event.py,
src/utils/tests/test_utils.py). test_app.py had both 'from mock import
patch' and a separate 'import mock' for mock.patch(...) usage elsewhere
in the file; both were swapped to their unittest.mock equivalents.

Removes the now-unused 'mock' pin from requirements.txt.

Full test suite re-run: 1295 tests, all pass, no change from baseline.

Part of Phase 2 of the Django 5.2 migration plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the abandoned boto v2 SDK with boto3 in
utils/management/commands/backup.py's S3 upload path, guarded by
Phase 1's utils/tests/test_backup.py.

Implementation decisions:

- boto.s3.connect_to_region(END_POINT, ..., host=S3_HOST) + Key +
  set_contents_from_file(cb=..., num_cb=200) becomes
  boto3.client("s3", region_name=..., endpoint_url=..., ...) +
  upload_fileobj(..., Callback=...).

- END_POINT vs S3_HOST: per janeway_global_settings.py's documented
  defaults (END_POINT = "eu-west-2" # eg. eu-west-1, S3_HOST =
  "s3.eu-west-2.amazonaws.com" # eg. s3.eu-west-1.amazonaws.com),
  END_POINT is a plain AWS region name and S3_HOST is that region's
  standard S3 endpoint hostname - not evidence of a non-AWS/custom
  S3-compatible endpoint. boto3.client() is therefore called with
  region_name=END_POINT (which boto3 needs to resolve/sign requests)
  *and* endpoint_url=f"https://{S3_HOST}" so a deployment that has
  customised S3_HOST to point somewhere else keeps behaving exactly as
  it did under boto2's host= override, rather than silently starting
  to ignore that setting.

- mycb()'s signature changes from cb(so_far, total) to
  Callback(bytes_transferred), since boto3's upload_fileobj only ever
  passes the callback the number of bytes transferred in that
  invocation (no running total or file size, unlike boto2). Rather
  than reconstructing a running total via a closure, the callback just
  reports the chunk size transferred and drops the "out of {total}"
  part of the printed message - simplest option that preserves visible
  upload progress without adding statefulness only used for a log
  line.

- requirements.txt: boto==2.46.1 -> boto3==1.43.75 (latest at time of
  writing).

test_backup.py's mocks were updated to target the new call sites
(boto3.client instead of boto.s3.connect_to_region/Key) while keeping
its behavioural assertions unchanged: bucket name, key naming under
'backups/...', and email-on-success/failure. The test's existing
_fake_dumpdata() workaround for the pre-existing sqlite/PGFileText
dumpdata incompatibility (flagged in the Phase 1 report as a decision
for whoever does this migration) is left as-is and not addressed here
- it is a separate, pre-existing production bug unrelated to the
boto3 swap, out of scope for this task.

utils.tests.test_backup run in isolation: 2/2 pass. Full suite:
1295 tests, all pass, no change from baseline.

Part of Phase 2 of the Django 5.2 migration plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…atch 1)

- django-hijack 3.2.1 -> 3.7.8
- django-recaptcha 3.0.0 -> 4.1.0
- django-simple-history 3.10.1 -> 3.12.0 (not 3.13.0: that release
  requires django>=5.2, dropping 4.2 support; 3.12.0 is the latest
  release still compatible with the Django 4.2.29 pin this phase
  keeps)
- django-tinymce 3.7.1 -> 5.0.0
- mozilla-django-oidc 4.0.1 -> 5.0.2
- django-bootstrap4 23.2 -> 26.1

django-recaptcha 4.x renamed its top-level package/app from `captcha`
to `django_recaptcha` (documented in the project's own README/install
instructions as of the 4.0.0 release). Updated the one INSTALLED_APPS
entry and the two field/widget imports in utils/forms.py accordingly.

beautifulsoup4 was transitively bumped 4.9.3 -> 4.15.0 by
django-bootstrap4's `beautifulsoup4>=4.10.0` floor; this is also the
target version for the Batch 2 utility-package bump, so left as-is.

Full suite: 1295 tests, all pass.
…ing (batch 2)

- beautifulsoup4 4.9.3 -> 4.15.0 (already transitively pulled in by
  batch 1's django-bootstrap4 bump; pin updated to match)
- colorlog 3.1.4 -> 6.12.0
- chardet 3.0.4 -> 7.6.0
- crossrefapi 1.5.0 -> 1.7.0
- freezegun 1.2.0 -> 1.5.5
- html2text 2017.10.4 -> 2025.4.15
- iso639-lang 2.6 -> 2.6.3
- more-itertools 10.1.0 -> 11.1.0

Batch 6 (urllib3<2 pin removal) folded in here rather than done as its
own later step: crossrefapi 1.7.0 hard-requires urllib3>=2.2.3, so
resolving this batch at all forces the ceiling's removal - it's no
longer a "try and see" experiment, it's a direct conflict. This is
also the strongest possible confirmation of the dependency analysis's
suspicion that the pin (originally added for openlibhums#3736, a
requests/boto2-era urllib3 2.0 incompatibility) is obsolete now that
Phase 2 moved us to boto3 and requests is pinned to 2.32.4, which
fully supports urllib3 2.x.

pip resolved urllib3 to 2.7.0 with the ceiling removed.

Full suite: 1295 tests, all pass.
…x (batch 3)

- lxml 6.0.0 -> 6.1.2
- markdown: was unpinned -> pinned ==3.10.3
- packaging 23.2 -> 26.3
- pdfminer.six: was unpinned -> pinned ==20260107
- psycopg2-binary 2.9.10 -> 2.9.12
- pyasn1 0.1.9 -> 0.6.4 (no direct call site in Janeway code; pure
  transitive dependency of the crypto/OIDC stack)
- PyJWT 2.4.0 -> 2.13.0
- Pillow 10.2.0 -> 12.3.0

pdfkit left untouched (permanently out of scope this migration).

Full suite: 1295 tests, all pass.
- python-dateutil 2.8.1 -> 2.9.0.post0
- python-crontab 2.2.3 -> 3.3.0 (major bump; confirmed real call site
  in cron/management/commands/install_cron.py - manually exercised
  with `manage.py install_cron --action=test` post-bump, which
  constructs CronTab(user=True), iterates jobs, calls tab.new(),
  cron_job.setall(), minute.every(), and tab.render() without writing
  to the real crontab; output was correct and unchanged in shape)
- requests 2.32.4 -> 2.34.2
- sqlparse 0.4.4 -> 0.6.0
- tqdm 4.66.3 -> 4.70.0
- docutils 0.21.2 -> 0.23

Full suite: 1295 tests, all pass.
- coverage: unpinned -> ==7.15.4
- ipdb: unpinned -> ==0.13.13
- marshmallow: unpinned -> ==4.3.1 (no direct call site in Janeway
  code; dev/tooling-only transitive dependency)
- freezegun: unpinned -> ==1.5.5 (matches the requirements.txt pin
  from batch 2, so both files now agree)
- snakeviz: unpinned -> ==2.2.2 (referenced only in a docstring in
  utils/management/base.py as an external profiling tool a developer
  runs manually; no import in Janeway code)
- django-browser-reload: was unpinned -> ==1.21.0
- tox: unpinned -> ==4.60.0
- ruff: was `>=0.15` -> ==0.16.4

Hygiene fix: pyproject.toml's `dev` extra pinned bare `ruff` while
dev-requirements.txt pinned `ruff>=0.15` - the two could silently
diverge. Made dev-requirements.txt the source of truth (it's what CI
and `make` targets actually install from) and pinned pyproject.toml's
`dev` extra to the exact same `ruff==0.16.4`.

Verified `ruff format --check .` against the new ruff version: 1252
files already formatted, no drift.

Full suite: 1295 tests, all pass.
Confirmed real call site: review/logic.py:serve_review_file() builds a
.docx review form via Document(), add_heading(text, 0/1/2),
add_paragraph(), add_table()/add_row()/.cells. Gated by the Phase 1
regression test review/tests/test_logic.py
(ServeReviewFileTests.test_serve_review_file_produces_valid_docx),
added specifically ahead of this upgrade.

Checked python-docx 1.2.0's add_heading() source directly: the
level-0-heading style is still "Title" in both 0.8.11 and 1.2.0 (not
"Heading 0" as flagged as a possible risk) - add_heading, add_table,
add_paragraph and save() all have unchanged signatures across the
1.0.0 boundary (type hints added only). No code change needed in
serve_review_file().

Verification:
- review.tests.test_logic passed against python-docx 0.8.11 (baseline)
- bumped to 1.2.0
- review.tests.test_logic passed again unchanged
- full suite: 1295 tests, all pass
Searched Janeway's own code thoroughly for django_countries usage
(CountryField, `import django_countries`, INSTALLED_APPS entry,
templates, plugins) and found none - it is not even in
INSTALLED_APPS. The `Country` model referenced in core/models.py and
core/logic.py is Janeway's own local model, unrelated to this
package. This is a genuinely zero-call-site dependency in the current
codebase (per the governing rule, full-suite-green is sufficient
evidence for such packages).

Confirmed the `[pyuca]` extra still resolves in 9.0.0
(provides_extra: ['pyuca'], requires_dist includes
'pyuca; extra == "pyuca"'). requires_python is now >=3.10, compatible
with this phase's Python 3.11 venv.

Full suite: 1295 tests, all pass.
- maxminddb 2.5.1 -> 3.1.1
- geoip2 4.8.0 -> 5.3.0
- Faker 17.6.0 -> 40.36.0

Confirmed real call sites:
- metrics/logic.py:get_iso_country_code() uses geoip2.database.Reader
  against the bundled GeoLite2-Country.mmdb, called from
  store_article_access() on every article view. This is already
  exercised end-to-end (not just imported) by the existing
  metrics/tests.py::ArticleAccessTests, which hit real article URLs
  and assert on the resulting ArticleAccess rows. Ran this test module
  before (geoip2 4.8.0/maxminddb 2.5.1: 4 passed) and after (geoip2
  5.3.0/maxminddb 3.1.1: 4 passed, unchanged) the bump.
- repository/management/commands/generate_preprints.py and
  utils/management/commands/create_fake_news_items.py both use
  `Faker()` with fake.sentence(), fake.text(), fake.first_name(),
  fake.last_name(), fake.word(), fake.date() - all long-stable core
  providers, no Faker.seed() call anywhere in Janeway's code.

Coverage gap (flagged, not fixed here): neither generate_preprints nor
create_fake_news_items has any existing automated test - grepped the
whole tree, found none. Since the Faker jump is 17.6.0 -> 40.36.0 (23
majors), verified by direct interpreter smoke-test that
fake.sentence()/.text()/.first_name()/.last_name()/.word()/.date()
all still work correctly under 40.36.0, but this is not a substitute
for real command-level test coverage. Recommend the orchestrating
session consider a dedicated test for at least one of these two
commands as a follow-up.

geoip2 5.3.0 pulled in aiohttp (already present via another
dependency, bumped 3.13.5 -> 3.14.3) for its new async client support;
not used by Janeway's synchronous call site.

Full suite: 1295 tests, all pass.
Installed django-upgrade==1.32.0 (current latest stable on PyPI) as a
dev-only dependency and ran it once against all of Janeway's own source
(git ls-files 'src/**/*.py', excluding plugins/third-party code) with
--target-version 5.2. Django itself stays pinned at 4.2.29; this is a
purely mechanical, backward-compatible syntax rewrite (Phase 4 of the
Django 5.2 migration plan). Followed by `ruff format .`.

Fixers that actually fired (45 source files touched, 0 migrations in
the final diff):

- django_urls (min_version 2.0): rewrote `re_path()` calls using simple,
  unambiguous regex groups (\d+ -> <int:...>, etc.) to `path()` with
  converters, across ~30 urls.py files (core/include_urls.py,
  journal/urls.py, repository/urls.py, review/urls.py, typesetting/urls.py,
  submission/urls.py, production/urls.py, proofing/urls.py, cms/urls.py,
  copyediting/urls.py, and many smaller ones). Calls with complex regex
  (alternation, char classes, `.*`, etc.) were correctly left as re_path().
- test_http_headers: rewrote HTTP_USER_AGENT=/HTTP_REFERER= kwargs on
  django.test.Client calls to headers={...} dicts, in
  core/tests/test_app.py (x2), core/tests/test_views.py (x1),
  metrics/tests.py (x4). journal/tests/test_middleware.py was NOT
  touched even though it has HTTP_ACCEPT_LANGUAGE kwargs, because those
  calls go through RequestFactory rather than the test Client, which
  this fixer does not target.
- request_headers (META["HTTP_*"] -> headers[...]): core/files.py (x2),
  core/logic.py, core/views.py, metrics/logic.py, proofing/views.py,
  repository/logic.py, submission/models.py, typesetting/views.py (x3).
- admin_action (short_description attr -> @admin.action(description=)):
  metrics/admin.py (x2).
- timezone_utc (django.utils.timezone.utc -> datetime.timezone.utc):
  api/oai/base.py.
- default_app_config removal: core/__init__.py.

Fixers that did NOT fire despite being expected from the prior phases'
dependency analysis:

- model_field_choices (choices=Enum.choices -> choices=Enum): 0 hits.
  core/models.py (RORStatus), journal/models.py (PublishingStatus), and
  utils/models.py (RORImportStatus) all define their TextChoices enum as
  a nested class inside the model class itself. django-upgrade's fixer
  only scans top-level ast.ClassDef nodes in the module body for
  Django-choices-type base classes, so it never sees these nested
  definitions. This is a real detection-limitation of the tool, not a
  bug we should hand-patch here.
- format_html: 0 hits. All existing format_html() calls in
  repository/models.py and core/models.py already use the correct
  argument form (format_html("...{}", value)); none used the
  .format()-then-pass-string anti-pattern this fixer targets.
- index_together, settings_storages, assert_set_methods,
  postgres_aggregate_order_by, staticfiles_find_all: confirmed still
  zero usage.

Escalation per Phase 3's rule - reverted, not applied:

- check_constraint_condition (CheckConstraint(check=...) ->
  CheckConstraint(condition=...)) fired in cms/models.py,
  core/model_utils.py (the check_exclusive_fields_constraint() helper),
  and 3 migration files (cms/migrations/0022, core/migrations/0104,
  submission/migrations/0085). Applying it broke `manage.py check` and
  the test suite outright: Django 4.2.29's CheckConstraint.__init__
  only accepts `check=`, not `condition=` - `condition` was only added
  in Django 5.1. The plan's assumption that all target-5.2 rewrites are
  backward-compatible with 4.2 does not hold for this one fixer. All 5
  files were reverted to `check=` for this phase; the rewrite to
  `condition=` is correct and should be reapplied once Django is
  actually bumped to >=5.1 (Phase 5+). No other changes in those 5
  files were reverted - only the check=/condition= keyword.

Verification:
- Baseline before rewrite: 1295 tests, all pass, 123.1s (matches
  Phase 3's final state).
- `manage.py check`: clean after the constraint revert (it errored with
  "unexpected keyword argument 'condition'" before the revert).
- `ruff format --check .`: clean.
- Full suite after rewrite + revert + reformat: 1295 tests, all pass,
  120.9s - identical pass count to baseline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Surfaced via python -W error on Django's RemovedInDjango50/51Warning
classes (applied programmatically, since -W/PYTHONWARNINGS command-line
parsing of dotted category paths fails before site-packages are on the
import path):

- utils/testing/helpers.py: create_journal_with_test_status() set
  journal.name (a SettingValue-backed property) before journal.save(),
  passing an unsaved Journal instance into a related filter
  (SettingValue.objects.get_or_create(journal=journal)). Under Django
  4.2 this silently matched journal_id IS NULL rows instead of the
  intended journal; Django 5.0+ raises ValueError. Reordered to save()
  before setting .name.
- core/tests/test_views.py: replaced the deprecated
  django.utils.timezone.utc alias with datetime.timezone.utc.

Not touched (not deprecated-API usage in Janeway's own code, both
confirmed as expected/non-actionable):
- test_affiliation_get_or_create_without_ror_value_error deliberately
  exercises the future ValueError path; it only appears as a failure
  here because forcing the warning to error mode pre-empts the
  ValueError it's designed to catch.
- The forms "default.html" template RemovedInDjango50Warning (4
  tests) reflects a Django 5.0 default-behavior change, not a
  deprecated call site in Janeway's code; resolves itself once Django
  is bumped.
Empirically staged per the migration plan: bumped only Django first,
keeping djangorestframework==3.15.2, django-debug-toolbar==5.1.0 and
django-modeltranslation==0.18.11 pinned at their old versions, to
isolate what actually breaks from Django itself vs. the three
version-gated packages.

Result: none of the three gated packages broke at their old pin
against Django 5.2.17 for any code path the test suite exercises.
Every failure below traces to Janeway's own code:

- utils/function_cache.py: mutable_cached_property.setter() called
  cached_property.__init__(self.func, self.name), but Django 5.0
  removed the deprecated 'name' argument (RemovedInDjango50Warning in
  4.2, hard TypeError in 5.x) since __set_name__ makes it redundant.
  Dropped the argument.
- core/model_utils.py: DynamicChoiceField.formfield() called
  form_element.choices.append(choice); Django 5.x's ChoiceField.choices
  is normalized through a getter/setter pair (BlankChoiceIterator on
  read) and is no longer a plain mutable list. Reassign via the
  choices setter instead.
- submission/admin.py, repository/admin.py: Django 5.x's admin system
  checks now enforce admin.E013 for filter_horizontal/filter_vertical
  entries naming a ManyToManyField with a custom 'through' model.
  ArticleAdmin and PreprintAdmin both listed 'keywords' (through
  KeywordArticle/KeywordPreprint) in filter_horizontal, redundantly
  duplicating the dedicated KeywordArticleInline/KeywordPreprintInline
  already present in the same admin classes. Removed the redundant,
  now-invalid filter_horizontal entries.
- core/models.py: File.index_full_text() called
  FileTextModel.objects.create(contents=..., file=self). 'file' is the
  reverse accessor for File.text (a OneToOneField), not a real field on
  FileText; Django 5.x's QuerySet.create() now explicitly rejects
  reverse-one-to-one kwargs (they were always a silent no-op in 4.2 -
  the relation is actually established by the very next line,
  self.text = file_text_obj). Dropped the ineffective kwarg.

Full suite: 1295/1295 tests considered, only 3 pre-existing failures
in comms.tests.NewsItemOrderingTest, all traced to an environment
clock artifact unrelated to Django (the sandbox's local wall clock
was CEST while Django's TIME_ZONE is UTC, and the run happened to
fall in the ~2h window after local midnight but before UTC midnight,
so datetime.date.today() in the test setup disagreed with
timezone.now() by a day). Not a regression; reproducible on 4.2 in
the same window.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… releases

- djangorestframework 3.15.2 -> 3.18.0 (3.18.0's own metadata now
  requires django>=5.2)
- django-debug-toolbar 5.1.0 -> 7.1.1
- django-modeltranslation 0.18.11 -> 0.20.3

Only django-debug-toolbar needed a Janeway-side fix, and it wasn't a
simple version incompatibility so much as a new requirement the
package introduced: 7.x added a persistent SQL history store backed
by a real Django model (debug_toolbar.models.HistoryEntry), which
requires the app to be registered in INSTALLED_APPS. Janeway's test
settings (core/janeway_global_settings.py) never listed
'debug_toolbar' in INSTALLED_APPS -- only core/dev_settings.py did --
even though core/urls.py has always conditionally included
debug_toolbar.urls whenever settings.IN_TEST_RUNNER is true. This was
a latent gap that 5.1.0 tolerated silently and 7.1.1 does not.

Fixes in core/janeway_global_settings.py:
- Add 'debug_toolbar' to INSTALLED_APPS when IN_TEST_RUNNER, mirroring
  the existing dev_settings.py behaviour for DEBUG mode.
- debug_toolbar.apps.DebugToolbarConfig.ready() now calls
  settings.MIGRATION_MODULES.setdefault(app_name, None). Janeway's
  SkipMigrations helper (used to skip migrations under the test
  runner) subclasses collections.abc.Mapping, which has no
  setdefault(). Added a setdefault() that mirrors the class's existing
  'every key is present and reads as None' semantics.
- Silence the resulting debug_toolbar.W001 system check
  (DebugToolbarMiddleware missing from MIDDLEWARE) -- intentional,
  since the middleware itself is still only enabled via
  dev_settings.py; the app is only registered here for its model.

Full suite: 1295/1295 tests considered, same 3 pre-existing
NewsItemOrderingTest failures as the prior commit (clock-artifact,
unrelated to these packages).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…o >= 5.1

Phase 4 intentionally left src/cms/models.py and src/core/model_utils.py
on CheckConstraint(check=...) since django-upgrade's
check_constraint_condition fixer targets Django's CheckConstraint(condition=)
replacement, only valid from Django 5.1. Now that Django is 5.2.17,
re-ran the fixer scoped to exactly those two files:

  django-upgrade --target-version 5.2 src/cms/models.py src/core/model_utils.py

The three migration files that also use CheckConstraint(check=...) were
deliberately excluded and remain untouched, since migrations freeze
historical state and shouldn't be rewritten:
- cms/migrations/0022_navigationitem_nav_item_has_either_link_or_sub_nav.py
- core/migrations/0104_location_organization_affiliation.py
- submission/migrations/0085_creditrecord_and_more.py

Full suite: 1295/1295 tests considered, same 3 pre-existing
clock-artifact NewsItemOrderingTest failures, unaffected by this change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
USE_L10N has been a no-op since Django 4.0 (localized formatting is
always enabled) and Django 5.0 removed it from
django.conf.global_settings entirely -- Janeway's USE_L10N = False no
longer has any effect. Removed per the migration plan's explicit
cleanup note rather than leaving inert configuration behind.

Full suite: 1295/1295 tests considered, same 3 pre-existing
clock-artifact NewsItemOrderingTest failures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Formatting fixup for the DynamicChoiceField.formfield() line changed
in the Django 5.2 bump commit (ffc4c31).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- dependencies: Django>=4.2 -> Django>=5.2. The codebase now relies on
  Django 5.1+ APIs (e.g. CheckConstraint(condition=...) in cms/models.py
  and core/model_utils.py), so declaring a >=4.2 floor was already
  factually wrong.
- tool.tox.testenv.deps: Django==4.2 -> Django==5.2.17. This tox env
  installs this pin *and* file:requirements.txt (which pins
  Django==5.2.17) together; left at 4.2 it was a straight conflicting
  duplicate pin that would make 'make tox' fail dependency resolution
  outright, not just run against a stale Django.

Not touched, flagged for separate follow-up: this file's
requires-python = ">=3.9" and the tox env_list's "py39" entry are now
inconsistent with Django 5.2's own Requires-Python: >=3.10 -- tox's
py39 env will fail to install Django 5.2 at all. Left alone since
Python-floor changes are Phase 6's remit per the migration plan, not
this phase's.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Django 5.2 itself requires Python >=3.10, and this migration's own
target range is Python 3.10-3.13. pyproject.toml still claimed >=3.9
and tox's env_list still tested py39, which is no longer valid.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
[tool.ruff]'s target-version was still py39, missed by the prior commit
that fixed requires-python/tox to >=3.10. Bumped to py310 for
consistency - ruff's target-version only affects which syntax
modernizations it's allowed to suggest, so this is a config-only change.
ruff format --check . remains clean (1252 files) - the only ruff check
actually part of this project's CI contract per CLAUDE.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…pins

Switched requirements.txt/dev-requirements.txt from exact '==' pins to
'~=' (compatible-release) specifiers, at whatever precision each
package's version already had (~=X.Y.Z where a patch segment exists,
~=X.Y for 2-segment versions like idna/pytz/packaging/docutils/
django-bootstrap4). This lets future 'pip install'/'pip install -U'
runs pick up patch/compatible releases automatically without a manual
pin bump, while still refusing anything that isn't declared compatible
(a minor/major bump still requires an explicit version-string edit).
pyproject.toml's own duplicate Django tox pin was updated to match,
for the same reason it was kept in sync with requirements.txt during
the Phase 5 Django bump.

Three deliberate exceptions, kept as exact pins:
- django-hCaptcha and pdfkit: both have standing leave-completely-
  untouched decisions from this migration (Phase 2 and the migration
  plan's scope notes, respectively) - kept exact rather than have their
  notation drift from what those decisions describe, even though
  neither has a newer release to float to today regardless.
- pdfminer.six: version is a single calendar-date integer (20260107,
  no dotted release segments), which PEP 440's ~= operator can't
  syntactically apply to (requires at least two release segments) -
  kept exact.

Verified this is behavior-neutral right now, not a hidden version
bump: reinstalled from the updated files without --upgrade and
confirmed no PyPI package resolved to a different version (only the
4 editable git installs rebuilt in place, which pip always does
regardless of pinning style). Full suite re-run after: 1295 tests, OK.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- CI matrix now tests 3.11-3.14 (was 3.9-3.12)
- pyproject.toml requires-python and ruff target-version bumped to 3.11
- tox env_list updated to py311-py314
- Dockerfile.base, .readthedocs.yml and .lando.yml base images bumped to 3.11

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@yakky
yakky requested a review from ajrbyers August 30, 2026 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants