Skip to content

Commit 1226cb2

Browse files
feat: get translated title for content highlights
1 parent 54b9690 commit 1226cb2

File tree

6 files changed

+434
-2
lines changed

6 files changed

+434
-2
lines changed

enterprise_catalog/apps/api/v1/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ class SegmentEvents:
1010
AI_CURATIONS_TASK_COMPLETED = 'edx.server.enterprise-catalog.ai-curations.task.completed'
1111
AI_CURATIONS_RESULTS_FOUND = 'edx.server.enterprise-catalog.ai-curations.results-found'
1212
AI_CURATIONS_RESULTS_NOT_FOUND = 'edx.server.enterprise-catalog.ai-curations.results-not-found'
13+
14+
15+
DEFAULT_TRANSLATION_LANGUAGE = 'en'
16+
AVAILABLE_TRANSLATION_LANGUAGES = ['es']

enterprise_catalog/apps/api/v1/serializers.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22
from re import findall, search
33

44
from django.db import IntegrityError, models
5+
from django.db.models import Prefetch
56
from rest_framework import serializers, status
67

78
from enterprise_catalog.apps.academy.models import Academy, Tag
9+
from enterprise_catalog.apps.api.v1.constants import (
10+
AVAILABLE_TRANSLATION_LANGUAGES,
11+
DEFAULT_TRANSLATION_LANGUAGE,
12+
)
813
from enterprise_catalog.apps.api.v1.utils import (
914
get_archived_content_count,
1015
get_enterprise_utm_context,
@@ -23,6 +28,7 @@
2328
from enterprise_catalog.apps.catalog.models import (
2429
CatalogQuery,
2530
ContentMetadata,
31+
ContentTranslation,
2632
EnterpriseCatalog,
2733
)
2834
from enterprise_catalog.apps.catalog.utils import get_content_filter_hash
@@ -360,6 +366,7 @@ class HighlightedContentSerializer(serializers.ModelSerializer):
360366
Serializer for the `HighlightedContent` model.
361367
"""
362368
aggregation_key = serializers.SerializerMethodField()
369+
title = serializers.SerializerMethodField()
363370

364371
class Meta:
365372
model = HighlightedContent
@@ -382,6 +389,22 @@ def get_aggregation_key(self, obj):
382389
"""
383390
return obj.aggregation_key
384391

392+
def get_title(self, obj):
393+
"""
394+
Returns the title, preferring translation if language is supported and available.
395+
"""
396+
lang = self.context.get('lang')
397+
title = obj.title
398+
399+
if not lang or lang == DEFAULT_TRANSLATION_LANGUAGE or lang not in AVAILABLE_TRANSLATION_LANGUAGES:
400+
return title
401+
402+
translations = obj.content_metadata.translations.all()
403+
if translations and translations[0].title:
404+
return translations[0].title
405+
406+
return title
407+
385408

386409
class HighlightSetSerializer(serializers.ModelSerializer):
387410
"""
@@ -406,8 +429,22 @@ def get_highlighted_content(self, obj):
406429
"""
407430
Returns the data for the associated content included in this HighlightSet object.
408431
"""
432+
lang = self.context.get('lang')
433+
context = dict(self.context)
434+
409435
qs = obj.highlighted_content.order_by('created').select_related('content_metadata')
410-
return HighlightedContentSerializer(qs, many=True).data
436+
437+
if lang in AVAILABLE_TRANSLATION_LANGUAGES and lang != DEFAULT_TRANSLATION_LANGUAGE:
438+
# Only prefetches translations if a supported non-English language is requested.
439+
qs = qs.prefetch_related(
440+
Prefetch(
441+
'content_metadata__translations',
442+
queryset=ContentTranslation.objects.filter(language_code=lang)
443+
)
444+
)
445+
context['lang'] = lang
446+
447+
return HighlightedContentSerializer(qs, many=True, context=context).data
411448

412449

413450
class EnterpriseCurationConfigSerializer(serializers.ModelSerializer):

enterprise_catalog/apps/api/v1/tests/test_curation_views.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from enterprise_catalog.apps.catalog.constants import COURSE, PROGRAM
2424
from enterprise_catalog.apps.catalog.tests.factories import (
2525
ContentMetadataFactory,
26+
ContentTranslationFactory,
2627
)
2728
from enterprise_catalog.apps.curation.tests.factories import (
2829
EnterpriseCurationConfigFactory,
@@ -526,6 +527,171 @@ def test_delete_not_allowed(self, is_catalog_staff, is_role_assigned_via_jwt):
526527
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
527528

528529

530+
@ddt.ddt
531+
class HighlightSetMultilingualTests(CurationAPITestBase):
532+
"""
533+
Test multilingual support in HighlightSetReadOnlyViewSet.
534+
"""
535+
def setUp(self):
536+
super().setUp()
537+
538+
# Create Spanish translations for the first three content items
539+
self.spanish_titles = [
540+
'Título en Español 1',
541+
'Título en Español 2',
542+
'Título en Español 3',
543+
]
544+
self.translations = []
545+
for idx, content_metadata in enumerate(self.highlighted_content_metadata_one[:3]):
546+
translation = ContentTranslationFactory(
547+
content_metadata=content_metadata,
548+
language_code='es',
549+
title=self.spanish_titles[idx]
550+
)
551+
self.translations.append(translation)
552+
553+
def test_list_with_spanish_language_parameter(self):
554+
"""
555+
Test that requesting highlight sets with lang=es returns Spanish translations.
556+
"""
557+
url = reverse('api:v1:highlight-sets-list') + f'?enterprise_customer={self.enterprise_uuid}&lang=es'
558+
self.set_up_catalog_learner()
559+
560+
response = self.client.get(url)
561+
assert response.status_code == status.HTTP_200_OK
562+
563+
highlight_sets_results = response.json()['results']
564+
assert len(highlight_sets_results) == 1
565+
566+
highlighted_content = highlight_sets_results[0]['highlighted_content']
567+
# First three should have Spanish titles
568+
for idx in range(3):
569+
assert highlighted_content[idx]['title'] == self.spanish_titles[idx]
570+
571+
# Last two should have original English titles (no translation)
572+
for idx in range(3, 5):
573+
original_title = self.highlighted_content_metadata_one[idx].json_metadata['title']
574+
assert highlighted_content[idx]['title'] == original_title
575+
576+
def test_list_with_english_language_parameter(self):
577+
"""
578+
Test that requesting highlight sets with lang=en returns original English titles.
579+
"""
580+
url = reverse('api:v1:highlight-sets-list') + f'?enterprise_customer={self.enterprise_uuid}&lang=en'
581+
self.set_up_catalog_learner()
582+
583+
response = self.client.get(url)
584+
assert response.status_code == status.HTTP_200_OK
585+
586+
highlight_sets_results = response.json()['results']
587+
highlighted_content = highlight_sets_results[0]['highlighted_content']
588+
589+
# All should have original English titles
590+
for idx in range(5):
591+
original_title = self.highlighted_content_metadata_one[idx].json_metadata['title']
592+
assert highlighted_content[idx]['title'] == original_title
593+
594+
def test_list_with_unsupported_language(self):
595+
"""
596+
Test that requesting with an unsupported language defaults to English.
597+
"""
598+
url = reverse('api:v1:highlight-sets-list') + f'?enterprise_customer={self.enterprise_uuid}&lang=fr'
599+
self.set_up_catalog_learner()
600+
601+
response = self.client.get(url)
602+
assert response.status_code == status.HTTP_200_OK
603+
604+
highlight_sets_results = response.json()['results']
605+
highlighted_content = highlight_sets_results[0]['highlighted_content']
606+
607+
# All should have original English titles (default behavior)
608+
for idx in range(5):
609+
original_title = self.highlighted_content_metadata_one[idx].json_metadata['title']
610+
assert highlighted_content[idx]['title'] == original_title
611+
612+
def test_list_without_language_parameter(self):
613+
"""
614+
Test that requesting highlight sets without lang parameter defaults to English.
615+
"""
616+
url = reverse('api:v1:highlight-sets-list') + f'?enterprise_customer={self.enterprise_uuid}'
617+
self.set_up_catalog_learner()
618+
619+
response = self.client.get(url)
620+
assert response.status_code == status.HTTP_200_OK
621+
622+
highlight_sets_results = response.json()['results']
623+
highlighted_content = highlight_sets_results[0]['highlighted_content']
624+
625+
# All should have original English titles (default behavior)
626+
for idx in range(5):
627+
original_title = self.highlighted_content_metadata_one[idx].json_metadata['title']
628+
assert highlighted_content[idx]['title'] == original_title
629+
630+
def test_detail_with_spanish_language_parameter(self):
631+
"""
632+
Test that retrieving a specific highlight set with lang=es returns Spanish translations.
633+
"""
634+
detail_url = reverse('api:v1:highlight-sets-detail', kwargs={'uuid': str(self.highlight_set_one.uuid)})
635+
detail_url += '?lang=es'
636+
self.set_up_catalog_learner()
637+
638+
response = self.client.get(detail_url)
639+
assert response.status_code == status.HTTP_200_OK
640+
641+
highlighted_content = response.json()['highlighted_content']
642+
643+
# First three should have Spanish titles
644+
for idx in range(3):
645+
assert highlighted_content[idx]['title'] == self.spanish_titles[idx]
646+
647+
# Last two should have original English titles (no translation)
648+
for idx in range(3, 5):
649+
original_title = self.highlighted_content_metadata_one[idx].json_metadata['title']
650+
assert highlighted_content[idx]['title'] == original_title
651+
652+
def test_detail_with_unsupported_language(self):
653+
"""
654+
Test that requesting with an unsupported language defaults to English.
655+
"""
656+
detail_url = reverse('api:v1:highlight-sets-detail', kwargs={'uuid': str(self.highlight_set_one.uuid)})
657+
detail_url += '?lang=fr' # French is not in AVAILABLE_TRANSLATION_LANGUAGES
658+
self.set_up_catalog_learner()
659+
660+
response = self.client.get(detail_url)
661+
assert response.status_code == status.HTTP_200_OK
662+
663+
highlighted_content = response.json()['highlighted_content']
664+
665+
# All should have original English titles (default behavior)
666+
for idx in range(5):
667+
original_title = self.highlighted_content_metadata_one[idx].json_metadata['title']
668+
assert highlighted_content[idx]['title'] == original_title
669+
670+
def test_translation_with_empty_title(self):
671+
"""
672+
Test that if a translation exists but has an empty title, it falls back to the original.
673+
"""
674+
# Create a translation with empty title
675+
content_metadata = self.highlighted_content_metadata_one[4]
676+
ContentTranslationFactory(
677+
content_metadata=content_metadata,
678+
language_code='es',
679+
title='' # Empty title
680+
)
681+
682+
detail_url = reverse('api:v1:highlight-sets-detail', kwargs={'uuid': str(self.highlight_set_one.uuid)})
683+
detail_url += '?lang=es'
684+
self.set_up_catalog_learner()
685+
686+
response = self.client.get(detail_url)
687+
assert response.status_code == status.HTTP_200_OK
688+
689+
highlighted_content = response.json()['highlighted_content']
690+
# Should fall back to original title when translation title is empty
691+
original_title = self.highlighted_content_metadata_one[4].json_metadata['title']
692+
assert highlighted_content[4]['title'] == original_title
693+
694+
529695
@ddt.ddt
530696
class HighlightSetViewSetTests(CurationAPITestBase):
531697
"""

0 commit comments

Comments
 (0)