feat: Added new discussion providers and features
refactor: Fixed linter issues refactor: Fixed styling issues refactor: Code refactor and removed conflicts refactor: Replaced feature dict with enum
This commit is contained in:
@@ -3,6 +3,7 @@ Provide django models to back the discussions app
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from enum import Enum
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
@@ -21,18 +22,118 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOvervi
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DEFAULT_PROVIDER_TYPE = 'legacy'
|
||||
|
||||
|
||||
class Features(Enum):
|
||||
"""
|
||||
Features to be used/mapped in discussion providers
|
||||
"""
|
||||
ADVANCED_IN_CONTEXT_DISCUSSION = 'advanced_in_context_discussion'
|
||||
ANONYMOUS_POSTING = 'anonymous_posting'
|
||||
AUTOMATIC_LEARNER_ENROLLMENT = 'automatic_learner_enrollment'
|
||||
BLACKOUT_DISCUSSION_DATES = 'blackout_discussion_dates'
|
||||
COMMUNITY_TA_SUPPORT = 'community_ta_support'
|
||||
COURSE_COHORT_SUPPORT = 'course_cohort_support'
|
||||
DIRECT_MESSAGES_FROM_INSTRUCTORS = 'direct_messages_from_instructors'
|
||||
DISCUSSION_PAGE = 'discussion-page'
|
||||
DISCUSSION_CONTENT_PROMPTS = 'discussion_content_prompts'
|
||||
EMAIL_NOTIFICATIONS = 'email_notifications'
|
||||
EMBEDDED_COURSE_SECTIONS = 'embedded-course-sections'
|
||||
GRADED_DISCUSSIONS = 'graded_discussions'
|
||||
IN_PLATFORM_NOTIFICATIONS = 'in_platform_notifications'
|
||||
INTERNATIONALIZATION_SUPPORT = 'internationalization_support'
|
||||
LTI = 'lti'
|
||||
LTI_ADVANCED_SHARING_MODE = 'lti_advanced_sharing_mode'
|
||||
LTI_BASIC_CONFIGURATION = 'lti_basic_configuration'
|
||||
PRIMARY_DISCUSSION_APP_EXPERIENCE = 'primary_discussion_app_experience'
|
||||
QUESTION_DISCUSSION_SUPPORT = 'question_discussion_support'
|
||||
REPORT_FLAG_CONTENT_TO_MODERATORS = 'report/flag_content_to_moderators'
|
||||
RESEARCH_DATA_EVENTS = 'research_data_events'
|
||||
SIMPLIFIED_IN_CONTEXT_DISCUSSION = 'simplified_in_context_discussion'
|
||||
USER_MENTIONS = 'user_mentions'
|
||||
WCAG_2_1 = 'wcag-2.1'
|
||||
WCAG_2_0_SUPPORT = 'wcag_2_0_support'
|
||||
|
||||
PROVIDER_FEATURE_MAP = {
|
||||
'legacy': [
|
||||
'discussion-page',
|
||||
'embedded-course-sections',
|
||||
'wcag-2.1',
|
||||
Features.DISCUSSION_PAGE.value,
|
||||
Features.WCAG_2_1.value,
|
||||
Features.AUTOMATIC_LEARNER_ENROLLMENT.value,
|
||||
Features.WCAG_2_0_SUPPORT.value,
|
||||
Features.INTERNATIONALIZATION_SUPPORT.value,
|
||||
Features.ANONYMOUS_POSTING.value,
|
||||
Features.REPORT_FLAG_CONTENT_TO_MODERATORS.value,
|
||||
Features.QUESTION_DISCUSSION_SUPPORT.value,
|
||||
Features.COMMUNITY_TA_SUPPORT.value,
|
||||
Features.BLACKOUT_DISCUSSION_DATES.value,
|
||||
Features.COURSE_COHORT_SUPPORT.value,
|
||||
Features.RESEARCH_DATA_EVENTS.value,
|
||||
],
|
||||
'piazza': [
|
||||
'discussion-page',
|
||||
'lti',
|
||||
Features.DISCUSSION_PAGE.value,
|
||||
Features.LTI.value,
|
||||
Features.WCAG_2_0_SUPPORT.value,
|
||||
Features.ANONYMOUS_POSTING.value,
|
||||
Features.REPORT_FLAG_CONTENT_TO_MODERATORS.value,
|
||||
Features.QUESTION_DISCUSSION_SUPPORT.value,
|
||||
Features.COMMUNITY_TA_SUPPORT.value,
|
||||
Features.EMAIL_NOTIFICATIONS.value,
|
||||
Features.BLACKOUT_DISCUSSION_DATES.value,
|
||||
Features.DISCUSSION_CONTENT_PROMPTS.value,
|
||||
Features.DIRECT_MESSAGES_FROM_INSTRUCTORS.value,
|
||||
Features.USER_MENTIONS.value,
|
||||
],
|
||||
'edx-next': [
|
||||
Features.AUTOMATIC_LEARNER_ENROLLMENT.value,
|
||||
Features.WCAG_2_0_SUPPORT.value,
|
||||
Features.INTERNATIONALIZATION_SUPPORT.value,
|
||||
Features.ANONYMOUS_POSTING.value,
|
||||
Features.REPORT_FLAG_CONTENT_TO_MODERATORS.value,
|
||||
Features.QUESTION_DISCUSSION_SUPPORT.value,
|
||||
Features.COMMUNITY_TA_SUPPORT.value,
|
||||
Features.EMAIL_NOTIFICATIONS.value,
|
||||
Features.BLACKOUT_DISCUSSION_DATES.value,
|
||||
Features.SIMPLIFIED_IN_CONTEXT_DISCUSSION.value,
|
||||
Features.ADVANCED_IN_CONTEXT_DISCUSSION.value,
|
||||
Features.COURSE_COHORT_SUPPORT.value,
|
||||
Features.RESEARCH_DATA_EVENTS.value,
|
||||
Features.DISCUSSION_CONTENT_PROMPTS.value,
|
||||
Features.GRADED_DISCUSSIONS.value,
|
||||
],
|
||||
'yellowdig': [
|
||||
Features.WCAG_2_0_SUPPORT.value,
|
||||
Features.ANONYMOUS_POSTING.value,
|
||||
Features.REPORT_FLAG_CONTENT_TO_MODERATORS.value,
|
||||
Features.QUESTION_DISCUSSION_SUPPORT.value,
|
||||
Features.COMMUNITY_TA_SUPPORT.value,
|
||||
Features.EMAIL_NOTIFICATIONS.value,
|
||||
Features.RESEARCH_DATA_EVENTS.value,
|
||||
Features.IN_PLATFORM_NOTIFICATIONS.value,
|
||||
Features.GRADED_DISCUSSIONS.value,
|
||||
Features.DIRECT_MESSAGES_FROM_INSTRUCTORS.value,
|
||||
Features.USER_MENTIONS.value,
|
||||
],
|
||||
'inscribe': [
|
||||
Features.PRIMARY_DISCUSSION_APP_EXPERIENCE.value,
|
||||
Features.LTI_BASIC_CONFIGURATION.value,
|
||||
],
|
||||
'discourse': [
|
||||
Features.PRIMARY_DISCUSSION_APP_EXPERIENCE.value,
|
||||
Features.LTI_BASIC_CONFIGURATION.value,
|
||||
Features.LTI_ADVANCED_SHARING_MODE.value,
|
||||
],
|
||||
'ed-discuss': [
|
||||
Features.PRIMARY_DISCUSSION_APP_EXPERIENCE.value,
|
||||
Features.LTI_BASIC_CONFIGURATION.value,
|
||||
Features.WCAG_2_0_SUPPORT.value,
|
||||
Features.INTERNATIONALIZATION_SUPPORT.value,
|
||||
Features.ANONYMOUS_POSTING.value,
|
||||
Features.REPORT_FLAG_CONTENT_TO_MODERATORS.value,
|
||||
Features.QUESTION_DISCUSSION_SUPPORT.value,
|
||||
Features.COMMUNITY_TA_SUPPORT.value,
|
||||
Features.EMAIL_NOTIFICATIONS.value,
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -223,6 +324,7 @@ class DiscussionsConfiguration(TimeStampedModel):
|
||||
provider_type=DEFAULT_PROVIDER_TYPE,
|
||||
)
|
||||
return configuration
|
||||
|
||||
# pylint: enable=undefined-variable
|
||||
|
||||
@property
|
||||
|
||||
279
openedx/core/djangoapps/discussions/serializers.py
Normal file
279
openedx/core/djangoapps/discussions/serializers.py
Normal file
@@ -0,0 +1,279 @@
|
||||
"""
|
||||
Serializers for Discussion views.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from lti_consumer.models import LtiConfiguration
|
||||
|
||||
from openedx.core.lib.courses import get_course_by_id
|
||||
from openedx.core.djangoapps.django_comment_common.models import CourseDiscussionSettings
|
||||
from lms.djangoapps.discussion.rest_api.serializers import DiscussionSettingsSerializer
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from .models import DiscussionsConfiguration, DEFAULT_PROVIDER_TYPE, Features, PROVIDER_FEATURE_MAP
|
||||
|
||||
|
||||
class LtiSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serialize LtiConfiguration responses
|
||||
"""
|
||||
class Meta:
|
||||
model = LtiConfiguration
|
||||
fields = [
|
||||
'lti_1p1_client_key',
|
||||
'lti_1p1_client_secret',
|
||||
'lti_1p1_launch_url',
|
||||
'version',
|
||||
]
|
||||
|
||||
def to_internal_value(self, data: dict) -> dict:
|
||||
"""
|
||||
Transform the incoming primitive data into a native value
|
||||
"""
|
||||
data = data or {}
|
||||
payload = {
|
||||
key: value
|
||||
for key, value in data.items()
|
||||
if key in self.Meta.fields
|
||||
}
|
||||
return payload
|
||||
|
||||
def update(self, instance: LtiConfiguration, validated_data: dict) -> LtiConfiguration:
|
||||
"""
|
||||
Create/update a model-backed instance
|
||||
"""
|
||||
instance = instance or LtiConfiguration()
|
||||
instance.config_store = LtiConfiguration.CONFIG_ON_DB
|
||||
if validated_data:
|
||||
for key, value in validated_data.items():
|
||||
if key in self.Meta.fields:
|
||||
setattr(instance, key, value)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class LegacySettingsSerializer(serializers.BaseSerializer):
|
||||
"""
|
||||
Serialize legacy discussions settings
|
||||
"""
|
||||
class Meta:
|
||||
fields = [
|
||||
'allow_anonymous',
|
||||
'allow_anonymous_to_peers',
|
||||
'discussion_blackouts',
|
||||
'discussion_topics',
|
||||
# The following fields are deprecated;
|
||||
# they technically still exist in Studio (so we mention them here),
|
||||
# but they are not supported in the new experience:
|
||||
# 'discussion_link',
|
||||
# 'discussion_sort_alpha',
|
||||
]
|
||||
fields_cohorts = [
|
||||
'always_divide_inline_discussions',
|
||||
'divided_course_wide_discussions',
|
||||
'divided_inline_discussions',
|
||||
'division_scheme',
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
We do not need this.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def to_internal_value(self, data: dict) -> dict:
|
||||
"""
|
||||
Transform the incoming primitive data into a native value
|
||||
"""
|
||||
if not isinstance(data.get('allow_anonymous', False), bool):
|
||||
raise serializers.ValidationError('Wrong type for allow_anonymous')
|
||||
if not isinstance(data.get('allow_anonymous_to_peers', False), bool):
|
||||
raise serializers.ValidationError('Wrong type for allow_anonymous_to_peers')
|
||||
if not isinstance(data.get('discussion_blackouts', []), list):
|
||||
raise serializers.ValidationError('Wrong type for discussion_blackouts')
|
||||
if not isinstance(data.get('discussion_topics', {}), dict):
|
||||
raise serializers.ValidationError('Wrong type for discussion_topics')
|
||||
return data
|
||||
|
||||
def to_representation(self, instance) -> dict:
|
||||
"""
|
||||
Serialize data into a dictionary, to be used as a response
|
||||
"""
|
||||
settings = {
|
||||
field.name: field.read_json(instance)
|
||||
for field in instance.fields.values()
|
||||
if field.name in self.Meta.fields
|
||||
}
|
||||
discussion_settings = CourseDiscussionSettings.get(instance.id)
|
||||
serializer = DiscussionSettingsSerializer(
|
||||
discussion_settings,
|
||||
context={
|
||||
'course': instance,
|
||||
'settings': discussion_settings,
|
||||
},
|
||||
partial=True,
|
||||
)
|
||||
settings.update({
|
||||
key: value
|
||||
for key, value in serializer.data.items()
|
||||
if key != 'id'
|
||||
})
|
||||
return settings
|
||||
|
||||
def update(self, instance, validated_data: dict):
|
||||
"""
|
||||
Update and save an existing instance
|
||||
"""
|
||||
save = False
|
||||
cohort_settings = {}
|
||||
for field, value in validated_data.items():
|
||||
if field in self.Meta.fields:
|
||||
setattr(instance, field, value)
|
||||
save = True
|
||||
elif field in self.Meta.fields_cohorts:
|
||||
cohort_settings[field] = value
|
||||
if cohort_settings:
|
||||
discussion_settings = CourseDiscussionSettings.get(instance.id)
|
||||
serializer = DiscussionSettingsSerializer(
|
||||
discussion_settings,
|
||||
context={
|
||||
'course': instance,
|
||||
'settings': discussion_settings,
|
||||
},
|
||||
data=cohort_settings,
|
||||
partial=True,
|
||||
)
|
||||
if serializer.is_valid(raise_exception=True):
|
||||
serializer.save()
|
||||
if save:
|
||||
modulestore().update_item(instance, self.context['user_id'])
|
||||
return instance
|
||||
|
||||
|
||||
class DiscussionsConfigurationSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serialize configuration responses
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = DiscussionsConfiguration
|
||||
fields = [
|
||||
'enabled',
|
||||
'provider_type',
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
We do not need this.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def to_internal_value(self, data: dict) -> dict:
|
||||
"""
|
||||
Transform the *incoming* primitive data into a native value.
|
||||
"""
|
||||
payload = {
|
||||
'context_key': data.get('course_key', ''),
|
||||
'enabled': data.get('enabled', False),
|
||||
'lti_configuration': data.get('lti_configuration', {}),
|
||||
'plugin_configuration': data.get('plugin_configuration', {}),
|
||||
'provider_type': data.get('provider_type', DEFAULT_PROVIDER_TYPE),
|
||||
}
|
||||
return payload
|
||||
|
||||
def to_representation(self, instance: DiscussionsConfiguration) -> dict:
|
||||
"""
|
||||
Serialize data into a dictionary, to be used as a response
|
||||
"""
|
||||
payload = super().to_representation(instance)
|
||||
lti_configuration_data = {}
|
||||
supports_lti = instance.supports('lti')
|
||||
if supports_lti:
|
||||
lti_configuration = LtiSerializer(instance.lti_configuration)
|
||||
lti_configuration_data = lti_configuration.data
|
||||
provider_type = instance.provider_type or DEFAULT_PROVIDER_TYPE
|
||||
plugin_configuration = instance.plugin_configuration
|
||||
if provider_type == 'legacy':
|
||||
course_key = instance.context_key
|
||||
course = get_course_by_id(course_key)
|
||||
legacy_settings = LegacySettingsSerializer(
|
||||
course,
|
||||
data=plugin_configuration,
|
||||
)
|
||||
if legacy_settings.is_valid(raise_exception=True):
|
||||
plugin_configuration = legacy_settings.data
|
||||
features_list = [feature.value for feature in Features]
|
||||
payload.update({
|
||||
'features': features_list,
|
||||
'lti_configuration': lti_configuration_data,
|
||||
'plugin_configuration': plugin_configuration,
|
||||
'providers': {
|
||||
'active': provider_type or DEFAULT_PROVIDER_TYPE,
|
||||
'available': PROVIDER_FEATURE_MAP,
|
||||
},
|
||||
})
|
||||
return payload
|
||||
|
||||
def update(self, instance: DiscussionsConfiguration, validated_data: dict) -> DiscussionsConfiguration:
|
||||
"""
|
||||
Update and save an existing instance
|
||||
"""
|
||||
for key in self.Meta.fields:
|
||||
value = validated_data.get(key)
|
||||
if value is not None:
|
||||
setattr(instance, key, value)
|
||||
# _update_* helpers assume `enabled` and `provider_type`
|
||||
# have already been set
|
||||
instance = self._update_lti(instance, validated_data)
|
||||
instance = self._update_plugin_configuration(instance, validated_data)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
def _update_lti(self, instance: DiscussionsConfiguration, validated_data: dict) -> DiscussionsConfiguration:
|
||||
"""
|
||||
Update LtiConfiguration
|
||||
"""
|
||||
lti_configuration_data = validated_data.get('lti_configuration')
|
||||
supports_lti = instance.supports('lti')
|
||||
if not supports_lti:
|
||||
instance.lti_configuration = None
|
||||
elif lti_configuration_data:
|
||||
lti_configuration = instance.lti_configuration or LtiConfiguration()
|
||||
lti_serializer = LtiSerializer(
|
||||
lti_configuration,
|
||||
data=lti_configuration_data,
|
||||
partial=True,
|
||||
)
|
||||
if lti_serializer.is_valid(raise_exception=True):
|
||||
lti_serializer.save()
|
||||
instance.lti_configuration = lti_configuration
|
||||
return instance
|
||||
|
||||
def _update_plugin_configuration(
|
||||
self,
|
||||
instance: DiscussionsConfiguration,
|
||||
validated_data: dict,
|
||||
) -> DiscussionsConfiguration:
|
||||
"""
|
||||
Create/update legacy provider settings
|
||||
"""
|
||||
updated_provider_type = validated_data.get('provider_type') or instance.provider_type
|
||||
will_support_legacy = bool(
|
||||
updated_provider_type == 'legacy'
|
||||
)
|
||||
if will_support_legacy:
|
||||
course_key = instance.context_key
|
||||
course = get_course_by_id(course_key)
|
||||
legacy_settings = LegacySettingsSerializer(
|
||||
course,
|
||||
context={
|
||||
'user_id': self.context['user_id'],
|
||||
},
|
||||
data=validated_data.get('plugin_configuration', {}),
|
||||
)
|
||||
if legacy_settings.is_valid(raise_exception=True):
|
||||
legacy_settings.save()
|
||||
instance.plugin_configuration = {}
|
||||
else:
|
||||
instance.plugin_configuration = validated_data.get('plugin_configuration') or {}
|
||||
return instance
|
||||
@@ -3,24 +3,19 @@ Handle view-logic for the djangoapp
|
||||
"""
|
||||
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
||||
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
|
||||
from lti_consumer.models import LtiConfiguration
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys import InvalidKeyError
|
||||
from rest_framework import serializers
|
||||
from rest_framework.permissions import BasePermission
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.djangoapps.student.roles import CourseStaffRole
|
||||
from lms.djangoapps.discussion.rest_api.serializers import DiscussionSettingsSerializer
|
||||
from openedx.core.djangoapps.django_comment_common.models import CourseDiscussionSettings
|
||||
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
|
||||
from openedx.core.lib.courses import get_course_by_id
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from .models import DEFAULT_PROVIDER_TYPE
|
||||
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
|
||||
from .models import DiscussionsConfiguration
|
||||
from .models import PROVIDER_FEATURE_MAP
|
||||
from .serializers import DiscussionsConfigurationSerializer
|
||||
|
||||
|
||||
class IsStaff(BasePermission):
|
||||
@@ -30,6 +25,7 @@ class IsStaff(BasePermission):
|
||||
We create our own copy of this because other versions of this check
|
||||
allow access to additional user roles.
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""
|
||||
Check if user has global or course staff permission
|
||||
@@ -44,147 +40,6 @@ class IsStaff(BasePermission):
|
||||
).has_user(request.user)
|
||||
|
||||
|
||||
class LtiSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serialize LtiConfiguration responses
|
||||
"""
|
||||
class Meta:
|
||||
model = LtiConfiguration
|
||||
fields = [
|
||||
'lti_1p1_client_key',
|
||||
'lti_1p1_client_secret',
|
||||
'lti_1p1_launch_url',
|
||||
'version',
|
||||
]
|
||||
|
||||
def to_internal_value(self, data: dict) -> dict:
|
||||
"""
|
||||
Transform the incoming primitive data into a native value
|
||||
"""
|
||||
data = data or {}
|
||||
payload = {
|
||||
key: value
|
||||
for key, value in data.items()
|
||||
if key in self.Meta.fields
|
||||
}
|
||||
return payload
|
||||
|
||||
def update(self, instance: LtiConfiguration, validated_data: dict) -> LtiConfiguration:
|
||||
"""
|
||||
Create/update a model-backed instance
|
||||
"""
|
||||
instance = instance or LtiConfiguration()
|
||||
instance.config_store = LtiConfiguration.CONFIG_ON_DB
|
||||
if validated_data:
|
||||
for key, value in validated_data.items():
|
||||
if key in self.Meta.fields:
|
||||
setattr(instance, key, value)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class LegacySettingsSerializer(serializers.BaseSerializer):
|
||||
"""
|
||||
Serialize legacy discussions settings
|
||||
"""
|
||||
class Meta:
|
||||
fields = [
|
||||
'allow_anonymous',
|
||||
'allow_anonymous_to_peers',
|
||||
'discussion_blackouts',
|
||||
'discussion_topics',
|
||||
# The following fields are deprecated;
|
||||
# they technically still exist in Studio (so we mention them here),
|
||||
# but they are not supported in the new experience:
|
||||
# 'discussion_link',
|
||||
# 'discussion_sort_alpha',
|
||||
]
|
||||
fields_cohorts = [
|
||||
'always_divide_inline_discussions',
|
||||
'divided_course_wide_discussions',
|
||||
'divided_inline_discussions',
|
||||
'division_scheme',
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
We do not need this.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def to_internal_value(self, data: dict) -> dict:
|
||||
"""
|
||||
Transform the incoming primitive data into a native value
|
||||
"""
|
||||
if not isinstance(data.get('allow_anonymous', False), bool):
|
||||
raise serializers.ValidationError('Wrong type for allow_anonymous')
|
||||
if not isinstance(data.get('allow_anonymous_to_peers', False), bool):
|
||||
raise serializers.ValidationError('Wrong type for allow_anonymous_to_peers')
|
||||
if not isinstance(data.get('discussion_blackouts', []), list):
|
||||
raise serializers.ValidationError('Wrong type for discussion_blackouts')
|
||||
if not isinstance(data.get('discussion_topics', {}), dict):
|
||||
raise serializers.ValidationError('Wrong type for discussion_topics')
|
||||
payload = {
|
||||
key: value
|
||||
for key, value in data.items() # lint-amnesty, pylint: disable=unnecessary-comprehension
|
||||
}
|
||||
return payload
|
||||
|
||||
def to_representation(self, instance) -> dict:
|
||||
"""
|
||||
Serialize data into a dictionary, to be used as a response
|
||||
"""
|
||||
settings = {
|
||||
field.name: field.read_json(instance)
|
||||
for field in instance.fields.values()
|
||||
if field.name in self.Meta.fields
|
||||
}
|
||||
discussion_settings = CourseDiscussionSettings.get(instance.id)
|
||||
serializer = DiscussionSettingsSerializer(
|
||||
discussion_settings,
|
||||
context={
|
||||
'course': instance,
|
||||
'settings': discussion_settings,
|
||||
},
|
||||
partial=True,
|
||||
)
|
||||
settings.update({
|
||||
key: value
|
||||
for key, value in serializer.data.items()
|
||||
if key != 'id'
|
||||
})
|
||||
return settings
|
||||
|
||||
def update(self, instance, validated_data: dict):
|
||||
"""
|
||||
Update and save an existing instance
|
||||
"""
|
||||
save = False
|
||||
cohort_settings = {}
|
||||
for field, value in validated_data.items():
|
||||
if field in self.Meta.fields:
|
||||
setattr(instance, field, value)
|
||||
save = True
|
||||
elif field in self.Meta.fields_cohorts:
|
||||
cohort_settings[field] = value
|
||||
if cohort_settings:
|
||||
discussion_settings = CourseDiscussionSettings.get(instance.id)
|
||||
serializer = DiscussionSettingsSerializer(
|
||||
discussion_settings,
|
||||
context={
|
||||
'course': instance,
|
||||
'settings': discussion_settings,
|
||||
},
|
||||
data=cohort_settings,
|
||||
partial=True,
|
||||
)
|
||||
if serializer.is_valid(raise_exception=True):
|
||||
serializer.save()
|
||||
if save:
|
||||
modulestore().update_item(instance, self.context['user_id'])
|
||||
return instance
|
||||
|
||||
|
||||
class DiscussionsConfigurationView(APIView):
|
||||
"""
|
||||
Handle configuration-related view-logic
|
||||
@@ -196,137 +51,6 @@ class DiscussionsConfigurationView(APIView):
|
||||
)
|
||||
permission_classes = (IsStaff,)
|
||||
|
||||
class Serializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serialize configuration responses
|
||||
"""
|
||||
class Meta:
|
||||
model = DiscussionsConfiguration
|
||||
fields = [
|
||||
'enabled',
|
||||
'provider_type',
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
We do not need this.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def to_internal_value(self, data: dict) -> dict:
|
||||
"""
|
||||
Transform the *incoming* primitive data into a native value.
|
||||
"""
|
||||
payload = {
|
||||
'context_key': data.get('course_key', ''),
|
||||
'enabled': data.get('enabled', False),
|
||||
'lti_configuration': data.get('lti_configuration', {}),
|
||||
'plugin_configuration': data.get('plugin_configuration', {}),
|
||||
'provider_type': data.get('provider_type', DEFAULT_PROVIDER_TYPE),
|
||||
}
|
||||
return payload
|
||||
|
||||
def to_representation(self, instance: DiscussionsConfiguration) -> dict:
|
||||
"""
|
||||
Serialize data into a dictionary, to be used as a response
|
||||
"""
|
||||
payload = super().to_representation(instance)
|
||||
lti_configuration_data = {}
|
||||
supports_lti = instance.supports('lti')
|
||||
if supports_lti:
|
||||
lti_configuration = LtiSerializer(instance.lti_configuration)
|
||||
lti_configuration_data = lti_configuration.data
|
||||
provider_type = instance.provider_type or DEFAULT_PROVIDER_TYPE
|
||||
plugin_configuration = instance.plugin_configuration
|
||||
if provider_type == 'legacy':
|
||||
course_key = instance.context_key
|
||||
course = get_course_by_id(course_key)
|
||||
legacy_settings = LegacySettingsSerializer(
|
||||
course,
|
||||
data=plugin_configuration,
|
||||
)
|
||||
if legacy_settings.is_valid(raise_exception=True):
|
||||
plugin_configuration = legacy_settings.data
|
||||
payload.update({
|
||||
'features': {
|
||||
'discussion-page',
|
||||
'embedded-course-sections',
|
||||
'lti',
|
||||
'wcag-2.1',
|
||||
},
|
||||
'lti_configuration': lti_configuration_data,
|
||||
'plugin_configuration': plugin_configuration,
|
||||
'providers': {
|
||||
'active': provider_type or DEFAULT_PROVIDER_TYPE,
|
||||
'available': PROVIDER_FEATURE_MAP,
|
||||
},
|
||||
})
|
||||
return payload
|
||||
|
||||
def update(self, instance: DiscussionsConfiguration, validated_data: dict) -> DiscussionsConfiguration:
|
||||
"""
|
||||
Update and save an existing instance
|
||||
"""
|
||||
for key in self.Meta.fields:
|
||||
value = validated_data.get(key)
|
||||
if value is not None:
|
||||
setattr(instance, key, value)
|
||||
# _update_* helpers assume `enabled` and `provider_type`
|
||||
# have already been set
|
||||
instance = self._update_lti(instance, validated_data)
|
||||
instance = self._update_plugin_configuration(instance, validated_data)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
def _update_lti(self, instance: DiscussionsConfiguration, validated_data: dict) -> DiscussionsConfiguration:
|
||||
"""
|
||||
Update LtiConfiguration
|
||||
"""
|
||||
lti_configuration_data = validated_data.get('lti_configuration')
|
||||
supports_lti = instance.supports('lti')
|
||||
if not supports_lti:
|
||||
instance.lti_configuration = None
|
||||
elif lti_configuration_data:
|
||||
lti_configuration = instance.lti_configuration or LtiConfiguration()
|
||||
lti_serializer = LtiSerializer(
|
||||
lti_configuration,
|
||||
data=lti_configuration_data,
|
||||
partial=True,
|
||||
)
|
||||
if lti_serializer.is_valid(raise_exception=True):
|
||||
lti_serializer.save()
|
||||
instance.lti_configuration = lti_configuration
|
||||
return instance
|
||||
|
||||
def _update_plugin_configuration(
|
||||
self,
|
||||
instance: DiscussionsConfiguration,
|
||||
validated_data: dict,
|
||||
) -> DiscussionsConfiguration:
|
||||
"""
|
||||
Create/update legacy provider settings
|
||||
"""
|
||||
updated_provider_type = validated_data.get('provider_type') or instance.provider_type
|
||||
will_support_legacy = bool(
|
||||
updated_provider_type == 'legacy'
|
||||
)
|
||||
if will_support_legacy:
|
||||
course_key = instance.context_key
|
||||
course = get_course_by_id(course_key)
|
||||
legacy_settings = LegacySettingsSerializer(
|
||||
course,
|
||||
context={
|
||||
'user_id': self.context['user_id'],
|
||||
},
|
||||
data=validated_data.get('plugin_configuration', {}),
|
||||
)
|
||||
if legacy_settings.is_valid(raise_exception=True):
|
||||
legacy_settings.save()
|
||||
instance.plugin_configuration = {}
|
||||
else:
|
||||
instance.plugin_configuration = validated_data.get('plugin_configuration') or {}
|
||||
return instance
|
||||
|
||||
# pylint: disable=redefined-builtin
|
||||
def get(self, request, course_key_string: str, **_kwargs) -> Response:
|
||||
"""
|
||||
@@ -334,7 +58,8 @@ class DiscussionsConfigurationView(APIView):
|
||||
"""
|
||||
course_key = _validate_course_key(course_key_string)
|
||||
configuration = DiscussionsConfiguration.get(course_key)
|
||||
serializer = self.Serializer(configuration)
|
||||
serializer = DiscussionsConfigurationSerializer(configuration)
|
||||
# breakpoint()
|
||||
return Response(serializer.data)
|
||||
|
||||
def post(self, request, course_key_string: str, **_kwargs) -> Response:
|
||||
@@ -343,7 +68,7 @@ class DiscussionsConfigurationView(APIView):
|
||||
"""
|
||||
course_key = _validate_course_key(course_key_string)
|
||||
configuration = DiscussionsConfiguration.get(course_key)
|
||||
serializer = self.Serializer(
|
||||
serializer = DiscussionsConfigurationSerializer(
|
||||
configuration,
|
||||
context={
|
||||
'user_id': request.user.id,
|
||||
|
||||
Reference in New Issue
Block a user