Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: code changes for ai_languages field #4586

Open
wants to merge 6 commits into
base: anawaz/prod-4306-1
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions course_discovery/apps/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1103,10 +1103,10 @@ class Meta(MinimalCourseRunSerializer.Meta):
'first_enrollable_paid_seat_price', 'has_ofac_restrictions', 'ofac_comment',
'enrollment_count', 'recent_enrollment_count', 'expected_program_type', 'expected_program_name',
'course_uuid', 'estimated_hours', 'content_language_search_facet_name', 'enterprise_subscription_inclusion',
'transcript_languages_search_facet_names', 'translation_languages'
'transcript_languages_search_facet_names', 'ai_languages'
)
read_only_fields = ('enrollment_count', 'recent_enrollment_count', 'content_language_search_facet_name',
'enterprise_subscription_inclusion', 'translation_languages')
'enterprise_subscription_inclusion', 'ai_languages')

def get_instructors(self, obj): # pylint: disable=unused-argument
# This field is deprecated. Use the staff field.
Expand Down
2 changes: 1 addition & 1 deletion course_discovery/apps/api/tests/test_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -782,7 +782,7 @@ def get_expected_data(cls, course_run, request):
'transcript_languages_search_facet_names': [
lang.get_search_facet_display() for lang in course_run.transcript_languages.all()
],
'translation_languages': course_run.translation_languages,
'ai_languages': course_run.ai_languages,
})
return expected

Expand Down
12 changes: 6 additions & 6 deletions course_discovery/apps/core/api_client/lms.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,23 +210,23 @@ def get_blocks_metadata(self, block_id: str, **kwargs):
cache_key = get_cache_key(block_id=block_id, resource=resource)
return self._get_blocks_data(block_id, cache_key, query_parameters, resource)

def get_course_run_translations(self, course_run_id: str):
def get_course_run_translations_and_transcriptions(self, course_run_id: str):
"""
Get translation information for a given course run.
Get translation and transcription information for a given course run.

Args:
course_run_id (str): The course run ID to fetch translation information for.
course_run_id (str): The course run ID to fetch information for.

Returns:
dict: A dictionary containing the translation information or an empty dict on error.
dict: A dictionary containing the relevant information or an empty dict on error.
"""
resource = settings.LMS_API_URLS['translations']
resource = settings.LMS_API_URLS['translations_and_transcriptions']
resource_url = urljoin(self.lms_url, resource)

try:
response = self.client.get(resource_url, params={'course_id': course_run_id})
response.raise_for_status()
return response.json()
except RequestException as e:
logger.exception(f'Failed to fetch translation data for course run [{course_run_id}]: {e}')
logger.exception(f'Failed to fetch data for course run [{course_run_id}]: {e}')
Copy link
Contributor

Choose a reason for hiding this comment

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

Log message can be specific i.e. translations_and_transcriptions data for course run.

return {}
36 changes: 22 additions & 14 deletions course_discovery/apps/core/tests/test_api_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,35 +237,43 @@ def test_get_blocks_data_cache_hit(self):
assert len(responses.calls) == 1

@responses.activate
def test_get_course_run_translations(self):
def test_get_course_run_translations_and_transcriptions(self):
"""
Verify that `get_course_run_translations` returns correct translation data.
Verify that `get_course_run_translations_and_transcriptions` returns correct data.
"""
course_run_id = 'course-v1:edX+DemoX+Demo_Course'
translation_data = {
"en": {"title": "Course Title", "language": "English"},
"fr": {"title": "Titre du cours", "language": "French"}
response_data = {
"available_translation_languages": [
{
"code": "ar",
"enabled": True,
"label": "Arabic"
}
],
"feature_enabled": True,
"feature_available": False,
"transcription_languages": ["en", "fr"]
}
resource = settings.LMS_API_URLS['translations']
resource = settings.LMS_API_URLS['translations_and_transcriptions']
resource_url = urljoin(self.partner.lms_url, resource)

responses.add(
responses.GET,
resource_url,
json=translation_data,
json=response_data,
status=200
)

result = self.lms.get_course_run_translations(course_run_id)
assert result == translation_data
result = self.lms.get_course_run_translations_and_transcriptions(course_run_id)
assert result == response_data

@responses.activate
def test_get_course_run_translations_with_error(self):
def test_get_course_run_translations_and_transcriptions_with_error(self):
"""
Verify that get_course_run_translations returns an empty dictionary when there's an error.
Verify that `get_course_run_translations_and_transcriptions` returns an empty dictionary when there's an error.
"""
course_run_id = 'course-v1:edX+DemoX+Demo_Course'
resource = settings.LMS_API_URLS['translations']
resource = settings.LMS_API_URLS['translations_and_transcriptions']
resource_url = urljoin(self.partner.lms_url, resource)

responses.add(
Expand All @@ -274,6 +282,6 @@ def test_get_course_run_translations_with_error(self):
status=500
)

result = self.lms.get_course_run_translations(course_run_id)
result = self.lms.get_course_run_translations_and_transcriptions(course_run_id)
assert result == {}
assert 'Failed to fetch translation data for course run [%s]' % course_run_id in self.log_messages['error'][0]
assert 'Failed to fetch data for course run [%s]' % course_run_id in self.log_messages['error'][0]
2 changes: 1 addition & 1 deletion course_discovery/apps/course_metadata/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ class CourseRunAdmin(SimpleHistoryAdmin):
raw_id_fields = ('course', 'draft_version',)
readonly_fields = [
'enrollment_count', 'recent_enrollment_count', 'hidden', 'key', 'enterprise_subscription_inclusion',
'variant_id', 'fixed_price_usd', 'translation_languages'
'variant_id', 'fixed_price_usd', 'ai_languages'
]
search_fields = ('uuid', 'key', 'title_override', 'course__title', 'slug', 'external_key', 'variant_id')
save_error = False
Expand Down
23 changes: 16 additions & 7 deletions course_discovery/apps/course_metadata/algolia_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def delegate_attributes(cls):
'secondary_description', 'tertiary_description']
facet_fields = ['availability_level', 'subject_names', 'levels', 'active_languages', 'staff_slugs',
'product_allowed_in', 'product_blocked_in', 'learning_type', 'learning_type_exp',
'product_translation_languages']
'product_ai_languages']
ranking_fields = ['availability_rank', 'product_recent_enrollment_count', 'promoted_in_spanish_index',
'product_value_per_click_usa', 'product_value_per_click_international',
'product_value_per_lead_usa', 'product_value_per_lead_international']
Expand Down Expand Up @@ -355,10 +355,16 @@ def product_max_effort(self):
return getattr(self.advertised_course_run, 'max_effort', None)

@property
def product_translation_languages(self):
if self.advertised_course_run and self.advertised_course_run.translation_languages:
return self.advertised_course_run.translation_languages
return []
def product_ai_languages(self):
if ai_langs:=(self.advertised_course_run and self.advertised_course_run.ai_languages):
return {
'translation_languages': [lang['label'] for lang in ai_langs['translation_languages']],
'transcription_languages': [lang['label'] for lang in ai_langs['transcription_languages']]
}
return {
'translation_languages': [],
'transcription_languages': []
}

@property
def owners(self):
Expand Down Expand Up @@ -544,8 +550,11 @@ def product_max_effort(self):
return self.max_hours_effort_per_week

@property
def product_translation_languages(self):
return []
def product_ai_languages(self):
return {
'translation_languages': [],
'transcription_languages': []
}

@property
def subject_names(self):
Expand Down
10 changes: 5 additions & 5 deletions course_discovery/apps/course_metadata/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class EnglishProductIndex(BaseProductIndex):
('staff_slugs', 'staff'), ('product_allowed_in', 'allowed_in'),
('product_blocked_in', 'blocked_in'), 'subscription_eligible',
'subscription_prices', 'learning_type', 'learning_type_exp',
('product_translation_languages', 'translation_languages'))
('product_ai_languages', 'ai_languages'))
ranking_fields = ('availability_rank', ('product_recent_enrollment_count', 'recent_enrollment_count'),
('product_value_per_click_usa', 'value_per_click_usa'),
('product_value_per_click_international', 'value_per_click_international'),
Expand Down Expand Up @@ -117,7 +117,7 @@ class EnglishProductIndex(BaseProductIndex):
'partner', 'availability', 'subject', 'level', 'language', 'product', 'program_type',
'filterOnly(staff)', 'filterOnly(allowed_in)', 'filterOnly(blocked_in)', 'skills.skill',
'skills.category', 'skills.subcategory', 'tags', 'subscription_eligible', 'subscription_prices',
'learning_type', 'learning_type_exp', 'translation_languages.code', 'translation_languages.label',
'learning_type', 'learning_type_exp', 'ai_languages.translation_languages', 'ai_languages.transcription_languages',
],
'customRanking': ['asc(availability_rank)', 'desc(recent_enrollment_count)']
}
Expand All @@ -135,7 +135,7 @@ class SpanishProductIndex(BaseProductIndex):
('staff_slugs', 'staff'), ('product_allowed_in', 'allowed_in'),
('product_blocked_in', 'blocked_in'), 'subscription_eligible',
'subscription_prices', 'learning_type', 'learning_type_exp',
('product_translation_languages', 'translation_languages'))
('product_ai_languages', 'ai_languages'))
ranking_fields = ('availability_rank', ('product_recent_enrollment_count', 'recent_enrollment_count'),
('product_value_per_click_usa', 'value_per_click_usa'),
('product_value_per_click_international', 'value_per_click_international'),
Expand Down Expand Up @@ -173,8 +173,8 @@ class SpanishProductIndex(BaseProductIndex):
'partner', 'availability', 'subject', 'level', 'language', 'product', 'program_type',
'filterOnly(staff)', 'filterOnly(allowed_in)', 'filterOnly(blocked_in)',
'skills.skill', 'skills.category', 'skills.subcategory', 'tags', 'subscription_eligible',
'subscription_prices', 'learning_type', 'learning_type_exp', 'translation_languages.code',
'translation_languages.label',
'subscription_prices', 'learning_type', 'learning_type_exp', 'ai_languages.translation_languages',
'ai_languages.transcription_languages',
],
'customRanking': ['desc(promoted_in_spanish_index)', 'asc(availability_rank)', 'desc(recent_enrollment_count)']
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
"""
Unit tests for the `update_course_ai_languages` management command.
"""
import datetime
from unittest.mock import patch

import ddt
from django.core.management import CommandError, call_command
from django.test import TestCase
from django.utils.timezone import now

from course_discovery.apps.course_metadata.models import CourseRun, CourseRunType
from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory, PartnerFactory, SeatFactory


@ddt.ddt
@patch('course_discovery.apps.core.api_client.lms.LMSAPIClient.get_course_run_translations_and_transcriptions')
class UpdateCourseAiLanguagesTests(TestCase):
"""Test Suite for the update_course_ai_languages management command."""

AI_LANGUAGES_DATA = {
'available_translation_languages': [
{'code': 'fr', 'label': 'French'},
{'code': 'cs', 'label': 'Czech'}
],
'feature_enabled': True,
}

AI_LANGUAGES_DATA_WITH_TRANSCRIPTIONS = {
**AI_LANGUAGES_DATA,
'transcription_languages': ['da', 'fr']
}

def setUp(self):
self.partner = PartnerFactory()
self.course_run = CourseRunFactory()

def assert_ai_langs(self, run, data):
self.assertListEqual(
run.ai_languages['translation_languages'],
data['available_translation_languages']
)
self.assertListEqual(
run.ai_languages['transcription_languages'],
[{'code': lang_code, 'label': lang_code} for lang_code in data.get('transcription_languages', [])]
)


@ddt.data(AI_LANGUAGES_DATA, AI_LANGUAGES_DATA_WITH_TRANSCRIPTIONS)
def test_update_ai_languages(self, mock_data, mock_get_translations_and_transcriptions):
"""Test the command with a valid course run and response data."""
mock_get_translations_and_transcriptions.return_value = mock_data

call_command('update_course_ai_languages', partner=self.partner.name)

course_run = CourseRun.objects.get(id=self.course_run.id)
self.assert_ai_langs(course_run, mock_data)

@ddt.data(AI_LANGUAGES_DATA, AI_LANGUAGES_DATA_WITH_TRANSCRIPTIONS)
def test_update_ai_languages_draft(self, mock_data, mock_get_translations_and_transcriptions):
"""
Test the command with both draft and non-draft course runs, ensuring that the both draft and non-draft
course runs are updated appropriately.
"""
mock_get_translations_and_transcriptions.return_value = mock_data
draft_course_run = CourseRunFactory(
draft=True, end=now() + datetime.timedelta(days=10)
)
course_run = CourseRunFactory(draft=False, draft_version_id=draft_course_run.id)

call_command('update_course_ai_languages', partner=self.partner.name)

course_run.refresh_from_db()
self.assert_ai_langs(course_run, mock_data)

draft_course_run.refresh_from_db()
self.assert_ai_langs(draft_course_run, mock_data)


@ddt.data(AI_LANGUAGES_DATA, AI_LANGUAGES_DATA_WITH_TRANSCRIPTIONS)
def test_update_ai_languages_no_translations(self, mock_data, mock_get_translations_and_transcriptions):
"""Test the command when no translations are returned for a course run."""
mock_get_translations_and_transcriptions.return_value = {
**mock_data,
'available_translation_languages': [],
}

call_command('update_course_ai_languages', partner=self.partner.name)

course_run = CourseRun.objects.get(id=self.course_run.id)
self.assertListEqual(course_run.ai_languages['translation_languages'], [])

@ddt.data(AI_LANGUAGES_DATA, AI_LANGUAGES_DATA_WITH_TRANSCRIPTIONS)
def test_command_with_active_flag(self, mock_data, mock_get_translations_and_transcriptions):
"""Test the command with the active flag filtering active course runs."""
mock_get_translations_and_transcriptions.return_value = mock_data

active_course_run = CourseRunFactory(end=now() + datetime.timedelta(days=10), ai_languages=None)
non_active_course_run = CourseRunFactory(end=now() - datetime.timedelta(days=10), ai_languages=None)

call_command('update_course_ai_languages', partner=self.partner.name, active=True)

active_course_run.refresh_from_db()
non_active_course_run.refresh_from_db()

self.assert_ai_langs(active_course_run, mock_data)
assert non_active_course_run.ai_languages is None

@ddt.data(AI_LANGUAGES_DATA, AI_LANGUAGES_DATA_WITH_TRANSCRIPTIONS)
def test_command_with_marketable_flag(self, mock_data, mock_get_translations_and_transcriptions):
"""Test the command with the marketable flag filtering marketable course runs."""
mock_get_translations_and_transcriptions.return_value = mock_data

verified_and_audit_type = CourseRunType.objects.get(slug='verified-audit')
verified_and_audit_type.is_marketable = True
verified_and_audit_type.save()

marketable_course_run = CourseRunFactory(
status='published',
slug='test-course-run',
type=verified_and_audit_type,
ai_languages=None
)
seat = SeatFactory(course_run=marketable_course_run)
marketable_course_run.seats.add(seat)

call_command('update_course_ai_languages', partner=self.partner.name, marketable=True)

marketable_course_run.refresh_from_db()
self.assert_ai_langs(marketable_course_run, mock_data)

@ddt.data(AI_LANGUAGES_DATA, AI_LANGUAGES_DATA_WITH_TRANSCRIPTIONS)
def test_command_with_marketable_and_active_flag(self, mock_data, mock_get_translations_and_transcriptions):
"""Test the command with the marketable and active flag filtering both marketable and active course runs."""
mock_get_translations_and_transcriptions.return_value = mock_data

non_active_non_marketable_course_run = CourseRunFactory(
end=now() - datetime.timedelta(days=10), ai_languages=None
)
active_non_marketable_course_run = CourseRunFactory(
end=now() + datetime.timedelta(days=10), ai_languages=None
)

verified_and_audit_type = CourseRunType.objects.get(slug='verified-audit')
verified_and_audit_type.is_marketable = True
verified_and_audit_type.save()

marketable_non_active_course_run = CourseRunFactory(
status='published',
slug='test-course-run',
type=verified_and_audit_type,
end=now() - datetime.timedelta(days=10), ai_languages=None
)
seat = SeatFactory(course_run=marketable_non_active_course_run)
marketable_non_active_course_run.seats.add(seat)

call_command('update_course_ai_languages', partner=self.partner.name, marketable=True, active=True)

marketable_non_active_course_run.refresh_from_db()
active_non_marketable_course_run.refresh_from_db()
non_active_non_marketable_course_run.refresh_from_db()
self.assert_ai_langs(marketable_non_active_course_run, mock_data)
self.assert_ai_langs(active_non_marketable_course_run, mock_data)
assert non_active_non_marketable_course_run.ai_languages is None

def test_command_no_partner(self, _):
"""Test the command raises an error if no valid partner is found."""
with self.assertRaises(CommandError):
call_command('update_course_ai_languages', partner='nonexistent-partner')
Loading
Loading