Working with versioned Pages

When djangocms-versioning is installed, django CMS pages become versioned. While this does not change how you interact with Page and most importantly PageContent objects in your code, it is important to understand how djangocms-versioning changes the result of querying PageContent objects.

Understanding the Page/PageContent relationship

Django CMS separates page structure from page content:

Page

The grouper model representing the page in the site tree. It holds non-versioned data like the page’s position in the navigation hierarchy.

PageContent

The content model holding the versioned content for a specific language: title, slug, template, meta description, and placeholders with plugins.

A single Page can have multiple PageContent objects — one per language, and potentially multiple versions per language (draft, published, archived, etc.).

Querying PageContent objects

Published content only (default)

The default objects manager only returns published content:

from cms.models import PageContent

# Get all published English page contents
PageContent.objects.filter(language="en")

# Get published content for a specific page
PageContent.objects.filter(page=my_page, language="en")

# Get published content for a ``Page`` object
page.get_content_obj("en")  # caching avoids db hit

This is the safe default for public-facing code —- draft and unpublished content is never accidentally exposed.

All versions (admin contexts only)

Use admin_manager when you need access to all versions. Only use this in admin views, not in public-facing code:

from cms.models import PageContent

# Get all page contents regardless of version state
PageContent.admin_manager.filter(page=my_page, language="en")

Filtering by version state

Note

Since version states are specific to djangocms-versioning, this code ties directly to the djangocms-versioning implementation and will not work with other versioning solutions.

To find content in a specific state:

from cms.models import PageContent
from djangocms_versioning.constants import DRAFT, PUBLISHED, UNPUBLISHED, ARCHIVED

# Get draft content for a page
PageContent.admin_manager.filter(
    page=my_page,
    language="en",
    versions__state=DRAFT
)

# Get all unpublished versions
PageContent.admin_manager.filter(versions__state=UNPUBLISHED)

Current content (draft or published)

Often you need the “current” version —- the draft if one exists, otherwise the published version. Use current_content():

from cms.models import PageContent

# Get current content for all languages of a page
for content in PageContent.admin_manager.filter(page=my_page).current_content():
    print(f"{content.language}: {content.title}")

# Get current English content
current = PageContent.admin_manager.filter(
    page=my_page,
    language="en"
).current_content().first()

Working with the Version model

Note

Since the Version model is specific to djangocms-versioning, this code ties directly to the djangocms-versioning implementation and will not work with other versioning solutions.

Each PageContent has an associated Version object that tracks its state:

from djangocms_versioning.models import Version

# Get the version for a content object
version = Version.objects.get_for_content(page_content)
print(version.state)  # 'draft', 'published', etc.
print(version.created_by)  # User who created this version
print(version.modified)  # Last modification timestamp

# Get all versions for a page/language combination
versions = Version.objects.filter_by_content_grouping_values(page_content)
for v in versions.order_by("-pk"):
    print(f"Version {v.number}: {v.state}")

Creating new page content versions

When creating content programmatically, use with_user() to track authorship:

from cms.models import Page, PageContent

# Create a new page (grouper)
page = Page.objects.create(node=parent_node)

# Create versioned content - this also creates a Version object
content = PageContent.objects.with_user(request.user).create(
    page=page,
    language="en",
    title="My New Page",
    slug="my-new-page",
    template="base.html",
)

The new content will be in draft state. To publish it:

from djangocms_versioning.models import Version

version = Version.objects.get_for_content(content)
version.publish(request.user)

Common patterns

Check if a page has unpublished changes

from cms.models import PageContent
from djangocms_versioning.constants import DRAFT, PUBLISHED

def has_unpublished_changes(page, language):
    """Returns True if page has a draft that differs from published."""
    contents = PageContent.admin_manager.filter(
        page=page,
        language=language
    ).current_content()
    return contents and contents.versions.first().state == DRAFT

Get the published version of a draft

Note

Since the Version model is specific to djangocms-versioning, this code ties directly to the djangocms-versioning implementation and will not work with other versioning solutions.

from djangocms_versioning.constants import PUBLISHED
from djangocms_versioning.models import Version

def get_published_sibling(draft_content):
    """Given a draft PageContent, find its published counterpart."""
    version = Version.objects.filter(
        content_type=ContentType.objects.get_for_model(draft_content),
        object_id__in=PageContent.admin_manager.filter(
            page=draft_content.page,
            language=draft_content.language
        ).values_list("pk", flat=True),
        state=PUBLISHED
    ).first()
    return version.content if version or None

Iterate over all pages with their current content

Remember to use the correct manager when using, e.g., prefetch_related or reverse relations.

from django.db.models import Prefetch

from cms.models import Page, PageContent

# Unoptimized with N + 1 fetches
# Manager needs to be specified in the reverse relation
for page in Page.objects.all():
    for content in page.pagecontent_set(manager="admin_manager").all():
        print(f"{page.pk}: {content.title} ({content.language})")

# Optimized with 2 fetches
# Manager needs to be specified in the Prefetch object
for page in Page.objects.prefetch_related(Prefetch("pagecontent_set", queryset=PageContent.admin_manager.all())).all():
    for content in page.pagecontent_set(manager="admin_manager").all():
        print(f"{page.pk}: {content.title} ({content.language})")