5.1.1 release notes

August 5, 2026

Welcome to django CMS 5.1.1!

These release notes cover what has changed since django CMS 5.1.0. This is a patch release, but it contains two security fixes, two database migrations, and a change to how page URLs are stored and validated. Read the upgrade instructions and the backward-incompatible section before updating an existing project.

If you are upgrading from django CMS 5.0.x or earlier, read the 5.1.0 release notes first — they cover the backward incompatible changes, deprecations, and removals of the 5.1 series.

Django and Python compatibility

django CMS 5.1 supports Django 5.2, 6.0, and 6.1. Django 4.2, 5.0, and 5.1 are no longer supported because those series no longer receive upstream maintenance. We highly recommend and only support the latest release of each supported series.

It supports Python 3.10, 3.11, 3.12, 3.13, and 3.14.

Release highlights

  • Two security fixes. A non-superuser page-user manager could manage — and therefore take over — superuser accounts. And the “paste a copy” endpoint only checked the target placeholder, so a client-supplied plugin id could be used to copy plugin content out of a placeholder the user is not allowed to read. Both are fixed; see Security fixes.

  • Page URLs are versioned with the content. The slug and overwrite URL now live on PageContent, so they are versioned together with the rest of the content. Path uniqueness per site and language is now enforced by a database constraint, and unpublished pages no longer resolve. Two migrations are included.

  • An optional page tree backend without django-treebeard. django CMS now ships its own materialized-path implementation. It is byte-compatible with treebeard’s path encoding, so it can be switched on and off per deployment without a migration. treebeard remains the default in 5.1.1.

  • Correct language when editing. Plugins re-rendered after an edit were rendered in the request language rather than the plugin’s own language.

How to upgrade to 5.1.1

Before upgrading, back up your database and review the backward-incompatible changes below. This release changes where page slugs and overwrite URLs are stored and adds a uniqueness constraint on page paths, so the migrations are not purely additive.

Upgrade django CMS:

python -m pip install --upgrade "django-cms==5.1.1"

Then apply the migrations, collect static files, and run the django CMS checks:

python manage.py migrate
python manage.py collectstatic --noinput
python manage.py cms check

Two migrations are applied:

  • cms.0044_pagecontent_slug_overwrite_url copies the currently effective slug and overwrite URL from PageUrl onto every page content.

  • cms.0045_pageurl_site_unique_path adds a denormalized site foreign key to PageUrl, fills it, resolves any pre-existing duplicate paths, and adds the uniqueness constraint.

Warning

If your database contains pages that share the same path on the same site and language — which was possible before, although only one of them was ever served — the migration keeps the path for one of them and leaves the others without a path until they are published again. Run the migration against a copy of your production database first and check the result.

Test custom plugins, apphooks, permissions, frontend editing, and all templates in a staging environment before deploying.

What’s new in 5.1.1

Security fixes

Superusers could be managed by subordinate page-user managers

get_subordinate_users() returned superuser accounts to any user holding can_change_permissions, in particular to any user granted that right through a GlobalPagePermission. Such a manager could edit a superuser account, delete it, or change its password through the page-user admin, and thereby take it over.

Superusers are now never subordinate to a non-superuser, however many permissions the latter holds. PageUserAdmin additionally rejects change, delete, and password-change requests for superuser objects, because Django’s password view resolves its object directly and only checks has_change_permission().

If your project relies on delegated user administration, review who holds GlobalPagePermission with can_change_permissions: such a manager has every non-superuser account on the site as a subordinate, not only the accounts they created. This is now spelled out in Permissions.

“Paste a copy” disclosed plugin content from unauthorized placeholders

The paste endpoints checked only that the user was allowed to add plugins to the target placeholder. The pasted plugin, however, is identified by a client-supplied id, so a user could paste a plugin (or a placeholder reference) from a placeholder they have no read access to into one they control, and read its content there.

Both _paste_plugin() and _paste_placeholder() now also check the source side through the new PlaceholderAdmin.has_paste_from_source_permission() hook. Pasting from the user’s own clipboard is unaffected.

Relatedly, a clipboard is now private to its owner: UserSettings.has_placeholder_change_permission() and PlaceholderForeignKey.run_checks() previously returned True unconditionally — for every user, not just the clipboard’s owner. They now return True only for the owner (or a superuser).

Page URLs

The slug and the overwrite URL of a page are now stored on the PageContent model, so they are versioned together with the rest of the content when a versioning package such as djangocms-versioning is installed. The PageUrl model remains the routing table, but its entries are derived from the published content only. A data migration copies the currently effective slug and overwrite URL from the existing PageUrl objects onto every page content.

This changes URL behavior in a few user-visible ways:

  • Changing the slug or overwrite URL of a draft no longer affects the live URL; the change takes effect when the draft is published. Consequently, these fields are no longer locked in the page admin while a published version exists. Without a versioning package every save remains public and updates the URL immediately, as before.

  • Unpublishing a page content removes the URL’s path, so the page — and its descendant pages — stop resolving. The slug stays reserved for the page and the path is restored on the next publish.

  • Publishing a version whose derived URL collides with the URL of another page on the same site raises an IntegrityError and the publish is not applied.

  • Path uniqueness per site and language is now also enforced by a database constraint. PageUrl carries a denormalized site foreign key for this purpose; a data migration fills it and resolves any pre-existing duplicate paths (of each set, only one page was actually served — it keeps the path, the others become unreachable until their next publish).

  • Updating the URLs of a page’s descendants (e.g. after a rename) now runs one query per tree level instead of two queries per descendant page.

Page.get_absolute_url() now returns None for unreachable pages; see Backward incompatible changes in 5.1.1 below.

Optional page tree backend without django-treebeard

django CMS now ships cms.utils.mptree, its own dependency-free materialized-path tree implementation for the page tree. It treats parent_id as the single source of truth and maintains path, depth, and numchild as a derived cache, operating set-based rather than pulling subtrees into Python, and locking at parent-row granularity.

Its fields are declared identically to treebeard’s MP_Node and its path encoding is byte-for-byte compatible with treebeard’s defaults (base-36 alphabet, steplen=4, no separators), so switching backends requires no migration — the same database works with either.

The backend is selected by the CMS_TREE_BACKEND setting and defaults to "treebeard". To opt in:

CMS_TREE_BACKEND = "mptree"

An environment variable of the same name is used as a fallback when the setting is not defined, which keeps the backends swappable in CI without touching the settings module. The value is resolved at import time, so changing it requires a process restart. In mptree mode, treebeard is never imported.

This is a preparatory step towards dropping the django-treebeard dependency. It remains a required dependency in 5.1.1, now pinned to >=4.3,<6.

Minor features

  • pytest is now configured for the django CMS test suite. Test settings moved from manage.py into cms/tests/settings.py, so the suite can be run with either pytest or manage.py test.

  • Wheels are now built from an explicit package allow-list and checked with check-wheel-contents in the publishing workflows.

  • The custom plugin how-to documents the XSS security contract for plugin authors: what django CMS escapes, what it does not, and where a plugin is responsible for escaping its own content. See How to create Plugins.

Bug Fixes

  • Fix superuser accounts being manageable by non-superuser page-user managers

  • Fix “paste a copy” disclosing plugin content from placeholders the user cannot read

  • Fix plugins being re-rendered in the request language instead of the plugin’s own language after an edit

  • Fix the squashed initial migration failing with a custom user model

  • Fix EmptyToolbar missing the clipboard and toolbar_language properties, which made the placeholder admin fail when no toolbar was attached to the request

  • Avoid N+1 queries when copying pages or plugins between languages

  • Remove the stray docs, scripts, and test_requirements directories from the wheel

  • Correct the Bootstrap icon license notice

Backward incompatible changes in 5.1.1

get_absolute_url() of unreachable pages

Page.get_absolute_url() (and consequently PageContent.get_absolute_url()) now returns None for pages without a reachable path — for example, while the page or one of its ancestors is unpublished. Previously a URL was fabricated from the bare slug, which pointed to a location that never resolved (or resolved to a different page).

Python code that consumes the result without checking it should be reviewed; django CMS’s own toolbar helpers already handle the None case. In templates, rendering the value unchecked now produces href="None" — use Django’s default filter to render a safe fallback instead:

<a href="{{ page.get_absolute_url|default:'' }}">{{ page.get_title }}</a>

Page slug and overwrite URL fields

The slug and overwrite URL are now fields of PageContent rather than of PageUrl. Code that wrote to PageUrl directly to rename a page should write to the page content instead and let django CMS derive the routing table from it. See Page URLs above for the behavioral consequences, including the new per-site, per-language uniqueness constraint on PageUrl.path.

Clipboard permissions

UserSettings.has_placeholder_change_permission() and PlaceholderForeignKey.run_checks() no longer return True unconditionally; they now grant access only to the clipboard’s owner and to superusers. Custom code that relied on the previous permissive behavior to access another user’s clipboard will now be denied.

PlaceholderAdmin subclasses that override the paste endpoints should account for the new has_paste_from_source_permission(request, source_placeholder, plugins) hook, which is consulted before the existing has_copy_from_clipboard_permission() check.

Wheel contents

The 5.1.0 wheel erroneously included the top-level directories docs, scripts, and test_requirements. They have been removed from the wheel without a deprecation period: they are not part of django CMS’s Python API, and projects importing from them were relying on a packaging accident. Wheels are now built from an explicit package allow-list and verified with check-wheel-contents in the publishing workflows.

Features deprecated in 5.1.1

Packaging

  • Shipping the internal test suite (cms.tests) in binary distributions (wheels) is deprecated; a future release will no longer include it (#8733). The suite remains part of the source distribution and stays available via pip install --no-binary django-cms django-cms or the release tarballs. The public testing utilities in cms.test_utils, including cms.test_utils.testcases.CMSTestCase, are not affected and remain in the wheel. Projects importing from cms.tests should switch to cms.test_utils.