From cb3a0e91e9b3e39ad5817c5e04abc8c1c9ce0f73 Mon Sep 17 00:00:00 2001 From: alangsto <46360176+alangsto@users.noreply.github.com> Date: Wed, 12 Jul 2023 14:25:59 -0400 Subject: [PATCH] feat: return lti launch url (#32718) --- .../courseware_api/tests/test_views.py | 115 ++++++++++++++++-- .../core/djangoapps/courseware_api/utils.py | 44 +++++++ .../core/djangoapps/courseware_api/views.py | 10 +- 3 files changed, 157 insertions(+), 12 deletions(-) diff --git a/openedx/core/djangoapps/courseware_api/tests/test_views.py b/openedx/core/djangoapps/courseware_api/tests/test_views.py index 620fafd9ee..a23457d6b0 100644 --- a/openedx/core/djangoapps/courseware_api/tests/test_views.py +++ b/openedx/core/djangoapps/courseware_api/tests/test_views.py @@ -11,10 +11,13 @@ import ddt from completion.test_utils import CompletionWaffleTestMixin, submit_completions_for_testing from django.conf import settings from django.contrib.auth import get_user_model +from django.test import override_settings from django.test.client import RequestFactory from edx_django_utils.cache import TieredCache from edx_toggles.toggles.testutils import override_waffle_flag +from lti_consumer.data import Lti1p3LaunchData +from lti_consumer.models import LtiConfiguration from xmodule.data import CertificatesDisplayBehaviors from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase @@ -430,19 +433,111 @@ class CourseApiTestViews(BaseCoursewareTests, MasqueradeMixin): assert 'can_access_proctored_exams' in courseware_data assert courseware_data['can_access_proctored_exams'] == result - @ddt.data( - True, - False - ) - def test_learning_assistant_launch_url(self, enabled): - with override_waffle_flag(COURSEWARE_LEARNING_ASSISTANT, active=enabled): - response = self.client.get(self.url) + @override_waffle_flag(COURSEWARE_LEARNING_ASSISTANT, active=False) + def test_learning_assistant_launch_url_disabled_waffle_flag(self): + response = self.client.get(self.url) launch_url = response.json()['learning_assistant_launch_url'] - if enabled: - assert launch_url == "" - else: + assert launch_url is None + + @override_waffle_flag(COURSEWARE_LEARNING_ASSISTANT, active=True) + @override_settings(LEARNING_ASSISTANT_CONFIG_ID=None) + def test_learning_assistant_launch_no_config(self): + response = self.client.get(self.url) + launch_url = response.json()['learning_assistant_launch_url'] + assert launch_url is None + + @override_waffle_flag(COURSEWARE_LEARNING_ASSISTANT, active=True) + def test_learning_assistant_launch_1p1(self): + test_config = LtiConfiguration.objects.create( + config_id=1, + version='lti_1p1' + ) + with override_settings(LEARNING_ASSISTANT_CONFIG_ID=test_config.id): + response = self.client.get(self.url) + launch_url = response.json()['learning_assistant_launch_url'] assert launch_url is None + @override_waffle_flag(COURSEWARE_LEARNING_ASSISTANT, active=True) + def test_learning_assistant_launch_not_staff_unverified(self): + test_config = LtiConfiguration.objects.create( + config_id=1, + version='lti_1p3' + ) + with override_settings(LEARNING_ASSISTANT_CONFIG_ID=test_config.id): + self.user.is_staff = False + self.user.save() + CourseEnrollment.enroll(self.user, self.course.id, 'audit') + response = self.client.get(self.url) + launch_url = response.json()['learning_assistant_launch_url'] + assert launch_url is None + + @ddt.data( + 'verified', + 'professional', + 'masters', + 'executive-education', + 'paid-executive-education', + 'paid-bootcamp' + ) + @mock.patch('openedx.core.djangoapps.courseware_api.utils.get_lti_1p3_launch_start_url') + @override_waffle_flag(COURSEWARE_LEARNING_ASSISTANT, active=True) + def test_learning_assistant_launch_url(self, enrollment_mode, mock_lti_launch): + test_config = LtiConfiguration.objects.create( + config_id=1, + version='lti_1p3' + ) + with override_settings(LEARNING_ASSISTANT_CONFIG_ID=test_config.id): + self.user.is_staff = False + self.user.save() + CourseEnrollment.enroll(self.user, self.course.id, enrollment_mode) + + expected_url = 'https://testlaunch.org' + mock_lti_launch.return_value = expected_url + + response = self.client.get(self.url) + launch_url = response.json()['learning_assistant_launch_url'] + assert launch_url == expected_url + + expected_launch_data = Lti1p3LaunchData( + user_id=self.user.id, + user_role='student', + email=self.user.email, + config_id=1, + resource_link_id='-'.join(['1', str(self.course.id)]), + context_id=str(self.course.id), + context_type=['course_offering'], + context_title=self.course.display_name + ) + mock_lti_launch.assert_called_with(expected_launch_data) + + @mock.patch('openedx.core.djangoapps.courseware_api.utils.get_lti_1p3_launch_start_url') + @override_waffle_flag(COURSEWARE_LEARNING_ASSISTANT, active=True) + def test_learning_assistant_launch_url_instructor(self, mock_lti_launch): + self.client.login(username='instructor', password='foo') + test_config = LtiConfiguration.objects.create( + config_id=1, + version='lti_1p3' + ) + with override_settings(LEARNING_ASSISTANT_CONFIG_ID=test_config.id): + expected_url = 'https://testlaunch.org' + mock_lti_launch.return_value = expected_url + + response = self.client.get(self.url) + launch_url = response.json()['learning_assistant_launch_url'] + assert launch_url == expected_url + + expected_launch_data = Lti1p3LaunchData( + user_id=self.instructor.id, + user_role='instructor', + email=self.instructor.email, + config_id=1, + resource_link_id='-'.join(['1', str(self.course.id)]), + context_id=str(self.course.id), + context_type=['course_offering'], + context_title=self.course.display_name + ) + mock_lti_launch.assert_called_with(expected_launch_data) + @ddt.ddt class SequenceApiTestViews(MasqueradeMixin, BaseCoursewareTests): diff --git a/openedx/core/djangoapps/courseware_api/utils.py b/openedx/core/djangoapps/courseware_api/utils.py index c7f28937d5..1110e05a77 100644 --- a/openedx/core/djangoapps/courseware_api/utils.py +++ b/openedx/core/djangoapps/courseware_api/utils.py @@ -3,9 +3,15 @@ Courseware API Mixins. """ from babel.numbers import get_currency_symbol +from django.conf import settings + +from lti_consumer.api import get_lti_1p3_launch_start_url +from lti_consumer.data import Lti1p3LaunchData +from lti_consumer.models import LtiConfiguration from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.student.models import CourseEnrollmentCelebration, UserCelebration +from lms.djangoapps.courseware.access import get_user_role from lms.djangoapps.courseware.utils import can_show_verified_upgrade, verified_upgrade_deadline_link from openedx.features.course_duration_limits.access import get_user_course_expiration_date from openedx.features.discounts.applicability import can_show_streak_discount_coupon @@ -66,3 +72,41 @@ def serialize_upgrade_info(user, course_overview, enrollment): 'sku': mode.sku, 'upgrade_url': verified_upgrade_deadline_link(user, course_overview), } + + +def get_learning_assistant_launch_url(user, course_key, enrollment_object, overview): + """ + Return the launch URL for an LTI based learning assistant + """ + config_id = getattr(settings, 'LEARNING_ASSISTANT_CONFIG_ID', None) + if not config_id: + return None + + try: + lti_config = LtiConfiguration.objects.get(config_id=config_id) + except LtiConfiguration.DoesNotExist: + return None + + if lti_config.version != 'lti_1p3': + return None + + user_role = get_user_role(user, course_key) + if ( + (enrollment_object and enrollment_object.mode not in CourseMode.VERIFIED_MODES) + and (user_role != 'staff' or user_role != 'instructor') + ): + return None + + launch_data = Lti1p3LaunchData( + user_id=user.id, + user_role=user_role, + email=user.email, + config_id=config_id, + resource_link_id='-'.join([str(config_id), str(course_key)]), + context_id=str(course_key), + context_type=['course_offering'], + context_title=overview.display_name + ) + + lti_url = get_lti_1p3_launch_start_url(launch_data) + return lti_url diff --git a/openedx/core/djangoapps/courseware_api/views.py b/openedx/core/djangoapps/courseware_api/views.py index 3248f17a33..20c2b79180 100644 --- a/openedx/core/djangoapps/courseware_api/views.py +++ b/openedx/core/djangoapps/courseware_api/views.py @@ -47,7 +47,7 @@ from lms.djangoapps.gating.api import get_entrance_exam_score, get_entrance_exam from lms.djangoapps.grades.api import CourseGradeFactory from lms.djangoapps.verify_student.services import IDVerificationService from openedx.core.djangoapps.agreements.api import get_integrity_signature -from openedx.core.djangoapps.courseware_api.utils import get_celebrations_dict +from openedx.core.djangoapps.courseware_api.utils import get_celebrations_dict, get_learning_assistant_launch_url from openedx.core.djangoapps.programs.utils import ProgramProgressMeter from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin @@ -365,7 +365,13 @@ class CoursewareMeta: Returns a URL for the learning assistant LTI launch if applicable, otherwise None """ if learning_assistant_is_active(self.course_key): - return "" # TODO: replace empty string with LTI launch URL as part of MST-1975 + lti_url = get_learning_assistant_launch_url( + self.effective_user, + self.course_key, + self.enrollment_object, + self.overview, + ) + return lti_url return None