Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5a1e062
feat: added spanish translations for algolia objects
irfanuddinahmad Dec 3, 2025
bf3fc20
fix: fixed tests
irfanuddinahmad Dec 3, 2025
8768a3b
fix: fixed failing tests
irfanuddinahmad Dec 3, 2025
1c0aaf6
fix: fix codestyle issues
irfanuddinahmad Dec 3, 2025
732e7d1
fix: fixed import ordering
irfanuddinahmad Dec 4, 2025
b7cb9a9
fix: fixed linter issues
irfanuddinahmad Dec 4, 2025
cdfd2b8
fix: fixed tests to include spanish objects
irfanuddinahmad Dec 4, 2025
120aff8
fix: fixed mock for algolia client
irfanuddinahmad Dec 4, 2025
c6653eb
fix: fixed code quality
irfanuddinahmad Dec 4, 2025
fb7f95f
fix: fixed circular import
irfanuddinahmad Dec 4, 2025
5d79cc9
fix: fixed local import
irfanuddinahmad Dec 4, 2025
81328c8
fix: added tests for spanish algolia helpers
irfanuddinahmad Dec 4, 2025
6708153
fix: fixed codestyle issues
irfanuddinahmad Dec 5, 2025
ef44ecb
fix: removed unwanted comments
irfanuddinahmad Dec 5, 2025
6c0b59a
feat: added command and models for pre-population of spanish content
irfanuddinahmad Dec 5, 2025
c1338a9
fix: fixed exception handling
irfanuddinahmad Dec 5, 2025
8edb1cb
fix: fixed tests
irfanuddinahmad Dec 5, 2025
6483af1
fix: fixed quality issues
irfanuddinahmad Dec 5, 2025
de6018c
fix: added tests
irfanuddinahmad Dec 5, 2025
e826413
fix: fixed codestyle issues
irfanuddinahmad Dec 5, 2025
e13aa7b
fix: fixed quality
irfanuddinahmad Dec 5, 2025
93ea9b9
fix: fixed import sorting
irfanuddinahmad Dec 5, 2025
6378f4d
fix: removed inline translation
irfanuddinahmad Dec 15, 2025
a1dd519
fix: fixed tests and logging
irfanuddinahmad Dec 15, 2025
db0d537
fix: fixed codestyle issues
Dec 15, 2025
449cd43
Merge pull request #1117 from openedx/iahmad/spanish-content
irfanuddinahmad Dec 15, 2025
f349d2a
fix: added integration test
irfanuddinahmad Dec 16, 2025
c94cf5f
fix: added performance test
irfanuddinahmad Dec 16, 2025
c4f6333
fix: optimized tests
irfanuddinahmad Dec 16, 2025
3c9f133
fix: fixed quality issues
irfanuddinahmad Dec 16, 2025
7034d89
fix: fixed linter issues
irfanuddinahmad Dec 16, 2025
f9bc6aa
fix: fixed prompt and tests
irfanuddinahmad Dec 19, 2025
db3da8e
fix: fixed tests
irfanuddinahmad Dec 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions enterprise_catalog/apps/ai_curation/utils/algolia_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@

from django.utils.html import strip_tags

from enterprise_catalog.apps.catalog.algolia_utils import (
get_initialized_algolia_client,
)
from enterprise_catalog.apps.api_client.algolia import AlgoliaSearchClient


@dataclass
Expand Down Expand Up @@ -60,6 +58,15 @@ def extract_program_data(hit: dict):
}


def get_initialized_algolia_client():
"""
Initializes and returns an Algolia client for updating search indices
"""
algolia_client = AlgoliaSearchClient()
algolia_client.init_index()
return algolia_client


def fetch_catalog_metadata_from_algolia(enterprise_catalog_query_title: str):
"""
Returns the ocm_courses, exec_ed_courses, programs, subjects from the
Expand Down
48 changes: 48 additions & 0 deletions enterprise_catalog/apps/ai_curation/utils/open_ai_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,51 @@ def get_keywords_to_prose(query):
return keywords_to_prose[0]

return ''


def translate_to_spanish(text):
"""
Translate the given text to Spanish using OpenAI.

Args:
text (str): The text to translate.

Returns:
str: The translated text in Spanish.
"""
if not text:
return ''

content = (
f"Translate the following text to Spanish. "
f"Return ONLY the translated text without any comments, "
f"explanations, or conversational responses:\n\n{text}"
)
messages = [
{
'content': content
}
]
translated_text = chat_completions(messages=messages)

if translated_text:
return translated_text[0]
return ''


def translate_object_fields(data, fields):
"""
Translate specified fields in a dictionary to Spanish.

