Signals

djangocms-versioning provides signals that allow you to react to version lifecycle events. These signals can be used for tasks like:

  • Invalidating caches

  • Updating external search indexes

  • Triggering workflows

  • Logging version changes

  • Sending notifications

Available Signals

pre_version_operation

Sent before a version state change occurs.

Signal sender: The content model class (e.g., PostContent)

post_version_operation

Sent after a version state change has been completed successfully.

Signal sender: The content model class (e.g., PostContent)

Signal Parameters

Both signals emit the following keyword arguments:

Signal Parameters

Parameter

Description

Type

sender

The content model class (e.g., PostContent, PageContent)

Model class

obj

The Version instance being operated on

Version

operation

The type of operation being performed

str (see Operations below)

token

A unique token to tie pre and post signals together

str (UUID)

unpublished

(For publish operations) List of versions that will be unpublished

list of Version objects

to_be_published

(For unpublish operations) List of versions that will be published as replacements

list of Version objects

Version Operations

The operation parameter can have one of these values (from djangocms_versioning.constants):

Version Operations

Operation Constant

Description

OPERATION_DRAFT

A new draft version has been created (or version moved to draft state)

OPERATION_PUBLISH

A draft version has been published

OPERATION_UNPUBLISH

A published version has been unpublished

OPERATION_ARCHIVE

A draft version has been archived

Signal Token

Each signal emission includes a unique token parameter that ties related pre and post signals together. This is particularly useful when:

  • Multiple signals are fired in quick succession

  • You need to correlate pre and post operations

  • You’re implementing transactional operations

Example:

signal_state = {}

@receiver(pre_version_operation)
def before_version_change(sender, obj, operation, token, **kwargs):
    signal_state[token] = {
        'start_time': timezone.now(),
        'operation': operation,
        'version_id': obj.pk,
    }

@receiver(post_version_operation)
def after_version_change(sender, obj, operation, token, **kwargs):
    state = signal_state.pop(token, {})
    duration = timezone.now() - state.get('start_time', timezone.now())
    print(f"Operation {operation} took {duration.total_seconds()}s")

Common Use Cases

Invalidating a Cache

from django.core.cache import cache
from django.dispatch import receiver
from djangocms_versioning.signals import post_version_operation
from blog.models import PostContent

@receiver(post_version_operation, sender=PostContent)
def invalidate_post_cache(sender, obj, operation, **kwargs):
    """Invalidate cache whenever a post version changes"""
    content = obj.content
    cache_key = f"post_{content.pk}"
    cache.delete(cache_key)

Updating a Search Index

from django.dispatch import receiver
from djangocms_versioning import constants
from djangocms_versioning.signals import post_version_operation
from blog.models import PostContent

@receiver(post_version_operation, sender=PostContent)
def update_search_index(sender, obj, operation, unpublished=None, **kwargs):
    """Update search index when posts are published/unpublished"""

    if operation == constants.OPERATION_PUBLISH:
        # New content is now public
        if unpublished:
            # This is an update of existing content
            index.update_document(obj.content)
        else:
            # This is a new publication
            index.add_document(obj.content)

    elif operation == constants.OPERATION_UNPUBLISH:
        # Content was removed from public
        index.remove_document(obj.content)

Sending Notifications

from django.dispatch import receiver
from django.core.mail import send_mail
from djangocms_versioning import constants
from djangocms_versioning.signals import post_version_operation
from blog.models import PostContent

@receiver(post_version_operation, sender=PostContent)
def notify_on_publish(sender, obj, operation, **kwargs):
    """Send email notification when post is published"""

    if operation == constants.OPERATION_PUBLISH:
        content = obj.content
        send_mail(
            subject=f"Post Published: {content.title}",
            message=f"Your post '{content.title}' has been published.",
            from_email='noreply@example.com',
            recipient_list=[obj.created_by.email],
            fail_silently=True,
        )

Logging Version Changes

