diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index 898d2cc0b9..14b5eedf1a 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -289,9 +289,15 @@ class DashboardTest(ModuleStoreTestCase): resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': course_reg_code.code}) self.assertEqual(resp.status_code, 200) - # freely enroll the user into course - resp = self.client.get(reverse('shoppingcart.views.register_courses')) - self.assertIn('success', resp.content) + redeem_url = reverse('register_code_redemption', args=[course_reg_code.code]) + response = self.client.get(redeem_url) + self.assertEquals(response.status_code, 200) + # check button text + self.assertTrue('Activate Course Enrollment' in response.content) + + #now activate the user by enrolling him/her to the course + response = self.client.post(redeem_url) + self.assertEquals(response.status_code, 200) response = self.client.get(reverse('dashboard')) self.assertIn('You can no longer access this course because payment has not yet been received', response.content) diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index 72e9961f79..208a62c8d9 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -1862,30 +1862,6 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa for res in res_json['sale']: self.validate_sale_records_response(res, course_registration_code, self.sale_invoice_1, 0) - def test_get_sale_records_features_with_used_code(self): - """ - Test that the response from get_sale_records is in json format and using one of the registration codes. - """ - for i in range(5): - course_registration_code = CourseRegistrationCode( - code='qwerty{}'.format(i), course_id=self.course.id.to_deprecated_string(), - created_by=self.instructor, invoice=self.sale_invoice_1 - ) - course_registration_code.save() - - PaidCourseRegistration.add_to_order(self.cart, self.course.id) - - # now using registration code - self.client.post(reverse('shoppingcart.views.use_code'), {'code': 'qwerty0'}) - - url = reverse('get_sale_records', kwargs={'course_id': self.course.id.to_deprecated_string()}) - response = self.client.get(url, {}) - res_json = json.loads(response.content) - self.assertIn('sale', res_json) - - for res in res_json['sale']: - self.validate_sale_records_response(res, course_registration_code, self.sale_invoice_1, 1) - def test_get_sale_records_features_with_multiple_invoices(self): """ Test that the response from get_sale_records is in json format for multiple invoices @@ -3038,7 +3014,8 @@ class TestCourseRegistrationCodes(ModuleStoreTestCase): for i in range(5): i += 1 registration_code_redemption = RegistrationCodeRedemption( - order_id=i, registration_code_id=i, redeemed_by=self.instructor + registration_code_id=i, + redeemed_by=self.instructor ) registration_code_redemption.save() @@ -3220,7 +3197,8 @@ class TestCourseRegistrationCodes(ModuleStoreTestCase): for i in range(9): i += 13 registration_code_redemption = RegistrationCodeRedemption( - order_id=i, registration_code_id=i, redeemed_by=self.instructor + registration_code_id=i, + redeemed_by=self.instructor ) registration_code_redemption.save() diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index f236925255..60dba8d13a 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -798,8 +798,6 @@ def get_sale_order_records(request, course_id): # pylint: disable=unused-argume ('company_contact_name', 'Company Contact Name'), ('company_contact_email', 'Company Contact Email'), ('total_amount', 'Total Amount'), - ('total_codes', 'Total Codes'), - ('total_used_codes', 'Total Used Codes'), ('logged_in_username', 'Login Username'), ('logged_in_email', 'Login User Email'), ('purchase_time', 'Date of Sale'), @@ -817,8 +815,6 @@ def get_sale_order_records(request, course_id): # pylint: disable=unused-argume ('coupon_code', 'Coupon Code'), ('unit_cost', 'Unit Price'), ('list_price', 'List Price'), - ('codes', 'Registration Codes'), - ('course_id', 'Course Id') ] db_columns = [x[0] for x in query_features] @@ -1156,7 +1152,7 @@ def generate_registration_codes(request, course_id): generated_registration_code = save_registration_code(request.user, course_id, sale_invoice, order=None) registration_codes.append(generated_registration_code) - site_name = microsite.get_value('SITE_NAME', 'localhost') + site_name = microsite.get_value('SITE_NAME', settings.SITE_NAME) course = get_course_by_id(course_id, depth=None) course_honor_mode = CourseMode.mode_for_course(course_id, 'honor') course_price = course_honor_mode.min_price diff --git a/lms/djangoapps/instructor_analytics/basic.py b/lms/djangoapps/instructor_analytics/basic.py index b6e73e1626..72237c5fef 100644 --- a/lms/djangoapps/instructor_analytics/basic.py +++ b/lms/djangoapps/instructor_analytics/basic.py @@ -60,7 +60,6 @@ def sale_order_record_features(course_id, features): """ sale_order_features = [x for x in SALE_ORDER_FEATURES if x in features] - course_reg_features = [x for x in COURSE_REGISTRATION_FEATURES if x in features] order_item_features = [x for x in ORDER_ITEM_FEATURES if x in features] # Extracting order information @@ -74,9 +73,6 @@ def sale_order_record_features(course_id, features): sale_order_dict.update({"logged_in_username": purchased_course.order.user.username}) sale_order_dict.update({"logged_in_email": purchased_course.order.user.email}) - sale_order_dict.update({"total_codes": 'N/A'}) - sale_order_dict.update({'total_used_codes': 'N/A'}) - # Extracting OrderItem information of unit_cost, list_price and status order_item_dict = dict((feature, getattr(purchased_course, feature, None)) for feature in order_item_features) @@ -89,22 +85,6 @@ def sale_order_record_features(course_id, features): order_item_dict.update({'coupon_code': ", ".join(coupon_codes)}) sale_order_dict.update(dict(order_item_dict.items())) - if getattr(purchased_course.order, 'order_type') == OrderTypes.BUSINESS: - registration_codes = CourseRegistrationCode.objects.filter(order=purchased_course.order, course_id=course_id) - sale_order_dict.update({"total_codes": registration_codes.count()}) - total_used_codes = RegistrationCodeRedemption.objects.filter(registration_code__in=registration_codes).count() - sale_order_dict.update({'total_used_codes': total_used_codes}) - - codes = [reg_code.code for reg_code in registration_codes] - - # Extracting registration code information - obj_course_reg_code = registration_codes.all()[:1].get() - course_reg_dict = dict((feature, getattr(obj_course_reg_code, feature)) - for feature in course_reg_features) - - course_reg_dict['course_id'] = course_id.to_deprecated_string() - course_reg_dict.update({'codes': ", ".join(codes)}) - sale_order_dict.update(dict(course_reg_dict.items())) return sale_order_dict diff --git a/lms/djangoapps/instructor_analytics/tests/test_basic.py b/lms/djangoapps/instructor_analytics/tests/test_basic.py index 3f681d7b6c..fd835578e9 100644 --- a/lms/djangoapps/instructor_analytics/tests/test_basic.py +++ b/lms/djangoapps/instructor_analytics/tests/test_basic.py @@ -238,8 +238,6 @@ class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase): self.assertEqual(sale_order_record['company_contact_name'], order.company_contact_name) self.assertEqual(sale_order_record['company_contact_email'], order.company_contact_email) self.assertEqual(sale_order_record['customer_reference_number'], order.customer_reference_number) - self.assertEqual(sale_order_record['total_used_codes'], order.registrationcoderedemption_set.all().count()) - self.assertEqual(sale_order_record['total_codes'], len(CourseRegistrationCode.objects.filter(order=order))) self.assertEqual(sale_order_record['unit_cost'], item.unit_cost) self.assertEqual(sale_order_record['list_price'], item.list_price) self.assertEqual(sale_order_record['status'], item.status) @@ -281,7 +279,7 @@ class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase): order.save() registration_code_redemption = RegistrationCodeRedemption( - order=order, registration_code_id=1, redeemed_by=self.instructor + registration_code_id=1, redeemed_by=self.instructor ) registration_code_redemption.save() registration_codes = CourseRegistrationCode.objects.all() diff --git a/lms/djangoapps/shoppingcart/exceptions.py b/lms/djangoapps/shoppingcart/exceptions.py index 714cc0323c..c4170e629f 100644 --- a/lms/djangoapps/shoppingcart/exceptions.py +++ b/lms/djangoapps/shoppingcart/exceptions.py @@ -37,18 +37,6 @@ class MultipleCouponsNotAllowedException(InvalidCartItem): pass -class RegCodeAlreadyExistException(InvalidCartItem): - pass - - -class ItemNotAllowedToRedeemRegCodeException(InvalidCartItem): - pass - - -class ItemDoesNotExistAgainstRegCodeException(InvalidCartItem): - pass - - class ReportException(Exception): pass @@ -63,3 +51,7 @@ class InvalidStatusToRetire(Exception): class UnexpectedOrderItemStatus(Exception): pass + + +class ItemNotFoundInCartException(Exception): + pass diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index 40bd6d01da..9c4bf906c8 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -44,11 +44,9 @@ from .exceptions import ( AlreadyEnrolledInCourseException, CourseDoesNotExistException, MultipleCouponsNotAllowedException, - RegCodeAlreadyExistException, - ItemDoesNotExistAgainstRegCodeException, - ItemNotAllowedToRedeemRegCodeException, InvalidStatusToRetire, UnexpectedOrderItemStatus, + ItemNotFoundInCartException ) from microsite_configuration import microsite @@ -552,6 +550,23 @@ class Order(models.Model): for item in self.orderitem_set.all(): # pylint: disable=no-member item.retire() + def find_item_by_course_id(self, course_id): + """ + course_id: Course id of the item to find + Returns OrderItem from the Order given a course_id + Raises exception ItemNotFoundException when the item + having the given course_id is not present in the cart + """ + cart_items = OrderItem.objects.filter(order=self).select_subclasses() + found_items = [] + for item in cart_items: + if getattr(item, 'course_id', None): + if item.course_id == course_id: + found_items.append(item) + if not found_items: + raise ItemNotFoundInCartException + return found_items + class OrderItem(TimeStampedModel): """ @@ -730,25 +745,6 @@ class CourseRegistrationCode(models.Model): order = models.ForeignKey(Order, db_index=True, null=True, related_name="purchase_order") invoice = models.ForeignKey(Invoice, null=True) - @classmethod - @transaction.commit_on_success - def free_user_enrollment(cls, cart): - """ - Here we enroll the user free for all courses available in shopping cart - """ - cart_items = cart.orderitem_set.all().select_subclasses() - if cart_items: - for item in cart_items: - CourseEnrollment.enroll(cart.user, item.course_id) - log.info("Enrolled '{0}' in free course '{1}'" - .format(cart.user.email, item.course_id)) # pylint: disable=no-member - item.status = 'purchased' - item.save() - - cart.status = 'purchased' - cart.purchase_time = datetime.now(pytz.utc) - cart.save() - class RegistrationCodeRedemption(models.Model): """ @@ -761,47 +757,12 @@ class RegistrationCodeRedemption(models.Model): course_enrollment = models.ForeignKey(CourseEnrollment, null=True) @classmethod - def delete_registration_redemption(cls, user, cart): + def is_registration_code_redeemed(cls, course_reg_code): """ - This method delete registration redemption + Checks the existence of the registration code + in the RegistrationCodeRedemption """ - reg_code_redemption = cls.objects.filter(redeemed_by=user, order=cart) - if reg_code_redemption: - reg_code_redemption.delete() - log.info('Registration code redemption entry removed for user {0} for order {1}'.format(user, cart.id)) - - @classmethod - def add_reg_code_redemption(cls, course_reg_code, order): - """ - add course registration code info into RegistrationCodeRedemption model - """ - cart_items = order.orderitem_set.all().select_subclasses() - - for item in cart_items: - if getattr(item, 'course_id'): - if item.course_id == course_reg_code.course_id: - # If the item qty is greater than 1 then the registration code should not be allowed to - # redeem - if item.qty > 1: - raise ItemNotAllowedToRedeemRegCodeException - # If another account tries to use a existing registration code before the student checks out, an - # error message will appear.The reg code is un-reusable. - code_redemption = cls.objects.filter(registration_code=course_reg_code) - if code_redemption: - log.exception("Registration code '{0}' already used".format(course_reg_code.code)) - raise RegCodeAlreadyExistException - - code_redemption = RegistrationCodeRedemption(registration_code=course_reg_code, order=order, redeemed_by=order.user) - code_redemption.save() - item.list_price = item.unit_cost - item.unit_cost = 0 - item.save() - log.info("Code '{0}' is used by user {1} against order id '{2}' " - .format(course_reg_code.code, order.user.username, order.id)) - return course_reg_code - - log.warning("Course item does not exist against registration code '{0}'".format(course_reg_code.code)) - raise ItemDoesNotExistAgainstRegCodeException + return cls.objects.filter(registration_code=course_reg_code).exists() @classmethod def create_invoice_generated_registration_redemption(cls, course_reg_code, user): diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py index f32c0a82a7..d7d7e935b8 100644 --- a/lms/djangoapps/shoppingcart/tests/test_views.py +++ b/lms/djangoapps/shoppingcart/tests/test_views.py @@ -491,12 +491,18 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.reg_code}) self.assertEqual(resp.status_code, 200) - # unit price should be updated to 0 for that course - item = self.cart.orderitem_set.all().select_subclasses()[0] - self.assertEquals(item.unit_cost, 0) - self.assertEqual(self.cart.total_cost, 0) + redeem_url = reverse('register_code_redemption', args=[self.reg_code]) + response = self.client.get(redeem_url) + self.assertEquals(response.status_code, 200) + # check button text + self.assertTrue('Activate Course Enrollment' in response.content) + + #now activate the user by enrolling him/her to the course + response = self.client.post(redeem_url) + self.assertEquals(response.status_code, 200) # now testing registration code already used scenario, reusing the same code + # the item has been removed when using the registration code for the first time resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.reg_code}) self.assertEqual(resp.status_code, 400) self.assertIn("Oops! The code '{0}' you entered is either invalid or expired".format(self.reg_code), resp.content) @@ -545,38 +551,6 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): info_log.assert_called_with( 'Coupon redemption entry removed for user {0} for order {1}'.format(self.user, reg_item.id)) - @patch('shoppingcart.views.log.info') - def test_reset_redemption_for_registration_code(self, info_log): - - self.add_reg_code(self.course_key) - reg_item = self.add_course_to_user_cart(self.course_key) - - resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.reg_code}) - self.assertEqual(resp.status_code, 200) - - resp = self.client.post(reverse('shoppingcart.views.reset_code_redemption', args=[])) - - self.assertEqual(resp.status_code, 200) - info_log.assert_called_with( - 'Registration code redemption entry removed for user {0} for order {1}'.format(self.user, reg_item.id)) - - @patch('shoppingcart.views.log.info') - def test_existing_reg_code_redemption_on_removing_item(self, info_log): - - self.add_reg_code(self.course_key) - reg_item = self.add_course_to_user_cart(self.course_key) - - resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.reg_code}) - self.assertEqual(resp.status_code, 200) - - resp = self.client.post(reverse('shoppingcart.views.remove_item', args=[]), - {'id': reg_item.id}) - - self.assertEqual(resp.status_code, 200) - self.assertEquals(self.cart.orderitem_set.count(), 0) - info_log.assert_called_with( - 'Registration code "{0}" redemption entry removed for user "{1}" for order item "{2}"'.format(self.reg_code, self.user, reg_item.id)) - @patch('shoppingcart.views.log.info') def test_coupon_discount_for_multiple_courses_in_cart(self, info_log): @@ -596,7 +570,8 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): elif item.id == cert_item.id: self.assertEquals(item.list_price, None) - # Delete the discounted item, corresponding coupon redemption should be removed for that particular discounted item + # Delete the discounted item, corresponding coupon redemption should + # be removed for that particular discounted item resp = self.client.post(reverse('shoppingcart.views.remove_item', args=[]), {'id': reg_item.id}) @@ -605,34 +580,6 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): info_log.assert_called_with( 'Coupon "{0}" redemption entry removed for user "{1}" for order item "{2}"'.format(self.coupon_code, self.user, reg_item.id)) - @patch('shoppingcart.views.log.info') - def test_reg_code_free_discount_with_multiple_courses_in_cart(self, info_log): - - reg_item = self.add_course_to_user_cart(self.course_key) - self.add_reg_code(self.course_key) - cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor') - self.assertEquals(self.cart.orderitem_set.count(), 2) - - resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.reg_code}) - self.assertEqual(resp.status_code, 200) - - # unit_cost should be 0 for that particular course for which registration code is registered - items = self.cart.orderitem_set.all().select_subclasses() - for item in items: - if item.id == reg_item.id: - self.assertEquals(item.unit_cost, 0) - elif item.id == cert_item.id: - self.assertEquals(item.list_price, None) - - # Delete the discounted item, corresponding reg code redemption should be removed for that particular item - resp = self.client.post(reverse('shoppingcart.views.remove_item', args=[]), - {'id': reg_item.id}) - - self.assertEqual(resp.status_code, 200) - self.assertEquals(self.cart.orderitem_set.count(), 1) - info_log.assert_called_with( - 'Registration code "{0}" redemption entry removed for user "{1}" for order item "{2}"'.format(self.reg_code, self.user, reg_item.id)) - @patch('shoppingcart.views.log.info') def test_delete_certificate_item(self, info_log): @@ -667,24 +614,6 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): info_log.assert_called_with( 'Coupon redemption entry removed for user {0} for order {1}'.format(self.user, reg_item.id)) - @patch('shoppingcart.views.log.info') - def test_remove_registration_code_redemption_on_clear_cart(self, info_log): - - reg_item = self.add_course_to_user_cart(self.course_key) - CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor') - self.assertEquals(self.cart.orderitem_set.count(), 2) - - self.add_reg_code(self.course_key) - resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.reg_code}) - self.assertEqual(resp.status_code, 200) - - resp = self.client.post(reverse('shoppingcart.views.clear_cart', args=[])) - self.assertEqual(resp.status_code, 200) - self.assertEquals(self.cart.orderitem_set.count(), 0) - - info_log.assert_called_with( - 'Registration code redemption entry removed for user {0} for order {1}'.format(self.user, reg_item.id)) - def test_add_course_to_cart_already_registered(self): CourseEnrollment.enroll(self.user, self.course_key) self.login_user() @@ -964,12 +893,16 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.reg_code}) self.assertEqual(resp.status_code, 200) - resp = self.client.get(reverse('shoppingcart.views.show_cart', args=[])) - self.assertIn('Register', resp.content) - # freely enroll the user into course - resp = self.client.get(reverse('shoppingcart.views.register_courses')) - self.assertIn('success', resp.content) + redeem_url = reverse('register_code_redemption', args=[self.reg_code]) + response = self.client.get(redeem_url) + self.assertEquals(response.status_code, 200) + # check button text + self.assertTrue('Activate Course Enrollment' in response.content) + + #now activate the user by enrolling him/her to the course + response = self.client.post(redeem_url) + self.assertEquals(response.status_code, 200) @patch('shoppingcart.views.render_to_response', render_mock) def test_reg_code_with_multiple_courses_and_checkout_scenario(self): @@ -984,6 +917,16 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.reg_code}) self.assertEqual(resp.status_code, 200) + redeem_url = reverse('register_code_redemption', args=[self.reg_code]) + resp = self.client.get(redeem_url) + self.assertEquals(resp.status_code, 200) + # check button text + self.assertTrue('Activate Course Enrollment' in resp.content) + + #now activate the user by enrolling him/her to the course + resp = self.client.post(redeem_url) + self.assertEquals(resp.status_code, 200) + resp = self.client.get(reverse('shoppingcart.views.show_cart', args=[])) self.assertIn('Payment', resp.content) self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') @@ -1001,10 +944,8 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): # make sure the enrollment_ids were stored in the PaidCourseRegistration items # refetch them first since they are updated - item1 = PaidCourseRegistration.objects.get(id=item1.id) - self.assertIsNotNone(item1.course_enrollment) - self.assertEqual(item1.course_enrollment.course_id, self.course_key) - + # item1 has been deleted from the the cart. + # User has been enrolled for the item1 item2 = PaidCourseRegistration.objects.get(id=item2.id) self.assertIsNotNone(item2.course_enrollment) self.assertEqual(item2.course_enrollment.course_id, self.testing_course.id) @@ -1123,9 +1064,9 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): redeem_url = reverse('register_code_redemption', args=[context['reg_code_info_list'][0]['code']]) #now activate the user by enrolling him/her to the course - response = self.client.post(redeem_url, **{'HTTP_HOST': 'localhost'}) + response = self.client.post(redeem_url) self.assertEquals(response.status_code, 200) - self.assertTrue('View Course' in response.content) + self.assertTrue('View Dashboard' in response.content) # now view the receipt page again to see if any registration codes # has been expired or not @@ -1240,7 +1181,6 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): self._assert_404(reverse('shoppingcart.views.update_user_cart', args=[])) self._assert_404(reverse('shoppingcart.views.reset_code_redemption', args=[]), use_post=True) self._assert_404(reverse('shoppingcart.views.billing_details', args=[])) - self._assert_404(reverse('shoppingcart.views.register_courses', args=[])) # TODO (ECOM-188): Once we complete the A/B test of separate @@ -1479,7 +1419,7 @@ class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase): url = reverse('register_code_redemption', args=['asdasd']) self.login_user() for i in xrange(30): # pylint: disable=unused-variable - response = self.client.post(url, **{'HTTP_HOST': 'localhost'}) + response = self.client.post(url) self.assertEquals(response.status_code, 404) # then the rate limiter should kick in and give a HttpForbidden response @@ -1489,7 +1429,7 @@ class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase): # now reset the time to 5 mins from now in future in order to unblock reset_time = datetime.now(UTC) + timedelta(seconds=300) with freeze_time(reset_time): - response = self.client.post(url, **{'HTTP_HOST': 'localhost'}) + response = self.client.post(url) self.assertEquals(response.status_code, 404) cache.clear() @@ -1503,7 +1443,7 @@ class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase): url = reverse('register_code_redemption', args=['asdasd']) self.login_user() for i in xrange(30): # pylint: disable=unused-variable - response = self.client.get(url, **{'HTTP_HOST': 'localhost'}) + response = self.client.get(url) self.assertEquals(response.status_code, 404) # then the rate limiter should kick in and give a HttpForbidden response @@ -1513,7 +1453,7 @@ class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase): # now reset the time to 5 mins from now in future in order to unblock reset_time = datetime.now(UTC) + timedelta(seconds=300) with freeze_time(reset_time): - response = self.client.get(url, **{'HTTP_HOST': 'localhost'}) + response = self.client.get(url) self.assertEquals(response.status_code, 404) cache.clear() @@ -1536,33 +1476,31 @@ class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase): 'customer_reference_number': '123A23F', 'internal_reference': '', 'invoice': '' } - response = self.client.post(url, data, **{'HTTP_HOST': 'localhost'}) + response = self.client.post(url, data) self.assertEquals(response.status_code, 200) # get the first registration from the newly created registration codes registration_code = CourseRegistrationCode.objects.all()[0].code redeem_url = reverse('register_code_redemption', args=[registration_code]) self.login_user() - response = self.client.get(redeem_url, **{'HTTP_HOST': 'localhost'}) + response = self.client.get(redeem_url) self.assertEquals(response.status_code, 200) # check button text self.assertTrue('Activate Course Enrollment' in response.content) #now activate the user by enrolling him/her to the course - response = self.client.post(redeem_url, **{'HTTP_HOST': 'localhost'}) + response = self.client.post(redeem_url) self.assertEquals(response.status_code, 200) - self.assertTrue('View Course' in response.content) + self.assertTrue('View Dashboard' in response.content) #now check that the registration code has already been redeemed and user is already registered in the course - redemption = RegistrationCodeRedemption.objects.get(registration_code__code=registration_code) - self.assertIsNotNone(redemption.course_enrollment) - - response = self.client.get(redeem_url, **{'HTTP_HOST': 'localhost'}) + RegistrationCodeRedemption.objects.filter(registration_code__code=registration_code) + response = self.client.get(redeem_url) self.assertEquals(len(RegistrationCodeRedemption.objects.filter(registration_code__code=registration_code)), 1) self.assertTrue("You've clicked a link for an enrollment code that has already been used." in response.content) #now check that the registration code has already been redeemed - response = self.client.post(redeem_url, **{'HTTP_HOST': 'localhost'}) + response = self.client.post(redeem_url) self.assertTrue("You've clicked a link for an enrollment code that has already been used." in response.content) #now check the response of the dashboard page diff --git a/lms/djangoapps/shoppingcart/urls.py b/lms/djangoapps/shoppingcart/urls.py index 36ec4df114..79e83bc7e5 100644 --- a/lms/djangoapps/shoppingcart/urls.py +++ b/lms/djangoapps/shoppingcart/urls.py @@ -17,7 +17,6 @@ urlpatterns = patterns('shoppingcart.views', # nopep8 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'), ) if settings.FEATURES.get('ENABLE_PAYMENT_FAKE'): diff --git a/lms/djangoapps/shoppingcart/views.py b/lms/djangoapps/shoppingcart/views.py index 85a4636df9..1da5c24bfc 100644 --- a/lms/djangoapps/shoppingcart/views.py +++ b/lms/djangoapps/shoppingcart/views.py @@ -29,15 +29,14 @@ from student.models import CourseEnrollment from .exceptions import ( ItemAlreadyInCartException, AlreadyEnrolledInCourseException, CourseDoesNotExistException, ReportTypeDoesNotExistException, - RegCodeAlreadyExistException, ItemDoesNotExistAgainstRegCodeException, MultipleCouponsNotAllowedException, InvalidCartItem, - ItemNotAllowedToRedeemRegCodeException + ItemNotFoundInCartException ) from .models import ( Order, OrderTypes, PaidCourseRegistration, OrderItem, Coupon, CouponRedemption, CourseRegistrationCode, RegistrationCodeRedemption, - Donation, DonationConfiguration + CourseRegCodeItem, Donation, DonationConfiguration ) from .processors import ( process_postpay_callback, render_purchase_form_html, @@ -90,9 +89,10 @@ def add_course_to_cart(request, course_id): except CourseDoesNotExistException: return HttpResponseNotFound(_('The course you requested does not exist.')) except ItemAlreadyInCartException: - return HttpResponseBadRequest(_('The course {0} is already in your cart.'.format(course_id))) + return HttpResponseBadRequest(_('The course {course_id} is already in your cart.'.format(course_id=course_id))) except AlreadyEnrolledInCourseException: - return HttpResponseBadRequest(_('You are already registered in course {0}.'.format(course_id))) + return HttpResponseBadRequest( + _('You are already registered in course {course_id}.'.format(course_id=course_id))) else: # in case a coupon redemption code has been applied, new items should also get a discount if applicable. order = paid_course_item.order @@ -129,7 +129,7 @@ def update_user_cart(request): try: item = OrderItem.objects.get(id=item_id, status='cart') except OrderItem.DoesNotExist: - log.exception('Cart OrderItem id={0} DoesNotExist'.format(item_id)) + log.exception('Cart OrderItem id={item_id} DoesNotExist'.format(item_id=item_id)) return HttpResponseNotFound('Order item does not exist.') item.qty = qty @@ -186,12 +186,8 @@ def clear_cart(request): coupon_redemption = CouponRedemption.objects.filter(user=request.user, order=cart.id) if coupon_redemption: coupon_redemption.delete() - log.info('Coupon redemption entry removed for user {0} for order {1}'.format(request.user, cart.id)) - - reg_code_redemption = RegistrationCodeRedemption.objects.filter(redeemed_by=request.user, order=cart.id) - if reg_code_redemption: - reg_code_redemption.delete() - log.info('Registration code redemption entry removed for user {0} for order {1}'.format(request.user, cart.id)) + log.info('Coupon redemption entry removed for user {user} for order {order_id}'.format(user=request.user, + order_id=cart.id)) return HttpResponse('Cleared') @@ -207,13 +203,14 @@ def remove_item(request): items = OrderItem.objects.filter(id=item_id, status='cart').select_subclasses() if not len(items): - log.exception('Cannot remove cart OrderItem id={0}. DoesNotExist or item is already purchased'.format(item_id)) + log.exception('Cannot remove cart OrderItem id={item_id}. DoesNotExist or item is already purchased'.format( + item_id=item_id)) else: item = items[0] if item.user == request.user: order_item_course_id = getattr(item, 'course_id') item.delete() - log.info('order item {0} removed for user {1}'.format(item_id, request.user)) + log.info('order item {item_id} removed for user {user}'.format(item_id=item_id, user=request.user)) remove_code_redemption(order_item_course_id, item_id, item, request.user) item.order.update_order_type() @@ -223,7 +220,7 @@ def remove_item(request): def remove_code_redemption(order_item_course_id, item_id, item, user): """ If an item removed from shopping cart then we will remove - the corresponding redemption info of coupon/registration code. + the corresponding redemption info of coupon code """ try: # Try to remove redemption information of coupon code, If exist. @@ -233,21 +230,10 @@ def remove_code_redemption(order_item_course_id, item_id, item, user): order=item.order_id ) coupon_redemption.delete() - log.info('Coupon "{0}" redemption entry removed for user "{1}" for order item "{2}"' - .format(coupon_redemption.coupon.code, user, item_id)) + log.info('Coupon "{code}" redemption entry removed for user "{user}" for order item "{item_id}"' + .format(code=coupon_redemption.coupon.code, user=user, item_id=item_id)) except CouponRedemption.DoesNotExist: - pass - - try: - # Try to remove redemption information of registration code, If exist. - reg_code_redemption = RegistrationCodeRedemption.objects.get(redeemed_by=user, order=item.order_id) - except RegistrationCodeRedemption.DoesNotExist: - log.debug('Code redemption does not exist for order item id={0}.'.format(item_id)) - else: - if order_item_course_id == reg_code_redemption.registration_code.course_id: - reg_code_redemption.delete() - log.info('Registration code "{0}" redemption entry removed for user "{1}" for order item "{2}"' - .format(reg_code_redemption.registration_code.code, user, item_id)) + log.debug('Code redemption does not exist for order item id={item_id}.'.format(item_id=item_id)) @login_required @@ -259,7 +245,6 @@ def reset_code_redemption(request): cart = Order.get_cart_for_user(request.user) cart.reset_cart_items_prices() CouponRedemption.delete_coupon_redemption(request.user, cart) - RegistrationCodeRedemption.delete_registration_redemption(request.user, cart) return HttpResponse('reset') @@ -267,20 +252,19 @@ def reset_code_redemption(request): @enforce_shopping_cart_enabled def use_code(request): """ - This method may generate the discount against valid coupon code - and save its entry into coupon redemption table - OR - Make the cart item free of cost against valid registration code. - Valid Code can be either coupon or registration code. + Valid Code can be either Coupon or Registration code. + For a valid Coupon Code, this applies the coupon code and generates a discount against all applicable items. + For a valid Registration code, it deletes the item from the shopping cart and redirects to the + Registration Code Redemption page. """ code = request.POST["code"] coupons = Coupon.objects.filter(code=code, is_active=True) if not coupons: - # If not coupon code then we check that code against course registration code + # If no coupons then we check that code against course registration code try: course_reg = CourseRegistrationCode.objects.get(code=code) except CourseRegistrationCode.DoesNotExist: - return HttpResponseNotFound(_("Discount does not exist against code '{0}'.".format(code))) + return HttpResponseNotFound(_("Discount does not exist against code '{code}'.".format(code=code))) return use_registration_code(course_reg, request.user) @@ -331,7 +315,7 @@ def register_code_redemption(request, registration_code): AUDIT_LOG.warning("Rate limit exceeded in registration code redemption.") return HttpResponseForbidden() - template_to_render = 'shoppingcart/registration_code_receipt.html' + template_to_render = 'shoppingcart/registration_code_redemption.html' if request.method == "GET": reg_code_is_valid, reg_code_already_redeemed, course_registration = get_reg_code_validity(registration_code, request, limiter) @@ -351,6 +335,18 @@ def register_code_redemption(request, registration_code): course = get_course_by_id(getattr(course_registration, 'course_id'), depth=0) if reg_code_is_valid and not reg_code_already_redeemed: + # remove the course from the cart if it was added there. + cart = Order.get_cart_for_user(request.user) + try: + cart_items = cart.find_item_by_course_id(course_registration.course_id) + + except ItemNotFoundInCartException: + pass + else: + for cart_item in cart_items: + if isinstance(cart_item, PaidCourseRegistration) or isinstance(cart_item, CourseRegCodeItem): + cart_item.delete() + #now redeem the reg code. redemption = RegistrationCodeRedemption.create_invoice_generated_registration_redemption(course_registration, request.user) redemption.course_enrollment = CourseEnrollment.enroll(request.user, course.id) @@ -375,19 +371,41 @@ def register_code_redemption(request, registration_code): def use_registration_code(course_reg, user): """ - This method utilize course registration code + This method utilize course registration code. + If the registration code is already redeemed, it returns an error. + Else, it identifies and removes the applicable OrderItem from the Order + and redirects the user to the Registration code redemption page. """ + if RegistrationCodeRedemption.is_registration_code_redeemed(course_reg): + log.warning("Registration code '{registration_code}' already used".format(registration_code=course_reg.code)) + return HttpResponseBadRequest(_( + "Oops! The code '{registration_code}' you entered is either invalid or expired".format( + registration_code=course_reg.code))) try: cart = Order.get_cart_for_user(user) - RegistrationCodeRedemption.add_reg_code_redemption(course_reg, cart) - except RegCodeAlreadyExistException: - return HttpResponseBadRequest(_("Oops! The code '{0}' you entered is either invalid or expired".format(course_reg.code))) - except ItemDoesNotExistAgainstRegCodeException: - return HttpResponseNotFound(_("Code '{0}' is not valid for any course in the shopping cart.".format(course_reg.code))) - except ItemNotAllowedToRedeemRegCodeException: - return HttpResponseNotFound(_("Cart item quantity should not be greater than 1 when applying activation code")) + cart_items = cart.find_item_by_course_id(course_reg.course_id) + except ItemNotFoundInCartException: + log.warning("Course item does not exist against registration code '{registration_code}'".format( + registration_code=course_reg.code)) + return HttpResponseNotFound(_( + "Code '{registration_code}' is not valid for any course in the shopping cart.".format( + registration_code=course_reg.code))) + else: + applicable_cart_items = [ + cart_item for cart_item in cart_items + if ( + (isinstance(cart_item, PaidCourseRegistration) or isinstance(cart_item, CourseRegCodeItem))and cart_item.qty == 1 + ) + ] + if not applicable_cart_items: + return HttpResponseNotFound( + _("Cart item quantity should not be greater than 1 when applying activation code")) - return HttpResponse(json.dumps({'response': 'success'}), content_type="application/json") + redemption_url = reverse('register_code_redemption', kwargs={'registration_code': course_reg.code}) + return HttpResponse( + json.dumps({'response': 'success', 'coupon_code_applied': False, 'redemption_url': redemption_url}), + content_type="application/json" + ) def use_coupon_code(coupons, user): @@ -405,22 +423,14 @@ def use_coupon_code(coupons, user): return HttpResponseBadRequest(_("Only one coupon redemption is allowed against an order")) if not is_redemption_applied: - log.warning("Course item does not exist for coupon '{0}'".format(coupons[0].code)) - return HttpResponseNotFound(_("Coupon '{0}' is not valid for any course in the shopping cart.".format(coupons[0].code))) + log.warning("Course item does not exist for coupon '{code}'".format(code=coupons[0].code)) + return HttpResponseNotFound( + _("Coupon '{code}' is not valid for any course in the shopping cart.".format(code=coupons[0].code))) - return HttpResponse(json.dumps({'response': 'success'}), content_type="application/json") - - -@login_required -@enforce_shopping_cart_enabled -def register_courses(request): - """ - This method enroll the user for available course(s) - in cart on which valid registration code is applied - """ - cart = Order.get_cart_for_user(request.user) - CourseRegistrationCode.free_user_enrollment(cart) - return HttpResponse(json.dumps({'response': 'success'}), content_type="application/json") + return HttpResponse( + json.dumps({'response': 'success', 'coupon_code_applied': True}), + content_type="application/json" + ) @require_config(DonationConfiguration) diff --git a/lms/static/js/shoppingcart/shoppingcart.js b/lms/static/js/shoppingcart/shoppingcart.js index 75c6d2a795..971bc8c512 100644 --- a/lms/static/js/shoppingcart/shoppingcart.js +++ b/lms/static/js/shoppingcart/shoppingcart.js @@ -117,6 +117,14 @@ var edx = edx || {}; $(document).ready(function() { // (click on the payment submit button). $('.cart-view form input[type="submit"]').click(function(event) { + // check if there is code exists in the inout_code field + // before going to make payment + // if exists then trigger click event of the apply code button + var code = $('div.code-input input#input_code').val(); + if (typeof(code) != 'undefined' && code != ''){ + $('div.code-input #submit-code').trigger('click'); + return false; + } var container = $('.confirm-enrollment.cart-view form'); var view = new edx.shoppingcart.showcart.CartView({ el:container diff --git a/lms/static/sass/views/_shoppingcart.scss b/lms/static/sass/views/_shoppingcart.scss index 18848b5e40..f5f8bd9ead 100644 --- a/lms/static/sass/views/_shoppingcart.scss +++ b/lms/static/sass/views/_shoppingcart.scss @@ -236,7 +236,6 @@ } a.course-link-bg-color { background-color: #00A1E5; - background-image: linear-gradient(#00A1E5, #00A1E5); border: 16px solid #00A1E5; box-shadow: 0 1px 0 0 #00A1E5 inset; text-shadow: 0 1px 0 #00A1E5; @@ -254,6 +253,11 @@ text-decoration: none; font-size: 24px; text-align: center; + &:hover { + background: $m-blue-d2; + border: 16px solid $m-blue-d2; + box-shadow: 0 1px 0 0 $m-blue-d2 inset; + } } input[type="submit"] { text-transform: none; @@ -272,6 +276,10 @@ text-decoration: none; text-shadow: 0 1px 0 #00A1E5; font-size: 24px; + &:hover { + background: $m-blue-d2; + box-shadow: none; + } } } diff --git a/lms/templates/shoppingcart/registration_code_receipt.html b/lms/templates/shoppingcart/registration_code_redemption.html similarity index 94% rename from lms/templates/shoppingcart/registration_code_receipt.html rename to lms/templates/shoppingcart/registration_code_redemption.html index c9bb5da476..b07ed42721 100644 --- a/lms/templates/shoppingcart/registration_code_receipt.html +++ b/lms/templates/shoppingcart/registration_code_redemption.html @@ -65,8 +65,8 @@ from courseware.courses import course_image_url, get_course_about_section % if not reg_code_already_redeemed: %if redemption_success: - <% course_url = reverse('info', args=[course.id.to_deprecated_string()]) %> - ${_("View Course     ▸")} + <% dashboard_url = reverse('dashboard') %> + ${_("View Dashboard     ▸")} %elif not registered_for_course:
diff --git a/lms/templates/shoppingcart/shopping_cart.html b/lms/templates/shoppingcart/shopping_cart.html index a9c4f5a42e..ef4db05be4 100644 --- a/lms/templates/shoppingcart/shopping_cart.html +++ b/lms/templates/shoppingcart/shopping_cart.html @@ -100,9 +100,7 @@ from django.utils.translation import ugettext as _
- % if amount == 0: - - % elif order_type == 'business': + % if order_type == 'business':

