feat: learner dash enrollment serializers (#30674)

* feat: add CourseProviderSerializer

* feat: add CourseSerializer

* feat: add CourseRunSerializer

* feat: add EnrollmentSerializer

* feat: add GradeDataSerializer

* feat: add CertificateSerializer

* feat: add EntitlementSerializer

* feat: add ProgramsSerializer

Co-authored-by: nsprenkle <nsprenkle@2u.com>
This commit is contained in:
Nathan Sprenkle
2022-07-05 11:12:12 -04:00
committed by GitHub
parent 9d1f31603e
commit 9d54fda8e8
2 changed files with 520 additions and 36 deletions

View File

@@ -14,11 +14,126 @@ class PlatformSettingsSerializer(serializers.Serializer):
courseSearchUrl = serializers.URLField()
class CourseProviderSerializer(serializers.Serializer):
"""Info about a course provider (institution/business)"""
name = serializers.CharField()
website = serializers.URLField()
email = serializers.EmailField()
class CourseSerializer(serializers.Serializer):
"""Serializer for course header info"""
bannerImgSrc = serializers.URLField()
courseName = serializers.CharField()
class CourseRunSerializer(serializers.Serializer):
"""Serializer for course run info"""
isPending = serializers.BooleanField()
isStarted = serializers.BooleanField()
isFinished = serializers.BooleanField()
isArchived = serializers.BooleanField()
courseNumber = serializers.CharField()
accessExpirationDate = serializers.DateTimeField()
minPassingGrade = serializers.DecimalField(max_digits=5, decimal_places=2)
endDate = serializers.DateTimeField()
homeUrl = serializers.URLField()
marketingUrl = serializers.URLField()
progressUrl = serializers.URLField()
unenrollUrl = serializers.URLField()
upgradeUrl = serializers.URLField()
class EnrollmentSerializer(serializers.Serializer):
"""Serializer for an enrollment"""
"""Info about this particular enrollment"""
isAudit = serializers.BooleanField()
isVerified = serializers.BooleanField()
canUpgrade = serializers.BooleanField()
isAuditAccessExpired = serializers.BooleanField()
isEmailEnabled = serializers.BooleanField()
class GradeDataSerializer(serializers.Serializer):
"""Info about grades for this enrollment"""
isPassing = serializers.BooleanField()
class CertificateSerializer(serializers.Serializer):
"""Certificate availability info"""
availableDate = serializers.DateTimeField(allow_null=True)
isRestricted = serializers.BooleanField()
isAvailable = serializers.BooleanField()
isEarned = serializers.BooleanField()
isDownloadable = serializers.BooleanField()
certPreviewUrl = serializers.URLField(allow_null=True)
certDownloadUrl = serializers.URLField(allow_null=True)
honorCertDownloadUrl = serializers.URLField(allow_null=True)
class AvailableEntitlementSessionSerializer(serializers.Serializer):
"""An available entitlement session"""
startDate = serializers.DateTimeField()
endDate = serializers.DateTimeField()
courseNumber = serializers.CharField()
class EntitlementSerializer(serializers.Serializer):
"""Entitlements info"""
availableSessions = serializers.ListField(
child=AvailableEntitlementSessionSerializer(), allow_empty=True
)
isRefundable = serializers.BooleanField()
isFulfilled = serializers.BooleanField()
canViewCourse = serializers.BooleanField()
changeDeadline = serializers.DateTimeField()
isExpired = serializers.BooleanField()
class RelatedProgramSerializer(serializers.Serializer):
"""Related programs information"""
provider = serializers.CharField()
programUrl = serializers.URLField()
bannerUrl = serializers.URLField()
logoUrl = serializers.URLField()
title = serializers.CharField()
# Note - this should probably be a choice, eventually
programType = serializers.CharField()
programTypeUrl = serializers.URLField()
numberOfCourses = serializers.IntegerField()
estimatedNumberOfWeeks = serializers.IntegerField()
class ProgramsSerializer(serializers.Serializer):
"""Programs information"""
relatedPrograms = serializers.ListField(
child=RelatedProgramSerializer(), allow_empty=True
)
class LearnerEnrollmentSerializer(serializers.Serializer):
"""Info for displaying an enrollment on the learner dashboard"""
courseProvider = CourseProviderSerializer(allow_null=True)
course = CourseSerializer()
courseRun = CourseRunSerializer()
enrollment = EnrollmentSerializer()
gradeData = GradeDataSerializer()
certificate = CertificateSerializer()
entitlements = EntitlementSerializer()
programs = ProgramsSerializer()
class UnfulfilledEntitlementSerializer(serializers.Serializer):
"""Serializer for an unfulfilled entitlement"""
@@ -30,6 +145,12 @@ class LearnerDashboardSerializer(serializers.Serializer):
"""Serializer for all info required to render the Learner Dashboard"""
edx = PlatformSettingsSerializer()
enrollments = serializers.ListField(child=EnrollmentSerializer(), allow_empty=True)
unfulfilledEntitlements = serializers.ListField(child=EntitlementSerializer(), allow_empty=True)
suggestedCourses = serializers.ListField(child=SuggestedCourseSerializer(), allow_empty=True)
enrollments = serializers.ListField(
child=LearnerEnrollmentSerializer(), allow_empty=True
)
unfulfilledEntitlements = serializers.ListField(
child=EntitlementSerializer(), allow_empty=True
)
suggestedCourses = serializers.ListField(
child=SuggestedCourseSerializer(), allow_empty=True
)

View File

@@ -1,25 +1,75 @@
"""Tests for serializers for the Learner Dashboard"""
import datetime
from random import choice, getrandbits, randint
from time import time
from unittest import TestCase
from unittest import mock
from uuid import uuid4
from lms.djangoapps.learner_dashboard.serializers import (
CertificateSerializer,
CourseProviderSerializer,
CourseRunSerializer,
CourseSerializer,
EnrollmentSerializer,
EntitlementSerializer,
GradeDataSerializer,
LearnerEnrollmentSerializer,
PlatformSettingsSerializer,
ProgramsSerializer,
LearnerDashboardSerializer,
)
def random_bool():
"""Test util for generating a random boolean"""
return bool(getrandbits(1))
def random_date(allow_null=False):
"""Test util for generating a random date, optionally blank"""
# If null allowed, return null half the time
if allow_null and random_bool():
return None
d = randint(1, int(time()))
return datetime.datetime.fromtimestamp(d)
def random_url(allow_null=False):
"""Test util for generating a random URL, optionally blank"""
# If null allowed, return null half the time
if allow_null and random_bool():
return None
random_uuid = uuid4()
return choice([f"{random_uuid}.example.com", f"example.com/{random_uuid}"])
def datetime_to_django_format(datetime_obj):
"""Util for matching serialized Django datetime format for comparison"""
if datetime_obj:
return datetime_obj.strftime("%Y-%m-%dT%H:%M:%SZ")
class TestPlatformSettingsSerializer(TestCase):
"""Tests for the PlatformSettingsSerializer"""
def test_happy_path(self):
input_data = {
@classmethod
def generate_test_platform_settings(cls):
"""Util to generate test platform settings data"""
return {
"feedbackEmail": f"{uuid4()}@example.com",
"supportEmail": f"{uuid4()}@example.com",
"billingEmail": f"{uuid4()}@example.com",
"courseSearchUrl": f"{uuid4()}.example.com/search",
}
def test_happy_path(self):
input_data = self.generate_test_platform_settings()
output_data = PlatformSettingsSerializer(input_data).data
assert output_data == {
@@ -30,6 +80,335 @@ class TestPlatformSettingsSerializer(TestCase):
}
class TestCourseProviderSerializer(TestCase):
"""Tests for the CourseProviderSerializer"""
@classmethod
def generate_test_provider_info(cls):
"""Util to generate test provider info"""
return {
"name": f"{uuid4()}",
"website": f"{uuid4()}.example.com",
"email": f"{uuid4()}@example.com",
}
def test_happy_path(self):
input_data = self.generate_test_provider_info()
output_data = CourseProviderSerializer(input_data).data
assert output_data == {
"name": input_data["name"],
"website": input_data["website"],
"email": input_data["email"],
}
class TestCourseSerializer(TestCase):
"""Tests for the CourseSerializer"""
@classmethod
def generate_test_course_info(cls):
"""Util to generate test course info"""
return {
"bannerImgSrc": f"example.com/assets/{uuid4()}",
"courseName": f"{uuid4()}",
}
def test_happy_path(self):
input_data = self.generate_test_course_info()
output_data = CourseSerializer(input_data).data
assert output_data == {
"bannerImgSrc": input_data["bannerImgSrc"],
"courseName": input_data["courseName"],
}
class TestCourseRunSerializer(TestCase):
"""Tests for the CourseRunSerializer"""
@classmethod
def generate_test_course_run_info(cls):
"""Util to generate test course run info"""
return {
"isPending": random_bool(),
"isStarted": random_bool(),
"isFinished": random_bool(),
"isArchived": random_bool(),
"courseNumber": f"{uuid4()}-101",
"accessExpirationDate": random_date(),
"minPassingGrade": randint(0, 10000) / 100,
"endDate": random_date(),
"homeUrl": f"{uuid4()}.example.com",
"marketingUrl": f"{uuid4()}.example.com",
"progressUrl": f"{uuid4()}.example.com",
"unenrollUrl": f"{uuid4()}.example.com",
"upgradeUrl": f"{uuid4()}.example.com",
}
def test_happy_path(self):
input_data = self.generate_test_course_run_info()
output_data = CourseRunSerializer(input_data).data
assert output_data == {
"isPending": input_data["isPending"],
"isStarted": input_data["isStarted"],
"isFinished": input_data["isFinished"],
"isArchived": input_data["isArchived"],
"courseNumber": input_data["courseNumber"],
"accessExpirationDate": datetime_to_django_format(
input_data["accessExpirationDate"]
),
"minPassingGrade": str(input_data["minPassingGrade"]),
"endDate": datetime_to_django_format(input_data["endDate"]),
"homeUrl": input_data["homeUrl"],
"marketingUrl": input_data["marketingUrl"],
"progressUrl": input_data["progressUrl"],
"unenrollUrl": input_data["unenrollUrl"],
"upgradeUrl": input_data["upgradeUrl"],
}
class TestEnrollmentSerializer(TestCase):
"""Tests for the EnrollmentSerializer"""
@classmethod
def generate_test_enrollment_info(cls):
"""Util to generate test enrollment info"""
return {
"isAudit": random_bool(),
"isVerified": random_bool(),
"canUpgrade": random_bool(),
"isAuditAccessExpired": random_bool(),
"isEmailEnabled": 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"],
}
class TestGradeDataSerializer(TestCase):
"""Tests for the GradeDataSerializer"""
@classmethod
def generate_test_grade_data(cls):
"""Util to generate test grade data"""
return {
"isPassing": random_bool(),
}
def test_happy_path(self):
input_data = self.generate_test_grade_data()
output_data = GradeDataSerializer(input_data).data
assert output_data == {
"isPassing": input_data["isPassing"],
}
class TestCertificateSerializer(TestCase):
"""Tests for the CertificateSerializer"""
@classmethod
def generate_test_certificate_info(cls):
"""Util to generate test certificate info"""
return {
"availableDate": random_date(allow_null=True),
"isRestricted": random_bool(),
"isAvailable": random_bool(),
"isEarned": random_bool(),
"isDownloadable": random_bool(),
"certPreviewUrl": random_url(allow_null=True),
"certDownloadUrl": random_url(allow_null=True),
"honorCertDownloadUrl": random_url(allow_null=True),
}
def test_happy_path(self):
input_data = self.generate_test_certificate_info()
output_data = CertificateSerializer(input_data).data
assert output_data == {
"availableDate": datetime_to_django_format(input_data["availableDate"]),
"isRestricted": input_data["isRestricted"],
"isAvailable": input_data["isAvailable"],
"isEarned": input_data["isEarned"],
"isDownloadable": input_data["isDownloadable"],
"certPreviewUrl": input_data["certPreviewUrl"],
"certDownloadUrl": input_data["certDownloadUrl"],
"honorCertDownloadUrl": input_data["honorCertDownloadUrl"],
}
class TestEntitlementSerializer(TestCase):
"""Tests for the EntitlementSerializer"""
@classmethod
def generate_test_session(cls):
"""Generate an test session with random dates and course run numbers"""
return {
"startDate": random_date(),
"endDate": random_date(),
"courseNumber": f"{uuid4()}-101",
}
@classmethod
def generate_test_entitlement_info(cls):
"""Util to generate test entitlement info"""
return {
"availableSessions": [
cls.generate_test_session() for _ in range(randint(0, 3))
],
"isRefundable": random_bool(),
"isFulfilled": random_bool(),
"canViewCourse": random_bool(),
"changeDeadline": random_date(),
"isExpired": random_bool(),
}
def test_happy_path(self):
input_data = self.generate_test_entitlement_info()
output_data = EntitlementSerializer(input_data).data
# Compare output sessions separately, since they're more complicated
output_sessions = output_data.pop("availableSessions")
for i, output_session in enumerate(output_sessions):
input_session = input_data["availableSessions"][i]
input_session["startDate"] = datetime_to_django_format(
input_session["startDate"]
)
input_session["endDate"] = datetime_to_django_format(
input_session["endDate"]
)
assert output_session == input_session
assert output_data == {
"isRefundable": input_data["isRefundable"],
"isFulfilled": input_data["isFulfilled"],
"canViewCourse": input_data["canViewCourse"],
"changeDeadline": datetime_to_django_format(input_data["changeDeadline"]),
"isExpired": input_data["isExpired"],
}
class TestProgramsSerializer(TestCase):
"""Tests for the ProgramsSerializer and RelatedProgramsSerializer"""
@classmethod
def generate_test_related_program(cls):
"""Generate a program with random test data"""
return {
"provider": f"{uuid4()} Inc.",
"programUrl": random_url(),
"bannerUrl": random_url(),
"logoUrl": random_url(),
"title": f"{uuid4()}",
"programType": f"{uuid4()}",
"programTypeUrl": random_url(),
"numberOfCourses": randint(0, 100),
"estimatedNumberOfWeeks": randint(0, 45),
}
@classmethod
def generate_test_programs_info(cls):
"""Util to generate test programs info"""
return {
"relatedPrograms": [
cls.generate_test_related_program() for _ in range(randint(0, 3))
],
}
def test_happy_path(self):
input_data = self.generate_test_programs_info()
output_data = ProgramsSerializer(input_data).data
related_programs = output_data.pop("relatedPrograms")
for i, related_program in enumerate(related_programs):
input_program = input_data["relatedPrograms"][i]
assert related_program == {
"provider": input_program["provider"],
"programUrl": input_program["programUrl"],
"bannerUrl": input_program["bannerUrl"],
"logoUrl": input_program["logoUrl"],
"title": input_program["title"],
"programType": input_program["programType"],
"programTypeUrl": input_program["programTypeUrl"],
"numberOfCourses": input_program["numberOfCourses"],
"estimatedNumberOfWeeks": input_program["estimatedNumberOfWeeks"],
}
self.assertDictEqual(output_data, {})
def test_empty_sessions(self):
input_data = {"relatedPrograms": []}
output_data = ProgramsSerializer(input_data).data
assert output_data == {"relatedPrograms": []}
class TestLearnerEnrollmentsSerializer(TestCase):
"""High-level tests for LearnerEnrollmentsSerializer"""
@classmethod
def generate_test_enrollments_data(cls):
return {
"courseProvider": TestCourseProviderSerializer.generate_test_provider_info(),
"course": TestCourseSerializer.generate_test_course_info(),
"courseRun": TestCourseRunSerializer.generate_test_course_run_info(),
"enrollment": TestEnrollmentSerializer.generate_test_enrollment_info(),
"gradeData": TestGradeDataSerializer.generate_test_grade_data(),
"certificate": TestCertificateSerializer.generate_test_certificate_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_enrollments_data()
output_data = LearnerEnrollmentSerializer(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_enrollments_data()
input_data["courseProvider"] = None
output_data = LearnerEnrollmentSerializer(input_data).data
expected_keys = [
"courseProvider",
"course",
"courseRun",
"enrollment",
"gradeData",
"certificate",
"entitlements",
"programs",
]
assert output_data.keys() == set(expected_keys)
class TestLearnerDashboardSerializer(TestCase):
"""High-level tests for Learner Dashboard serialization"""
@@ -57,41 +436,25 @@ class TestLearnerDashboardSerializer(TestCase):
},
)
def test_linkage(self):
"""Test that serializers link to their appropriate outputs"""
input_data = {
"edx": {},
"enrollments": [],
"unfulfilledEntitlements": [],
"suggestedCourses": [],
}
serializer = LearnerDashboardSerializer(input_data)
with mock.patch(
"lms.djangoapps.learner_dashboard.serializers.PlatformSettingsSerializer.to_representation"
) as mock_platform_settings_serializer:
mock_platform_settings_serializer.return_value = mock_platform_settings_serializer
output_data = serializer.data
self.assertDictEqual(
output_data,
{
"edx": mock_platform_settings_serializer,
"enrollments": [],
"unfulfilledEntitlements": [],
"suggestedCourses": [],
},
)
@mock.patch(
"lms.djangoapps.learner_dashboard.serializers.LearnerEnrollmentSerializer.to_representation"
)
@mock.patch(
"lms.djangoapps.learner_dashboard.serializers.PlatformSettingsSerializer.to_representation"
)
def test_linkage2(self, mock_platform_settings_serializer):
"""Second example of paradigm using test-level patching"""
mock_platform_settings_serializer.return_value = mock_platform_settings_serializer
def test_linkage(
self, mock_platform_settings_serializer, mock_learner_enrollment_serializer
):
mock_platform_settings_serializer.return_value = (
mock_platform_settings_serializer
)
mock_learner_enrollment_serializer.return_value = (
mock_learner_enrollment_serializer
)
input_data = {
"edx": {},
"enrollments": [],
"enrollments": [{}],
"unfulfilledEntitlements": [],
"suggestedCourses": [],
}
@@ -101,7 +464,7 @@ class TestLearnerDashboardSerializer(TestCase):
output_data,
{
"edx": mock_platform_settings_serializer,
"enrollments": [],
"enrollments": [mock_learner_enrollment_serializer],
"unfulfilledEntitlements": [],
"suggestedCourses": [],
},