* feat: get suggested courses * style: run black * docs: update suggested courses mock * docs: remove cardID from mock * docs: fix mock course.title to course.courseName * docs: fix mock course.bannerUrl to bannerImgSrc * docs: fix mock provider to courseProvider * docs: remove old mock courseProvider fields * docs: fix mock "grades" to "gradeData" * docs: fix mock remove courseRun.lastEnrolled * docs: fix mock add enrollment.lastEnrolled * docs: mock remove enrollment.isStarted * docs: mock fix bad nesting in courseRun * docs: mock certificates to certificate * docs: mock remove certificate.isAvailable * docs: mock remove entitlement.isEntitlement * docs: mock add entitlement.expirationDate * docs: mock fix some entitlement inconsistencies * docs: mock remove entitlement.canViewCourse * docs: mock nest relatedPrograms under programs * fix: return null for missing resumeUrl * test: return null for missing resumeUrl * refactor: update suggested course fields * refactor: update related programs fields * docs: remove entitlement.expirationDate from mock Co-authored-by: nsprenkle <nsprenkle@2u.com>
350 lines
12 KiB
Python
350 lines
12 KiB
Python
"""
|
|
Views for the learner dashboard.
|
|
"""
|
|
from django.conf import settings
|
|
from edx_django_utils import monitoring as monitoring_utils
|
|
from opaque_keys.edx.keys import CourseKey
|
|
from rest_framework.response import Response
|
|
from rest_framework.generics import RetrieveAPIView
|
|
|
|
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.views.dashboard import (
|
|
complete_course_mode_info,
|
|
get_course_enrollments,
|
|
get_org_black_and_whitelist_for_site,
|
|
get_filtered_course_entitlements,
|
|
)
|
|
from common.djangoapps.util.milestones_helpers import (
|
|
get_pre_requisite_courses_not_completed,
|
|
)
|
|
from lms.djangoapps.bulk_email.models import Optout
|
|
from lms.djangoapps.bulk_email.models_api import is_bulk_email_feature_enabled
|
|
from lms.djangoapps.commerce.utils import EcommerceService
|
|
from lms.djangoapps.courseware.access import administrative_accesses_to_course_for_user
|
|
from lms.djangoapps.courseware.access_utils import (
|
|
check_course_open_for_learner,
|
|
)
|
|
from lms.djangoapps.learner_home.serializers import LearnerDashboardSerializer
|
|
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.features.enterprise_support.api import (
|
|
enterprise_customer_from_session_or_learner_data,
|
|
)
|
|
|
|
|
|
def get_platform_settings():
|
|
"""Get settings used for platform level connections: emails, url routes, etc."""
|
|
|
|
return {
|
|
"supportEmail": settings.DEFAULT_FEEDBACK_EMAIL,
|
|
"billingEmail": settings.PAYMENT_SUPPORT_EMAIL,
|
|
"courseSearchUrl": marketing_link("COURSES"),
|
|
}
|
|
|
|
|
|
def get_user_account_confirmation_info(user):
|
|
"""Determine if a user needs to verify their account and related URL info"""
|
|
|
|
activation_email_support_link = (
|
|
configuration_helpers.get_value(
|
|
"ACTIVATION_EMAIL_SUPPORT_LINK", settings.ACTIVATION_EMAIL_SUPPORT_LINK
|
|
)
|
|
or settings.SUPPORT_SITE_LINK
|
|
)
|
|
|
|
email_confirmation = {
|
|
"isNeeded": not user.is_active,
|
|
"sendEmailUrl": activation_email_support_link,
|
|
}
|
|
|
|
return email_confirmation
|
|
|
|
|
|
def get_enrollments(user, org_allow_list, org_block_list, course_limit=None):
|
|
"""Get enrollments and enrollment course modes for user"""
|
|
|
|
course_enrollments = list(
|
|
get_course_enrollments(user, org_allow_list, org_block_list, course_limit)
|
|
)
|
|
|
|
# Sort the enrollments by enrollment date
|
|
course_enrollments.sort(key=lambda x: x.created, reverse=True)
|
|
|
|
# Record how many courses there are so that we can get a better
|
|
# understanding of usage patterns on prod.
|
|
monitoring_utils.accumulate("num_courses", len(course_enrollments))
|
|
|
|
# Retrieve the course modes for each course
|
|
enrolled_course_ids = [enrollment.course_id for enrollment in course_enrollments]
|
|
__, unexpired_course_modes = CourseMode.all_and_unexpired_modes_for_courses(
|
|
enrolled_course_ids
|
|
)
|
|
course_modes_by_course = {
|
|
course_id: {mode.slug: mode for mode in modes}
|
|
for course_id, modes in unexpired_course_modes.items()
|
|
}
|
|
|
|
# Construct a dictionary of course mode information
|
|
# used to render the course list. We re-use the course modes dict
|
|
# we loaded earlier to avoid hitting the database.
|
|
course_mode_info = {
|
|
enrollment.course_id: complete_course_mode_info(
|
|
enrollment.course_id,
|
|
enrollment,
|
|
modes=course_modes_by_course[enrollment.course_id],
|
|
)
|
|
for enrollment in course_enrollments
|
|
}
|
|
|
|
return course_enrollments, course_mode_info
|
|
|
|
|
|
def get_entitlements(user, org_allow_list, org_block_list):
|
|
"""Get entitlements for the user"""
|
|
(
|
|
filtered_entitlements,
|
|
course_entitlement_available_sessions,
|
|
unfulfilled_entitlement_pseudo_sessions,
|
|
) = get_filtered_course_entitlements(user, org_allow_list, org_block_list)
|
|
fulfilled_entitlements_by_course_key = {}
|
|
unfulfilled_entitlements = []
|
|
|
|
for course_entitlement in filtered_entitlements:
|
|
if course_entitlement.enrollment_course_run:
|
|
course_id = str(course_entitlement.enrollment_course_run.course.id)
|
|
fulfilled_entitlements_by_course_key[course_id] = course_entitlement
|
|
else:
|
|
unfulfilled_entitlements.append(course_entitlement)
|
|
|
|
return (
|
|
fulfilled_entitlements_by_course_key,
|
|
unfulfilled_entitlements,
|
|
course_entitlement_available_sessions,
|
|
unfulfilled_entitlement_pseudo_sessions,
|
|
)
|
|
|
|
|
|
def get_course_overviews_for_pseudo_sessions(unfulfilled_entitlement_pseudo_sessions):
|
|
"""
|
|
Get course overviews for entitlement pseudo sessions. This is required for
|
|
serializing course providers for entitlements.
|
|
|
|
Returns: dict of course overviews, keyed by CourseKey
|
|
"""
|
|
course_ids = []
|
|
|
|
# Get course IDs from unfulfilled entitlement pseudo sessions
|
|
for pseudo_session in unfulfilled_entitlement_pseudo_sessions.values():
|
|
course_id = pseudo_session.get("key")
|
|
if course_id:
|
|
course_ids.append(CourseKey.from_string(course_id))
|
|
|
|
return CourseOverview.get_from_ids(course_ids)
|
|
|
|
|
|
def get_email_settings_info(user, course_enrollments):
|
|
"""
|
|
Given a user and enrollments, determine which courses allow bulk email (show_email_settings_for)
|
|
and which the learner has opted out from (optouts)
|
|
"""
|
|
course_optouts = Optout.objects.filter(user=user).values_list(
|
|
"course_id", flat=True
|
|
)
|
|
|
|
# only show email settings for course where bulk email is turned on
|
|
show_email_settings_for = frozenset(
|
|
enrollment.course_id
|
|
for enrollment in course_enrollments
|
|
if (is_bulk_email_feature_enabled(enrollment.course_id))
|
|
)
|
|
|
|
return show_email_settings_for, course_optouts
|
|
|
|
|
|
def get_ecommerce_payment_page(user):
|
|
"""Determine the ecommerce payment page URL if enabled for this user"""
|
|
ecommerce_service = EcommerceService()
|
|
return (
|
|
ecommerce_service.payment_page_url()
|
|
if ecommerce_service.is_enabled(user)
|
|
else None
|
|
)
|
|
|
|
|
|
def get_cert_statuses(user, course_enrollments):
|
|
"""Get cert status by course for user enrollments"""
|
|
return {
|
|
enrollment.course_id: cert_info(user, enrollment)
|
|
for enrollment in course_enrollments
|
|
}
|
|
|
|
|
|
def _get_courses_with_unmet_prerequisites(user, course_enrollments):
|
|
"""
|
|
Determine which courses have unmet prerequisites.
|
|
NOTE: that courses w/out prerequisites, or with met prerequisites are not returned
|
|
in the output dict. That way we can do a simple "course_id in dict" check.
|
|
|
|
Returns: {
|
|
<course_id>: { "courses": [listing of unmet prerequisites] }
|
|
}
|
|
"""
|
|
|
|
courses_having_prerequisites = frozenset(
|
|
enrollment.course_id
|
|
for enrollment in course_enrollments
|
|
if enrollment.course_overview.pre_requisite_courses
|
|
)
|
|
|
|
return get_pre_requisite_courses_not_completed(user, courses_having_prerequisites)
|
|
|
|
|
|
def check_course_access(user, course_enrollments):
|
|
"""
|
|
Wrapper for checks surrounding user ability to view courseware
|
|
|
|
Returns: {
|
|
<course_enrollment.id>: {
|
|
"has_unmet_prerequisites": True/False,
|
|
"is_too_early_to_view": True/False,
|
|
"user_has_staff_access": True/False
|
|
}
|
|
}
|
|
"""
|
|
|
|
course_access_dict = {}
|
|
|
|
courses_with_unmet_prerequisites = _get_courses_with_unmet_prerequisites(
|
|
user, course_enrollments
|
|
)
|
|
|
|
for course_enrollment in course_enrollments:
|
|
course_access_dict[course_enrollment.course_id] = {
|
|
"has_unmet_prerequisites": course_enrollment.course_id
|
|
in courses_with_unmet_prerequisites,
|
|
"is_too_early_to_view": not check_course_open_for_learner(
|
|
user, course_enrollment.course
|
|
),
|
|
"user_has_staff_access": any(
|
|
administrative_accesses_to_course_for_user(
|
|
user, course_enrollment.course_id
|
|
)
|
|
),
|
|
}
|
|
|
|
return course_access_dict
|
|
|
|
|
|
def get_course_programs(user, course_enrollments, site):
|
|
"""
|
|
Get programs related to the courses the user is enrolled in.
|
|
|
|
Returns: {
|
|
<course_id>: {
|
|
"programs": [list of programs]
|
|
}
|
|
}
|
|
"""
|
|
meter = ProgramProgressMeter(site, user, enrollments=course_enrollments, include_course_entitlements=True)
|
|
return meter.invert_programs()
|
|
|
|
|
|
def get_suggested_courses():
|
|
"""
|
|
Currently just returns general recommendations from settings
|
|
"""
|
|
empty_course_suggestions = {"courses": [], "is_personalized_recommendation": False}
|
|
return (
|
|
configuration_helpers.get_value(
|
|
"GENERAL_RECOMMENDATION", settings.GENERAL_RECOMMENDATION
|
|
)
|
|
or empty_course_suggestions
|
|
)
|
|
|
|
|
|
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
|
|
# Get user, determine if user needs to confirm email account
|
|
user = request.user
|
|
site = request.site
|
|
email_confirmation = get_user_account_confirmation_info(user)
|
|
|
|
# Gather info for enterprise dashboard
|
|
enterprise_customer = enterprise_customer_from_session_or_learner_data(request)
|
|
|
|
# Get the org whitelist or the org blacklist for the current site
|
|
site_org_whitelist, site_org_blacklist = get_org_black_and_whitelist_for_site()
|
|
|
|
# Get entitlements and course overviews for serializing
|
|
(
|
|
fulfilled_entitlements_by_course_key,
|
|
unfulfilled_entitlements,
|
|
course_entitlement_available_sessions,
|
|
unfulfilled_entitlement_pseudo_sessions,
|
|
) = get_entitlements(user, site_org_whitelist, site_org_blacklist)
|
|
pseudo_session_course_overviews = get_course_overviews_for_pseudo_sessions(
|
|
unfulfilled_entitlement_pseudo_sessions
|
|
)
|
|
|
|
# Get enrollments
|
|
course_enrollments, course_mode_info = get_enrollments(
|
|
user, site_org_whitelist, site_org_blacklist
|
|
)
|
|
|
|
# Get email opt-outs for student
|
|
show_email_settings_for, course_optouts = get_email_settings_info(
|
|
user, course_enrollments
|
|
)
|
|
|
|
# Get cert status by course
|
|
cert_statuses = get_cert_statuses(user, course_enrollments)
|
|
|
|
# Determine view access for course, (for showing courseware link) involves:
|
|
course_access_checks = check_course_access(user, course_enrollments)
|
|
|
|
# Get programs related to the courses the user is enrolled in
|
|
programs = get_course_programs(user, course_enrollments, site)
|
|
|
|
# e-commerce info
|
|
ecommerce_payment_page = get_ecommerce_payment_page(user)
|
|
|
|
# Gather urls for course card resume buttons.
|
|
resume_button_urls = get_resume_urls_for_enrollments(user, course_enrollments)
|
|
|
|
# Get suggested courses
|
|
suggested_courses = get_suggested_courses().get("courses", [])
|
|
|
|
learner_dash_data = {
|
|
"emailConfirmation": email_confirmation,
|
|
"enterpriseDashboard": enterprise_customer,
|
|
"platformSettings": get_platform_settings(),
|
|
"enrollments": course_enrollments,
|
|
"unfulfilledEntitlements": unfulfilled_entitlements,
|
|
"suggestedCourses": suggested_courses,
|
|
}
|
|
|
|
context = {
|
|
"ecommerce_payment_page": ecommerce_payment_page,
|
|
"cert_statuses": cert_statuses,
|
|
"course_mode_info": course_mode_info,
|
|
"course_optouts": course_optouts,
|
|
"course_access_checks": course_access_checks,
|
|
"resume_course_urls": resume_button_urls,
|
|
"show_email_settings_for": show_email_settings_for,
|
|
"fulfilled_entitlements": fulfilled_entitlements_by_course_key,
|
|
"course_entitlement_available_sessions": course_entitlement_available_sessions,
|
|
"unfulfilled_entitlement_pseudo_sessions": unfulfilled_entitlement_pseudo_sessions,
|
|
"pseudo_session_course_overviews": pseudo_session_course_overviews,
|
|
"programs": programs,
|
|
}
|
|
|
|
response_data = LearnerDashboardSerializer(
|
|
learner_dash_data, context=context
|
|
).data
|
|
return Response(response_data)
|