From 6e0914ad5d0f8d0833602a87af3da295b2400dc6 Mon Sep 17 00:00:00 2001 From: julianajlk Date: Wed, 23 Oct 2019 09:33:11 -0400 Subject: [PATCH] add FPD banner to courseware (#22113) REV-980 --- .../course_modes/tests/test_views.py | 4 +- common/djangoapps/course_modes/views.py | 2 +- lms/djangoapps/courseware/module_render.py | 2 + lms/djangoapps/courseware/tests/test_views.py | 4 +- lms/static/sass/_build-lms-v1.scss | 1 + lms/static/sass/bootstrap/lms-main.scss | 2 +- .../sass/features/_course-experience.scss | 20 ------ .../sass/features/_first-purchase-banner.scss | 27 ++++++++ lms/static/sass/views/_verification.scss | 4 +- .../tests/views/test_course_home.py | 4 +- openedx/features/course_experience/utils.py | 27 -------- .../course_experience/views/course_home.py | 2 +- openedx/features/discounts/applicability.py | 8 +-- openedx/features/discounts/utils.py | 66 ++++++++++++++++++- 14 files changed, 110 insertions(+), 63 deletions(-) create mode 100644 lms/static/sass/features/_first-purchase-banner.scss diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py index 1894711f00..3a83509594 100644 --- a/common/djangoapps/course_modes/tests/test_views.py +++ b/common/djangoapps/course_modes/tests/test_views.py @@ -398,8 +398,8 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest self.assertEquals(course_mode, expected_mode) - @patch('openedx.features.course_experience.utils.can_receive_discount') - @patch('openedx.features.course_experience.utils.discount_percentage') + @patch('openedx.features.discounts.utils.can_receive_discount') + @patch('openedx.features.discounts.utils.discount_percentage') def test_discount_on_track_selection(self, discount_percentage_mock, can_receive_discount_mock): can_receive_discount_mock.return_value = True discount_percentage_mock.return_value = 15 diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 6bcc521f83..77d12334be 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -36,7 +36,7 @@ from openedx.core.djangoapps.embargo import api as embargo_api from openedx.core.djangoapps.enrollments.permissions import ENROLL_IN_COURSE from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.course_duration_limits.models import CourseDurationLimitConfig -from openedx.features.course_experience.utils import get_first_purchase_offer_banner_fragment +from openedx.features.discounts.utils import get_first_purchase_offer_banner_fragment from openedx.features.discounts.applicability import discount_percentage from student.models import CourseEnrollment from util.db import outer_atomic diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index b6ce3b34a0..a50e18a725 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -85,6 +85,7 @@ from openedx.core.lib.xblock_utils import ( from openedx.core.lib.xblock_utils import request_token as xblock_request_token from openedx.core.lib.xblock_utils import wrap_xblock from openedx.features.course_duration_limits.access import course_expiration_wrapper +from openedx.features.discounts.utils import offer_banner_wrapper from student.models import anonymous_id_for_user, user_by_anonymous_id from student.roles import CourseBetaTesterRole from track import contexts @@ -729,6 +730,7 @@ def get_module_system_for_user( block_wrappers.append(partial(display_access_messages, user)) block_wrappers.append(partial(course_expiration_wrapper, user)) + block_wrappers.append(partial(offer_banner_wrapper, user)) if settings.FEATURES.get('DISPLAY_DEBUG_INFO_TO_STAFF'): if is_masquerading_as_specific_student(user, course_id): diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 3fcc003667..8a1e699d6f 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -225,8 +225,8 @@ class IndexQueryTestCase(ModuleStoreTestCase): NUM_PROBLEMS = 20 @ddt.data( - (ModuleStoreEnum.Type.mongo, 10, 182), - (ModuleStoreEnum.Type.split, 4, 176), + (ModuleStoreEnum.Type.mongo, 10, 186), + (ModuleStoreEnum.Type.split, 4, 180), ) @ddt.unpack def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count): diff --git a/lms/static/sass/_build-lms-v1.scss b/lms/static/sass/_build-lms-v1.scss index ed3e8ad2d1..fae155c379 100644 --- a/lms/static/sass/_build-lms-v1.scss +++ b/lms/static/sass/_build-lms-v1.scss @@ -74,6 +74,7 @@ @import 'features/content-type-gating'; @import 'features/course-duration-limits'; @import 'features/enterprise-learner-portal-banner'; +@import 'features/first-purchase-banner'; // search @import 'search/search'; diff --git a/lms/static/sass/bootstrap/lms-main.scss b/lms/static/sass/bootstrap/lms-main.scss index b1a066465b..fc384030be 100644 --- a/lms/static/sass/bootstrap/lms-main.scss +++ b/lms/static/sass/bootstrap/lms-main.scss @@ -26,7 +26,7 @@ $static-path: '../..'; @import 'features/course-upgrade-message'; @import 'features/course-duration-limits'; @import 'features/enterprise-learner-portal-banner'; - +@import 'features/first-purchase-banner'; // Individual Pages @import "views/program-marketing-page"; diff --git a/lms/static/sass/features/_course-experience.scss b/lms/static/sass/features/_course-experience.scss index 23ed9e204c..70b7789846 100644 --- a/lms/static/sass/features/_course-experience.scss +++ b/lms/static/sass/features/_course-experience.scss @@ -20,26 +20,6 @@ } } -// First purchase offer banner -.first-purchase-offer-banner { - background-color: #DEE3F1; - font-size: 16px; - border-radius: 7px; - padding: 20px; - margin: 20px auto; - - .first-purchase-offer-banner-bold { - font-weight: bold; - color: #23419F; - } - - a { - color: #00496f; - text-decoration: underline; - font-weight: bold; - } -} - // Course call to action message .course-message { display: flex; diff --git a/lms/static/sass/features/_first-purchase-banner.scss b/lms/static/sass/features/_first-purchase-banner.scss new file mode 100644 index 0000000000..60bfe28214 --- /dev/null +++ b/lms/static/sass/features/_first-purchase-banner.scss @@ -0,0 +1,27 @@ +// First purchase offer banner +.first-purchase-offer-banner { + background-color: #DEE3F1; + font-size: 16px; + border-radius: 7px; + padding: 20px; + margin: 20px auto; + box-sizing: border-box; + line-height: 1.5; + + .first-purchase-offer-banner-bold { + font-weight: bold; + color: #393f43; + } + + a { + color: #23419F !important; + text-decoration: underline !important; + font-weight: bold !important; + border-bottom: none; + } + +} + +#seq_content .first-purchase-offer-banner { + max-width: $text-width-readability-max; +} diff --git a/lms/static/sass/views/_verification.scss b/lms/static/sass/views/_verification.scss index 9624b84803..9a78893ec1 100644 --- a/lms/static/sass/views/_verification.scss +++ b/lms/static/sass/views/_verification.scss @@ -2265,11 +2265,11 @@ .first-purchase-offer-banner-bold { font-weight: bold; - color: #23419f; + color: #393f43; } a { - color: #00496f; + color: #23419F; text-decoration: underline !important; font-weight: bold; border-bottom: none; 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 90de4d8c70..9f71e21641 100644 --- a/openedx/features/course_experience/tests/views/test_course_home.py +++ b/openedx/features/course_experience/tests/views/test_course_home.py @@ -413,8 +413,8 @@ class TestCourseHomePageAccess(CourseHomePageTestCase): ) self.assertRedirects(response, expected_url) - @mock.patch('openedx.features.course_experience.utils.discount_percentage') - @mock.patch('openedx.features.course_experience.utils.can_receive_discount') + @mock.patch('openedx.features.discounts.utils.discount_percentage') + @mock.patch('openedx.features.discounts.utils.can_receive_discount') @ddt.data( [True, 15], [True, 13], diff --git a/openedx/features/course_experience/utils.py b/openedx/features/course_experience/utils.py index dd9bbdc01d..9b7842ada6 100644 --- a/openedx/features/course_experience/utils.py +++ b/openedx/features/course_experience/utils.py @@ -228,30 +228,3 @@ def get_resume_block(block): if resume_block: return resume_block return block - - -def get_first_purchase_offer_banner_fragment(user, course): - if user and course: - discount_expiration_date = get_discount_expiration_date(user, course) - if (discount_expiration_date and - can_receive_discount(user=user, course=course, discount_expiration_date=discount_expiration_date)): - # Translator: xgettext:no-python-format - offer_message = _(u'{banner_open} Upgrade by {discount_expiration_date} and save {percentage}% ' - u'[{strikeout_price}]{span_close}{br}Discount will be automatically applied at checkout. ' - u'{a_open}Upgrade Now{a_close}{div_close}') - return Fragment(HTML(offer_message).format( - a_open=HTML(u'').format( - upgrade_link=verified_upgrade_deadline_link(user=user, course=course) - ), - a_close=HTML(''), - br=HTML('
'), - banner_open=HTML( - '
' - ), - discount_expiration_date=discount_expiration_date.strftime(u'%B %d'), - percentage=discount_percentage(), - span_close=HTML(''), - div_close=HTML('
'), - strikeout_price=HTML(format_strikeout_price(user, course, check_for_discount=False)[0]) - )) - return None diff --git a/openedx/features/course_experience/views/course_home.py b/openedx/features/course_experience/views/course_home.py index aeace743fa..f56fe26066 100644 --- a/openedx/features/course_experience/views/course_home.py +++ b/openedx/features/course_experience/views/course_home.py @@ -30,7 +30,7 @@ from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from openedx.core.djangoapps.util.maintenance_banner import add_maintenance_banner from openedx.features.course_duration_limits.access import generate_course_expired_fragment from openedx.features.course_experience.course_tools import CourseToolsPluginManager -from openedx.features.course_experience.utils import get_first_purchase_offer_banner_fragment +from openedx.features.discounts.utils import get_first_purchase_offer_banner_fragment from openedx.features.discounts.utils import format_strikeout_price from student.models import CourseEnrollment from util.views import ensure_valid_course_key diff --git a/openedx/features/discounts/applicability.py b/openedx/features/discounts/applicability.py index 6807c01a25..4a5eac5cd9 100644 --- a/openedx/features/discounts/applicability.py +++ b/openedx/features/discounts/applicability.py @@ -49,6 +49,10 @@ def get_discount_expiration_date(user, course): Returns the date when the discount expires for the user. Returns none if the user is not enrolled. """ + # anonymous users should never get the discount + if user.is_anonymous: + return None + course_enrollment = CourseEnrollment.objects.filter( user=user, course=course.id, @@ -99,10 +103,6 @@ def can_receive_discount(user, course, discount_expiration_date=None): # TODO: Add additional conditions to return False here - # anonymous users should never get the discount - if user.is_anonymous: - return False - # Check if discount has expired if not discount_expiration_date: discount_expiration_date = get_discount_expiration_date(user, course) diff --git a/openedx/features/discounts/utils.py b/openedx/features/discounts/utils.py index bdd6bda86d..dcc70b6619 100644 --- a/openedx/features/discounts/utils.py +++ b/openedx/features/discounts/utils.py @@ -2,11 +2,43 @@ Utility functions for working with discounts and discounted pricing. """ +import six from django.utils.translation import ugettext as _ from course_modes.models import get_course_prices, format_course_price +from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangolib.markup import HTML +from web_fragments.fragment import Fragment +from openedx.features.discounts.applicability import ( + can_receive_discount, + get_discount_expiration_date, + discount_percentage +) -from .applicability import can_receive_discount, discount_percentage + +def offer_banner_wrapper(user, block, view, frag, context): # pylint: disable=W0613 + """ + A wrapper that prepends the First Purchase Discount banner if + the user hasn't upgraded yet. + """ + if block.category != "vertical": + return frag + + course = CourseOverview.get_from_id(block.course_id) + offer_banner_fragment = get_first_purchase_offer_banner_fragment(user, course) + + if not offer_banner_fragment: + return frag + + # Course content must be escaped to render correctly due to the way the + # way the XBlock rendering works. Transforming the safe markup to unicode + # escapes correctly. + offer_banner_fragment.content = six.text_type(offer_banner_fragment.content) + + offer_banner_fragment.add_content(frag.content) + offer_banner_fragment.add_fragment_resources(frag) + + return offer_banner_fragment def format_strikeout_price(user, course, base_price=None, check_for_discount=True): @@ -55,3 +87,35 @@ def format_strikeout_price(user, course, base_price=None, check_for_discount=Tru ) else: return (HTML(u"{}").format(original_price), False) + + +def get_first_purchase_offer_banner_fragment(user, course): + """ + Return an HTML Fragment with First Purcahse Discount message, + which has the discount_expiration_date, price, + discount percentage and a link to upgrade. + """ + if user and course: + discount_expiration_date = get_discount_expiration_date(user, course) + if (discount_expiration_date and + can_receive_discount(user=user, course=course, discount_expiration_date=discount_expiration_date)): + # Translator: xgettext:no-python-format + offer_message = _(u'{banner_open} Upgrade by {discount_expiration_date} and save {percentage}% ' + u'[{strikeout_price}]{span_close}{br}Discount will be automatically applied at checkout. ' + u'{a_open}Upgrade Now{a_close}{div_close}') + return Fragment(HTML(offer_message).format( + a_open=HTML(u'').format( + upgrade_link=verified_upgrade_deadline_link(user=user, course=course) + ), + a_close=HTML(''), + br=HTML('
'), + banner_open=HTML( + '
' + ), + discount_expiration_date=discount_expiration_date.strftime(u'%B %d'), + percentage=discount_percentage(), + span_close=HTML(''), + div_close=HTML('
'), + strikeout_price=HTML(format_strikeout_price(user, course, check_for_discount=False)[0]) + )) + return None