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:
@@ -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: {
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
15
openedx/core/djangoapps/discussions/urls.py
Normal file
15
openedx/core/djangoapps/discussions/urls.py
Normal 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',
|
||||
),
|
||||
]
|
||||
110
openedx/core/djangoapps/discussions/views.py
Normal file
110
openedx/core/djangoapps/discussions/views.py
Normal 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
|
||||
Reference in New Issue
Block a user