feat: Added Api to create/retrieve course live configurations (#30012)
This commit is contained in:
1
.github/workflows/unit-test-shards.json
vendored
1
.github/workflows/unit-test-shards.json
vendored
@@ -104,6 +104,7 @@
|
||||
"openedx/core/djangoapps/crawlers/",
|
||||
"openedx/core/djangoapps/credentials/",
|
||||
"openedx/core/djangoapps/credit/",
|
||||
"openedx/core/djangoapps/course_live/",
|
||||
"openedx/core/djangoapps/dark_lang/",
|
||||
"openedx/core/djangoapps/debug/",
|
||||
"openedx/core/djangoapps/demographics/",
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
Configure the django app
|
||||
"""
|
||||
from django.apps import AppConfig
|
||||
from edx_django_utils.plugins import PluginURLs
|
||||
|
||||
from openedx.core.djangoapps.plugins.constants import ProjectType
|
||||
|
||||
|
||||
class CourseLiveConfig(AppConfig):
|
||||
@@ -11,4 +14,17 @@ class CourseLiveConfig(AppConfig):
|
||||
|
||||
name = "openedx.core.djangoapps.course_live"
|
||||
|
||||
plugin_app = {}
|
||||
plugin_app = {
|
||||
PluginURLs.CONFIG: {
|
||||
ProjectType.LMS: {
|
||||
PluginURLs.NAMESPACE: '',
|
||||
PluginURLs.REGEX: r'^api/course_live/',
|
||||
PluginURLs.RELATIVE_PATH: 'urls',
|
||||
},
|
||||
ProjectType.CMS: {
|
||||
PluginURLs.NAMESPACE: '',
|
||||
PluginURLs.REGEX: r'^api/course_live/',
|
||||
PluginURLs.RELATIVE_PATH: 'urls',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ from edx_toggles.toggles import LegacyWaffleFlagNamespace
|
||||
|
||||
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
|
||||
|
||||
|
||||
WAFFLE_NAMESPACE = LegacyWaffleFlagNamespace(name='course_live')
|
||||
|
||||
# .. toggle_name: course_live.enable_course_live
|
||||
|
||||
@@ -3,10 +3,17 @@ Models course live integrations.
|
||||
"""
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from simple_history.models import HistoricalRecords
|
||||
from lti_consumer.models import LtiConfiguration
|
||||
from model_utils.models import TimeStampedModel
|
||||
from opaque_keys.edx.django.models import CourseKeyField
|
||||
from simple_history.models import HistoricalRecords
|
||||
|
||||
AVAILABLE_PROVIDERS = {
|
||||
'zoom': {
|
||||
'name': 'Zoom LTI PRO',
|
||||
'features': []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class CourseLiveConfiguration(TimeStampedModel):
|
||||
|
||||
28
openedx/core/djangoapps/course_live/permissions.py
Normal file
28
openedx/core/djangoapps/course_live/permissions.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
API library for Django REST Framework permissions-oriented workflows
|
||||
"""
|
||||
from rest_framework.permissions import BasePermission
|
||||
|
||||
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole, GlobalStaff
|
||||
from openedx.core.lib.api.view_utils import validate_course_key
|
||||
|
||||
|
||||
class IsStaffOrInstructor(BasePermission):
|
||||
"""
|
||||
Check if user is global or course staff
|
||||
|
||||
Permission that checks to see if the user is global staff, course
|
||||
staff, course admin,If none of those conditions are met, HTTP403 is returned.
|
||||
"""
|
||||
|
||||
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)
|
||||
)
|
||||
@@ -9,6 +9,7 @@ from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from openedx.core.djangoapps.course_apps.plugins import CourseApp
|
||||
from openedx.core.djangoapps.course_live.config.waffle import ENABLE_COURSE_LIVE
|
||||
|
||||
from .models import CourseLiveConfiguration
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
159
openedx/core/djangoapps/course_live/serializers.py
Normal file
159
openedx/core/djangoapps/course_live/serializers.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
Serializers for course live views.
|
||||
"""
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email
|
||||
from lti_consumer.models import LtiConfiguration
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import AVAILABLE_PROVIDERS, CourseLiveConfiguration
|
||||
|
||||
|
||||
class LtiSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serialize LtiConfiguration responses
|
||||
"""
|
||||
lti_config = serializers.JSONField()
|
||||
|
||||
class Meta:
|
||||
model = LtiConfiguration
|
||||
fields = [
|
||||
'lti_1p1_client_key',
|
||||
'lti_1p1_client_secret',
|
||||
'lti_1p1_launch_url',
|
||||
'version',
|
||||
'lti_config'
|
||||
]
|
||||
read_only = [
|
||||
'version'
|
||||
]
|
||||
|
||||
def validate_lti_config(self, value):
|
||||
"""
|
||||
Validates if lti_config contains all required data i.e. custom_instructor_email
|
||||
"""
|
||||
additional_parameters = value.get('additional_parameters', None)
|
||||
custom_instructor_email = additional_parameters.get('custom_instructor_email', None)
|
||||
if additional_parameters and custom_instructor_email:
|
||||
try:
|
||||
validate_email(custom_instructor_email)
|
||||
except ValidationError as error:
|
||||
raise serializers.ValidationError(f'{custom_instructor_email} is not valid email address') from error
|
||||
return value
|
||||
raise serializers.ValidationError('custom_instructor_email is required value in additional_parameters')
|
||||
|
||||
def create(self, validated_data):
|
||||
lti_config = validated_data.pop('lti_config', None)
|
||||
instance = LtiConfiguration()
|
||||
instance.version = 'lti_1p1'
|
||||
|
||||
for key, value in validated_data.items():
|
||||
if key in set(self.Meta.fields).difference(self.Meta.read_only):
|
||||
setattr(instance, key, value)
|
||||
|
||||
pii_sharing_allowed = self.context.get('pii_sharing_allowed', False)
|
||||
instance.lti_config = {
|
||||
"pii_share_username": pii_sharing_allowed,
|
||||
"pii_share_email": pii_sharing_allowed,
|
||||
"additional_parameters": lti_config['additional_parameters']
|
||||
}
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
def update(self, instance: LtiConfiguration, validated_data: dict) -> LtiConfiguration:
|
||||
"""
|
||||
Create/update a model-backed instance
|
||||
"""
|
||||
instance.config_store = LtiConfiguration.CONFIG_ON_DB
|
||||
lti_config = validated_data.pop('lti_config', None)
|
||||
if lti_config.get('additional_parameters', None):
|
||||
instance.lti_config['additional_parameters'] = lti_config.get('additional_parameters')
|
||||
|
||||
if validated_data:
|
||||
for key, value in validated_data.items():
|
||||
if key in self.Meta.fields:
|
||||
setattr(instance, key, value)
|
||||
|
||||
pii_sharing_allowed = self.context.get('pii_sharing_allowed', False)
|
||||
instance.pii_share_username = pii_sharing_allowed
|
||||
instance.pii_share_email = pii_sharing_allowed
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class CourseLiveConfigurationSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serialize configuration responses
|
||||
"""
|
||||
lti_configuration = LtiSerializer(many=False, read_only=False)
|
||||
|
||||
class Meta:
|
||||
model = CourseLiveConfiguration
|
||||
|
||||
fields = ['course_key', 'provider_type', 'enabled', 'lti_configuration']
|
||||
read_only_fields = ['course_key']
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
Create a new CourseLiveConfiguration entry in model
|
||||
"""
|
||||
lti_config = validated_data.pop('lti_configuration')
|
||||
instance = CourseLiveConfiguration()
|
||||
instance = self._update_course_live_instance(instance, validated_data)
|
||||
instance = self._update_lti(instance, lti_config)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
def update(self, instance: CourseLiveConfiguration, validated_data: dict) -> CourseLiveConfiguration:
|
||||
"""
|
||||
Update and save an existing instance
|
||||
"""
|
||||
lti_config = validated_data.pop('lti_configuration')
|
||||
instance = self._update_course_live_instance(instance, validated_data)
|
||||
instance = self._update_lti(instance, lti_config)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
def _update_course_live_instance(self, instance: CourseLiveConfiguration, data: dict) -> CourseLiveConfiguration:
|
||||
"""
|
||||
Adds data to courseLiveConfiguration model instance
|
||||
"""
|
||||
instance.course_key = self.context.get('course_id')
|
||||
instance.enabled = self.validated_data.get('enabled', False)
|
||||
|
||||
if data.get('provider_type') in AVAILABLE_PROVIDERS:
|
||||
instance.provider_type = data.get('provider_type')
|
||||
else:
|
||||
raise serializers.ValidationError(
|
||||
f'Provider type {data.get("provider_type")} does not exist')
|
||||
return instance
|
||||
|
||||
def to_representation(self, instance: CourseLiveConfiguration) -> dict:
|
||||
"""
|
||||
Serialize data into a dictionary, to be used as a response
|
||||
"""
|
||||
payload = super().to_representation(instance)
|
||||
payload.update({'pii_sharing_allowed': self.context['pii_sharing_allowed']})
|
||||
return payload
|
||||
|
||||
def _update_lti(
|
||||
self,
|
||||
instance: CourseLiveConfiguration,
|
||||
lti_config: dict,
|
||||
) -> CourseLiveConfiguration:
|
||||
"""
|
||||
Update LtiConfiguration
|
||||
"""
|
||||
|
||||
lti_serializer = LtiSerializer(
|
||||
instance.lti_configuration or None,
|
||||
data=lti_config,
|
||||
partial=True,
|
||||
context={
|
||||
'pii_sharing_allowed': self.context.get('pii_sharing_allowed', False),
|
||||
}
|
||||
)
|
||||
if lti_serializer.is_valid(raise_exception=True):
|
||||
lti_serializer.save()
|
||||
instance.lti_configuration = lti_serializer.instance
|
||||
return instance
|
||||
189
openedx/core/djangoapps/course_live/tests/test_views.py
Normal file
189
openedx/core/djangoapps/course_live/tests/test_views.py
Normal file
@@ -0,0 +1,189 @@
|
||||
"""
|
||||
Test for course live app views
|
||||
"""
|
||||
import json
|
||||
|
||||
from django.urls import reverse
|
||||
from lti_consumer.models import CourseAllowPIISharingInLTIFlag
|
||||
from rest_framework.test import APITestCase
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import CourseUserType, ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from ..models import AVAILABLE_PROVIDERS, CourseLiveConfiguration
|
||||
|
||||
|
||||
class TestCourseLiveConfigurationView(ModuleStoreTestCase, APITestCase):
|
||||
"""
|
||||
Unit tests for the CourseLiveConfigurationView.
|
||||
"""
|
||||
password = 'test'
|
||||
|
||||
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 API url. """
|
||||
return reverse(
|
||||
'course_live', kwargs={'course_id': str(self.course.id)}
|
||||
)
|
||||
|
||||
def _get(self):
|
||||
return self.client.get(self.url)
|
||||
|
||||
def _post(self, data):
|
||||
return self.client.post(self.url, data, format='json')
|
||||
|
||||
def create_course_live_config(self):
|
||||
"""
|
||||
creates a courseLiveConfiguration
|
||||
"""
|
||||
CourseAllowPIISharingInLTIFlag.objects.create(course_id=self.course.id, enabled=True)
|
||||
lti_config = {
|
||||
'lti_1p1_client_key': 'this_is_key',
|
||||
'lti_1p1_client_secret': 'this_is_secret',
|
||||
'lti_1p1_launch_url': 'example.com',
|
||||
'lti_config': {
|
||||
'additional_parameters': {
|
||||
'custom_instructor_email': "email@example.com"
|
||||
}
|
||||
},
|
||||
}
|
||||
course_live_config_data = {
|
||||
'enabled': True,
|
||||
'provider_type': 'zoom',
|
||||
'lti_configuration': lti_config
|
||||
}
|
||||
response = self._post(course_live_config_data)
|
||||
return lti_config, course_live_config_data, response
|
||||
|
||||
def test_pii_sharing_not_allowed(self):
|
||||
"""
|
||||
Test response if PII sharing is not allowed
|
||||
"""
|
||||
response = self._get()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
expected_data = {'pii_sharing_allowed': False, 'message': 'PII sharing is not allowed on this course'}
|
||||
self.assertEqual(response.data, expected_data)
|
||||
|
||||
def test_pii_sharing_is_allowed(self):
|
||||
"""
|
||||
Test response if PII sharing is allowed
|
||||
"""
|
||||
CourseAllowPIISharingInLTIFlag.objects.create(course_id=self.course.id, enabled=True)
|
||||
response = self._get()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
expected_data = {
|
||||
'enabled': False,
|
||||
'lti_configuration': {
|
||||
'lti_1p1_client_key': '',
|
||||
'lti_1p1_client_secret': '',
|
||||
'lti_1p1_launch_url': '',
|
||||
'lti_config': None,
|
||||
'version': None
|
||||
},
|
||||
'provider_type': ''
|
||||
}
|
||||
content = json.loads(response.content.decode('utf-8'))
|
||||
self.assertEqual(content, expected_data)
|
||||
|
||||
def test_create_configurations_data(self):
|
||||
"""
|
||||
Create and test courseLiveConfiguration data in database
|
||||
"""
|
||||
lti_config, data, response = self.create_course_live_config()
|
||||
course_live_configurations = CourseLiveConfiguration.get(self.course.id)
|
||||
lti_configuration = CourseLiveConfiguration.get(self.course.id).lti_configuration
|
||||
|
||||
self.assertEqual(self.course.id, course_live_configurations.course_key)
|
||||
self.assertEqual(data['enabled'], course_live_configurations.enabled)
|
||||
self.assertEqual(data['provider_type'], course_live_configurations.provider_type)
|
||||
|
||||
self.assertEqual(lti_config['lti_1p1_client_key'], lti_configuration.lti_1p1_client_key)
|
||||
self.assertEqual(lti_config['lti_1p1_client_secret'], lti_configuration.lti_1p1_client_secret)
|
||||
self.assertEqual(lti_config['lti_1p1_launch_url'], lti_configuration.lti_1p1_launch_url)
|
||||
self.assertEqual({
|
||||
'pii_share_username': True,
|
||||
'pii_share_email': True,
|
||||
'additional_parameters': {'custom_instructor_email': 'email@example.com'}
|
||||
}, lti_configuration.lti_config)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_create_configurations_response(self):
|
||||
"""
|
||||
Create and test POST request response data
|
||||
"""
|
||||
lti_config, course_live_config_data, response = self.create_course_live_config()
|
||||
expected_data = {
|
||||
'course_key': str(self.course.id),
|
||||
'enabled': True,
|
||||
'pii_sharing_allowed': True,
|
||||
'provider_type': 'zoom',
|
||||
'lti_configuration': {
|
||||
'lti_1p1_client_key': 'this_is_key',
|
||||
'lti_1p1_client_secret': 'this_is_secret',
|
||||
'lti_1p1_launch_url': 'example.com',
|
||||
'version': 'lti_1p1',
|
||||
'lti_config': {
|
||||
'pii_share_email': True,
|
||||
'pii_share_username': True,
|
||||
'additional_parameters': {
|
||||
'custom_instructor_email': 'email@example.com'
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
content = json.loads(response.content.decode('utf-8'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(content, expected_data)
|
||||
|
||||
def test_post_error_messages(self):
|
||||
"""
|
||||
Test all related validation messages are recived
|
||||
"""
|
||||
CourseAllowPIISharingInLTIFlag.objects.create(course_id=self.course.id, enabled=True)
|
||||
response = self._post({})
|
||||
content = json.loads(response.content.decode('utf-8'))
|
||||
expected_data = {
|
||||
'provider_type': ['This field is required.'],
|
||||
'lti_configuration': ['This field is required.']
|
||||
}
|
||||
self.assertEqual(content, expected_data)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
|
||||
class TestCourseLiveProvidersView(ModuleStoreTestCase, APITestCase):
|
||||
"""
|
||||
Tests for course live provider 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 live providers API url.
|
||||
"""
|
||||
return reverse(
|
||||
'live_providers', kwargs={'course_id': str(self.course.id)}
|
||||
)
|
||||
|
||||
def test_response_has_correct_data(self):
|
||||
expected_data = {
|
||||
'providers': {
|
||||
'active': '',
|
||||
'available': AVAILABLE_PROVIDERS
|
||||
}
|
||||
}
|
||||
response = self.client.get(self.url)
|
||||
content = json.loads(response.content.decode('utf-8'))
|
||||
self.assertEqual(content, expected_data)
|
||||
16
openedx/core/djangoapps/course_live/urls.py
Normal file
16
openedx/core/djangoapps/course_live/urls.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
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
|
||||
|
||||
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'),
|
||||
]
|
||||
191
openedx/core/djangoapps/course_live/views.py
Normal file
191
openedx/core/djangoapps/course_live/views.py
Normal file
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
View for course live app
|
||||
"""
|
||||
from typing import Dict
|
||||
|
||||
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 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 openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
|
||||
|
||||
from ...lib.api.view_utils import verify_course_exists
|
||||
from .models import AVAILABLE_PROVIDERS, CourseLiveConfiguration
|
||||
from .serializers import CourseLiveConfigurationSerializer
|
||||
|
||||
|
||||
class CourseLiveConfigurationView(APIView):
|
||||
"""
|
||||
View for configuring CourseLive settings.
|
||||
"""
|
||||
authentication_classes = (
|
||||
JwtAuthentication,
|
||||
BearerAuthenticationAllowInactiveUser,
|
||||
SessionAuthenticationAllowInactiveUser
|
||||
)
|
||||
permission_classes = (IsStaffOrInstructor,)
|
||||
|
||||
@apidocs.schema(
|
||||
parameters=[
|
||||
apidocs.path_parameter(
|
||||
'course_id',
|
||||
str,
|
||||
description="The course for which to get provider list",
|
||||
)
|
||||
],
|
||||
responses={
|
||||
200: CourseLiveConfigurationSerializer,
|
||||
401: "The requester is not authenticated.",
|
||||
403: "The requester cannot access the specified course.",
|
||||
404: "The requested course does not exist.",
|
||||
},
|
||||
)
|
||||
@ensure_valid_course_key
|
||||
@verify_course_exists()
|
||||
def get(self, request: Request, course_id: str) -> Response:
|
||||
"""
|
||||
Handle HTTP/GET requests
|
||||
"""
|
||||
pii_sharing_allowed = get_lti_pii_sharing_state_for_course(course_id)
|
||||
if not pii_sharing_allowed:
|
||||
return Response({
|
||||
"pii_sharing_allowed": pii_sharing_allowed,
|
||||
"message": "PII sharing is not allowed on this course"
|
||||
})
|
||||
|
||||
configuration = CourseLiveConfiguration.get(course_id)
|
||||
serializer = CourseLiveConfigurationSerializer(configuration, context={
|
||||
"pii_sharing_allowed": pii_sharing_allowed,
|
||||
})
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
@apidocs.schema(
|
||||
parameters=[
|
||||
apidocs.path_parameter(
|
||||
'course_id',
|
||||
str,
|
||||
description="The course for which to get provider list",
|
||||
),
|
||||
apidocs.path_parameter(
|
||||
'lti_1p1_client_key',
|
||||
str,
|
||||
description="The LTI provider's client key",
|
||||
),
|
||||
apidocs.path_parameter(
|
||||
'lti_1p1_client_secret',
|
||||
str,
|
||||
description="The LTI provider's client secretL",
|
||||
),
|
||||
apidocs.path_parameter(
|
||||
'lti_1p1_launch_url',
|
||||
str,
|
||||
description="The LTI provider's launch URL",
|
||||
),
|
||||
apidocs.path_parameter(
|
||||
'provider_type',
|
||||
str,
|
||||
description="The LTI provider's launch URL",
|
||||
),
|
||||
apidocs.parameter(
|
||||
'lti_config',
|
||||
apidocs.ParameterLocation.QUERY,
|
||||
object,
|
||||
description="The lti_config object with required additional parameters ",
|
||||
),
|
||||
],
|
||||
responses={
|
||||
200: CourseLiveConfigurationSerializer,
|
||||
400: "Required parameters are missing.",
|
||||
401: "The requester is not authenticated.",
|
||||
403: "The requester cannot access the specified course.",
|
||||
404: "The requested course does not exist.",
|
||||
},
|
||||
)
|
||||
@ensure_valid_course_key
|
||||
@verify_course_exists()
|
||||
def post(self, request, course_id: str) -> Response:
|
||||
"""
|
||||
Handle HTTP/POST requests
|
||||
"""
|
||||
pii_sharing_allowed = get_lti_pii_sharing_state_for_course(course_id)
|
||||
if not pii_sharing_allowed:
|
||||
return Response({
|
||||
"pii_sharing_allowed": pii_sharing_allowed,
|
||||
"message": "PII sharing is not allowed on this course"
|
||||
})
|
||||
|
||||
configuration = CourseLiveConfiguration.get(course_id)
|
||||
serializer = CourseLiveConfigurationSerializer(
|
||||
configuration,
|
||||
data=request.data,
|
||||
context={
|
||||
"pii_sharing_allowed": pii_sharing_allowed,
|
||||
"course_id": course_id
|
||||
}
|
||||
)
|
||||
if not serializer.is_valid():
|
||||
raise ValidationError(serializer.errors)
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class CourseLiveProvidersView(APIView):
|
||||
"""
|
||||
Read only view that lists details of LIVE providers available for a course.
|
||||
"""
|
||||
authentication_classes = (
|
||||
JwtAuthentication,
|
||||
BearerAuthenticationAllowInactiveUser,
|
||||
SessionAuthenticationAllowInactiveUser
|
||||
)
|
||||
permission_classes = (IsStaffOrInstructor,)
|
||||
|
||||
@apidocs.schema(
|
||||
parameters=[
|
||||
apidocs.string_parameter(
|
||||
'course_id',
|
||||
apidocs.ParameterLocation.PATH,
|
||||
description="The course for which to get provider list",
|
||||
)
|
||||
],
|
||||
responses={
|
||||
200: CourseLiveConfigurationSerializer,
|
||||
401: "The requester is not authenticated.",
|
||||
403: "The requester cannot access the specified course.",
|
||||
404: "The requested course does not exist.",
|
||||
},
|
||||
)
|
||||
@ensure_valid_course_key
|
||||
@verify_course_exists()
|
||||
def get(self, request, course_id: str, **_kwargs) -> Response:
|
||||
"""
|
||||
Handle HTTP/GET requests
|
||||
"""
|
||||
data = self.get_provider_data(course_id)
|
||||
return Response(data)
|
||||
|
||||
@staticmethod
|
||||
def get_provider_data(course_id: str) -> Dict:
|
||||
"""
|
||||
Get provider data for specified course
|
||||
Args:
|
||||
course_id (str): course key string
|
||||
|
||||
Returns:
|
||||
Dict: course Live providers
|
||||
"""
|
||||
configuration = CourseLiveConfiguration.get(course_id)
|
||||
return {
|
||||
"providers": {
|
||||
"active": configuration.provider_type if configuration else "",
|
||||
"available": AVAILABLE_PROVIDERS
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user