From 052683f92686f8da01be76e39df79f4efea4af0d Mon Sep 17 00:00:00 2001 From: Christie Rice <8483753+crice100@users.noreply.github.com> Date: Wed, 27 Mar 2019 14:33:57 -0400 Subject: [PATCH] REVEM-203 Add course and program info to dashboard metadata --- common/djangoapps/student/views/dashboard.py | 15 +- lms/djangoapps/experiments/utils.py | 135 +++++++++++++++++- .../experiments/dashboard_metadata.html | 2 +- themes/edx.org/lms/templates/dashboard.html | 2 +- 4 files changed, 141 insertions(+), 13 deletions(-) diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py index eed86a24ef..3d12c30ab5 100644 --- a/common/djangoapps/student/views/dashboard.py +++ b/common/djangoapps/student/views/dashboard.py @@ -22,7 +22,7 @@ from six import text_type, iteritems import track.views from bulk_email.models import BulkEmailFlag, Optout # pylint: disable=import-error -from course_modes.models import CourseMode, get_cosmetic_display_price +from course_modes.models import CourseMode from courseware.access import has_access from edxmako.shortcuts import render_to_response, render_to_string from entitlements.models import CourseEntitlement @@ -42,7 +42,7 @@ from openedx.core.djangoapps.waffle_utils import WaffleFlag, WaffleFlagNamespace from openedx.core.djangoapps.user_api.accounts.utils import is_secondary_email_feature_enabled_for_user from openedx.core.djangolib.markup import HTML, Text from openedx.features.enterprise_support.api import get_dashboard_consent_notification -from lms.djangoapps.experiments.utils import get_experiment_dashboard_metadata_context +from lms.djangoapps.experiments.utils import get_experiment_dashboard_metadata_context, get_dashboard_course_info from openedx.features.journals.api import journals_enabled from shoppingcart.api import order_history from shoppingcart.models import CourseRegistrationCode, DonationConfiguration @@ -60,12 +60,12 @@ from xmodule.modulestore.django import modulestore log = logging.getLogger("edx.student") +# TODO START: Delete waffle flag as part of REVEM-204 experiments_namespace = WaffleFlagNamespace(name=u'student.experiments') -#TODO START: Delete waffle flag as part of REVEM-204. -dashboard_metadata_flag = WaffleFlag(experiments_namespace, +DASHBOARD_METADATA_FLAG = WaffleFlag(experiments_namespace, u'dashboard_metadata', flag_undefined_default=False) -#TODO END: REVEM-204 +# TODO END: REVEM-204 def get_org_black_and_whitelist_for_site(): @@ -878,8 +878,11 @@ def student_dashboard(request): 'recovery_email_activation_message': recovery_email_activation_message, # TODO START: Clean up REVEM-205 & REVEM-204. # The below context is for experiments in dashboard_metadata - 'course_prices': get_experiment_dashboard_metadata_context(course_enrollments) if dashboard_metadata_flag.is_enabled() else None, + 'course_prices': get_experiment_dashboard_metadata_context(course_enrollments) if DASHBOARD_METADATA_FLAG.is_enabled() else None, # TODO END: Clean up REVEM-205 & REVEM-204. + # TODO START: clean up as part of REVEM-199 (START) + 'course_info': get_dashboard_course_info(user, course_enrollments), + # TODO START: clean up as part of REVEM-199 (END) } if ecommerce_service.is_enabled(request.user): diff --git a/lms/djangoapps/experiments/utils.py b/lms/djangoapps/experiments/utils.py index 68f1f1c6c0..b3ac41c412 100644 --- a/lms/djangoapps/experiments/utils.py +++ b/lms/djangoapps/experiments/utils.py @@ -24,6 +24,8 @@ logger = logging.getLogger(__name__) # TODO: clean up as part of REVEM-199 (START) +experiments_namespace = WaffleFlagNamespace(name=u'experiments') + # .. feature_toggle_name: experiments.add_programs # .. feature_toggle_type: flag # .. feature_toggle_default: False @@ -36,7 +38,7 @@ logger = logging.getLogger(__name__) # .. feature_toggle_tickets: REVEM-63, REVEM-198 # .. feature_toggle_status: supported PROGRAM_INFO_FLAG = WaffleFlag( - waffle_namespace=WaffleFlagNamespace(name=u'experiments'), + waffle_namespace=experiments_namespace, flag_name=u'add_programs', flag_undefined_default=False ) @@ -54,11 +56,26 @@ PROGRAM_INFO_FLAG = WaffleFlag( # .. feature_toggle_tickets: REVEM-118, REVEM-206 # .. feature_toggle_status: supported PROGRAM_PRICE_FLAG = WaffleFlag( - waffle_namespace=WaffleFlagNamespace(name=u'experiments'), + waffle_namespace=experiments_namespace, flag_name=u'add_program_price', flag_undefined_default=False ) -# TODO: clean up as part of REVEM-199 (END) + +# .. feature_toggle_name: experiments.add_dashboard_info +# .. feature_toggle_type: flag +# .. feature_toggle_default: False +# .. feature_toggle_description: Toggle for adding info about each course to the dashboard metadata +# .. feature_toggle_category: experiments +# .. feature_toggle_use_cases: monitored_rollout +# .. feature_toggle_creation_date: 2019-3-28 +# .. feature_toggle_expiration_date: None +# .. feature_toggle_warnings: None +# .. feature_toggle_tickets: REVEM-118 +# .. feature_toggle_status: supported +DASHBOARD_INFO_FLAG = WaffleFlag(experiments_namespace, + u'add_dashboard_info', + flag_undefined_default=False) +# TODO END: clean up as part of REVEM-199 (End) def check_and_get_upgrade_link_and_date(user, enrollment=None, course=None): @@ -206,6 +223,27 @@ def is_enrolled_in_course_run(course_run, enrollment_course_ids): u'Unable to determine if user was enrolled since the course key {} is invalid'.format(key) ) return False # Invalid course run key. Assume user is not enrolled. + + +def get_dashboard_course_info(user, dashboard_enrollments): + """ + Given a list of enrollments shown on the dashboard, return a dict of course ids and experiment info for that course + """ + course_info = None + if DASHBOARD_INFO_FLAG.is_enabled(): + # Get the enrollments here since the dashboard filters out those with completed entitlements + user_enrollments = CourseEnrollment.objects.select_related('course').filter(user_id=user.id) + audit_enrollments = user_enrollments.filter(mode='audit') + + course_info = { + str(dashboard_enrollment.course): get_base_experiment_metadata_context(dashboard_enrollment.course, + user, + dashboard_enrollment, + user_enrollments, + audit_enrollments) + for dashboard_enrollment in dashboard_enrollments + } + return course_info # TODO: clean up as part of REVEM-199 (END) @@ -213,6 +251,7 @@ def get_experiment_user_metadata_context(course, user): """ Return a context dictionary with the keys used by the user_metadata.html. """ + # TODO: call get_base_experiment_metadata_context(), and then add in the bits that are only needed by user_metadata enrollment_mode = None enrollment_time = None enrollment = None @@ -324,7 +363,93 @@ def get_experiment_user_metadata_context(course, user): } -#TODO START: Clean up REVEM-205 +def get_base_experiment_metadata_context(course, user, enrollment, user_enrollments, audit_enrollments): + """ + Return a context dictionary with the keys used by dashboard_metadata.html. + """ + enrollment_mode = None + enrollment_time = None + # TODO: clean up as part of REVEM-199 (START) + program_key = get_program_context(course, user_enrollments, audit_enrollments) + # TODO: clean up as part of REVEM-199 (END) + if enrollment.is_active: + enrollment_mode = enrollment.mode + enrollment_time = enrollment.created + + # upgrade_link and upgrade_date should be None if user has passed their dynamic pacing deadline. + upgrade_link, upgrade_date = check_and_get_upgrade_link_and_date(user, enrollment, course) + + return { + 'upgrade_link': upgrade_link, + 'upgrade_price': unicode(get_cosmetic_verified_display_price(course)), + 'enrollment_mode': enrollment_mode, + 'enrollment_time': enrollment_time, + 'pacing_type': 'self_paced' if course.self_paced else 'instructor_paced', + 'upgrade_deadline': upgrade_date, + 'course_key': course.id, + 'course_start': course.start, + 'course_end': course.end, + # TODO: clean up as part of REVEM-199 (START) + 'program_key_fields': program_key, + # TODO: clean up as part of REVEM-199 (END) + } + + +# TODO: clean up as part of REVEM-199 (START) +def get_program_context(course, user_enrollments, audit_enrollments): + """ + Return a context dictionary with program information. + """ + program_key = None + if PROGRAM_INFO_FLAG.is_enabled(): + programs = get_programs(course=course.id) + if programs: + # A course can be in multiple programs, but we're just grabbing the first one + program = programs[0] + complete_enrollment = False + has_courses_left_to_purchase = False + total_courses = None + courses = program.get('courses') + courses_left_to_purchase_price = None + courses_left_to_purchase_url = None + program_uuid = program.get('uuid') + is_eligible_for_one_click_purchase = program.get('is_program_eligible_for_one_click_purchase') + if courses is not None: + total_courses = len(courses) + complete_enrollment = is_enrolled_in_all_courses(courses, user_enrollments) + + # Get the price and purchase URL of the program courses the user has yet to purchase. Say a + # program has 3 courses (A, B and C), and the user previously purchased a certificate for A. + # The user is enrolled in audit mode for B. The "left to purchase price" should be the price of + # B+C. + non_audit_enrollments = [en for en in user_enrollments if en not in audit_enrollments] + courses_left_to_purchase = get_unenrolled_courses(courses, non_audit_enrollments) + if courses_left_to_purchase: + has_courses_left_to_purchase = True + if courses_left_to_purchase and is_eligible_for_one_click_purchase: + courses_left_to_purchase_price, courses_left_to_purchase_skus = \ + get_program_price_and_skus(courses_left_to_purchase) + if courses_left_to_purchase_skus: + courses_left_to_purchase_url = EcommerceService().get_checkout_page_url( + *courses_left_to_purchase_skus, program_uuid=program_uuid) + + program_key = { + 'uuid': program_uuid, + 'title': program.get('title'), + 'marketing_url': program.get('marketing_url'), + 'status': program.get('status'), + 'is_eligible_for_one_click_purchase': is_eligible_for_one_click_purchase, + 'total_courses': total_courses, + 'complete_enrollment': complete_enrollment, + 'has_courses_left_to_purchase': has_courses_left_to_purchase, + 'courses_left_to_purchase_price': courses_left_to_purchase_price, + 'courses_left_to_purchase_url': courses_left_to_purchase_url, + } + return program_key +# TODO: clean up as part of REVEM-199 (START) + + +# TODO START: Clean up REVEM-205 def get_experiment_dashboard_metadata_context(enrollments): """ Given a list of enrollments return a dict of course ids with their prices. @@ -333,7 +458,7 @@ def get_experiment_dashboard_metadata_context(enrollments): :return: dict of courses: course price for dashboard metadata """ return {str(enrollment.course): enrollment.course_price for enrollment in enrollments} -#TODO END: Clean up REVEM-205 +# TODO END: Clean up REVEM-205 def stable_bucketing_hash_group(group_name, group_count, username): diff --git a/lms/templates/experiments/dashboard_metadata.html b/lms/templates/experiments/dashboard_metadata.html index ecdbe48b8d..95a842eda6 100644 --- a/lms/templates/experiments/dashboard_metadata.html +++ b/lms/templates/experiments/dashboard_metadata.html @@ -5,7 +5,7 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json <% -dashboard_metadata = { 'course_prices': course_prices } +dashboard_metadata = { 'course_prices': course_prices, 'course_info': course_info} %>