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:
Parameter |
Description |
Type |
|---|---|---|
|
The content model class (e.g., PostContent, PageContent) |
Model class |
|
The Version instance being operated on |
Version |
|
The type of operation being performed |
str (see Operations below) |
|
A unique token to tie pre and post signals together |
str (UUID) |
|
(For publish operations) List of versions that will be unpublished |
list of Version objects |
|
(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):
Operation Constant |
Description |
|---|---|
|
A new draft version has been created (or version moved to draft state) |
|
A draft version has been published |
|
A published version has been unpublished |
|
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:
pre_version_operationsignal (operation=``OPERATION_PUBLISH``)Old published version transitions to unpublished
post_version_operationsignal (operation=``OPERATION_UNPUBLISH``) withto_be_publishedparameterNew version transitions to published
post_version_operationsignal (operation=``OPERATION_PUBLISH``) withunpublishedparameter
This order ensures you can handle the transition properly in your signal handlers.
Best Practices¶
Keep signals fast: Long-running operations in signals can block the UI. Consider using Celery for heavy operations.
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
Use tokens for debugging: The token parameter helps correlate related operations in logs.
Document your assumptions: Make it clear which operations your handler responds to.
Be idempotent: Signal handlers may be called multiple times; design them to be safe when repeated.