feat: integrate amplitude API with learner_home frontend [VAN-1124] (#31098)

This commit is contained in:
Syed Sajjad Hussain Shah
2022-10-10 12:09:28 +05:00
committed by GitHub
parent 3bd94f78f9
commit 52de7e9066
5 changed files with 189 additions and 7 deletions

View File

@@ -252,7 +252,7 @@ class TestCourseRecommendationApiView(SharedModuleStoreTestCase):
'marketing_url': 'https://www.edx.org/course/introduction-to-computer-science-and-programming-7'
}
@mock.patch('lms.djangoapps.learner_dashboard.api.v0.views.get_personalized_course_recommendations', )
@mock.patch('lms.djangoapps.learner_dashboard.api.v0.views.get_personalized_course_recommendations')
@mock.patch('lms.djangoapps.learner_dashboard.api.v0.views.get_course_data')
def test_no_recommendations_from_amplitude(self, mocked_get_course_data,
mocked_get_personalized_course_recommendations):

View File

@@ -2,20 +2,22 @@
from contextlib import contextmanager
import json
from unittest import TestCase
from unittest import mock, TestCase
from unittest.mock import Mock, patch
from urllib.parse import urlencode
from uuid import uuid4
import ddt
from django.conf import settings
from django.urls import reverse
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from edx_toggles.toggles.testutils import override_waffle_flag
from opaque_keys.edx.keys import CourseKey
from rest_framework.test import APITestCase
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.entitlements.tests.factories import CourseEntitlementFactory
from common.djangoapps.student.toggles import ENABLE_AMPLITUDE_RECOMMENDATIONS
from common.djangoapps.student.tests.factories import (
CourseEnrollmentFactory,
UserFactory,
@@ -42,14 +44,14 @@ from openedx.core.djangoapps.content.course_overviews.tests.factories import (
CourseOverviewFactory,
)
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
from openedx.core.djangoapps.catalog.tests.factories import (
CourseFactory as CatalogCourseFactory,
)
from xmodule.modulestore.tests.django_utils import (
TEST_DATA_SPLIT_MODULESTORE,
SharedModuleStoreTestCase,
)
from xmodule.modulestore.tests.factories import CourseFactory
from openedx.core.djangoapps.catalog.tests.factories import (
CourseFactory as CatalogCourseFactory,
)
ENTERPRISE_ENABLED = "ENABLE_ENTERPRISE_INTEGRATION"
@@ -750,3 +752,86 @@ class TestDashboardMasquerade(BaseTestDashboardView):
# username has priority in the lookup
assert response.status_code == 200
assert self.get_first_course_id(response) == str(user_3_enrollment.course_id)
class TestCourseRecommendationApiView(SharedModuleStoreTestCase):
"""Unit tests for the course recommendations on learner home page."""
password = 'test'
url = reverse_lazy('learner_home:courses')
def setUp(self):
super().setUp()
self.user = UserFactory()
self.client.login(username=self.user.username, password=self.password)
self.recommended_courses = ['MITx+6.00.1x', 'IBM+PY0101EN', 'HarvardX+CS50P', 'UQx+IELTSx', 'HarvardX+CS50x',
'Harvard+CS50z', 'BabsonX+EPS03x', 'TUMx+QPLS2x', 'NYUx+FCS.NET.1', 'MichinX+101x']
self.course_data = {
'course_key': 'MITx+6.00.1x',
'title': 'Introduction to Computer Science and Programming Using Python',
'owners': [{'logo_image_url': 'https://www.logo_image_url.com'}],
'marketing_url': 'https://www.marketing_url.com'
}
@override_waffle_flag(ENABLE_AMPLITUDE_RECOMMENDATIONS, active=False)
def test_waffle_flag_off(self):
"""
Verify API returns 400 if waffle flag is off.
"""
response = self.client.get(self.url)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.data, None)
@override_waffle_flag(ENABLE_AMPLITUDE_RECOMMENDATIONS, active=True)
@mock.patch('lms.djangoapps.learner_home.views.get_personalized_course_recommendations')
@mock.patch('lms.djangoapps.learner_home.views.get_course_data')
def test_no_recommendations_from_amplitude(self, mocked_get_course_data,
mocked_get_personalized_course_recommendations):
"""
Verify API returns 400 if no course recommendations from amplitude.
"""
mocked_get_personalized_course_recommendations.return_value = [False, []]
mocked_get_course_data.return_value = self.course_data
response = self.client.get(self.url)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.data, None)
@override_waffle_flag(ENABLE_AMPLITUDE_RECOMMENDATIONS, active=True)
@mock.patch('lms.djangoapps.learner_home.views.get_personalized_course_recommendations')
@mock.patch('lms.djangoapps.learner_home.views.get_course_data')
def test_get_course_recommendations(self, mocked_get_course_data,
mocked_get_personalized_course_recommendations):
"""
Verify API returns course recommendations.
"""
mocked_get_personalized_course_recommendations.return_value = [False, self.recommended_courses]
mocked_get_course_data.return_value = self.course_data
expected_recommendations_length = 5
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_length)
@override_waffle_flag(ENABLE_AMPLITUDE_RECOMMENDATIONS, active=True)
@mock.patch('lms.djangoapps.learner_home.views.get_personalized_course_recommendations')
@mock.patch('lms.djangoapps.learner_home.views.get_course_data')
def test_get_enrollable_course_recommendations(self, mocked_get_course_data,
mocked_get_personalized_course_recommendations):
"""
Verify API returns course recommendations for courses in which user is not enrolled.
"""
mocked_get_personalized_course_recommendations.return_value = [False, self.recommended_courses]
mocked_get_course_data.return_value = self.course_data
course_keys = ['course-v1:IBM+PY0101EN+Run_0', 'course-v1:UQx+IELTSx+Run_0', 'course-v1:MITx+6.00.1x+Run_0',
'course-v1:HarvardX+CS50P+Run_0', 'course-v1:Harvard+CS50z+Run_0', 'course-v1:TUMx+QPLS2x+Run_0']
expected_recommendations = 4
# enrolling in 6 courses
for course_key in course_keys:
CourseEnrollmentFactory(course_id=course_key, user=self.user)
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)

