Skip to content

feat: Support djangocms-versioning contract#336

Open
fsbraun wants to merge 9 commits intomasterfrom
feat/contract
Open

feat: Support djangocms-versioning contract#336
fsbraun wants to merge 9 commits intomasterfrom
feat/contract

Conversation

@fsbraun
Copy link
Member

@fsbraun fsbraun commented Feb 4, 2026

Description

Add support for the djangocms-versioning contract and make alias versioning integration more robust and configuration-driven.

New Features:

  • Support retrieving djangocms-versioning configuration via the CMS extension contract on app configs.

Bug Fixes:

  • Prevent errors when djangocms-versioning or its configuration is absent by guarding version access and imports in alias models.

Enhancements:

  • Initialize alias versioning and moderation settings at app config construction time using discovered versioning items.
  • Update alias content name resolution and content caching to better handle missing content and optional versioning integration.
  • Simplify versioning detection by iterating installed app configs instead of relying on a specific app lookup.

Related resources

  • #...
  • #...

Checklist

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 4, 2026

Reviewer's Guide

Refactors djangocms_alias to use the djangocms-versioning contract API and to be more defensive when versioning is absent, including wiring versioning configuration via the CMS app config constructor, introducing a contract-based helper to obtain the VersionableItem, relaxing the versioning-enabled detection, and making Alias name/content resolution robust to missing content or versioning imports.

Sequence diagram for djangocms_alias versioning contract resolution

sequenceDiagram
    actor DjangoAppRegistry
    participant AliasCMSConfig
    participant Utils
    participant VersioningContract as VersioningContractProvider
    participant VersioningLib as DjangocmsVersioning

    DjangoAppRegistry->>AliasCMSConfig: __init__(app_config)
    activate AliasCMSConfig
    AliasCMSConfig->>Utils: get_versionable_item(cms_config)
    activate Utils
    alt cms_config provides contract API
        Utils->>AliasCMSConfig: has attribute get_contract
        Utils->>VersioningContract: get_contract(djangocms_versioning)
        VersioningContract-->>Utils: VersionableItem or null
    else no contract API
        Utils-->>AliasCMSConfig: None
    end
    Utils-->>AliasCMSConfig: VersionableItem or None
    deactivate Utils

    AliasCMSConfig->>AliasCMSConfig: set djangocms_moderation_enabled
    AliasCMSConfig->>AliasCMSConfig: set djangocms_versioning_enabled

    alt djangocms_versioning_enabled and VersionableItem is null
        AliasCMSConfig->>VersioningLib: import __version__, VersionableItem
        VersioningLib-->>AliasCMSConfig: __version__, VersionableItem
        AliasCMSConfig->>AliasCMSConfig: validate djangocms_versioning_version >= 2.4
    end

    alt versioning is available
        AliasCMSConfig->>AliasCMSConfig: build VersionableItem config for AliasContent
        AliasCMSConfig->>AliasCMSConfig: assign versioning list
    end

    deactivate AliasCMSConfig
Loading

Class diagram for updated djangocms_alias configuration and models

classDiagram
    class CMSAppConfig {
    }

    class AliasCMSConfig {
        +bool cms_enabled
        +list moderated_models
        +list cms_wizards
        +bool djangocms_moderation_enabled
        +bool djangocms_versioning_enabled
        +list versioning
        +__init__(app_config)
    }

    class Alias {
        +dict _content_cache
        +get_name(language)
        +get_content(language, show_draft_content)
        +get_placeholder(language, show_draft_content)
    }

    class AliasContent {
    }

    class VersionableItem {
        +model content_model
        +str grouper_field_name
        +list extra_grouping_fields
        +dict version_list_filter_lookups
        +callable copy_function
        +callable grouper_selector_option_label
        +str grouper_admin_mixin
    }

    class Utils {
        +get_versionable_item(cms_config)
        +is_versioning_enabled()
        +emit_content_change(objs, sender)
    }

    CMSAppConfig <|-- AliasCMSConfig
    AliasCMSConfig ..> Utils : uses
    AliasCMSConfig ..> VersionableItem : configures
    VersionableItem ..> AliasContent : versions
    Alias o--> AliasContent : contents
    Alias ..> Utils : uses (indirectly via versioning)
Loading

File-Level Changes

Change Details Files
Wire djangocms-versioning configuration through AliasCMSConfig using the contract-based VersionableItem instead of hard-coded settings and module checks.
  • Remove global djangocms_versioning_installed flag and class-level enablement attributes.
  • Add AliasCMSConfig.init to resolve VersionableItem via a new get_versionable_item helper and compute moderation/versioning enabled flags from settings and contract presence.
  • Gate versioning setup inside init and only perform djangocms_versioning version checks when VersionableItem is not provided via contract.
  • Move versioning configuration list to an instance attribute self.versioning built from VersionableItem and existing AliasContent metadata.
djangocms_alias/cms_config.py
Introduce contract-aware utilities for versioning and simplify feature detection across installed apps.
  • Add cached get_versionable_item(cms_config) helper that uses a get_contract("djangocms_versioning") hook when present.
  • Rewrite is_versioning_enabled to scan app configs and use cms_extension.is_content_model_versioned on the first compatible app, falling back cleanly when no extension is found.
  • Decorate helpers with functools.cache to avoid repeated contract and app lookup work.
