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.
treebeardremains 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_urlcopies the currently effective slug and overwrite URL fromPageUrlonto every page content.cms.0045_pageurl_site_unique_pathadds a denormalizedsiteforeign key toPageUrl, 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.
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
IntegrityErrorand the publish is not applied.Path uniqueness per site and language is now also enforced by a database constraint.
PageUrlcarries a denormalizedsiteforeign 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¶
pytestis now configured for the django CMS test suite. Test settings moved frommanage.pyintocms/tests/settings.py, so the suite can be run with eitherpytestormanage.py test.Wheels are now built from an explicit package allow-list and checked with
check-wheel-contentsin 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
EmptyToolbarmissing theclipboardandtoolbar_languageproperties, which made the placeholder admin fail when no toolbar was attached to the requestAvoid N+1 queries when copying pages or plugins between languages
Remove the stray
docs,scripts, andtest_requirementsdirectories from the wheelCorrect 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 viapip install --no-binary django-cms django-cmsor the release tarballs. The public testing utilities incms.test_utils, includingcms.test_utils.testcases.CMSTestCase, are not affected and remain in the wheel. Projects importing fromcms.testsshould switch tocms.test_utils.