feat: add remaining learner dash serializers (#30707)

* feat: add UnfulfilledEntitlementSerializer

* feat: add SuggestedCoursesSerializer

* feat: add new enrollment serializer fields

* test: new enrollment serializer fields

* feat: add new entitlement serializer fields

* feat: add EmailConfirmationSerializer

* feat: add EnterpriseDashboardsSerializer

* fix: update learner view with new serializers

Co-authored-by: nsprenkle <nsprenkle@2u.com>
This commit is contained in:
Nathan Sprenkle
2022-07-13 15:46:49 -04:00
committed by GitHub
parent 143ea1dbbc
commit 71fabb7f87
4 changed files with 277 additions and 24 deletions

View File

@@ -25,6 +25,8 @@ def get_platform_settings():
def dashboard_view(request): # pylint: disable=unused-argument
"""List of courses a user is enrolled in or entitled to"""
learner_dash_data = {
"emailConfirmation": None,
"enterpriseDashboards": None,
"platformSettings": get_platform_settings(),
"enrollments": [],
"unfulfilledEntitlements": [],

View File

@@ -54,6 +54,8 @@ class EnrollmentSerializer(serializers.Serializer):
canUpgrade = serializers.BooleanField()
isAuditAccessExpired = serializers.BooleanField()
isEmailEnabled = serializers.BooleanField()
lastEnrolled = serializers.DateTimeField()
isEnrolled = serializers.BooleanField()
class GradeDataSerializer(serializers.Serializer):
@@ -94,6 +96,7 @@ class EntitlementSerializer(serializers.Serializer):
canViewCourse = serializers.BooleanField()
changeDeadline = serializers.DateTimeField()
isExpired = serializers.BooleanField()
expirationDate = serializers.DateTimeField()
class RelatedProgramSerializer(serializers.Serializer):
@@ -135,20 +138,55 @@ class LearnerEnrollmentSerializer(serializers.Serializer):
class UnfulfilledEntitlementSerializer(serializers.Serializer):
"""Serializer for an unfulfilled entitlement"""
courseProvider = CourseProviderSerializer(allow_null=True)
course = CourseSerializer()
entitlements = EntitlementSerializer()
programs = ProgramsSerializer()
class SuggestedCourseSerializer(serializers.Serializer):
"""Serializer for a suggested course"""
bannerUrl = serializers.URLField()
logoUrl = serializers.URLField()
title = serializers.CharField()
courseUrl = serializers.URLField()
class EmailConfirmationSerializer(serializers.Serializer):
"""Serializer for email confirmation banner resources"""
isNeeded = serializers.BooleanField()
sendEmailUrl = serializers.URLField()
class EnterpriseDashboardSerializer(serializers.Serializer):
"""Serializer for individual enterprise dashboard data"""
label = serializers.CharField()
url = serializers.URLField()
class EnterpriseDashboardsSerializer(serializers.Serializer):
"""Listing of available enterprise dashboards"""
availableDashboards = serializers.ListField(
child=EnterpriseDashboardSerializer(), allow_empty=True
)
mostRecentDashboard = EnterpriseDashboardSerializer()
class LearnerDashboardSerializer(serializers.Serializer):
"""Serializer for all info required to render the Learner Dashboard"""
emailConfirmation = EmailConfirmationSerializer()
enterpriseDashboards = EnterpriseDashboardsSerializer()
platformSettings = PlatformSettingsSerializer()
enrollments = serializers.ListField(
child=LearnerEnrollmentSerializer(), allow_empty=True
)
unfulfilledEntitlements = serializers.ListField(
child=EntitlementSerializer(), allow_empty=True
child=UnfulfilledEntitlementSerializer(), allow_empty=True
)
suggestedCourses = serializers.ListField(
child=SuggestedCourseSerializer(), allow_empty=True

View File

@@ -89,6 +89,8 @@ class TestDashboardView(SharedModuleStoreTestCase, APITestCase):
response_data = json.loads(response.content)
expected_keys = set(
[
"emailConfirmation",
"enterpriseDashboards",
"platformSettings",
"enrollments",
"unfulfilledEntitlements",

View File

@@ -12,13 +12,17 @@ from lms.djangoapps.learner_dashboard.serializers import (
CourseProviderSerializer,
CourseRunSerializer,
CourseSerializer,
EmailConfirmationSerializer,
EnrollmentSerializer,
EnterpriseDashboardsSerializer,
EntitlementSerializer,
GradeDataSerializer,
LearnerEnrollmentSerializer,
PlatformSettingsSerializer,
ProgramsSerializer,
LearnerDashboardSerializer,
SuggestedCourseSerializer,
UnfulfilledEntitlementSerializer,
)
@@ -49,6 +53,16 @@ def random_url(allow_null=False):
return choice([f"{random_uuid}.example.com", f"example.com/{random_uuid}"])
def random_grade():
"""Return a random grade (0-100) with 2 decimal places of padding"""
return randint(0, 10000) / 100
def decimal_to_grade_format(decimal):
"""Util for matching serialized grade format, pads a decimal to 2 places"""
return "{:.2f}".format(decimal)
def datetime_to_django_format(datetime_obj):
"""Util for matching serialized Django datetime format for comparison"""
if datetime_obj:
@@ -136,7 +150,7 @@ class TestCourseRunSerializer(TestCase):
"isArchived": random_bool(),
"courseNumber": f"{uuid4()}-101",
"accessExpirationDate": random_date(),
"minPassingGrade": randint(0, 10000) / 100,
"minPassingGrade": random_grade(),
"endDate": random_date(),
"homeUrl": f"{uuid4()}.example.com",
"marketingUrl": f"{uuid4()}.example.com",
@@ -158,7 +172,7 @@ class TestCourseRunSerializer(TestCase):
"accessExpirationDate": datetime_to_django_format(
input_data["accessExpirationDate"]
),
"minPassingGrade": str(input_data["minPassingGrade"]),
"minPassingGrade": decimal_to_grade_format(input_data["minPassingGrade"]),
"endDate": datetime_to_django_format(input_data["endDate"]),
"homeUrl": input_data["homeUrl"],
"marketingUrl": input_data["marketingUrl"],
@@ -180,19 +194,26 @@ class TestEnrollmentSerializer(TestCase):
"canUpgrade": random_bool(),
"isAuditAccessExpired": random_bool(),
"isEmailEnabled": random_bool(),
"lastEnrolled": random_date(),
"isEnrolled": random_bool(),
}
def test_happy_path(self):
input_data = self.generate_test_enrollment_info()
output_data = EnrollmentSerializer(input_data).data
assert output_data == {
"isAudit": input_data["isAudit"],
"isVerified": input_data["isVerified"],
"canUpgrade": input_data["canUpgrade"],
"isAuditAccessExpired": input_data["isAuditAccessExpired"],
"isEmailEnabled": input_data["isEmailEnabled"],
}
self.assertDictEqual(
output_data,
{
"isAudit": input_data["isAudit"],
"isVerified": input_data["isVerified"],
"canUpgrade": input_data["canUpgrade"],
"isAuditAccessExpired": input_data["isAuditAccessExpired"],
"isEmailEnabled": input_data["isEmailEnabled"],
"lastEnrolled": datetime_to_django_format(input_data["lastEnrolled"]),
"isEnrolled": input_data["isEnrolled"],
},
)
class TestGradeDataSerializer(TestCase):
@@ -271,6 +292,7 @@ class TestEntitlementSerializer(TestCase):
"canViewCourse": random_bool(),
"changeDeadline": random_date(),
"isExpired": random_bool(),
"expirationDate": random_date(),
}
def test_happy_path(self):
@@ -295,6 +317,7 @@ class TestEntitlementSerializer(TestCase):
"canViewCourse": input_data["canViewCourse"],
"changeDeadline": datetime_to_django_format(input_data["changeDeadline"]),
"isExpired": input_data["isExpired"],
"expirationDate": datetime_to_django_format(input_data["expirationDate"]),
}
@@ -388,25 +411,179 @@ class TestLearnerEnrollmentsSerializer(TestCase):
]
assert output_data.keys() == set(expected_keys)
def test_allowed_empty(self):
"""Tests for allowed null fields, mostly that nothing breaks"""
input_data = self.generate_test_enrollments_data()
input_data["courseProvider"] = None
output_data = LearnerEnrollmentSerializer(input_data).data
class TestUnfulfilledEntitlementSerializer(TestCase):
"""High-level tests for UnfulfilledEntitlementSerializer"""
@classmethod
def generate_test_entitlements_data(cls):
return {
"courseProvider": TestCourseProviderSerializer.generate_test_provider_info(),
"course": TestCourseSerializer.generate_test_course_info(),
"entitlements": TestEntitlementSerializer.generate_test_entitlement_info(),
"programs": TestProgramsSerializer.generate_test_programs_info(),
}
def test_happy_path(self):
"""Test that nothing breaks and the output fields look correct"""
input_data = self.generate_test_entitlements_data()
output_data = UnfulfilledEntitlementSerializer(input_data).data
expected_keys = [
"courseProvider",
"course",
"courseRun",
"enrollment",
"gradeData",
"certificate",
"entitlements",
"programs",
]
assert output_data.keys() == set(expected_keys)
def test_allowed_empty(self):
"""Tests for allowed null fields, mostly that nothing breaks"""
input_data = self.generate_test_entitlements_data()
input_data["courseProvider"] = None
output_data = UnfulfilledEntitlementSerializer(input_data).data
expected_keys = [
"courseProvider",
"course",
"entitlements",
"programs",
]
assert output_data.keys() == set(expected_keys)
class TestSuggestedCourseSerializer(TestCase):
"""High-level tests for SuggestedCourseSerializer"""
@classmethod
def generate_test_suggested_courses(cls):
return {
"bannerUrl": random_url(),
"logoUrl": random_url(),
"title": f"{uuid4()}",
"courseUrl": random_url(),
}
def test_structure(self):
"""Test that nothing breaks and the output fields look correct"""
input_data = self.generate_test_suggested_courses()
output_data = SuggestedCourseSerializer(input_data).data
expected_keys = [
"bannerUrl",
"logoUrl",
"title",
"courseUrl",
]
assert output_data.keys() == set(expected_keys)
def test_happy_path(self):
"""Test that data serializes correctly"""
input_data = self.generate_test_suggested_courses()
output_data = SuggestedCourseSerializer(input_data).data
self.assertDictEqual(
output_data,
{
"bannerUrl": input_data["bannerUrl"],
"logoUrl": input_data["logoUrl"],
"title": input_data["title"],
"courseUrl": input_data["courseUrl"],
},
)
class TestEmailConfirmationSerializer(TestCase):
"""High-level tests for EmailConfirmationSerializer"""
@classmethod
def generate_test_data(cls):
return {
"isNeeded": random_bool(),
"sendEmailUrl": random_url(),
}
def test_structure(self):
"""Test that nothing breaks and the output fields look correct"""
input_data = self.generate_test_data()
output_data = EmailConfirmationSerializer(input_data).data
expected_keys = [
"isNeeded",
"sendEmailUrl",
]
assert output_data.keys() == set(expected_keys)
def test_happy_path(self):
"""Test that data serializes correctly"""
input_data = self.generate_test_data()
output_data = EmailConfirmationSerializer(input_data).data
self.assertDictEqual(
output_data,
{
"isNeeded": input_data["isNeeded"],
"sendEmailUrl": input_data["sendEmailUrl"],
},
)
class TestEnterpriseDashboardsSerializer(TestCase):
"""High-level tests for EnterpriseDashboardsSerializer"""
@classmethod
def generate_test_dashboard(cls):
return {
"label": f"{uuid4()}",
"url": random_url(),
}
@classmethod
def generate_test_data(cls):
return {
"availableDashboards": [
cls.generate_test_dashboard() for _ in range(randint(0, 3))
],
"mostRecentDashboard": cls.generate_test_dashboard()
if random_bool()
else None,
}
def test_structure(self):
"""Test that nothing breaks and the output fields look correct"""
input_data = self.generate_test_data()
output_data = EnterpriseDashboardsSerializer(input_data).data
expected_keys = [
"availableDashboards",
"mostRecentDashboard",
]
assert output_data.keys() == set(expected_keys)
def test_happy_path(self):
"""Test that data serializes correctly"""
input_data = self.generate_test_data()
output_data = EnterpriseDashboardsSerializer(input_data).data
self.assertDictEqual(
output_data,
{
"availableDashboards": input_data["availableDashboards"],
"mostRecentDashboard": input_data["mostRecentDashboard"],
},
)
class TestLearnerDashboardSerializer(TestCase):
"""High-level tests for Learner Dashboard serialization"""
@@ -418,6 +595,8 @@ class TestLearnerDashboardSerializer(TestCase):
"""Test that empty inputs return the right keys"""
input_data = {
"emailConfirmation": None,
"enterpriseDashboards": None,
"platformSettings": None,
"enrollments": [],
"unfulfilledEntitlements": [],
@@ -428,6 +607,8 @@ class TestLearnerDashboardSerializer(TestCase):
self.assertDictEqual(
output_data,
{
"emailConfirmation": None,
"enterpriseDashboards": None,
"platformSettings": None,
"enrollments": [],
"unfulfilledEntitlements": [],
@@ -435,36 +616,66 @@ class TestLearnerDashboardSerializer(TestCase):
},
)
@mock.patch(
"lms.djangoapps.learner_dashboard.serializers.SuggestedCourseSerializer.to_representation"
)
@mock.patch(
"lms.djangoapps.learner_dashboard.serializers.UnfulfilledEntitlementSerializer.to_representation"
)
@mock.patch(
"lms.djangoapps.learner_dashboard.serializers.LearnerEnrollmentSerializer.to_representation"
)
@mock.patch(
"lms.djangoapps.learner_dashboard.serializers.PlatformSettingsSerializer.to_representation"
)
@mock.patch(
"lms.djangoapps.learner_dashboard.serializers.EnterpriseDashboardsSerializer.to_representation"
)
@mock.patch(
"lms.djangoapps.learner_dashboard.serializers.EmailConfirmationSerializer.to_representation"
)
def test_linkage(
self, mock_platform_settings_serializer, mock_learner_enrollment_serializer
self,
mock_email_confirmation_serializer,
mock_enterprise_dashboards_serializer,
mock_platform_settings_serializer,
mock_learner_enrollment_serializer,
mock_entitlements_serializer,
mock_suggestions_serializer,
):
mock_email_confirmation_serializer.return_value = (
mock_email_confirmation_serializer
)
mock_enterprise_dashboards_serializer.return_value = (
mock_enterprise_dashboards_serializer
)
mock_platform_settings_serializer.return_value = (
mock_platform_settings_serializer
)
mock_learner_enrollment_serializer.return_value = (
mock_learner_enrollment_serializer
)
mock_entitlements_serializer.return_value = mock_entitlements_serializer
mock_suggestions_serializer.return_value = mock_suggestions_serializer
input_data = {
"emailConfirmation": {},
"enterpriseDashboards": [{}],
"platformSettings": {},
"enrollments": [{}],
"unfulfilledEntitlements": [],
"suggestedCourses": [],
"unfulfilledEntitlements": [{}],
"suggestedCourses": [{}],
}
output_data = LearnerDashboardSerializer(input_data).data
self.assertDictEqual(
output_data,
{
"emailConfirmation": mock_email_confirmation_serializer,
"enterpriseDashboards": mock_enterprise_dashboards_serializer,
"platformSettings": mock_platform_settings_serializer,
"enrollments": [mock_learner_enrollment_serializer],
"unfulfilledEntitlements": [],
"suggestedCourses": [],
"unfulfilledEntitlements": [mock_entitlements_serializer],
"suggestedCourses": [mock_suggestions_serializer],
},
)