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 uuid import uuid4
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.urls import reverse_lazy
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
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.tests.factories import (
|
||||
CourseFactory,
|
||||
@@ -18,7 +22,14 @@ from openedx.core.djangoapps.catalog.tests.factories import (
|
||||
ProgramFactory
|
||||
)
|
||||
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.features.enterprise_support.tests.factories import (
|
||||
EnterpriseCourseEnrollmentFactory,
|
||||
EnterpriseCustomerFactory,
|
||||
EnterpriseCustomerUserFactory
|
||||
)
|
||||
|
||||
PROGRAMS_UTILS_MODULE = 'openedx.core.djangoapps.programs.utils'
|
||||
|
||||
@@ -116,3 +127,84 @@ class TestProgramProgressDetailView(ProgramsApiConfigMixin, SharedModuleStoreTes
|
||||
response = self.client.get(self.url)
|
||||
assert response.status_code == 404
|
||||
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 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'
|
||||
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(),
|
||||
name='program_progress_detail'),
|
||||
]
|
||||
|
||||
@@ -1,18 +1,170 @@
|
||||
""" 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.permissions import IsAuthenticated
|
||||
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from openedx.core.djangoapps.programs.utils import (
|
||||
ProgramProgressMeter,
|
||||
get_certificates,
|
||||
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):
|
||||
"""
|
||||
**Use Case**
|
||||
|
||||
@@ -152,7 +152,7 @@ class ProgramProgressMeter:
|
||||
will only inspect this one program, not all programs the user may be
|
||||
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.user = user
|
||||
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
|
||||
self.course_run_ids.append(enrollment_id)
|
||||
|
||||
self.entitlements = list(CourseEntitlement.unexpired_entitlements_for_user(self.user))
|
||||
self.course_uuids = [str(entitlement.course_uuid) for entitlement in self.entitlements]
|
||||
self.course_uuids = []
|
||||
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:
|
||||
self.programs = [get_programs(uuid=uuid)]
|
||||
|
||||
Reference in New Issue
Block a user