diff --git a/openedx/core/djangoapps/discussions/apps.py b/openedx/core/djangoapps/discussions/apps.py index 7ecc1e0c1a..107346423f 100644 --- a/openedx/core/djangoapps/discussions/apps.py +++ b/openedx/core/djangoapps/discussions/apps.py @@ -5,6 +5,8 @@ from django.apps import AppConfig from edx_django_utils.plugins import PluginSettings from edx_django_utils.plugins import PluginURLs +from openedx.core.djangoapps.plugins.constants import ProjectType + class DiscussionsConfig(AppConfig): """ @@ -13,6 +15,11 @@ class DiscussionsConfig(AppConfig): name = 'openedx.core.djangoapps.discussions' plugin_app = { PluginURLs.CONFIG: { + ProjectType.LMS: { + PluginURLs.NAMESPACE: '', + PluginURLs.REGEX: r'^discussions/', + PluginURLs.RELATIVE_PATH: 'urls', + }, }, PluginSettings.CONFIG: { }, diff --git a/openedx/core/djangoapps/discussions/docs/decisions/0003-configuration-rest-api.rst b/openedx/core/djangoapps/discussions/docs/decisions/0003-configuration-rest-api.rst new file mode 100644 index 0000000000..5cb0b3fb67 --- /dev/null +++ b/openedx/core/djangoapps/discussions/docs/decisions/0003-configuration-rest-api.rst @@ -0,0 +1,95 @@ +Expose Discussion Configuration via HTTP API +============================================ + + +Status +------ + +Proposal + + +Context +------- + +As part of the BD-03 initiative (Blended Development, Project 3), +we have previously created a discussion provider configuration backend, +`DiscussionsConfiguration`, as well as a new microfrontend (MFE), +`frontend-app-course-authoring`. + +However, these two systems cannot yet interact with one another. + +This document proposes the creation of a new HTTP API to connect the two. + + +Requirements +------------ + +For a given `context_key`, this API must allow: +- retrieval of: + - the list of available providers + - any options for the active provider +- creation of: + - new configurations +- updating of: + - existing configurations +- deletion/disabling of: + - unneeded/inactive configurations + + +Consideration +------------- + +The API should follow existing best-practices and technology as exists +in `edx-platform`. We should _not_ introduce new API architecture. + + +Decision +-------- + +We propose to implement this as a Django REST Framework (DRF)-based HTTP API. + +This API will provide the following HTTP methods: +- GET + - Retrieve collection of active and available providers, + as well as their options +- POST + - Create, update, or disable a configuration + + +Payload Shape +------------- + +The payload is expected to be shaped like this (key names subject to change): + +.. code-block:: python + payload = { + 'context_key': str(configuration.context_key), + 'enabled': configuration.enabled, + 'features': { + 'discussion-page', + 'embedded-course-sections', + 'lti', + 'wcag-2.1', + }, + 'plugin_configuration': configuration.plugin_configuration, + 'providers': { + 'active': configuration.provider_type or '', + 'available': { + provider: { + 'features': PROVIDER_FEATURE_MAP.get(provider) or [], + } + for provider in configuration.available_providers + }, + }, + } + +The following configuration values are explicitly omitted; +this should be left entirely up to the MFE. +- name +- logo +- description +- support_level +- documentation URL + +The LTI configuration (keys, secrets, URLs, etc.) are considered +out-of-scope at this time. diff --git a/openedx/core/djangoapps/discussions/urls.py b/openedx/core/djangoapps/discussions/urls.py new file mode 100644 index 0000000000..7bf9330893 --- /dev/null +++ b/openedx/core/djangoapps/discussions/urls.py @@ -0,0 +1,15 @@ +""" +Configure URL endpoints for the djangoapp +""" +from django.conf.urls import url + +from .views import DiscussionsConfigurationView + + +urlpatterns = [ + url( + r'^api/v0/(?P.+)$', + DiscussionsConfigurationView.as_view(), + name='discussions', + ), +] diff --git a/openedx/core/djangoapps/discussions/views.py b/openedx/core/djangoapps/discussions/views.py new file mode 100644 index 0000000000..2eaebc6b12 --- /dev/null +++ b/openedx/core/djangoapps/discussions/views.py @@ -0,0 +1,110 @@ +""" +Handle view-logic for the djangoapp +""" +from opaque_keys.edx.keys import CourseKey +from opaque_keys import InvalidKeyError +from rest_framework import serializers +from rest_framework.response import Response +from rest_framework.views import APIView + +from openedx.core.lib.api.permissions import IsStaff +from openedx.core.lib.api.view_utils import view_auth_classes + +from .models import DiscussionsConfiguration + + +PROVIDER_FEATURE_MAP = { + 'cs_comments_service': [ + 'discussion-page', + 'embedded-course-sections', + 'lti', + 'wcag-2.1', + ], + 'piazza': [ + 'discussion-page', + 'lti', + ], +} + + +@view_auth_classes() +class DiscussionsConfigurationView(APIView): + """ + Handle configuration-related view-logic + """ + permission_classes = (IsStaff,) + + class Serializer(serializers.BaseSerializer): + """ + Serialize configuration responses + """ + + def create(self, validated_data): + """ + Create and save a new instance + """ + raise NotImplementedError + + def to_internal_data(self, data): + """ + Transform the *incoming* primitive data into a native value. + """ + raise NotImplementedError + + def to_representation(self, instance) -> dict: + """ + Serialize data into a dictionary, to be used as a response + """ + payload = { + 'context_key': str(instance.context_key), + 'enabled': instance.enabled, + 'features': { + 'discussion-page', + 'embedded-course-sections', + 'lti', + 'wcag-2.1', + }, + 'plugin_configuration': instance.plugin_configuration, + 'providers': { + 'active': instance.provider_type or '', + 'available': { + provider: { + 'features': PROVIDER_FEATURE_MAP.get(provider) or [], + } + for provider in instance.available_providers + }, + }, + } + return payload + + def update(self, instance, validated_data): + """ + Update and save an existing instance + """ + raise NotImplementedError + + # pylint: disable=redefined-builtin + def get(self, request, course_key_string, **_kwargs) -> Response: + """ + Handle HTTP/GET requests + """ + course_key = self._validate_course_key(course_key_string) + configuration = DiscussionsConfiguration.get(course_key) + serializer = self.Serializer(configuration) + return Response(serializer.data) + + def _validate_course_key(self, course_key_string: str) -> CourseKey: + """ + Validate and parse a course_key string, if supported + """ + try: + course_key = CourseKey.from_string(course_key_string) + except InvalidKeyError as error: + raise serializers.ValidationError( + f"{course_key_string} is not a valid CourseKey" + ) from error + if course_key.deprecated: + raise serializers.ValidationError( + 'Deprecated CourseKeys (Org/Course/Run) are not supported.' + ) + return course_key