Skip to content

fix: Show fallback languages if changelist view#334

Open
fsbraun wants to merge 4 commits intomasterfrom
fix/no-content
Open

fix: Show fallback languages if changelist view#334
fsbraun wants to merge 4 commits intomasterfrom
fix/no-content

Conversation

@fsbraun
Copy link
Member

@fsbraun fsbraun commented Feb 3, 2026

Description

Improve alias content resolution and naming when a requested language or versioning integration is unavailable.

Bug Fixes:

  • Return alias content using available languages as a fallback when the requested language has no content in the changelist view.
  • Avoid errors in alias naming when djangocms-versioning is not installed or content has no associated version.

Enhancements:

image

Related resources

Checklist

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 3, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Adjust alias content resolution to better handle missing languages in the changelist view and make alias naming resilient when versioning is absent or content lacks versions, while caching contents per language for multi-language lookups.

Class diagram for updated Alias content and naming logic

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

    class AliasContent {
        +int id
        +string language
        +string name
        +versions
    }

    class Version {
        +int id
        +string state
    }

    class DjangocmsVersioningConstants {
        <<module>>
        +string DRAFT
    }

    Alias "1" o-- "*" AliasContent : contents
    AliasContent "1" o-- "*" Version : versions

    Alias ..> DjangocmsVersioningConstants : uses

    %% Method behavior notes encoded via additional pseudo methods
    Alias : get_name(language)
    Alias :   1. content = get_content(language, show_draft_content=True)
    Alias :   2. if no content use first _content_cache value as fallback
    Alias :   3. name = content.name or Alias_pk_No_content
    Alias :   4. try import DRAFT from djangocms_versioning.constants
    Alias :   5. if version exists and version.state == DRAFT
    Alias :        return name_Not_published
    Alias :   6. on ImportError ModuleNotFoundError AttributeError ignore
    Alias :   7. return name

    Alias : get_content(language, show_draft_content)
    Alias :   A. if language in _content_cache return cached
    Alias :   B. else build qs via admin_manager.latest_content or all
    Alias :   C. iterate qs and cache first content per language
    Alias :   D. return _content_cache.get(language)
Loading

File-Level Changes

Change Details Files
Improve alias name resolution when requested language content or versioning metadata is unavailable.
  • Update get_name to fall back to the first available cached content if no content is found for the requested language.
  • Change versioning integration to a guarded import inside get_name, catching ImportError, ModuleNotFoundError, and AttributeError so alias naming works even when djangocms-versioning is not installed or content lacks a version.
  • Preserve existing naming convention for draft versions by still appending '(Not published)' when a draft version is detected.
djangocms_alias/models.py
Cache alias contents per language and enable language fallback in get_content.
  • Replace language-specific filtering followed by a single .first() with an iteration over the full queryset to populate the internal _content_cache keyed by content.language.
  • Return content for the requested language from the populated cache, allowing reuse across calls and supporting future fallback logic.
  • Ensure that get_placeholder indirectly benefits from the updated get_content behaviour, enabling placeholder retrieval when the exact language content is missing but other languages are available.
djangocms_alias/models.py

Assessment against linked issues

Issue Objective Addressed Explanation
#331 Ensure aliases in the alias list display a meaningful name instead of "missing language" when the requested language has no content, ideally reusing the detail-page naming (e.g., "Alias X (No content)") or falling back to another language.
#331 Improve alias content resolution so that alias naming is robust across languages and does not break when versioning integration (djangocms-versioning) is unavailable.

Possibly linked issues

  • #unknown: They both address alias naming and behavior when language-specific content is missing, reducing “missing language” confusion.

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

if version.state == DRAFT:
return f"{name} (Not published)"

except (ImportError, ModuleNotFoundError, AttributeError):

Check notice

Code scanning / CodeQL

Empty except Note

'except' clause does nothing but pass and there is no explanatory comment.

Copilot Autofix

AI about 24 hours ago

In general, empty except blocks should be replaced with explicit, documented handling: narrow the exceptions where possible, add comments explaining why they are safely ignored, and/or log unexpected issues. Here, the goal is to annotate alias names as “(Not published)” when draft versioning is available, and otherwise silently fall back to the plain name. We can preserve that behavior while removing the “empty except” smell by (1) guarding against content being None before entering the try, and (2) adding a short explanatory comment in the except block to clarify that missing versioning or versions is expected and intentionally ignored.

Concretely, in Alias.get_name in djangocms_alias/models.py, we will:

  • Add an early if content is None: return name to avoid AttributeError from content.versions.
  • Keep the try importing DRAFT and accessing content.versions.first().
  • Keep catching the same exceptions, but add a brief comment explaining that the project may not have versioning installed or the content may not be versioned, and that in those cases we intentionally fall back to the base name.
    This removes the “empty except” issue while keeping the same observable behavior (plain name when versioning is unavailable or when any of the anticipated exceptions is raised).

