Files
edx-platform/lms/djangoapps/grades/api/v1/utils.py
2019-02-05 15:15:02 -05:00

238 lines
8.8 KiB
Python

"""
Define some view level utility functions here that multiple view modules will share
"""
from contextlib import contextmanager
from functools import wraps
from rest_framework import status
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.pagination import CursorPagination
from rest_framework.response import Response
from rest_framework.views import APIView
from six import text_type
from django.contrib.auth import get_user_model
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin
from student.models import CourseEnrollment
from util.query import use_read_replica_if_available
USER_MODEL = get_user_model()
def get_course_key(request, course_id=None):
if not course_id:
return CourseKey.from_string(request.GET.get('course_id'))
return CourseKey.from_string(course_id)
def verify_course_exists(view_func):
"""
A decorator to wrap a view function that takes `course_key` as a parameter.
Raises:
An API error if the `course_key` is invalid, or if no `CourseOverview` exists for the given key.
"""
@wraps(view_func)
def wrapped_function(self, request, **kwargs):
"""
Wraps the given view_function.
"""
try:
course_key = get_course_key(request, kwargs.get('course_id'))
except InvalidKeyError:
raise self.api_error(
status_code=status.HTTP_404_NOT_FOUND,
developer_message='The provided course key cannot be parsed.',
error_code='invalid_course_key'
)
if not CourseOverview.get_from_id_if_exists(course_key):
raise self.api_error(
status_code=status.HTTP_404_NOT_FOUND,
developer_message=u"Requested grade for unknown course {course}".format(course=text_type(course_key)),
error_code='course_does_not_exist'
)
return view_func(self, request, **kwargs)
return wrapped_function
class CourseEnrollmentPagination(CursorPagination):
"""
Paginates over CourseEnrollment objects.
"""
ordering = 'id'
page_size = 50
page_size_query_param = 'page_size'
def get_page_size(self, request):
"""
Get the page size based on the defined page size parameter if defined.
"""
try:
page_size_string = request.query_params[self.page_size_query_param]
return int(page_size_string)
except (KeyError, ValueError):
pass
return self.page_size
class PaginatedAPIView(APIView):
"""
An `APIView` class enhanced with the pagination methods of `GenericAPIView`.
"""
# pylint: disable=attribute-defined-outside-init
@property
def paginator(self):
"""
The paginator instance associated with the view, or `None`.
"""
if not hasattr(self, '_paginator'):
if self.pagination_class is None:
self._paginator = None
else:
self._paginator = self.pagination_class()
return self._paginator
def paginate_queryset(self, queryset):
"""
Return a single page of results, or `None` if pagination is disabled.
"""
if self.paginator is None:
return None
return self.paginator.paginate_queryset(queryset, self.request, view=self)
def get_paginated_response(self, data):
"""
Return a paginated style `Response` object for the given output data.
"""
assert self.paginator is not None
return self.paginator.get_paginated_response(data)
class GradeViewMixin(DeveloperErrorViewMixin):
"""
Mixin class for Grades related views.
"""
def _get_single_user(self, request, course_key, user_id=None):
"""
Returns a single USER_MODEL object corresponding to either the user_id provided, or if no id is provided,
then the request's `username` parameter, or the current `request.user` if no `username` was provided.
Args:
request (Request): django request object to check for username or request.user object
course_key (CourseLocator): The course to retrieve user grades for.
user_id (int): Optional user id to fetch the user object for.
Returns:
A USER_MODEL object.
Raises:
USER_MODEL.DoesNotExist if no such user exists.
CourseEnrollment.DoesNotExist if the user is not enrolled in the given course.
"""
# May raise USER_MODEL.DoesNotExist if no user matching the given query exists.
if user_id:
grade_user = USER_MODEL.objects.get(id=user_id)
elif 'username' in request.GET:
grade_user = USER_MODEL.objects.get(username=request.GET.get('username'))
else:
grade_user = request.user
# May raise CourseEnrollment.DoesNotExist if no enrollment exists for this user/course.
_ = CourseEnrollment.objects.get(user=grade_user, course_id=course_key)
return grade_user
@contextmanager
def _get_user_or_raise(self, request, course_key):
"""
Raises an API error if the username specified by the request does not exist, or if the
user is not enrolled in the given course.
Args:
request (Request): django request object to check for username or request.user object
course_key (CourseLocator): The course to retrieve user grades for.
Yields:
A USER_MODEL object.
"""
try:
yield self._get_single_user(request, course_key)
except USER_MODEL.DoesNotExist:
raise self.api_error(
status_code=status.HTTP_404_NOT_FOUND,
developer_message='The user matching the requested username does not exist.',
error_code='user_does_not_exist'
)
except CourseEnrollment.DoesNotExist:
raise self.api_error(
status_code=status.HTTP_404_NOT_FOUND,
developer_message='The user matching the requested username is not enrolled in this course',
error_code='user_not_enrolled'
)
def _get_single_user_grade(self, grade_user, course_key):
"""
Returns a grade response for the user object corresponding to the request's 'username' parameter,
or the current request.user if no 'username' was provided.
Args:
request (Request): django request object to check for username or request.user object
course_key (CourseLocator): The course to retrieve user grades for.
Returns:
A serializable list of grade responses
"""
course_grade = CourseGradeFactory().read(grade_user, course_key=course_key)
return Response([self._serialize_user_grade(grade_user, course_key, course_grade)])
def _paginate_users(self, course_key, course_enrollment_filter=None, related_models=None):
"""
Args:
course_key (CourseLocator): The course to retrieve grades for.
course_enrollment_filter: Optional dictionary of keyword arguments to pass
to `CourseEnrollment.filter()`.
related_models: Optional list of related models to join to the CourseEnrollment table.
Returns:
A list of users, pulled from a paginated queryset of enrollments, who are enrolled in the given course.
"""
filter_kwargs = {
'course_id': course_key,
'is_active': True,
}
filter_kwargs.update(course_enrollment_filter or {})
enrollments_in_course = use_read_replica_if_available(
CourseEnrollment.objects.filter(**filter_kwargs)
)
if related_models:
enrollments_in_course = enrollments_in_course.select_related(*related_models)
paged_enrollments = self.paginate_queryset(enrollments_in_course)
return [enrollment.user for enrollment in paged_enrollments]
def _serialize_user_grade(self, user, course_key, course_grade):
"""
Serialize a single grade to dict to use in Responses
"""
return {
'username': user.username,
'email': user.email,
'course_id': str(course_key),
'passed': course_grade.passed,
'percent': course_grade.percent,
'letter_grade': course_grade.letter_grade,
}
def perform_authentication(self, request):
"""
Ensures that the user is authenticated (e.g. not an AnonymousUser).
"""
super(GradeViewMixin, self).perform_authentication(request)
if request.user.is_anonymous:
raise AuthenticationFailed