feat: add masters program restrictions on recommendations (#31857)
Co-authored-by: Syed Sajjad Hussain Shah <syed.sajjad@H7FKF7K6XD.local>
This commit is contained in:
committed by
GitHub
parent
1acd382131
commit
1ef66409c9
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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"), [])
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user