feat: add Instructor Dashboard ORA summary API v2 (#37858)

This commit is contained in:
Daniel Wong
2026-01-20 14:32:06 -06:00
committed by GitHub
parent 9f48073921
commit f65d423c77
5 changed files with 182 additions and 1 deletions

View File

@@ -66,3 +66,32 @@ def get_open_response_assessment_list(course):
ora_items.append(ora_assessment_data)
return ora_items
def get_ora_summary(course):
"""
Return aggregated ORA statistics for a course.
"""
ora_items = get_open_response_assessment_list(course)
summary = {
'total_units': 0,
'total_assessments': 0,
'total_responses': 0,
'training': 0,
'peer': 0,
'self': 0,
'waiting': 0,
'staff': 0,
'final_grade_received': 0,
}
for item in ora_items:
summary['total_assessments'] += 1
summary['total_units'] += 1 # Assuming one assessment per unit
summary['total_responses'] += item['total']
summary['training'] += item['training']
summary['peer'] += item['peer']
summary['self'] += item['self']
summary['waiting'] += item['waiting']
summary['staff'] += item['staff']
summary['final_grade_received'] += item['final_grade_received']
return summary

View File

@@ -1036,3 +1036,93 @@ class ORAViewTest(ORABaseViewsTest):
assert response.status_code == 200
data = response.data['results']
assert len(data) == 0
class ORASummaryViewTest(ORABaseViewsTest):
"""
Tests for the ORASummaryView API endpoints.
"""
view_name = "instructor_api_v2:ora_summary"
def setUp(self):
super().setUp()
self.log_in()
def _get_url(self, course_id=None):
"""Helper to get the API URL."""
if course_id is None:
course_id = str(self.course_key)
return reverse(self.view_name, kwargs={'course_id': course_id})
def test_get_ora_summary(self):
"""Test retrieving the ORA summary."""
BlockFactory.create(
category="openassessment",
parent_location=self.course.location,
display_name="test2",
)
response = self.client.get(
self._get_url()
)
assert response.status_code == 200
data = response.data
assert 'total_units' in data
assert 'total_assessments' in data
assert 'total_responses' in data
assert 'training' in data
assert 'peer' in data
assert 'self' in data
assert 'waiting' in data
assert 'staff' in data
assert 'final_grade_received' in data
assert data['total_units'] == 2
assert data['total_assessments'] == 2
assert data['total_responses'] == 0
assert data['training'] == 0
assert data['peer'] == 0
assert data['self'] == 0
assert data['waiting'] == 0
assert data['staff'] == 0
assert data['final_grade_received'] == 0
def test_invalid_course_id(self):
"""Test error handling for invalid course ID."""
invalid_course_id = 'invalid-course-id'
url = self._get_url()
response = self.client.get(url.replace(str(self.course_key), invalid_course_id))
assert response.status_code == 404
def test_permission_denied_for_non_staff(self):
"""Test that non-staff users cannot access the endpoint."""
# Log out staff
self.client.logout()
# Create a non-staff user and enroll them in the course
user = UserFactory(password="password")
CourseEnrollment.enroll(user, self.course_key)
# Log in as the non-staff user
self.client.login(username=user.username, password="password")
response = self.client.get(self._get_url())
assert response.status_code == 403
def test_permission_allowed_for_instructor(self):
"""Test that instructor users can access the endpoint."""
# Log out staff user
self.client.logout()
# Create instructor for this course
instructor = InstructorFactory(course_key=self.course_key, password="password")
# Log in as instructor
self.client.login(username=instructor.username, password="password")
# Access the endpoint
response = self.client.get(self._get_url())
assert response.status_code == 200

View File

@@ -46,6 +46,11 @@ v2_api_urls = [
api_v2.ORAView.as_view(),
name='ora_assessments'
),
re_path(
rf'^courses/{COURSE_ID_PATTERN}/ora_summary$',
api_v2.ORASummaryView.as_view(),
name='ora_summary'
),
]
urlpatterns = [

View File

@@ -27,7 +27,7 @@ from lms.djangoapps.instructor import permissions
from lms.djangoapps.instructor.views.api import _display_unit, get_student_from_identifier
from lms.djangoapps.instructor.views.instructor_task_helpers import extract_task_features
from lms.djangoapps.instructor_task import api as task_api
from lms.djangoapps.instructor.ora import get_open_response_assessment_list
from lms.djangoapps.instructor.ora import get_open_response_assessment_list, get_ora_summary
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin
from openedx.core.lib.courses import get_course_by_id
from .serializers_v2 import (
@@ -35,6 +35,7 @@ from .serializers_v2 import (
CourseInformationSerializerV2,
BlockDueDateSerializerV2,
ORASerializer,
ORASummarySerializer,
)
from .tools import (
find_unit,
@@ -402,3 +403,44 @@ class ORAView(GenericAPIView):
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
class ORASummaryView(GenericAPIView):
"""
View to get a summary of Open Response Assessments (ORAs) for a given course.
* Requires token authentication.
* Only instructors or staff for the course are able to access this view.
"""
permission_classes = [IsAuthenticated, permissions.InstructorPermission]
permission_name = permissions.VIEW_DASHBOARD
serializer_class = ORASummarySerializer
def get_course(self):
"""
Retrieve the course object based on the course_id URL parameter.
Validates that the course exists and is not deprecated.
Raises NotFound if the course does not exist.
"""
course_id = self.kwargs.get("course_id")
try:
course_key = CourseKey.from_string(course_id)
except InvalidKeyError as exc:
log.error("Unable to find course with course key %s while loading the Instructor Dashboard.", course_id)
raise NotFound("Course not found") from exc
if course_key.deprecated:
raise NotFound("Course not found")
course = get_course_by_id(course_key, depth=None)
return course
def get(self, request, *args, **kwargs):
"""
Return a summary of ORAs for the specified course.
"""
course = self.get_course()
items = get_ora_summary(course)
serializer = self.get_serializer(items)
return Response(serializer.data)

View File

@@ -433,3 +433,18 @@ class ORASerializer(serializers.Serializer):
waiting = serializers.IntegerField()
staff = serializers.IntegerField()
final_grade_received = serializers.IntegerField()
class ORASummarySerializer(serializers.Serializer):
"""
Aggregated ORA statistics for a course
"""
total_units = serializers.IntegerField()
total_assessments = serializers.IntegerField()
total_responses = serializers.IntegerField()
training = serializers.IntegerField()
peer = serializers.IntegerField()
self = serializers.IntegerField()
waiting = serializers.IntegerField()
staff = serializers.IntegerField()
final_grade_received = serializers.IntegerField()