Added Program Purchase button to the Programs dashboard
Learners can upgrade or enroll as verified in all remaining courses in a program from their programs dashboard [LEARNER-1899]
This commit is contained in:
committed by
McKenzie Welter
parent
679bd2c6a6
commit
de6d48a698
@@ -5,11 +5,12 @@ from django.http import Http404
|
||||
from django.views.decorators.http import require_GET
|
||||
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from commerce.utils import EcommerceService
|
||||
|
||||
from lms.djangoapps.learner_dashboard.utils import FAKE_COURSE_KEY, strip_course_id
|
||||
from openedx.core.djangoapps.catalog.utils import get_programs
|
||||
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.core.djangoapps.programs.utils import (
|
||||
ProgramDataExtender,
|
||||
ProgramMarketingDataExtender,
|
||||
ProgramProgressMeter,
|
||||
get_certificates,
|
||||
get_program_marketing_url
|
||||
@@ -54,11 +55,13 @@ def program_details(request, program_uuid):
|
||||
if not program_data:
|
||||
raise Http404
|
||||
|
||||
program_data = ProgramDataExtender(program_data, request.user).extend()
|
||||
program_data = ProgramMarketingDataExtender(program_data, request.user).extend()
|
||||
course_data = meter.progress(programs=[program_data], count_only=False)[0]
|
||||
certificate_data = get_certificates(request.user, program_data)
|
||||
|
||||
program_data.pop('courses')
|
||||
skus = program_data.get('skus')
|
||||
ecommerce_service = EcommerceService()
|
||||
|
||||
urls = {
|
||||
'program_listing_url': reverse('program_listing_view'),
|
||||
@@ -66,6 +69,7 @@ def program_details(request, program_uuid):
|
||||
reverse('course_modes_choose', kwargs={'course_id': FAKE_COURSE_KEY})
|
||||
),
|
||||
'commerce_api_url': reverse('commerce_api:v0:baskets:create'),
|
||||
'buy_button_url': ecommerce_service.get_checkout_page_url(*skus)
|
||||
}
|
||||
|
||||
context = {
|
||||
@@ -77,7 +81,7 @@ def program_details(request, program_uuid):
|
||||
'user_preferences': get_user_preferences(request.user),
|
||||
'program_data': program_data,
|
||||
'course_data': course_data,
|
||||
'certificate_data': certificate_data,
|
||||
'certificate_data': certificate_data
|
||||
}
|
||||
|
||||
return render_to_response('learner_dashboard/program_details.html', context)
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
|
||||
initialize: function(options) {
|
||||
this.options = options;
|
||||
|
||||
this.programModel = new Backbone.Model(this.options.programData);
|
||||
this.courseData = new Backbone.Model(this.options.courseData);
|
||||
this.certificateCollection = new Backbone.Collection(this.options.certificateData);
|
||||
@@ -60,7 +61,8 @@
|
||||
totalCount: totalCount,
|
||||
inProgressCount: inProgressCount,
|
||||
remainingCount: remainingCount,
|
||||
completedCount: completedCount
|
||||
completedCount: completedCount,
|
||||
completeProgramURL: this.options.urls.buy_button_url
|
||||
};
|
||||
data = $.extend(data, this.programModel.toJSON());
|
||||
HtmlUtils.setHtml(this.$el, this.tpl(data));
|
||||
|
||||
@@ -52,6 +52,14 @@ define([
|
||||
marketing_url: 'someurl',
|
||||
status: 'active',
|
||||
credit_redemption_overview: '',
|
||||
discount_data: {
|
||||
currency: 'USD',
|
||||
discount_value: 0,
|
||||
is_discounted: false,
|
||||
total_incl_tax: 300,
|
||||
total_incl_tax_excl_discounts: 300
|
||||
},
|
||||
full_program_price: 300,
|
||||
card_image_url: 'some image',
|
||||
faq: [],
|
||||
price_ranges: [
|
||||
@@ -117,7 +125,8 @@ define([
|
||||
credit_backing_organizations: [],
|
||||
weeks_to_complete_min: 8,
|
||||
weeks_to_complete_max: 8,
|
||||
min_hours_effort_per_week: null
|
||||
min_hours_effort_per_week: null,
|
||||
is_learner_eligible_for_one_click_purchase: false
|
||||
},
|
||||
courseData: {
|
||||
completed: [
|
||||
@@ -549,7 +558,42 @@ define([
|
||||
view.render();
|
||||
expect($(view.$('.upgrade-message .card-msg')).text().trim()).toEqual('Certificate Status:');
|
||||
expect($(view.$('.upgrade-message .price')).text().trim()).toEqual('$10.00');
|
||||
expect($(view.$('.upgrade-button')[0]).text().trim()).toEqual('Buy Certificate');
|
||||
expect($(view.$('.upgrade-button.single-course-run')[0]).text().trim()).toEqual('Upgrade to Verified');
|
||||
});
|
||||
|
||||
it('should render full program purchase link', function() {
|
||||
view = initView({
|
||||
programData: $.extend({}, options.programData, {
|
||||
is_learner_eligible_for_one_click_purchase: true
|
||||
})
|
||||
});
|
||||
view.render();
|
||||
expect($(view.$('.upgrade-button.complete-program')).text().trim().
|
||||
replace(/\s+/g, ' ')).
|
||||
toEqual(
|
||||
'Upgrade All Remaining Courses ( $300 USD )'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render partial program purchase link', function() {
|
||||
view = initView({
|
||||
programData: $.extend({}, options.programData, {
|
||||
is_learner_eligible_for_one_click_purchase: true,
|
||||
discount_data: {
|
||||
currency: 'USD',
|
||||
discount_value: 30,
|
||||
is_discounted: true,
|
||||
total_incl_tax: 300,
|
||||
total_incl_tax_excl_discounts: 270
|
||||
}
|
||||
})
|
||||
});
|
||||
view.render();
|
||||
expect($(view.$('.upgrade-button.complete-program')).text().trim().
|
||||
replace(/\s+/g, ' ')).
|
||||
toEqual(
|
||||
'Upgrade All Remaining Courses ( $270 $300 USD )'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render enrollment information', function() {
|
||||
|
||||
@@ -285,8 +285,10 @@
|
||||
}
|
||||
|
||||
.program-heading {
|
||||
width: 100%;
|
||||
margin-bottom: 40px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-direction: column;
|
||||
|
||||
.program-heading-title {
|
||||
font-family: "Open Sans";
|
||||
@@ -300,6 +302,7 @@
|
||||
.program-heading-message {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.course-enroll-view {
|
||||
@@ -387,7 +390,34 @@
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.upgrade-button {
|
||||
background: palette(success, text);
|
||||
border-color: palette(success, text);
|
||||
border-radius: 0;
|
||||
padding: 7px;
|
||||
text-align: center;
|
||||
font-size: 0.9375em;
|
||||
|
||||
/* IE11 CSS styles */
|
||||
@media(min-width: $bp-screen-md) and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
||||
@include float(right);
|
||||
}
|
||||
&.complete-program {
|
||||
margin: 10px 15px 10px 5px;
|
||||
align-self: flex-start;
|
||||
|
||||
@media(min-width: $bp-screen-md) {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.list-price {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.program-course-card {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
@@ -462,22 +492,6 @@
|
||||
.upgrade-message {
|
||||
flex-wrap: wrap;
|
||||
|
||||
.upgrade-button {
|
||||
background: palette(success, text);
|
||||
border-color: palette(success, text);
|
||||
height: 37px;
|
||||
width: 128px;
|
||||
border-radius: 0;
|
||||
padding: 7px 0 0 0;
|
||||
text-align: center;
|
||||
font-size: 0.9375em;
|
||||
|
||||
/* IE11 CSS styles */
|
||||
@media(min-width: $bp-screen-md) and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
||||
@include float(right);
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
width: 100%;
|
||||
margin: 5px 0;
|
||||
|
||||
@@ -20,6 +20,23 @@
|
||||
<div><%- gettext('To complete the program, you must earn a verified certificate for each course.') %></div>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if (is_learner_eligible_for_one_click_purchase) { %>
|
||||
<a href="<%- completeProgramURL %>" class="btn-brand btn cta-primary upgrade-button complete-program">
|
||||
<%- gettext('Upgrade All Remaining Courses (')%>
|
||||
<% if (discount_data.is_discounted) { %>
|
||||
<span class='list-price'>
|
||||
<%- StringUtils.interpolate(
|
||||
gettext('${listPrice}'), {listPrice: discount_data.total_incl_tax_excl_discounts}
|
||||
)
|
||||
%>
|
||||
</span>
|
||||
<% } %>
|
||||
<%- StringUtils.interpolate(
|
||||
gettext(' ${price} {currency} )'), {price: full_program_price, currency: discount_data.currency}
|
||||
)
|
||||
%>
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="course-list-headings">
|
||||
<% if (inProgressCount) { %>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<span class="price"> <%- price %></span>
|
||||
</div>
|
||||
<div class="action col-12 md-col-4">
|
||||
<a href="<%- upgrade_url %>" class="btn-brand btn cta-primary upgrade-button">
|
||||
<%- gettext('Buy Certificate') %>
|
||||
<a href="<%- upgrade_url %>" class="btn-brand btn cta-primary upgrade-button single-course-run">
|
||||
<%- gettext('Upgrade to Verified') %>
|
||||
<a>
|
||||
</div>
|
||||
|
||||
@@ -846,7 +846,8 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
|
||||
self.course_price = 100
|
||||
self.number_of_courses = 2
|
||||
self.program = ProgramFactory(
|
||||
courses=[self._create_course(self.course_price) for __ in range(self.number_of_courses)]
|
||||
courses=[self._create_course(self.course_price) for __ in range(self.number_of_courses)],
|
||||
applicable_seat_types=['verified']
|
||||
)
|
||||
|
||||
def _create_course(self, course_price):
|
||||
@@ -940,7 +941,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
|
||||
"""
|
||||
Learner should be eligible for one click purchase if:
|
||||
- program is eligible for one click purchase
|
||||
- learner is not enrolled in any of the course runs associated with the program
|
||||
- There are courses remaining that have not been purchased and enrolled in.
|
||||
"""
|
||||
data = ProgramMarketingDataExtender(self.program, self.user).extend()
|
||||
self.assertTrue(data['is_learner_eligible_for_one_click_purchase'])
|
||||
@@ -954,14 +955,17 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
|
||||
data = ProgramMarketingDataExtender(program, self.user).extend()
|
||||
self.assertFalse(data['is_learner_eligible_for_one_click_purchase'])
|
||||
|
||||
course = self._create_course(self.course_price)
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course['course_runs'][0]['key'])
|
||||
course1 = self._create_course(self.course_price)
|
||||
course2 = self._create_course(self.course_price)
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course1['course_runs'][0]['key'], mode='verified')
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course2['course_runs'][0]['key'], mode='audit')
|
||||
program2 = ProgramFactory(
|
||||
courses=[course],
|
||||
is_program_eligible_for_one_click_purchase=True
|
||||
courses=[course1, course2],
|
||||
is_program_eligible_for_one_click_purchase=True,
|
||||
applicable_seat_types=['verified'],
|
||||
)
|
||||
data = ProgramMarketingDataExtender(program2, self.user).extend()
|
||||
self.assertFalse(data['is_learner_eligible_for_one_click_purchase'])
|
||||
self.assertTrue(data['is_learner_eligible_for_one_click_purchase'])
|
||||
|
||||
def test_multiple_published_course_runs(self):
|
||||
"""
|
||||
@@ -993,7 +997,8 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
|
||||
)
|
||||
])
|
||||
],
|
||||
is_program_eligible_for_one_click_purchase=True
|
||||
is_program_eligible_for_one_click_purchase=True,
|
||||
applicable_seat_types=['verified']
|
||||
)
|
||||
data = ProgramMarketingDataExtender(program, self.user).extend()
|
||||
|
||||
@@ -1065,6 +1070,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
|
||||
"""
|
||||
User shouldn't be able to do a one click purchase of a program if a program has no applicable seat types.
|
||||
"""
|
||||
self.program['applicable_seat_types'] = []
|
||||
data = ProgramMarketingDataExtender(self.program, self.user).extend()
|
||||
|
||||
self.assertEqual(len(data['skus']), 0)
|
||||
|
||||
@@ -600,10 +600,17 @@ class ProgramMarketingDataExtender(ProgramDataExtender):
|
||||
skus = []
|
||||
if is_learner_eligible_for_one_click_purchase:
|
||||
for course in self.data['courses']:
|
||||
is_learner_eligible_for_one_click_purchase = not any(
|
||||
course_run['is_enrolled'] for course_run in course['course_runs']
|
||||
)
|
||||
if is_learner_eligible_for_one_click_purchase:
|
||||
add_course_sku = False
|
||||
for course_run in course['course_runs']:
|
||||
(enrollment_mode, active) = CourseEnrollment.enrollment_mode_for_user(
|
||||
self.user,
|
||||
CourseKey.from_string(course_run['key'])
|
||||
)
|
||||
if enrollment_mode not in applicable_seat_types or not active:
|
||||
add_course_sku = True
|
||||
break
|
||||
|
||||
if add_course_sku:
|
||||
published_course_runs = filter(lambda run: run['status'] == 'published', course['course_runs'])
|
||||
if len(published_course_runs) == 1:
|
||||
for seat in published_course_runs[0]['seats']:
|
||||
@@ -615,15 +622,16 @@ class ProgramMarketingDataExtender(ProgramDataExtender):
|
||||
is_learner_eligible_for_one_click_purchase = False
|
||||
skus = []
|
||||
break
|
||||
else:
|
||||
skus = []
|
||||
break
|
||||
|
||||
if skus:
|
||||
try:
|
||||
User = get_user_model()
|
||||
service_user = User.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
|
||||
api = ecommerce_api_client(service_user)
|
||||
api_user = self.user
|
||||
if not self.user.is_authenticated():
|
||||
user = get_user_model()
|
||||
service_user = user.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
|
||||
api_user = service_user
|
||||
|
||||
api = ecommerce_api_client(api_user)
|
||||
|
||||
# Make an API call to calculate the discounted price
|
||||
discount_data = api.baskets.calculate.get(sku=skus)
|
||||
@@ -639,6 +647,8 @@ class ProgramMarketingDataExtender(ProgramDataExtender):
|
||||
})
|
||||
except (ConnectionError, SlumberBaseException, Timeout):
|
||||
log.exception('Failed to get discount price for following product SKUs: %s ', ', '.join(skus))
|
||||
else:
|
||||
is_learner_eligible_for_one_click_purchase = False
|
||||
|
||||
self.data.update({
|
||||
'is_learner_eligible_for_one_click_purchase': is_learner_eligible_for_one_click_purchase,
|
||||
|
||||
Reference in New Issue
Block a user