diff --git a/openedx/core/djangoapps/discussions/models.py b/openedx/core/djangoapps/discussions/models.py index 21df38995c..33eddaf4f3 100644 --- a/openedx/core/djangoapps/discussions/models.py +++ b/openedx/core/djangoapps/discussions/models.py @@ -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 diff --git a/openedx/core/djangoapps/discussions/serializers.py b/openedx/core/djangoapps/discussions/serializers.py new file mode 100644 index 0000000000..646a37b5d5 --- /dev/null +++ b/openedx/core/djangoapps/discussions/serializers.py @@ -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 diff --git a/openedx/core/djangoapps/discussions/views.py b/openedx/core/djangoapps/discussions/views.py index 18a7ae9a1e..9aaffa0dd4 100644 --- a/openedx/core/djangoapps/discussions/views.py +++ b/openedx/core/djangoapps/discussions/views.py @@ -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,