Skip to content

Commit aa9674f

Browse files
docs: Update docs to explain the djangocms_versioning contract (#511)
* docs: Update docs to explain the djangocms_versioning contract * Update docs/introduction/working_with_pages.rst Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Update docs/introduction/working_with_pages.rst Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Update docs/explanations/admin_options.rst --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
1 parent 462c67c commit aa9674f

File tree

6 files changed

+488
-35
lines changed

6 files changed

+488
-35
lines changed

djangocms_versioning/cms_config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ def __init__(self):
5050
self.add_to_context = {}
5151
self.add_to_field_extension = {}
5252

53+
contract = "djangocms_versioning", VersionableItem
54+
5355
@cached_property
5456
def versionables_by_content(self):
5557
"""Returns a dict of {content_model_cls: VersionableItem obj}"""

docs/api/contract.rst

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
.. _versioning_contract:
2+
3+
Versioning contract
4+
===================
5+
6+
Django CMS uses a contract-based approach for versioning, allowing different versioning
7+
implementations to integrate with the CMS and its ecosystem. **djangocms-versioning defines
8+
the versioning contract** for django CMS. This section describes the contract that
9+
djangocms-versioning does implement and other versioning packages must implement to work
10+
with django CMS.
11+
12+
Overview
13+
--------
14+
15+
The contract is implemented through django CMS's ``CMSAppExtension`` mechanism in the
16+
``cms_config.py`` module. When installed, djangocms-versioning becomes the versioning
17+
provider for all content types that register with it—including django CMS pages,
18+
aliases, stories, and any custom content models.
19+
20+
.. note::
21+
22+
**djangocms-versioning** is the reference implementation endorsed by the django CMS
23+
Association.
24+
25+
Contract definition
26+
-------------------
27+
28+
The contract is defined in djangocms-versioning's ``cms_config.py`` using the
29+
``contract`` class attribute, a 2-tuple consisting of the contract name (``"djangocms_versioning"``)
30+
and the contract class (``VersionableItem``):
31+
32+
.. code-block:: python
33+
34+
from cms.app_base import CMSAppExtension
35+
from .datastructures import VersionableItem
36+
37+
class VersioningCMSExtension(CMSAppExtension):
38+
contract = "djangocms_versioning", VersionableItem
39+
40+
def __init__(self):
41+
self.versionables = []
42+
43+
def configure_app(self, cms_config):
44+
# Process the versioning configuration
45+
if hasattr(cms_config, "versioning"):
46+
self.handle_versioning_setting(cms_config)
47+
# ... additional setup
48+
49+
The ``contract`` attribute is a tuple of:
50+
51+
1. The contract name (``"djangocms_versioning"``)
52+
2. The ``VersionableItem`` class that apps use to register their content models
53+
54+
This allows other packages to register for versioning without importing directly from
55+
djangocms-versioning, enabling alternative implementations to provide the same contract.
56+
57+
Contract components
58+
-------------------
59+
60+
VersionableItem class
61+
~~~~~~~~~~~~~~~~~~~~~
62+
63+
The ``VersionableItem`` class defines how a content model participates in versioning.
64+
At minimum, it must accept:
65+
66+
``content_model``
67+
The Django model class that stores versioned content (the :term:`content model`).
68+
69+
``grouper_field_name``
70+
The name of the foreign key field on the content model that points to the
71+
:term:`grouper model`.
72+
73+
``copy_function``
74+
An (optional) callable that creates a copy of a content object when creating new versions.
75+
76+
Additional optional parameters are djangocms-versioning-specific and may include:
77+
78+
- ``extra_grouping_fields``: Additional fields for grouping versions (e.g., ``language``)
79+
- ``on_publish``, ``on_unpublish``, ``on_draft_create``, ``on_archive``: Lifecycle hooks
80+
- ``preview_url``: Function to generate preview URLs for versions
81+
- ``content_admin_mixin``: Custom admin mixin for the content model
82+
- ``grouper_admin_mixin``: Custom admin mixin for the grouper model
83+
84+
Manager modifications
85+
~~~~~~~~~~~~~~~~~~~~~
86+
87+
A versioning package typically modifies the content model's managers:
88+
89+
``objects`` manager
90+
Should filter to return only published content by default, ensuring unpublished
91+
content never leaks to the public.
92+
93+
``admin_manager``
94+
Should provide access to all content versions, for use in admin contexts only.
95+
96+
These managers enable the pattern:
97+
98+
.. code-block:: python
99+
100+
# Public queries - only published content
101+
PostContent.objects.filter(...)
102+
103+
# Admin queries - all versions accessible
104+
PostContent.admin_manager.filter(...)
105+
106+
Registration mechanism
107+
----------------------
108+
109+
Content models register for versioning via ``cms_config.py``:
110+
111+
.. code-block:: python
112+
113+
# myapp/cms_config.py
114+
from cms.app_base import CMSAppConfig
115+
116+
from .models import MyContent
117+
118+
119+
class MyAppConfig(CMSAppConfig):
120+
djangocms_versioning_enabled = True # <contract>_enabled = True
121+
122+
def __init__(self, app):
123+
super().__init__(app)
124+
125+
# Dynamically get the installed contract object
126+
VersionableItem = self.get_contract("djangocms_versioning")
127+
128+
self.versioning = [
129+
VersionableItem(
130+
content_model=MyContent,
131+
grouper_field_name="grouper",
132+
grouper_admin_mixin="__default__",
133+
),
134+
]
135+
136+
The ``djangocms_versioning_enabled = True`` attribute signals that this app wants to
137+
use the versioning extension. The ``get_contract("djangocms_versioning")`` call retrieves the
138+
``VersionableItem`` class from the installed versioning package, allowing the app to
139+
register its content models for versioning.
140+
141+
Implementing alternative versioning packages
142+
--------------------------------------------
143+
144+
Alternative versioning packages must follow the contract defined by djangocms-versioning.
145+
Specifically, they must:
146+
147+
1. **Export a VersionableItem class** (or compatible equivalent) that other packages
148+
can discover and use for registration.
149+
150+
2. **Process the** ``versioning`` **attribute** from ``CMSAppConfig`` subclasses that
151+
have ``djangocms_versioning_enabled = True``.
152+
153+
3. **Modify content model managers** to provide the ``objects`` / ``admin_manager``
154+
pattern expected by django CMS and ecosystem packages.
155+
156+
4. **(Optional) Provide version state information** for the CMS toolbar and admin
157+
interfaces. djangocms-versioning does this by injecting a ``content_indicator``
158+
method onto content models that returns status strings (e.g., ``"published"``,
159+
``"draft"``, ``"dirty"``). Alternative implementations may define their own states
160+
or omit this functionality.
161+
162+
Ecosystem compatibility
163+
-----------------------
164+
165+
Packages in the django CMS ecosystem (such as djangocms-alias and djangocms-stories)
166+
register their content models using the versioning contract. When you install a
167+
versioning package, it becomes responsible for managing versions of *all* registered
168+
content types.
169+
170+
This means:
171+
172+
- Switching versioning packages affects all versioned content across your site
173+
- Alternative implementations must handle registrations from ecosystem packages
174+
- The version states and workflow defined by your versioning package apply universally
175+
176+
See also
177+
--------
178+
179+
- :doc:`/introduction/versioning_integration` for integrating your models with versioning
180+
- :doc:`advanced_configuration` for customizing versioning behavior
181+
- :doc:`models` for the Version model reference

docs/explanations/admin_options.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ Example:
9393
class PostContentAdmin(ExtendedVersionAdminMixin, admin.ModelAdmin):
9494
list_display = ["title"]
9595
96-
The :term:`ExtendedVersionAdminMixin` also has functionality to alter fields from other apps. By adding the :term:`admin_field_modifiers` to a given apps :term:`cms_config`,
97-
in the form of a dictionary of {model_name: {field: method}}, the admin for the model, will alter the field, using the method provided.
96+
The :term:`ExtendedVersionAdminMixin` also has functionality to alter fields from other apps. By adding the :term:`extended_admin_field_modifiers` to a given app's :term:`cms_config`,
97+
in the form of a dictionary of {model_name: {field: method}}, the admin for the model will alter the field using the method provided.
9898

9999
.. code-block:: python
100100
@@ -116,10 +116,10 @@ Adding State Indicators
116116

117117
djangocms-versioning provides status indicators for django CMS' content models, you may know them from the page tree in django-cms:
118118

119-
.. image:: static/Status-indicators.png
119+
.. image:: /static/Status-indicators.png
120120
:width: 50%
121121

122-
You can use these on your content model's changelist view admin by adding the following fixin to the model's Admin class:
122+
You can use these on your content model's changelist view admin by adding the following mixin to the model's Admin class:
123123

124124
.. code-block:: python
125125

docs/index.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Welcome to "djangocms-versioning"'s documentation!
66
:caption: Tutorials:
77

88
introduction/basic_concepts
9+
introduction/working_with_pages
910
introduction/versioning_integration
1011

1112
.. toctree::
@@ -24,6 +25,7 @@ Welcome to "djangocms-versioning"'s documentation!
2425
api/advanced_configuration
2526
api/signals
2627
api/management_commands
28+
api/contract
2729
api/settings
2830

2931
.. toctree::
@@ -79,3 +81,19 @@ Glossary
7981
existing version. By default it will copy the current published version,
8082
but when reverting to an old version, a specific unpublished or archived version
8183
will be used. A customizable copy function is used for this.
84+
85+
cms_config
86+
The ``cms_config.py`` file in a Django app that defines how the app
87+
integrates with django CMS and djangocms-versioning. It contains a
88+
``CMSAppConfig`` subclass with versioning settings.
89+
90+
ExtendedVersionAdminMixin
91+
A mixin class for Django admin that adds versioning-related fields and
92+
actions to the admin interface, including author, modified date,
93+
versioning state, and version management actions.
94+
95+
extended_admin_field_modifiers
96+
A configuration option in :term:`cms_config` that allows customizing
97+
how fields are displayed in admin views that use the
98+
:term:`ExtendedVersionAdminMixin`. Defined as a dictionary mapping
99+
models to field transformation functions.

docs/introduction/versioning_integration.rst

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -76,40 +76,50 @@ assumes that the site can be changed and those changes should be versioned, we w
7676
Register the model for versioning
7777
----------------------------------
7878

79-
Now we need to make versioning aware of these models. So we have to register them in the `cms_config.py` file.
80-
A very basic configuration would look like this:
79+
Now we need to make versioning aware of these models. So we have to register them in the
80+
``cms_config.py`` file. A basic configuration looks like this:
8181

8282
.. code-block:: python
8383
8484
# blog/cms_config.py
8585
from cms.app_base import CMSAppConfig
86-
from djangocms_versioning.datastructures import VersionableItem, default_copy
8786
from .models import PostContent
8887
8988
90-
class BlogCMSConfig(CMSAppConfig):
89+
class BlogCMSConfig(CMSAppConfig):
9190
djangocms_versioning_enabled = True
92-
versioning = [
93-
VersionableItem(
94-
content_model=PostContent,
95-
grouper_field_name='post',
96-
copy_function=default_copy,
97-
grouper_admin_mixin="__default__",
98-
),
99-
]
100-
101-
In this configuration we must specify the :term:`content model <content model>` (`PostContent`),
102-
the name of the field that is a foreign key to the :term:`grouper model <grouper model>` (`post`)
103-
and a :term:`copy function <copy function>`. For simple model structures, the `default_copy` function
104-
which we have used is sufficient, but in many cases you might need to write your own custom :term:`copy function <copy function>`
105-
(more on that below).
91+
92+
def __init__(self, app):
93+
super().__init__(app)
94+
95+
# Discover the VersionableItem class from the installed versioning package
96+
VersionableItem = self.get_contract("djangocms_versioning")
97+
98+
self.versioning = [
99+
VersionableItem(
100+
content_model=PostContent,
101+
grouper_field_name='post',
102+
grouper_admin_mixin="__default__",
103+
),
104+
]
105+
106+
In this configuration we must specify the :term:`content model <content model>` (``PostContent``)
107+
and the name of the field that is a foreign key to the :term:`grouper model <grouper model>`
108+
(``post``).
109+
110+
.. note::
111+
112+
**Best practice:** Always use ``self.get_contract("djangocms_versioning")`` to obtain
113+
the ``VersionableItem`` class rather than importing directly from djangocms-versioning.
114+
This ensures your code works with any versioning package that implements the
115+
``djangocms_versioning`` contract. See :ref:`versioning_contract` for details.
106116

107117
.. versionadded:: 2.4.0
108118

109-
The `grouper_admin_mixin` parameter is optional. For backwards compatibility, it defaults to ``None``.
110-
To add the default state indicators, make it ``"__default__"``. This will use the
111-
:class:`~djangocms_versioning.admin.DefaultGrouperAdminMixin` which includes the state indicator, author and modified date.
112-
If you want to use a different mixin, you can specify it here.
119+
The ``grouper_admin_mixin`` parameter is optional. For backwards compatibility, it
120+
defaults to ``None``. To add the default state indicators, set it to ``"__default__"``.
121+
This will use the :class:`~djangocms_versioning.admin.DefaultGrouperAdminMixin` which
122+
includes the state indicator, author and modified date.
113123

114124
Once a model is registered for versioning its behaviour changes:
115125

@@ -227,7 +237,6 @@ This is probably not how one would want things to work in this scenario, so to f
227237
228238
# blog/cms_config.py
229239
from cms.app_base import CMSAppConfig
230-
from djangocms_versioning.datastructures import VersionableItem
231240
from .models import PostContent, Poll, Answer
232241
233242
@@ -263,15 +272,20 @@ This is probably not how one would want things to work in this scenario, so to f
263272
return new_content
264273
265274
266-
class BlogCMSConfig(CMSAppConfig):
275+
class BlogCMSConfig(CMSAppConfig):
267276
djangocms_versioning_enabled = True
268-
versioning = [
269-
VersionableItem(
270-
content_model=PostContent,
271-
grouper_field_name='post',
272-
copy_function=custom_copy,
273-
),
274-
]
277+
278+
def __init__(self, app):
279+
super().__init__(app)
280+
VersionableItem = self.get_contract("djangocms_versioning")
281+
282+
self.versioning = [
283+
VersionableItem(
284+
content_model=PostContent,
285+
grouper_field_name='post',
286+
copy_function=custom_copy,
287+
),
288+
]
275289
276290
As you can see from the example above the :term:`copy function <copy function>` takes one param (the content object of the version we're copying)
277291
and returns the copied content object. We have customized it to create not just a new PostContent object (which `default_copy` would have done),

0 commit comments

Comments
 (0)