No new imports or helper methods are required.


Suggested changeset 1
djangocms_alias/models.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/djangocms_alias/models.py b/djangocms_alias/models.py
--- a/djangocms_alias/models.py
+++ b/djangocms_alias/models.py
@@ -160,15 +160,18 @@
         if not content:
             content = next(iter(self._content_cache.values()), None)
         name = getattr(content, "name", f"Alias {self.pk} (No content)")
+        if content is None:
+            return name
         try:
             from djangocms_versioning.constants import DRAFT
 
             version = content.versions.first()
 
-            if version.state == DRAFT:
+            if version and version.state == DRAFT:
                 return f"{name} (Not published)"
         except (ImportError, ModuleNotFoundError, AttributeError):
-            pass
+            # Versioning is not installed or content is not versioned; fall back to the base name.
+            return name
         return name
 
     def get_content(self, language=None, show_draft_content=False):
EOF
@@ -160,15 +160,18 @@
if not content:
content = next(iter(self._content_cache.values()), None)
name = getattr(content, "name", f"Alias {self.pk} (No content)")
if content is None:
return name
try:
from djangocms_versioning.constants import DRAFT

version = content.versions.first()

if version.state == DRAFT:
if version and version.state == DRAFT:
return f"{name} (Not published)"
except (ImportError, ModuleNotFoundError, AttributeError):
pass
# Versioning is not installed or content is not versioned; fall back to the base name.
return name
return name

def get_content(self, language=None, show_draft_content=False):
Copilot is powered by AI and may make mistakes. Always verify output.
@fsbraun fsbraun committed this autofix suggestion about 24 hours ago.
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 1 issue, and left some high level feedback:

  • The new get_content implementation populates _content_cache without considering the show_draft_content flag, so once the cache is filled by a call with one flag value it will be reused for subsequent calls with the other flag value, which may return inconsistent draft/published content; consider scoping the cache by show_draft_content or avoiding cross-flag reuse.
  • When populating _content_cache in get_content, using setdefault on the unfiltered queryset relies on its implicit ordering to decide which content per language is cached; to make the behavior deterministic and closer to the previous .first() semantics, it would be safer to add an explicit ordering (e.g. by -pk or -created) or to reuse latest_content() for both branches.
  • In get_name, falling back to next(iter(self._content_cache.values()), None) may pick an arbitrary language rather than a predictable one; if possible, prefer a specific fallback strategy (e.g. the current active language, default language, or a defined priority list) to avoid surprising alias names in multi-language setups.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `get_content` implementation populates `_content_cache` without considering the `show_draft_content` flag, so once the cache is filled by a call with one flag value it will be reused for subsequent calls with the other flag value, which may return inconsistent draft/published content; consider scoping the cache by `show_draft_content` or avoiding cross-flag reuse.
- When populating `_content_cache` in `get_content`, using `setdefault` on the unfiltered queryset relies on its implicit ordering to decide which content per language is cached; to make the behavior deterministic and closer to the previous `.first()` semantics, it would be safer to add an explicit ordering (e.g. by `-pk` or `-created`) or to reuse `latest_content()` for both branches.
- In `get_name`, falling back to `next(iter(self._content_cache.values()), None)` may pick an arbitrary language rather than a predictable one; if possible, prefer a specific fallback strategy (e.g. the current active language, default language, or a defined priority list) to avoid surprising alias names in multi-language setups.

## Individual Comments

### Comment 1
<location> `djangocms_alias/models.py:174` </location>
<code_context>
-
-            self._content_cache[language] = qs.first()
-            return self._content_cache[language]
+            for content in qs:
+                self._content_cache.setdefault(content.language, content)
+            return self._content_cache.get(language)

     def get_placeholder(self, language=None, show_draft_content=False):
</code_context>

<issue_to_address>
**suggestion (performance):** Missing negative caching for languages with no content can cause repeated DB hits.

With the new loop, when `qs` has no items for a `language`, nothing is stored in `_content_cache` for that key, so every `get_content(language)` call re-queries the DB. To preserve negative caching, also cache the no-content case, e.g. by setting `self._content_cache.setdefault(language, None)` when `language` is not present in `qs`.

```suggestion
    def get_content(self, language=None, show_draft_content=False):
                qs = self.contents(manager="admin_manager").latest_content()
            else:
                qs = self.contents.all()
            for content in qs:
                self._content_cache.setdefault(content.language, content)
            if language not in self._content_cache:
                # Negative cache: remember that this language has no content
                self._content_cache[language] = None
            return self._content_cache.get(language)
```
</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.

bug: aliases show "missing language"

1 participant