Extract upsell into template, extract ecomm call, use mobile utils
This commit is contained in:
@@ -5,6 +5,7 @@ Module rendering
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import textwrap
|
||||
from collections import OrderedDict
|
||||
from functools import partial
|
||||
|
||||
@@ -17,8 +18,10 @@ from django.template.context_processors import csrf
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.urls import reverse
|
||||
from django.http import Http404, HttpResponse, HttpResponseForbidden
|
||||
from django.utils.text import slugify
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from edx_django_utils.cache import RequestCache
|
||||
from edx_django_utils.monitoring import set_custom_metrics_for_course_key, set_monitoring_transaction_name
|
||||
from edx_proctoring.services import ProctoringService
|
||||
from opaque_keys import InvalidKeyError
|
||||
@@ -34,6 +37,7 @@ from xblock.runtime import KvsFieldData
|
||||
import static_replace
|
||||
from capa.xqueue_interface import XQueueInterface
|
||||
from courseware.access import get_user_role, has_access
|
||||
from courseware.access_response import IncorrectPartitionGroupError
|
||||
from courseware.entrance_exams import user_can_skip_entrance_exam, user_has_passed_entrance_exam
|
||||
from courseware.masquerade import (
|
||||
MasqueradingKeyValueStore,
|
||||
@@ -43,7 +47,6 @@ from courseware.masquerade import (
|
||||
)
|
||||
from courseware.model_data import DjangoKeyValueStore, FieldDataCache
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from edx_django_utils.cache import RequestCache
|
||||
from eventtracking import tracker
|
||||
from lms.djangoapps.courseware.field_overrides import OverrideFieldData
|
||||
from lms.djangoapps.grades.signals.signals import SCORE_PUBLISHED
|
||||
@@ -55,6 +58,7 @@ from openedx.core.djangoapps.bookmarks.services import BookmarksService
|
||||
from openedx.core.djangoapps.crawlers.models import CrawlersConfig
|
||||
from openedx.core.djangoapps.credit.services import CreditService
|
||||
from openedx.core.djangoapps.util.user_utils import SystemUser
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
from openedx.core.lib.gating.services import GatingService
|
||||
from openedx.core.lib.license import wrap_with_license
|
||||
from openedx.core.lib.url_utils import quote_slashes, unquote_slashes
|
||||
@@ -71,7 +75,6 @@ from student.roles import CourseBetaTesterRole
|
||||
from track import contexts
|
||||
from util import milestones_helpers
|
||||
from util.json_request import JsonResponse
|
||||
from django.utils.text import slugify
|
||||
from web_fragments.fragment import Fragment
|
||||
from xmodule.util.sandboxing import can_execute_unsafe_code, get_python_lib_zip
|
||||
from xblock_django.user_service import DjangoXBlockUserService
|
||||
@@ -176,6 +179,7 @@ def toc_for_course(user, request, course, active_chapter, active_section, field_
|
||||
for chapter in chapters:
|
||||
# Only show required content, if there is required content
|
||||
# chapter.hide_from_toc is read-only (bool)
|
||||
# xss-lint: disable=python-deprecated-display-name
|
||||
display_id = slugify(chapter.display_name_with_default_escaped)
|
||||
local_hide_from_toc = False
|
||||
if required_content:
|
||||
@@ -197,6 +201,7 @@ def toc_for_course(user, request, course, active_chapter, active_section, field_
|
||||
found_active_section = True
|
||||
|
||||
section_context = {
|
||||
# xss-lint: disable=python-deprecated-display-name
|
||||
'display_name': section.display_name_with_default_escaped,
|
||||
'url_name': section.url_name,
|
||||
'format': section.format if section.format is not None else '',
|
||||
@@ -220,6 +225,7 @@ def toc_for_course(user, request, course, active_chapter, active_section, field_
|
||||
last_processed_chapter = chapter
|
||||
|
||||
toc_chapters.append({
|
||||
# xss-lint: disable=python-deprecated-display-name
|
||||
'display_name': chapter.display_name_with_default_escaped,
|
||||
'display_id': display_id,
|
||||
'url_name': chapter.url_name,
|
||||
@@ -331,13 +337,17 @@ def get_module(user, request, usage_key, field_data_cache,
|
||||
log.debug("Error in get_module: ItemNotFoundError")
|
||||
return None
|
||||
|
||||
except:
|
||||
except: # pylint: disable=W0702
|
||||
# Something has gone terribly wrong, but still not letting it turn into a 500.
|
||||
log.exception("Error in get_module")
|
||||
return None
|
||||
|
||||
|
||||
def display_access_messages(user, block, view, frag, context):
|
||||
def display_access_messages(user, block, view, frag, context): # pylint: disable=W0613
|
||||
"""
|
||||
An XBlock wrapper that replaces the content fragment with a fragment or message determined by
|
||||
the has_access check.
|
||||
"""
|
||||
blocked_prior_sibling = RequestCache('display_access_messages_prior_sibling')
|
||||
|
||||
load_access = has_access(user, 'load', block, block.scope_ids.usage_id.course_key)
|
||||
@@ -347,7 +357,7 @@ def display_access_messages(user, block, view, frag, context):
|
||||
|
||||
prior_sibling = blocked_prior_sibling.get_cached_response(block.parent)
|
||||
|
||||
if prior_sibling.is_found and prior_sibling.value == load_access:
|
||||
if prior_sibling.is_found and prior_sibling.value.error_code == load_access.error_code:
|
||||
return Fragment(u"")
|
||||
else:
|
||||
blocked_prior_sibling.set(block.parent, load_access)
|
||||
@@ -355,16 +365,16 @@ def display_access_messages(user, block, view, frag, context):
|
||||
if load_access.user_fragment:
|
||||
msg_fragment = load_access.user_fragment
|
||||
elif load_access.user_message:
|
||||
msg_fragment = Fragment(textwrap.dedent(u"""\
|
||||
msg_fragment = Fragment(textwrap.dedent(HTML(u"""\
|
||||
<div>{}</div>
|
||||
""".format(load_access.user_message)))
|
||||
""").format(load_access.user_message)))
|
||||
else:
|
||||
msg_fragment = Fragment(u"")
|
||||
|
||||
if load_access.developer_message and has_access(user, 'staff', block, block.scope_ids.usage_id.course_key):
|
||||
msg_fragment.content += textwrap.dedent(u"""\
|
||||
msg_fragment.content += textwrap.dedent(HTML(u"""\
|
||||
<div>{}</div>
|
||||
""".format(load_access.developer_message))
|
||||
""").format(load_access.developer_message))
|
||||
|
||||
return msg_fragment
|
||||
|
||||
@@ -384,6 +394,7 @@ def get_xqueue_callback_url_prefix(request):
|
||||
return settings.XQUEUE_INTERFACE.get('callback_url', prefix)
|
||||
|
||||
|
||||
# pylint: disable=too-many-statements
|
||||
def get_module_for_descriptor(user, request, descriptor, field_data_cache, course_key,
|
||||
position=None, wrap_xmodule_display=True, grade_bucket_type=None,
|
||||
static_asset_path='', disable_staff_debug_info=False,
|
||||
@@ -508,7 +519,8 @@ def get_module_system_for_user(
|
||||
static_asset_path=static_asset_path,
|
||||
user_location=user_location,
|
||||
request_token=request_token,
|
||||
course=course
|
||||
course=course,
|
||||
will_recheck_access=True,
|
||||
)
|
||||
|
||||
def get_event_handler(event_type):
|
||||
@@ -838,7 +850,7 @@ def get_module_for_descriptor_internal(user, descriptor, student_data, course_id
|
||||
track_function, xqueue_callback_url_prefix, request_token,
|
||||
position=None, wrap_xmodule_display=True, grade_bucket_type=None,
|
||||
static_asset_path='', user_location=None, disable_staff_debug_info=False,
|
||||
course=None):
|
||||
course=None, will_recheck_access=False):
|
||||
"""
|
||||
Actually implement get_module, without requiring a request.
|
||||
|
||||
@@ -883,12 +895,18 @@ def get_module_for_descriptor_internal(user, descriptor, student_data, course_id
|
||||
user_needs_access_check = getattr(user, 'known', True) and not isinstance(user, SystemUser)
|
||||
if user_needs_access_check:
|
||||
access = has_access(user, 'load', descriptor, course_id)
|
||||
if not access:
|
||||
if access.user_message or access.user_fragment:
|
||||
# This content will be access restricted by modifying the outgoing html
|
||||
return descriptor
|
||||
else:
|
||||
return None
|
||||
# A descriptor should only be returned if either the user has access, or the user doesn't have access, but
|
||||
# the failed access has a message for the user and the caller of this function specifies it will check access
|
||||
# again. This allows blocks to show specific error message or upsells when access is denied.
|
||||
caller_will_handle_access_error = (
|
||||
not access
|
||||
and will_recheck_access
|
||||
and (access.user_message or access.user_fragment)
|
||||
and isinstance(access, IncorrectPartitionGroupError)
|
||||
)
|
||||
if access or caller_will_handle_access_error:
|
||||
return descriptor
|
||||
return None
|
||||
return descriptor
|
||||
|
||||
|
||||
@@ -1034,6 +1052,7 @@ def get_module_by_usage_id(request, course_id, usage_id, disable_staff_debug_inf
|
||||
|
||||
tracking_context = {
|
||||
'module': {
|
||||
# xss-lint: disable=python-deprecated-display-name
|
||||
'display_name': descriptor.display_name_with_default_escaped,
|
||||
'usage_key': unicode(descriptor.location),
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
@import 'features/learner-profile';
|
||||
@import 'features/journals';
|
||||
@import 'features/_unsupported-browser-alert';
|
||||
@import 'features/content-type-gating';
|
||||
|
||||
// search
|
||||
@import 'search/search';
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
@import 'features/course-upgrade-message';
|
||||
@import 'features/learner-analytics-dashboard';
|
||||
@import 'features/journals';
|
||||
@import 'features/content-type-gating';
|
||||
|
||||
|
||||
// Responsive Design
|
||||
@import 'header';
|
||||
|
||||
32
lms/static/sass/features/_content-type-gating.scss
Normal file
32
lms/static/sass/features/_content-type-gating.scss
Normal file
@@ -0,0 +1,32 @@
|
||||
.content-paywall {
|
||||
margin-top: 10px;
|
||||
border-radius: 5px 5px 5px 5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border: lightgrey 1px solid;
|
||||
padding: 15px 20px;
|
||||
}
|
||||
.content-paywall h3 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.content-paywall .fa-lock {
|
||||
color: black;
|
||||
margin-right: 10px;
|
||||
font-size: 24px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.content-paywall .certDIV_1 {
|
||||
color: rgb(25, 125, 29);
|
||||
height: 20px;
|
||||
width: 300px;
|
||||
font: normal normal 600 normal 14px / 20px 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
.content-paywall .certA_2 {
|
||||
text-decoration: underline !important;
|
||||
color: rgb(0, 117, 180);
|
||||
font: normal normal 400 normal 16px / 25.6px 'Open Sans';
|
||||
}
|
||||
.content-paywall img {
|
||||
height: 60px;
|
||||
}
|
||||
@@ -6,13 +6,12 @@ of audit learners.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import textwrap
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
|
||||
import crum
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from web_fragments.fragment import Fragment
|
||||
@@ -23,6 +22,7 @@ from lms.djangoapps.courseware.masquerade import (
|
||||
get_masquerading_user_group,
|
||||
)
|
||||
from xmodule.partitions.partitions import Group, UserPartition, UserPartitionError
|
||||
from openedx.core.lib.mobile_utils import is_request_from_mobile_app
|
||||
from openedx.features.course_duration_limits.config import (
|
||||
CONTENT_TYPE_GATING_FLAG,
|
||||
CONTENT_TYPE_GATING_STUDIO_UI_FLAG,
|
||||
@@ -79,92 +79,34 @@ def create_content_gating_partition(course):
|
||||
|
||||
|
||||
class ContentTypeGatingPartition(UserPartition):
|
||||
|
||||
"""
|
||||
A custom UserPartition which allows us to override the access denied messaging in regards
|
||||
to gated content.
|
||||
"""
|
||||
def access_denied_fragment(self, block, user, user_group, allowed_groups):
|
||||
ecomm_service = EcommerceService()
|
||||
ecommerce_checkout = ecomm_service.is_enabled(user)
|
||||
ecommerce_checkout_link = ''
|
||||
ecommerce_bulk_checkout_link = ''
|
||||
verified_mode = None
|
||||
CourseMode = apps.get_model('course_modes.CourseMode')
|
||||
modes = CourseMode.modes_for_course_dict(block.scope_ids.usage_id.course_key)
|
||||
verified_mode = modes.get(CourseMode.VERIFIED, '')
|
||||
|
||||
if ecommerce_checkout and verified_mode.sku:
|
||||
ecommerce_checkout_link = ecomm_service.get_checkout_page_url(verified_mode.sku)
|
||||
|
||||
verified_mode = modes.get(CourseMode.VERIFIED)
|
||||
if verified_mode is None:
|
||||
return None
|
||||
ecommerce_checkout_link = self._get_checkout_link(user, verified_mode.sku)
|
||||
|
||||
request = crum.get_current_request()
|
||||
if 'org.edx.mobile' in request.META.get('HTTP_USER_AGENT', ''):
|
||||
upsell = ''
|
||||
else:
|
||||
upsell = textwrap.dedent("""\
|
||||
<span class="certDIV_1" style="">
|
||||
<a href="{ecommerce_checkout_link}" class="certA_2">
|
||||
Upgrade to unlock (${min_price})
|
||||
</a>
|
||||
</span>
|
||||
""".format(
|
||||
ecommerce_checkout_link=ecommerce_checkout_link,
|
||||
# TODO: Does this need i18n?
|
||||
min_price=verified_mode.min_price,
|
||||
))
|
||||
|
||||
frag = Fragment(textwrap.dedent(u"""\
|
||||
<div class=".content-paywall">
|
||||
<div>
|
||||
<h3>
|
||||
<span class="fa fa-lock" aria-hidden="true"></span>
|
||||
Verified Track Access
|
||||
</h3>
|
||||
<span style=" padding: 10px 0;">
|
||||
Graded assessments are available to Verified Track learners.
|
||||
</span>
|
||||
{upsell}
|
||||
</div>
|
||||
<img src="https://courses.edx.org/static/images/edx-verified-mini-cert.png">
|
||||
</div>
|
||||
""".format(
|
||||
upsell=upsell,
|
||||
)))
|
||||
frag.add_css(textwrap.dedent("""\
|
||||
.content-paywall {
|
||||
margin-top: 10px;
|
||||
border-radius: 5px 5px 5px 5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border: lightgrey 1px solid;
|
||||
padding: 15px 20px;
|
||||
}
|
||||
.content-paywall h3 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.content-paywall .fa-lock {
|
||||
color: black;
|
||||
margin-right: 10px;
|
||||
font-size: 24px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.content-paywall .certDIV_1 {
|
||||
color: rgb(25, 125, 29);
|
||||
height: 20px;
|
||||
width: 300px;
|
||||
font: normal normal 600 normal 14px / 20px 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
.content-paywall .certA_2 {
|
||||
text-decoration: underline !important;
|
||||
color: rgb(0, 117, 180);
|
||||
font: normal normal 400 normal 16px / 25.6px 'Open Sans';
|
||||
}
|
||||
.content-paywall img {
|
||||
height: 60px;
|
||||
}
|
||||
"""))
|
||||
frag = Fragment(render_to_string('content_type_gating/access_denied_message.html', {
|
||||
'mobile_app': is_request_from_mobile_app(request),
|
||||
'ecommerce_checkout_link': ecommerce_checkout_link,
|
||||
'min_price': str(verified_mode.min_price)
|
||||
}))
|
||||
return frag
|
||||
|
||||
def access_denied_message(self, block, user, user_group, allowed_groups):
|
||||
return "Graded assessments are available to Verified Track learners. Upgrade to Unlock."
|
||||
|
||||
def _get_checkout_link(self, user, sku):
|
||||
ecomm_service = EcommerceService()
|
||||
ecommerce_checkout = ecomm_service.is_enabled(user)
|
||||
if ecommerce_checkout and sku:
|
||||
return ecomm_service.get_checkout_page_url(sku) or ''
|
||||
|
||||
|
||||
class ContentTypeGatingPartitionScheme(object):
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
<div class="content-paywall">
|
||||
<div>
|
||||
<h3>
|
||||
<span class="fa fa-lock" aria-hidden="true"></span>
|
||||
{% trans "Verified Track Access" %}
|
||||
</h3>
|
||||
<span style=" padding: 10px 0;">
|
||||
{% trans "Graded assessments are available to Verified Track learners." %}
|
||||
</span>
|
||||
{% if not mobile_app and ecommerce_checkout_link %}
|
||||
<span class="certDIV_1" style="">
|
||||
<a href="{{ecommerce_checkout_link}}" class="certA_2">
|
||||
{% trans "Upgrade to unlock" %} (${{min_price}})
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<img src="{% static '/images/edx-verified-mini-cert.png' %}">
|
||||
</div>
|
||||
@@ -104,7 +104,8 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
|
||||
self.audit_user = UserFactory.create()
|
||||
self.enrollment = CourseEnrollmentFactory.create(user=self.audit_user, course_id=self.course.id, mode='audit')
|
||||
|
||||
def assert_block_is_gated(self, block, is_gated):
|
||||
@patch("crum.get_current_request")
|
||||
def assert_block_is_gated(self, block, is_gated, mock_get_current_request):
|
||||
'''
|
||||
This functions asserts whether the passed in block is gated by content type gating.
|
||||
This is determined by checking whether the has_access method called the IncorrectPartitionGroupError.
|
||||
@@ -113,6 +114,8 @@ class TestProblemTypeAccess(SharedModuleStoreTestCase):
|
||||
whether the IncorrectPartitionGroupError was called.
|
||||
'''
|
||||
fake_request = Mock()
|
||||
fake_request = self.factory.get('')
|
||||
mock_get_current_request.return_value = fake_request
|
||||
|
||||
with patch.object(IncorrectPartitionGroupError, '__init__',
|
||||
wraps=IncorrectPartitionGroupError.__init__) as mock_access_error:
|
||||
|
||||
@@ -25,6 +25,8 @@ BLOCK_STRUCTURES_SETTINGS = dict(
|
||||
|
||||
COURSE_KEY_PATTERN = r'(?P<course_key_string>[^/+]+(/|\+)[^/+]+(/|\+)[^/?]+)'
|
||||
COURSE_ID_PATTERN = COURSE_KEY_PATTERN.replace('course_key_string', 'course_id')
|
||||
USAGE_KEY_PATTERN = r'(?P<usage_key_string>(?:i4x://?[^/]+/[^/]+/[^/]+/[^@]+(?:@[^/]+)?)|(?:[^/]+))'
|
||||
|
||||
|
||||
COURSE_MODE_DEFAULTS = {
|
||||
'bulk_sku': None,
|
||||
|
||||
Reference in New Issue
Block a user