Args:
data (dict): The dictionary containing fields to translate.
fields (list): A list of field names to translate.

Returns:
dict: A new dictionary with translated fields.
"""
translated_data = data.copy()
for field in fields:
if field in data and isinstance(data[field], str):
translated_data[field] = translate_to_spanish(data[field])
return translated_data
55 changes: 55 additions & 0 deletions enterprise_catalog/apps/api/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
_algolia_object_from_product,
configure_algolia_index,
create_algolia_objects,
create_spanish_algolia_object,
get_algolia_object_id,
get_initialized_algolia_client,
get_pathway_course_keys,
Expand Down Expand Up @@ -867,6 +868,32 @@ def add_video_to_algolia_objects(
batched_metadata = _batched_metadata_with_queries(json_metadata, queries)
_add_in_algolia_products_by_object_id(algolia_products_by_object_id, batched_metadata)

# Create and index Spanish version
json_metadata_es = create_spanish_algolia_object(json_metadata, video)

if json_metadata_es:
# enterprise customer uuids for Spanish
batched_metadata_es = _batched_metadata(
json_metadata_es,
customer_uuids,
'enterprise_customer_uuids',
'{}-customer-uuids-{}',
)
_add_in_algolia_products_by_object_id(algolia_products_by_object_id, batched_metadata_es)

# enterprise catalog uuids for Spanish
batched_metadata_es = _batched_metadata(
json_metadata_es,
catalog_uuids,
'enterprise_catalog_uuids',
'{}-catalog-uuids-{}',
)
_add_in_algolia_products_by_object_id(algolia_products_by_object_id, batched_metadata_es)

# enterprise catalog queries for Spanish
batched_metadata_es = _batched_metadata_with_queries(json_metadata_es, queries)
_add_in_algolia_products_by_object_id(algolia_products_by_object_id, batched_metadata_es)


def add_metadata_to_algolia_objects(
metadata,
Expand Down Expand Up @@ -954,6 +981,32 @@ def add_metadata_to_algolia_objects(
batched_metadata = _batched_metadata_with_queries(json_metadata, queries)
_add_in_algolia_products_by_object_id(algolia_products_by_object_id, batched_metadata)

# Create and index Spanish version
json_metadata_es = create_spanish_algolia_object(json_metadata, metadata)

if json_metadata_es:
# enterprise catalog uuids for Spanish
batched_metadata_es = _batched_metadata(
json_metadata_es,
catalog_uuids,
'enterprise_catalog_uuids',
'{}-catalog-uuids-{}',
)
_add_in_algolia_products_by_object_id(algolia_products_by_object_id, batched_metadata_es)

# enterprise customer uuids for Spanish
batched_metadata_es = _batched_metadata(
json_metadata_es,
customer_uuids,
'enterprise_customer_uuids',
'{}-customer-uuids-{}',
)
_add_in_algolia_products_by_object_id(algolia_products_by_object_id, batched_metadata_es)

# enterprise catalog queries for Spanish
batched_metadata_es = _batched_metadata_with_queries(json_metadata_es, queries)
_add_in_algolia_products_by_object_id(algolia_products_by_object_id, batched_metadata_es)


def get_algolia_objects_from_course_content_metadata(content_metadata):
content_key = content_metadata.content_key
Expand Down Expand Up @@ -1059,6 +1112,7 @@ def _get_algolia_products_for_batch(
)
).prefetch_related(
Prefetch('catalog_queries', queryset=all_catalog_queries),
'translations',
)
if getattr(settings, 'SHOULD_INDEX_COURSES_WITH_RESTRICTED_RUNS', False):
# Make the courses that we index actually contain restricted runs in the payload.
Expand All @@ -1081,6 +1135,7 @@ def _get_algolia_products_for_batch(
parent_content_key__in=course_content_keys
).prefetch_related(
Prefetch('catalog_queries', queryset=all_catalog_queries),
'translations',
)
course_run_content_keys = [cm.content_key for cm in content_metadata_courseruns]
videos = Video.objects.filter(
Expand Down
150 changes: 150 additions & 0 deletions enterprise_catalog/apps/api/tests/test_algolia_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"""
Integration tests for Algolia indexing.
"""
from unittest import mock

from django.db import connection
from django.test import TestCase
from django.test.utils import CaptureQueriesContext

from enterprise_catalog.apps.api.tasks import _reindex_algolia
from enterprise_catalog.apps.catalog.constants import COURSE
from enterprise_catalog.apps.catalog.models import ContentTranslation
from enterprise_catalog.apps.catalog.tests.factories import (
CatalogQueryFactory,
ContentMetadataFactory,
EnterpriseCatalogFactory,
)


class AlgoliaIntegrationTests(TestCase):
"""
Integration tests for Algolia indexing logic.
"""

@mock.patch('enterprise_catalog.apps.api.tasks._retrieve_inactive_tmp_indices')
@mock.patch('enterprise_catalog.apps.api.tasks._delete_indices')
@mock.patch('enterprise_catalog.apps.api.tasks.configure_algolia_index')
@mock.patch('enterprise_catalog.apps.api.tasks.get_initialized_algolia_client')
def test_algolia_indexing_with_spanish_translation(
self,
mock_get_client,
_mock_configure,
_mock_delete,
_mock_retrieve
):
"""
Test that _reindex_algolia pushes double the objects when Spanish translation exists.
"""
# Setup
mock_client = mock.Mock()
mock_get_client.return_value = mock_client

# Create a course
course = ContentMetadataFactory(content_type=COURSE, content_key='course-v1:Test+Course')

# Create catalog query and associate with course
catalog_query = CatalogQueryFactory()
course.catalog_queries.add(catalog_query)

# Create enterprise catalog associated with query
EnterpriseCatalogFactory(catalog_query=catalog_query)

content_keys = [course.content_key]

# --- Execution 1: No Translation ---
_reindex_algolia(
indexable_content_keys=content_keys,
nonindexable_content_keys=[],
dry_run=False
)

# Verify 3 objects pushed (catalog_uuids, customer_uuids, catalog_queries)
self.assertTrue(mock_client.replace_all_objects.called)
# Get the generator passed to replace_all_objects
args, _ = mock_client.replace_all_objects.call_args
generator = args[0]
# Consume generator to count objects
objects = list(generator)
self.assertEqual(len(objects), 3, "Should have 3 objects (English only)")
self.assertFalse(any(obj['objectID'].endswith('-es') for obj in objects))

# Reset mock
mock_client.reset_mock()

# --- Execution 2: With Translation ---
ContentTranslation.objects.create(
content_metadata=course,
language_code='es',
title='Curso de Prueba'
)

_reindex_algolia(
indexable_content_keys=content_keys,
nonindexable_content_keys=[],
dry_run=False
)

# Verify 6 objects pushed (3 English + 3 Spanish)
self.assertTrue(mock_client.replace_all_objects.called)
args, _ = mock_client.replace_all_objects.call_args
generator = args[0]
objects = list(generator)
self.assertEqual(len(objects), 6, "Should have 6 objects (English + Spanish)")

# Verify IDs
english_obj = next(obj for obj in objects if '-es-' not in obj['objectID'])
spanish_obj = next(obj for obj in objects if '-es-' in obj['objectID'])

self.assertIsNotNone(english_obj)
self.assertIsNotNone(spanish_obj)
self.assertEqual(spanish_obj['title'], 'Curso de Prueba')

@mock.patch('enterprise_catalog.apps.api.tasks._retrieve_inactive_tmp_indices')
@mock.patch('enterprise_catalog.apps.api.tasks._delete_indices')
@mock.patch('enterprise_catalog.apps.api.tasks.configure_algolia_index')
@mock.patch('enterprise_catalog.apps.api.tasks.get_initialized_algolia_client')
def test_algolia_indexing_performance(
self,
mock_get_client,
_mock_configure,
_mock_delete,
_mock_retrieve
):
"""
Test that indexing performance (DB queries) does not scale linearly with number of items.
"""
mock_client = mock.Mock()
mock_get_client.return_value = mock_client

# Helper to create content
def create_content(key_suffix):
course = ContentMetadataFactory(content_type=COURSE, content_key=f'course-v1:Test+Course+{key_suffix}')
catalog_query = CatalogQueryFactory()
course.catalog_queries.add(catalog_query)
EnterpriseCatalogFactory(catalog_query=catalog_query)
return course.content_key

# Warmup
key1 = create_content('1')
_reindex_algolia([key1], [], dry_run=False)

# Measure for 1 item
key2 = create_content('2')
with CaptureQueriesContext(connection) as ctx1:
_reindex_algolia([key2], [], dry_run=False)
count_1_item = len(ctx1)

# Measure for 2 items
key3 = create_content('3')
key4 = create_content('4')
with CaptureQueriesContext(connection) as ctx2:
_reindex_algolia([key3, key4], [], dry_run=False)
count_2_items = len(ctx2)

# The difference should be small (constant overhead), not double.
# Ideally count_2_items should be close to count_1_item.
diff = count_2_items - count_1_item

# We expect the difference to be 0 or very small (not proportional to N)
self.assertLess(diff, 5, f"Query count increased significantly: {count_1_item} vs {count_2_items}")
Loading