Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions djangocms_versioning/cms_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def __init__(self):
self.add_to_context = {}
self.add_to_field_extension = {}

contract = "djangocms_versioning", VersionableItem

@cached_property
def versionables_by_content(self):
"""Returns a dict of {content_model_cls: VersionableItem obj}"""
Expand Down
181 changes: 181 additions & 0 deletions docs/api/contract.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
.. _versioning_contract:

Versioning contract
===================

Django CMS uses a contract-based approach for versioning, allowing different versioning
implementations to integrate with the CMS and its ecosystem. **djangocms-versioning defines
the versioning contract** for django CMS. This section describes the contract that
djangocms-versioning does implement and other versioning packages must implement to work
with django CMS.

Overview
--------

The contract is implemented through django CMS's ``CMSAppExtension`` mechanism in the
``cms_config.py`` module. When installed, djangocms-versioning becomes the versioning
provider for all content types that register with it—including django CMS pages,
aliases, stories, and any custom content models.

.. note::

**djangocms-versioning** is the reference implementation endorsed by the django CMS
Association.

Contract definition
-------------------

The contract is defined in djangocms-versioning's ``cms_config.py`` using the
``contract`` class attribute, a 2-tuple consisting of the contract name (``"djangocms_versioning"``)
and the contract class (``VersionableItem``):

.. code-block:: python

from cms.app_base import CMSAppExtension
from .datastructures import VersionableItem

class VersioningCMSExtension(CMSAppExtension):
contract = "djangocms_versioning", VersionableItem

def __init__(self):
self.versionables = []

def configure_app(self, cms_config):
# Process the versioning configuration
if hasattr(cms_config, "versioning"):
self.handle_versioning_setting(cms_config)
# ... additional setup

The ``contract`` attribute is a tuple of:

1. The contract name (``"djangocms_versioning"``)
2. The ``VersionableItem`` class that apps use to register their content models

This allows other packages to register for versioning without importing directly from
djangocms-versioning, enabling alternative implementations to provide the same contract.

Contract components
-------------------

VersionableItem class
~~~~~~~~~~~~~~~~~~~~~

The ``VersionableItem`` class defines how a content model participates in versioning.
At minimum, it must accept:

``content_model``
The Django model class that stores versioned content (the :term:`content model`).

``grouper_field_name``
The name of the foreign key field on the content model that points to the
:term:`grouper model`.

``copy_function``
An (optional) callable that creates a copy of a content object when creating new versions.

Additional optional parameters are djangocms-versioning-specific and may include:

- ``extra_grouping_fields``: Additional fields for grouping versions (e.g., ``language``)
- ``on_publish``, ``on_unpublish``, ``on_draft_create``, ``on_archive``: Lifecycle hooks
- ``preview_url``: Function to generate preview URLs for versions
- ``content_admin_mixin``: Custom admin mixin for the content model
- ``grouper_admin_mixin``: Custom admin mixin for the grouper model

Manager modifications
~~~~~~~~~~~~~~~~~~~~~

A versioning package typically modifies the content model's managers:

``objects`` manager
Should filter to return only published content by default, ensuring unpublished
content never leaks to the public.

``admin_manager``
Should provide access to all content versions, for use in admin contexts only.

These managers enable the pattern:

.. code-block:: python

# Public queries - only published content
PostContent.objects.filter(...)

# Admin queries - all versions accessible
PostContent.admin_manager.filter(...)

Registration mechanism
----------------------

Content models register for versioning via ``cms_config.py``:

.. code-block:: python

# myapp/cms_config.py
from cms.app_base import CMSAppConfig

from .models import MyContent


class MyAppConfig(CMSAppConfig):
djangocms_versioning_enabled = True # <contract>_enabled = True

def __init__(self, app):
super().__init__(app)

# Dynamically get the installed contract object
VersionableItem = self.get_contract("djangocms_versioning")

self.versioning = [
VersionableItem(
content_model=MyContent,
grouper_field_name="grouper",
grouper_admin_mixin="__default__",
),
]

The ``djangocms_versioning_enabled = True`` attribute signals that this app wants to
use the versioning extension. The ``get_contract("djangocms_versioning")`` call retrieves the
``VersionableItem`` class from the installed versioning package, allowing the app to
register its content models for versioning.

Implementing alternative versioning packages
--------------------------------------------

Alternative versioning packages must follow the contract defined by djangocms-versioning.
Specifically, they must:

1. **Export a VersionableItem class** (or compatible equivalent) that other packages
can discover and use for registration.

2. **Process the** ``versioning`` **attribute** from ``CMSAppConfig`` subclasses that
have ``djangocms_versioning_enabled = True``.

3. **Modify content model managers** to provide the ``objects`` / ``admin_manager``
pattern expected by django CMS and ecosystem packages.

4. **(Optional) Provide version state information** for the CMS toolbar and admin
interfaces. djangocms-versioning does this by injecting a ``content_indicator``
method onto content models that returns status strings (e.g., ``"published"``,
``"draft"``, ``"dirty"``). Alternative implementations may define their own states
or omit this functionality.

Ecosystem compatibility
-----------------------

Packages in the django CMS ecosystem (such as djangocms-alias and djangocms-stories)
register their content models using the versioning contract. When you install a
versioning package, it becomes responsible for managing versions of *all* registered
content types.

