From 1ec47e325dfc1be27a3fa8211d7630fd0d32be2b Mon Sep 17 00:00:00 2001
From: Dave Chamberlain <dchamberlain@edx.org>
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/test_views.py  | 159 +++++++++-------------
 credentials/apps/api/urls.py              |   1 +
 credentials/apps/api/v1/filters.py        |  16 +++
 credentials/apps/api/v1/tests/__init__.py |   1 -
 credentials/apps/api/v1/tests/test_api.py |  91 +++++++++++++
 credentials/apps/api/v1/urls.py           |   3 +-
 credentials/apps/api/v1/views.py          |  12 +-
 credentials/apps/api/v2/__init__.py       |   0
 credentials/apps/api/v2/filters.py        |  16 +++
 credentials/apps/api/v2/tests/__init__.py |   0
 credentials/apps/api/v2/tests/test_api.py |  93 +++++++++++++
 credentials/apps/api/v2/urls.py           |  13 ++
 credentials/apps/api/v2/views.py          |  78 +++++++++++
 requirements/base.txt                     |   2 +-
 16 files changed, 381 insertions(+), 119 deletions(-)
 create mode 100644 credentials/apps/api/v1/filters.py
 create mode 100644 credentials/apps/api/v1/tests/test_api.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_api.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 15d6b5af50..73e768139c 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 8dbf0074c5..cd6749039b 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/test_views.py b/credentials/apps/api/tests/test_views.py
index c125deae29..febe6371e0 100644
--- a/credentials/apps/api/tests/test_views.py
+++ b/credentials/apps/api/tests/test_views.py
@@ -1,6 +1,3 @@
-"""
-Tests for credentials service views.
-"""
 # pylint: disable=no-member
 from __future__ import unicode_literals
 import json
@@ -17,20 +14,71 @@
 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 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, 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': []}
+
+
 @ddt.ddt
-class UserCredentialViewSetTests(APITestCase):
+class BaseUserCredentialViewSetTests(object):
     """ Tests for GenerateCredentialView. """
 
-    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 +497,11 @@ 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.
     """
+    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 +527,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 +545,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 +555,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 +610,13 @@ 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. """
 
-    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 6e81274fc0..ae5ac508f9 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 0000000000..bc53837da8
--- /dev/null
+++ b/credentials/apps/api/v1/filters.py
@@ -0,0 +1,16 @@
+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 d25572751d..e69de29bb2 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_api.py b/credentials/apps/api/v1/tests/test_api.py
new file mode 100644
index 0000000000..ff4ace4220
--- /dev/null
+++ b/credentials/apps/api/v1/tests/test_api.py
@@ -0,0 +1,91 @@
+"""
+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.test_views import CredentialViewSetTests, BaseUserCredentialViewSetTests, \
+    BaseUserCredentialViewSetPermissionsTests, BaseCourseCredentialViewSetTests
+from credentials.apps.credentials.models import UserCredential
+from credentials.apps.credentials.tests import factories
+
+
+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_uuid_but_not_id(self):
+        """ Verify a list end point of program credentials will work with
+        program_uuid filter.
+        """
+        self.assert_list_without_id_filter(path=self.list_path, data={'program_uuid': self.program_id}, expected={
+            'error': 'A program_id query string parameter is required for filtering program credentials.'
+        })
+
+    def test_list_with_both_uuid_and_id(self):
+        """ Verify a list end point of program credentials will work with
+        program_uuid filter.
+        """
+        error_message = {'error': 'A program_uuid query string parameter should not appear in V1 queries.'}
+        self.assert_list_without_id_filter(path=self.list_path,
+                                           data={'program_uuid': self.program_id,
+                                                 'program_id': self.program_id},
+                                           expected=error_message)
+
+    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="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, CredentialViewSetTests):
+    list_path = reverse("api:v1:coursecredential-list")
diff --git a/credentials/apps/api/v1/urls.py b/credentials/apps/api/v1/urls.py
index 0186399fa6..98d22d958d 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 9afec21170..f26c044163 100644
--- a/credentials/apps/api/v1/views.py
+++ b/credentials/apps/api/v1/views.py
@@ -1,12 +1,10 @@
-"""
-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.v1.filters import UserCredentialFilter
 
 from credentials.apps.api.permissions import UserCredentialViewSetPermissions
 from credentials.apps.api.serializers import UserCredentialCreationSerializer, UserCredentialSerializer
@@ -46,7 +44,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):
@@ -54,6 +52,10 @@ def list(self, request, *args, **kwargs):
             raise ValidationError(
                 {'error': 'A program_id query string parameter is required for filtering program credentials.'})
 
+        if self.request.query_params.get('program_uuid'):
+            raise ValidationError(
+                {'error': 'A program_uuid query string parameter should not appear in V1 queries.'})
+
         # pylint: disable=maybe-no-member
         return super(ProgramsCredentialsViewSet, self).list(request, *args, **kwargs)
 
diff --git a/credentials/apps/api/v2/__init__.py b/credentials/apps/api/v2/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/credentials/apps/api/v2/filters.py b/credentials/apps/api/v2/filters.py
new file mode 100644
index 0000000000..4dd00f6e9f
--- /dev/null
+++ b/credentials/apps/api/v2/filters.py
@@ -0,0 +1,16 @@
+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 0000000000..e69de29bb2
diff --git a/credentials/apps/api/v2/tests/test_api.py b/credentials/apps/api/v2/tests/test_api.py
new file mode 100644
index 0000000000..2930a2554d
--- /dev/null
+++ b/credentials/apps/api/v2/tests/test_api.py
@@ -0,0 +1,93 @@
+"""
+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.test_views import CredentialViewSetTests, 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(CredentialViewSetTests):
+    """ 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="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, CredentialViewSetTests):
+    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 0000000000..4ff4ed3252
--- /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 0000000000..596b2b9af5
--- /dev/null
+++ b/credentials/apps/api/v2/views.py
@@ -0,0 +1,78 @@
+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.credentials.models import UserCredential
+
+from credentials.apps.api.v2.filters import UserCredentialFilter
+
+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 fa29a3916a..f2e17aee6e 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