Files
edx-platform/openedx/core/djangoapps/credit/views.py
Manjinder Singh eb694528e7 Adding metrics to oauth2authentication class (#22970)
Currently, we are working on removing the rest_framework_auth library from edx-platform. For this push, we need to remove the oauth2Authentication class. This PR creates a new class oauth2AuthenticationDeprecated that adds additional new relic metrics. The metrics would allow us to see how often this class is used and its success rate. The hope is that this information will help us with transitioning to a different authentication class.
2020-01-30 09:13:51 -05:00

207 lines
7.6 KiB
Python

"""
Views for the credit Django app.
"""
import datetime
import logging
import pytz
import six
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.lib.api.authentication import OAuth2AuthenticationDeprecated
from six import text_type
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.mixins import PutAsCreateMixin
from openedx.core.lib.api.permissions import IsStaffOrOwner
log = logging.getLogger(__name__)
AUTHENTICATION_CLASSES = (JwtAuthentication, OAuth2AuthenticationDeprecated, 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(CreditProviderViewSet, self).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)
# 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(text_type(ex))
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(CreditProviderCallbackView, self).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 = six.text_type(course_key)
try:
course_key = CourseKey.from_string(course_key)
except InvalidKeyError:
raise ValidationError({'detail': '[{}] is not a valid course key.'.format(course_key)})
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(CreditCourseViewSet, self).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(CreditCourseViewSet, self).get_object()