From 619420bb80e9e0a0d5c095138869efc36090d52c Mon Sep 17 00:00:00 2001 From: Renzo Lucioni Date: Thu, 11 Feb 2016 12:28:25 -0500 Subject: [PATCH] Add API endpoint for issuing programs certificates Allows staff users to manually enqueue the task responsible for awarding programs certificates. ECOM-3692. --- lms/djangoapps/support/tests/test_programs.py | 70 +++++++++++++++++++ lms/djangoapps/support/urls.py | 1 + lms/djangoapps/support/views/__init__.py | 1 + lms/djangoapps/support/views/programs.py | 57 +++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 lms/djangoapps/support/tests/test_programs.py create mode 100644 lms/djangoapps/support/views/programs.py diff --git a/lms/djangoapps/support/tests/test_programs.py b/lms/djangoapps/support/tests/test_programs.py new file mode 100644 index 0000000000..9d2356e40f --- /dev/null +++ b/lms/djangoapps/support/tests/test_programs.py @@ -0,0 +1,70 @@ +# pylint: disable=missing-docstring +from django.core.urlresolvers import reverse +from django.test import TestCase +import mock +from oauth2_provider.tests.factories import AccessTokenFactory, ClientFactory + +from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin +from student.tests.factories import UserFactory + + +class IssueProgramCertificatesViewTests(TestCase, ProgramsApiConfigMixin): + password = 'password' + + def setUp(self): + super(IssueProgramCertificatesViewTests, self).setUp() + + self.create_programs_config() + + self.path = reverse('support:programs-certify') + self.user = UserFactory(password=self.password, is_staff=True) + self.data = {'username': self.user.username} + self.headers = {} + + self.client.login(username=self.user.username, password=self.password) + + def _verify_response(self, status_code): + """Verify that the endpoint returns the provided status code and enqueues the task if appropriate.""" + with mock.patch('lms.djangoapps.support.views.programs.award_program_certificates.delay') as mock_task: + response = self.client.post(self.path, self.data, **self.headers) + + self.assertEqual(response.status_code, status_code) + self.assertEqual(status_code == 200, mock_task.called) + + def test_authentication_required(self): + """Verify that the endpoint requires authentication.""" + self.client.logout() + + self._verify_response(403) + + def test_session_auth(self): + """Verify that the endpoint supports session auth.""" + self._verify_response(200) + + def test_oauth(self): + """Verify that the endpoint supports OAuth 2.0.""" + access_token = AccessTokenFactory(user=self.user, client=ClientFactory()).token # pylint: disable=no-member + self.headers['HTTP_AUTHORIZATION'] = 'Bearer ' + access_token + + self.client.logout() + + self._verify_response(200) + + def test_staff_permissions_required(self): + """Verify that staff permissions are required to access the endpoint.""" + self.user.is_staff = False + self.user.save() # pylint: disable=no-member + + self._verify_response(403) + + def test_certification_disabled(self): + """Verify that the endpoint returns a 400 when program certification is disabled.""" + self.create_programs_config(enable_certification=False) + + self._verify_response(400) + + def test_username_required(self): + """Verify that the endpoint returns a 400 when a username isn't provided.""" + self.data.pop('username') + + self._verify_response(400) diff --git a/lms/djangoapps/support/urls.py b/lms/djangoapps/support/urls.py index 50ebe6021c..e0e0c9aad8 100644 --- a/lms/djangoapps/support/urls.py +++ b/lms/djangoapps/support/urls.py @@ -16,4 +16,5 @@ urlpatterns = patterns( views.EnrollmentSupportListView.as_view(), name="enrollment_list" ), + url(r'^programs/certify/$', views.IssueProgramCertificatesView.as_view(), name='programs-certify'), ) diff --git a/lms/djangoapps/support/views/__init__.py b/lms/djangoapps/support/views/__init__.py index fa1d3c46a9..2c0ce8e9f7 100644 --- a/lms/djangoapps/support/views/__init__.py +++ b/lms/djangoapps/support/views/__init__.py @@ -6,3 +6,4 @@ from .index import * from .certificate import * from .enrollments import * from .refund import * +from .programs import IssueProgramCertificatesView diff --git a/lms/djangoapps/support/views/programs.py b/lms/djangoapps/support/views/programs.py new file mode 100644 index 0000000000..2fdf72ae46 --- /dev/null +++ b/lms/djangoapps/support/views/programs.py @@ -0,0 +1,57 @@ +# pylint: disable=missing-docstring +import logging + +from rest_framework import permissions, status, views +from rest_framework.authentication import SessionAuthentication +from rest_framework.response import Response +from rest_framework_oauth.authentication import OAuth2Authentication + +from openedx.core.djangoapps.programs.models import ProgramsApiConfig +from openedx.core.djangoapps.programs.tasks.v1.tasks import award_program_certificates + + +log = logging.getLogger(__name__) + + +class IssueProgramCertificatesView(views.APIView): + """ + **Use Cases** + + Trigger the task responsible for awarding program certificates on behalf + of the user with the provided username. + + **Example Requests** + + POST /support/programs/certify/ + { + 'username': 'foo' + } + + **Returns** + + * 200 on success. + * 400 if program certification is disabled or a username is not provided. + * 401 if the request is not authenticated. + * 403 if the authenticated user does not have staff permissions. + """ + authentication_classes = (SessionAuthentication, OAuth2Authentication) + permission_classes = (permissions.IsAuthenticated, permissions.IsAdminUser,) + + def post(self, request): + if not ProgramsApiConfig.current().is_certification_enabled: + return Response( + {'error': 'Program certification is disabled.'}, + status=status.HTTP_400_BAD_REQUEST + ) + + username = request.data.get('username') + if username: + log.info('Enqueuing program certification task for user [%s]', username) + award_program_certificates.delay(username) + + return Response() + else: + return Response( + {'error': 'A username is required in order to issue program certificates.'}, + status=status.HTTP_400_BAD_REQUEST + )