From 4b5f4f1eff03bc5419f4876939e09bc5b3738937 Mon Sep 17 00:00:00 2001 From: Zainab Amir Date: Wed, 9 Nov 2022 03:05:28 +0500 Subject: [PATCH] feat: add serializer for recommendations api (#31270) --- lms/djangoapps/learner_home/serializers.py | 20 +++++ .../learner_home/test_serializers.py | 76 +++++++++++++++++++ lms/djangoapps/learner_home/test_views.py | 41 ++++++++-- lms/djangoapps/learner_home/views.py | 25 +++--- 4 files changed, 145 insertions(+), 17 deletions(-) diff --git a/lms/djangoapps/learner_home/serializers.py b/lms/djangoapps/learner_home/serializers.py index a28deb75f5..9cea70f3a1 100644 --- a/lms/djangoapps/learner_home/serializers.py +++ b/lms/djangoapps/learner_home/serializers.py @@ -515,6 +515,26 @@ class UnfulfilledEntitlementSerializer(serializers.Serializer): ).data +class RecommendedCourseSerializer(serializers.Serializer): + """Serializer for a recommended course from the recommendation engine""" + + courseKey = serializers.CharField(source="course_key") + logoImageUrl = serializers.URLField(source="logo_image_url") + marketingUrl = serializers.URLField(source="marketing_url") + title = serializers.CharField() + + +class CourseRecommendationSerializer(serializers.Serializer): + """Recommended courses by the Amplitude""" + + courses = serializers.ListField( + child=RecommendedCourseSerializer(), allow_empty=True + ) + isPersonalizedRecommendation = serializers.BooleanField( + source="is_personalized_recommendation" + ) + + class SuggestedCourseSerializer(serializers.Serializer): """Serializer for a suggested course from recommendation engine""" diff --git a/lms/djangoapps/learner_home/test_serializers.py b/lms/djangoapps/learner_home/test_serializers.py index 3422d3709c..4a54838208 100644 --- a/lms/djangoapps/learner_home/test_serializers.py +++ b/lms/djangoapps/learner_home/test_serializers.py @@ -30,6 +30,7 @@ from openedx.core.djangoapps.content.course_overviews.tests.factories import ( from lms.djangoapps.learner_home.serializers import ( CertificateSerializer, CourseProviderSerializer, + CourseRecommendationSerializer, CourseRunSerializer, CourseSerializer, EmailConfirmationSerializer, @@ -960,6 +961,81 @@ class TestUnfulfilledEntitlementSerializer(LearnerDashboardBaseTest): assert expected_keys == actual_keys +class TestCourseRecommendationSerializer(TestCase): + """High-level tests for CourseRecommendationSerializer""" + + @classmethod + def mock_recommended_courses(cls, courses_count=2): + """Sample course data""" + + recommended_courses = [] + + for _ in range(courses_count): + recommended_courses.append( + { + "course_key": str(uuid4()), + "logo_image_url": random_url(), + "marketing_url": random_url(), + "title": str(uuid4()), + }, + ) + + return recommended_courses + + def test_no_recommended_courses(self): + """That that data serializes correctly for empty courses list""" + + recommended_courses = self.mock_recommended_courses(courses_count=0) + + output_data = CourseRecommendationSerializer( + { + "courses": recommended_courses, + "is_personalized_recommendation": False, + } + ).data + + self.assertDictEqual( + output_data, + { + "courses": [], + "isPersonalizedRecommendation": False, + }, + ) + + def test_happy_path(self): + """Test that data serializes correctly""" + + recommended_courses = self.mock_recommended_courses() + + output_data = CourseRecommendationSerializer( + { + "courses": recommended_courses, + "is_personalized_recommendation": True, + } + ).data + + self.assertDictEqual( + output_data, + { + "courses": [ + { + "courseKey": recommended_courses[0]["course_key"], + "logoImageUrl": recommended_courses[0]["logo_image_url"], + "marketingUrl": recommended_courses[0]["marketing_url"], + "title": recommended_courses[0]["title"], + }, + { + "courseKey": recommended_courses[1]["course_key"], + "logoImageUrl": recommended_courses[1]["logo_image_url"], + "marketingUrl": recommended_courses[1]["marketing_url"], + "title": recommended_courses[1]["title"], + }, + ], + "isPersonalizedRecommendation": True, + }, + ) + + class TestSuggestedCourseSerializer(TestCase): """High-level tests for SuggestedCourseSerializer""" diff --git a/lms/djangoapps/learner_home/test_views.py b/lms/djangoapps/learner_home/test_views.py index e849352072..add9b273c0 100644 --- a/lms/djangoapps/learner_home/test_views.py +++ b/lms/djangoapps/learner_home/test_views.py @@ -890,7 +890,8 @@ class TestCourseRecommendationApiView(SharedModuleStoreTestCase): @override_waffle_flag(ENABLE_LEARNER_HOME_AMPLITUDE_RECOMMENDATIONS, active=True) @mock.patch( - "lms.djangoapps.learner_home.views.get_personalized_course_recommendations", Mock(side_effect=Exception) + "lms.djangoapps.learner_home.views.get_personalized_course_recommendations", + Mock(side_effect=Exception), ) def test_amplitude_api_unexpected_error(self): """ @@ -920,9 +921,11 @@ class TestCourseRecommendationApiView(SharedModuleStoreTestCase): response = self.client.get(self.url) self.assertEqual(response.status_code, 200) - self.assertEqual(response.data.get("is_personalized_recommendation"), True) + + response_content = json.loads(response.content) + self.assertEqual(response_content.get("isPersonalizedRecommendation"), True) self.assertEqual( - len(response.data.get("courses")), expected_recommendations_length + len(response_content.get("courses")), expected_recommendations_length ) @override_waffle_flag(ENABLE_LEARNER_HOME_AMPLITUDE_RECOMMENDATIONS, active=True) @@ -930,7 +933,9 @@ class TestCourseRecommendationApiView(SharedModuleStoreTestCase): @mock.patch( "lms.djangoapps.learner_home.views.get_personalized_course_recommendations" ) - def test_general_recommendations(self, mocked_get_personalized_course_recommendations): + def test_general_recommendations( + self, mocked_get_personalized_course_recommendations + ): """ Test that a user gets general recommendations for the control group. """ @@ -941,8 +946,26 @@ class TestCourseRecommendationApiView(SharedModuleStoreTestCase): response = self.client.get(self.url) self.assertEqual(response.status_code, 200) - self.assertEqual(response.data.get("is_personalized_recommendation"), False) - self.assertEqual(response.data.get("courses"), self.GENERAL_RECOMMENDATIONS) + + response_content = json.loads(response.content) + self.assertEqual(response_content.get("isPersonalizedRecommendation"), False) + self.assertEqual( + response_content.get("courses"), + [ + { + "courseKey": self.GENERAL_RECOMMENDATIONS[0]["course_key"], + "logoImageUrl": self.GENERAL_RECOMMENDATIONS[0]["logo_image_url"], + "marketingUrl": self.GENERAL_RECOMMENDATIONS[0]["marketing_url"], + "title": self.GENERAL_RECOMMENDATIONS[0]["title"], + }, + { + "courseKey": self.GENERAL_RECOMMENDATIONS[1]["course_key"], + "logoImageUrl": self.GENERAL_RECOMMENDATIONS[1]["logo_image_url"], + "marketingUrl": self.GENERAL_RECOMMENDATIONS[1]["marketing_url"], + "title": self.GENERAL_RECOMMENDATIONS[1]["title"], + }, + ], + ) @override_waffle_flag(ENABLE_LEARNER_HOME_AMPLITUDE_RECOMMENDATIONS, active=True) @mock.patch( @@ -975,5 +998,7 @@ class TestCourseRecommendationApiView(SharedModuleStoreTestCase): response = self.client.get(self.url) self.assertEqual(response.status_code, 200) - self.assertEqual(response.data.get("is_personalized_recommendation"), True) - self.assertEqual(len(response.data.get("courses")), expected_recommendations) + + response_content = json.loads(response.content) + self.assertEqual(response_content.get("isPersonalizedRecommendation"), True) + self.assertEqual(len(response_content.get("courses")), expected_recommendations) diff --git a/lms/djangoapps/learner_home/views.py b/lms/djangoapps/learner_home/views.py index b2e2f62f9c..d4e093b894 100644 --- a/lms/djangoapps/learner_home/views.py +++ b/lms/djangoapps/learner_home/views.py @@ -48,7 +48,10 @@ from lms.djangoapps.courseware.access import administrative_accesses_to_course_f from lms.djangoapps.courseware.access_utils import ( check_course_open_for_learner, ) -from lms.djangoapps.learner_home.serializers import LearnerDashboardSerializer +from lms.djangoapps.learner_home.serializers import ( + CourseRecommendationSerializer, + LearnerDashboardSerializer, +) from lms.djangoapps.learner_home.waffle import ( should_show_learner_home_amplitude_recommendations, ) @@ -581,10 +584,12 @@ class CourseRecommendationApiView(APIView): if is_control: return Response( - { - "courses": settings.GENERAL_RECOMMENDATIONS, - "is_personalized_recommendation": False, - }, + CourseRecommendationSerializer( + { + "courses": settings.GENERAL_RECOMMENDATIONS, + "is_personalized_recommendation": False, + } + ).data, status=200, ) @@ -622,9 +627,11 @@ class CourseRecommendationApiView(APIView): {"count": len(recommended_courses)}, ) return Response( - { - "courses": recommended_courses, - "is_personalized_recommendation": not is_control, - }, + CourseRecommendationSerializer( + { + "courses": recommended_courses, + "is_personalized_recommendation": not is_control, + } + ).data, status=200, )