Skip to content

Commit 41ac896

Browse files
authored
Support cross-locale slug lookup in language switcher (#6634)
The language switcher previously failed when switching between locales where document slugs differ. For example, when switching from German "add-on-badges-abzeichen" back to English "add-on-badges", users would see a 404 error. This commit enhances the get_visible_document_or_404 function to: - Find correct documents across locales with different slugs - Look up documents by finding related translations in any locale - Maintain backward compatibility with existing behavior Added comprehensive tests to verify the new functionality works with: - Direct locale/slug matches - Cross-locale lookups (any locale to any locale) - Document fallbacks when translations don't exist
1 parent fe4309d commit 41ac896

File tree

2 files changed

+165
-17
lines changed

2 files changed

+165
-17
lines changed

kitsune/wiki/tests/test_utils.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@
2525
generate_short_url,
2626
get_featured_articles,
2727
get_kb_visited,
28+
get_visible_document_or_404,
2829
has_visited_kb,
2930
num_active_contributors,
3031
remove_expired_from_kb_visited,
3132
update_kb_visited,
3233
)
34+
from django.http import Http404
3335

3436

3537
class ActiveContributorsTestCase(TestCase):
@@ -700,3 +702,135 @@ def test_with_product_and_topic_2(self):
700702
},
701703
)
702704
self.assertFalse(self.session.modified)
705+
706+
707+
class GetVisibleDocumentTests(TestCase):
708+
def setUp(self):
709+
self.user = UserFactory()
710+
711+
self.en_doc = ApprovedRevisionFactory(
712+
document__locale="en-US",
713+
document__slug="test-document",
714+
is_ready_for_localization=True,
715+
).document
716+
717+
self.de_rev = TranslatedRevisionFactory(
718+
document__locale="de",
719+
document__slug="test-document-de",
720+
document__parent=self.en_doc,
721+
based_on=self.en_doc.current_revision,
722+
)
723+
self.de_doc = self.de_rev.document
724+
725+
self.fr_rev = TranslatedRevisionFactory(
726+
document__locale="fr",
727+
document__slug="test-document-fr",
728+
document__parent=self.en_doc,
729+
based_on=self.en_doc.current_revision,
730+
)
731+
self.fr_doc = self.fr_rev.document
732+
733+
def test_get_document_by_locale_and_slug_direct_match(self):
734+
"""Test getting a document when there's a direct match for locale and slug."""
735+
doc = get_visible_document_or_404(self.user, locale="en-US", slug="test-document")
736+
self.assertEqual(doc, self.en_doc)
737+
738+
doc = get_visible_document_or_404(self.user, locale="de", slug="test-document-de")
739+
self.assertEqual(doc, self.de_doc)
740+
741+
doc = get_visible_document_or_404(self.user, locale="fr", slug="test-document-fr")
742+
self.assertEqual(doc, self.fr_doc)
743+
744+
def test_get_document_via_translation_for_nondefault_locale(self):
745+
"""Test getting a document via its parent when no direct match in the requested locale."""
746+
with self.assertRaises(Http404):
747+
# This should raise a 404 because we're explicitly
748+
# setting look_for_translation_via_parent=False
749+
get_visible_document_or_404(
750+
self.user,
751+
locale="en-US",
752+
slug="test-document-de",
753+
look_for_translation_via_parent=False,
754+
)
755+
756+
# This should also raise a 404 because cross-locale lookups should only work when
757+
# look_for_translation_via_parent is True
758+
with self.assertRaises(Http404):
759+
get_visible_document_or_404(self.user, locale="en-US", slug="test-document-de")
760+
761+
def test_cross_locale_lookup(self):
762+
"""Test the new cross-locale lookup functionality for language switcher."""
763+
doc = get_visible_document_or_404(
764+
self.user,
765+
locale="en-US",
766+
slug="test-document-de",
767+
look_for_translation_via_parent=True,
768+
)
769+
self.assertEqual(doc, self.en_doc)
770+
771+
doc = get_visible_document_or_404(
772+
self.user,
773+
locale="en-US",
774+
slug="test-document-fr",
775+
look_for_translation_via_parent=True,
776+
)
777+
self.assertEqual(doc, self.en_doc)
778+
779+
doc = get_visible_document_or_404(
780+
self.user, locale="de", slug="test-document", look_for_translation_via_parent=True
781+
)
782+
self.assertEqual(doc, self.de_doc)
783+
784+
doc = get_visible_document_or_404(
785+
self.user, locale="de", slug="test-document-fr", look_for_translation_via_parent=True
786+
)
787+
self.assertEqual(doc, self.de_doc)
788+
789+
def test_fallback_to_parent(self):
790+
"""Test falling back to parent document when translation doesn't exist."""
791+
no_es_doc = ApprovedRevisionFactory(
792+
document__locale="en-US", document__slug="no-spanish"
793+
).document
794+
795+
with self.assertRaises(Http404):
796+
get_visible_document_or_404(
797+
self.user, locale="es", slug="no-spanish", look_for_translation_via_parent=True
798+
)
799+
800+
doc = get_visible_document_or_404(
801+
self.user,
802+
locale="es",
803+
slug="no-spanish",
804+
look_for_translation_via_parent=True,
805+
return_parent_if_no_translation=True,
806+
)
807+
self.assertEqual(doc, no_es_doc)
808+
809+
def test_nonexistent_document(self):
810+
"""Test that we get a 404 for documents that don't exist in any locale."""
811+
with self.assertRaises(Http404):
812+
get_visible_document_or_404(
813+
self.user,
814+
locale="en-US",
815+
slug="doesnt-exist",
816+
look_for_translation_via_parent=True,
817+
)
818+
819+
with self.assertRaises(Http404):
820+
get_visible_document_or_404(
821+
self.user, locale="de", slug="doesnt-exist", look_for_translation_via_parent=True
822+
)
823+
824+
def test_unapproved_revision(self):
825+
"""Test that users can't see documents without approved revisions."""
826+
unapproved_doc = RevisionFactory(
827+
document__locale="en-US", document__slug="unapproved", is_approved=False
828+
).document
829+
830+
random_user = UserFactory()
831+
with self.assertRaises(Http404):
832+
get_visible_document_or_404(random_user, locale="en-US", slug="unapproved")
833+
834+
creator = unapproved_doc.revisions.first().creator
835+
doc = get_visible_document_or_404(creator, locale="en-US", slug="unapproved")
836+
self.assertEqual(doc, unapproved_doc)

kitsune/wiki/utils.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -177,34 +177,48 @@ def get_visible_document_or_404(
177177
"""
178178
Get the document specified by the keyword arguments and visible to the given user, or 404.
179179
"""
180+
180181
try:
181182
return Document.objects.get_visible(user, **kwargs)
182183
except Document.DoesNotExist:
183184
pass
184185

185-
if (
186-
not look_for_translation_via_parent
187-
or not (locale := kwargs.get("locale"))
188-
or (locale == settings.WIKI_DEFAULT_LANGUAGE)
189-
):
190-
# We either don't want to try to find the translation via its parent, or it doesn't
191-
# make sense, because we're not making a locale-specific request or the locale we're
192-
# requesting is already the default locale.
186+
if not (locale := kwargs.get("locale")):
193187
raise Http404
194188

195-
# We couldn't find a visible translation in the requested non-default locale, so let's
196-
# see if we can find a visible translation via its parent.
197-
kwargs.update(locale=settings.WIKI_DEFAULT_LANGUAGE)
198-
parent = get_object_or_404(Document.objects.visible(user, **kwargs))
189+
slug = kwargs.get("slug")
199190

200-
# If there's a visible translation of the parent for the requested locale, return it.
201-
if translation := parent.translated_to(locale, visible_for_user=user):
202-
return translation
191+
if not (slug and look_for_translation_via_parent):
192+
raise Http404
193+
194+
other_docs_qs = (
195+
Document.objects.filter(slug=slug).exclude(locale=locale).select_related("parent")
196+
)
203197

204-
# Otherwise, we're left with the parent.
198+
for doc in other_docs_qs:
199+
base_doc = doc.parent or doc
200+
if base_doc.locale == settings.WIKI_DEFAULT_LANGUAGE:
201+
if base_doc.locale == locale:
202+
return base_doc
203+
translation = base_doc.translated_to(locale, visible_for_user=user)
204+
if translation:
205+
return translation
206+
else:
207+
translation = base_doc.translated_to(locale, visible_for_user=user)
208+
if translation:
209+
return translation
210+
211+
# Don't try final fallback if not looking for translations or in default language
212+
if not look_for_translation_via_parent or locale == settings.WIKI_DEFAULT_LANGUAGE:
213+
raise Http404
214+
215+
kwargs["locale"] = settings.WIKI_DEFAULT_LANGUAGE
216+
parent = get_object_or_404(Document.objects.visible(user, **kwargs))
217+
translation = parent.translated_to(locale, visible_for_user=user)
218+
if translation:
219+
return translation
205220
if return_parent_if_no_translation:
206221
return parent
207-
208222
raise Http404
209223

210224

0 commit comments

Comments
 (0)