|
| 1 | +ADR-016: Provide Modulestore CRUD APIs via Custom DRF Layers |
| 2 | +============================================================ |
| 3 | + |
| 4 | +:Status: Proposed |
| 5 | +:Date: 2026-04-08 |
| 6 | +:Deciders: API Working Group |
| 7 | + |
| 8 | +Context |
| 9 | +======= |
| 10 | + |
| 11 | +Open edX currently lacks comprehensive REST APIs to create, view, update, and delete modulestore |
| 12 | +entities (courses, blocks). Modulestore is not backed by standard Django models, so DRF cannot be |
| 13 | +applied with model serializers directly. |
| 14 | + |
| 15 | +Decision |
| 16 | +======== |
| 17 | + |
| 18 | +Implement modulestore APIs using DRF **with custom serializers and service methods**: |
| 19 | + |
| 20 | +1. Create DRF ViewSets for modulestore resources (course, block). |
| 21 | +2. Use explicit, non-model serializers for validation and representation. |
| 22 | +3. Enforce permissions and visibility rules appropriate for authoring roles. |
| 23 | +4. Provide OpenAPI schemas and examples for all operations. |
| 24 | + |
| 25 | +Relevance in edx-platform |
| 26 | +========================= |
| 27 | + |
| 28 | +* **Modulestore is not ORM-backed**: Courses and blocks live in modulestore |
| 29 | + (MongoDB/split); ``xmodule.modulestore`` exposes ``get_course()``, ``get_item()``, |
| 30 | + ``update_item()``, etc. DRF model serializers do not apply directly. |
| 31 | +* **Existing read-only APIs**: ``openedx/core/djangoapps/olx_rest_api/views.py`` |
| 32 | + uses ``@api_view(['GET'])`` and ``view_auth_classes()``, calls |
| 33 | + ``modulestore().get_item()`` and ``serialize_modulestore_block_for_learning_core()``, |
| 34 | + and returns a custom JSON shape (no ModelSerializer). Contentstore course API |
| 35 | + (``cms/djangoapps/contentstore/api/views/utils.py``) uses ``BaseCourseView`` and |
| 36 | + ``modulestore().get_course()`` with custom depth handling. |
| 37 | +* **Studio/course authoring**: Contentstore views (e.g. ``contentstore/views/block.py``, |
| 38 | + ``course.py``) perform create/update/delete via Python APIs, not REST; this ADR |
| 39 | + proposes exposing CRUD via DRF with custom serializers and service-layer methods. |
| 40 | + |
| 41 | +Code examples |
| 42 | +============= |
| 43 | + |
| 44 | +**Custom serializer (no model):** |
| 45 | + |
| 46 | +.. code-block:: python |
| 47 | +
|
| 48 | + from rest_framework import serializers |
| 49 | +
|
| 50 | + class ModulestoreBlockSerializer(serializers.Serializer): |
| 51 | + id = serializers.CharField(read_only=True) |
| 52 | + block_type = serializers.CharField() |
| 53 | + display_name = serializers.CharField(required=False) |
| 54 | + parent = serializers.CharField(required=False) |
| 55 | +
|
| 56 | + def create(self, validated_data): |
| 57 | + return modulestore_service.create_block( |
| 58 | + self.context["course_key"], validated_data |
| 59 | + ) |
| 60 | +
|
| 61 | + def update(self, instance, validated_data): |
| 62 | + return modulestore_service.update_block(instance, validated_data) |
| 63 | +
|
| 64 | +**ViewSet with custom get_queryset / get_object:** |
| 65 | + |
| 66 | +.. code-block:: python |
| 67 | +
|
| 68 | + from rest_framework import viewsets |
| 69 | + from openedx.core.lib.api.view_utils import view_auth_classes |
| 70 | +
|
| 71 | + @view_auth_classes() |
| 72 | + class ModulestoreBlockViewSet(viewsets.ModelViewSet): |
| 73 | + serializer_class = ModulestoreBlockSerializer |
| 74 | + permission_classes = [IsAuthenticated, HasStudioWriteAccess] |
| 75 | +
|
| 76 | + def get_object(self): |
| 77 | + usage_key = UsageKey.from_string(self.kwargs["usage_key"]) |
| 78 | + if not has_studio_read_access(self.request.user, usage_key.course_key): |
| 79 | + raise PermissionDenied() |
| 80 | + return modulestore().get_item(usage_key) |
| 81 | +
|
| 82 | + def get_queryset(self): |
| 83 | + course_key = CourseKey.from_string(self.kwargs["course_id"]) |
| 84 | + return modulestore().get_items(course_key, ...) # or service method |
| 85 | +
|
| 86 | + # Router: /api/modulestore/courses/<course_id>/blocks/ list, retrieve, create, update, destroy |
| 87 | +
|
| 88 | +**Service layer (recommended):** |
| 89 | + |
| 90 | +.. code-block:: python |
| 91 | +
|
| 92 | + # services.py |
| 93 | + def get_block(usage_key, user): |
| 94 | + if not has_studio_read_access(user, usage_key.course_key): |
| 95 | + raise PermissionDenied() |
| 96 | + return modulestore().get_item(usage_key) |
| 97 | +
|
| 98 | + def update_block(usage_key, user, data): |
| 99 | + block = get_block(usage_key, user) |
| 100 | + if not has_studio_write_access(user, usage_key.course_key): |
| 101 | + raise PermissionDenied() |
| 102 | + # Apply data to block, then modulestore().update_item(...) |
| 103 | + return block |
| 104 | +
|
| 105 | +Consequences |
| 106 | +============ |
| 107 | + |
| 108 | +* Pros |
| 109 | + |
| 110 | + * Enables cleaner authoring/integration flows for Studio/MFEs and external tools. |
| 111 | + * Standardizes modulestore interactions behind documented REST APIs. |
| 112 | + |
| 113 | +* Cons / Costs |
| 114 | + |
| 115 | + * Significant implementation effort; careful security/authorization required. |
| 116 | + * Backing store migrations must be abstracted behind stable service interfaces. |
| 117 | + |
| 118 | +Implementation Notes |
| 119 | +==================== |
| 120 | + |
| 121 | +* Start with read-only endpoints (GET) for course structure/blocks, then add write operations. |
| 122 | +* Ensure stable contracts independent of modulestore backend. |
| 123 | + |
| 124 | +References |
| 125 | +========== |
| 126 | + |
| 127 | +* “Modulestore APIs” recommendation in the Open edX REST API standardization notes. |
0 commit comments