From 384f22ff96cb1472cd5ada41b56f9011748edaf7 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 28 Jul 2017 14:13:26 -0400 Subject: [PATCH] Add per-user metadata to course pages to make experimentation easier --- common/djangoapps/course_modes/models.py | 54 +++++++++ .../course_modes/tests/test_models.py | 32 ++++- .../tests/test_field_override_performance.py | 54 ++++----- .../courseware/tests/test_course_info.py | 4 +- lms/djangoapps/courseware/tests/test_views.py | 30 +---- lms/djangoapps/courseware/views/index.py | 16 ++- lms/djangoapps/courseware/views/views.py | 110 +++++------------- lms/djangoapps/discussion/views.py | 13 ++- lms/djangoapps/experiments/utils.py | 48 ++++++++ lms/templates/main.html | 1 + lms/templates/user_metadata.html | 51 ++++++++ .../tests/views/test_course_home.py | 2 +- .../tests/views/test_course_updates.py | 2 +- .../course_experience/views/course_sock.py | 3 +- scripts/xss_linter.py | 2 +- 15 files changed, 266 insertions(+), 156 deletions(-) create mode 100644 lms/djangoapps/experiments/utils.py create mode 100644 lms/templates/user_metadata.html diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index 55c62c177e..a407251e23 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -12,6 +12,7 @@ from django.db import models from django.db.models import Q from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _ +from django.utils.encoding import force_text from openedx.core.djangoapps.xmodule_django.models import CourseKeyField from request_cache.middleware import RequestCache, ns_request_cached @@ -693,6 +694,59 @@ def invalidate_course_mode_cache(sender, **kwargs): # pylint: disable=unused-a RequestCache.clear_request_cache(name=CourseMode.CACHE_NAMESPACE) +def get_cosmetic_verified_display_price(course): + """ + Returns the minimum verified cert course price as a string preceded by correct currency, or 'Free'. + """ + return get_course_prices(course, verified_only=True)[1] + + +def get_cosmetic_display_price(course): + """ + Returns the course price as a string preceded by correct currency, or 'Free'. + """ + return get_course_prices(course)[1] + + +def get_course_prices(course, verified_only=False): + """ + Return registration_price and cosmetic_display_prices. + registration_price is the minimum price for the course across all course modes. + cosmetic_display_prices is the course price as a string preceded by correct currency, or 'Free'. + """ + # Find the + if verified_only: + registration_price = CourseMode.min_course_price_for_verified_for_currency( + course.id, + settings.PAID_COURSE_REGISTRATION_CURRENCY[0] + ) + else: + registration_price = CourseMode.min_course_price_for_currency( + course.id, + settings.PAID_COURSE_REGISTRATION_CURRENCY[0] + ) + + currency_symbol = settings.PAID_COURSE_REGISTRATION_CURRENCY[1] + + if registration_price > 0: + price = registration_price + # Handle course overview objects which have no cosmetic_display_price + elif hasattr(course, 'cosmetic_display_price'): + price = course.cosmetic_display_price + else: + price = None + + if price: + # Translators: This will look like '$50', where {currency_symbol} is a symbol such as '$' and {price} is a + # numerical amount in that currency. Adjust this display as needed for your language. + cosmetic_display_price = _("{currency_symbol}{price}").format(currency_symbol=currency_symbol, price=price) + else: + # Translators: This refers to the cost of the course. In this case, the course costs nothing so it is free. + cosmetic_display_price = _('Free') + + return registration_price, force_text(cosmetic_display_price) + + class CourseModesArchive(models.Model): """ Store the past values of course_mode that a course had in the past. We decided on having diff --git a/common/djangoapps/course_modes/tests/test_models.py b/common/djangoapps/course_modes/tests/test_models.py index fc5bb8987f..bbc8b27482 100644 --- a/common/djangoapps/course_modes/tests/test_models.py +++ b/common/djangoapps/course_modes/tests/test_models.py @@ -11,13 +11,18 @@ from datetime import datetime, timedelta import ddt import pytz from django.core.exceptions import ValidationError -from django.test import TestCase +from django.test import TestCase, override_settings +from mock import patch from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locator import CourseLocator from course_modes.helpers import enrollment_mode_display -from course_modes.models import CourseMode, Mode, invalidate_course_mode_cache +from course_modes.models import CourseMode, Mode, invalidate_course_mode_cache, get_cosmetic_display_price from course_modes.tests.factories import CourseModeFactory +from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.tests.django_utils import ( + ModuleStoreTestCase, +) @ddt.ddt @@ -474,3 +479,26 @@ class CourseModeModelTest(TestCase): self.assertTrue(is_error_expected, "Did not expect a ValidationError to be thrown.") else: self.assertFalse(is_error_expected, "Expected a ValidationError to be thrown.") + + +class TestDisplayPrices(ModuleStoreTestCase): + @override_settings(PAID_COURSE_REGISTRATION_CURRENCY=["USD", "$"]) + def test_get_cosmetic_display_price(self): + """ + Check that get_cosmetic_display_price() returns the correct price given its inputs. + """ + course = CourseFactory.create() + registration_price = 99 + course.cosmetic_display_price = 10 + with patch('course_modes.models.CourseMode.min_course_price_for_currency', return_value=registration_price): + # Since registration_price is set, it overrides the cosmetic_display_price and should be returned + self.assertEqual(get_cosmetic_display_price(course), "$99") + + registration_price = 0 + with patch('course_modes.models.CourseMode.min_course_price_for_currency', return_value=registration_price): + # Since registration_price is not set, cosmetic_display_price should be returned + self.assertEqual(get_cosmetic_display_price(course), "$10") + + course.cosmetic_display_price = 0 + # Since both prices are not set, there is no price, thus "Free" + self.assertEqual(get_cosmetic_display_price(course), "Free") diff --git a/lms/djangoapps/ccx/tests/test_field_override_performance.py b/lms/djangoapps/ccx/tests/test_field_override_performance.py index 450e57941c..0e0f54dd38 100644 --- a/lms/djangoapps/ccx/tests/test_field_override_performance.py +++ b/lms/djangoapps/ccx/tests/test_field_override_performance.py @@ -237,18 +237,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): # # of sql queries to default, # # of mongo queries, # ) - ('no_overrides', 1, True, False): (23, 1), - ('no_overrides', 2, True, False): (23, 1), - ('no_overrides', 3, True, False): (23, 1), - ('ccx', 1, True, False): (23, 1), - ('ccx', 2, True, False): (23, 1), - ('ccx', 3, True, False): (23, 1), - ('no_overrides', 1, False, False): (23, 1), - ('no_overrides', 2, False, False): (23, 1), - ('no_overrides', 3, False, False): (23, 1), - ('ccx', 1, False, False): (23, 1), - ('ccx', 2, False, False): (23, 1), - ('ccx', 3, False, False): (23, 1), + ('no_overrides', 1, True, False): (24, 1), + ('no_overrides', 2, True, False): (24, 1), + ('no_overrides', 3, True, False): (24, 1), + ('ccx', 1, True, False): (24, 1), + ('ccx', 2, True, False): (24, 1), + ('ccx', 3, True, False): (24, 1), + ('no_overrides', 1, False, False): (24, 1), + ('no_overrides', 2, False, False): (24, 1), + ('no_overrides', 3, False, False): (24, 1), + ('ccx', 1, False, False): (24, 1), + ('ccx', 2, False, False): (24, 1), + ('ccx', 3, False, False): (24, 1), } @@ -260,19 +260,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase): __test__ = True TEST_DATA = { - ('no_overrides', 1, True, False): (23, 3), - ('no_overrides', 2, True, False): (23, 3), - ('no_overrides', 3, True, False): (23, 3), - ('ccx', 1, True, False): (23, 3), - ('ccx', 2, True, False): (23, 3), - ('ccx', 3, True, False): (23, 3), - ('ccx', 1, True, True): (24, 3), - ('ccx', 2, True, True): (24, 3), - ('ccx', 3, True, True): (24, 3), - ('no_overrides', 1, False, False): (23, 3), - ('no_overrides', 2, False, False): (23, 3), - ('no_overrides', 3, False, False): (23, 3), - ('ccx', 1, False, False): (23, 3), - ('ccx', 2, False, False): (23, 3), - ('ccx', 3, False, False): (23, 3), + ('no_overrides', 1, True, False): (24, 3), + ('no_overrides', 2, True, False): (24, 3), + ('no_overrides', 3, True, False): (24, 3), + ('ccx', 1, True, False): (24, 3), + ('ccx', 2, True, False): (24, 3), + ('ccx', 3, True, False): (24, 3), + ('ccx', 1, True, True): (25, 3), + ('ccx', 2, True, True): (25, 3), + ('ccx', 3, True, True): (25, 3), + ('no_overrides', 1, False, False): (24, 3), + ('no_overrides', 2, False, False): (24, 3), + ('no_overrides', 3, False, False): (24, 3), + ('ccx', 1, False, False): (24, 3), + ('ccx', 2, False, False): (24, 3), + ('ccx', 3, False, False): (24, 3), } diff --git a/lms/djangoapps/courseware/tests/test_course_info.py b/lms/djangoapps/courseware/tests/test_course_info.py index 00b4881808..e9459f382c 100644 --- a/lms/djangoapps/courseware/tests/test_course_info.py +++ b/lms/djangoapps/courseware/tests/test_course_info.py @@ -388,7 +388,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest self.assertEqual(resp.status_code, 200) def test_num_queries_instructor_paced(self): - self.fetch_course_info_with_queries(self.instructor_paced_course, 25, 3) + self.fetch_course_info_with_queries(self.instructor_paced_course, 26, 3) def test_num_queries_self_paced(self): - self.fetch_course_info_with_queries(self.self_paced_course, 25, 3) + self.fetch_course_info_with_queries(self.self_paced_course, 26, 3) diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 4f10ce7677..b54855b36e 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -211,8 +211,8 @@ class IndexQueryTestCase(ModuleStoreTestCase): NUM_PROBLEMS = 20 @ddt.data( - (ModuleStoreEnum.Type.mongo, 10, 144), - (ModuleStoreEnum.Type.split, 4, 144), + (ModuleStoreEnum.Type.mongo, 10, 145), + (ModuleStoreEnum.Type.split, 4, 145), ) @ddt.unpack def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count): @@ -577,26 +577,6 @@ class ViewsTestCase(ModuleStoreTestCase): response = self.client.get(request_url) self.assertEqual(response.status_code, 404) - @override_settings(PAID_COURSE_REGISTRATION_CURRENCY=["USD", "$"]) - def test_get_cosmetic_display_price(self): - """ - Check that get_cosmetic_display_price() returns the correct price given its inputs. - """ - registration_price = 99 - self.course.cosmetic_display_price = 10 - with patch('course_modes.models.CourseMode.min_course_price_for_currency', return_value=registration_price): - # Since registration_price is set, it overrides the cosmetic_display_price and should be returned - self.assertEqual(views.get_cosmetic_display_price(self.course), "$99") - - registration_price = 0 - with patch('course_modes.models.CourseMode.min_course_price_for_currency', return_value=registration_price): - # Since registration_price is not set, cosmetic_display_price should be returned - self.assertEqual(views.get_cosmetic_display_price(self.course), "$10") - - self.course.cosmetic_display_price = 0 - # Since both prices are not set, there is no price, thus "Free" - self.assertEqual(views.get_cosmetic_display_price(self.course), "Free") - def test_jump_to_invalid(self): # TODO add a test for invalid location # TODO add a test for no data * @@ -1464,12 +1444,12 @@ class ProgressPageTests(ProgressPageBaseTests): """Test that query counts remain the same for self-paced and instructor-paced courses.""" SelfPacedConfiguration(enabled=self_paced_enabled).save() self.setup_course(self_paced=self_paced) - with self.assertNumQueries(40, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST), check_mongo_calls(1): + with self.assertNumQueries(41, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST), check_mongo_calls(1): self._get_progress_page() @ddt.data( - (False, 40, 26), - (True, 33, 22) + (False, 41, 27), + (True, 34, 23) ) @ddt.unpack def test_progress_queries(self, enable_waffle, initial, subsequent): diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py index c088ae3106..444a6c8531 100644 --- a/lms/djangoapps/courseware/views/index.py +++ b/lms/djangoapps/courseware/views/index.py @@ -22,6 +22,7 @@ from web_fragments.fragment import Fragment from edxmako.shortcuts import render_to_response, render_to_string from lms.djangoapps.courseware.exceptions import CourseAccessRedirect +from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context from lms.djangoapps.gating.api import get_entrance_exam_score_ratio, get_entrance_exam_usage_key from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory from openedx.core.djangoapps.crawlers.models import CrawlersConfig @@ -34,6 +35,7 @@ from openedx.features.course_experience.views.course_sock import CourseSockFragm from openedx.features.enterprise_support.api import data_sharing_consent_required from shoppingcart.models import CourseRegistrationCode from student.views import is_course_blocked +from student.models import CourseEnrollment from util.views import ensure_valid_course_key from xmodule.modulestore.django import modulestore from xmodule.x_module import STUDENT_VIEW @@ -52,8 +54,6 @@ from ..model_data import FieldDataCache from ..module_render import get_module_for_descriptor, toc_for_course from .views import ( CourseTabView, - check_and_get_upgrade_link, - get_cosmetic_verified_display_price ) log = logging.getLogger("edx.courseware.views.index") @@ -325,6 +325,7 @@ class CoursewareIndex(View): """ course_url_name = default_course_url_name(self.course.id) course_url = reverse(course_url_name, kwargs={'course_id': unicode(self.course.id)}) + courseware_context = { 'csrf': csrf(self.request)['csrf_token'], 'course': self.course, @@ -344,11 +345,14 @@ class CoursewareIndex(View): 'section_title': None, 'sequence_title': None, 'disable_accordion': COURSE_OUTLINE_PAGE_FLAG.is_enabled(self.course.id), - # TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts - 'upgrade_link': check_and_get_upgrade_link(request, self.effective_user, self.course.id), - 'upgrade_price': get_cosmetic_verified_display_price(self.course), - # ENDTODO } + courseware_context.update( + get_experiment_user_metadata_context( + request, + self.course, + self.effective_user, + ) + ) table_of_contents = toc_for_course( self.effective_user, self.request, diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index d48f50a088..e51675b544 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -14,7 +14,7 @@ import waffle from certificates import api as certs_api from certificates.models import CertificateStatuses from commerce.utils import EcommerceService -from course_modes.models import CourseMode +from course_modes.models import (CourseMode, get_course_prices) from courseware.access import has_access, has_ccx_coach_role from courseware.access_utils import check_course_open_for_learner from courseware.courses import ( @@ -61,6 +61,7 @@ from ipware.ip import get_ip from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException from lms.djangoapps.ccx.utils import prep_course_for_grading from lms.djangoapps.courseware.exceptions import CourseAccessRedirect, Redirect +from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory from lms.djangoapps.instructor.enrollment import uses_shib from lms.djangoapps.instructor.views.api import require_global_staff @@ -322,13 +323,14 @@ def course_info(request, course_id): 'dates_fragment': dates_fragment, 'url_to_enroll': CourseTabView.url_to_enroll(course_key), 'course_tools': course_tools, - - # TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts - 'upgrade_link': check_and_get_upgrade_link(request, user, course.id), - 'upgrade_price': get_cosmetic_verified_display_price(course), - 'course_tools': course_tools, - # ENDTODO } + context.update( + get_experiment_user_metadata_context( + request, + course, + user, + ) + ) # Get the URL of the user's last position in order to display the 'where you were last' message context['resume_course_url'] = None @@ -348,20 +350,6 @@ def course_info(request, course_id): UPGRADE_COOKIE_NAME = 'show_upgrade_notification' -# TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts -def check_and_get_upgrade_link(request, user, course_id): - upgrade_link = None - - if request.user.is_authenticated(): - upgrade_data = VerifiedUpgradeDeadlineDate(None, user, course_id=course_id) - if upgrade_data.is_enabled: - upgrade_link = upgrade_data.link - request.need_to_set_upgrade_cookie = True - - return upgrade_link -# ENDTODO - - class StaticCourseTabView(EdxFragmentView): """ View that displays a static course tab with a given name. @@ -521,7 +509,8 @@ class CourseTabView(EdxFragmentView): # Disable student view button if user is staff and # course is not yet visible to students. supports_preview_menu = False - return { + + context = { 'course': course, 'tab': tab, 'active_page': tab.get('type', None), @@ -530,11 +519,15 @@ class CourseTabView(EdxFragmentView): 'supports_preview_menu': supports_preview_menu, 'uses_pattern_library': True, 'disable_courseware_js': True, - # TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts - 'upgrade_link': check_and_get_upgrade_link(request, request.user, course.id), - 'upgrade_price': get_cosmetic_verified_display_price(course), - # ENDTODO } + context.update( + get_experiment_user_metadata_context( + request, + course, + request.user, + ) + ) + return context def render_to_fragment(self, request, course=None, page_context=None, **kwargs): """ @@ -585,59 +578,6 @@ def registered_for_course(course, user): return False -def get_cosmetic_verified_display_price(course): - """ - Returns the minimum verified cert course price as a string preceded by correct currency, or 'Free'. - """ - return get_course_prices(course, verified_only=True)[1] - - -def get_cosmetic_display_price(course): - """ - Returns the course price as a string preceded by correct currency, or 'Free'. - """ - return get_course_prices(course)[1] - - -def get_course_prices(course, verified_only=False): - """ - Return registration_price and cosmetic_display_prices. - registration_price is the minimum price for the course across all course modes. - cosmetic_display_prices is the course price as a string preceded by correct currency, or 'Free'. - """ - # Find the - if verified_only: - registration_price = CourseMode.min_course_price_for_verified_for_currency( - course.id, - settings.PAID_COURSE_REGISTRATION_CURRENCY[0] - ) - else: - registration_price = CourseMode.min_course_price_for_currency( - course.id, - settings.PAID_COURSE_REGISTRATION_CURRENCY[0] - ) - - currency_symbol = settings.PAID_COURSE_REGISTRATION_CURRENCY[1] - - if registration_price > 0: - price = registration_price - # Handle course overview objects which have no cosmetic_display_price - elif hasattr(course, 'cosmetic_display_price'): - price = course.cosmetic_display_price - else: - price = None - - if price: - # Translators: This will look like '$50', where {currency_symbol} is a symbol such as '$' and {price} is a - # numerical amount in that currency. Adjust this display as needed for your language. - cosmetic_display_price = _("{currency_symbol}{price}").format(currency_symbol=currency_symbol, price=price) - else: - # Translators: This refers to the cost of the course. In this case, the course costs nothing so it is free. - cosmetic_display_price = _('Free') - - return registration_price, cosmetic_display_price - - class EnrollStaffView(View): """ Displays view for registering in the course to a global staff user. @@ -927,7 +867,6 @@ def _progress(request, course_key, student_id): grade_summary = course_grade.summary studio_url = get_studio_url(course, 'settings/grading') - # checking certificate generation configuration enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(student, course_key) @@ -943,11 +882,14 @@ def _progress(request, course_key, student_id): 'passed': is_course_passed(course, grade_summary), 'credit_course_requirements': _credit_course_requirements(course_key, student), 'certificate_data': _get_cert_data(student, course, course_key, is_active, enrollment_mode), - # TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts - 'upgrade_link': check_and_get_upgrade_link(request, student, course.id), - 'upgrade_price': get_cosmetic_verified_display_price(course), - # ENDTODO } + context.update( + get_experiment_user_metadata_context( + request, + course, + student, + ) + ) with outer_atomic(): response = render_to_response('courseware/progress.html', context) diff --git a/lms/djangoapps/discussion/views.py b/lms/djangoapps/discussion/views.py index 4a0f126e18..44fc1ef0d1 100644 --- a/lms/djangoapps/discussion/views.py +++ b/lms/djangoapps/discussion/views.py @@ -24,6 +24,7 @@ from rest_framework import status from web_fragments.fragment import Fragment import django_comment_client.utils as utils +from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context import lms.lib.comment_client as cc from courseware.access import has_access from courseware.courses import get_course_with_access @@ -44,7 +45,6 @@ from django_comment_client.utils import ( strip_none ) from django_comment_common.utils import ThreadContext, get_course_discussion_settings, set_course_discussion_settings -from lms.djangoapps.courseware.views.views import check_and_get_upgrade_link, get_cosmetic_verified_display_price from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from student.models import CourseEnrollment from util.json_request import JsonResponse, expect_json @@ -481,13 +481,16 @@ def _create_discussion_board_context(request, base_context, thread=None): 'category_map': course_settings["category_map"], 'course_settings': course_settings, 'is_commentable_divided': is_commentable_divided(course_key, discussion_id, course_discussion_settings), - # TODO: (Experimental Code). See https://openedx.atlassian.net/wiki/display/RET/2.+In-course+Verification+Prompts - 'upgrade_link': check_and_get_upgrade_link(request, user, course.id), - 'upgrade_price': get_cosmetic_verified_display_price(course), - # ENDTODO # If the default topic id is None the front-end code will look for a topic that contains "General" 'discussion_default_topic_id': _get_discussion_default_topic_id(course), }) + context.update( + get_experiment_user_metadata_context( + request, + course, + user, + ) + ) return context diff --git a/lms/djangoapps/experiments/utils.py b/lms/djangoapps/experiments/utils.py new file mode 100644 index 0000000000..1515680183 --- /dev/null +++ b/lms/djangoapps/experiments/utils.py @@ -0,0 +1,48 @@ +from student.models import CourseEnrollment +from course_modes.models import ( + get_cosmetic_verified_display_price +) +from courseware.date_summary import ( + VerifiedUpgradeDeadlineDate +) + + +def check_and_get_upgrade_link(request, user, course_id): + """ + For an authenticated user, return a link to allow them to upgrade + in the specified course. + """ + if request.user.is_authenticated(): + upgrade_data = VerifiedUpgradeDeadlineDate(None, user, course_id=course_id) + if upgrade_data.is_enabled: + request.need_to_set_upgrade_cookie = True + return upgrade_data + + return None + + +def get_experiment_user_metadata_context(request, course, user): + """ + Return a context dictionary with the keys used by the user_metadata.html. + """ + enrollment_mode = None + enrollment_time = None + try: + enrollment = CourseEnrollment.objects.get(user_id=user.id, course_id=course.id) + if enrollment.is_active: + enrollment_mode = enrollment.mode + enrollment_time = enrollment.created + except CourseEnrollment.DoesNotExist: + pass # Not enrolled, used the default None values + + upgrade_data = check_and_get_upgrade_link(request, user, course.id) + + return { + 'upgrade_link': upgrade_data and upgrade_data.link, + 'upgrade_price': 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_data and upgrade_data.date, + 'course_key': course.id, + } diff --git a/lms/templates/main.html b/lms/templates/main.html index e25c17cc8c..de5e62a941 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -104,6 +104,7 @@ from pipeline_mako import render_require_js_path_overrides <%block name="head_extra"/> <%include file="/courseware/experiments.html"/> + <%include file="user_metadata.html"/> <%static:optional_include_mako file="head-extra.html" is_theming_enabled="True" /> <%include file="widgets/optimizely.html" /> diff --git a/lms/templates/user_metadata.html b/lms/templates/user_metadata.html new file mode 100644 index 0000000000..1b829df3ae --- /dev/null +++ b/lms/templates/user_metadata.html @@ -0,0 +1,51 @@ +<%page expression_filter="h"/> +<%! +from openedx.core.djangolib.js_utils import dump_js_escaped_json +from eventtracking import tracker +from opaque_keys.edx.keys import CourseKey +%> +<% +user_metadata = { + key: context.get(key) + for key in ( + 'username', + 'user_id', + 'course_id', + 'enrollment_mode', + 'upgrade_link', + 'upgrade_deadline', + 'upgrade_price', + 'pacing_type', + ) +} + +if user: + user_metadata['username'] = user.username + user_metadata['user_id'] = user.id + +for datekey in ('schedule_start', 'enrollment_time'): + user_metadata[datekey] = ( + context.get(datekey).isoformat() if context.get(datekey) else None + ) + +course_key = context.get('course_key') +if course and not course_key: + course_key = course.id + +if course_key: + if isinstance(course_key, CourseKey): + user_metadata['course_key_fields'] = { + 'org': course_key.org, + 'course': course_key.course, + 'run': course_key.run, + } + + if not course_id: + user_metadata['course_id'] = unicode(course_key) + elif isinstance(course_key, basestring): + user_metadata['course_id'] = course_key + +%> + diff --git a/openedx/features/course_experience/tests/views/test_course_home.py b/openedx/features/course_experience/tests/views/test_course_home.py index e4b73175e8..6248aa9a9f 100644 --- a/openedx/features/course_experience/tests/views/test_course_home.py +++ b/openedx/features/course_experience/tests/views/test_course_home.py @@ -160,7 +160,7 @@ class TestCourseHomePage(CourseHomePageTestCase): course_home_url(self.course) # Fetch the view and verify the query counts - with self.assertNumQueries(40, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): + with self.assertNumQueries(41, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): with check_mongo_calls(4): url = course_home_url(self.course) self.client.get(url) diff --git a/openedx/features/course_experience/tests/views/test_course_updates.py b/openedx/features/course_experience/tests/views/test_course_updates.py index ad2d481381..aa7f3f1ec8 100644 --- a/openedx/features/course_experience/tests/views/test_course_updates.py +++ b/openedx/features/course_experience/tests/views/test_course_updates.py @@ -127,7 +127,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase): course_updates_url(self.course) # Fetch the view and verify that the query counts haven't changed - with self.assertNumQueries(31, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): + with self.assertNumQueries(32, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): with check_mongo_calls(4): url = course_updates_url(self.course) self.client.get(url) diff --git a/openedx/features/course_experience/views/course_sock.py b/openedx/features/course_experience/views/course_sock.py index f55edb755a..418c705506 100644 --- a/openedx/features/course_experience/views/course_sock.py +++ b/openedx/features/course_experience/views/course_sock.py @@ -6,9 +6,8 @@ from opaque_keys.edx.keys import CourseKey from web_fragments.fragment import Fragment from student.models import CourseEnrollment -from course_modes.models import CourseMode +from course_modes.models import CourseMode, get_cosmetic_verified_display_price from courseware.date_summary import VerifiedUpgradeDeadlineDate -from courseware.views.views import get_cosmetic_verified_display_price from openedx.core.djangoapps.plugin_api.views import EdxFragmentView diff --git a/scripts/xss_linter.py b/scripts/xss_linter.py index cd6fc16eeb..17e5cf240f 100755 --- a/scripts/xss_linter.py +++ b/scripts/xss_linter.py @@ -2392,7 +2392,7 @@ class MakoTemplateLinter(BaseLinter): contexts = [{'index': 0, 'type': 'html'}] javascript_types = [ 'text/javascript', 'text/ecmascript', 'application/ecmascript', 'application/javascript', - 'text/x-mathjax-config', 'json/xblock-args' + 'text/x-mathjax-config', 'json/xblock-args', 'application/json', ] html_types = ['text/template'] for context in contexts_re.finditer(mako_template):