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 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
}

View File

@@ -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'),
]

View File

@@ -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**

View File

@@ -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)]