Also adds JwtAuthentication to the list of allowed authentication methods, so the Grades REST API can be easily accessed from Enterprise management commands.
227 lines
7.9 KiB
Python
227 lines
7.9 KiB
Python
""" API v0 views. """
|
|
import logging
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.http import Http404
|
|
from opaque_keys import InvalidKeyError
|
|
from opaque_keys.edx.keys import CourseKey
|
|
from rest_framework import status
|
|
from rest_framework.exceptions import AuthenticationFailed
|
|
from rest_framework.generics import GenericAPIView, ListAPIView
|
|
from rest_framework.response import Response
|
|
|
|
from courseware.access import has_access
|
|
from lms.djangoapps.ccx.utils import prep_course_for_grading
|
|
from lms.djangoapps.courseware import courses
|
|
from lms.djangoapps.grades.api.serializers import GradingPolicySerializer
|
|
from lms.djangoapps.grades.new.course_grade import CourseGradeFactory
|
|
from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser
|
|
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes
|
|
from student.roles import CourseStaffRole
|
|
|
|
log = logging.getLogger(__name__)
|
|
USER_MODEL = get_user_model()
|
|
|
|
|
|
@view_auth_classes()
|
|
class GradeViewMixin(DeveloperErrorViewMixin):
|
|
"""
|
|
Mixin class for Grades related views.
|
|
"""
|
|
def _get_course(self, course_key_string, user, access_action):
|
|
"""
|
|
Returns the course for the given course_key_string after
|
|
verifying the requested access to the course by the given user.
|
|
"""
|
|
try:
|
|
course_key = CourseKey.from_string(course_key_string)
|
|
except InvalidKeyError:
|
|
return self.make_error_response(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
developer_message='The provided course key cannot be parsed.',
|
|
error_code='invalid_course_key'
|
|
)
|
|
|
|
try:
|
|
return courses.get_course_with_access(
|
|
user,
|
|
access_action,
|
|
course_key,
|
|
check_if_enrolled=True
|
|
)
|
|
except Http404:
|
|
log.info('Course with ID "%s" not found', course_key_string)
|
|
return self.make_error_response(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
developer_message='The user, the course or both do not exist.',
|
|
error_code='user_or_course_does_not_exist'
|
|
)
|
|
|
|
def _get_effective_user(self, request, course):
|
|
"""
|
|
Returns the user object corresponding to the request's 'username' parameter,
|
|
or the current request.user if no 'username' was provided.
|
|
|
|
Verifies that the request.user has access to the requested users's grades.
|
|
Returns a 403 error response if access is denied, or a 404 error response if the user does not exist.
|
|
"""
|
|
|
|
# Use the request user's if none provided.
|
|
if 'username' in request.GET:
|
|
username = request.GET.get('username')
|
|
else:
|
|
username = request.user.username
|
|
|
|
if request.user.username == username:
|
|
# Any user may request her own grades
|
|
return request.user
|
|
|
|
# Only a user with staff access may request grades for a user other than herself.
|
|
if not has_access(request.user, CourseStaffRole.ROLE, course):
|
|
log.info(
|
|
'User %s tried to access the grade for user %s.',
|
|
request.user.username,
|
|
username
|
|
)
|
|
return self.make_error_response(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
developer_message='The user requested does not match the logged in user.',
|
|
error_code='user_mismatch'
|
|
)
|
|
|
|
try:
|
|
return USER_MODEL.objects.get(username=username)
|
|
|
|
except USER_MODEL.DoesNotExist:
|
|
return self.make_error_response(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
developer_message='The user matching the requested username does not exist.',
|
|
error_code='user_does_not_exist'
|
|
)
|
|
|
|
def perform_authentication(self, request):
|
|
"""
|
|
Ensures that the user is authenticated (e.g. not an AnonymousUser), unless DEBUG mode is enabled.
|
|
"""
|
|
super(GradeViewMixin, self).perform_authentication(request)
|
|
if request.user.is_anonymous():
|
|
raise AuthenticationFailed
|
|
|
|
|
|
class UserGradeView(GradeViewMixin, GenericAPIView):
|
|
"""
|
|
**Use Case**
|
|
|
|
* Get the current course grades for a user in a course.
|
|
|
|
The currently logged-in user may request her own grades, or a user with staff access to the course may request
|
|
any enrolled user's grades.
|
|
|
|
**Example Request**
|
|
|
|
GET /api/grades/v0/course_grade/{course_id}/users/?username={username}
|
|
|
|
**GET Parameters**
|
|
|
|
A GET request may include the following parameters.
|
|
|
|
* course_id: (required) A string representation of a Course ID.
|
|
* username: (optional) A string representation of a user's username.
|
|
Defaults to the currently logged-in user's username.
|
|
|
|
**GET Response Values**
|
|
|
|
If the request for information about the course grade
|
|
is successful, an HTTP 200 "OK" response is returned.
|
|
|
|
The HTTP 200 response has the following values.
|
|
|
|
* username: A string representation of a user's username passed in the request.
|
|
|
|
* course_id: A string representation of a Course ID.
|
|
|
|
* passed: Boolean representing whether the course has been
|
|
passed according the course's grading policy.
|
|
|
|
* percent: A float representing the overall grade for the course
|
|
|
|
* letter_grade: A letter grade as defined in grading_policy (e.g. 'A' 'B' 'C' for 6.002x) or None
|
|
|
|
|
|
**Example GET Response**
|
|
|
|
[{
|
|
"username": "bob",
|
|
"course_key": "edX/DemoX/Demo_Course",
|
|
"passed": false,
|
|
"percent": 0.03,
|
|
"letter_grade": None,
|
|
}]
|
|
|
|
"""
|
|
def get(self, request, course_id):
|
|
"""
|
|
Gets a course progress status.
|
|
|
|
Args:
|
|
request (Request): Django request object.
|
|
course_id (string): URI element specifying the course location.
|
|
|
|
Return:
|
|
A JSON serialized representation of the requesting user's current grade status.
|
|
"""
|
|
|
|
course = self._get_course(course_id, request.user, 'load')
|
|
if isinstance(course, Response):
|
|
# Returns a 404 if course_id is invalid, or request.user is not enrolled in the course
|
|
return course
|
|
|
|
grade_user = self._get_effective_user(request, course)
|
|
if isinstance(grade_user, Response):
|
|
# Returns a 403 if the request.user can't access grades for the requested user,
|
|
# or a 404 if the requested user does not exist.
|
|
return grade_user
|
|
|
|
prep_course_for_grading(course, request)
|
|
course_grade = CourseGradeFactory().create(grade_user, course)
|
|
return Response([{
|
|
'username': grade_user.username,
|
|
'course_key': course_id,
|
|
'passed': course_grade.passed,
|
|
'percent': course_grade.percent,
|
|
'letter_grade': course_grade.letter_grade,
|
|
}])
|
|
|
|
|
|
class CourseGradingPolicy(GradeViewMixin, ListAPIView):
|
|
"""
|
|
**Use Case**
|
|
|
|
Get the course grading policy.
|
|
|
|
**Example requests**:
|
|
|
|
GET /api/grades/v0/policy/{course_id}/
|
|
|
|
**Response Values**
|
|
|
|
* assignment_type: The type of the assignment, as configured by course
|
|
staff. For example, course staff might make the assignment types Homework,
|
|
Quiz, and Exam.
|
|
|
|
* count: The number of assignments of the type.
|
|
|
|
* dropped: Number of assignments of the type that are dropped.
|
|
|
|
* weight: The weight, or effect, of the assignment type on the learner's
|
|
final grade.
|
|
"""
|
|
|
|
allow_empty = False
|
|
|
|
def get(self, request, course_id, **kwargs):
|
|
course = self._get_course(course_id, request.user, 'staff')
|
|
if isinstance(course, Response):
|
|
return course
|
|
return Response(GradingPolicySerializer(course.raw_grader, many=True).data)
|