Skip to content

Commit

Permalink
feat: whole course translations waffle (#34302)
Browse files Browse the repository at this point in the history
* feat: add waffle flag for whole course translations

* feat: put ai translations behind waffle

* chore: add ai_translations to quality matrix

* chore: add language and translation flag to courseware api (#34309)

---------

Co-authored-by: Leangseu Kim <[email protected]>
  • Loading branch information
nsprenkle and leangseu-edx committed Mar 19, 2024
1 parent 7cd15f2 commit 17e2f69
Show file tree
Hide file tree
Showing 9 changed files with 69 additions and 12 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pylint-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
matrix:
include:
- module-name: lms-1
path: "--django-settings-module=lms.envs.test lms/djangoapps/badges/ lms/djangoapps/branding/ lms/djangoapps/bulk_email/ lms/djangoapps/bulk_enroll/ lms/djangoapps/bulk_user_retirement/ lms/djangoapps/ccx/ lms/djangoapps/certificates/ lms/djangoapps/commerce/ lms/djangoapps/course_api/ lms/djangoapps/course_blocks/ lms/djangoapps/course_home_api/ lms/djangoapps/course_wiki/ lms/djangoapps/coursewarehistoryextended/ lms/djangoapps/debug/ lms/djangoapps/courseware/ lms/djangoapps/course_goals/ lms/djangoapps/rss_proxy/"
path: "--django-settings-module=lms.envs.test lms/djangoapps/ai_translation/ lms/djangoapps/badges/ lms/djangoapps/branding/ lms/djangoapps/bulk_email/ lms/djangoapps/bulk_enroll/ lms/djangoapps/bulk_user_retirement/ lms/djangoapps/ccx/ lms/djangoapps/certificates/ lms/djangoapps/commerce/ lms/djangoapps/course_api/ lms/djangoapps/course_blocks/ lms/djangoapps/course_home_api/ lms/djangoapps/course_wiki/ lms/djangoapps/coursewarehistoryextended/ lms/djangoapps/debug/ lms/djangoapps/courseware/ lms/djangoapps/course_goals/ lms/djangoapps/rss_proxy/"
- module-name: lms-2
path: "--django-settings-module=lms.envs.test lms/djangoapps/gating/ lms/djangoapps/grades/ lms/djangoapps/instructor/ lms/djangoapps/instructor_analytics/ lms/djangoapps/discussion/ lms/djangoapps/edxnotes/ lms/djangoapps/email_marketing/ lms/djangoapps/experiments/ lms/djangoapps/instructor_task/ lms/djangoapps/learner_dashboard/ lms/djangoapps/learner_home/ lms/djangoapps/lms_initialization/ lms/djangoapps/lms_xblock/ lms/djangoapps/lti_provider/ lms/djangoapps/mailing/ lms/djangoapps/mobile_api/ lms/djangoapps/monitoring/ lms/djangoapps/ora_staff_grader/ lms/djangoapps/program_enrollments/ lms/djangoapps/rss_proxy lms/djangoapps/static_template_view/ lms/djangoapps/staticbook/ lms/djangoapps/support/ lms/djangoapps/survey/ lms/djangoapps/teams/ lms/djangoapps/tests/ lms/djangoapps/user_tours/ lms/djangoapps/verify_student/ lms/djangoapps/mfe_config_api/ lms/envs/ lms/lib/ lms/tests.py"
- module-name: openedx-1
Expand Down
6 changes: 3 additions & 3 deletions lms/djangoapps/ai_translation/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def _init_translations_client(self):
client_secret=settings.TRANSLATIONS_SERVICE_EDX_OAUTH2_SECRET,
)

def translate(self, content, language, block_id):
def translate(self, content, source_language, target_language, block_id):
"""Request translated version of content from translations IDA"""

url = f"{settings.AI_TRANSLATIONS_API_URL}/translate-xblock/"
Expand All @@ -42,8 +42,8 @@ def translate(self, content, language, block_id):
}
payload = {
"block_id": str(block_id),
"source_language": "en",
"target_language": language,
"source_language": source_language,
"target_language": target_language,
"content": content,
"content_hash": sha256(content.encode("utf-8")).hexdigest(),
}
Expand Down
26 changes: 26 additions & 0 deletions lms/djangoapps/ai_translation/waffle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
Waffle config for the AI Translations service
"""

from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag

# Namespace
WAFFLE_NAMESPACE = "ai_translations"
LOG_PREFIX = "AI translations: "

# .. toggle_name: SOME_FEATURE_NAME
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: Enabling this feature allows course content to be sent to the AI Translations service
# for automatic translation of course content.
# .. toggle_warning: Requires ai-translations IDA to be available
# .. toggle_use_cases: opt_in
# .. toggle_creation_date: 2024-02-27
WHOLE_COURSE_TRANSLATIONS = CourseWaffleFlag(
f"{WAFFLE_NAMESPACE}.whole_course_translations", __name__, LOG_PREFIX
)


def whole_course_translations_enabled_for_course(course_key):
"""Helper to determine if whole course translation is enabled for the given context"""
return WHOLE_COURSE_TRANSLATIONS.is_enabled(course_key=course_key)
2 changes: 2 additions & 0 deletions lms/djangoapps/course_home_api/course_metadata/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,5 @@ class CourseHomeMetadataSerializer(VerifiedModeSerializer):
user_timezone = serializers.CharField()
can_view_certificate = serializers.BooleanField()
course_modes = CourseModeSerrializer(many=True)
language = serializers.CharField()
whole_course_translation_enabled = serializers.BooleanField()
3 changes: 3 additions & 0 deletions lms/djangoapps/course_home_api/course_metadata/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from lms.djangoapps.courseware.courses import check_course_access
from lms.djangoapps.courseware.masquerade import setup_masquerade
from lms.djangoapps.courseware.tabs import get_course_tab_list
from lms.djangoapps.ai_translation.waffle import whole_course_translations_enabled_for_course


@method_decorator(transaction.non_atomic_requests, name='dispatch')
Expand Down Expand Up @@ -137,6 +138,8 @@ def get(self, request, *args, **kwargs):
'user_timezone': user_timezone,
'can_view_certificate': certificates_viewable_for_course(course),
'course_modes': course_modes,
'language': course.language,
'whole_course_translation_enabled': whole_course_translations_enabled_for_course(course_key),
}
context = self.get_serializer_context()
context['course'] = course
Expand Down
5 changes: 4 additions & 1 deletion lms/djangoapps/courseware/views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1586,7 +1586,10 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True, disable_sta
student_view_context = request.GET.dict()
student_view_context['show_bookmark_button'] = request.GET.get('show_bookmark_button', '0') == '1'
student_view_context['show_title'] = request.GET.get('show_title', '1') == '1'
student_view_context['translate_lang'] = request.GET.get('translate_lang')

# AI Translation args
student_view_context['src_lang'] = request.GET.get('src_lang')
student_view_context['dest_lang'] = request.GET.get('dest_lang')

is_learning_mfe = is_request_from_learning_mfe(request)
# Right now, we only care about this in regards to the Learning MFE because it results
Expand Down
29 changes: 25 additions & 4 deletions xmodule/html_block.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# lint-amnesty, pylint: disable=missing-module-docstring

import copy
import json
import logging
import os
import re
Expand All @@ -11,6 +10,7 @@

from django.conf import settings
from fs.errors import ResourceNotFound
from lms.djangoapps.ai_translation.waffle import whole_course_translations_enabled_for_course
from lxml import etree
from path import Path as path
from web_fragments.fragment import Fragment
Expand Down Expand Up @@ -92,7 +92,7 @@ def student_view(self, context):
Return a fragment that contains the html for the student view
"""
# If a translation is requested and the ai_translation service is available, use translate_view
if (context.get("translate_lang") and self.runtime.service(self, 'ai_translation')):
if self.should_translate_content(context):
html = self.get_translated_html(context)
else:
html = self.get_html()
Expand All @@ -103,12 +103,33 @@ def student_view(self, context):
shim_xmodule_js(fragment, 'HTMLModule')
return fragment

def should_translate_content(self, context):
""" Determines whether to translate content, based on feature config and args. """

# Feature must be enabled
if not whole_course_translations_enabled_for_course(self.location.course_key):
return False

# Service must be enabled
if not self.runtime.service(self, 'ai_translation'):
return False

# Both source and destination language must be supplied
# and they must be different than each other to trigger translation
src_lang = context.get("src_lang")
dest_lang = context.get("dest_lang")
if src_lang and dest_lang and (src_lang != dest_lang):
return True

return False

def get_translated_html(self, context):
""" Returns translated html required for rendering the block, replacing placeholder values"""
if self.data:
translate_lang = context.get("translate_lang")
src_lang = context.get("src_lang")
dest_lang = context.get("dest_lang")
translation_service = self.runtime.service(self, 'ai_translation')
translated_html = translation_service.translate(self.data, translate_lang, self.location)
translated_html = translation_service.translate(self.data, src_lang, dest_lang, self.location)
return self.substitute_keywords(translated_html)
return self.data

Expand Down
3 changes: 2 additions & 1 deletion xmodule/tests/test_conditional.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,9 @@ def get_block_for_location(self, location):
return self.test_system.get_block_for_descriptor(block)

@patch('xmodule.x_module.block_global_local_resource_url')
@patch('xmodule.html_block.whole_course_translations_enabled_for_course', return_value=False)
@patch.dict(settings.FEATURES, {'ENABLE_EDXNOTES': False})
def test_conditional_block(self, _):
def test_conditional_block(self, _, mock_translate): # pylint: disable=unused-argument
"""Make sure that conditional block works"""
# edx - HarvardX
# cond_test - ER22x
Expand Down
5 changes: 3 additions & 2 deletions xmodule/tests/test_html_block.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# lint-amnesty, pylint: disable=missing-module-docstring

import unittest
from unittest.mock import Mock
from unittest.mock import Mock, patch

import ddt
from django.contrib.auth.models import AnonymousUser
Expand Down Expand Up @@ -83,7 +83,8 @@ def test_common_values(self, html):
STUDENT_VIEW,
PUBLIC_VIEW,
)
def test_student_preview_view(self, view):
@patch("xmodule.html_block.whole_course_translations_enabled_for_course", return_value=False)
def test_student_preview_view(self, view, mock_translations_enabled): # pylint: disable=unused-argument
"""
Ensure that student_view and public_view renders correctly.
"""
Expand Down

0 comments on commit 17e2f69

Please sign in to comment.