diff --git a/openedx/core/djangoapps/course_live/apps.py b/openedx/core/djangoapps/course_live/apps.py index 81932e0a9b89..0098756b9667 100644 --- a/openedx/core/djangoapps/course_live/apps.py +++ b/openedx/core/djangoapps/course_live/apps.py @@ -26,5 +26,15 @@ class CourseLiveConfig(AppConfig): PluginURLs.REGEX: r'^api/course_live/', PluginURLs.RELATIVE_PATH: 'urls', }, + 'settings_config': { + 'lms.djangoapp': { + 'common': {'relative_path': 'settings.common'}, + 'production': {'relative_path': 'settings.production'}, + }, + 'cms.djangoapp': { + 'common': {'relative_path': 'settings.common'}, + 'production': {'relative_path': 'settings.production'}, + }, + }, } } diff --git a/openedx/core/djangoapps/course_live/providers.py b/openedx/core/djangoapps/course_live/providers.py index 1d6b1e808edb..9c1955e61e7b 100644 --- a/openedx/core/djangoapps/course_live/providers.py +++ b/openedx/core/djangoapps/course_live/providers.py @@ -81,7 +81,7 @@ def has_valid_global_keys(self) -> bool: raise NotImplementedError() -class Zoom(LiveProvider): +class Zoom(LiveProvider, HasGlobalCredentials): """ Zoom LTI PRO live provider """ @@ -92,9 +92,43 @@ class Zoom(LiveProvider): ] @property - def is_enabled(self): + def has_free_tier(self) -> bool: + """ + Check if free tier is enabled by checking for valid keys + """ + return self.has_valid_global_keys() + + @property + def is_enabled(self) -> bool: return True + @staticmethod + def get_global_keys() -> Dict: + """ + Get keys from settings + """ + try: + COURSE_LIVE_GLOBAL_CREDENTIALS = { + "KEY": settings.ZOOM_BUTTON_GLOBAL_KEY, + "SECRET": settings.ZOOM_BUTTON_GLOBAL_SECRET, + "URL": settings.ZOOM_BUTTON_GLOBAL_URL, + } + return COURSE_LIVE_GLOBAL_CREDENTIALS + except AttributeError: + return {} + + def has_valid_global_keys(self) -> bool: + """ + Check if keys are valid and not None + """ + credentials = self.get_global_keys() + if credentials: + self.key = credentials.get('KEY') + self.secret = credentials.get('SECRET') + self.url = credentials.get('URL') + return bool(self.key and self.secret and self.url) + return False + class BigBlueButton(LiveProvider, HasGlobalCredentials): """ diff --git a/openedx/core/djangoapps/course_live/settings/__init__.py b/openedx/core/djangoapps/course_live/settings/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/core/djangoapps/course_live/settings/common.py b/openedx/core/djangoapps/course_live/settings/common.py new file mode 100644 index 000000000000..ff716aa1b7c8 --- /dev/null +++ b/openedx/core/djangoapps/course_live/settings/common.py @@ -0,0 +1,19 @@ +""" +Common settings for Live Video Conferencing Tools. +""" + + +def plugin_settings(settings): + """ + Common settings for Live Video Conferencing Tools + Set these variables in the Tutor Config or lms.yml for local testing + ZOOM_BUTTON_GLOBAL_KEY: + ZOOM_BUTTON_GLOBAL_SECRET: + ZOOM_BUTTON_GLOBAL_URL: + ZOOM_INSTRUCTOR_EMAIL: + """ + # zoom settings + settings.ZOOM_BUTTON_GLOBAL_KEY = "" + settings.ZOOM_BUTTON_GLOBAL_SECRET = "" + settings.ZOOM_BUTTON_GLOBAL_URL = "" + settings.ZOOM_INSTRUCTOR_EMAIL = "" diff --git a/openedx/core/djangoapps/course_live/settings/production.py b/openedx/core/djangoapps/course_live/settings/production.py new file mode 100644 index 000000000000..27429f393425 --- /dev/null +++ b/openedx/core/djangoapps/course_live/settings/production.py @@ -0,0 +1,23 @@ +""" +Production settings for Live Video Conferencing Tools. +""" + + +def plugin_settings(settings): + """ + Production settings for Live Video Conferencing Tools + """ + + # zoom settings + settings.ZOOM_BUTTON_GLOBAL_KEY = settings.ENV_TOKENS.get( + "ZOOM_BUTTON_GLOBAL_KEY", settings.ZOOM_BUTTON_GLOBAL_KEY + ) + settings.ZOOM_BUTTON_GLOBAL_SECRET = settings.ENV_TOKENS.get( + "ZOOM_BUTTON_GLOBAL_SECRET", settings.ZOOM_BUTTON_GLOBAL_SECRET + ) + settings.ZOOM_BUTTON_GLOBAL_URL = settings.ENV_TOKENS.get( + "ZOOM_BUTTON_GLOBAL_URL", settings.ZOOM_BUTTON_GLOBAL_URL + ) + settings.ZOOM_INSTRUCTOR_EMAIL = settings.ENV_TOKENS.get( + "ZOOM_INSTRUCTOR_EMAIL", settings.ZOOM_INSTRUCTOR_EMAIL + ) diff --git a/openedx/core/djangoapps/course_live/urls.py b/openedx/core/djangoapps/course_live/urls.py index e8fa2fc1d500..c806aa68d73e 100644 --- a/openedx/core/djangoapps/course_live/urls.py +++ b/openedx/core/djangoapps/course_live/urls.py @@ -9,7 +9,8 @@ from openedx.core.djangoapps.course_live.views import ( CourseLiveConfigurationView, CourseLiveIframeView, - CourseLiveProvidersView + CourseLiveProvidersView, + CourseLiveZoomView, ) urlpatterns = [ @@ -19,4 +20,6 @@ CourseLiveProvidersView.as_view(), name='live_providers'), re_path(fr'^iframe/{settings.COURSE_ID_PATTERN}/?$', CourseLiveIframeView.as_view(), name='live_iframe'), + re_path(rf"^configure_zoom/{settings.COURSE_ID_PATTERN}/?$", + CourseLiveZoomView.as_view(), name="zoom"), ] diff --git a/openedx/core/djangoapps/course_live/views.py b/openedx/core/djangoapps/course_live/views.py index d148e9301897..f6e185d35162 100644 --- a/openedx/core/djangoapps/course_live/views.py +++ b/openedx/core/djangoapps/course_live/views.py @@ -3,9 +3,12 @@ """ from typing import Dict +from django.conf import settings import edx_api_doc_tools as apidocs from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication -from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser +from edx_rest_framework_extensions.auth.session.authentication import ( + SessionAuthenticationAllowInactiveUser, +) from lti_consumer.api import get_lti_pii_sharing_state_for_course from opaque_keys.edx.keys import CourseKey from rest_framework import permissions, status @@ -25,6 +28,27 @@ from .serializers import CourseLiveConfigurationSerializer +def is_zoom_creds_global(serialized_data): + """ + returns True or False if zoom creds are global or not respoectively + """ + if serialized_data.get("provider_type", "") == "zoom": + key = serialized_data.get("lti_configuration", {}).get("lti_1p1_client_key", "") + url = serialized_data.get("lti_configuration", {}).get("lti_1p1_launch_url", "") + email = ( + serialized_data.get("lti_configuration", {}) + .get("lti_config", "") + .get("additional_parameters", {}) + .get("custom_instructor_email", "") + ) + global_key = settings.ZOOM_BUTTON_GLOBAL_KEY + global_url = settings.ZOOM_BUTTON_GLOBAL_URL + global_email = settings.ZOOM_INSTRUCTOR_EMAIL + if key == global_key and url == global_url and email == global_email: + return True + return False + + class CourseLiveConfigurationView(APIView): """ View for configuring CourseLive settings. @@ -62,8 +86,12 @@ def get(self, request: Request, course_id: str) -> Response: "pii_sharing_allowed": get_lti_pii_sharing_state_for_course(course_id), "course_id": course_id }) + serialized_data = serializer.data + serialized_data["global_zoom_creds_enabled"] = is_zoom_creds_global( + serialized_data + ) - return Response(serializer.data) + return Response(serialized_data) @apidocs.schema( parameters=[ @@ -136,7 +164,12 @@ def post(self, request, course_id: str) -> Response: if not serializer.is_valid(): raise ValidationError(serializer.errors) serializer.save() - return Response(serializer.data) + serialized_data = serializer.data + serialized_data["global_zoom_creds_enabled"] = is_zoom_creds_global( + serialized_data + ) + + return Response(serialized_data) class CourseLiveProvidersView(APIView): @@ -277,3 +310,72 @@ def get(self, request, course_id: str, **_kwargs) -> Response: "iframe": iframe.content } return Response(data, status=status.HTTP_200_OK) + + +class CourseLiveZoomView(APIView): + """ + View for configuring Zoom Global Credentials settings. + """ + @ensure_valid_course_key + @verify_course_exists() + def post(self, request, course_id: str) -> Response: + """ + Handle HTTP/POST requests + """ + request.data["enabled"] = True + request.data["lti_configuration"] = { + "lti_1p1_client_key": settings.ZOOM_BUTTON_GLOBAL_KEY, + "lti_1p1_client_secret": settings.ZOOM_BUTTON_GLOBAL_SECRET, + "lti_1p1_launch_url": settings.ZOOM_BUTTON_GLOBAL_URL, + "lti_config": { + "additional_parameters": { + "custom_instructor_email": settings.ZOOM_INSTRUCTOR_EMAIL, + } + }, + "version": "lti_1p1", + } + request.data["provider_type"] = "zoom" + request.data["pii_sharing_allowed"] = True + request.data["free_tier"] = False + + pii_sharing_allowed = get_lti_pii_sharing_state_for_course(course_id) + provider = ( + ProviderManager() + .get_enabled_providers() + .get(request.data.get("provider_type")) + ) + + if not pii_sharing_allowed and provider.requires_pii_sharing(): + return Response( + { + "pii_sharing_allowed": pii_sharing_allowed, + "message": "PII sharing is not allowed on this course", + } + ) + if ( + provider + and not provider.additional_parameters + and request.data.get("lti_configuration", False) + ): + # Add empty lti config if none is provided in case additional params are not required + request.data["lti_configuration"]["lti_config"] = { + "additional_parameters": {} + } + configuration = CourseLiveConfiguration.objects.filter( + course_key=course_id + ).last() + serializer = CourseLiveConfigurationSerializer( + configuration, + data=request.data, + context={ + "pii_sharing_allowed": pii_sharing_allowed, + "course_id": course_id, + "provider": provider, + }, + ) + if not serializer.is_valid(): + raise ValidationError(serializer.errors) + serializer.save() + serialized_data = serializer.data + serialized_data["global_zoom_creds_enabled"] = True + return Response(serialized_data)