View File

@@ -12,4 +12,5 @@ urlpatterns = [
re_path(
r"^mock/init/?", mock_views.InitializeView.as_view(), name="mock_initialize"
),
re_path(r"^recommendation/courses/$", views.CourseRecommendationApiView.as_view(), name="courses"),
]

View File

@@ -0,0 +1,33 @@
"""API utils"""
import logging
import requests
from django.conf import settings
log = logging.getLogger(__name__)
def get_personalized_course_recommendations(user_id):
""" Get personalize recommendations from Amplitude. """
headers = {
'Authorization': f'Api-Key {settings.AMPLITUDE_API_KEY}',
'Content-Type': 'application/json'
}
params = {
'user_id': user_id,
'get_recs': True,
'rec_id': settings.REC_ID,
}
try:
response = requests.get(settings.AMPLITUDE_URL, params=params, headers=headers)
if response.status_code == 200:
response = response.json()
recommendations = response.get('userData', {}).get('recommendations', [])
if recommendations:
is_control = recommendations[0].get('is_control')
recommended_course_keys = recommendations[0].get('items')
return is_control, recommended_course_keys
except Exception as ex: # pylint: disable=broad-except
log.warning(f'Cannot get recommendations from Amplitude: {ex}')
return True, []

View File

@@ -8,19 +8,25 @@ from django.core.exceptions import MultipleObjectsReturned
from edx_django_utils import monitoring as monitoring_utils
from opaque_keys.edx.keys import CourseKey
from rest_framework.exceptions import PermissionDenied, NotFound
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.generics import RetrieveAPIView
from rest_framework.views import APIView
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.edxmako.shortcuts import marketing_link
from common.djangoapps.student.helpers import cert_info, get_resume_urls_for_enrollments
from common.djangoapps.student.models import get_user_by_username_or_email
from common.djangoapps.student.models import CourseEnrollment, get_user_by_username_or_email
from common.djangoapps.student.toggles import should_show_amplitude_recommendations
from common.djangoapps.student.views.dashboard import (
complete_course_mode_info,
get_course_enrollments,
get_org_black_and_whitelist_for_site,
get_filtered_course_entitlements,
)
from common.djangoapps.track import segment
from common.djangoapps.util.milestones_helpers import (
get_pre_requisite_courses_not_completed,
)
@@ -32,9 +38,11 @@ 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.utils import get_personalized_course_recommendations
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.programs.utils import ProgramProgressMeter
from openedx.core.djangoapps.catalog.utils import get_course_data
from openedx.features.enterprise_support.api import (
enterprise_customer_from_session_or_learner_data,
get_enterprise_learner_data_from_db,
@@ -401,3 +409,58 @@ class InitializeView(RetrieveAPIView): # pylint: disable=unused-argument
learner_dash_data, context=context
).data
return Response(response_data)
class CourseRecommendationApiView(APIView):
"""
**Example Request**
GET /api/learner_home/recommendation/courses/
"""
authentication_classes = (JwtAuthentication, SessionAuthenticationAllowInactiveUser,)
permission_classes = (IsAuthenticated,)
def get(self, request):
""" Retrieves course recommendations details of a user in a specified course. """
if not should_show_amplitude_recommendations():
return Response(status=400)
user_id = request.user.id
is_control, course_keys = get_personalized_course_recommendations(user_id)
# Emits an event to track student dashboard page visits.
segment.track(
user_id,
'edx.bi.user.recommendations.viewed',
{
'is_personalized_recommendation': not is_control,
}
)
if is_control or not course_keys:
return Response(status=400)
recommended_courses = []
user_enrolled_course_keys = set()
fields = ['title', 'owners', 'marketing_url']
course_enrollments = CourseEnrollment.enrollments_for_user(request.user)
for course_enrollment in course_enrollments:
course_key = f'{course_enrollment.course_id.org}+{course_enrollment.course_id.course}'
user_enrolled_course_keys.add(course_key)
# Pick 5 course keys, excluding the user's already enrolled course(s).
enrollable_course_keys = list(set(course_keys).difference(user_enrolled_course_keys))[:5]
for course_id in enrollable_course_keys:
course_data = get_course_data(course_id, fields)
if course_data:
recommended_courses.append({
'course_key': course_id,
'title': course_data['title'],
'logo_image_url': course_data['owners'][0]['logo_image_url'],
'marketing_url': course_data.get('marketing_url')
})
segment.track(user_id, 'edx.bi.user.recommendations.count', {'count': len(recommended_courses)})
return Response({'courses': recommended_courses, 'is_personalized_recommendation': not is_control}, status=200)