From 2f7295873cac22fad1146ed053e99fc20d3a3fd2 Mon Sep 17 00:00:00 2001 From: Ali Nawaz Date: Mon, 24 Feb 2025 20:48:08 +0500 Subject: [PATCH] temp: just a stash --- course_discovery/apps/core/api_client/lms.py | 21 +++ .../tests/test_update_course_ai_languages.py | 172 ++++++++++++++++++ .../commands/update_course_ai_languages.py | 90 +++++++++ .../commands/update_course_ai_translations.py | 21 +-- 4 files changed, 285 insertions(+), 19 deletions(-) create mode 100644 course_discovery/apps/course_metadata/management/commands/tests/test_update_course_ai_languages.py create mode 100644 course_discovery/apps/course_metadata/management/commands/update_course_ai_languages.py diff --git a/course_discovery/apps/core/api_client/lms.py b/course_discovery/apps/core/api_client/lms.py index 916e0cf6d3..62817152dd 100644 --- a/course_discovery/apps/core/api_client/lms.py +++ b/course_discovery/apps/core/api_client/lms.py @@ -230,3 +230,24 @@ def get_course_run_translations(self, course_run_id: str): except RequestException as e: logger.exception(f'Failed to fetch translation data for course run [{course_run_id}]: {e}') return {} + + def get_course_run_translations_and_transcriptions(self, course_run_id: str): + """ + Get translation and transcription information for a given course run. + + Args: + course_run_id (str): The course run ID to fetch translation information for. + + Returns: + dict: A dictionary containing the relevant information or an empty dict on error. + """ + 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 data for course run [{course_run_id}]: {e}') + return {} diff --git a/course_discovery/apps/course_metadata/management/commands/tests/test_update_course_ai_languages.py b/course_discovery/apps/course_metadata/management/commands/tests/test_update_course_ai_languages.py new file mode 100644 index 0000000000..e3f35a1496 --- /dev/null +++ b/course_discovery/apps/course_metadata/management/commands/tests/test_update_course_ai_languages.py @@ -0,0 +1,172 @@ +""" +Unit tests for the `update_course_ai_languages` management command. +""" +import datetime +from unittest.mock import patch + +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 + + +@patch('course_discovery.apps.core.api_client.lms.LMSAPIClient.get_course_run_translations') +class UpdateCourseAiTranslationsTests(TestCase): + """Test Suite for the update_course_ai_translations management command.""" + + AI_LANGS_DATA = { + 'available_translation_languages': [ + {'code': 'fr', 'label': 'French'}, + {'code': 'es', 'label': 'Spanish'} + ], + 'feature_enabled': True + } + + AI_LANGS_DATA_WITH_TRANSCRIPTIONS = { + **AI_LANGS_DATA, + 'transcription_languages': ['en', 'gf'] + } + + def setUp(self): + self.partner = PartnerFactory() + self.course_run = CourseRunFactory() + + def test_update_course_run_translations(self, mock_get_translations): + """Test the command with a valid course run and translation data.""" + mock_get_translations.return_value = self.TRANSLATION_DATA + + call_command('update_course_ai_translations', partner=self.partner.name) + + course_run = CourseRun.objects.get(id=self.course_run.id) + self.assertListEqual( + course_run.translation_languages, + self.TRANSLATION_DATA['available_translation_languages'] + ) + + def test_update_course_run_translations_draft(self, mock_get_translations): + """ + Test the command with both draft and non-draft course runs, ensuring that the both draft and non-draft + course runs are updated with the available translation languages. + """ + mock_get_translations.return_value = self.TRANSLATION_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_translations", partner=self.partner.name) + + course_run.refresh_from_db() + self.assertListEqual( + course_run.translation_languages, self.TRANSLATION_DATA["available_translation_languages"], + ) + + draft_course_run.refresh_from_db() + self.assertListEqual( + draft_course_run.translation_languages, self.TRANSLATION_DATA["available_translation_languages"], + ) + + def test_command_with_no_translations(self, mock_get_translations): + """Test the command when no translations are returned for a course run.""" + mock_get_translations.return_value = { + **self.TRANSLATION_DATA, + 'available_translation_languages': [], + 'feature_enabled': False + } + + call_command('update_course_ai_translations', partner=self.partner.name) + + course_run = CourseRun.objects.get(id=self.course_run.id) + self.assertListEqual(course_run.translation_languages, []) + + def test_command_with_active_flag(self, mock_get_translations): + """Test the command with the active flag filtering active course runs.""" + mock_get_translations.return_value = { + **self.TRANSLATION_DATA, + 'available_translation_languages': [{'code': 'fr', 'label': 'French'}] + } + + active_course_run = CourseRunFactory(end=now() + datetime.timedelta(days=10)) + non_active_course_run = CourseRunFactory(end=now() - datetime.timedelta(days=10), translation_languages=[]) + + call_command('update_course_ai_translations', partner=self.partner.name, active=True) + + active_course_run.refresh_from_db() + non_active_course_run.refresh_from_db() + + self.assertListEqual( + active_course_run.translation_languages, + [{'code': 'fr', 'label': 'French'}] + ) + self.assertListEqual(non_active_course_run.translation_languages, []) + + def test_command_with_marketable_flag(self, mock_get_translations): + """Test the command with the marketable flag filtering marketable course runs.""" + mock_get_translations.return_value = { + **self.TRANSLATION_DATA, + 'available_translation_languages': [{'code': 'es', 'label': 'Spanish'}] + } + + 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 + ) + seat = SeatFactory(course_run=marketable_course_run) + marketable_course_run.seats.add(seat) + + call_command('update_course_ai_translations', partner=self.partner.name, marketable=True) + + marketable_course_run.refresh_from_db() + self.assertListEqual( + marketable_course_run.translation_languages, + [{'code': 'es', 'label': 'Spanish'}] + ) + + def test_command_with_marketable_and_active_flag(self, mock_get_translations): + """Test the command with the marketable and active flag filtering both marketable and active course runs.""" + mock_get_translations.return_value = { + **self.TRANSLATION_DATA, + 'available_translation_languages': [{'code': 'fr', 'label': 'French'}] + } + + non_active_non_marketable_course_run = CourseRunFactory( + end=now() - datetime.timedelta(days=10), translation_languages=[]) + active_non_marketable_course_run = CourseRunFactory(end=now() + datetime.timedelta(days=10)) + + 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), translation_languages=[] + ) + seat = SeatFactory(course_run=marketable_non_active_course_run) + marketable_non_active_course_run.seats.add(seat) + + call_command('update_course_ai_translations', partner=self.partner.name, marketable=True, active=True) + + marketable_non_active_course_run.refresh_from_db() + self.assertListEqual( + marketable_non_active_course_run.translation_languages, + [{'code': 'fr', 'label': 'French'}] + ) + self.assertListEqual( + active_non_marketable_course_run.translation_languages, + [{'code': 'fr', 'label': 'French'}] + ) + self.assertListEqual(non_active_non_marketable_course_run.translation_languages, []) + + 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_translations', partner='nonexistent-partner') diff --git a/course_discovery/apps/course_metadata/management/commands/update_course_ai_languages.py b/course_discovery/apps/course_metadata/management/commands/update_course_ai_languages.py new file mode 100644 index 0000000000..3e7105f947 --- /dev/null +++ b/course_discovery/apps/course_metadata/management/commands/update_course_ai_languages.py @@ -0,0 +1,90 @@ +""" +Management command to fetch translation and transcription information from the LMS and update the CourseRun model. +""" + +import logging + +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError + +from course_discovery.apps.core.api_client.lms import LMSAPIClient +from course_discovery.apps.course_metadata.models import CourseRun, Partner + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = 'Fetches Content AI Translations and Transcriptions metadata from the LMS and updates the CourseRun model in Discovery.' + + def add_arguments(self, parser): + parser.add_argument( + '--partner', + type=str, + default=settings.DEFAULT_PARTNER_ID, + help='Specify the partner name or ID to fetch translations for. ' + 'Defaults to the partner configured in settings.DEFAULT_PARTNER_ID.', + ) + parser.add_argument( + '--active', + action='store_true', + default=False, + help='Only update translations for active course runs. Defaults to False.', + ) + parser.add_argument( + '--marketable', + action='store_true', + default=False, + help='Only update translations for marketable course runs. Defaults to False.', + ) + + def handle(self, *args, **options): + """ + Example usage: ./manage.py update_course_ai_languages --partner=edx --active --marketable + """ + partner_identifier = options.get('partner') + partner = Partner.objects.filter(name__iexact=partner_identifier).first() + + if not partner: + raise CommandError('No partner object found. Ensure that the Partner data is correctly configured.') + + lms_api_client = LMSAPIClient(partner) + + course_runs = CourseRun.objects.all() + + if options['active'] and options['marketable']: + course_runs = course_runs.marketable().union(course_runs.active()) + elif options['active']: + course_runs = course_runs.active() + elif options['marketable']: + course_runs = course_runs.marketable() + + for course_run in course_runs: + try: + translation_data = lms_api_client.get_course_run_translations_and_transcriptions(course_run.key) + available_translation_languages = ( + translation_data.get('available_translation_languages', []) + if translation_data.get('feature_enabled', False) + else [] + ) + available_transcription_languages = translation_data.get('transcription_languages', []) + + # Remove any keys other than `code` and `label` + available_translation_languages = [{'code': lang['code'], 'label': lang['label']} for lang in available_translation_languages] + + # Add the labels for the codes. Currently we set the code as the label. We will be fixing this shortly + available_transcription_languages = [{'code': lang, 'label': lang} for lang in available_transcription_languages] + + course_run.ai_languages = { + "translation_languages": available_translation_languages, + "transcription_languages": available_transcription_languages + } + course_run.save(update_fields=["ai_languages"]) + + if course_run.draft_version: + course_run.draft_version.ai_languages = course_run.ai_languages + course_run.draft_version.save(update_fields=["ai_languages"]) + logger.info(f'Updated ai languages for {course_run.key} (both draft and non-draft versions)') + else: + logger.info(f'Updated ai languages for {course_run.key} (non-draft version only)') + except Exception as e: # pylint: disable=broad-except + logger.error(f'Error processing {course_run.key}: {e}') diff --git a/course_discovery/apps/course_metadata/management/commands/update_course_ai_translations.py b/course_discovery/apps/course_metadata/management/commands/update_course_ai_translations.py index d72329131a..8cbd5826b0 100644 --- a/course_discovery/apps/course_metadata/management/commands/update_course_ai_translations.py +++ b/course_discovery/apps/course_metadata/management/commands/update_course_ai_translations.py @@ -61,35 +61,18 @@ def handle(self, *args, **options): for course_run in course_runs: try: translation_data = lms_api_client.get_course_run_translations(course_run.key) - available_translation_languages = ( + + course_run.translation_languages = ( translation_data.get('available_translation_languages', []) if translation_data.get('feature_enabled', False) else [] ) - available_transcription_languages = translation_data.get('transcription_languages', []) - - # Remove any keys other than `code` and `label` - available_translation_languages = [{'code': lang['code'], 'label': lang['label']} for lang in available_translation_languages] - - # Add the labels for the codes. Currently we set the code as the label. We will be fixing this shortly - available_transcription_languages = [{'code': lang, 'label': lang} for lang in available_transcription_languages] - - course_run.translation_languages = available_translation_languages course_run.save(update_fields=["translation_languages"]) - course_run.ai_languages = { - "translation_languages": available_translation_languages, - "transcription_languages": available_transcription_languages - } - course_run.save(update_fields=["ai_languages"]) - if course_run.draft_version: course_run.draft_version.translation_languages = course_run.translation_languages course_run.draft_version.save(update_fields=["translation_languages"]) logger.info(f'Updated translations for {course_run.key} (both draft and non-draft versions)') - - course_run.draft_version.ai_languages = course_run.ai_languages - course_run.draft_version.save(update_fields=["ai_languages"]) else: logger.info(f'Updated translations for {course_run.key} (non-draft version only)') except Exception as e: # pylint: disable=broad-except