refactor: learner home cleanup (#31240)

* refactor: remove dev logging

* style: run black

* refactor: move masquerade check into utils

* style: remove unnecessary assignment

* style: update dependency orderings

* refactor: add function tracing for perf testing

* refactor: move grade data fetching out of serializer

This allows us to separately profile the collection of grade data

* fix: add missing requires_context metadata

* refactor: split out serialization for profiling
This commit is contained in:
Nathan Sprenkle
2022-11-02 14:55:03 -04:00
committed by GitHub
parent 1def767c9f
commit 178f1541d7
7 changed files with 266 additions and 176 deletions

View File

@@ -519,8 +519,7 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem
if not UserProfile.objects.filter(user=user).exists():
return redirect(reverse('account_settings'))
enable_learner_home_mfe = should_redirect_to_learner_home_mfe()
if enable_learner_home_mfe:
if should_redirect_to_learner_home_mfe():
return redirect(settings.LEARNER_HOME_MICROFRONTEND_URL)
platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME)

View File

@@ -10,7 +10,6 @@ from opaque_keys.edx.keys import CourseKey
from rest_framework import serializers
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.helpers import user_has_passing_grade_in_course
from openedx.features.course_experience import course_home_url
from xmodule.data import CertificatesDisplayBehaviors
@@ -63,6 +62,8 @@ class CourseProviderSerializer(serializers.Serializer):
class CourseSerializer(serializers.Serializer):
"""Course header information, derived from a CourseOverview"""
requires_context = True
bannerImgSrc = serializers.URLField(source="image_urls.small")
courseName = serializers.CharField(source="display_name_with_default")
courseNumber = serializers.CharField(source="display_number_with_default")
@@ -139,6 +140,8 @@ class CoursewareAccessSerializer(serializers.Serializer):
Mirrors logic in "show_courseware_links_for" from old dashboard.py
"""
requires_context = True
hasUnmetPrerequisites = serializers.SerializerMethodField()
isTooEarly = serializers.SerializerMethodField()
isStaff = serializers.SerializerMethodField()
@@ -182,6 +185,8 @@ class EnrollmentSerializer(serializers.Serializer):
show email settings.
"""
requires_context = True
accessExpirationDate = serializers.SerializerMethodField()
isAudit = serializers.SerializerMethodField()
hasStarted = serializers.SerializerMethodField()
@@ -238,15 +243,19 @@ class EnrollmentSerializer(serializers.Serializer):
class GradeDataSerializer(serializers.Serializer):
"""Info about grades for this enrollment"""
requires_context = True
isPassing = serializers.SerializerMethodField()
def get_isPassing(self, enrollment):
return user_has_passing_grade_in_course(enrollment)
return self.context.get("grade_statuses", {}).get(enrollment.course_id, False)
class CertificateSerializer(serializers.Serializer):
"""Certificate availability info"""
requires_context = True
availableDate = serializers.SerializerMethodField()
isRestricted = serializers.SerializerMethodField()
isEarned = serializers.SerializerMethodField()

View File

