feat: add API to return course live iframe (#30179)
feat: add API to return course live iframe
This commit is contained in:
@@ -3,6 +3,7 @@ API library for Django REST Framework permissions-oriented workflows
|
||||
"""
|
||||
from rest_framework.permissions import BasePermission
|
||||
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole, GlobalStaff
|
||||
from openedx.core.lib.api.view_utils import validate_course_key
|
||||
|
||||
@@ -26,3 +27,22 @@ class IsStaffOrInstructor(BasePermission):
|
||||
CourseInstructorRole(course_key).has_user(request.user) or
|
||||
CourseStaffRole(course_key).has_user(request.user)
|
||||
)
|
||||
|
||||
|
||||
class IsEnrolledOrStaff(BasePermission):
|
||||
"""
|
||||
Check if user is enrolled in the course or staff
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
course_key_string = view.kwargs.get('course_id')
|
||||
course_key = validate_course_key(course_key_string)
|
||||
|
||||
if GlobalStaff().has_user(request.user):
|
||||
return True
|
||||
|
||||
return (
|
||||
CourseInstructorRole(course_key).has_user(request.user) or
|
||||
CourseStaffRole(course_key).has_user(request.user) or
|
||||
CourseEnrollment.is_enrolled(request.user, course_key)
|
||||
)
|
||||
|
||||
@@ -22,6 +22,11 @@ class CourseLiveTab(LtiCourseLaunchMixin, TabFragmentViewMixin, EnrolledTab):
|
||||
allow_multiple = False
|
||||
is_dynamic = True
|
||||
title = gettext_lazy("Live")
|
||||
ROLE_MAP = {
|
||||
'student': 'Student',
|
||||
'staff': 'Administrator',
|
||||
'instructor': 'Administrator',
|
||||
}
|
||||
|
||||
@request_cached()
|
||||
def _get_lti_config(self, course: CourseBlock) -> LtiConfiguration:
|
||||
|
||||
@@ -3,9 +3,11 @@ Test for course live app views
|
||||
"""
|
||||
import json
|
||||
|
||||
from django.test import RequestFactory
|
||||
from django.urls import reverse
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from lti_consumer.models import CourseAllowPIISharingInLTIFlag
|
||||
from lti_consumer.models import CourseAllowPIISharingInLTIFlag, LtiConfiguration
|
||||
from markupsafe import Markup
|
||||
from rest_framework.test import APITestCase
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import CourseUserType, ModuleStoreTestCase
|
||||
@@ -266,3 +268,80 @@ class TestCourseLiveProvidersView(ModuleStoreTestCase, APITestCase):
|
||||
response = self.client.get(self.url)
|
||||
content = json.loads(response.content.decode('utf-8'))
|
||||
self.assertEqual(content, expected_data)
|
||||
|
||||
|
||||
class TestCourseLiveIFrameView(ModuleStoreTestCase, APITestCase):
|
||||
"""
|
||||
Unit tests for course live iframe view
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
store = ModuleStoreEnum.Type.split
|
||||
self.course = CourseFactory.create(default_store=store)
|
||||
self.user = self.create_user_for_course(self.course, user_type=CourseUserType.GLOBAL_STAFF)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""
|
||||
Returns the course live iframe API url.
|
||||
"""
|
||||
return reverse(
|
||||
'live_iframe', kwargs={'course_id': str(self.course.id)}
|
||||
)
|
||||
|
||||
def test_api_returns_live_iframe(self):
|
||||
request = RequestFactory().get(self.url)
|
||||
request.user = self.user
|
||||
live_config = CourseLiveConfiguration.objects.create(
|
||||
course_key=self.course.id,
|
||||
enabled=True,
|
||||
provider_type="zoom",
|
||||
)
|
||||
live_config.lti_configuration = LtiConfiguration.objects.create(
|
||||
config_store=LtiConfiguration.CONFIG_ON_DB,
|
||||
lti_config={
|
||||
"pii_share_username": 'true',
|
||||
"pii_share_email": 'true',
|
||||
"additional_parameters": {
|
||||
"custom_instructor_email": "test@gmail.com"
|
||||
}
|
||||
},
|
||||
lti_1p1_launch_url='http://test.url',
|
||||
lti_1p1_client_key='test_client_key',
|
||||
lti_1p1_client_secret='test_client_secret',
|
||||
)
|
||||
live_config.save()
|
||||
with override_waffle_flag(ENABLE_COURSE_LIVE, True):
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsInstance(response.data['iframe'], Markup)
|
||||
self.assertIn('iframe', str(response.data['iframe']))
|
||||
|
||||
def test_non_authenticated_user(self):
|
||||
"""
|
||||
Verify that 401 is returned if user is not authenticated.
|
||||
"""
|
||||
self.client.logout()
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_not_enrolled_user(self):
|
||||
"""
|
||||
Verify that 403 is returned if user is not enrolled.
|
||||
"""
|
||||
self.user = self.create_user_for_course(self.course, user_type=CourseUserType.UNENROLLED)
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_live_configuration_disabled(self):
|
||||
"""
|
||||
Verify that proper error message is returned if live configuration is disabled.
|
||||
"""
|
||||
CourseLiveConfiguration.objects.create(
|
||||
course_key=self.course.id,
|
||||
enabled=False,
|
||||
provider_type="zoom",
|
||||
)
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.data['developer_message'], 'Course live is not enabled for this course.')
|
||||
|
||||
@@ -6,11 +6,17 @@ course live API URLs.
|
||||
from django.conf import settings
|
||||
from django.urls import re_path
|
||||
|
||||
from openedx.core.djangoapps.course_live.views import CourseLiveConfigurationView, CourseLiveProvidersView
|
||||
from openedx.core.djangoapps.course_live.views import (
|
||||
CourseLiveConfigurationView,
|
||||
CourseLiveIframeView,
|
||||
CourseLiveProvidersView
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
re_path(fr'^course/{settings.COURSE_ID_PATTERN}/?$',
|
||||
CourseLiveConfigurationView.as_view(), name='course_live'),
|
||||
re_path(fr'^providers/{settings.COURSE_ID_PATTERN}/?$',
|
||||
CourseLiveProvidersView.as_view(), name='live_providers'),
|
||||
re_path(fr'^iframe/{settings.COURSE_ID_PATTERN}/?$',
|
||||
CourseLiveIframeView.as_view(), name='live_iframe'),
|
||||
]
|
||||
|
||||
@@ -7,13 +7,17 @@ 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 lti_consumer.api import get_lti_pii_sharing_state_for_course
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from common.djangoapps.util.views import ensure_valid_course_key
|
||||
from openedx.core.djangoapps.course_live.permissions import IsStaffOrInstructor
|
||||
from lms.djangoapps.courseware.courses import get_course_with_access
|
||||
from openedx.core.djangoapps.course_live.permissions import IsEnrolledOrStaff, IsStaffOrInstructor
|
||||
from openedx.core.djangoapps.course_live.tab import CourseLiveTab
|
||||
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
|
||||
|
||||
from ...lib.api.view_utils import verify_course_exists
|
||||
@@ -207,3 +211,70 @@ class CourseLiveProvidersView(APIView):
|
||||
"available": AVAILABLE_PROVIDERS
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class CourseLiveIframeView(APIView):
|
||||
"""
|
||||
A view for retrieving course live iFrame.
|
||||
|
||||
Path: ``api/course_live/iframe/{course_id}/``
|
||||
|
||||
Accepts: [GET]
|
||||
|
||||
------------------------------------------------------------------------------------
|
||||
GET
|
||||
------------------------------------------------------------------------------------
|
||||
|
||||
**Returns**
|
||||
|
||||
* 200: OK - Contains a course live zoom iframe.
|
||||
* 401: The requesting user is not authenticated.
|
||||
* 403: The requesting user lacks access to the course.
|
||||
* 404: The requested course does not exist.
|
||||
|
||||
**Response**
|
||||
|
||||
In the case of a 200 response code, the response will be iframe HTML.
|
||||
|
||||
**Example**
|
||||
|
||||
{
|
||||
"iframe": "
|
||||
<iframe
|
||||
id='lti-tab-embed'
|
||||
style='width: 100%; min-height: 800px; border: none'
|
||||
srcdoc='{srcdoc}'
|
||||
>
|
||||
</iframe>
|
||||
",
|
||||
}
|
||||
|
||||
"""
|
||||
authentication_classes = (
|
||||
JwtAuthentication,
|
||||
BearerAuthenticationAllowInactiveUser,
|
||||
SessionAuthenticationAllowInactiveUser
|
||||
)
|
||||
permission_classes = (permissions.IsAuthenticated, IsEnrolledOrStaff)
|
||||
|
||||
@ensure_valid_course_key
|
||||
@verify_course_exists()
|
||||
def get(self, request, course_id: str, **_kwargs) -> Response:
|
||||
"""
|
||||
Handle HTTP/GET requests
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course_live_tab = CourseLiveTab({})
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
|
||||
if not course_live_tab.is_enabled(course, request.user):
|
||||
error_data = {
|
||||
"developer_message": "Course live is not enabled for this course."
|
||||
}
|
||||
return Response(error_data, status=status.HTTP_200_OK)
|
||||
|
||||
iframe = course_live_tab.render_to_fragment(request, course)
|
||||
data = {
|
||||
"iframe": iframe.content
|
||||
}
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
Reference in New Issue
Block a user