feat: add Instructor Dashboard ORA summary API v2 (#37858)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user