This means:

- Switching versioning packages affects all versioned content across your site
- Alternative implementations must handle registrations from ecosystem packages
- The version states and workflow defined by your versioning package apply universally

See also
--------

- :doc:`/introduction/versioning_integration` for integrating your models with versioning
- :doc:`advanced_configuration` for customizing versioning behavior
- :doc:`models` for the Version model reference
6 changes: 3 additions & 3 deletions docs/explanations/admin_options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ Example:
class PostContentAdmin(ExtendedVersionAdminMixin, admin.ModelAdmin):
list_display = ["title"]

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`,
in the form of a dictionary of {model_name: {field: method}}, the admin for the model, will alter the field, using the method provided.
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`,
in the form of a dictionary of {model_name: {field: method}}, the admin for the model will alter the field using the method provided.

.. code-block:: python

Expand All @@ -116,7 +116,7 @@ Adding State Indicators

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

.. image:: static/Status-indicators.png
.. image:: /static/Status-indicators.png
:width: 50%

You can use these on your content model's changelist view admin by adding the following fixin to the model's Admin class:
Expand Down
18 changes: 18 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Welcome to "djangocms-versioning"'s documentation!
:caption: Tutorials:

introduction/basic_concepts
introduction/working_with_pages
introduction/versioning_integration

.. toctree::
Expand All @@ -24,6 +25,7 @@ Welcome to "djangocms-versioning"'s documentation!
api/advanced_configuration
api/signals
api/management_commands
api/contract
api/settings

.. toctree::
Expand Down Expand Up @@ -79,3 +81,19 @@ Glossary
existing version. By default it will copy the current published version,
but when reverting to an old version, a specific unpublished or archived version
will be used. A customizable copy function is used for this.

cms_config
The ``cms_config.py`` file in a Django app that defines how the app
integrates with django CMS and djangocms-versioning. It contains a
``CMSAppConfig`` subclass with versioning settings.

ExtendedVersionAdminMixin
A mixin class for Django admin that adds versioning-related fields and
actions to the admin interface, including author, modified date,
versioning state, and version management actions.

extended_admin_field_modifiers
A configuration option in :term:`cms_config` that allows customizing
how fields are displayed in admin views that use the
:term:`ExtendedVersionAdminMixin`. Defined as a dictionary mapping
models to field transformation functions.
76 changes: 45 additions & 31 deletions docs/introduction/versioning_integration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,40 +76,50 @@ assumes that the site can be changed and those changes should be versioned, we w
Register the model for versioning
----------------------------------

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

.. code-block:: python

# blog/cms_config.py
from cms.app_base import CMSAppConfig
from djangocms_versioning.datastructures import VersionableItem, default_copy
from .models import PostContent


class BlogCMSConfig(CMSAppConfig):
class BlogCMSConfig(CMSAppConfig):
djangocms_versioning_enabled = True
versioning = [
VersionableItem(
content_model=PostContent,
grouper_field_name='post',
copy_function=default_copy,
grouper_admin_mixin="__default__",
),
]

In this configuration we must specify the :term:`content model <content model>` (`PostContent`),
the name of the field that is a foreign key to the :term:`grouper model <grouper model>` (`post`)
and a :term:`copy function <copy function>`. For simple model structures, the `default_copy` function
which we have used is sufficient, but in many cases you might need to write your own custom :term:`copy function <copy function>`
(more on that below).

def __init__(self, app):
super().__init__(app)

# Discover the VersionableItem class from the installed versioning package
VersionableItem = self.get_contract("djangocms_versioning")

self.versioning = [
VersionableItem(
content_model=PostContent,
grouper_field_name='post',
grouper_admin_mixin="__default__",
),
]

In this configuration we must specify the :term:`content model <content model>` (``PostContent``)
and the name of the field that is a foreign key to the :term:`grouper model <grouper model>`
(``post``).

.. note::

**Best practice:** Always use ``self.get_contract("djangocms_versioning")`` to obtain
the ``VersionableItem`` class rather than importing directly from djangocms-versioning.
This ensures your code works with any versioning package that implements the
``djangocms_versioning`` contract. See :ref:`versioning_contract` for details.

.. versionadded:: 2.4.0

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

Once a model is registered for versioning its behaviour changes:

Expand Down Expand Up @@ -227,7 +237,6 @@ This is probably not how one would want things to work in this scenario, so to f

# blog/cms_config.py
from cms.app_base import CMSAppConfig
from djangocms_versioning.datastructures import VersionableItem
from .models import PostContent, Poll, Answer


Expand Down Expand Up @@ -263,15 +272,20 @@ This is probably not how one would want things to work in this scenario, so to f
return new_content


class BlogCMSConfig(CMSAppConfig):
class BlogCMSConfig(CMSAppConfig):
djangocms_versioning_enabled = True
versioning = [
VersionableItem(
content_model=PostContent,
grouper_field_name='post',
copy_function=custom_copy,
),
]

def __init__(self, app):
super().__init__(app)
VersionableItem = self.get_contract("djangocms_versioning")

self.versioning = [
VersionableItem(
content_model=PostContent,
grouper_field_name='post',
copy_function=custom_copy,
),
]

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)
and returns the copied content object. We have customized it to create not just a new PostContent object (which `default_copy` would have done),
Expand Down
Loading
Loading