diff --git a/enterprise_catalog/apps/academy/tests/factories.py b/enterprise_catalog/apps/academy/tests/factories.py new file mode 100644 index 000000000..c12e6faf6 --- /dev/null +++ b/enterprise_catalog/apps/academy/tests/factories.py @@ -0,0 +1,58 @@ +from uuid import uuid4 + +import factory +from factory.fuzzy import FuzzyText + +from enterprise_catalog.apps.academy.models import Academy, Tag +from enterprise_catalog.apps.catalog.tests.factories import ( + EnterpriseCatalogFactory, +) + + +class TagFactory(factory.django.DjangoModelFactory): + """ + Test factory for the `Tag` model + """ + class Meta: + model = Tag + + +class AcademyFactory(factory.django.DjangoModelFactory): + """ + Test factory for the `Academy` model + """ + class Meta: + model = Academy + + uuid = factory.LazyFunction(uuid4) + title = FuzzyText(length=32) + short_description = FuzzyText(length=32) + long_description = FuzzyText(length=255) + + @factory.post_generation + def enterprise_catalogs(self, create, extracted, **kwargs): + if not create: + # Simple build, do nothing. + return + + if extracted: + for enterprise_catalog in extracted: + self.enterprise_catalogs.add(enterprise_catalog) # pylint: disable=no-member + else: + enterprise_catalog1 = EnterpriseCatalogFactory() + enterprise_catalog2 = EnterpriseCatalogFactory() + self.enterprise_catalogs.set([enterprise_catalog1, enterprise_catalog2]) # pylint: disable=no-member + + @factory.post_generation + def tags(self, create, extracted, **kwargs): + if not create: + # Simple build, do nothing. + return + + if extracted: + for tag in extracted: + self.tags.add(tag) # pylint: disable=no-member + else: + tag1 = TagFactory() + tag2 = TagFactory() + self.tags.set([tag1, tag2]) # pylint: disable=no-member diff --git a/enterprise_catalog/apps/api/v1/serializers.py b/enterprise_catalog/apps/api/v1/serializers.py index 3ec2acf45..3df2dc1ba 100644 --- a/enterprise_catalog/apps/api/v1/serializers.py +++ b/enterprise_catalog/apps/api/v1/serializers.py @@ -4,6 +4,7 @@ from django.db import IntegrityError, models from rest_framework import serializers, status +from enterprise_catalog.apps.academy.models import Academy, Tag from enterprise_catalog.apps.api.v1.utils import ( get_enterprise_utm_context, get_most_recent_modified_time, @@ -392,3 +393,25 @@ def get_highlight_sets(self, obj): } for highlight_set in catalog_highlight_sets ] + + +class TagsSerializer(serializers.ModelSerializer): + """ + Serializer for the `Tag` model. + """ + class Meta: + model = Tag + fields = '__all__' + + +class AcademySerializer(serializers.ModelSerializer): + """ + Serializer for the `Academy` model. + """ + enterprise_catalogs = EnterpriseCatalogSerializer(many=True) + tags = TagsSerializer(many=True) + + class Meta: + model = Academy + fields = '__all__' + lookup_field = 'uuid' diff --git a/enterprise_catalog/apps/api/v1/tests/test_views.py b/enterprise_catalog/apps/api/v1/tests/test_views.py index d500960e6..ee679145a 100644 --- a/enterprise_catalog/apps/api/v1/tests/test_views.py +++ b/enterprise_catalog/apps/api/v1/tests/test_views.py @@ -10,12 +10,14 @@ import pytz from django.conf import settings from django.db import IntegrityError +from django.utils.http import urlencode from django.utils.text import slugify from rest_framework import status from rest_framework.reverse import reverse from rest_framework.settings import api_settings from six.moves.urllib.parse import quote_plus +from enterprise_catalog.apps.academy.tests.factories import AcademyFactory from enterprise_catalog.apps.api.v1.serializers import ContentMetadataSerializer from enterprise_catalog.apps.api.v1.tests.mixins import APITestMixin from enterprise_catalog.apps.api.v1.utils import is_any_course_run_active @@ -2312,3 +2314,54 @@ def test_content_metadata_delete_not_implemented(self): """ response = self.client.delete(urljoin(self.url, f"{self.content_key_1}/")) assert response.status_code == 405 + + +@ddt.ddt +class AcademiesViewSetTests(APITestMixin): + """ + Tests for the AcademyViewSet. + """ + def setUp(self): + super().setUp() + self.set_up_catalog_learner() + self.academy1 = AcademyFactory() + self.academy2 = AcademyFactory() + self.enterprise_catalog_query = CatalogQueryFactory(uuid=uuid.uuid4()) + self.enterprise_catalog1 = EnterpriseCatalogFactory(catalog_query=self.enterprise_catalog_query) + self.enterprise_catalog1.academies.add(self.academy1) + self.enterprise_catalog2 = EnterpriseCatalogFactory(catalog_query=self.enterprise_catalog_query) + self.enterprise_catalog2.academies.add(self.academy2) + + def test_list_for_academies(self): + """ + Verify the viewset returns enterprise specific academies + """ + params = { + 'enterprise_customer': str(self.enterprise_catalog2.enterprise_customer.uuid) + } + url = reverse('api:v1:academies-list') + '?{}'.format(urlencode(params)) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['count'], 1) + results = response.data['results'] + self.assertEqual(uuid.UUID(results[0]['uuid']), self.academy2.uuid) + + def test_retrieve_for_academies(self): + """ + Verify the viewset retrieves an academy + """ + url = reverse('api:v1:academies-detail', kwargs={ + 'uuid': self.academy2.uuid, + }) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(uuid.UUID(response.data['uuid']), self.academy2.uuid) + + def test_list_with_missing_enterprise_customer(self): + """ + Verify the viewset returns no records when enterprise customer is missing in params + """ + url = reverse('api:v1:academies-list') + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['count'], 0) diff --git a/enterprise_catalog/apps/api/v1/urls.py b/enterprise_catalog/apps/api/v1/urls.py index 284c6da9d..351effbe3 100644 --- a/enterprise_catalog/apps/api/v1/urls.py +++ b/enterprise_catalog/apps/api/v1/urls.py @@ -4,6 +4,9 @@ from django.urls import path, re_path from rest_framework.routers import DefaultRouter +from enterprise_catalog.apps.api.v1.views.academies import ( + AcademiesReadOnlyViewSet, +) from enterprise_catalog.apps.api.v1.views.catalog_csv import CatalogCsvView from enterprise_catalog.apps.api.v1.views.catalog_csv_data import ( CatalogCsvDataView, @@ -53,6 +56,7 @@ router.register(r'enterprise-curations-admin', EnterpriseCurationConfigViewSet, basename='enterprise-curations-admin') router.register(r'highlight-sets', HighlightSetReadOnlyViewSet, basename='highlight-sets') router.register(r'highlight-sets-admin', HighlightSetViewSet, basename='highlight-sets-admin') +router.register(r'academies', AcademiesReadOnlyViewSet, basename='academies') urlpatterns = [ path('enterprise-catalogs/catalog_csv_data', CatalogCsvDataView.as_view(), @@ -67,6 +71,12 @@ path('enterprise-catalogs/catalog_workbook', CatalogWorkbookView.as_view(), name='catalog-workbook' ), + path('academies', AcademiesReadOnlyViewSet.as_view({'get': 'list'}), + name='academies-list' + ), + path('academies//', AcademiesReadOnlyViewSet.as_view({'get': 'retrieve'}), + name='academies-detail' + ), re_path( r'^enterprise-catalogs/(?P[\S]+)/get_content_metadata', EnterpriseCatalogGetContentMetadata.as_view({'get': 'get'}), diff --git a/enterprise_catalog/apps/api/v1/views/academies.py b/enterprise_catalog/apps/api/v1/views/academies.py new file mode 100644 index 000000000..3d5a79c67 --- /dev/null +++ b/enterprise_catalog/apps/api/v1/views/academies.py @@ -0,0 +1,46 @@ +from django.utils.functional import cached_property +from edx_rest_framework_extensions.auth.jwt.authentication import ( + JwtAuthentication, +) +from rest_framework import permissions, viewsets +from rest_framework.authentication import SessionAuthentication +from rest_framework.renderers import JSONRenderer +from rest_framework_xml.renderers import XMLRenderer + +from enterprise_catalog.apps.academy.models import Academy +from enterprise_catalog.apps.api.v1.serializers import AcademySerializer + + +class AcademiesReadOnlyViewSet(viewsets.ReadOnlyModelViewSet): + """ Viewset for Read Only operations on Academies """ + authentication_classes = [JwtAuthentication, SessionAuthentication] + permission_classes = [permissions.IsAuthenticated] + renderer_classes = [JSONRenderer, XMLRenderer] + serializer_class = AcademySerializer + lookup_field = 'uuid' + + @cached_property + def request_action(self): + return getattr(self, 'action', None) + + def get_queryset(self): + """ + Returns the queryset corresponding to all academies the requesting user has access to. + """ + enterprise_customer = self.request.GET.get('enterprise_customer', False) + all_academies = Academy.objects.all() + if self.request_action == 'list': + if enterprise_customer: + user_accessible_academy_uuids = [] + for academy in all_academies: + academy_associated_catalogs = academy.enterprise_catalogs.all() + enterprise_associated_catalogs = academy_associated_catalogs.filter( + enterprise_uuid=enterprise_customer + ) + if enterprise_associated_catalogs: + user_accessible_academy_uuids.append(academy.uuid) + return all_academies.filter(uuid__in=user_accessible_academy_uuids) + else: + return Academy.objects.none() + + return Academy.objects.all()