Commit d48100d3 by Marko Jevtic

[LEARNER-1367] Move Program Marketing page from private themes repo to edx platform repo

[LEARNER-311] Enable a purchase button on the Program page - platform (WL)
parent b68a4814
......@@ -1001,7 +1001,11 @@ class TestProgramMarketingView(SharedModuleStoreTestCase):
course_run = CourseRunFactory(key=unicode(modulestore_course.id)) # pylint: disable=no-member
course = CatalogCourseFactory(course_runs=[course_run])
cls.data = ProgramFactory(uuid=cls.program_uuid, courses=[course])
cls.data = ProgramFactory(
courses=[course],
is_program_eligible_for_one_click_purchase=False,
uuid=cls.program_uuid,
)
def test_404_if_no_data(self, mock_cache):
"""
......
......@@ -20,6 +20,7 @@ from django.db.models import Q
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, QueryDict
from django.shortcuts import redirect
from django.utils.decorators import method_decorator
from django.utils.text import slugify
from django.utils.timezone import UTC
from django.utils.translation import ugettext as _
from django.views.decorators.cache import cache_control
......@@ -832,13 +833,16 @@ def program_marketing(request, program_uuid):
raise Http404
program = ProgramMarketingDataExtender(program_data, request.user).extend()
program['type_slug'] = slugify(program['type'])
skus = program.get('skus')
ecommerce_service = EcommerceService()
return render_to_response('courseware/program_marketing.html', {
'buy_button_href': ecommerce_service.get_checkout_page_url(*skus) if skus else '#courses',
'program': program,
})
context = {'program': program}
if program.get('is_learner_eligible_for_one_click_purchase') and skus:
context['buy_button_href'] = ecommerce_service.get_checkout_page_url(*skus)
return render_to_response('courseware/program_marketing.html', context)
@transaction.non_atomic_requests
......
(function($) { // eslint-disable-line wrap-iife
'use strict';
$.fn.extend({
/*
* leanModal prepares an element to be a modal dialog. Call it once on the
* element that launches the dialog, when the page is ready. This function
* will add a .click() handler that properly opens the dialog.
*
* The launching element must:
* - be an <a> element, not a button,
* - have an href= attribute identifying the id of the dialog element,
* - have rel='leanModal'.
*/
leanModal: function(options) {
var defaults = {
top: 100,
overlay: 0.5,
closeButton: null,
position: 'fixed'
};
options = $.extend(defaults, options); // eslint-disable-line no-param-reassign
function closeModal(modalId, e) {
$('#lean_overlay').fadeOut(200);
$('iframe', modalId).attr('src', '');
$(modalId).css({display: 'none'});
if (modalId === '#modal_clone') {
$(modalId).remove();
}
e.preventDefault();
$(document).off('keydown.leanModal');
}
return this.each(function() {
var o = options;
$(this).click(function(e) {
var modalId = $(this).attr('href'),
modalClone, modalCloneHtml, notice, $notice;
$('.modal').hide();
if ($(modalId).hasClass('video-modal')) {
// Video modals need to be cloned before being presented as a modal
// This is because actions on the video get recorded in the history.
// Deleting the video (clone) prevents the odd back button behavior.
modalClone = $(modalId).clone(true, true);
modalClone.attr('id', 'modal_clone');
modalCloneHtml = edx.HtmlUtils.template(modalClone);
$(modalId).after(
edx.HtmlUtils.ensureHtml(modalCloneHtml).toString()
);
modalId = '#modal_clone';
}
$(document).on('keydown.leanModal', function(event) {
if (event.which === 27) {
closeModal(modalId, event);
}
});
$('#lean_overlay').click(function(ev) {
closeModal(modalId, ev);
});
$(o.closeButton).click(function(ev) {
closeModal(modalId, ev);
});
// To enable closing of email modal when copy button hit
$(o.copyEmailButton).click(function(ev) {
closeModal(modalId, ev);
});
$('#lean_overlay').css({display: 'block', opacity: 0});
$('#lean_overlay').fadeTo(200, o.overlay);
$('iframe', modalId).attr('src', $('iframe', modalId).data('src'));
if ($(modalId).hasClass('email-modal')) {
$(modalId).css({
width: 80 + '%',
height: 80 + '%',
position: o.position,
opacity: 0,
'z-index': 11000,
left: 10 + '%',
top: 10 + '%'
});
} else {
$(modalId).css({
position: o.position,
opacity: 0,
'z-index': 11000,
left: 50 + '%',
'margin-left': -($(modalId).outerWidth() / 2) + 'px',
top: o.top + 'px'
});
}
$(modalId).show().fadeTo(200, 1);
$(modalId).find('.notice').hide()
.html('');
notice = $(this).data('notice');
if (notice !== undefined) {
$notice = $(modalId).find('.notice');
$notice.show().text(notice);
// This is for activating leanModal links that were in the notice.
// We should have a cleaner way of allowing all dynamically added leanmodal links to work.
$notice.find('a[rel*=leanModal]').leanModal({
top: 120,
overlay: 1,
closeButton: '.close-modal',
position: 'absolute'
});
}
e.preventDefault();
});
});
}
});
$(document).ready(function($) { // eslint-disable-line no-shadow
$('button[rel*=leanModal]').each(function() {
var sep, embed;
$(this).leanModal({top: 120, overlay: 1, closeButton: '.close-modal', position: 'absolute'});
embed = $($(this).attr('href')).find('iframe');
if (embed.length > 0 && embed.attr('src')) {
sep = (embed.attr('src').indexOf('?') > 0) ? '&' : '?';
embed.data('src', embed.attr('src') + sep + 'autoplay=1&rel=0');
embed.attr('src', '');
}
});
});
})(jQuery);
function initializeCourseSlider() {
'use strict';
var isMobileResolution = $(window).width() <= 767,
sliderExists = $('.course-slider-xs').hasClass('slick-slider');
$('.course-card').toggleClass('slidable', isMobileResolution);
if (isMobileResolution) { // Second condition will avoid the multiple calls from resize
$('.copy-meta-mobile').show();
$('.copy-meta').hide();
if (!sliderExists) {
$('.course-slider-xs').slick({
arrows: false,
centerMode: true,
centerPadding: '40px',
slidesToShow: 1
});
}
} else {
$('.copy-meta').show();
$('.copy-meta-mobile').hide();
if (sliderExists) {
$('.course-slider-xs').slick('unslick');
$('.course-slider-xs').html();
$('.slick-arrow, .pageInfo').hide();
}
}
}
function paginate(page, size, total) {
'use strict';
var start = size * page,
end = (start + size - 1) >= total ? total - 1 : (start + size - 1);
$('.profile-item-desktop').each(function(index, item) {
if (index >= start && index <= end) {
$(item).css('display', 'block');
} else {
$(item).css('display', 'none');
}
});
$('.pagination-start').text(start + 1);
$('.pagination-end').text(end + 1);
}
$.fn.getFocusableChildren = function() {
'use strict';
return $(this)
/* eslint max-len: 0 */
.find('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object:not([disabled]), embed, *[tabindex], *[contenteditable]')
.filter(':visible');
};
$(document).ready(function() {
'use strict';
// Create MutationObserver which prevents the body of
// the page from scrolling when a modal window is displayed
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if ($(mutation.target).css('display') === 'block') {
$('body').css('overflow', 'hidden');
} else {
$('body').css('overflow', 'auto');
}
});
});
// Custom function showing current slide
var $status = $('.pagingInfo');
var $slickElement = $('.course-slider-xs');
// Instructor pagination
var page = 0,
size = 4,
total = parseInt($('.instructor-size').text(), 10),
maxPages = Math.ceil(total / size) - 1;
paginate(page, size, total);
initializeCourseSlider();
// In order to restrict focus, we added two pseudo <a> elements, one before the instructor modal and one after.
// When reaching the first <a>, we focus the last element in the dialog.
// If there is no focusable element, we focus the close button.
// When focusing the last <a>, we focus the first control in the dialog.
$('.focusKeeper:even').on('focus', function(event) {
event.preventDefault();
if ($(this).parent().find('.modal-body')
.getFocusableChildren().length) {
$(this).parent().find('.modal-body')
.getFocusableChildren()
.filter(':last')
.focus();
} else {
$(this).parent().find('.modal_close a')
.focus();
}
});
$('.focusKeeper:odd').on('focus', function(event) {
event.preventDefault();
$(this).parent().find('.modal_close a')
.focus();
});
$(window).resize(function() {
initializeCourseSlider();
});
// Initialize instructor bio modals
$('.instructor-image, .instructor-label').leanModal({closeButton: '.modal_close', top: '10%'});
$('.modal').each(function(index, element) {
observer.observe(element, {attributes: true, attributeFilter: ['style']});
});
$slickElement.on('init reInit afterChange', function(event, slick, currentSlide) {
// currentSlide is undefined on init -- set it to 0 in this case (currentSlide is 0 based)
var i = currentSlide || 1;
$status.text(i + ' of ' + slick.slideCount);
});
// Initialize FAQ
$('ul.faq-links-list li.item').click(function() {
if ($(this).find('.answer').hasClass('hidden')) {
$(this).find('.answer').removeClass('hidden');
$(this).addClass('expanded');
} else {
$(this).find('.answer').addClass('hidden');
$(this).removeClass('expanded');
}
});
if (page < maxPages) {
$('#pagination-next').addClass('active');
$('#pagination-next > span.sr').attr('aria-hidden', 'false');
}
$('#pagination-next').click(function() {
if (page === maxPages) {
return false;
}
if (page + 1 === maxPages) {
$(this).removeClass('active');
$(this).children('span.sr').attr('aria-hidden', 'true');
}
page = page + 1;
paginate(page, size, total);
$('#pagination-previous').addClass('active');
$('#pagination-previous > span.sr').attr('aria-hidden', 'false');
return false;
});
$('#pagination-previous').click(function() {
if (page === 0) {
return false;
}
if (page - 1 === 0) {
$(this).removeClass('active');
$(this).children('span.sr').attr('aria-hidden', 'true');
}
page = page - 1;
paginate(page, size, total);
$('#pagination-next').addClass('active');
$('#pagination-next > span.sr').attr('aria-hidden', 'false');
return false;
});
$('#accordion-group').accordion({
header: '> .accordion-item > .accordion-head',
collapsible: true,
active: false,
heightStyle: 'content'
});
});
......@@ -28,3 +28,6 @@
@import 'features/course-experience';
@import 'features/course-search';
@import 'features/course-sock';
// Views
@import "views/program-marketing-page";
......@@ -886,6 +886,18 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
self.assertEqual(data['full_program_price'], program_full_price)
self.assertEqual(data['avg_price_per_course'], program_full_price / self.number_of_courses)
def test_course_pricing_when_all_course_runs_have_no_seats(self):
course = ModuleStoreCourseFactory()
course = self.update_course(course, self.user.id)
course_run = CourseRunFactory(key=unicode(course.id), seats=[])
program = ProgramFactory(courses=[CourseFactory(course_runs=[course_run])])
data = ProgramMarketingDataExtender(program, self.user).extend()
self.assertEqual(data['number_of_courses'], len(program['courses']))
self.assertEqual(data['full_program_price'], 0.0)
self.assertEqual(data['avg_price_per_course'], 0.0)
@ddt.data(True, False)
@mock.patch(UTILS_MODULE + '.has_access')
def test_can_enroll(self, can_enroll, mock_has_access):
......
......@@ -504,9 +504,9 @@ class ProgramMarketingDataExtender(ProgramDataExtender):
self.instructors = {}
# Values for programs' price calculation.
self.data['avg_price_per_course'] = 0
self.data['avg_price_per_course'] = 0.0
self.data['number_of_courses'] = 0
self.data['full_program_price'] = 0
self.data['full_program_price'] = 0.0
def _extend_program(self):
"""Aggregates data from the program data structure."""
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment