feat: learner home course provider and grade data (#30959)
* feat: get course provider info * feat: get grade data * feat: get course provider for entitlements * style: run black Co-authored-by: nsprenkle <nsprenkle@2u.com>
This commit is contained in:
@@ -6,9 +6,11 @@ from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.student.helpers import user_has_passing_grade_in_course
|
||||
from openedx.features.course_experience import course_home_url
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
|
||||
@@ -17,6 +19,7 @@ class LiteralField(serializers.Field):
|
||||
"""
|
||||
Custom Field for use with fields that will always intentionally serialize to the same static value.
|
||||
"""
|
||||
|
||||
def __init__(self, literal_value):
|
||||
super().__init__()
|
||||
self.literal_value = literal_value
|
||||
@@ -37,9 +40,9 @@ class PlatformSettingsSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class CourseProviderSerializer(serializers.Serializer):
|
||||
"""Info about a course provider (institution/business)"""
|
||||
"""Info about a course provider (institution/business) from a CourseOverview"""
|
||||
|
||||
name = serializers.CharField()
|
||||
name = serializers.CharField(source="display_org_with_default")
|
||||
|
||||
|
||||
class CourseSerializer(serializers.Serializer):
|
||||
@@ -223,7 +226,10 @@ class EnrollmentSerializer(serializers.Serializer):
|
||||
class GradeDataSerializer(serializers.Serializer):
|
||||
"""Info about grades for this enrollment"""
|
||||
|
||||
isPassing = serializers.BooleanField()
|
||||
isPassing = serializers.SerializerMethodField()
|
||||
|
||||
def get_isPassing(self, enrollment):
|
||||
return user_has_passing_grade_in_course(enrollment)
|
||||
|
||||
|
||||
class CertificateSerializer(serializers.Serializer):
|
||||
@@ -287,9 +293,9 @@ class CertificateSerializer(serializers.Serializer):
|
||||
class AvailableEntitlementSessionSerializer(serializers.Serializer):
|
||||
"""An available entitlement session"""
|
||||
|
||||
startDate = serializers.DateTimeField(source='start')
|
||||
endDate = serializers.DateTimeField(source='end')
|
||||
courseId = serializers.CharField(source='key')
|
||||
startDate = serializers.DateTimeField(source="start")
|
||||
endDate = serializers.DateTimeField(source="end")
|
||||
courseId = serializers.CharField(source="key")
|
||||
|
||||
|
||||
class EntitlementSerializer(serializers.Serializer):
|
||||
@@ -298,7 +304,7 @@ class EntitlementSerializer(serializers.Serializer):
|
||||
|
||||
availableSessions = serializers.SerializerMethodField()
|
||||
uuid = serializers.UUIDField()
|
||||
isRefundable = serializers.BooleanField(source='is_entitlement_refundable')
|
||||
isRefundable = serializers.BooleanField(source="is_entitlement_refundable")
|
||||
isFulfilled = serializers.SerializerMethodField()
|
||||
changeDeadline = serializers.SerializerMethodField()
|
||||
isExpired = serializers.SerializerMethodField()
|
||||
@@ -314,8 +320,10 @@ class EntitlementSerializer(serializers.Serializer):
|
||||
return bool(instance.expired_at)
|
||||
|
||||
def get_availableSessions(self, instance):
|
||||
avaialableSessions = self.context['course_entitlement_available_sessions'].get(str(instance.uuid))
|
||||
return AvailableEntitlementSessionSerializer(avaialableSessions, many=True).data
|
||||
availableSessions = self.context["course_entitlement_available_sessions"].get(
|
||||
str(instance.uuid)
|
||||
)
|
||||
return AvailableEntitlementSessionSerializer(availableSessions, many=True).data
|
||||
|
||||
def get_expirationDate(self, instance):
|
||||
if instance.expired_at is not None:
|
||||
@@ -327,7 +335,7 @@ class EntitlementSerializer(serializers.Serializer):
|
||||
return self.get_expirationDate(instance)
|
||||
|
||||
def get_enrollmentUrl(self, instance):
|
||||
return reverse('entitlements_api:v1:enrollments', args=[str(instance.uuid)])
|
||||
return reverse("entitlements_api:v1:enrollments", args=[str(instance.uuid)])
|
||||
|
||||
|
||||
class RelatedProgramSerializer(serializers.Serializer):
|
||||
@@ -356,24 +364,27 @@ class LearnerEnrollmentSerializer(serializers.Serializer):
|
||||
Info for displaying an enrollment on the learner dashboard.
|
||||
Derived from a CourseEnrollment with added context.
|
||||
"""
|
||||
|
||||
requires_context = True
|
||||
|
||||
course = CourseSerializer()
|
||||
courseProvider = CourseProviderSerializer(source="course_overview")
|
||||
courseRun = CourseRunSerializer(source="*")
|
||||
enrollment = EnrollmentSerializer(source="*")
|
||||
certificate = CertificateSerializer(source="*")
|
||||
entitlement = serializers.SerializerMethodField()
|
||||
gradeData = GradeDataSerializer(source="*")
|
||||
|
||||
# TODO - remove "allow_null" as each of these are implemented, temp for testing.
|
||||
courseProvider = CourseProviderSerializer(allow_null=True)
|
||||
gradeData = GradeDataSerializer(allow_null=True)
|
||||
programs = ProgramsSerializer(allow_null=True)
|
||||
|
||||
def get_entitlement(self, instance):
|
||||
"""
|
||||
If this enrollment is the fulfillment of an entitlement, include information about the entitlement
|
||||
"""
|
||||
entitlement = self.context['fulfilled_entitlements'].get(str(instance.course_id))
|
||||
entitlement = self.context["fulfilled_entitlements"].get(
|
||||
str(instance.course_id)
|
||||
)
|
||||
if entitlement:
|
||||
return EntitlementSerializer(entitlement, context=self.context).data
|
||||
else:
|
||||
@@ -391,23 +402,24 @@ class UnfulfilledEntitlementSerializer(serializers.Serializer):
|
||||
|
||||
# This is the static constant data returned as the 'enrollment' key for all unfulfilled enrollments.
|
||||
STATIC_ENTITLEMENT_ENROLLMENT_DATA = {
|
||||
'accessExpirationDate': None,
|
||||
'isAudit': False,
|
||||
'hasStarted': False,
|
||||
'hasAccess': True,
|
||||
'isVerified': False,
|
||||
'canUpgrade': False,
|
||||
'isAuditAccessExpired': False,
|
||||
'isEmailEnabled': False,
|
||||
'hasOptedOutOfEmail': False,
|
||||
'lastEnrolled': None,
|
||||
'isEnrolled': False,
|
||||
"accessExpirationDate": None,
|
||||
"isAudit": False,
|
||||
"hasStarted": False,
|
||||
"hasAccess": True,
|
||||
"isVerified": False,
|
||||
"canUpgrade": False,
|
||||
"isAuditAccessExpired": False,
|
||||
"isEmailEnabled": False,
|
||||
"hasOptedOutOfEmail": False,
|
||||
"lastEnrolled": None,
|
||||
"isEnrolled": False,
|
||||
}
|
||||
|
||||
class _PseudoSessionCourseSerializer(serializers.Serializer):
|
||||
"""
|
||||
'Private' Serilizer for the 'course' key data. This data comes from the pseudo session
|
||||
'Private' Serializer for the 'course' key data. This data comes from the pseudo session
|
||||
"""
|
||||
|
||||
bannerImgSrc = serializers.URLField(source="image.src")
|
||||
courseName = serializers.CharField(source="title")
|
||||
courseNumber = serializers.CharField(source="key")
|
||||
@@ -415,9 +427,9 @@ class UnfulfilledEntitlementSerializer(serializers.Serializer):
|
||||
# These fields contain all real data and will be serialized
|
||||
entitlement = EntitlementSerializer(source="*")
|
||||
course = serializers.SerializerMethodField()
|
||||
courseProvider = serializers.SerializerMethodField()
|
||||
|
||||
# Change after data is implemented. This data is required
|
||||
courseProvider = CourseProviderSerializer(allow_null=True)
|
||||
programs = ProgramsSerializer(allow_null=True)
|
||||
|
||||
# These fields are literal values that do not change
|
||||
@@ -427,8 +439,27 @@ class UnfulfilledEntitlementSerializer(serializers.Serializer):
|
||||
enrollment = LiteralField(STATIC_ENTITLEMENT_ENROLLMENT_DATA)
|
||||
|
||||
def get_course(self, instance):
|
||||
pseudo_session = self.context['unfulfilled_entitlement_pseudo_sessions'].get(str(instance.uuid))
|
||||
return UnfulfilledEntitlementSerializer._PseudoSessionCourseSerializer(pseudo_session).data
|
||||
pseudo_session = self.context["unfulfilled_entitlement_pseudo_sessions"].get(
|
||||
str(instance.uuid)
|
||||
)
|
||||
return UnfulfilledEntitlementSerializer._PseudoSessionCourseSerializer(
|
||||
pseudo_session
|
||||
).data
|
||||
|
||||
def get_courseProvider(self, entitlement):
|
||||
"""Look up course provider from CourseOverview matching the pseudo session"""
|
||||
pseudo_session = self.context["unfulfilled_entitlement_pseudo_sessions"].get(
|
||||
str(entitlement.uuid)
|
||||
)
|
||||
course_overview = None
|
||||
|
||||
if pseudo_session:
|
||||
course_key = CourseKey.from_string(pseudo_session["key"])
|
||||
course_overview = self.context.get("pseudo_session_course_overviews").get(
|
||||
course_key
|
||||
)
|
||||
|
||||
return CourseProviderSerializer(course_overview, allow_null=True).data
|
||||
|
||||
|
||||
class SuggestedCourseSerializer(serializers.Serializer):
|
||||
@@ -450,11 +481,11 @@ class EmailConfirmationSerializer(serializers.Serializer):
|
||||
class EnterpriseDashboardSerializer(serializers.Serializer):
|
||||
"""Serializer for individual enterprise dashboard data"""
|
||||
|
||||
label = serializers.CharField(source='name')
|
||||
label = serializers.CharField(source="name")
|
||||
url = serializers.SerializerMethodField()
|
||||
|
||||
def get_url(self, instance):
|
||||
return urljoin(settings.ENTERPRISE_LEARNER_PORTAL_BASE_URL, instance['uuid'])
|
||||
return urljoin(settings.ENTERPRISE_LEARNER_PORTAL_BASE_URL, instance["uuid"])
|
||||
|
||||
|
||||
class LearnerDashboardSerializer(serializers.Serializer):
|
||||
|
||||
@@ -9,6 +9,8 @@ from uuid import uuid4
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
import ddt
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
||||
@@ -17,7 +19,12 @@ from common.djangoapps.student.tests.factories import (
|
||||
CourseEnrollmentFactory,
|
||||
UserFactory,
|
||||
)
|
||||
from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory as CatalogCourseRunFactory
|
||||
from openedx.core.djangoapps.catalog.tests.factories import (
|
||||
CourseRunFactory as CatalogCourseRunFactory,
|
||||
)
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import (
|
||||
CourseOverviewFactory,
|
||||
)
|
||||
from lms.djangoapps.learner_home.serializers import (
|
||||
CertificateSerializer,
|
||||
CourseProviderSerializer,
|
||||
@@ -72,6 +79,31 @@ class LearnerDashboardBaseTest(SharedModuleStoreTestCase):
|
||||
|
||||
return test_enrollment
|
||||
|
||||
def create_test_entitlement_and_sessions(self):
|
||||
"""
|
||||
Create a test entitlement
|
||||
|
||||
Returns: (unfulfilled_entitlement, pseudo_sessions, available_sessions)
|
||||
"""
|
||||
unfulfilled_entitlement = CourseEntitlementFactory.create()
|
||||
|
||||
# Create pseudo-sessions
|
||||
pseudo_sessions = {
|
||||
str(unfulfilled_entitlement.uuid): CatalogCourseRunFactory.create()
|
||||
}
|
||||
|
||||
# Create available sessions
|
||||
available_sessions = {
|
||||
str(unfulfilled_entitlement.uuid): CatalogCourseRunFactory.create_batch(3)
|
||||
}
|
||||
|
||||
# Create related course overviews
|
||||
course_key_str = pseudo_sessions[str(unfulfilled_entitlement.uuid)]["key"]
|
||||
course_key = CourseKey.from_string(course_key_str)
|
||||
course_overview = CourseOverviewFactory.create(id=course_key)
|
||||
|
||||
return unfulfilled_entitlement, pseudo_sessions, available_sessions
|
||||
|
||||
def _assert_all_keys_equal(self, dicts):
|
||||
element_0 = dicts[0]
|
||||
for element in dicts[1:]:
|
||||
@@ -102,7 +134,7 @@ class TestPlatformSettingsSerializer(TestCase):
|
||||
}
|
||||
|
||||
|
||||
class TestCourseProviderSerializer(TestCase):
|
||||
class TestCourseProviderSerializer(LearnerDashboardBaseTest):
|
||||
"""Tests for the CourseProviderSerializer"""
|
||||
|
||||
@classmethod
|
||||
@@ -113,12 +145,12 @@ class TestCourseProviderSerializer(TestCase):
|
||||
}
|
||||
|
||||
def test_happy_path(self):
|
||||
input_data = self.generate_test_provider_info()
|
||||
test_enrollment = self.create_test_enrollment()
|
||||
|
||||
input_data = test_enrollment.course_overview
|
||||
output_data = CourseProviderSerializer(input_data).data
|
||||
|
||||
assert output_data == {
|
||||
"name": input_data["name"],
|
||||
}
|
||||
self.assertEqual(output_data["name"], test_enrollment.course_overview.org)
|
||||
|
||||
|
||||
class TestCourseSerializer(LearnerDashboardBaseTest):
|
||||
@@ -344,23 +376,29 @@ class TestEnrollmentSerializer(LearnerDashboardBaseTest):
|
||||
self.assertFalse(output["hasStarted"])
|
||||
|
||||
|
||||
class TestGradeDataSerializer(TestCase):
|
||||
@ddt.ddt
|
||||
class TestGradeDataSerializer(LearnerDashboardBaseTest):
|
||||
"""Tests for the GradeDataSerializer"""
|
||||
|
||||
@classmethod
|
||||
def generate_test_grade_data(cls):
|
||||
"""Util to generate test grade data"""
|
||||
return {
|
||||
"isPassing": random_bool(),
|
||||
}
|
||||
@mock.patch(
|
||||
"lms.djangoapps.learner_home.serializers.user_has_passing_grade_in_course"
|
||||
)
|
||||
@ddt.data(True, False, None)
|
||||
def test_happy_path(self, is_passing, mock_get_grade_data):
|
||||
# Given a course where I am/not passing
|
||||
input_data = self.create_test_enrollment()
|
||||
mock_get_grade_data.return_value = is_passing
|
||||
|
||||
def test_happy_path(self):
|
||||
input_data = self.generate_test_grade_data()
|
||||
# When I serialize grade data
|
||||
output_data = GradeDataSerializer(input_data).data
|
||||
|
||||
assert output_data == {
|
||||
"isPassing": input_data["isPassing"],
|
||||
}
|
||||
# Then I get the correct data shape out
|
||||
self.assertDictEqual(
|
||||
output_data,
|
||||
{
|
||||
"isPassing": is_passing,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -581,13 +619,13 @@ class TestCertificateSerializer(LearnerDashboardBaseTest):
|
||||
class TestEntitlementSerializer(TestCase):
|
||||
"""Tests for the EntitlementSerializer"""
|
||||
|
||||
def _assert_availale_sessions(self, input_sessions, output_sessions):
|
||||
def _assert_available_sessions(self, input_sessions, output_sessions):
|
||||
assert len(output_sessions) == len(input_sessions)
|
||||
for input_session, output_session in zip(input_sessions, output_sessions):
|
||||
assert output_session == {
|
||||
'startDate': input_session['start'],
|
||||
'endDate': input_session['end'],
|
||||
'courseId': input_session['key']
|
||||
"startDate": input_session["start"],
|
||||
"endDate": input_session["end"],
|
||||
"courseId": input_session["key"],
|
||||
}
|
||||
|
||||
@ddt.unpack
|
||||
@@ -595,26 +633,33 @@ class TestEntitlementSerializer(TestCase):
|
||||
def test_serialize_entitlement(self, isExpired, isEnrolled):
|
||||
entitlement_kwargs = {}
|
||||
if isExpired:
|
||||
entitlement_kwargs['expired_at'] = datetime.now()
|
||||
entitlement_kwargs["expired_at"] = datetime.now()
|
||||
if isEnrolled:
|
||||
entitlement_kwargs['enrollment_course_run'] = CourseEnrollmentFactory.create()
|
||||
entitlement_kwargs[
|
||||
"enrollment_course_run"
|
||||
] = CourseEnrollmentFactory.create()
|
||||
entitlement = CourseEntitlementFactory.create(**entitlement_kwargs)
|
||||
available_sessions = CatalogCourseRunFactory.create_batch(4)
|
||||
course_entitlement_available_sessions = {
|
||||
str(entitlement.uuid): available_sessions
|
||||
}
|
||||
|
||||
output_data = EntitlementSerializer(entitlement, context={
|
||||
'course_entitlement_available_sessions': course_entitlement_available_sessions
|
||||
}).data
|
||||
output_data = EntitlementSerializer(
|
||||
entitlement,
|
||||
context={
|
||||
"course_entitlement_available_sessions": course_entitlement_available_sessions
|
||||
},
|
||||
).data
|
||||
|
||||
output_sessions = output_data.pop('availableSessions')
|
||||
self._assert_availale_sessions(available_sessions, output_sessions)
|
||||
output_sessions = output_data.pop("availableSessions")
|
||||
self._assert_available_sessions(available_sessions, output_sessions)
|
||||
|
||||
if isExpired:
|
||||
expected_expiration_date = entitlement.expired_at
|
||||
else:
|
||||
expected_expiration_date = date.today() + timedelta(days=entitlement.get_days_until_expiration())
|
||||
expected_expiration_date = date.today() + timedelta(
|
||||
days=entitlement.get_days_until_expiration()
|
||||
)
|
||||
|
||||
assert output_data == {
|
||||
"isRefundable": entitlement.is_entitlement_refundable(),
|
||||
@@ -623,7 +668,7 @@ class TestEntitlementSerializer(TestCase):
|
||||
"isExpired": bool(entitlement.expired_at),
|
||||
"expirationDate": expected_expiration_date,
|
||||
"uuid": str(entitlement.uuid),
|
||||
"enrollmentUrl": f"/api/entitlements/v1/entitlements/{entitlement.uuid}/enrollments"
|
||||
"enrollmentUrl": f"/api/entitlements/v1/entitlements/{entitlement.uuid}/enrollments",
|
||||
}
|
||||
|
||||
|
||||
@@ -736,14 +781,29 @@ class TestUnfulfilledEntitlementSerializer(LearnerDashboardBaseTest):
|
||||
def test_happy_path(self):
|
||||
"""Test that nothing breaks and the output fields look correct"""
|
||||
unfulfilled_entitlement = CourseEntitlementFactory.create()
|
||||
pseudo_sessions = {str(unfulfilled_entitlement.uuid): CatalogCourseRunFactory.create()}
|
||||
available_sessions = {str(unfulfilled_entitlement.uuid): CatalogCourseRunFactory.create_batch(3)}
|
||||
context = {
|
||||
'unfulfilled_entitlement_pseudo_sessions': pseudo_sessions,
|
||||
'course_entitlement_available_sessions': available_sessions,
|
||||
pseudo_sessions = {
|
||||
str(unfulfilled_entitlement.uuid): CatalogCourseRunFactory.create()
|
||||
}
|
||||
available_sessions = {
|
||||
str(unfulfilled_entitlement.uuid): CatalogCourseRunFactory.create_batch(3)
|
||||
}
|
||||
|
||||
output_data = UnfulfilledEntitlementSerializer(unfulfilled_entitlement, context=context).data
|
||||
# create course overview for course provider info
|
||||
course_key_str = pseudo_sessions[str(unfulfilled_entitlement.uuid)]["key"]
|
||||
course_key = CourseKey.from_string(course_key_str)
|
||||
course_overview = CourseOverviewFactory.create(id=course_key)
|
||||
|
||||
pseudo_session_course_overviews = {course_key: course_overview}
|
||||
|
||||
context = {
|
||||
"unfulfilled_entitlement_pseudo_sessions": pseudo_sessions,
|
||||
"course_entitlement_available_sessions": available_sessions,
|
||||
"pseudo_session_course_overviews": pseudo_session_course_overviews,
|
||||
}
|
||||
|
||||
output_data = UnfulfilledEntitlementSerializer(
|
||||
unfulfilled_entitlement, context=context
|
||||
).data
|
||||
|
||||
expected_keys = [
|
||||
"courseProvider",
|
||||
@@ -753,14 +813,18 @@ class TestUnfulfilledEntitlementSerializer(LearnerDashboardBaseTest):
|
||||
"courseRun",
|
||||
"gradeData",
|
||||
"certificate",
|
||||
"enrollment"
|
||||
"enrollment",
|
||||
]
|
||||
|
||||
assert output_data.keys() == set(expected_keys)
|
||||
assert output_data['courseRun'] is None
|
||||
assert output_data['gradeData'] is None
|
||||
assert output_data['certificate'] is None
|
||||
assert output_data['enrollment'] == UnfulfilledEntitlementSerializer.STATIC_ENTITLEMENT_ENROLLMENT_DATA
|
||||
assert output_data["courseProvider"] is not None
|
||||
assert output_data["courseRun"] is None
|
||||
assert output_data["gradeData"] is None
|
||||
assert output_data["certificate"] is None
|
||||
assert (
|
||||
output_data["enrollment"]
|
||||
== UnfulfilledEntitlementSerializer.STATIC_ENTITLEMENT_ENROLLMENT_DATA
|
||||
)
|
||||
|
||||
def test_static_enrollment_data(self):
|
||||
"""
|
||||
@@ -768,7 +832,9 @@ class TestUnfulfilledEntitlementSerializer(LearnerDashboardBaseTest):
|
||||
This test is to ensure that that dict has the same keys as returned by the LearnerEnrollmentSerializer
|
||||
"""
|
||||
output_data = TestEnrollmentSerializer().serialize_test_enrollment()
|
||||
expected_keys = UnfulfilledEntitlementSerializer.STATIC_ENTITLEMENT_ENROLLMENT_DATA.keys()
|
||||
expected_keys = (
|
||||
UnfulfilledEntitlementSerializer.STATIC_ENTITLEMENT_ENROLLMENT_DATA.keys()
|
||||
)
|
||||
actual_keys = output_data.keys()
|
||||
assert expected_keys == actual_keys
|
||||
|
||||
@@ -888,7 +954,9 @@ class TestEnterpriseDashboardSerializer(TestCase):
|
||||
output_data,
|
||||
{
|
||||
"label": input_data["name"],
|
||||
"url": settings.ENTERPRISE_LEARNER_PORTAL_BASE_URL + '/' + input_data["uuid"],
|
||||
"url": settings.ENTERPRISE_LEARNER_PORTAL_BASE_URL
|
||||
+ "/"
|
||||
+ input_data["uuid"],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -899,9 +967,14 @@ class TestLearnerDashboardSerializer(LearnerDashboardBaseTest):
|
||||
# Show full diff for serialization issues
|
||||
maxDiff = None
|
||||
|
||||
def make_test_context(self, enrollments=None, enrollments_with_entitlements=None, unfulfilled_entitlements=None):
|
||||
def make_test_context(
|
||||
self,
|
||||
enrollments=None,
|
||||
enrollments_with_entitlements=None,
|
||||
unfulfilled_entitlements=None,
|
||||
):
|
||||
"""
|
||||
Given enrollments and entitlements, generate a mathing serializer context
|
||||
Given enrollments and entitlements, generate a matching serializer context
|
||||
"""
|
||||
enrollments = enrollments or []
|
||||
enrollments_with_entitlements = enrollments_with_entitlements or []
|
||||
@@ -938,6 +1011,17 @@ class TestLearnerDashboardSerializer(LearnerDashboardBaseTest):
|
||||
for entitlement in all_entitlements
|
||||
}
|
||||
|
||||
# Create related course overviews for entitlement pseudo sessions
|
||||
pseudo_session_course_overviews = {}
|
||||
for unfulfilled_entitlement in unfulfilled_entitlement_pseudo_sessions:
|
||||
course_key_str = unfulfilled_entitlement_pseudo_sessions[
|
||||
unfulfilled_entitlement
|
||||
]["key"]
|
||||
course_key = CourseKey.from_string(course_key_str)
|
||||
course_overview = CourseOverviewFactory.create(id=course_key)
|
||||
|
||||
pseudo_session_course_overviews[course_key] = course_overview
|
||||
|
||||
input_context = {
|
||||
"resume_course_urls": resume_course_urls,
|
||||
"ecommerce_payment_page": random_url(),
|
||||
@@ -945,6 +1029,7 @@ class TestLearnerDashboardSerializer(LearnerDashboardBaseTest):
|
||||
"fulfilled_entitlements": fulfilled_entitlements,
|
||||
"unfulfilled_entitlement_pseudo_sessions": unfulfilled_entitlement_pseudo_sessions,
|
||||
"course_entitlement_available_sessions": course_entitlement_available_sessions,
|
||||
"pseudo_session_course_overviews": pseudo_session_course_overviews,
|
||||
}
|
||||
return input_context
|
||||
|
||||
@@ -998,10 +1083,7 @@ class TestLearnerDashboardSerializer(LearnerDashboardBaseTest):
|
||||
|
||||
def test_entitlements(self):
|
||||
# One standard enrollment, one fulfilled entitlement, one unfulfilled enrollment
|
||||
enrollments = [
|
||||
self.create_test_enrollment(),
|
||||
self.create_test_enrollment()
|
||||
]
|
||||
enrollments = [self.create_test_enrollment(), self.create_test_enrollment()]
|
||||
unfulfilled_entitlements = [CourseEntitlementFactory.create()]
|
||||
|
||||
input_context = self.make_test_context(
|
||||
@@ -1027,7 +1109,7 @@ class TestLearnerDashboardSerializer(LearnerDashboardBaseTest):
|
||||
self._assert_all_keys_equal(courses)
|
||||
# Non-entitlement enrollment should have no entitlement info
|
||||
assert not courses[0]['entitlement']
|
||||
# Fulfuilled and Unfulfilled entitlement should have identical keys
|
||||
# Fulfilled and Unfulfilled entitlement should have identical keys
|
||||
fulfilled_entitlement = courses[1]['entitlement']
|
||||
unfulfilled_entitlement = courses[2]['entitlement']
|
||||
assert fulfilled_entitlement
|
||||
|
||||
@@ -10,9 +10,9 @@ import ddt
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from lms.djangoapps.learner_home.test_utils import create_test_enrollment
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.entitlements.tests.factories import CourseEntitlementFactory
|
||||
from common.djangoapps.student.tests.factories import (
|
||||
@@ -20,7 +20,9 @@ from common.djangoapps.student.tests.factories import (
|
||||
UserFactory,
|
||||
)
|
||||
from lms.djangoapps.bulk_email.models import Optout
|
||||
from lms.djangoapps.learner_home.test_utils import create_test_enrollment
|
||||
from lms.djangoapps.learner_home.views import (
|
||||
get_course_overviews_for_pseudo_sessions,
|
||||
get_email_settings_info,
|
||||
get_enrollments,
|
||||
get_platform_settings,
|
||||
@@ -28,7 +30,12 @@ from lms.djangoapps.learner_home.views import (
|
||||
get_entitlements,
|
||||
)
|
||||
from lms.djangoapps.learner_home.test_serializers import random_url
|
||||
from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory as CatalogCourseRunFactory
|
||||
from openedx.core.djangoapps.catalog.tests.factories import (
|
||||
CourseRunFactory as CatalogCourseRunFactory,
|
||||
)
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import (
|
||||
CourseOverviewFactory,
|
||||
)
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
TEST_DATA_SPLIT_MODULESTORE,
|
||||
SharedModuleStoreTestCase,
|
||||
@@ -36,7 +43,7 @@ from xmodule.modulestore.tests.django_utils import (
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
|
||||
ENTERPRISE_ENABLED = 'ENABLE_ENTERPRISE_INTEGRATION'
|
||||
ENTERPRISE_ENABLED = "ENABLE_ENTERPRISE_INTEGRATION"
|
||||
|
||||
|
||||
class TestGetPlatformSettings(TestCase):
|
||||
@@ -173,7 +180,7 @@ class TestGetEntitlements(SharedModuleStoreTestCase):
|
||||
self,
|
||||
filtered_entitlements,
|
||||
course_entitlement_available_sessions,
|
||||
unfulfilled_entitlement_pseudo_sessions
|
||||
unfulfilled_entitlement_pseudo_sessions,
|
||||
):
|
||||
"""
|
||||
Context manager utility for mocking get_filtered_course_entitlements.
|
||||
@@ -184,7 +191,10 @@ class TestGetEntitlements(SharedModuleStoreTestCase):
|
||||
course_entitlement_available_sessions,
|
||||
unfulfilled_entitlement_pseudo_sessions,
|
||||
)
|
||||
with patch('lms.djangoapps.learner_home.views.get_filtered_course_entitlements', return_value=return_value):
|
||||
with patch(
|
||||
"lms.djangoapps.learner_home.views.get_filtered_course_entitlements",
|
||||
return_value=return_value,
|
||||
):
|
||||
yield
|
||||
|
||||
def create_test_fulfilled_entitlement(self):
|
||||
@@ -213,7 +223,9 @@ class TestGetEntitlements(SharedModuleStoreTestCase):
|
||||
|
||||
available_sessions = {}
|
||||
for entitlement in fulfilled_test_entitlements + unfulfilled_test_entitlements:
|
||||
available_sessions[str(entitlement.uuid)] = CatalogCourseRunFactory.create_batch(3)
|
||||
available_sessions[
|
||||
str(entitlement.uuid)
|
||||
] = CatalogCourseRunFactory.create_batch(3)
|
||||
|
||||
pseudo_sessions = {}
|
||||
for entitlement in unfulfilled_test_entitlements:
|
||||
@@ -222,7 +234,7 @@ class TestGetEntitlements(SharedModuleStoreTestCase):
|
||||
with self.mock_get_filtered_course_entitlements(
|
||||
fulfilled_test_entitlements + unfulfilled_test_entitlements,
|
||||
available_sessions,
|
||||
pseudo_sessions
|
||||
pseudo_sessions,
|
||||
):
|
||||
(
|
||||
fulfilled_entitlements_by_course_key,
|
||||
@@ -231,7 +243,9 @@ class TestGetEntitlements(SharedModuleStoreTestCase):
|
||||
unfulfilled_entitlement_pseudo_sessions,
|
||||
) = get_entitlements(self.user, None, None)
|
||||
|
||||
assert len(fulfilled_entitlements_by_course_key) == len(fulfilled_test_entitlements)
|
||||
assert len(fulfilled_entitlements_by_course_key) == len(
|
||||
fulfilled_test_entitlements
|
||||
)
|
||||
assert len(unfulfilled_entitlements) == len(unfulfilled_test_entitlements)
|
||||
assert set(unfulfilled_entitlements) == set(unfulfilled_test_entitlements)
|
||||
assert course_entitlement_available_sessions is available_sessions
|
||||
@@ -257,6 +271,43 @@ class TestGetEntitlements(SharedModuleStoreTestCase):
|
||||
assert not unfulfilled_entitlement_pseudo_sessions
|
||||
|
||||
|
||||
class TestGetCourseOverviewsForPseudoSessions(SharedModuleStoreTestCase):
|
||||
"""Tests for get_course_overviews_for_pseudo_sessions"""
|
||||
|
||||
def test_basic(self):
|
||||
# Given several unfulfilled entitlements
|
||||
unfulfilled_entitlement_uuids = [uuid4() for _ in range(3)]
|
||||
pseudo_sessions = {}
|
||||
for uuid in unfulfilled_entitlement_uuids:
|
||||
pseudo_sessions[str(uuid)] = CatalogCourseRunFactory.create()
|
||||
|
||||
# ... that have matching CourseOverviews
|
||||
expected_course_overviews = {}
|
||||
for pseudo_session in pseudo_sessions.values():
|
||||
course_key = CourseKey.from_string(pseudo_session["key"])
|
||||
mock_course = CourseFactory.create(
|
||||
org=course_key.org, run=course_key.run, number=course_key.course
|
||||
)
|
||||
mock_course_overview = CourseOverviewFactory.create(id=mock_course.id)
|
||||
expected_course_overviews[course_key] = mock_course_overview
|
||||
|
||||
# When I try to get course overviews, keyed by course key
|
||||
course_overviews = get_course_overviews_for_pseudo_sessions(pseudo_sessions)
|
||||
|
||||
# Then they map to the correct courses
|
||||
self.assertDictEqual(course_overviews, expected_course_overviews)
|
||||
|
||||
def test_no_pseudo_sessions(self):
|
||||
# Given no pseudo sessions
|
||||
pseudo_sessions = {}
|
||||
|
||||
# When I query course overviews
|
||||
course_overviews = get_course_overviews_for_pseudo_sessions(pseudo_sessions)
|
||||
|
||||
# Then I should get an empty dict
|
||||
self.assertDictEqual(course_overviews, {})
|
||||
|
||||
|
||||
class TestGetEmailSettingsInfo(SharedModuleStoreTestCase):
|
||||
"""Tests for get_email_settings_info"""
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ Views for the learner dashboard.
|
||||
"""
|
||||
from django.conf import settings
|
||||
from edx_django_utils import monitoring as monitoring_utils
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.generics import RetrieveAPIView
|
||||
|
||||
@@ -26,8 +27,11 @@ from lms.djangoapps.courseware.access_utils import (
|
||||
check_course_open_for_learner,
|
||||
)
|
||||
from lms.djangoapps.learner_home.serializers import LearnerDashboardSerializer
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.features.enterprise_support.api import enterprise_customer_from_session_or_learner_data
|
||||
from openedx.features.enterprise_support.api import (
|
||||
enterprise_customer_from_session_or_learner_data,
|
||||
)
|
||||
|
||||
|
||||
def get_platform_settings():
|
||||
@@ -98,32 +102,48 @@ def get_enrollments(user, org_allow_list, org_block_list, course_limit=None):
|
||||
|
||||
|
||||
def get_entitlements(user, org_allow_list, org_block_list):
|
||||
"""Get entitlments for the user"""
|
||||
"""Get entitlements for the user"""
|
||||
(
|
||||
filtered_entitlements,
|
||||
course_entitlement_available_sessions,
|
||||
unfulfilled_entitlement_pseudo_sessions
|
||||
) = get_filtered_course_entitlements(
|
||||
user, org_allow_list, org_block_list
|
||||
)
|
||||
unfulfilled_entitlement_pseudo_sessions,
|
||||
) = get_filtered_course_entitlements(user, org_allow_list, org_block_list)
|
||||
fulfilled_entitlements_by_course_key = {}
|
||||
unfulfulled_entitlements = []
|
||||
unfulfilled_entitlements = []
|
||||
|
||||
for course_entitlement in filtered_entitlements:
|
||||
if course_entitlement.enrollment_course_run:
|
||||
course_id = str(course_entitlement.enrollment_course_run.course.id)
|
||||
fulfilled_entitlements_by_course_key[course_id] = course_entitlement
|
||||
else:
|
||||
unfulfulled_entitlements.append(course_entitlement)
|
||||
unfulfilled_entitlements.append(course_entitlement)
|
||||
|
||||
return (
|
||||
fulfilled_entitlements_by_course_key,
|
||||
unfulfulled_entitlements,
|
||||
unfulfilled_entitlements,
|
||||
course_entitlement_available_sessions,
|
||||
unfulfilled_entitlement_pseudo_sessions,
|
||||
)
|
||||
|
||||
|
||||
def get_course_overviews_for_pseudo_sessions(unfulfilled_entitlement_pseudo_sessions):
|
||||
"""
|
||||
Get course overviews for entitlement pseudo sessions. This is required for
|
||||
serializing course providers for entitlements.
|
||||
|
||||
Returns: dict of course overviews, keyed by CourseKey
|
||||
"""
|
||||
course_ids = []
|
||||
|
||||
# Get course IDs from unfulfilled entitlement pseudo sessions
|
||||
for pseudo_session in unfulfilled_entitlement_pseudo_sessions.values():
|
||||
course_id = pseudo_session.get("key")
|
||||
if course_id:
|
||||
course_ids.append(CourseKey.from_string(course_id))
|
||||
|
||||
return CourseOverview.get_from_ids(course_ids)
|
||||
|
||||
|
||||
def get_email_settings_info(user, course_enrollments):
|
||||
"""
|
||||
Given a user and enrollments, determine which courses allow bulk email (show_email_settings_for)
|
||||
@@ -231,13 +251,16 @@ class InitializeView(RetrieveAPIView): # pylint: disable=unused-argument
|
||||
# Get the org whitelist or the org blacklist for the current site
|
||||
site_org_whitelist, site_org_blacklist = get_org_black_and_whitelist_for_site()
|
||||
|
||||
# Get entitlements
|
||||
# Get entitlements and course overviews for serializing
|
||||
(
|
||||
fulfilled_entitlements_by_course_key,
|
||||
unfulfulled_entitlements,
|
||||
unfulfilled_entitlements,
|
||||
course_entitlement_available_sessions,
|
||||
unfulfilled_entitlement_pseudo_sessions
|
||||
unfulfilled_entitlement_pseudo_sessions,
|
||||
) = get_entitlements(user, site_org_whitelist, site_org_blacklist)
|
||||
pseudo_session_course_overviews = get_course_overviews_for_pseudo_sessions(
|
||||
unfulfilled_entitlement_pseudo_sessions
|
||||
)
|
||||
|
||||
# Get enrollments
|
||||
course_enrollments, course_mode_info = get_enrollments(
|
||||
@@ -268,7 +291,7 @@ class InitializeView(RetrieveAPIView): # pylint: disable=unused-argument
|
||||
"enterpriseDashboard": enterprise_customer,
|
||||
"platformSettings": get_platform_settings(),
|
||||
"enrollments": course_enrollments,
|
||||
"unfulfilledEntitlements": unfulfulled_entitlements,
|
||||
"unfulfilledEntitlements": unfulfilled_entitlements,
|
||||
"suggestedCourses": [],
|
||||
}
|
||||
|
||||
@@ -283,6 +306,7 @@ class InitializeView(RetrieveAPIView): # pylint: disable=unused-argument
|
||||
"fulfilled_entitlements": fulfilled_entitlements_by_course_key,
|
||||
"course_entitlement_available_sessions": course_entitlement_available_sessions,
|
||||
"unfulfilled_entitlement_pseudo_sessions": unfulfilled_entitlement_pseudo_sessions,
|
||||
"pseudo_session_course_overviews": pseudo_session_course_overviews,
|
||||
}
|
||||
|
||||
response_data = LearnerDashboardSerializer(
|
||||
|
||||
Reference in New Issue
Block a user