Merge PR #26718 bd03/api/init

* Commits:
  feat: Add discussions API endpoint [BD-03]
  docs: Add ADR for DiscussionsConfiguration HTTP API [BD-03]
This commit is contained in:
stvn
2021-03-11 00:17:16 -08:00
4 changed files with 227 additions and 0 deletions

View File

@@ -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: {
},

View File

@@ -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.

View File

@@ -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<course_key_string>.+)$',
DiscussionsConfigurationView.as_view(),
name='discussions',
),
]

View File

@@ -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