feat: add masters program restrictions on recommendations (#31857)

Co-authored-by: Syed Sajjad  Hussain Shah <syed.sajjad@H7FKF7K6XD.local>
This commit is contained in:
Syed Sajjad Hussain Shah
2023-03-03 15:31:52 +05:00
committed by GitHub
parent 1acd382131
commit 1ef66409c9
5 changed files with 108 additions and 38 deletions

View File

@@ -439,3 +439,20 @@ class TestCourseRecommendationApiView(TestCase):
self.assertEqual(
segment_mock.call_args[0][2]["is_control"], expected_is_control
)
@mock.patch(
"lms.djangoapps.learner_dashboard.api.v0.views.is_user_enrolled_in_masters_program"
)
def test_no_recommendations_for_masters_program_learners(
self, is_user_enrolled_in_masters_program_mock
):
"""
Verify API returns no recommendations if a user is enrolled in masters program.
"""
is_user_enrolled_in_masters_program_mock.return_value = True
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data.get("is_control"), None)
self.assertEqual(len(response.data.get("courses")), 0)

View File

@@ -27,6 +27,7 @@ from openedx.core.djangoapps.programs.utils import (
from lms.djangoapps.learner_recommendations.utils import (
filter_recommended_courses,
get_amplitude_course_recommendations,
is_user_enrolled_in_masters_program,
)
@@ -391,10 +392,10 @@ class CourseRecommendationApiView(APIView):
},
)
def _general_recommendations_response(self, user_id, is_control, recommendations):
def _recommendations_response(self, user_id, is_control, recommendations, amplitude_recommendations):
"""Helper method for general recommendations response"""
self._emit_recommendations_viewed_event(
user_id, is_control, recommendations, amplitude_recommendations=False
user_id, is_control, recommendations, amplitude_recommendations
)
return Response(
{
@@ -417,9 +418,11 @@ class CourseRecommendationApiView(APIView):
def get(self, request):
"""Retrieves course recommendations details of a user in a specified course."""
user_id = request.user.id
fallback_recommendations = (
settings.GENERAL_RECOMMENDATIONS if show_fallback_recommendations() else []
)
if is_user_enrolled_in_masters_program(request.user):
return self._recommendations_response(user_id, None, [], False)
fallback_recommendations = settings.GENERAL_RECOMMENDATIONS if show_fallback_recommendations() else []
try:
(
@@ -429,15 +432,15 @@ class CourseRecommendationApiView(APIView):
) = get_amplitude_course_recommendations(user_id, settings.DASHBOARD_AMPLITUDE_RECOMMENDATION_ID)
except Exception as ex: # pylint: disable=broad-except
logger.warning(f"Cannot get recommendations from Amplitude: {ex}")
return self._general_recommendations_response(
user_id, None, fallback_recommendations
return self._recommendations_response(
user_id, None, fallback_recommendations, False
)
is_control = is_control if has_is_control else None
if is_control or is_control is None or not course_keys:
return self._general_recommendations_response(
user_id, is_control, fallback_recommendations
return self._recommendations_response(
user_id, is_control, fallback_recommendations, False
)
ip_address = get_client_ip(request)[0]
@@ -446,18 +449,11 @@ class CourseRecommendationApiView(APIView):
request.user, course_keys, user_country_code=user_country_code, recommendation_count=5
)
if not filtered_courses:
return self._general_recommendations_response(
user_id, is_control, fallback_recommendations
return self._recommendations_response(
user_id, is_control, fallback_recommendations, False
)
recommended_courses = list(map(self._course_data, filtered_courses))
self._emit_recommendations_viewed_event(
user_id, is_control, recommended_courses
)
return Response(
{
"courses": recommended_courses,
"is_control": is_control,
},
status=200,
return self._recommendations_response(
user_id, is_control, recommended_courses, True
)

View File

@@ -305,3 +305,22 @@ class TestCourseRecommendationApiView(TestCase):
assert segment_track_mock.call_count == 1
assert segment_track_mock.call_args[0][1] == "edx.bi.user.recommendations.viewed"
self.assertEqual(segment_track_mock.call_args[0][2]["is_control"], expected_is_control)
@override_waffle_flag(ENABLE_LEARNER_HOME_AMPLITUDE_RECOMMENDATIONS, active=True)
@mock.patch(
"lms.djangoapps.learner_home.recommendations.views.is_user_enrolled_in_masters_program"
)
def test_no_recommendations_for_masters_program_learners(
self, is_user_enrolled_in_masters_program_mock
):
"""
Verify API returns no recommendations if a user is enrolled in masters program.
"""
is_user_enrolled_in_masters_program_mock.return_value = True
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
response_content = json.loads(response.content)
self.assertEqual(response_content.get("isControl"), None)
self.assertEqual(response_content.get("courses"), [])

View File

@@ -26,6 +26,7 @@ from lms.djangoapps.learner_home.recommendations.waffle import (
from lms.djangoapps.learner_recommendations.utils import (
filter_recommended_courses,
get_amplitude_course_recommendations,
is_user_enrolled_in_masters_program,
)
@@ -55,6 +56,10 @@ class CourseRecommendationApiView(APIView):
return Response(status=404)
user_id = request.user.id
if is_user_enrolled_in_masters_program(request.user):
return self._recommendations_response(user_id, None, [], False)
fallback_recommendations = settings.GENERAL_RECOMMENDATIONS if show_fallback_recommendations() else []
try:
@@ -63,11 +68,11 @@ class CourseRecommendationApiView(APIView):
)
except Exception as ex: # pylint: disable=broad-except
logger.warning(f"Cannot get recommendations from Amplitude: {ex}")
return self._general_recommendations_response(user_id, None, fallback_recommendations)
return self._recommendations_response(user_id, None, fallback_recommendations, False)
is_control = is_control if has_is_control else None
if is_control or is_control is None or not course_keys:
return self._general_recommendations_response(user_id, is_control, fallback_recommendations)
return self._recommendations_response(user_id, is_control, fallback_recommendations, False)
ip_address = get_client_ip(request)[0]
user_country_code = country_code_from_ip(ip_address).upper()
@@ -78,19 +83,10 @@ class CourseRecommendationApiView(APIView):
# the list of amplitude recommendations, show general recommendations
# to the user.
if not filtered_courses:
return self._general_recommendations_response(user_id, is_control, fallback_recommendations)
return self._recommendations_response(user_id, is_control, fallback_recommendations, False)
recommended_courses = list(map(self._course_data, filtered_courses))
self._emit_recommendations_viewed_event(user_id, is_control, recommended_courses)
return Response(
CourseRecommendationSerializer(
{
"courses": recommended_courses,
"is_control": is_control,
}
).data,
status=200,
)
return self._recommendations_response(user_id, is_control, recommended_courses, True)
def _emit_recommendations_viewed_event(
self, user_id, is_control, recommended_courses, amplitude_recommendations=True
@@ -107,10 +103,10 @@ class CourseRecommendationApiView(APIView):
},
)
def _general_recommendations_response(self, user_id, is_control, recommended_courses):
def _recommendations_response(self, user_id, is_control, recommended_courses, amplitude_recommendations):
""" Helper method for general recommendations response. """
self._emit_recommendations_viewed_event(
user_id, is_control, recommended_courses, amplitude_recommendations=False
user_id, is_control, recommended_courses, amplitude_recommendations
)
return Response(
CourseRecommendationSerializer(

View File

@@ -9,6 +9,7 @@ from algoliasearch.search_client import SearchClient
from django.conf import settings
from common.djangoapps.student.models import CourseEnrollment
from lms.djangoapps.program_enrollments.constants import ProgramEnrollmentStatuses
from openedx.core.djangoapps.catalog.utils import get_course_data, get_programs
from lms.djangoapps.program_enrollments.api import fetch_program_enrollments_by_student
@@ -191,6 +192,28 @@ def get_amplitude_course_recommendations(user_id, recommendation_id):
return True, False, []
def is_user_enrolled_in_masters_program(user):
"""
Checks if a user is enrolled in any masters program
Args:
user: The user object
Returns:
True if the user is enrolled in any masters program otherwise False
"""
program_enrollments = fetch_program_enrollments_by_student(
user=user,
program_enrollment_statuses=ProgramEnrollmentStatuses.__ACTIVE__,
)
uuids = [enrollment.program_uuid for enrollment in program_enrollments]
enrolled_programs = get_programs(uuids=uuids) or []
for enrolled_program in enrolled_programs:
if enrolled_program.get("type", None) == "Masters":
return True
return False
def filter_recommended_courses(
user,
unfiltered_course_keys,
@@ -205,12 +228,28 @@ def filter_recommended_courses(
2. If user is seeing the recommendations on a course about pages, filter that course out of recommendations.
3. Remove the courses which is restricted in user region.
Args:
user: The user for which the recommendations need to be pulled
unfiltered_course_keys: recommended course keys that needs to be filtered
recommendation_count: the maximum count of recommendations to be returned
user_country_code: if provided, will apply location restrictions to recommendations
request_course: if provided, will filter out that course from recommendations (used for course about page)
Returns:
filtered_recommended_courses (list): A list of filtered course objects.
"""
filtered_recommended_courses = []
fields = [
"key", "uuid", "title", "owners", "image", "url_slug", "course_runs", "location_restriction", "marketing_url",
"key",
"uuid",
"title",
"owners",
"image",
"url_slug",
"course_runs",
"location_restriction",
"marketing_url",
"programs",
]
# Remove the course keys a user is already enrolled in
@@ -228,8 +267,11 @@ def filter_recommended_courses(
break
course_data = get_course_data(course_id, fields, querystring={'marketable_course_runs_only': 1})
if (course_data and course_data.get("course_runs", [])
and not _has_country_restrictions(course_data, user_country_code)):
if (
course_data
and course_data.get("course_runs", [])
and not _has_country_restrictions(course_data, user_country_code)
):
filtered_recommended_courses.append(course_data)
return filtered_recommended_courses