diff --git a/lms/djangoapps/instructor/tests/test_registration_codes.py b/lms/djangoapps/instructor/tests/test_registration_codes.py new file mode 100644 index 0000000000..2812fdfd9c --- /dev/null +++ b/lms/djangoapps/instructor/tests/test_registration_codes.py @@ -0,0 +1,291 @@ +""" +Test for the registration code status information. +""" +from courseware.tests.factories import InstructorFactory +from xmodule.modulestore.tests.factories import CourseFactory +from django.utils.translation import ugettext as _ +from shoppingcart.models import ( + Invoice, CourseRegistrationCodeInvoiceItem, CourseRegistrationCode, + CourseRegCodeItem, Order, RegistrationCodeRedemption +) +from student.models import CourseEnrollment +from student.roles import CourseSalesAdminRole +from nose.plugins.attrib import attr +import json +from student.tests.factories import UserFactory, CourseModeFactory +from django.core.urlresolvers import reverse +from django.test.utils import override_settings +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase + + +@attr('shard_1') +@override_settings(REGISTRATION_CODE_LENGTH=8) +class TestCourseRegistrationCodeStatus(ModuleStoreTestCase): + """ + Test registration code status. + """ + + def setUp(self): + super(TestCourseRegistrationCodeStatus, self).setUp() + self.course = CourseFactory.create() + CourseModeFactory.create(course_id=self.course.id, min_price=50) + self.instructor = InstructorFactory(course_key=self.course.id) + self.client.login(username=self.instructor.username, password='test') + CourseSalesAdminRole(self.course.id).add_users(self.instructor) + + # create testing invoice + self.sale_invoice = Invoice.objects.create( + total_amount=1234.32, company_name='Test1', company_contact_name='TestName', + company_contact_email='Test@company.com', recipient_name='Testw', recipient_email='test1@test.com', + customer_reference_number='2Fwe23S', internal_reference="A", course_id=self.course.id, is_valid=True + ) + self.invoice_item = CourseRegistrationCodeInvoiceItem.objects.create( + invoice=self.sale_invoice, + qty=1, + unit_price=1234.32, + course_id=self.course.id + ) + self.lookup_code_url = reverse('look_up_registration_code', + kwargs={'course_id': unicode(self.course.id)}) + + self.registration_code_detail_url = reverse('registration_code_details', + kwargs={'course_id': unicode(self.course.id)}) + + url = reverse('generate_registration_codes', + kwargs={'course_id': self.course.id.to_deprecated_string()}) + + data = { + 'total_registration_codes': 12, + 'company_name': 'Test Group', + 'company_contact_name': 'Test@company.com', + 'company_contact_email': 'Test@company.com', + 'unit_price': 122.45, + 'recipient_name': 'Test123', + 'recipient_email': 'test@123.com', + 'address_line_1': 'Portland Street', + 'address_line_2': '', + 'address_line_3': '', + 'city': '', + 'state': '', + 'zip': '', + 'country': '', + 'customer_reference_number': '123A23F', + 'internal_reference': '', + 'invoice': '' + } + + response = self.client.post(url, data) + self.assertEqual(response.status_code, 200, response.content) + + def test_look_up_invalid_registration_code(self): + """ + Verify the view returns HTTP status 400 if an invalid registration code is passed. + Also, verify the data returned includes a message indicating the error, + and the is_registration_code_valid is set to False. + """ + data = { + 'registration_code': 'invalid_reg_code' + } + response = self.client.get(self.lookup_code_url, data) + self.assertEqual(response.status_code, 400) + json_dict = json.loads(response.content) + message = _('The enrollment code ({code}) was not found for the {course_name} course.').format( + course_name=self.course.display_name, code=data['registration_code'] + ) + self.assertEqual(message, json_dict['message']) + self.assertFalse(json_dict['is_registration_code_valid']) + self.assertFalse(json_dict['is_registration_code_redeemed']) + + def test_look_up_valid_registration_code(self): + """ + test lookup for the valid registration code + and that registration code has been redeemed by user + and then mark the registration code as in_valid + when marking as invalidate, it also lookup for + registration redemption entry and also delete + that redemption entry and un_enroll the student + who used that registration code for their enrollment. + """ + for i in range(2): + CourseRegistrationCode.objects.create( + code='reg_code{}'.format(i), + course_id=unicode(self.course.id), + created_by=self.instructor, + invoice=self.sale_invoice, + invoice_item=self.invoice_item, + mode_slug='honor' + ) + + reg_code = CourseRegistrationCode.objects.all()[0] + student = UserFactory() + enrollment = CourseEnrollment.enroll(student, self.course.id) + + RegistrationCodeRedemption.objects.create( + registration_code=reg_code, + redeemed_by=student, + course_enrollment=enrollment + ) + + data = { + 'registration_code': reg_code.code + } + response = self.client.get(self.lookup_code_url, data) + self.assertEqual(response.status_code, 200) + json_dict = json.loads(response.content) + self.assertTrue(json_dict['is_registration_code_valid']) + self.assertTrue(json_dict['is_registration_code_redeemed']) + + # now mark that registration code as invalid + data = { + 'registration_code': reg_code.code, + 'action_type': 'invalidate_registration_code' + } + response = self.client.post(self.registration_code_detail_url, data) + self.assertEqual(response.status_code, 200) + + json_dict = json.loads(response.content) + message = _('This enrollment code has been canceled. It can no longer be used.') + self.assertEqual(message, json_dict['message']) + + # now check that the registration code should be marked as invalid in the db. + reg_code = CourseRegistrationCode.objects.get(code=reg_code.code) + self.assertEqual(reg_code.is_valid, False) + + redemption = RegistrationCodeRedemption.get_registration_code_redemption(reg_code.code, self.course.id) + self.assertIsNone(redemption) + + # now the student course enrollment should be false. + enrollment = CourseEnrollment.get_enrollment(student, self.course.id) + self.assertEqual(enrollment.is_active, False) + + def test_lookup_valid_redeemed_registration_code(self): + """ + test to lookup for the valid and redeemed registration code + and then mark that registration code as un_redeemed + which will unenroll the user and delete the redemption + entry from the database. + """ + student = UserFactory() + self.client.login(username=student.username, password='test') + cart = Order.get_cart_for_user(student) + cart.order_type = 'business' + cart.save() + CourseRegCodeItem.add_to_order(cart, self.course.id, 2) + cart.purchase() + + reg_code = CourseRegistrationCode.objects.filter(order=cart)[0] + + enrollment = CourseEnrollment.enroll(student, self.course.id) + + RegistrationCodeRedemption.objects.create( + registration_code=reg_code, + redeemed_by=student, + course_enrollment=enrollment + ) + self.client.login(username=self.instructor.username, password='test') + data = { + 'registration_code': reg_code.code + } + response = self.client.get(self.lookup_code_url, data) + self.assertEqual(response.status_code, 200) + json_dict = json.loads(response.content) + self.assertTrue(json_dict['is_registration_code_valid']) + self.assertTrue(json_dict['is_registration_code_redeemed']) + + # now mark the registration code as unredeemed + # this will unenroll the user and removed the redemption entry from + # the database. + + data = { + 'registration_code': reg_code.code, + 'action_type': 'unredeem_registration_code' + } + response = self.client.post(self.registration_code_detail_url, data) + self.assertEqual(response.status_code, 200) + + json_dict = json.loads(response.content) + message = _('This enrollment code has been marked as unused.') + self.assertEqual(message, json_dict['message']) + + redemption = RegistrationCodeRedemption.get_registration_code_redemption(reg_code.code, self.course.id) + self.assertIsNone(redemption) + + # now the student course enrollment should be false. + enrollment = CourseEnrollment.get_enrollment(student, self.course.id) + self.assertEqual(enrollment.is_active, False) + + def test_apply_invalid_reg_code_when_updating_code_information(self): + """ + test to apply an invalid registration code + when updating the registration code information. + """ + data = { + 'registration_code': 'invalid_registration_code', + 'action_type': 'unredeem_registration_code' + } + response = self.client.post(self.registration_code_detail_url, data) + self.assertEqual(response.status_code, 400) + + json_dict = json.loads(response.content) + message = _('The enrollment code ({code}) was not found for the {course_name} course.').format( + course_name=self.course.display_name, code=data['registration_code'] + ) + self.assertEqual(message, json_dict['message']) + + def test_mark_registration_code_as_valid(self): + """ + test to mark the invalid registration code + as valid + """ + for i in range(2): + CourseRegistrationCode.objects.create( + code='reg_code{}'.format(i), + course_id=self.course.id.to_deprecated_string(), + created_by=self.instructor, + invoice=self.sale_invoice, + invoice_item=self.invoice_item, + mode_slug='honor', + is_valid=False + ) + + reg_code = CourseRegistrationCode.objects.all()[0] + data = { + 'registration_code': reg_code.code, + 'action_type': 'validate_registration_code' + } + response = self.client.post(self.registration_code_detail_url, data) + self.assertEqual(response.status_code, 200) + + json_dict = json.loads(response.content) + message = _('The enrollment code has been restored.') + self.assertEqual(message, json_dict['message']) + + # now check that the registration code should be marked as valid in the db. + reg_code = CourseRegistrationCode.objects.get(code=reg_code.code) + self.assertEqual(reg_code.is_valid, True) + + def test_returns_error_when_unredeeming_already_unredeemed_registration_code_redemption(self): + """ + test to mark the already unredeemed registration code as unredeemed. + """ + for i in range(2): + CourseRegistrationCode.objects.create( + code='reg_code{}'.format(i), + course_id=self.course.id.to_deprecated_string(), + created_by=self.instructor, + invoice=self.sale_invoice, + invoice_item=self.invoice_item, + mode_slug='honor' + ) + + reg_code = CourseRegistrationCode.objects.all()[0] + data = { + 'registration_code': reg_code.code, + 'action_type': 'unredeem_registration_code' + } + response = self.client.post(self.registration_code_detail_url, data) + self.assertEqual(response.status_code, 400) + + json_dict = json.loads(response.content) + message = _('The redemption does not exist against enrollment code ({code}).').format(code=reg_code.code) + self.assertEqual(message, json_dict['message']) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 5c349dc803..0e286a8722 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -1253,7 +1253,7 @@ def registration_codes_csv(file_name, codes_list, csv_type=None): # csv headers query_features = [ 'code', 'redeem_code_url', 'course_id', 'company_name', 'created_by', - 'redeemed_by', 'invoice_id', 'purchaser', 'customer_reference_number', 'internal_reference' + 'redeemed_by', 'invoice_id', 'purchaser', 'customer_reference_number', 'internal_reference', 'is_valid' ] registration_codes = instructor_analytics.basic.course_registration_features(query_features, codes_list, csv_type) diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 56903b2768..9211371526 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -205,6 +205,7 @@ def _section_e_commerce(course, access, paid_mode, coupons_enabled, reports_enab 'list_financial_report_downloads_url': reverse('list_financial_report_downloads', kwargs={'course_id': unicode(course_key)}), 'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': unicode(course_key)}), + 'look_up_registration_code': reverse('look_up_registration_code', kwargs={'course_id': unicode(course_key)}), 'coupons': coupons, 'sales_admin': access['sales_admin'], 'coupons_enabled': coupons_enabled, diff --git a/lms/djangoapps/instructor/views/registration_codes.py b/lms/djangoapps/instructor/views/registration_codes.py new file mode 100644 index 0000000000..5d28eda86b --- /dev/null +++ b/lms/djangoapps/instructor/views/registration_codes.py @@ -0,0 +1,126 @@ +""" +E-commerce Tab Instructor Dashboard Query Registration Code Status. +""" +from django.core.urlresolvers import reverse +from django.views.decorators.http import require_GET, require_POST +from instructor.enrollment import get_email_params, send_mail_to_student +from django.utils.translation import ugettext as _ +from courseware.courses import get_course_by_id +from instructor.views.api import require_level +from student.models import CourseEnrollment +from util.json_request import JsonResponse +from shoppingcart.models import CourseRegistrationCode, RegistrationCodeRedemption +from opaque_keys.edx.locations import SlashSeparatedCourseKey +from django.views.decorators.cache import cache_control +import logging + +log = logging.getLogger(__name__) + + +@cache_control(no_cache=True, no_store=True, must_revalidate=True) +@require_level('staff') +@require_GET +def look_up_registration_code(request, course_id): # pylint: disable=unused-argument + """ + Look for the registration_code in the database. + and check if it is still valid, allowed to redeem or not. + """ + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) + code = request.GET.get('registration_code') + course = get_course_by_id(course_key, depth=0) + try: + registration_code = CourseRegistrationCode.objects.get(code=code) + except CourseRegistrationCode.DoesNotExist: + return JsonResponse({ + 'is_registration_code_exists': False, + 'is_registration_code_valid': False, + 'is_registration_code_redeemed': False, + 'message': _('The enrollment code ({code}) was not found for the {course_name} course.').format( + code=code, course_name=course.display_name + ) + }, status=400) # status code 200: OK by default + + reg_code_already_redeemed = RegistrationCodeRedemption.is_registration_code_redeemed(code) + + registration_code_detail_url = reverse('registration_code_details', kwargs={'course_id': unicode(course_id)}) + + return JsonResponse({ + 'is_registration_code_exists': True, + 'is_registration_code_valid': registration_code.is_valid, + 'is_registration_code_redeemed': reg_code_already_redeemed, + 'registration_code_detail_url': registration_code_detail_url + }) # status code 200: OK by default + + +@cache_control(no_cache=True, no_store=True, must_revalidate=True) +@require_level('staff') +@require_POST +def registration_code_details(request, course_id): + """ + Post handler to mark the registration code as + 1) valid + 2) invalid + 3) Unredeem. + + """ + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) + code = request.POST.get('registration_code') + action_type = request.POST.get('action_type') + course = get_course_by_id(course_key, depth=0) + action_type_messages = { + 'invalidate_registration_code': _('This enrollment code has been canceled. It can no longer be used.'), + 'unredeem_registration_code': _('This enrollment code has been marked as unused.'), + 'validate_registration_code': _('The enrollment code has been restored.') + } + try: + registration_code = CourseRegistrationCode.objects.get(code=code) + except CourseRegistrationCode.DoesNotExist: + return JsonResponse({ + 'message': _('The enrollment code ({code}) was not found for the {course_name} course.').format( + code=code, course_name=course.display_name + )}, status=400) + + if action_type == 'invalidate_registration_code': + registration_code.is_valid = False + registration_code.save() + if RegistrationCodeRedemption.is_registration_code_redeemed(code): + code_redemption = RegistrationCodeRedemption.get_registration_code_redemption(code, course_key) + delete_redemption_entry(request, code_redemption, course_key) + + if action_type == 'validate_registration_code': + registration_code.is_valid = True + registration_code.save() + + if action_type == 'unredeem_registration_code': + code_redemption = RegistrationCodeRedemption.get_registration_code_redemption(code, course_key) + if code_redemption is None: + return JsonResponse({ + 'message': _('The redemption does not exist against enrollment code ({code}).').format( + code=code)}, status=400) + + delete_redemption_entry(request, code_redemption, course_key) + + return JsonResponse({'message': action_type_messages[action_type]}) + + +def delete_redemption_entry(request, code_redemption, course_key): + """ + delete the redemption entry from the table and + unenroll the user who used the registration code + for the enrollment and send him/her the unenrollment email. + """ + user = code_redemption.redeemed_by + email_address = code_redemption.redeemed_by.email + full_name = code_redemption.redeemed_by.profile.name + CourseEnrollment.unenroll(user, course_key, skip_refund=True) + + course = get_course_by_id(course_key, depth=0) + email_params = get_email_params(course, True, secure=request.is_secure()) + email_params['message'] = 'enrolled_unenroll' + email_params['email_address'] = email_address + email_params['full_name'] = full_name + send_mail_to_student(email_address, email_params) + + # remove the redemption entry from the database. + log.info('deleting redemption entry (%s) from the database.', code_redemption.id) + code_redemption.delete() diff --git a/lms/djangoapps/instructor_analytics/basic.py b/lms/djangoapps/instructor_analytics/basic.py index 10502069f2..1413118fdb 100644 --- a/lms/djangoapps/instructor_analytics/basic.py +++ b/lms/djangoapps/instructor_analytics/basic.py @@ -32,7 +32,7 @@ SALE_ORDER_FEATURES = ('id', 'company_name', 'company_contact_name', 'company_co 'bill_to_country', 'order_type',) AVAILABLE_FEATURES = STUDENT_FEATURES + PROFILE_FEATURES -COURSE_REGISTRATION_FEATURES = ('code', 'course_id', 'created_by', 'created_at') +COURSE_REGISTRATION_FEATURES = ('code', 'course_id', 'created_by', 'created_at', 'is_valid') COUPON_FEATURES = ('code', 'course_id', 'percentage_discount', 'description', 'expiration_date', 'is_active') diff --git a/lms/djangoapps/shoppingcart/migrations/0028_auto__add_field_courseregistrationcode_is_valid.py b/lms/djangoapps/shoppingcart/migrations/0028_auto__add_field_courseregistrationcode_is_valid.py new file mode 100644 index 0000000000..f82bff4a97 --- /dev/null +++ b/lms/djangoapps/shoppingcart/migrations/0028_auto__add_field_courseregistrationcode_is_valid.py @@ -0,0 +1,259 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'CourseRegistrationCode.is_valid' + db.add_column('shoppingcart_courseregistrationcode', 'is_valid', + self.gf('django.db.models.fields.BooleanField')(default=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'CourseRegistrationCode.is_valid' + db.delete_column('shoppingcart_courseregistrationcode', 'is_valid') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'shoppingcart.certificateitem': { + 'Meta': {'object_name': 'CertificateItem', '_ormbases': ['shoppingcart.OrderItem']}, + 'course_enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']"}), + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '128', 'db_index': 'True'}), + 'mode': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), + 'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'shoppingcart.coupon': { + 'Meta': {'object_name': 'Coupon'}, + 'code': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 5, 29, 0, 0)'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'expiration_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'percentage_discount': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'shoppingcart.couponredemption': { + 'Meta': {'object_name': 'CouponRedemption'}, + 'coupon': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Coupon']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'shoppingcart.courseregcodeitem': { + 'Meta': {'object_name': 'CourseRegCodeItem', '_ormbases': ['shoppingcart.OrderItem']}, + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '128', 'db_index': 'True'}), + 'mode': ('django.db.models.fields.SlugField', [], {'default': "'honor'", 'max_length': '50'}), + 'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'shoppingcart.courseregcodeitemannotation': { + 'Meta': {'object_name': 'CourseRegCodeItemAnnotation'}, + 'annotation': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'shoppingcart.courseregistrationcode': { + 'Meta': {'object_name': 'CourseRegistrationCode'}, + 'code': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 5, 29, 0, 0)'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_by_user'", 'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'invoice': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Invoice']", 'null': 'True'}), + 'invoice_item': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.CourseRegistrationCodeInvoiceItem']", 'null': 'True'}), + 'is_valid': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'mode_slug': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'}), + 'order': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'purchase_order'", 'null': 'True', 'to': "orm['shoppingcart.Order']"}) + }, + 'shoppingcart.courseregistrationcodeinvoiceitem': { + 'Meta': {'object_name': 'CourseRegistrationCodeInvoiceItem', '_ormbases': ['shoppingcart.InvoiceItem']}, + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '128', 'db_index': 'True'}), + 'invoiceitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.InvoiceItem']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'shoppingcart.donation': { + 'Meta': {'object_name': 'Donation', '_ormbases': ['shoppingcart.OrderItem']}, + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'donation_type': ('django.db.models.fields.CharField', [], {'default': "'general'", 'max_length': '32'}), + 'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'shoppingcart.donationconfiguration': { + 'Meta': {'object_name': 'DonationConfiguration'}, + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'shoppingcart.invoice': { + 'Meta': {'object_name': 'Invoice'}, + 'address_line_1': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'address_line_2': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'address_line_3': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'company_contact_email': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'company_contact_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'company_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}), + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), + 'customer_reference_number': ('django.db.models.fields.CharField', [], {'max_length': '63', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'internal_reference': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'is_valid': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), + 'recipient_email': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'recipient_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'state': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'total_amount': ('django.db.models.fields.FloatField', [], {}), + 'zip': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True'}) + }, + 'shoppingcart.invoicehistory': { + 'Meta': {'object_name': 'InvoiceHistory'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'invoice': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Invoice']"}), + 'snapshot': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}) + }, + 'shoppingcart.invoiceitem': { + 'Meta': {'object_name': 'InvoiceItem'}, + 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), + 'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'invoice': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Invoice']"}), + 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), + 'qty': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'unit_price': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}) + }, + 'shoppingcart.invoicetransaction': { + 'Meta': {'object_name': 'InvoiceTransaction'}, + 'amount': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'invoice': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Invoice']"}), + 'last_modified_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'last_modified_by_user'", 'to': "orm['auth.User']"}), + 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'started'", 'max_length': '32'}) + }, + 'shoppingcart.order': { + 'Meta': {'object_name': 'Order'}, + 'bill_to_cardtype': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'bill_to_ccnum': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}), + 'bill_to_city': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'bill_to_country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'bill_to_first': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'bill_to_last': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'bill_to_postalcode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'bill_to_state': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}), + 'bill_to_street1': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'bill_to_street2': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'company_contact_email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'company_contact_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'company_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}), + 'customer_reference_number': ('django.db.models.fields.CharField', [], {'max_length': '63', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order_type': ('django.db.models.fields.CharField', [], {'default': "'personal'", 'max_length': '32'}), + 'processor_reply_dump': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'purchase_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'recipient_email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'recipient_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'refunded_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'shoppingcart.orderitem': { + 'Meta': {'object_name': 'OrderItem'}, + 'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}), + 'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}), + 'fulfilled_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'line_desc': ('django.db.models.fields.CharField', [], {'default': "'Misc. Item'", 'max_length': '1024'}), + 'list_price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '30', 'decimal_places': '2'}), + 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), + 'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}), + 'qty': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'refund_requested_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), + 'report_comments': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'service_fee': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32', 'db_index': 'True'}), + 'unit_cost': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'shoppingcart.paidcourseregistration': { + 'Meta': {'object_name': 'PaidCourseRegistration', '_ormbases': ['shoppingcart.OrderItem']}, + 'course_enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']", 'null': 'True'}), + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '128', 'db_index': 'True'}), + 'mode': ('django.db.models.fields.SlugField', [], {'default': "'honor'", 'max_length': '50'}), + 'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'shoppingcart.paidcourseregistrationannotation': { + 'Meta': {'object_name': 'PaidCourseRegistrationAnnotation'}, + 'annotation': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'shoppingcart.registrationcoderedemption': { + 'Meta': {'object_name': 'RegistrationCodeRedemption'}, + 'course_enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']", 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']", 'null': 'True'}), + 'redeemed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 5, 29, 0, 0)', 'null': 'True'}), + 'redeemed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'registration_code': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.CourseRegistrationCode']"}) + }, + 'student.courseenrollment': { + 'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'}, + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['shoppingcart'] \ No newline at end of file diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index b462d268c7..8c2433012b 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -1161,6 +1161,7 @@ class CourseRegistrationCode(models.Model): created_at = models.DateTimeField(default=datetime.now(pytz.utc)) order = models.ForeignKey(Order, db_index=True, null=True, related_name="purchase_order") mode_slug = models.CharField(max_length=100, null=True) + is_valid = models.BooleanField(default=True) # For backwards compatibility, we maintain the FK to "invoice" # In the future, we will remove this in favor of the FK @@ -1196,10 +1197,21 @@ class RegistrationCodeRedemption(models.Model): Checks the existence of the registration code in the RegistrationCodeRedemption """ - return cls.objects.filter(registration_code=course_reg_code).exists() + return cls.objects.filter(registration_code__code=course_reg_code).exists() @classmethod - def create_invoice_generated_registration_redemption(cls, course_reg_code, user): + def get_registration_code_redemption(cls, code, course_id): + """ + Returns the registration code redemption object if found else returns None. + """ + try: + code_redemption = cls.objects.get(registration_code__code=code, registration_code__course_id=course_id) + except cls.DoesNotExist: + code_redemption = None + return code_redemption + + @classmethod + def create_invoice_generated_registration_redemption(cls, course_reg_code, user): # pylint: disable=invalid-name """ This function creates a RegistrationCodeRedemption entry in case the registration codes were invoice generated and thus the order_id is missing. diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py index b72eb5d4de..75d480495f 100644 --- a/lms/djangoapps/shoppingcart/tests/test_views.py +++ b/lms/djangoapps/shoppingcart/tests/test_views.py @@ -121,12 +121,14 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): percentage_discount=self.percentage_discount, created_by=self.user, is_active=is_active) coupon.save() - def add_reg_code(self, course_key, mode_slug='honor'): + def add_reg_code(self, course_key, mode_slug='honor', is_valid=True): """ add dummy registration code into models """ course_reg_code = CourseRegistrationCode( - code=self.reg_code, course_id=course_key, created_by=self.user, mode_slug=mode_slug + code=self.reg_code, course_id=course_key, + created_by=self.user, mode_slug=mode_slug, + is_valid=is_valid ) course_reg_code.save() @@ -387,6 +389,23 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): self.assertEqual(resp.status_code, 404) self.assertIn("Discount does not exist against code '{0}'.".format(self.coupon_code), resp.content) + def test_inactive_registration_code_returns_error(self): + """ + test to redeem inactive registration code and + it returns an error. + """ + course_key = self.course_key.to_deprecated_string() + self.add_reg_code(course_key, is_valid=False) + self.add_course_to_user_cart(self.course_key) + + # now apply the inactive registration code + # it will raise an exception + resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.reg_code}) + self.assertEqual(resp.status_code, 400) + self.assertIn( + "This enrollment code ({enrollment_code}) is no longer valid.".format( + enrollment_code=self.reg_code), resp.content) + def test_course_does_not_exist_in_cart_against_valid_reg_code(self): course_key = self.course_key.to_deprecated_string() + 'testing' self.add_reg_code(course_key) @@ -525,7 +544,6 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): self.assertEqual(coupon.is_active, False) def test_course_free_discount_for_valid_active_reg_code(self): - self.add_reg_code(self.course_key) self.add_course_to_user_cart(self.course_key) @@ -546,7 +564,9 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): # 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) + self.assertIn("This enrollment code ({enrollment_code}) is not valid.".format( + enrollment_code=self.reg_code + ), resp.content) def test_upgrade_from_valid_reg_code(self): """Use a valid registration code to upgrade from honor to verified mode. """ diff --git a/lms/djangoapps/shoppingcart/views.py b/lms/djangoapps/shoppingcart/views.py index 41c14937d3..30e67d872f 100644 --- a/lms/djangoapps/shoppingcart/views.py +++ b/lms/djangoapps/shoppingcart/views.py @@ -287,17 +287,14 @@ def get_reg_code_validity(registration_code, request, limiter): except CourseRegistrationCode.DoesNotExist: reg_code_is_valid = False else: - reg_code_is_valid = True - try: - RegistrationCodeRedemption.objects.get(registration_code__code=registration_code) - except RegistrationCodeRedemption.DoesNotExist: - reg_code_already_redeemed = False + if course_registration.is_valid: + reg_code_is_valid = True else: - reg_code_already_redeemed = True - + reg_code_is_valid = False + reg_code_already_redeemed = RegistrationCodeRedemption.is_registration_code_redeemed(registration_code) if not reg_code_is_valid: # tick the rate limiter counter - AUDIT_LOG.info("Redemption of a non existing RegistrationCode {code}".format(code=registration_code)) + AUDIT_LOG.info("Redemption of a invalid RegistrationCode %s", registration_code) limiter.tick_bad_request_counter(request) raise Http404() @@ -430,15 +427,24 @@ def _is_enrollment_code_an_update(course, user, redemption_code): def use_registration_code(course_reg, user): """ This method utilize course registration code. + If the registration code is invalid, it returns an error. 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(u"Registration code '%s' already used", course_reg.code) + if not course_reg.is_valid: + log.warning(u"The enrollment code (%s) is no longer valid.", course_reg.code) return HttpResponseBadRequest( - _("Oops! The code '{registration_code}' you entered is either invalid or expired").format( - registration_code=course_reg.code + _("This enrollment code ({enrollment_code}) is no longer valid.").format( + enrollment_code=course_reg.code + ) + ) + + if RegistrationCodeRedemption.is_registration_code_redeemed(course_reg.code): + log.warning(u"This enrollment code ({%s}) has already been used.", course_reg.code) + return HttpResponseBadRequest( + _("This enrollment code ({enrollment_code}) is not valid.").format( + enrollment_code=course_reg.code ) ) try: @@ -893,6 +899,7 @@ def _show_receipt_html(request, order): 'course_name': course.display_name, 'redemption_url': reverse('register_code_redemption', args=[course_registration_code.code]), 'code': course_registration_code.code, + 'is_valid': course_registration_code.is_valid, 'is_redeemed': RegistrationCodeRedemption.objects.filter( registration_code=course_registration_code).exists(), }) diff --git a/lms/static/js/instructor_dashboard/ecommerce.js b/lms/static/js/instructor_dashboard/ecommerce.js index c301349fe4..6e93ed8082 100644 --- a/lms/static/js/instructor_dashboard/ecommerce.js +++ b/lms/static/js/instructor_dashboard/ecommerce.js @@ -27,6 +27,11 @@ var edx = edx || {}; }); $(function() { + var $registration_code_status_form = $("form#set_regcode_status_form"), + $lookup_button = $('#lookup_regcode', $registration_code_status_form), + $registration_code_status_form_error = $('#regcode_status_form_error', $registration_code_status_form), + $registration_code_status_form_success = $('#regcode_status_form_success', $registration_code_status_form); + $( "#coupon_expiration_date" ).datepicker({ minDate: 0 }); @@ -43,7 +48,7 @@ var edx = edx || {}; return $(".reports .msg-confirm").css({ "display": "block" }); - }, + }, error: function(std_ajax_err) { request_response_error.text(gettext('Error generating grades. Please try again.')); return $(".reports .msg-error").css({ @@ -52,5 +57,126 @@ var edx = edx || {}; } }); }); + $lookup_button.click(function () { + $registration_code_status_form_error.hide(); + $lookup_button.attr('disabled', true); + var url = $(this).data('endpoint'); + var lookup_registration_code = $('#set_regcode_status_form input[name="regcode_code"]').val(); + if (lookup_registration_code == '') { + $registration_code_status_form_error.show(); + $registration_code_status_form_error.text(gettext('Enter the enrollment code.')); + $lookup_button.removeAttr('disabled'); + return false; + } + $.ajax({ + type: "GET", + data: { + "registration_code" : lookup_registration_code + }, + url: url, + success: function (data) { + var is_registration_code_valid = data.is_registration_code_valid, + is_registration_code_redeemed = data.is_registration_code_redeemed, + is_registration_code_exists = data.is_registration_code_exists; + + $lookup_button.removeAttr('disabled'); + if (is_registration_code_exists == 'false') { + $registration_code_status_form_error.hide(); + $registration_code_status_form_error.show(); + $registration_code_status_form_error.text(gettext(data.message)); + } + else { + var actions_links = ''; + var actions = []; + if (is_registration_code_valid == true) { + actions.push( + { + 'action_url': data.registration_code_detail_url, + 'action_name': gettext('Cancel enrollment code'), + 'registration_code': lookup_registration_code, + 'action_type': 'invalidate_registration_code' + } + ); + } + else { + actions.push( + { + 'action_url': data.registration_code_detail_url, + 'action_name': gettext('Restore enrollment code'), + 'registration_code': lookup_registration_code, + 'action_type': 'validate_registration_code' + } + ); + } + if (is_registration_code_redeemed == true) { + actions.push( + { + 'action_url': data.registration_code_detail_url, + 'action_name': gettext('Mark enrollment code as unused'), + 'registration_code': lookup_registration_code, + 'action_type': 'unredeem_registration_code' + } + ); + } + is_registration_code_redeemed = is_registration_code_redeemed ? 'Yes' : 'No'; + is_registration_code_valid = is_registration_code_valid ? 'Yes' : 'No'; + // load the underscore template. + var template_data = _.template($('#enrollment-code-lookup-links-tpl').text()); + var registration_code_lookup_actions = template_data( + { + lookup_registration_code: lookup_registration_code, + is_registration_code_redeemed: is_registration_code_redeemed, + is_registration_code_valid: is_registration_code_valid, + actions: actions + } + ); + + // before insertAfter do this. + // remove the first element after the registration_code_status_form + // so it doesn't duplicate the registration_code_lookup_actions in the UI. + $registration_code_status_form.next().remove(); + $(registration_code_lookup_actions).insertAfter($registration_code_status_form); + } + }, + error: function(jqXHR, textStatus, errorThrown) { + var data = $.parseJSON(jqXHR.responseText); + $lookup_button.removeAttr('disabled'); + $registration_code_status_form_error.text(gettext(data.message)); + $registration_code_status_form_error.show(); + } + }); + }); + $("section#invalidate_registration_code_modal").on('click', 'a.registration_code_action_link', function(event) { + event.preventDefault(); + $registration_code_status_form_error.attr('style', 'display: none'); + $lookup_button.attr('disabled', true); + var url = $(this).data('endpoint'); + var action_type = $(this).data('action-type'); + var registration_code = $(this).data('registration-code'); + $.ajax({ + type: "POST", + data: { + "registration_code": registration_code, + "action_type": action_type + }, + url: url, + success: function (data) { + $('#set_regcode_status_form input[name="regcode_code"]').val(''); + $registration_code_status_form.next().remove(); + $registration_code_status_form_error.hide(); + $lookup_button.removeAttr('disabled'); + $registration_code_status_form_success.text(gettext(data.message)); + $registration_code_status_form_success.show(); + $registration_code_status_form_success.fadeOut(3000); + }, + error: function(jqXHR, textStatus, errorThrown) { + var data = $.parseJSON(jqXHR.responseText); + $registration_code_status_form_error.hide(); + $lookup_button.removeAttr('disabled'); + $registration_code_status_form_error.show(); + $registration_code_status_form_error.text(gettext(data.message)); + } + }); + }); }); -})(Backbone, $, _, gettext); \ No newline at end of file +})(Backbone, $, _, gettext); diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss index 028c43bd3c..a32f204f52 100644 --- a/lms/static/sass/course/instructor/_instructor_2.scss +++ b/lms/static/sass/course/instructor/_instructor_2.scss @@ -1624,7 +1624,8 @@ input[name="subject"] { width: 930px; } // coupon edit and add modals - #add-coupon-modal, #edit-coupon-modal, #set-course-mode-price-modal, #registration_code_generation_modal{ + #add-coupon-modal, #invalidate_registration_code_modal, #edit-coupon-modal, + #set-course-mode-price-modal, #registration_code_generation_modal{ .inner-wrapper { background: $white; } @@ -1639,7 +1640,7 @@ input[name="subject"] { @include margin-left(-325px); border-radius: 2px; input[type="button"]#update_coupon_button, input[type="button"]#add_coupon_button, - input[type="button"]#set_course_button { + input[type="button"]#set_course_button, input[type="button"]#lookup_regcode { @include button(simple, $blue); @extend .button-reset; display: block; @@ -1689,6 +1690,25 @@ input[name="subject"] { margin-bottom: 0px !important; } } + table.tb_registration_code_status{ + margin-top: $baseline; + color: #555; + thead { + font-size: 14px; + tr th:last-child { + text-align: center; + } + } + tbody { + font-size: 14px; + tr td:last-child { + text-align: center; + a:first-child{ + margin-right: 10px; + } + } + } + } form#generate_codes ol.list-input{ li{ width: 278px; @@ -1763,7 +1783,7 @@ input[name="subject"] { height: 40px; border-radius: 3px; } - #coupon-content, #course-content, #registration-content { + #coupon-content, #course-content, #registration-content, #regcode-content { padding: $baseline; header { margin: 0; diff --git a/lms/templates/instructor/instructor_dashboard_2/e-commerce.html b/lms/templates/instructor/instructor_dashboard_2/e-commerce.html index 9caf18669c..4b9b30089e 100644 --- a/lms/templates/instructor/instructor_dashboard_2/e-commerce.html +++ b/lms/templates/instructor/instructor_dashboard_2/e-commerce.html @@ -6,16 +6,20 @@ <%include file="edit_coupon_modal.html" args="section_data=section_data" /> <%include file="set_course_mode_price_modal.html" args="section_data=section_data" /> <%include file="generate_registarion_codes_modal.html" args="section_data=section_data" /> +<%include file="invalidate_registration_code_modal.html" args="section_data=section_data" />