import logging
from django.dispatch import receiver
from djangocms_versioning import constants
from djangocms_versioning.signals import pre_version_operation, post_version_operation
from blog.models import PostContent

logger = logging.getLogger(__name__)

@receiver(pre_version_operation, sender=PostContent)
def log_version_change_start(sender, obj, operation, token, **kwargs):
    logger.info(
        f"Starting {operation} for {sender.__name__} #{obj.pk}",
        extra={'token': token, 'version_id': obj.pk}
    )

@receiver(post_version_operation, sender=PostContent)
def log_version_change_complete(sender, obj, operation, token, **kwargs):
    logger.info(
        f"Completed {operation} for {sender.__name__} #{obj.pk}",
        extra={'token': token, 'version_id': obj.pk}
    )

Handling Multiple Versions in One Action

When you publish a version, the old published version(s) are automatically unpublished. This results in multiple signals being sent. The unpublished parameter on the publish signal and to_be_published parameter on the unpublish signal help you understand what’s happening:

from django.dispatch import receiver
from djangocms_versioning import constants
from djangocms_versioning.signals import post_version_operation
from cms.models import PageContent

@receiver(post_version_operation, sender=PageContent)
def handle_publish_scenario(sender, obj, operation,
                            unpublished=None, to_be_published=None, **kwargs):
    """
    Handle different publish/unpublish scenarios
    """

    if operation == constants.OPERATION_PUBLISH:
        if unpublished:
            # This is replacing another published version
            print(f"Replacing {len(unpublished)} versions")
            # Update index: replace old with new
        else:
            # This is the first time this content is published
            print("First publication")
            # Update index: add new entry

    elif operation == constants.OPERATION_UNPUBLISH:
        if to_be_published:
            # Another version will be published as replacement
            print(f"{len(to_be_published)} versions will be published")
            # No action needed, let publish signal handle it
        else:
            # Content is being removed from publication entirely
            print("Content unpublished, no replacement")
            # Update index: remove entry

Listening to CMS Page Signals (DjangoCMS 4.0+)

The CMS used to provide page publish and unpublish signals which were removed in DjangoCMS 4.0. To replicate that behavior, listen to the PageContent model:

from django.dispatch import receiver
from cms.models import PageContent
from djangocms_versioning import constants
from djangocms_versioning.signals import post_version_operation

@receiver(post_version_operation, sender=PageContent)
def on_page_publish_unpublish(sender, obj, operation, **kwargs):
    """React to page publish/unpublish operations"""

    if operation == constants.OPERATION_PUBLISH:
        # Page has been published
        page = obj.content.page
        print(f"Page published: {page.get_title()}")

    elif operation == constants.OPERATION_UNPUBLISH:
        # Page has been unpublished
        page = obj.content.page
        print(f"Page unpublished: {page.get_title()}")

Signal Execution Order

When publishing a version, the following order of events occurs:

  1. pre_version_operation signal (operation=``OPERATION_PUBLISH``)

  2. Old published version transitions to unpublished

  3. post_version_operation signal (operation=``OPERATION_UNPUBLISH``) with to_be_published parameter

  4. New version transitions to published

  5. post_version_operation signal (operation=``OPERATION_PUBLISH``) with unpublished parameter

This order ensures you can handle the transition properly in your signal handlers.

Best Practices

  1. Keep signals fast: Long-running operations in signals can block the UI. Consider using Celery for heavy operations.

  2. Handle errors gracefully: If your signal handler raises an exception, it may break the operation:

    @receiver(post_version_operation, sender=PostContent)
    def my_signal_handler(sender, obj, operation, **kwargs):
        try:
            # Do something risky
            risky_operation()
        except Exception as e:
            logger.error(f"Signal handler failed: {e}", exc_info=True)
            # Don't re-raise; let the version operation complete
    
  3. Use tokens for debugging: The token parameter helps correlate related operations in logs.

  4. Document your assumptions: Make it clear which operations your handler responds to.

  5. Be idempotent: Signal handlers may be called multiple times; design them to be safe when repeated.