Add per-user metadata to course pages to make experimentation easier
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
48
lms/djangoapps/experiments/utils.py
Normal file
48
lms/djangoapps/experiments/utils.py
Normal file
@@ -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,
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
51
lms/templates/user_metadata.html
Normal file
51
lms/templates/user_metadata.html
Normal file
@@ -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
|
||||
|
||||
%>
|
||||
<script type="application/json" id="user-metadata">
|
||||
${user_metadata | n, dump_js_escaped_json}
|
||||
</script>
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user