Files
edx-platform/openedx/core/djangoapps/credit/views.py

205 lines
7.6 KiB
Python

"""
Views for the credit Django app.
"""
import datetime
import logging
import pytz
from django.conf import settings
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from rest_framework import generics, mixins, permissions, views, viewsets
from rest_framework.authentication import SessionAuthentication
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response
from openedx.core.djangoapps.credit.api import create_credit_request
from openedx.core.djangoapps.credit.exceptions import (
CreditApiBadRequest,
InvalidCourseKey,
InvalidCreditRequest,
UserNotEligibleException
)
from openedx.core.djangoapps.credit.models import (
CREDIT_PROVIDER_ID_REGEX,
CreditCourse,
CreditEligibility,
CreditProvider,
CreditRequest
)
from openedx.core.djangoapps.credit.serializers import (
CreditCourseSerializer,
CreditEligibilitySerializer,
CreditProviderCallbackSerializer,
CreditProviderSerializer
)
from openedx.core.lib.api.authentication import BearerAuthentication
from openedx.core.lib.api.mixins import PutAsCreateMixin
from openedx.core.lib.api.permissions import IsStaffOrOwner
log = logging.getLogger(__name__)
AUTHENTICATION_CLASSES = (JwtAuthentication, BearerAuthentication, SessionAuthentication,)
class CreditProviderViewSet(viewsets.ReadOnlyModelViewSet):
""" Credit provider endpoints. """
lookup_field = 'provider_id'
lookup_value_regex = CREDIT_PROVIDER_ID_REGEX
authentication_classes = AUTHENTICATION_CLASSES
pagination_class = None
permission_classes = (permissions.IsAuthenticated,)
queryset = CreditProvider.objects.all()
serializer_class = CreditProviderSerializer
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
# Filter by provider ID
provider_ids = self.request.GET.get('provider_ids', None)
if provider_ids:
provider_ids = provider_ids.split(',')
queryset = queryset.filter(provider_id__in=provider_ids)
return queryset
class CreditProviderRequestCreateView(views.APIView):
""" Creates a credit request for the given user and course, if the user is eligible for credit."""
authentication_classes = AUTHENTICATION_CLASSES
permission_classes = (permissions.IsAuthenticated, IsStaffOrOwner,)
def post(self, request, provider_id):
""" POST handler. """
# Get the provider, or return HTTP 404 if it doesn't exist
provider = generics.get_object_or_404(CreditProvider, provider_id=provider_id)
# Validate the course key
course_key = request.data.get('course_key')
try:
course_key = CourseKey.from_string(course_key)
except InvalidKeyError:
raise InvalidCourseKey(course_key) # lint-amnesty, pylint: disable=raise-missing-from
# Validate the username
username = request.data.get('username')
if not username:
raise ValidationError({'detail': 'A username must be specified.'})
# Ensure the user is actually eligible to receive credit
if not CreditEligibility.is_user_eligible_for_credit(course_key, username):
raise UserNotEligibleException(course_key, username)
try:
credit_request = create_credit_request(course_key, provider.provider_id, username)
return Response(credit_request)
except CreditApiBadRequest as ex:
raise InvalidCreditRequest(str(ex)) # lint-amnesty, pylint: disable=raise-missing-from
class CreditProviderCallbackView(views.APIView):
""" Callback used by credit providers to update credit request status. """
# This endpoint should be open to all external credit providers.
authentication_classes = ()
permission_classes = ()
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def post(self, request, provider_id):
""" POST handler. """
provider = generics.get_object_or_404(CreditProvider, provider_id=provider_id)
data = request.data
# Ensure the input data is valid
serializer = CreditProviderCallbackSerializer(data=data, provider=provider)
serializer.is_valid(raise_exception=True)
# Update the credit request status
request_uuid = data['request_uuid']
new_status = data['status']
credit_request = generics.get_object_or_404(CreditRequest, uuid=request_uuid, provider=provider)
old_status = credit_request.status
credit_request.status = new_status
credit_request.save()
log.info(
'Updated [%s] CreditRequest [%s] from status [%s] to [%s].',
provider_id, request_uuid, old_status, new_status
)
return Response()
class CreditEligibilityView(generics.ListAPIView):
""" Returns eligibility for a user-course combination. """
authentication_classes = AUTHENTICATION_CLASSES
pagination_class = None
permission_classes = (permissions.IsAuthenticated, IsStaffOrOwner)
serializer_class = CreditEligibilitySerializer
queryset = CreditEligibility.objects.all()
def filter_queryset(self, queryset):
username = self.request.GET.get('username')
course_key = self.request.GET.get('course_key')
if not (username and course_key):
raise ValidationError(
{'detail': 'Both the course_key and username querystring parameters must be supplied.'})
course_key = str(course_key)
try:
course_key = CourseKey.from_string(course_key)
except InvalidKeyError:
raise ValidationError({'detail': f'[{course_key}] is not a valid course key.'}) # lint-amnesty, pylint: disable=raise-missing-from
return queryset.filter(
username=username,
course__course_key=course_key,
deadline__gt=datetime.datetime.now(pytz.UTC)
)
class CreditCourseViewSet(PutAsCreateMixin, mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
""" CreditCourse endpoints. """
lookup_field = 'course_key'
lookup_value_regex = settings.COURSE_KEY_REGEX
queryset = CreditCourse.objects.all()
serializer_class = CreditCourseSerializer
authentication_classes = AUTHENTICATION_CLASSES
permission_classes = (permissions.IsAuthenticated, permissions.IsAdminUser)
# In Django Rest Framework v3, there is a default pagination
# class that transmutes the response data into a dictionary
# with pagination information. The original response data (a list)
# is stored in a "results" value of the dictionary.
# For backwards compatibility with the existing API, we disable
# the default behavior by setting the pagination_class to None.
pagination_class = None
# This CSRF exemption only applies when authenticating without SessionAuthentication.
# SessionAuthentication will enforce CSRF protection.
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def get_object(self):
# Convert the serialized course key into a CourseKey instance
# so we can look up the object.
course_key = self.kwargs.get(self.lookup_field)
if course_key is not None:
self.kwargs[self.lookup_field] = CourseKey.from_string(course_key)
return super().get_object()