djangocms_alias/utils.py
Make Alias model name and content access tolerant of missing versioning and missing content while preserving draft/published labelling.
  • Remove hard dependency on is_versioning_enabled in Alias.get_name and instead try-import djangocms_versioning constants, handling ImportError/AttributeError gracefully.
  • Ensure Alias.get_name falls back to using any cached content if language-specific content is missing before constructing the display name.
  • Change get_content to populate the content cache for all languages from the queryset and then look up the requested language, instead of filtering per-language and caching only that entry.
djangocms_alias/models.py
Drop unused module-level reference to versioning enablement in admin filters.
  • Remove djangocms_versioning_enabled alias from filters module as it is no longer required.
djangocms_alias/filters.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • The new AliasCMSConfig.__init__ overrides the base implementation but never calls super().__init__(app_config), which risks leaving the underlying CMSAppConfig uninitialized and may break other cms app config features.
  • get_versionable_item is decorated with @cache but takes a cms_config instance argument, which is likely unhashable and will cause a TypeError; consider removing the cache or changing the API to use a hashable key (e.g. app label) instead.
  • The revised is_versioning_enabled now iterates all app configs, returns on the first with cms_extension, and includes a print(app_config) debug statement; this changes semantics from checking the djangocms_versioning app specifically and may give incorrect results and noisy output—restrict the lookup to the versioning app and remove the print.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `AliasCMSConfig.__init__` overrides the base implementation but never calls `super().__init__(app_config)`, which risks leaving the underlying `CMSAppConfig` uninitialized and may break other cms app config features.
- `get_versionable_item` is decorated with `@cache` but takes a `cms_config` instance argument, which is likely unhashable and will cause a `TypeError`; consider removing the cache or changing the API to use a hashable key (e.g. app label) instead.
- The revised `is_versioning_enabled` now iterates all app configs, returns on the first with `cms_extension`, and includes a `print(app_config)` debug statement; this changes semantics from checking the `djangocms_versioning` app specifically and may give incorrect results and noisy output—restrict the lookup to the versioning app and remove the print.

## Individual Comments

### Comment 1
<location> `djangocms_alias/cms_config.py:25-34` </location>
<code_context>
+    def __init__(self, app_config):
</code_context>

<issue_to_address>
**issue (bug_risk):** Using an overridden __init__ and instance attributes for CMS config is likely to break Django CMS/CMSAppConfig integration.

Django CMS expects app config options to be defined as class attributes and introspected without instantiating the class. A custom `__init__` that doesn’t match the standard `__init__(self, *args, **kwargs)` and doesn’t call `super().__init__` can break its initialization flow, and using `self.versioning` instead of a class-level `versioning` means the versioning extension may never see the config. Please move `djangocms_moderation_enabled`, `djangocms_versioning_enabled`, and `versioning` back to class attributes (optionally computed via helpers), or at minimum restore the standard `__init__` signature and call `super().__init__`.
</issue_to_address>

### Comment 2
<location> `djangocms_alias/cms_config.py:26-35` </location>
<code_context>
+        VersionableItem = get_versionable_item(self)
</code_context>

<issue_to_address>
**suggestion:** The `VersionableItem` name is reused for two different concepts, which makes the control flow hard to reason about and easy to break.

Here `VersionableItem` is first bound to the result of `get_versionable_item(self)` (an instance or `None`), and later reused for the imported `VersionableItem` class. This name shadowing is brittle (e.g. if a false-y but non-`None` value is ever returned) and hard to follow. Use distinct names (e.g. `versionable_item = get_versionable_item(self)`) and import the `VersionableItem` class under its own name if needed, without relying on truthiness for control flow.

Suggested implementation:

```python
    def __init__(self, app_config):
        versionable_item = get_versionable_item(self)
        self.djangocms_moderation_enabled = getattr(settings, "MODERATING_ALIAS_MODELS_ENABLED", True)
        self.djangocms_versioning_enabled = getattr(
            settings, "VERSIONING_ALIAS_MODELS_ENABLED", versionable_item is not None
        )

        if self.djangocms_versioning_enabled:

```

1. Ensure that elsewhere in this file, the `VersionableItem` class is imported and referenced only as a class (e.g. from `djangocms_versioning.helpers import VersionableItem`), and not reused as a local variable name.
2. If there are later references in this method that were relying on the local `VersionableItem` binding (e.g. `if VersionableItem:` or passing it around), update them to use `versionable_item` instead, and keep the `VersionableItem` name reserved for the class only.
</issue_to_address>

### Comment 3
<location> `djangocms_alias/utils.py:6-9` </location>
<code_context>


-def is_versioning_enabled():
+@cache
+def get_versionable_item(cms_config) -> type | None:
+    if hasattr(cms_config, "get_contract"):
+        return cms_config.get_contract("djangocms_versioning")
+    return None
+
+
+@cache
+def is_versioning_enabled() -> bool:
     from .models import AliasContent

-    try:
-        app_config = apps.get_app_config("djangocms_versioning")
-        return app_config.cms_extension.is_content_model_versioned(AliasContent)
-    except LookupError:
-        return False
</code_context>

<issue_to_address>
**issue (bug_risk):** New `is_versioning_enabled` logic can return the wrong result and includes a stray `print`.

The loop will return on the first `app_config` with a `cms_extension`, even if it’s not `djangocms_versioning`, so the result can be wrong when another CMS app is encountered first. `print(app_config)` will also pollute stdout in production. Please either restore the explicit `apps.get_app_config("djangocms_versioning")` logic or ensure you filter by `app_config.label`/`name` before calling `cms_extension.is_content_model_versioned`, and remove the `print`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant