From c6fa3bf330ca615c4f6613ba1086073ae5479947 Mon Sep 17 00:00:00 2001 From: Dave Chamberlain Date: Tue, 20 Dec 2016 11:05:13 -0500 Subject: [PATCH] Added V2 functionality to handle queries against a program_uuid instead of program_id Added unit tests for this V2 functionality Adjusted V1 unit tests to confirm it is a V1 request not a V2 ECOM-6482 --- credentials/apps/api/filters.py | 11 -- credentials/apps/api/serializers.py | 4 +- credentials/apps/api/tests/mixins.py | 59 ++++++++++ credentials/apps/api/tests/test_views.py | 114 +++----------------- credentials/apps/api/urls.py | 1 + credentials/apps/api/v1/filters.py | 14 +++ credentials/apps/api/v1/tests/__init__.py | 1 - credentials/apps/api/v1/tests/test_views.py | 76 +++++++++++++ credentials/apps/api/v1/urls.py | 3 +- credentials/apps/api/v1/views.py | 9 +- credentials/apps/api/v2/__init__.py | 0 credentials/apps/api/v2/filters.py | 14 +++ credentials/apps/api/v2/tests/__init__.py | 0 credentials/apps/api/v2/tests/test_views.py | 94 ++++++++++++++++ credentials/apps/api/v2/urls.py | 13 +++ credentials/apps/api/v2/views.py | 76 +++++++++++++ requirements/base.txt | 2 +- 17 files changed, 369 insertions(+), 122 deletions(-) create mode 100644 credentials/apps/api/v1/filters.py create mode 100644 credentials/apps/api/v1/tests/test_views.py create mode 100644 credentials/apps/api/v2/__init__.py create mode 100644 credentials/apps/api/v2/filters.py create mode 100644 credentials/apps/api/v2/tests/__init__.py create mode 100644 credentials/apps/api/v2/tests/test_views.py create mode 100644 credentials/apps/api/v2/urls.py create mode 100644 credentials/apps/api/v2/views.py diff --git a/credentials/apps/api/filters.py b/credentials/apps/api/filters.py index 15d6b5af5..73e768139 100644 --- a/credentials/apps/api/filters.py +++ b/credentials/apps/api/filters.py @@ -5,17 +5,6 @@ from credentials.apps.credentials.models import UserCredential -class ProgramFilter(django_filters.FilterSet): - """ Allows for filtering program credentials by their program_id and status - using a query string argument. - """ - program_id = django_filters.NumberFilter(name="program_credentials__program_id") - - class Meta: - model = UserCredential - fields = ['program_id', 'status'] - - class CourseFilter(django_filters.FilterSet): """ Allows for filtering course credentials by their course_id, status and certificate_type using a query string argument. diff --git a/credentials/apps/api/serializers.py b/credentials/apps/api/serializers.py index 8dbf0074c..cd6749039 100644 --- a/credentials/apps/api/serializers.py +++ b/credentials/apps/api/serializers.py @@ -25,11 +25,11 @@ def to_internal_value(self, data): try: if 'program_id' in data and data.get('program_id'): credential_id = data['program_id'] - return ProgramCertificate.objects.get(program_id=data['program_id'], is_active=True) + return ProgramCertificate.objects.get(program_id=credential_id, is_active=True) elif 'course_id' in data and data.get('course_id') and data.get('certificate_type'): credential_id = data['course_id'] return CourseCertificate.objects.get( - course_id=data['course_id'], + course_id=credential_id, certificate_type=data['certificate_type'], is_active=True ) diff --git a/credentials/apps/api/tests/mixins.py b/credentials/apps/api/tests/mixins.py index 4968a5532..304cacff4 100644 --- a/credentials/apps/api/tests/mixins.py +++ b/credentials/apps/api/tests/mixins.py @@ -2,10 +2,16 @@ Mixins for Credentials API tests. """ from time import time +import json from django.conf import settings +from django.contrib.auth.models import Group +from rest_framework.test import APIRequestFactory import jwt +from credentials.apps.api.serializers import UserCredentialSerializer +from credentials.apps.core.constants import Role +from credentials.apps.core.tests.factories import UserFactory JWT_AUTH = 'JWT_AUTH' @@ -51,3 +57,56 @@ def default_payload(self, user, admin=False, ttl=1): "given_name": "", "family_name": "", } + + +class CredentialViewSetTestsMixin(object): + """ Base Class for ProgramCredentialViewSetTests and CourseCredentialViewSetTests. """ + + list_path = None + user_credential = None + + # pylint: disable=no-member + def setUp(self): + super(CredentialViewSetTestsMixin, self).setUp() + + self.user = UserFactory() + self.user.groups.add(Group.objects.get(name=Role.ADMINS)) + self.client.force_authenticate(self.user) + self.request = APIRequestFactory().get('/') + + def assert_permission_required(self, data): + """ + Ensure access to these APIs is restricted to those with explicit model + permissions. + """ + self.client.force_authenticate(user=UserFactory()) + response = self.client.get(self.list_path, data) + self.assertEqual(response.status_code, 403) + + def assert_list_without_id_filter(self, path, expected, data=None): + """Helper method used for making request and assertions. """ + response = self.client.get(path, data) + + self.assertEqual(response.status_code, 400) + self.assertEqual(response.data, expected) + + def assert_list_with_id_filter(self, data=None, should_exist=True): + """Helper method used for making request and assertions. """ + expected = self._generate_results(should_exist) + response = self.client.get(self.list_path, data) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data, expected) + + def assert_list_with_status_filter(self, data, should_exist=True): + """Helper method for making request and assertions. """ + expected = self._generate_results(should_exist) + response = self.client.get(self.list_path, data, expected) + self.assertEqual(json.loads(response.content), expected) + + def _generate_results(self, exists=True): + if exists: + return {'count': 1, 'next': None, 'previous': None, + 'results': [UserCredentialSerializer(self.user_credential, context={'request': self.request}).data]} + + return {'count': 0, 'next': None, 'previous': None, 'results': []} diff --git a/credentials/apps/api/tests/test_views.py b/credentials/apps/api/tests/test_views.py index c125deae2..efd19c5e3 100644 --- a/credentials/apps/api/tests/test_views.py +++ b/credentials/apps/api/tests/test_views.py @@ -1,14 +1,10 @@ -""" -Tests for credentials service views. -""" -# pylint: disable=no-member from __future__ import unicode_literals import json import ddt from django.contrib.auth.models import Group, Permission from django.core.urlresolvers import reverse -from rest_framework.test import APITestCase, APIRequestFactory +from rest_framework.test import APIRequestFactory from testfixtures import LogCapture from credentials.apps.api.serializers import UserCredentialSerializer @@ -17,20 +13,20 @@ from credentials.apps.credentials.models import UserCredential from credentials.apps.credentials.tests import factories - JSON_CONTENT_TYPE = 'application/json' LOGGER_NAME = 'credentials.apps.credentials.issuers' LOGGER_NAME_SERIALIZER = 'credentials.apps.api.serializers' @ddt.ddt -class UserCredentialViewSetTests(APITestCase): +class BaseUserCredentialViewSetTests(object): """ Tests for GenerateCredentialView. """ + # pylint: disable=no-member - list_path = reverse("api:v1:usercredential-list") + list_path = None def setUp(self): - super(UserCredentialViewSetTests, self).setUp() + super(BaseUserCredentialViewSetTests, self).setUp() self.user = UserFactory() self.client.force_authenticate(self.user) @@ -449,10 +445,13 @@ def test_users_lists_access_by_authenticated_users(self): @ddt.ddt -class UserCredentialViewSetPermissionsTests(APITestCase): +class BaseUserCredentialViewSetPermissionsTests(object): """ Thoroughly exercise the custom view- and object-level permissions for this viewset. """ + # pylint: disable=no-member + + list_path = None def make_user(self, group=None, perm=None, **kwargs): """ DRY helper to create users with specific groups and/or permissions. """ @@ -478,10 +477,9 @@ def test_list(self, user_kwargs, expected_status): The list method (GET) requires either 'view' permission, or for the 'username' query parameter to match that of the requesting user. """ - list_path = reverse("api:v1:usercredential-list") self.client.force_authenticate(self.make_user(**user_kwargs)) - response = self.client.get(list_path, {'username': 'test-user'}) + response = self.client.get(self.list_path, {'username': 'test-user'}) self.assertEqual(response.status_code, expected_status) @ddt.data( @@ -497,7 +495,6 @@ def test_create(self, user_kwargs, expected_status): """ The creation (POST) method requires the 'add' permission. """ - list_path = reverse('api:v1:usercredential-list') program_certificate = factories.ProgramCertificateFactory() post_data = { 'username': 'test-user', @@ -508,7 +505,7 @@ def test_create(self, user_kwargs, expected_status): } self.client.force_authenticate(self.make_user(**user_kwargs)) - response = self.client.post(list_path, data=json.dumps(post_data), content_type=JSON_CONTENT_TYPE) + response = self.client.post(self.list_path, data=json.dumps(post_data), content_type=JSON_CONTENT_TYPE) self.assertEqual(response.status_code, expected_status) @ddt.data( @@ -563,95 +560,14 @@ def test_partial_update(self, user_kwargs, expected_status): self.assertEqual(response.status_code, expected_status) -class CredentialViewSetTests(APITestCase): - """ Base Class for ProgramCredentialViewSetTests and CourseCredentialViewSetTests. """ - - list_path = None - user_credential = None - - def setUp(self): - super(CredentialViewSetTests, self).setUp() - - self.user = UserFactory() - self.user.groups.add(Group.objects.get(name=Role.ADMINS)) - self.client.force_authenticate(self.user) - self.request = APIRequestFactory().get('/') - - def assert_permission_required(self, data): - """ - Ensure access to these APIs is restricted to those with explicit model - permissions. - """ - self.client.force_authenticate(user=UserFactory()) - response = self.client.get(self.list_path, data) - self.assertEqual(response.status_code, 403) - - def assert_list_without_id_filter(self, path, expected): - """Helper method used for making request and assertions. """ - response = self.client.get(path) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.data, expected) - - def assert_list_with_id_filter(self, data): - """Helper method used for making request and assertions. """ - expected = {'count': 1, 'next': None, 'previous': None, - 'results': [UserCredentialSerializer(self.user_credential, context={'request': self.request}).data]} - response = self.client.get(self.list_path, data) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.data, expected) - - def assert_list_with_status_filter(self, data): - """Helper method for making request and assertions. """ - expected = {'count': 1, 'next': None, 'previous': None, - 'results': [UserCredentialSerializer(self.user_credential, context={'request': self.request}).data]} - response = self.client.get(self.list_path, data, expected) - self.assertEqual(json.loads(response.content), expected) - - -class ProgramCredentialViewSetTests(CredentialViewSetTests): - """ Tests for ProgramCredentialViewSetTests. """ - - list_path = reverse("api:v1:programcredential-list") - - def setUp(self): - super(ProgramCredentialViewSetTests, self).setUp() - - self.program_certificate = factories.ProgramCertificateFactory() - self.program_id = self.program_certificate.program_id - self.user_credential = factories.UserCredentialFactory.create(credential=self.program_certificate) - self.request = APIRequestFactory().get('/') - - def test_list_without_program_id(self): - """ Verify a list end point of program credentials will work only with - program_id filter. - """ - self.assert_list_without_id_filter(path=self.list_path, expected={ - 'error': 'A program_id query string parameter is required for filtering program credentials.' - }) - - def test_list_with_program_id_filter(self): - """ Verify the list endpoint supports filter data by program_id.""" - program_cert = factories.ProgramCertificateFactory(program_id=001) - factories.UserCredentialFactory.create(credential=program_cert) - self.assert_list_with_id_filter(data={'program_id': self.program_id}) - - def test_list_with_status_filter(self): - """ Verify the list endpoint supports filtering by status.""" - factories.UserCredentialFactory.create_batch(2, status="revoked", username=self.user_credential.username) - self.assert_list_with_status_filter(data={'program_id': self.program_id, 'status': UserCredential.AWARDED}, ) - - def test_permission_required(self): - """ Verify that requests require explicit model permissions. """ - self.assert_permission_required({'program_id': self.program_id, 'status': UserCredential.AWARDED}) - - -class CourseCredentialViewSetTests(CredentialViewSetTests): +class BaseCourseCredentialViewSetTests(object): """ Tests for CourseCredentialViewSetTests. """ + # pylint: disable=no-member - list_path = reverse("api:v1:coursecredential-list") + list_path = None def setUp(self): - super(CourseCredentialViewSetTests, self).setUp() + super(BaseCourseCredentialViewSetTests, self).setUp() self.course_certificate = factories.CourseCertificateFactory() self.course_id = self.course_certificate.course_id diff --git a/credentials/apps/api/urls.py b/credentials/apps/api/urls.py index 6e81274fc..ae5ac508f 100644 --- a/credentials/apps/api/urls.py +++ b/credentials/apps/api/urls.py @@ -8,4 +8,5 @@ urlpatterns = [ url(r'^v1/', include('credentials.apps.api.v1.urls', namespace='v1')), + url(r'^v2/', include('credentials.apps.api.v2.urls', namespace='v2')), ] diff --git a/credentials/apps/api/v1/filters.py b/credentials/apps/api/v1/filters.py new file mode 100644 index 000000000..9b5d979eb --- /dev/null +++ b/credentials/apps/api/v1/filters.py @@ -0,0 +1,14 @@ +import django_filters + +from credentials.apps.credentials.models import UserCredential + + +class UserCredentialFilter(django_filters.FilterSet): + """ Allows for filtering program credentials by their program_id and status + using a query string argument. + """ + program_id = django_filters.NumberFilter(name="program_credentials__program_id") + + class Meta: + model = UserCredential + fields = ['program_id', 'status'] diff --git a/credentials/apps/api/v1/tests/__init__.py b/credentials/apps/api/v1/tests/__init__.py index d25572751..e69de29bb 100644 --- a/credentials/apps/api/v1/tests/__init__.py +++ b/credentials/apps/api/v1/tests/__init__.py @@ -1 +0,0 @@ -# Create your tests in sub-packages prefixed with "test_" (e.g. test_views). diff --git a/credentials/apps/api/v1/tests/test_views.py b/credentials/apps/api/v1/tests/test_views.py new file mode 100644 index 000000000..2905ba392 --- /dev/null +++ b/credentials/apps/api/v1/tests/test_views.py @@ -0,0 +1,76 @@ +""" +Tests for credentials service views. +""" +# pylint: disable=no-member +from __future__ import unicode_literals + +from django.core.urlresolvers import reverse +from rest_framework.test import APIRequestFactory, APITestCase + +from credentials.apps.api.tests.mixins import CredentialViewSetTestsMixin +from credentials.apps.api.tests.test_views import ( + BaseUserCredentialViewSetTests, BaseUserCredentialViewSetPermissionsTests, BaseCourseCredentialViewSetTests, +) +from credentials.apps.credentials.models import UserCredential +from credentials.apps.credentials.tests import factories + + +class ProgramCredentialViewSetTests(CredentialViewSetTestsMixin, APITestCase): + """ Tests for ProgramCredentialViewSetTests. """ + + list_path = reverse("api:v1:programcredential-list") + + def setUp(self): + super(ProgramCredentialViewSetTests, self).setUp() + + self.program_certificate = factories.ProgramCertificateFactory() + self.program_id = self.program_certificate.program_id + self.user_credential = factories.UserCredentialFactory.create(credential=self.program_certificate) + self.request = APIRequestFactory().get('/') + + def test_list_without_program_id(self): + """ Verify a list end point of program credentials will work only with + program_id filter. + """ + self.assert_list_without_id_filter(path=self.list_path, expected={ + 'error': 'A program_id query string parameter is required for filtering program credentials.' + }) + + def test_list_with_program_id_filter(self): + """ Verify the list endpoint supports filter data by program_id.""" + program_cert = factories.ProgramCertificateFactory(program_id=1) + factories.UserCredentialFactory.create(credential=program_cert) + self.assert_list_with_id_filter(data={'program_id': self.program_id}) + + def test_list_with_program_invalid_id_filter(self): + """ Verify the list endpoint supports filter data by program_id.""" + program_cert = factories.ProgramCertificateFactory(program_id=1) + factories.UserCredentialFactory.create(credential=program_cert) + self.assert_list_with_id_filter(data={'program_id': 50}, should_exist=False) + + def test_list_with_status_filter(self): + """ Verify the list endpoint supports filtering by status.""" + factories.UserCredentialFactory.create_batch(2, status=UserCredential.REVOKED, + username=self.user_credential.username) + self.assert_list_with_status_filter(data={'program_id': self.program_id, 'status': UserCredential.AWARDED}) + + def test_list_with_bad_status_filter(self): + """ Verify the list endpoint supports filtering by status.""" + self.assert_list_with_status_filter(data={'program_id': self.program_id, 'status': UserCredential.REVOKED}, + should_exist=False) + + def test_permission_required(self): + """ Verify that requests require explicit model permissions. """ + self.assert_permission_required({'program_id': self.program_id, 'status': UserCredential.AWARDED}) + + +class UserCredentialViewSetTests(BaseUserCredentialViewSetTests, APITestCase): + list_path = reverse("api:v1:usercredential-list") + + +class UserCredentialViewSetPermissionsTests(BaseUserCredentialViewSetPermissionsTests, APITestCase): + list_path = reverse("api:v1:usercredential-list") + + +class CourseCredentialViewSetTests(BaseCourseCredentialViewSetTests, CredentialViewSetTestsMixin, APITestCase): + list_path = reverse("api:v1:coursecredential-list") diff --git a/credentials/apps/api/v1/urls.py b/credentials/apps/api/v1/urls.py index 0186399fa..98d22d958 100644 --- a/credentials/apps/api/v1/urls.py +++ b/credentials/apps/api/v1/urls.py @@ -1,11 +1,10 @@ -""" API v1 URLs. """ from rest_framework.routers import DefaultRouter from credentials.apps.api.v1 import views router = DefaultRouter() # pylint: disable=invalid-name -# URL can not have hyphen as it is not currently supported by slumber +# URLs can not have hyphen as it is not currently supported by slumber # as mentioned https://github.com/samgiles/slumber/issues/44 router.register(r'user_credentials', views.UserCredentialViewSet) router.register(r'program_credentials', views.ProgramsCredentialsViewSet, base_name='programcredential') diff --git a/credentials/apps/api/v1/views.py b/credentials/apps/api/v1/views.py index 9afec2117..c8070b27c 100644 --- a/credentials/apps/api/v1/views.py +++ b/credentials/apps/api/v1/views.py @@ -1,18 +1,15 @@ -""" -Credentials service API views (v1). -""" import logging from django.http import Http404 from rest_framework import mixins, viewsets from rest_framework.exceptions import ValidationError -from credentials.apps.api.filters import ProgramFilter, CourseFilter +from credentials.apps.api.filters import CourseFilter from credentials.apps.api.permissions import UserCredentialViewSetPermissions from credentials.apps.api.serializers import UserCredentialCreationSerializer, UserCredentialSerializer +from credentials.apps.api.v1.filters import UserCredentialFilter from credentials.apps.credentials.models import UserCredential - log = logging.getLogger(__name__) @@ -46,7 +43,7 @@ def create(self, request, *args, **kwargs): class ProgramsCredentialsViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): """It will return the all credentials for programs.""" queryset = UserCredential.objects.all() - filter_class = ProgramFilter + filter_class = UserCredentialFilter serializer_class = UserCredentialSerializer def list(self, request, *args, **kwargs): diff --git a/credentials/apps/api/v2/__init__.py b/credentials/apps/api/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/credentials/apps/api/v2/filters.py b/credentials/apps/api/v2/filters.py new file mode 100644 index 000000000..8ffa83275 --- /dev/null +++ b/credentials/apps/api/v2/filters.py @@ -0,0 +1,14 @@ +import django_filters + +from credentials.apps.credentials.models import UserCredential + + +class UserCredentialFilter(django_filters.FilterSet): + """ Allows for filtering program credentials by their program_uuid and status + using a query string argument. + """ + program_uuid = django_filters.UUIDFilter(name="program_credentials__program_uuid") + + class Meta: + model = UserCredential + fields = ['program_uuid', 'status'] diff --git a/credentials/apps/api/v2/tests/__init__.py b/credentials/apps/api/v2/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/credentials/apps/api/v2/tests/test_views.py b/credentials/apps/api/v2/tests/test_views.py new file mode 100644 index 000000000..50c508a30 --- /dev/null +++ b/credentials/apps/api/v2/tests/test_views.py @@ -0,0 +1,94 @@ +""" +Tests for credentials service views. +""" +from __future__ import unicode_literals + +from django.core.urlresolvers import reverse +from rest_framework.test import APIRequestFactory, APITestCase + +from credentials.apps.api.tests.mixins import CredentialViewSetTestsMixin +from credentials.apps.api.tests.test_views import ( + BaseUserCredentialViewSetTests, BaseUserCredentialViewSetPermissionsTests, BaseCourseCredentialViewSetTests, +) +from credentials.apps.credentials.models import UserCredential +from credentials.apps.credentials.tests import factories + +JSON_CONTENT_TYPE = 'application/json' +LOGGER_NAME = 'credentials.apps.credentials.issuers' +LOGGER_NAME_SERIALIZER = 'credentials.apps.api.serializers' + + +class ProgramCredentialViewSetTests(CredentialViewSetTestsMixin, APITestCase): + """ Tests for ProgramCredentialViewSetTests. """ + + list_path = reverse("api:v2:programcredential-list") + + def setUp(self): + super(ProgramCredentialViewSetTests, self).setUp() + + self.program_certificate = factories.ProgramCertificateFactory() + self.program_id = self.program_certificate.program_id + self.program_uuid = self.program_certificate.program_uuid + self.user_credential = factories.UserCredentialFactory.create(credential=self.program_certificate) + self.request = APIRequestFactory().get('/') + + def test_list_without_uuid(self): + """ Verify a list end point of program credentials will work with + program_uuid filter. + """ + error_message = {'error': 'A UUID query string parameter is required for filtering program credentials.'} + self.assert_list_without_id_filter(path=self.list_path, expected=error_message) + + def test_list_without_uuid_but_with_id(self): + """ Verify a list end point of program credentials will work with + program_uuid filter. + """ + error_message = {'error': 'A UUID query string parameter is required for filtering program credentials.'} + self.assert_list_without_id_filter(path=self.list_path, + data={'program_id': self.program_id}, + expected=error_message) + + def test_list_with_uuid_and_id(self): + """ Verify a list end point of program credentials will not work with + program_id filter. + """ + error_message = {'error': 'A program_id query string parameter was found in a V2 API request.'} + self.assert_list_without_id_filter(path=self.list_path, + data={'program_uuid': self.program_uuid, 'program_id': self.program_id}, + expected=error_message) + + def test_list_with_program_uuid_filter(self): + """ Verify the list endpoint supports filter data by program_uuid.""" + self.assert_list_with_id_filter(data={'program_uuid': self.program_uuid}) + + def test_list_with_invalid_uuid(self): + """ Verify the list endpoint will fail if given a bad uuid.""" + self.program_uuid = '12345678=0DAC-CAD0-ABCD-fedcba987654' + self.assert_list_with_id_filter(data={'program_uuid': self.program_uuid}, should_exist=False) + + def test_list_with_status_filter(self): + """ Verify the list endpoint supports filtering by status.""" + factories.UserCredentialFactory.create_batch(2, status=UserCredential.REVOKED, + username=self.user_credential.username) + self.assert_list_with_status_filter(data={'program_uuid': self.program_uuid, 'status': UserCredential.AWARDED}) + + def test_list_with_bad_status_filter(self): + """ Verify the list endpoint supports filtering by status when there isn't anything available.""" + self.assert_list_with_status_filter(data={'program_uuid': self.program_uuid, 'status': UserCredential.REVOKED}, + should_exist=False) + + def test_permission_required(self): + """ Verify that requests require explicit model permissions. """ + self.assert_permission_required({'program_uuid': self.program_uuid, 'status': UserCredential.AWARDED}) + + +class UserCredentialViewSetTests(BaseUserCredentialViewSetTests, APITestCase): + list_path = reverse("api:v2:usercredential-list") + + +class UserCredentialViewSetPermissionsTests(BaseUserCredentialViewSetPermissionsTests, APITestCase): + list_path = reverse("api:v2:usercredential-list") + + +class CourseCredentialViewSetTests(BaseCourseCredentialViewSetTests, CredentialViewSetTestsMixin, APITestCase): + list_path = reverse("api:v2:coursecredential-list") diff --git a/credentials/apps/api/v2/urls.py b/credentials/apps/api/v2/urls.py new file mode 100644 index 000000000..4ff4ed325 --- /dev/null +++ b/credentials/apps/api/v2/urls.py @@ -0,0 +1,13 @@ +from rest_framework.routers import DefaultRouter + +from credentials.apps.api.v2 import views + + +router = DefaultRouter() # pylint: disable=invalid-name +# URLs can not have hyphen as it is not currently supported by slumber +# as mentioned https://github.com/samgiles/slumber/issues/44 +router.register(r'user_credentials', views.UserCredentialViewSet) +router.register(r'program_credentials', views.ProgramsCredentialsViewSet, base_name='programcredential') +router.register(r'course_credentials', views.CourseCredentialsViewSet, base_name='coursecredential') + +urlpatterns = router.urls diff --git a/credentials/apps/api/v2/views.py b/credentials/apps/api/v2/views.py new file mode 100644 index 000000000..770700c41 --- /dev/null +++ b/credentials/apps/api/v2/views.py @@ -0,0 +1,76 @@ +import logging + +from django.http import Http404 +from rest_framework import mixins, viewsets +from rest_framework.exceptions import ValidationError + +from credentials.apps.api.filters import CourseFilter +from credentials.apps.api.permissions import UserCredentialViewSetPermissions +from credentials.apps.api.serializers import UserCredentialCreationSerializer, UserCredentialSerializer +from credentials.apps.api.v2.filters import UserCredentialFilter +from credentials.apps.credentials.models import UserCredential + +log = logging.getLogger(__name__) + + +class UserCredentialViewSet(viewsets.ModelViewSet): + """ UserCredentials endpoints. """ + + queryset = UserCredential.objects.all() + filter_fields = ('username', 'status') + serializer_class = UserCredentialSerializer + permission_classes = (UserCredentialViewSetPermissions,) + + def list(self, request, *args, **kwargs): + if not request.query_params.get('username'): + raise ValidationError( + {'error': 'A username query string parameter is required for filtering user credentials.'}) + + # provide an additional permission check related to the username + # query string parameter. See also `UserCredentialViewSetPermissions` + if not request.user.has_perm('credentials.view_usercredential') and ( + request.user.username.lower() != request.query_params['username'].lower() + ): + raise Http404 + + return super(UserCredentialViewSet, self).list(request, *args, **kwargs) # pylint: disable=maybe-no-member + + def create(self, request, *args, **kwargs): + self.serializer_class = UserCredentialCreationSerializer + return super(UserCredentialViewSet, self).create(request, *args, **kwargs) + + +class ProgramsCredentialsViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + """It will return the all credentials for programs based on the program_uuid.""" + queryset = UserCredential.objects.all() + filter_class = UserCredentialFilter + serializer_class = UserCredentialSerializer + + def list(self, request, *args, **kwargs): + # Validate that we do have a program_uuid to use + if not self.request.query_params.get('program_uuid'): + raise ValidationError( + {'error': 'A UUID query string parameter is required for filtering program credentials.'}) + + # Confirmation that we are not supplying both parameters. We should only be providing the program_uuid in V2 + if self.request.query_params.get('program_id'): + raise ValidationError( + {'error': 'A program_id query string parameter was found in a V2 API request.'}) + + # pylint: disable=maybe-no-member + return super(ProgramsCredentialsViewSet, self).list(request, *args, **kwargs) + + +class CourseCredentialsViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + """It will return the all credentials for courses.""" + queryset = UserCredential.objects.all() + filter_class = CourseFilter + serializer_class = UserCredentialSerializer + + def list(self, request, *args, **kwargs): + if not self.request.query_params.get('course_id'): + raise ValidationError( + {'error': 'A course_id query string parameter is required for filtering course credentials.'}) + + # pylint: disable=maybe-no-member + return super(CourseCredentialsViewSet, self).list(request, *args, **kwargs) diff --git a/requirements/base.txt b/requirements/base.txt index b2623690f..b8abda25b 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ django-extensions==1.6.1 django-libsass==0.6 django-storages-redux==1.3 django-waffle==0.11.1 -django-filter==0.11.0 +django-filter==1.0.1 djangorestframework==3.2.3 djangorestframework-jwt==1.7.2 django-rest-swagger==0.3.4