@@ -172,8 +170,14 @@ from django.utils.translation import ugettext as _ } ) .success(function(data) { - location.reload(true); - }) + if (data.coupon_code_applied) { + location.reload(true); // Reload the page if the coupon code was applied. + } + else { + // Redirect to the redemption URL if the Registration code was applied. + location.href = data.redemption_url; + } + }) .error(function(data,status) { if(status=="parsererror"){ location.reload(true); @@ -199,23 +203,15 @@ from django.utils.translation import ugettext as _ }) }); - $('#register').click(function(event){ - event.preventDefault(); - var post_url = "${reverse('shoppingcart.views.register_courses')}"; - $.post(post_url) - .success(function(data) { - window.location.href = "${reverse('dashboard')}"; - }) - .error(function(data,status) { - if(status=="parsererror"){ - location.reload(true); - }else{ - showErrorMsgs(data.responseText) - } - }) - }); - $("input[name='billing-details']").click(function(event){ + // check if there is code exists in the inout_code field + // before going to billing details page + // if exists then trigger click event of the apply code button + var code = $('div.code-input input#input_code').val(); + if (code!= ''){ + $('div.code-input #submit-code').trigger('click'); + return false; + } event.preventDefault(); location.href = "${reverse('shoppingcart.views.billing_details')}"; });