feat: api to fetch all programs where an enterprise learner is enrolled

This commit is contained in:
muhammad-ammar
2022-03-02 12:14:57 +05:00
parent 0d9e845f1b
commit c35025fd9d
4 changed files with 261 additions and 8 deletions

View File

@@ -5,11 +5,15 @@ Unit tests for Learner Dashboard REST APIs and Views
from unittest import mock from unittest import mock
from uuid import uuid4 from uuid import uuid4
from django.core.cache import cache
from django.urls import reverse_lazy from django.urls import reverse_lazy
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory as ModuleStoreCourseFactory from xmodule.modulestore.tests.factories import CourseFactory as ModuleStoreCourseFactory
from common.djangoapps.student.tests.factories import UserFactory from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
from lms.djangoapps.program_enrollments.rest_api.v1.tests.test_views import ProgramCacheMixin
from lms.djangoapps.program_enrollments.tests.factories import ProgramEnrollmentFactory
from openedx.core.djangoapps.catalog.cache import SITE_PROGRAM_UUIDS_CACHE_KEY_TPL
from openedx.core.djangoapps.catalog.constants import PathwayType from openedx.core.djangoapps.catalog.constants import PathwayType
from openedx.core.djangoapps.catalog.tests.factories import ( from openedx.core.djangoapps.catalog.tests.factories import (
CourseFactory, CourseFactory,
@@ -18,7 +22,14 @@ from openedx.core.djangoapps.catalog.tests.factories import (
ProgramFactory ProgramFactory
) )
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration
from openedx.core.djangolib.testing.utils import skip_unless_lms from openedx.core.djangolib.testing.utils import skip_unless_lms
from openedx.features.enterprise_support.tests.factories import (
EnterpriseCourseEnrollmentFactory,
EnterpriseCustomerFactory,
EnterpriseCustomerUserFactory
)
PROGRAMS_UTILS_MODULE = 'openedx.core.djangoapps.programs.utils' PROGRAMS_UTILS_MODULE = 'openedx.core.djangoapps.programs.utils'
@@ -116,3 +127,84 @@ class TestProgramProgressDetailView(ProgramsApiConfigMixin, SharedModuleStoreTes
response = self.client.get(self.url) response = self.client.get(self.url)
assert response.status_code == 404 assert response.status_code == 404
assert response.data['error_code'] == 'No program data available.' assert response.data['error_code'] == 'No program data available.'
class TestProgramsView(SharedModuleStoreTestCase, ProgramCacheMixin):
"""Unit tests for the program details page."""
enterprise_uuid = str(uuid4())
program_uuid = str(uuid4())
password = 'test'
url = reverse_lazy('learner_dashboard:v0:program_list', kwargs={'enterprise_uuid': enterprise_uuid})
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user = UserFactory()
modulestore_course = ModuleStoreCourseFactory()
course_run = CourseRunFactory(key=str(modulestore_course.id))
course = CourseFactory(course_runs=[course_run])
enterprise_customer = EnterpriseCustomerFactory(uuid=cls.enterprise_uuid)
enterprise_customer_user = EnterpriseCustomerUserFactory(
user_id=cls.user.id,
enterprise_customer=enterprise_customer
)
CourseEnrollmentFactory(
is_active=True,
course_id=modulestore_course.id,
user=cls.user
)
EnterpriseCourseEnrollmentFactory(
course_id=modulestore_course.id,
enterprise_customer_user=enterprise_customer_user
)
cls.program = ProgramFactory(
uuid=cls.program_uuid,
courses=[course],
title='Journey to cooking',
type='MicroMasters',
authoring_organizations=[{
'key': 'MAX'
}],
)
cls.site = SiteFactory(domain='test.localhost')
def setUp(self):
super().setUp()
self.client.login(username=self.user.username, password=self.password)
self.set_program_in_catalog_cache(self.program_uuid, self.program)
ProgramEnrollmentFactory.create(
user=self.user,
program_uuid=self.program_uuid,
external_user_key='0001',
)
@with_site_configuration(configuration={'COURSE_CATALOG_API_URL': 'foo'})
def test_program_list(self):
"""
Verify API returns proper response.
"""
cache.set(
SITE_PROGRAM_UUIDS_CACHE_KEY_TPL.format(domain=self.site.domain),
[self.program_uuid],
None
)
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
program = response.data[0]
assert len(program)
assert program['uuid'] == self.program['uuid']
assert program['title'] == self.program['title']
assert program['type'] == self.program['type']
assert program['authoring_organizations'] == self.program['authoring_organizations']
assert program['banner_image'] == self.program['banner_image']
assert program['progress'] == {
'uuid': self.program['uuid'],
'completed': 0,
'in_progress': 0,
'not_started': 1
}

View File

@@ -4,10 +4,17 @@ Learner Dashboard API v0 URLs.
from django.urls import re_path from django.urls import re_path
from lms.djangoapps.learner_dashboard.api.v0.views import ProgramProgressDetailView from lms.djangoapps.learner_dashboard.api.v0.views import Programs, ProgramProgressDetailView
UUID_REGEX_PATTERN = r'[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?4[0-9a-fA-F]{3}-?[89abAB][0-9a-fA-F]{3}-?[0-9a-fA-F]{12}'
app_name = 'v0' app_name = 'v0'
urlpatterns = [ urlpatterns = [
re_path(
fr'^programs/(?P<enterprise_uuid>{UUID_REGEX_PATTERN})/$',
Programs.as_view(),
name='program_list'
),
re_path(r'^programs/(?P<program_uuid>[0-9a-f-]+)/progress_details/$', ProgramProgressDetailView.as_view(), re_path(r'^programs/(?P<program_uuid>[0-9a-f-]+)/progress_details/$', ProgramProgressDetailView.as_view(),
name='program_progress_detail'), name='program_progress_detail'),
] ]

View File

@@ -1,18 +1,170 @@
""" API v0 views. """ """ API v0 views. """
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from enterprise.models import EnterpriseCourseEnrollment
from rest_framework.authentication import SessionAuthentication from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from common.djangoapps.student.models import CourseEnrollment
from openedx.core.djangoapps.programs.utils import ( from openedx.core.djangoapps.programs.utils import (
ProgramProgressMeter,
get_certificates, get_certificates,
get_industry_and_credit_pathways, get_industry_and_credit_pathways,
get_program_urls, get_program_and_course_data,
get_program_and_course_data get_program_urls
) )
class Programs(APIView):
"""
**Use Case**
* Get a list of all programs in which request user has enrolled.
**Example Request**
GET /api/dashboard/v0/programs/{enterprise_uuid}/
**GET Parameters**
A GET request must include the following parameters.
* enterprise_uuid: UUID of an enterprise customer.
**Example GET Response**
[
{
"uuid": "ff41a5eb-2a73-4933-8e80-a1c66068ed2c",
"title": "edX Demonstration Program",
"type": "MicroMasters",
"banner_image": {
"large": {
"url": "http://localhost:18381/media/programs/banner_images/ff41a5eb-2a73-4933-8e80.large.jpg",
"width": 1440,
"height": 480
},
"medium": {
"url": "http://localhost:18381/media/programs/banner_images/ff41a5eb-2a73-4933-8e80.medium.jpg",
"width": 726,
"height": 242
},
"small": {
"url": "http://localhost:18381/media/programs/banner_images/ff41a5eb-2a73-4933-8e80.small.jpg",
"width": 435,
"height": 145
},
"x-small": {
"url": "http://localhost:18381/media/programs/banner_images/ff41a5eb-2a73-4933-8e8.x-small.jpg",
"width": 348,
"height": 116
}
},
"authoring_organizations": [
{
"key": "edX"
}
],
"progress": {
"uuid": "ff41a5eb-2a73-4933-8e80-a1c66068ed2c",
"completed": 0,
"in_progress": 0,
"not_started": 2
}
}
]
"""
authentication_classes = (JwtAuthentication, SessionAuthentication,)
permission_classes = (IsAuthenticated,)
def get(self, request, enterprise_uuid):
"""
Return a list of a enterprise learner's all enrolled programs with their progress.
Args:
request (Request): DRF request object.
enterprise_uuid (string): UUID of an enterprise customer.
"""
user = request.user
enrollments = self._get_enterprise_course_enrollments(enterprise_uuid, user)
meter = ProgramProgressMeter(
request.site,
user,
enrollments=enrollments,
mobile_only=False,
include_course_entitlements=False
)
engaged_programs = meter.engaged_programs
progress = meter.progress(programs=engaged_programs)
programs = self._extract_minimal_required_programs_data(engaged_programs)
programs = self._combine_programs_data_and_progress(programs, progress)
return Response(programs)
def _combine_programs_data_and_progress(self, programs_data, programs_progress):
"""
Return the combined program and progress data so that api clinet can easily process the data.
"""
for program_data in programs_data:
program_progress = next((item for item in programs_progress if item['uuid'] == program_data['uuid']), None)
program_data['progress'] = program_progress
return programs_data
def _extract_minimal_required_programs_data(self, programs_data):
"""
Return only the minimal required program data need for program listing page.
"""
def transform(key, value):
transformers = {'authoring_organizations': transform_authoring_organizations}
if key in transformers:
return transformers[key](value)
return value
def transform_authoring_organizations(authoring_organizations):
"""
Extract only the required data for `authoring_organizations` for a program
"""
transformed_authoring_organizations = []
for authoring_organization in authoring_organizations:
transformed_authoring_organizations.append({'key': authoring_organization['key']})
return transformed_authoring_organizations
program_data_keys = ['uuid', 'title', 'type', 'banner_image', 'authoring_organizations']
programs = []
for program_data in programs_data:
program = {}
for program_data_key in program_data_keys:
program[program_data_key] = transform(program_data_key, program_data[program_data_key])
programs.append(program)
return programs
def _get_enterprise_course_enrollments(self, enterprise_uuid, user):
"""
Return only enterprise enrollments for a user.
"""
enterprise_enrollment_course_ids = list(EnterpriseCourseEnrollment.objects.filter(
enterprise_customer_user__user_id=user.id,
enterprise_customer_user__enterprise_customer__uuid=enterprise_uuid,
).values_list('course_id', flat=True))
course_enrollments = CourseEnrollment.enrollments_for_user(user).filter(
course_id__in=enterprise_enrollment_course_ids
).select_related('course')
return list(course_enrollments)
class ProgramProgressDetailView(APIView): class ProgramProgressDetailView(APIView):
""" """
**Use Case** **Use Case**

View File

@@ -152,7 +152,7 @@ class ProgramProgressMeter:
will only inspect this one program, not all programs the user may be will only inspect this one program, not all programs the user may be
engaged with. engaged with.
""" """
def __init__(self, site, user, enrollments=None, uuid=None, mobile_only=False): def __init__(self, site, user, enrollments=None, uuid=None, mobile_only=False, include_course_entitlements=True):
self.site = site self.site = site
self.user = user self.user = user
self.mobile_only = mobile_only self.mobile_only = mobile_only
@@ -172,8 +172,10 @@ class ProgramProgressMeter:
# We can't use dict.keys() for this because the course run ids need to be ordered # We can't use dict.keys() for this because the course run ids need to be ordered
self.course_run_ids.append(enrollment_id) self.course_run_ids.append(enrollment_id)
self.entitlements = list(CourseEntitlement.unexpired_entitlements_for_user(self.user)) self.course_uuids = []
self.course_uuids = [str(entitlement.course_uuid) for entitlement in self.entitlements] if include_course_entitlements:
self.entitlements = list(CourseEntitlement.unexpired_entitlements_for_user(self.user))
self.course_uuids = [str(entitlement.course_uuid) for entitlement in self.entitlements]
if uuid: if uuid:
self.programs = [get_programs(uuid=uuid)] self.programs = [get_programs(uuid=uuid)]