feat: api to fetch all programs where an enterprise learner is enrolled
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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**
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
Reference in New Issue
Block a user