-

${_('Registration Codes')}

+

${_('Enrollment Codes')}

%if section_data['sales_admin']:

${_('Create one or more pre-paid course enrollment codes. Students can use these codes to enroll in the course.')}

${_('Create Enrollment Codes')} +

+

${_('Cancel, restore, or mark an enrollment code as unused.')}

+ ${_('Change Enrollment Code Status')}
%endif

${_('Download a .csv file of all enrollment codes for this course')}

@@ -464,6 +468,9 @@ $('#course_price_link').click(function () { reset_input_fields(); }); + $('#query_registration_code_link').click(function () { + reset_input_fields(); + }); $('#add_coupon_link').click(function () { reset_input_fields(); }); @@ -579,6 +586,8 @@ $("#edit-coupon-modal").attr("aria-hidden", "true"); $(".edit-right").focus(); $("#set-course-mode-price-modal").attr("aria-hidden", "true"); + $("#invalidate_registration_code_modal").attr("aria-hidden", "true"); + $("#registration_code_generation_modal").attr("aria-hidden", "true"); $("#add_coupon_button").removeAttr('disabled'); $("#set_course_button").removeAttr('disabled'); @@ -602,10 +611,11 @@ $("#edit-coupon-modal .close-modal").click(onModalClose); $('#registration_code_generation_modal .close-modal').click(onModalClose); $("#set-course-mode-price-modal .close-modal").click(reset_input_fields); + $("#invalidate_registration_code_modal .close-modal").click(reset_input_fields); // Hitting the ESC key will exit the modal - $("#add-coupon-modal, #edit-coupon-modal, #set-course-mode-price-modal, #registration_code_generation_modal").on("keydown", function (e) { + $("#add-coupon-modal, #edit-coupon-modal, #set-course-mode-price-modal, #invalidate_registration_code_modal, #registration_code_generation_modal").on("keydown", function (e) { var keyCode = e.keyCode || e.which; // 27 is the ESC key if (keyCode === 27) { @@ -613,14 +623,20 @@ $("#add-coupon-modal .close-modal").click(); $("#set-course-mode-price-modal .close-modal").click(); $("#edit-coupon-modal .close-modal").click(); + $("#invalidate_registration_code_modal .close-modal").click(); + $('#registration_code_generation_modal .close-modal').click(); } }); }); var reset_input_fields = function () { $('#error-msg').val(''); - $('#error-msg').hide() + $('#error-msg').hide(); $('#add_coupon_form #coupon_form_error').attr('style', 'display: none'); + $("form#set_regcode_status_form").next().remove(); + $('#set_regcode_status_form #regcode_status_form_error').attr('style', 'display: none'); + $('#set_regcode_status_form #regcode_status_form_success').attr('style', 'display: none'); + $('#set_regcode_status_form input#lookup_regcode').removeAttr('disabled'); $('#set_price_form #course_form_error').attr('style', 'display: none'); $('#generate_codes #registration_code_form_error').attr('style', 'display: none'); $('#add_coupon_form #coupon_form_error').text(); @@ -629,6 +645,7 @@ $('input#coupon_discount').val(''); $('textarea#coupon_description').val(''); $('input[name="company_name"]').val(''); + $('input[name="regcode_code"]').val(''); $('input[name="total_registration_codes"]').val(''); $('input[name="address_line_1"]').val(''); $('input[name="address_line_2"]').val(''); diff --git a/lms/templates/instructor/instructor_dashboard_2/enrollment-code-lookup-links.underscore b/lms/templates/instructor/instructor_dashboard_2/enrollment-code-lookup-links.underscore new file mode 100644 index 0000000000..1052b32bc6 --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/enrollment-code-lookup-links.underscore @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + +
<%- gettext("Code") %> <%- gettext("Used") %> <%- gettext("Valid") %> <%- gettext("Actions") %>
<%- lookup_registration_code %> <%- is_registration_code_redeemed %> <%- is_registration_code_valid %> + <% _.each(actions, function(action){ %> + + <%- action.action_name %> + + <% }); %> + +
diff --git a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html index 0a8871f317..a6f8119237 100644 --- a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html +++ b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html @@ -76,7 +76,7 @@ ## Include Underscore templates <%block name="header_extras"> -% for template_name in ["cohorts", "cohort-editor", "cohort-group-header", "cohort-selector", "cohort-form", "notification", "cohort-state", "cohort-discussions-inline", "cohort-discussions-course-wide", "cohort-discussions-category","cohort-discussions-subcategory"]: +% for template_name in ["cohorts", "enrollment-code-lookup-links", "cohort-editor", "cohort-group-header", "cohort-selector", "cohort-form", "notification", "cohort-state", "cohort-discussions-inline", "cohort-discussions-course-wide", "cohort-discussions-category","cohort-discussions-subcategory"]: diff --git a/lms/templates/instructor/instructor_dashboard_2/invalidate_registration_code_modal.html b/lms/templates/instructor/instructor_dashboard_2/invalidate_registration_code_modal.html new file mode 100644 index 0000000000..e55c94d6c5 --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/invalidate_registration_code_modal.html @@ -0,0 +1,42 @@ +<%! from django.utils.translation import ugettext as _ %> +<%! from django.core.urlresolvers import reverse %> +<%page args="section_data"/> + diff --git a/lms/templates/shoppingcart/receipt.html b/lms/templates/shoppingcart/receipt.html index 8fa0dad639..fffc7a2991 100644 --- a/lms/templates/shoppingcart/receipt.html +++ b/lms/templates/shoppingcart/receipt.html @@ -84,6 +84,8 @@ from courseware.courses import course_image_url, get_course_about_section, get_c % if reg_code_info['is_redeemed']: ${_("Used")} + % elif not reg_code_info['is_valid']: + ${_("Invalid")} % else: ${_("Available")} % endif diff --git a/lms/urls.py b/lms/urls.py index f06f358b7c..46d82f125b 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -237,6 +237,19 @@ if settings.WIKI_ENABLED: ) if settings.COURSEWARE_ENABLED: + COURSE_URLS = patterns( + '', + url( + r'^look_up_registration_code$', + 'instructor.views.registration_codes.look_up_registration_code', + name='look_up_registration_code' + ), + url( + r'^registration_code_details$', + 'instructor.views.registration_codes.registration_code_details', + name='registration_code_details' + ) + ) urlpatterns += ( url(r'^courses/{}/jump_to/(?P.*)$'.format(settings.COURSE_ID_PATTERN), 'courseware.views.jump_to', name="jump_to"), @@ -357,6 +370,7 @@ if settings.COURSEWARE_ENABLED: url(r'^courses/{}/get_coupon_info$'.format(settings.COURSE_ID_PATTERN), 'instructor.views.coupons.get_coupon_info', name="get_coupon_info"), + url(r'^courses/{}/'.format(settings.COURSE_ID_PATTERN), include(COURSE_URLS)), # see ENABLE_INSTRUCTOR_LEGACY_DASHBOARD section for legacy dash urls # Open Ended grading views