feat: [VAN-986] Add amplitude API (#30675)

* feat: [VAN-986] Add amplitude API

* feat: add suggestion

* feat: add spinner

Co-authored-by: Zainab Amir <zainab.amir@arbisoft.com>
This commit is contained in:
Attiya Ishaque
2022-07-05 16:23:38 +05:00
committed by GitHub
parent 4d6df67a02
commit 9d1f31603e
8 changed files with 211 additions and 72 deletions

View File

@@ -2666,3 +2666,5 @@ COURSE_LIVE_HELP_URL = "https://edx.readthedocs.io/projects/edx-partner-course-s
# keys for big blue button live provider
COURSE_LIVE_GLOBAL_CREDENTIALS = {}
PERSONALIZED_RECOMMENDATION_COOKIE_NAME = 'edx-user-personalized-recommendation'

View File

@@ -19,7 +19,7 @@ from collections import defaultdict, namedtuple # lint-amnesty, pylint: disable
from datetime import date, datetime, timedelta # lint-amnesty, pylint: disable=wrong-import-order
from functools import total_ordering # lint-amnesty, pylint: disable=wrong-import-order
from importlib import import_module # lint-amnesty, pylint: disable=wrong-import-order
from urllib.parse import urlencode, urljoin
from urllib.parse import unquote, urlencode, urljoin
import crum
from config_models.models import ConfigurationModel
@@ -438,6 +438,21 @@ def get_potentially_retired_user_by_username_and_hash(username, hashed_username)
return User.objects.get(username__in=locally_hashed_usernames)
def is_personalized_recommendation_for_user(course_id):
"""
Returns the personalized recommendation value from the cookie.
"""
request = crum.get_current_request()
recommended_courses = \
request.COOKIES.get(settings.PERSONALIZED_RECOMMENDATION_COOKIE_NAME, None) if request else None
if recommended_courses:
recommended_courses = json.loads(unquote(recommended_courses))
if course_id in recommended_courses['course_keys']:
return recommended_courses['is_personalized_recommendation']
return None
class UserStanding(models.Model):
"""
This table contains a student's account's status.
@@ -1557,6 +1572,11 @@ class CourseEnrollment(models.Model):
self.course_id)
segment_properties['course_start'] = self.course.start
segment_properties['course_pacing'] = self.course.pacing
is_personalized_recommendation = is_personalized_recommendation_for_user(str(self.course_id))
if is_personalized_recommendation is not None:
segment_properties['is_personalized_recommendation'] = is_personalized_recommendation
with tracker.get_tracker().context(event_name, context):
tracker.emit(event_name, data)
segment.track(self.user_id, event_name, segment_properties, traits=segment_traits)

View File

@@ -0,0 +1,31 @@
"""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()
is_control = response['userData']['recommendations'][0]['is_control']
course_keys = response['userData']['recommendations'][0]['items']
return is_control, course_keys
except Exception as ex: # pylint: disable=broad-except
log.exception(f'Cannot get recommendations from Amplitude: {ex}')
return True, []

View File

@@ -4,12 +4,17 @@ Learner Dashboard API v0 URLs.
from django.urls import re_path
from lms.djangoapps.learner_dashboard.api.v0.views import Programs, ProgramProgressDetailView
from lms.djangoapps.learner_dashboard.api.v0.views import (
Programs,
ProgramProgressDetailView,
CourseRecommendationApiView
)
UUID_REGEX_PATTERN = r'[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?4[0-9a-fA-F]{3}-?[89abAB][0-9a-fA-F]{3}-?[0-9a-fA-F]{12}'
app_name = 'v0'
urlpatterns = [
re_path(r'^recommendation/courses/$', CourseRecommendationApiView.as_view(), name='courses'),
re_path(
fr'^programs/(?P<enterprise_uuid>{UUID_REGEX_PATTERN})/$',
Programs.as_view(),

View File

@@ -8,6 +8,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.track import segment
from openedx.core.djangoapps.programs.utils import (
ProgramProgressMeter,
get_certificates,
@@ -15,6 +16,8 @@ from openedx.core.djangoapps.programs.utils import (
get_program_and_course_data,
get_program_urls
)
from openedx.core.djangoapps.catalog.utils import get_course_data
from lms.djangoapps.learner_dashboard.api.utils import get_personalized_course_recommendations
class Programs(APIView):
@@ -335,3 +338,45 @@ class ProgramProgressDetailView(APIView):
'credit_pathways': credit_pathways,
}
)
class CourseRecommendationApiView(APIView):
"""
**Example Request**
GET api/dashboard/v0/recommendation/courses/
"""
authentication_classes = (JwtAuthentication, SessionAuthentication,)
permission_classes = (IsAuthenticated,)
def get(self, request):
""" Retrieves course recommendations details of a user in a specified course. """
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:
return Response(status=400)
recommended_courses = []
for course_id in course_keys:
course_data = get_course_data(course_id)
if course_data:
recommended_courses.append({
'course_key': course_data['key'],
'title': str(course_data['title']),
'logo_image_url': course_data['owners'][0]['logo_image_url'],
'marketing_url': course_data.get('marketing_url')
})
else:
return Response(status=400)
return Response({'courses': recommended_courses, 'is_personalized_recommendation': not is_control}, status=200)

View File

@@ -4711,6 +4711,12 @@ SAVE_FOR_LATER_EMAIL_RATE_LIMIT = '5/h'
EDX_BRAZE_API_KEY = None
EDX_BRAZE_API_SERVER = None
### SETTINGS FOR AMPLITUDE ####
AMPLITUDE_URL = ''
AMPLITUDE_API_KEY = ''
REC_ID = ''
GENERAL_RECOMMENDATION = {}
############### Settings for Retirement #####################
# .. setting_name: RETIRED_USERNAME_PREFIX
# .. setting_default: retired__user_
@@ -5132,3 +5138,5 @@ ENTERPRISE_BACKEND_SERVICE_EDX_OAUTH2_PROVIDER_URL = "http://127.0.0.1:8000/oaut
# keys for big blue button live provider
COURSE_LIVE_GLOBAL_CREDENTIALS = {}
PERSONALIZED_RECOMMENDATION_COOKIE_NAME = 'edx-user-personalized-recommendation'

View File

@@ -1,96 +1,94 @@
/* global gettext */
import React from 'react';
import Cookies from 'js-cookie';
class RecommendationsPanel extends React.Component {
constructor(props) {
super(props);
this.domainInfo = { domain: props.sharedCookieDomain, expires: 365, path: '/' };
this.cookieName = props.cookieName;
this.onCourseSelect = this.onCourseSelect.bind(this);
this.getCourseList = this.getCourseList.bind(this);
this.state = {
isPersonalizedRecommendation: false,
coursesList: [],
};
}
onCourseSelect(courseKey) {
window.analytics.track('edx.bi.user.recommended.course.click', {
course_key: courseKey,
is_personalized_recommendation: false, // TODO: Use state here with default false and update its value from API response.
is_personalized_recommendation: this.state.isPersonalizedRecommendation,
});
let recommendedCourses = Cookies.get(this.cookieName);
if (typeof recommendedCourses === 'undefined') {
recommendedCourses = { course_keys: [courseKey] };
} else {
recommendedCourses = JSON.parse(recommendedCourses);
if (!recommendedCourses.course_keys.includes(courseKey)) {
recommendedCourses.course_keys.push(courseKey);
}
}
recommendedCourses['is_personalized_recommendation'] = this.state.isPersonalizedRecommendation;
Cookies.set(this.cookieName, JSON.stringify(recommendedCourses), this.domainInfo);
};
getCourseList = async () => {
const coursesRecommendationData = await fetch(`${this.props.lmsRootUrl}/api/dashboard/v0/recommendation/courses/`)
.then(response => {
if (response.status === 400) {
return this.props.generalRecommendations;
} else {
return response.json();
}
}
);
this.setState({
coursesList: coursesRecommendationData['courses'],
isPersonalizedRecommendation: coursesRecommendationData['is_personalized_recommendation']
});
};
componentDidMount() {
this.getCourseList();
};
render() {
return (
<div className="p-4 panel-background">
<div className="recommend-heading mb-4">{gettext('Recommendations for you')}</div>
<div className="course-card box-shadow-down-1 bg-white mb-3">
<div className="box-shadow-down-1 image-box">
<img
className="panel-course-img"
src="https://source.unsplash.com/lQGJCMY5qcM"
alt="course image"
/>
</div>
<div className="course-title pl-3">
<a href="#" className="course-link" onClick={() => this.onCourseSelect('add-course-key-1')}>
The Chemistry of Life
<div className={this.state.coursesList.length ? '' : 'spinner-container'}>
{this.state.coursesList.length ? this.state.coursesList.map(course => (
<a href={course.marketing_url} className="course-link"
onClick={() => this.onCourseSelect(course.course_key)}>
<div className="course-card box-shadow-down-1 bg-white mb-3">
<div className="box-shadow-down-1 image-box">
<img
className="panel-course-img"
src={course.logo_image_url}
alt="course image"
/>
</div>
<div className="course-title pl-3">
{course.title}
</div>
</div>
</a>
</div>
</div>
<div className="course-card box-shadow-down-1 bg-white mb-3">
<div className="box-shadow-down-1 image-box">
<img
className="panel-course-img"
src="https://source.unsplash.com/KltoLK6Mk-g"
alt="course image"
/>
</div>
<div className="course-title pl-3">
<a href="#" className="course-link" onClick={() => this.onCourseSelect('add-course-key-2')}>
Drug Discovery & Medicinal Chemistry
</a>
</div>
</div>
<div className="course-card box-shadow-down-1 bg-white mb-3">
<div className="box-shadow-down-1 image-box">
<img
className="panel-course-img"
src="https://source.unsplash.com/_BJVJ4WcV1M"
alt="course image"
/>
</div>
<div className="course-title pl-3">
<a href="#" className="course-link" onClick={() => this.onCourseSelect('add-course-key-3')}>
From Fossil Resources to Biomass: A Chemistry Perspective
</a>
</div>
</div>
<div className="course-card box-shadow-down-1 bg-white mb-3">
<div className="box-shadow-down-1 image-box">
<img
className="panel-course-img"
src="https://source.unsplash.com/NKhckz9B78c"
alt="course image"
/>
</div>
<div className="course-title pl-3">
<a href="#" className="course-link" onClick={() => this.onCourseSelect('add-course-key-4')}>
Digital Biomaterials
</a>
</div>
</div>
<div className="course-card box-shadow-down-1 bg-white mb-3">
<div className="box-shadow-down-1 image-box">
<img
className="panel-course-img"
src="https://source.unsplash.com/x649mR6yBIs"
alt="course image"
/>
</div>
<div className="course-title pl-3">
<a href="#" className="course-link" onClick={() => this.onCourseSelect('add-course-key-5')}>
Basic Steps in Magnetic Resonance
</a>
</div>
)) : (
<div className="d-flex justify-content-center align-items-center">
<div role="status" className="spinner">
<span className="sr-only">{gettext('loading')}</span>
</div>
</div>
)}
</div>
{this.props.exploreCoursesUrl ? (
<div className="d-flex justify-content-center">
<a href={this.props.exploreCoursesUrl} className="panel-explore-courses justify-content-center align-items-center">
<a href={this.props.exploreCoursesUrl}
className="panel-explore-courses justify-content-center align-items-center">
{gettext('Explore courses')}
<span className="icon fa fa-search search-icon" aria-hidden="true"/>
</a>

View File

@@ -739,3 +739,33 @@ def get_programs_for_organization(organization):
Retrieve list of program uuids authored by a given organization
"""
return cache.get(PROGRAMS_BY_ORGANIZATION_CACHE_KEY_TPL.format(org_key=organization))
def get_course_data(course_key_str):
"""
Retrieve information about the course with the given course key.
Arguments:
course_key_str: key for the course about which we are retrieving information.
Returns:
dict with details about specified course.
"""
user, catalog_integration = check_catalog_integration_and_get_user(error_message_field='Course UUID')
if user:
api_client = get_catalog_api_client(user)
base_api_url = get_catalog_api_base_url()
if course_key_str:
course_cache_key = f'{catalog_integration.CACHE_KEY}.course.{course_key_str}'
data = get_api_data(
catalog_integration,
'courses',
resource_id=course_key_str,
api_client=api_client,
base_api_url=base_api_url,
cache_key=course_cache_key if catalog_integration.is_cache_enabled else None,
long_term_cache=True,
many=False,
)
if data:
return data