Commit 099d02d1 by chrisndodge

Merge pull request #6124 from edx/muhhshoaib/WL-164

Unpurchased items in the shoppingcart should be removed if the enrollment period has closed
parents b0eba8a6 829f7dba
......@@ -770,6 +770,17 @@ class CourseEnrollment(models.Model):
return enrollment_number
@classmethod
def is_enrollment_closed(cls, user, course):
"""
Returns a boolean value regarding whether the user has access to enroll in the course. Returns False if the
enrollment has been closed.
"""
# Disable the pylint error here, as per ormsbee. This local import was previously
# in CourseEnrollment.enroll
from courseware.access import has_access # pylint: disable=import-error
return not has_access(user, 'enroll', course)
@classmethod
def is_course_full(cls, course):
"""
Returns a boolean value regarding whether a course has already reached it's max enrollment
......@@ -904,8 +915,6 @@ class CourseEnrollment(models.Model):
Also emits relevant events for analytics purposes.
"""
from courseware.access import has_access
# All the server-side checks for whether a user is allowed to enroll.
try:
course = modulestore().get_course(course_key)
......@@ -921,7 +930,7 @@ class CourseEnrollment(models.Model):
if check_access:
if course is None:
raise NonExistentCourseError
if not has_access(user, 'enroll', course):
if CourseEnrollment.is_enrollment_closed(user, course):
log.warning(
"User {0} failed to enroll in course {1} because enrollment is closed".format(
user.username,
......
......@@ -152,6 +152,15 @@ class Order(models.Model):
return False
@classmethod
def remove_cart_item_from_order(cls, item):
"""
Removes the item from the cart if the item.order.status == 'cart'.
"""
if item.order.status == 'cart':
log.info("Item {0} removed from the user cart".format(item.id))
item.delete()
@property
def total_cost(self):
"""
......
"""
Tests for Shopping Cart views
"""
import pytz
from urlparse import urlparse
from django.http import HttpRequest
......@@ -1104,6 +1105,120 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
@override_settings(MODULESTORE=MODULESTORE_CONFIG)
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
class ShoppingcartViewsClosedEnrollment(ModuleStoreTestCase):
"""
Test suite for ShoppingcartViews Course Enrollments Closed or not
"""
def setUp(self):
super(ShoppingcartViewsClosedEnrollment, self).setUp()
self.user = UserFactory.create()
self.user.set_password('password')
self.user.save()
self.instructor = AdminFactory.create()
self.cost = 40
self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course')
self.course_key = self.course.id
self.course_mode = CourseMode(course_id=self.course_key,
mode_slug="honor",
mode_display_name="honor cert",
min_price=self.cost)
self.course_mode.save()
self.testing_course = CourseFactory.create(
org='Edx',
number='999',
display_name='Testing Super Course',
metadata={"invitation_only": False}
)
self.course_mode = CourseMode(course_id=self.testing_course.id,
mode_slug="honor",
mode_display_name="honor cert",
min_price=self.cost)
self.course_mode.save()
self.cart = Order.get_cart_for_user(self.user)
self.now = datetime.now(pytz.UTC)
self.tomorrow = self.now + timedelta(days=1)
self.nextday = self.tomorrow + timedelta(days=1)
def login_user(self):
"""
Helper fn to login self.user
"""
self.client.login(username=self.user.username, password="password")
@patch('shoppingcart.views.render_to_response', render_mock)
def test_to_check_that_cart_item_enrollment_is_closed(self):
self.login_user()
reg_item1 = PaidCourseRegistration.add_to_order(self.cart, self.course_key)
PaidCourseRegistration.add_to_order(self.cart, self.testing_course.id)
# update the testing_course enrollment dates
self.testing_course.enrollment_start = self.tomorrow
self.testing_course.enrollment_end = self.nextday
self.testing_course = self.update_course(self.testing_course, self.user.id)
# testing_course enrollment is closed but the course is in the cart
# so we delete that item from the cart and display the message in the cart
resp = self.client.get(reverse('shoppingcart.views.show_cart', args=[]))
self.assertEqual(resp.status_code, 200)
self.assertIn("{course_name} has been removed because the enrollment period has closed.".format(course_name=self.testing_course.display_name), resp.content)
((template, context), _tmp) = render_mock.call_args
self.assertEqual(template, 'shoppingcart/shopping_cart.html')
self.assertEqual(context['order'], self.cart)
self.assertIn(reg_item1, context['shoppingcart_items'][0])
self.assertEqual(1, len(context['shoppingcart_items']))
self.assertEqual(True, context['is_course_enrollment_closed'])
self.assertIn(self.testing_course.display_name, context['appended_expired_course_names'])
def test_to_check_that_cart_item_enrollment_is_closed_when_clicking_the_payment_button(self):
self.login_user()
PaidCourseRegistration.add_to_order(self.cart, self.course_key)
PaidCourseRegistration.add_to_order(self.cart, self.testing_course.id)
# update the testing_course enrollment dates
self.testing_course.enrollment_start = self.tomorrow
self.testing_course.enrollment_end = self.nextday
self.testing_course = self.update_course(self.testing_course, self.user.id)
# testing_course enrollment is closed but the course is in the cart
# so we delete that item from the cart and display the message in the cart
resp = self.client.get(reverse('shoppingcart.views.verify_cart'))
self.assertEqual(resp.status_code, 200)
self.assertTrue(json.loads(resp.content)['is_course_enrollment_closed'])
resp = self.client.get(reverse('shoppingcart.views.show_cart', args=[]))
self.assertEqual(resp.status_code, 200)
self.assertIn("{course_name} has been removed because the enrollment period has closed.".format(course_name=self.testing_course.display_name), resp.content)
self.assertIn('40.00', resp.content)
def test_is_enrollment_closed_when_order_type_is_business(self):
self.login_user()
self.cart.order_type = 'business'
self.cart.save()
PaidCourseRegistration.add_to_order(self.cart, self.course_key)
CourseRegCodeItem.add_to_order(self.cart, self.testing_course.id, 2)
# update the testing_course enrollment dates
self.testing_course.enrollment_start = self.tomorrow
self.testing_course.enrollment_end = self.nextday
self.testing_course = self.update_course(self.testing_course, self.user.id)
resp = self.client.post(reverse('shoppingcart.views.billing_details'))
self.assertEqual(resp.status_code, 200)
self.assertTrue(json.loads(resp.content)['is_course_enrollment_closed'])
# testing_course enrollment is closed but the course is in the cart
# so we delete that item from the cart and display the message in the cart
resp = self.client.get(reverse('shoppingcart.views.show_cart', args=[]))
self.assertEqual(resp.status_code, 200)
self.assertIn("{course_name} has been removed because the enrollment period has closed.".format(course_name=self.testing_course.display_name), resp.content)
self.assertIn('40.00', resp.content)
@override_settings(MODULESTORE=MODULESTORE_CONFIG)
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase):
"""
Test suite for RegistrationCodeRedemption Course Enrollments
......
......@@ -16,6 +16,7 @@ urlpatterns = patterns('shoppingcart.views', # nopep8
url(r'^update_user_cart/$', 'update_user_cart'),
url(r'^reset_code_redemption/$', 'reset_code_redemption'),
url(r'^billing_details/$', 'billing_details', name='billing_details'),
url(r'^verify_cart/$', 'verify_cart'),
url(r'^register_courses/$', 'register_courses'),
)
......
......@@ -148,25 +148,27 @@ def show_cart(request):
This view shows cart items.
"""
cart = Order.get_cart_for_user(request.user)
total_cost = cart.total_cost
cart_items = cart.orderitem_set.all().select_subclasses()
shoppingcart_items = []
for cart_item in cart_items:
course_key = getattr(cart_item, 'course_id')
if course_key:
course = get_course_by_id(course_key, depth=0)
shoppingcart_items.append((cart_item, course))
is_any_course_expired, expired_cart_items, expired_cart_item_names, valid_cart_item_tuples = \
verify_for_closed_enrollment(request.user, cart)
site_name = microsite.get_value('SITE_NAME', settings.SITE_NAME)
if is_any_course_expired:
for expired_item in expired_cart_items:
Order.remove_cart_item_from_order(expired_item)
cart.update_order_type()
appended_expired_course_names = ", ".join(expired_cart_item_names)
callback_url = request.build_absolute_uri(
reverse("shoppingcart.views.postpay_callback")
)
form_html = render_purchase_form_html(cart, callback_url=callback_url)
context = {
'order': cart,
'shoppingcart_items': shoppingcart_items,
'amount': total_cost,
'shoppingcart_items': valid_cart_item_tuples,
'amount': cart.total_cost,
'is_course_enrollment_closed': is_any_course_expired,
'appended_expired_course_names': appended_expired_course_names,
'site_name': site_name,
'form_html': form_html,
'currency_symbol': settings.PAID_COURSE_REGISTRATION_CURRENCY[1],
......@@ -559,8 +561,7 @@ def billing_details(request):
"""
cart = Order.get_cart_for_user(request.user)
cart_items = cart.orderitem_set.all()
cart_items = cart.orderitem_set.all().select_subclasses()
if getattr(cart, 'order_type') != OrderTypes.BUSINESS:
raise Http404('Page not found!')
......@@ -589,11 +590,65 @@ def billing_details(request):
cart.add_billing_details(company_name, company_contact_name, company_contact_email, recipient_name,
recipient_email, customer_reference_number)
is_any_course_expired, __, __, __ = verify_for_closed_enrollment(request.user)
return JsonResponse({
'response': _('success')
'response': _('success'),
'is_course_enrollment_closed': is_any_course_expired
}) # status code 200: OK by default
def verify_for_closed_enrollment(user, cart=None):
"""
A multi-output helper function.
inputs:
user: a user object
cart: If a cart is provided it uses the same object, otherwise fetches the user's cart.
Returns:
is_any_course_expired: True if any of the items in the cart has it's enrollment period closed. False otherwise.
expired_cart_items: List of courses with enrollment period closed.
expired_cart_item_names: List of names of the courses with enrollment period closed.
valid_cart_item_tuples: List of courses which are still open for enrollment.
"""
if cart is None:
cart = Order.get_cart_for_user(user)
expired_cart_items = []
expired_cart_item_names = []
valid_cart_item_tuples = []
cart_items = cart.orderitem_set.all().select_subclasses()
is_any_course_expired = False
for cart_item in cart_items:
course_key = getattr(cart_item, 'course_id', None)
if course_key is not None:
course = get_course_by_id(course_key, depth=0)
if CourseEnrollment.is_enrollment_closed(user, course):
is_any_course_expired = True
expired_cart_items.append(cart_item)
expired_cart_item_names.append(course.display_name)
else:
valid_cart_item_tuples.append((cart_item, course))
return is_any_course_expired, expired_cart_items, expired_cart_item_names, valid_cart_item_tuples
@require_http_methods(["GET"])
@login_required
@enforce_shopping_cart_enabled
def verify_cart(request):
"""
Called when the user clicks the button to transfer control to CyberSource.
Returns a JSON response with is_course_enrollment_closed set to True if any of the courses has its
enrollment period closed. If all courses are still valid, is_course_enrollment_closed set to False.
"""
is_any_course_expired, __, __, __ = verify_for_closed_enrollment(request.user)
return JsonResponse(
{
'is_course_enrollment_closed': is_any_course_expired
}
) # status code 200: OK by default
@login_required
def show_receipt(request, ordernum):
"""
......
var edx = edx || {};
(function($) {
'use strict';
edx.shoppingcart = edx.shoppingcart || {};
edx.shoppingcart.showcart = {};
/**
* View for making shoppingcart
* @constructor
* @param {Object} params
* @param {Object} params.el - The payment form element.
*/
edx.shoppingcart.showcart.CartView = function(params) {
/**
* cart view that checks that all the cart items are valid (course enrollment is closed or not)
* before the form submitted to the payment processor.
* @param {Object} form - The form to modify.
*/
/**
* Check for all the cart items are still valid (courses enrollment are not closed)
*
* @returns {Object} The promise from the AJAX call to the server,
* which checks for cart items are valid or not and returns the boolean
* { is_course_enrollment_closed: <boolead> }
*/
var isCourseEnrollmentAllowed = function() {
return $.ajax({
url: "/shoppingcart/verify_cart/",
type: "GET"
});
};
var view = {
/**
* Initialize the view.
*
* @param {Object} params
* @param {JQuery selector} params.el - The payment form element.
* @returns {CartView}
*/
initialize: function(params) {
this.$el = params.el;
_.bindAll(view,
'submit', 'responseFromServer',
'submitPaymentForm', 'errorFromServer'
);
return this;
},
/**
* Handle a click event on the "payment form submit" button.
* This will contact the LMS server to check for all the
* valid cart items (courses enrollment should not be closed at this point)
* then send the user to the external payment processor or redirects to the
* dashboard page
*
* @param {Object} event - The click event.
*/
submit: function(event) {
// Prevent form submission
if (event) {
event.preventDefault();
}
// Immediately disable the submit button to prevent duplicate submissions
this.$el.find('input[type="submit"]').addClass("disabled");
this.$paymentForm = this.$el;
isCourseEnrollmentAllowed()
.done(this.responseFromServer)
.fail(this.errorFromServer);
return this;
},
/**
* Send signed payment parameters to the external
* payment processor if cart items are valid else redirect to
* shoppingcart.
*
* @param {boolean} data.is_course_enrollment_closed
*/
responseFromServer: function(data) {
if (data.is_course_enrollment_closed == true) {
location.href = "/shoppingcart";
}
else {
this.submitPaymentForm(this.$paymentForm);
}
},
/**
* In case the server responded back with errors
*
*/
errorFromServer: function() {
// Immediately enable the submit button to allow submission
this.$el.find('input[type="submit"]').removeClass("disabled");
},
/**
* Submit the payment from to the external payment processor.
*
* @param {Object} form
*/
submitPaymentForm: function(form) {
form.submit();
}
};
view.initialize(params);
return view;
};
$(document).ready(function() {
// (click on the payment submit button).
$('.cart-view form input[type="submit"]').click(function(event) {
var container = $('.confirm-enrollment.cart-view form');
var view = new edx.shoppingcart.showcart.CartView({
el:container
}).submit(event);
});
});
})(jQuery);
\ No newline at end of file
......@@ -261,6 +261,10 @@
exports: 'js/dashboard/donation',
deps: ['jquery', 'underscore', 'gettext']
},
'js/shoppingcart/shoppingcart.js': {
exports: 'js/shoppingcart/shoppingcart',
deps: ['jquery', 'underscore', 'gettext']
},
// Backbone classes loaded explicitly until they are converted to use RequireJS
'js/models/cohort': {
......@@ -382,6 +386,7 @@
'lms/include/js/spec/staff_debug_actions_spec.js',
'lms/include/js/spec/views/notification_spec.js',
'lms/include/js/spec/dashboard/donation.js',
'lms/include/js/spec/shoppingcart/shoppingcart_spec.js',
'lms/include/js/spec/student_account/account_spec.js',
'lms/include/js/spec/student_account/access_spec.js',
'lms/include/js/spec/student_account/login_spec.js',
......
define(['js/common_helpers/ajax_helpers', 'js/shoppingcart/shoppingcart'],
function(AjaxHelpers) {
'use strict';
describe("edx.shoppingcart.showcart.CartView", function() {
var view = null;
var requests = null;
beforeEach(function() {
setFixtures('<section class="wrapper confirm-enrollment shopping-cart cart-view"><form action="" method="post"><input type="hidden" name="" value="" /><i class="icon-caret-right"></i><input type="submit" value="Payment"/></form></section>');
view = new edx.shoppingcart.showcart.CartView({
el: $('.confirm-enrollment.cart-view form')
});
spyOn(view, 'responseFromServer').andCallFake(function() {});
// Spy on AJAX requests
requests = AjaxHelpers.requests(this);
view.submit();
// Verify that the client contacts the server to
// check for all th valid cart items
AjaxHelpers.expectRequest(
requests, "GET", "/shoppingcart/verify_cart/"
);
});
it("cart has invalid items, course enrollment has been closed", function() {
// Simulate a response from the server containing the
// parameter 'is_course_enrollment_closed'. This decides that
// do we have all the cart items valid in the cart or not
AjaxHelpers.respondWithJson(requests, {
is_course_enrollment_closed: true
});
expect(view.responseFromServer).toHaveBeenCalled();
var data = view.responseFromServer.mostRecentCall.args[0]
expect(data.is_course_enrollment_closed).toBe(true);
});
it("cart has all valid items, course enrollment is still open", function() {
// Simulate a response from the server containing the
// parameter 'is_course_enrollment_closed'. This decides that
// do we have all the cart items valid in the cart or not
AjaxHelpers.respondWithJson(requests, {
is_course_enrollment_closed: false
});
expect(view.responseFromServer).toHaveBeenCalled();
var data = view.responseFromServer.mostRecentCall.args[0]
expect(data.is_course_enrollment_closed).toBe(false);
});
});
}
);
\ No newline at end of file
......@@ -171,6 +171,15 @@
}
}
}
#expiry-msg {
padding: 15px;
background-color: #f2f2f2;
margin-top: 3px;
font-family: $sans-serif;
font-size: 14px;
text-shadow: 0px 1px 1px #fff;
border-top: 1px solid #f0f0f0;
}
.confirm-enrollment {
.title {
font-size:24px;
......@@ -885,6 +894,14 @@
text-align: center;
margin-top: 20px;
text-transform: initial;
margin-bottom: 5px;
}
p {
font-size: 14px;
font-family: $sans-serif;
color: #9d9d9d;
text-align: center;
text-shadow: 0px 1px 1px #fff;
}
a.blue{
display: inline-block;
......
......@@ -8,7 +8,7 @@
<%block name="custom_content">
<div class="container">
% if shoppingcart_items:
<section class="confirm-enrollment shopping-cart">
<section class="confirm-enrollment shopping-cart billing-details-view">
<h3>${_('You can proceed to payment at any point in time. Any additional information you provide will be included in your receipt.')}</h3>
<div class="billing-data">
<div class="col-half">
......@@ -93,6 +93,8 @@
return false;
}
event.preventDefault();
// Disable the submit button to prevent duplicate submissions
$(this).addClass("disabled");
var post_url = "${reverse('billing_details')}";
var data = {
"company_name" : $('input[name="company_name"]').val(),
......@@ -104,10 +106,15 @@
};
$.post(post_url, data)
.success(function(data) {
payment_form.submit();
})
if (data.is_course_enrollment_closed == true) {
location.href = "${reverse('shoppingcart.views.show_cart')}";
}
else {
payment_form.submit();
}
})
.error(function(data,status) {
$(this).removeClass("disabled");
})
});
});
......
......@@ -21,13 +21,15 @@ from django.utils.translation import ugettext as _
% endif
</%block>
% if is_course_enrollment_closed:
<p id="expiry-msg">${_('{course_names} has been removed because the enrollment period has closed.').format(course_names=appended_expired_course_names)}</p>
% endif:
<%
discount_applied = False
order_type = 'personal'
%>
<section class="wrapper confirm-enrollment shopping-cart">
<section class="wrapper confirm-enrollment shopping-cart cart-view">
% for item, course in shoppingcart_items:
% if loop.index > 0 :
<hr>
......@@ -135,7 +137,10 @@ from django.utils.translation import ugettext as _
% else:
<div class="empty-cart" >
<h2>${_('Your Shopping cart is currently empty.')}</h2>
<a href="${marketing_link('COURSES')}" class="blue">${_('View Courses')}</a>
% if is_course_enrollment_closed:
<p>${_('{course_names} has been removed because the enrollment period has closed.').format(course_names=appended_expired_course_names)}</p>
% endif
<a href="${marketing_link('COURSES')}" class="blue">${_('View Courses')}</a>
</div>
% endif
......@@ -146,7 +151,7 @@ from django.utils.translation import ugettext as _
var isSpinnerBtnEnabled = true;
var prevQty = 0;
$('a.btn-remove').click(function(event) {
$('a.btn-remove').click(function(event) {
event.preventDefault();
var post_url = "${reverse('shoppingcart.views.remove_item')}";
$.post(post_url, {id:$(this).data('item-id')})
......
......@@ -4,10 +4,11 @@ from django.utils.translation import ugettext as _
<%inherit file="../main.html" />
<%namespace name='static' file='/static_content.html'/>
<%block name="pagetitle">${_("Shopping cart")}</%block>
<%! from django.conf import settings %>
<%! from microsite_configuration import microsite %>
<%block name="headextra">
<script type="text/javascript" src="${static.url('js/shoppingcart/shoppingcart.js')}"></script>
</%block>
<%block name="bodyextra">
<div class="container">
......
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