@@ -6,10 +6,10 @@ from random import randint
from unittest import mock
from uuid import uuid4
import ddt
from django.conf import settings
from django.urls import reverse
from django.test import TestCase
import ddt
from opaque_keys.edx.keys import CourseKey
@@ -164,11 +164,7 @@ class TestCourseSerializer(LearnerDashboardBaseTest):
"""Tests for the CourseSerializer"""
def create_test_context(self, course_id):
return {
"course_share_urls": {
course_id: random_url()
}
}
return {"course_share_urls": {course_id: random_url()}}
def test_happy_path(self):
test_enrollment = self.create_test_enrollment()
@@ -182,7 +178,7 @@ class TestCourseSerializer(LearnerDashboardBaseTest):
"bannerImgSrc": test_enrollment.course_overview.banner_image_url,
"courseName": test_enrollment.course_overview.display_name_with_default,
"courseNumber": test_enrollment.course_overview.display_number_with_default,
"socialShareUrl": test_context['course_share_urls'][course_id]
"socialShareUrl": test_context["course_share_urls"][course_id],
}
@@ -430,17 +426,18 @@ class TestEnrollmentSerializer(LearnerDashboardBaseTest):
class TestGradeDataSerializer(LearnerDashboardBaseTest):
"""Tests for the GradeDataSerializer"""
@mock.patch(
"lms.djangoapps.learner_home.serializers.user_has_passing_grade_in_course"
)
def create_test_context(self, course, is_passing):
"""Get a test context object"""
return {"grade_statuses": {course.id: is_passing}}
@ddt.data(True, False, None)
def test_happy_path(self, is_passing, mock_get_grade_data):
def test_happy_path(self, is_passing):
# Given a course where I am/not passing
input_data = self.create_test_enrollment()
mock_get_grade_data.return_value = is_passing
input_context = self.create_test_context(input_data.course, is_passing)
# When I serialize grade data
output_data = GradeDataSerializer(input_data).data
output_data = GradeDataSerializer(input_data, context=input_context).data
# Then I get the correct data shape out
self.assertDictEqual(
@@ -907,9 +904,10 @@ class TestUnfulfilledEntitlementSerializer(LearnerDashboardBaseTest):
output_data["enrollment"]
== UnfulfilledEntitlementSerializer.STATIC_ENTITLEMENT_ENROLLMENT_DATA
)
assert output_data["course"] == CourseSerializer(
pseudo_session_course_overviews.popitem()[1]
).data
assert (
output_data["course"]
== CourseSerializer(pseudo_session_course_overviews.popitem()[1]).data
)
assert output_data["courseProvider"] is not None
assert output_data["programs"] == {"relatedPrograms": []}
@@ -1078,7 +1076,7 @@ class TestEnterpriseDashboardSerializer(TestCase):
class TestSocialMediaSettingsSiteSerializer(TestCase):
""" Tests for the SocialMediaSiteSettingsSerializer """
"""Tests for the SocialMediaSiteSettingsSerializer"""
@classmethod
def generate_test_social_media_settings(cls):
@@ -1103,13 +1101,13 @@ class TestSocialMediaSettingsSiteSerializer(TestCase):
class TestSocialShareSettingsSerializer(TestCase):
""" Tests for the SocialShareSettingsSerializer """
"""Tests for the SocialShareSettingsSerializer"""
@classmethod
def generate_test_social_share_settings(cls):
return {
"twitter": TestSocialMediaSettingsSiteSerializer.generate_test_social_media_settings(),
"facebook": TestSocialMediaSettingsSiteSerializer.generate_test_social_media_settings()
"facebook": TestSocialMediaSettingsSiteSerializer.generate_test_social_media_settings(),
}
def test_structure(self):
@@ -1118,10 +1116,7 @@ class TestSocialShareSettingsSerializer(TestCase):
output_data = SocialShareSettingsSerializer(input_data).data
expected_keys = [
"twitter",
"facebook"
]
expected_keys = ["twitter", "facebook"]
self.assertEqual(output_data.keys(), set(expected_keys))

View File

@@ -24,7 +24,11 @@ from common.djangoapps.student.tests.factories import (
)
from common.djangoapps.util.course import get_encoded_course_sharing_utm_params
from lms.djangoapps.bulk_email.models import Optout
from lms.djangoapps.learner_home.test_utils import create_test_enrollment, random_string
from lms.djangoapps.learner_home.test_utils import (
create_test_enrollment,
random_string,
random_url,
)
from lms.djangoapps.learner_home.views import (
get_course_overviews_for_pseudo_sessions,
get_course_programs,
@@ -38,9 +42,11 @@ from lms.djangoapps.learner_home.views import (
get_social_share_settings,
get_course_share_urls,
)
from lms.djangoapps.learner_home.test_serializers import random_url
from lms.djangoapps.learner_home.waffle import ENABLE_LEARNER_HOME_AMPLITUDE_RECOMMENDATIONS
from lms.djangoapps.learner_home.waffle import (
ENABLE_LEARNER_HOME_AMPLITUDE_RECOMMENDATIONS,
)
from openedx.core.djangoapps.catalog.tests.factories import (
CourseFactory as CatalogCourseFactory,
CourseRunFactory as CatalogCourseRunFactory,
ProgramFactory,
)
@@ -48,9 +54,6 @@ 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,
@@ -425,7 +428,7 @@ class TestGetEnterpriseCustomer(TestCase):
class TestGetSocialShareSettings(TestCase):
""" Tests for get_social_share_settings """
"""Tests for get_social_share_settings"""
def test_get_social_share_settings(self):
share_settings = {
@@ -443,16 +446,16 @@ class TestGetSocialShareSettings(TestCase):
"facebook": {
"is_enabled": True,
"brand": share_settings["FACEBOOK_BRAND"],
"utm_params": utm_sources.get("facebook")
"utm_params": utm_sources.get("facebook"),
},
"twitter": {
"is_enabled": True,
"brand": share_settings["TWITTER_BRAND"],
"utm_params": utm_sources.get("twitter")
}
"utm_params": utm_sources.get("twitter"),
},
}
@patch('lms.djangoapps.learner_home.views.get_encoded_course_sharing_utm_params')
@patch("lms.djangoapps.learner_home.views.get_encoded_course_sharing_utm_params")
def test_social_share_settings__empty(self, mock_get_utm_params):
share_settings = {}
mock_get_utm_params.return_value = {}
@@ -461,23 +464,15 @@ class TestGetSocialShareSettings(TestCase):
social_share_settings = get_social_share_settings()
assert social_share_settings == {
"facebook": {
"is_enabled": False,
"brand": "edX.org",
"utm_params": None
},
"twitter": {
"is_enabled": False,
"brand": "edX.org",
"utm_params": None
}
"facebook": {"is_enabled": False, "brand": "edX.org", "utm_params": None},
"twitter": {"is_enabled": False, "brand": "edX.org", "utm_params": None},
}
class TestGetCourseShareUrls(TestCase):
""" Tests for get_course_share_urls """
"""Tests for get_course_share_urls"""
@patch('lms.djangoapps.learner_home.views.get_link_for_about_page')
@patch("lms.djangoapps.learner_home.views.get_link_for_about_page")
def test_get_course_share_urls(self, mock_about_page_link):
enrollments = CourseEnrollmentFactory.create_batch(3)
course_share_urls = get_course_share_urls(enrollments)
@@ -825,20 +820,30 @@ class TestDashboardMasquerade(BaseTestDashboardView):
class TestCourseRecommendationApiView(SharedModuleStoreTestCase):
"""Unit tests for the course recommendations on learner home page."""
password = 'test'
url = reverse_lazy('learner_home:courses')
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.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'
"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_LEARNER_HOME_AMPLITUDE_RECOMMENDATIONS, active=False)
@@ -851,10 +856,13 @@ class TestCourseRecommendationApiView(SharedModuleStoreTestCase):
self.assertEqual(response.data, None)
@override_waffle_flag(ENABLE_LEARNER_HOME_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):
@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.
"""
@@ -866,34 +874,54 @@ class TestCourseRecommendationApiView(SharedModuleStoreTestCase):
self.assertEqual(response.data, None)
@override_waffle_flag(ENABLE_LEARNER_HOME_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):
@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_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)
self.assertEqual(response.data.get("is_personalized_recommendation"), True)
self.assertEqual(
len(response.data.get("courses")), expected_recommendations_length
)
@override_waffle_flag(ENABLE_LEARNER_HOME_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):
@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_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']
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:
@@ -901,5 +929,5 @@ 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)
self.assertEqual(response.data.get("is_personalized_recommendation"), True)
self.assertEqual(len(response.data.get("courses")), expected_recommendations)

View File

@@ -5,8 +5,16 @@ import requests
from time import time
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import MultipleObjectsReturned
from rest_framework.exceptions import PermissionDenied, NotFound
from common.djangoapps.student.models import (
get_user_by_username_or_email,
)
log = logging.getLogger(__name__)
User = get_user_model()
def exec_time_logged(func):
@@ -34,27 +42,67 @@ def exec_time_logged(func):
return wrap_func
def get_masquerade_user(request):
"""
Determine if the user is masquerading
Returns:
- masquerade_user if allowed and masquerade user found
- None if not masquerading
Raises:
- PermissionDenied if user is not staff
- NotFound if masquerade user does not exist
"""
if request.GET.get("user"):
if not request.user.is_staff:
log.info(
f"[Learner Home] {request.user.username} attempted to masquerade but is not staff"
)
raise PermissionDenied()
masquerade_identifier = request.GET.get("user")
try:
masquerade_user = get_user_by_username_or_email(masquerade_identifier)
except User.DoesNotExist:
raise NotFound() # pylint: disable=raise-missing-from
except MultipleObjectsReturned:
msg = (
f"[Learner Home] {masquerade_identifier} could refer to multiple learners. "
" Defaulting to username."
)
log.info(msg)
masquerade_user = User.objects.get(username=masquerade_identifier)
success_msg = (
f"[Learner Home] {request.user.username} masquerades as "
f"{masquerade_user.username} - {masquerade_user.email}"
)
log.info(success_msg)
return masquerade_user
def get_personalized_course_recommendations(user_id):
""" Get personalize recommendations from Amplitude. """
"""Get personalize recommendations from Amplitude."""
headers = {
'Authorization': f'Api-Key {settings.AMPLITUDE_API_KEY}',
'Content-Type': 'application/json'
"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,
"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', [])
recommendations = response.get("userData", {}).get("recommendations", [])
if recommendations:
is_control = recommendations[0].get('is_control')
recommended_course_keys = recommendations[0].get('items')
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}')
log.warning(f"Cannot get recommendations from Amplitude: {ex}")
return True, []

View File

@@ -3,28 +3,18 @@ Views for the learner dashboard.
"""
from collections import OrderedDict
import logging
from time import time
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import MultipleObjectsReturned
from django.urls import reverse
from completion.exceptions import UnavailableCompletionData
from completion.utilities import get_key_to_last_completed_block
from common.djangoapps.util.course import (
get_encoded_course_sharing_utm_params,
get_link_for_about_page,
)
from edx_django_utils import monitoring as monitoring_utils
from edx_django_utils.monitoring import function_trace
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from edx_rest_framework_extensions.auth.session.authentication import (
SessionAuthenticationAllowInactiveUser,
)
from opaque_keys.edx.keys import CourseKey
from openedx.features.course_duration_limits.access import (
get_user_course_expiration_date,
)
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
@@ -32,18 +22,22 @@ from rest_framework.views import APIView
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.edxmako.shortcuts import marketing_link
from common.djangoapps.student.helpers import cert_info
from common.djangoapps.student.models import (
CourseEnrollment,
get_user_by_username_or_email,
from common.djangoapps.student.helpers import (
cert_info,
user_has_passing_grade_in_course,
)
from common.djangoapps.student.models import CourseEnrollment
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,
get_org_black_and_whitelist_for_site,
)
from common.djangoapps.track import segment
from common.djangoapps.util.course import (
get_encoded_course_sharing_utm_params,
get_link_for_about_page,
)
from common.djangoapps.util.milestones_helpers import (
get_pre_requisite_courses_not_completed,
)
@@ -55,25 +49,29 @@ 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.waffle import should_show_learner_home_amplitude_recommendations
from lms.djangoapps.learner_home.waffle import (
should_show_learner_home_amplitude_recommendations,
)
from lms.djangoapps.learner_home.utils import (
get_masquerade_user,
get_personalized_course_recommendations,
exec_time_logged,
)
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.course_duration_limits.access import (
get_user_course_expiration_date,
)
from openedx.features.enterprise_support.api import (
enterprise_customer_from_session_or_learner_data,
get_enterprise_learner_data_from_db,
)
logger = logging.getLogger(__name__)
User = get_user_model()
@exec_time_logged
@function_trace("get_platform_settings")
def get_platform_settings():
"""Get settings used for platform level connections: emails, url routes, etc."""
@@ -84,7 +82,7 @@ def get_platform_settings():
}
@exec_time_logged
@function_trace("get_user_account_confirmation_info")
def get_user_account_confirmation_info(user):
"""Determine if a user needs to verify their account and related URL info"""
@@ -103,7 +101,7 @@ def get_user_account_confirmation_info(user):
return email_confirmation
@exec_time_logged
@function_trace("get_enrollments")
def get_enrollments(user, org_allow_list, org_block_list, course_limit=None):
"""Get enrollments and enrollment course modes for user"""
@@ -143,7 +141,7 @@ def get_enrollments(user, org_allow_list, org_block_list, course_limit=None):
return course_enrollments, course_mode_info
@exec_time_logged
@function_trace("get_entitlements")
def get_entitlements(user, org_allow_list, org_block_list):
"""Get entitlements for the user"""
(
@@ -169,7 +167,7 @@ def get_entitlements(user, org_allow_list, org_block_list):
)
@exec_time_logged
@function_trace("get_course_overviews_for_pseudo_sessions")
def get_course_overviews_for_pseudo_sessions(unfulfilled_entitlement_pseudo_sessions):
"""
Get course overviews for entitlement pseudo sessions. This is required for
@@ -188,6 +186,7 @@ def get_course_overviews_for_pseudo_sessions(unfulfilled_entitlement_pseudo_sess
return CourseOverview.get_from_ids(course_ids)
@function_trace("get_email_settings_info")
def get_email_settings_info(user, course_enrollments):
"""
Given a user and enrollments, determine which courses allow bulk email (show_email_settings_for)
@@ -207,7 +206,7 @@ def get_email_settings_info(user, course_enrollments):
return show_email_settings_for, course_optouts
@exec_time_logged
@function_trace("get_enterprise_customer")
def get_enterprise_customer(user, request, is_masquerading):
"""
If we are not masquerading, try to load the enterprise learner from session data, falling back to the db.
@@ -220,7 +219,7 @@ def get_enterprise_customer(user, request, is_masquerading):
return enterprise_customer_from_session_or_learner_data(request)
@exec_time_logged
@function_trace("get_ecommerce_payment_page")
def get_ecommerce_payment_page(user):
"""Determine the ecommerce payment page URL if enabled for this user"""
ecommerce_service = EcommerceService()
@@ -231,7 +230,7 @@ def get_ecommerce_payment_page(user):
)
@exec_time_logged
@function_trace("get_cert_statuses")
def get_cert_statuses(user, course_enrollments):
"""Get cert status by course for user enrollments"""
return {
@@ -240,13 +239,13 @@ def get_cert_statuses(user, course_enrollments):
}
@exec_time_logged
@function_trace("get_org_block_and_allow_lists")
def get_org_block_and_allow_lists():
"""Proxy for get_org_black_and_whitelist_for_site to allow for modification / profiling"""
return get_org_black_and_whitelist_for_site()
@exec_time_logged
@function_trace("get_resume_urls_for_course_enrollments")
def get_resume_urls_for_course_enrollments(user, course_enrollments):
"""
Modeled off of get_resume_urls_for_enrollments but removes check for actual presence of block
@@ -289,7 +288,7 @@ def _get_courses_with_unmet_prerequisites(user, course_enrollments):
return get_pre_requisite_courses_not_completed(user, courses_having_prerequisites)
@exec_time_logged
@function_trace("check_course_access")
def check_course_access(user, course_enrollments):
"""
Wrapper for checks surrounding user ability to view courseware
@@ -326,7 +325,7 @@ def check_course_access(user, course_enrollments):
return course_access_dict
@exec_time_logged
@function_trace("get_course_programs")
def get_course_programs(user, course_enrollments, site):
"""
Get programs related to the courses the user is enrolled in.
@@ -343,7 +342,7 @@ def get_course_programs(user, course_enrollments, site):
return meter.invert_programs()
@exec_time_logged
@function_trace("get_suggested_courses")
def get_suggested_courses():
"""
Currently just returns general recommendations from settings
@@ -357,7 +356,7 @@ def get_suggested_courses():
)
@exec_time_logged
@function_trace("get_social_share_settings")
def get_social_share_settings():
"""Config around social media sharing campaigns"""
@@ -383,7 +382,7 @@ def get_social_share_settings():
}
@exec_time_logged
@function_trace("get_course_share_urls")
def get_course_share_urls(course_enrollments):
"""Get course URLs for sharing on social media"""
return {
@@ -392,7 +391,7 @@ def get_course_share_urls(course_enrollments):
}
@exec_time_logged
@function_trace("get_audit_access_deadlines")
def get_audit_access_deadlines(user, course_enrollments):
"""
Get audit access deadlines for each course enrollment
@@ -408,40 +407,39 @@ def get_audit_access_deadlines(user, course_enrollments):
}
@function_trace("get_user_grade_passing_statuses")
def get_user_grade_passing_statuses(course_enrollments):
"""
Get "passing" status for user in each course
Returns:
- Dict {course_id: <boolean (True = Passing grade, False = Failing grade)>}
"""
return {
course_enrollment.course_id: user_has_passing_grade_in_course(course_enrollment)
for course_enrollment in course_enrollments
}
@function_trace("serialize_learner_home_data")
def serialize_learner_home_data(data, context):
"""Wrapper for serialization so we can profile"""
return LearnerDashboardSerializer(data, context=context).data
class InitializeView(RetrieveAPIView): # pylint: disable=unused-argument
"""List of courses a user is enrolled in or entitled to"""
def get(self, request, *args, **kwargs): # pylint: disable=unused-argument
if request.GET.get("user"):
if not request.user.is_staff:
logger.info(
f"[Learner Home] {request.user.username} attempted to masquerade but is not staff"
)
raise PermissionDenied()
"""Get masquerade user and proxy to init request"""
masquerade_user = get_masquerade_user(request)
masquerade_identifier = request.GET.get("user")
try:
masquerade_user = get_user_by_username_or_email(masquerade_identifier)
except User.DoesNotExist:
raise NotFound() # pylint: disable=raise-missing-from
except MultipleObjectsReturned:
msg = (
f"[Learner Home] {masquerade_identifier} could refer to multiple learners. "
" Defaulting to username."
)
logger.info(msg)
masquerade_user = User.objects.get(username=masquerade_identifier)
success_msg = (
f"[Learner Home] {request.user.username} masquerades as "
f"{masquerade_user.username} - {masquerade_user.email}"
)
logger.info(success_msg)
return self._initialize(masquerade_user, True)
if masquerade_user:
return self._initialize(masquerade_user, is_masquerade=True)
else:
return self._initialize(request.user, False)
return self._initialize(request.user)
def _initialize(self, user, is_masquerade):
def _initialize(self, user, is_masquerade=False):
"""
Load information required for displaying the learner home
"""
@@ -484,6 +482,9 @@ class InitializeView(RetrieveAPIView): # pylint: disable=unused-argument
user, course_enrollments
)
# Get grade passing status by course
grade_statuses = get_user_grade_passing_statuses(course_enrollments)
# Get cert status by course
cert_statuses = get_cert_statuses(user, course_enrollments)
@@ -524,6 +525,7 @@ class InitializeView(RetrieveAPIView): # pylint: disable=unused-argument
"course_mode_info": course_mode_info,
"course_optouts": course_optouts,
"course_access_checks": course_access_checks,
"grade_statuses": grade_statuses,
"resume_course_urls": resume_button_urls,
"course_share_urls": course_share_urls,
"show_email_settings_for": show_email_settings_for,
@@ -534,16 +536,7 @@ class InitializeView(RetrieveAPIView): # pylint: disable=unused-argument
"programs": programs,
}
t1 = time()
response_data = LearnerDashboardSerializer(
learner_dash_data, context=context
).data
t2 = time()
logger.info(
f"Finished serializing home info for {user.username} in {(t2-t1):.4f}s"
)
response_data = serialize_learner_home_data(learner_dash_data, context)
return Response(response_data)
@@ -555,11 +548,14 @@ class CourseRecommendationApiView(APIView):
GET /api/learner_home/recommendation/courses/
"""
authentication_classes = (JwtAuthentication, SessionAuthenticationAllowInactiveUser,)
authentication_classes = (
JwtAuthentication,
SessionAuthenticationAllowInactiveUser,
)
permission_classes = (IsAuthenticated,)
def get(self, request):
""" Retrieves course recommendations details of a user in a specified course. """
"""Retrieves course recommendations details of a user in a specified course."""
if not should_show_learner_home_amplitude_recommendations():
return Response(status=400)
@@ -569,10 +565,10 @@ class CourseRecommendationApiView(APIView):
# Emits an event to track student dashboard page visits.
segment.track(
user_id,
'edx.bi.user.recommendations.viewed',
"edx.bi.user.recommendations.viewed",
{
'is_personalized_recommendation': not is_control,
}
"is_personalized_recommendation": not is_control,
},
)
if is_control or not course_keys:
@@ -580,24 +576,38 @@ class CourseRecommendationApiView(APIView):
recommended_courses = []
user_enrolled_course_keys = set()
fields = ['title', 'owners', 'marketing_url']
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}'
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]
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')
})
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)
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,
)

View File

@@ -4,6 +4,7 @@ waffle switches for the teams app.
"""
from edx_toggles.toggles import WaffleFlag
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
# Namespace for Learner Home MFE waffle flags.