Commit bfe75276 by chrisndodge

Merge pull request #8292 from edx/muhhshoaib/SOL-916-invalidate-revalidate-registration-codes

SOL 916 invalidate revalidate registration codes
parents f78be26a 4dff8ecf
"""
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'])
...@@ -1253,7 +1253,7 @@ def registration_codes_csv(file_name, codes_list, csv_type=None): ...@@ -1253,7 +1253,7 @@ def registration_codes_csv(file_name, codes_list, csv_type=None):
# csv headers # csv headers
query_features = [ query_features = [
'code', 'redeem_code_url', 'course_id', 'company_name', 'created_by', '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) registration_codes = instructor_analytics.basic.course_registration_features(query_features, codes_list, csv_type)
......
...@@ -205,6 +205,7 @@ def _section_e_commerce(course, access, paid_mode, coupons_enabled, reports_enab ...@@ -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', 'list_financial_report_downloads_url': reverse('list_financial_report_downloads',
kwargs={'course_id': unicode(course_key)}), kwargs={'course_id': unicode(course_key)}),
'list_instructor_tasks_url': reverse('list_instructor_tasks', 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, 'coupons': coupons,
'sales_admin': access['sales_admin'], 'sales_admin': access['sales_admin'],
'coupons_enabled': coupons_enabled, 'coupons_enabled': coupons_enabled,
......
"""
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()
...@@ -32,7 +32,7 @@ SALE_ORDER_FEATURES = ('id', 'company_name', 'company_contact_name', 'company_co ...@@ -32,7 +32,7 @@ SALE_ORDER_FEATURES = ('id', 'company_name', 'company_contact_name', 'company_co
'bill_to_country', 'order_type',) 'bill_to_country', 'order_type',)
AVAILABLE_FEATURES = STUDENT_FEATURES + PROFILE_FEATURES 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') COUPON_FEATURES = ('code', 'course_id', 'percentage_discount', 'description', 'expiration_date', 'is_active')
......
# -*- 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
...@@ -1161,6 +1161,7 @@ class CourseRegistrationCode(models.Model): ...@@ -1161,6 +1161,7 @@ class CourseRegistrationCode(models.Model):
created_at = models.DateTimeField(default=datetime.now(pytz.utc)) created_at = models.DateTimeField(default=datetime.now(pytz.utc))
order = models.ForeignKey(Order, db_index=True, null=True, related_name="purchase_order") order = models.ForeignKey(Order, db_index=True, null=True, related_name="purchase_order")
mode_slug = models.CharField(max_length=100, null=True) mode_slug = models.CharField(max_length=100, null=True)
is_valid = models.BooleanField(default=True)
# For backwards compatibility, we maintain the FK to "invoice" # For backwards compatibility, we maintain the FK to "invoice"
# In the future, we will remove this in favor of the FK # In the future, we will remove this in favor of the FK
...@@ -1196,10 +1197,21 @@ class RegistrationCodeRedemption(models.Model): ...@@ -1196,10 +1197,21 @@ class RegistrationCodeRedemption(models.Model):
Checks the existence of the registration code Checks the existence of the registration code
in the RegistrationCodeRedemption 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 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 @classmethod
def create_invoice_generated_registration_redemption(cls, course_reg_code, user): 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 This function creates a RegistrationCodeRedemption entry in case the registration codes were invoice generated
and thus the order_id is missing. and thus the order_id is missing.
......
...@@ -121,12 +121,14 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -121,12 +121,14 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
percentage_discount=self.percentage_discount, created_by=self.user, is_active=is_active) percentage_discount=self.percentage_discount, created_by=self.user, is_active=is_active)
coupon.save() 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 add dummy registration code into models
""" """
course_reg_code = CourseRegistrationCode( 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() course_reg_code.save()
...@@ -387,6 +389,23 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -387,6 +389,23 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
self.assertEqual(resp.status_code, 404) self.assertEqual(resp.status_code, 404)
self.assertIn("Discount does not exist against code '{0}'.".format(self.coupon_code), resp.content) 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): def test_course_does_not_exist_in_cart_against_valid_reg_code(self):
course_key = self.course_key.to_deprecated_string() + 'testing' course_key = self.course_key.to_deprecated_string() + 'testing'
self.add_reg_code(course_key) self.add_reg_code(course_key)
...@@ -525,7 +544,6 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -525,7 +544,6 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
self.assertEqual(coupon.is_active, False) self.assertEqual(coupon.is_active, False)
def test_course_free_discount_for_valid_active_reg_code(self): def test_course_free_discount_for_valid_active_reg_code(self):
self.add_reg_code(self.course_key) self.add_reg_code(self.course_key)
self.add_course_to_user_cart(self.course_key) self.add_course_to_user_cart(self.course_key)
...@@ -546,7 +564,9 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -546,7 +564,9 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
# the item has been removed when using the registration code for the first time # 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}) resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.reg_code})
self.assertEqual(resp.status_code, 400) 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): def test_upgrade_from_valid_reg_code(self):
"""Use a valid registration code to upgrade from honor to verified mode. """ """Use a valid registration code to upgrade from honor to verified mode. """
......
...@@ -287,17 +287,14 @@ def get_reg_code_validity(registration_code, request, limiter): ...@@ -287,17 +287,14 @@ def get_reg_code_validity(registration_code, request, limiter):
except CourseRegistrationCode.DoesNotExist: except CourseRegistrationCode.DoesNotExist:
reg_code_is_valid = False reg_code_is_valid = False
else: else:
if course_registration.is_valid:
reg_code_is_valid = True reg_code_is_valid = True
try:
RegistrationCodeRedemption.objects.get(registration_code__code=registration_code)
except RegistrationCodeRedemption.DoesNotExist:
reg_code_already_redeemed = False
else: 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: if not reg_code_is_valid:
# tick the rate limiter counter # 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) limiter.tick_bad_request_counter(request)
raise Http404() raise Http404()
...@@ -430,15 +427,24 @@ def _is_enrollment_code_an_update(course, user, redemption_code): ...@@ -430,15 +427,24 @@ def _is_enrollment_code_an_update(course, user, redemption_code):
def use_registration_code(course_reg, user): def use_registration_code(course_reg, user):
""" """
This method utilize course registration code. 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. If the registration code is already redeemed, it returns an error.
Else, it identifies and removes the applicable OrderItem from the Order Else, it identifies and removes the applicable OrderItem from the Order
and redirects the user to the Registration code redemption page. and redirects the user to the Registration code redemption page.
""" """
if RegistrationCodeRedemption.is_registration_code_redeemed(course_reg): if not course_reg.is_valid:
log.warning(u"Registration code '%s' already used", course_reg.code) log.warning(u"The enrollment code (%s) is no longer valid.", course_reg.code)
return HttpResponseBadRequest( return HttpResponseBadRequest(
_("Oops! The code '{registration_code}' you entered is either invalid or expired").format( _("This enrollment code ({enrollment_code}) is no longer valid.").format(
registration_code=course_reg.code 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: try:
...@@ -893,6 +899,7 @@ def _show_receipt_html(request, order): ...@@ -893,6 +899,7 @@ def _show_receipt_html(request, order):
'course_name': course.display_name, 'course_name': course.display_name,
'redemption_url': reverse('register_code_redemption', args=[course_registration_code.code]), 'redemption_url': reverse('register_code_redemption', args=[course_registration_code.code]),
'code': course_registration_code.code, 'code': course_registration_code.code,
'is_valid': course_registration_code.is_valid,
'is_redeemed': RegistrationCodeRedemption.objects.filter( 'is_redeemed': RegistrationCodeRedemption.objects.filter(
registration_code=course_registration_code).exists(), registration_code=course_registration_code).exists(),
}) })
......
...@@ -27,6 +27,11 @@ var edx = edx || {}; ...@@ -27,6 +27,11 @@ var edx = edx || {};
}); });
$(function() { $(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({ $( "#coupon_expiration_date" ).datepicker({
minDate: 0 minDate: 0
}); });
...@@ -52,5 +57,126 @@ var edx = edx || {}; ...@@ -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); })(Backbone, $, _, gettext);
...@@ -1624,7 +1624,8 @@ input[name="subject"] { ...@@ -1624,7 +1624,8 @@ input[name="subject"] {
width: 930px; width: 930px;
} }
// coupon edit and add modals // 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 { .inner-wrapper {
background: $white; background: $white;
} }
...@@ -1639,7 +1640,7 @@ input[name="subject"] { ...@@ -1639,7 +1640,7 @@ input[name="subject"] {
@include margin-left(-325px); @include margin-left(-325px);
border-radius: 2px; border-radius: 2px;
input[type="button"]#update_coupon_button, input[type="button"]#add_coupon_button, 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); @include button(simple, $blue);
@extend .button-reset; @extend .button-reset;
display: block; display: block;
...@@ -1689,6 +1690,25 @@ input[name="subject"] { ...@@ -1689,6 +1690,25 @@ input[name="subject"] {
margin-bottom: 0px !important; 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{ form#generate_codes ol.list-input{
li{ li{
width: 278px; width: 278px;
...@@ -1763,7 +1783,7 @@ input[name="subject"] { ...@@ -1763,7 +1783,7 @@ input[name="subject"] {
height: 40px; height: 40px;
border-radius: 3px; border-radius: 3px;
} }
#coupon-content, #course-content, #registration-content { #coupon-content, #course-content, #registration-content, #regcode-content {
padding: $baseline; padding: $baseline;
header { header {
margin: 0; margin: 0;
......
...@@ -8,16 +8,20 @@ import pytz ...@@ -8,16 +8,20 @@ import pytz
<%include file="edit_coupon_modal.html" args="section_data=section_data" /> <%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="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="generate_registarion_codes_modal.html" args="section_data=section_data" />
<%include file="invalidate_registration_code_modal.html" args="section_data=section_data" />
<div class="ecommerce-wrapper"> <div class="ecommerce-wrapper">
<h3 class="error-msgs" id="error-msg"></h3> <h3 class="error-msgs" id="error-msg"></h3>
<div id = "accordion"> <div id = "accordion">
<div class="wrap"> <div class="wrap">
<h2>${_('Registration Codes')}</h2> <h2>${_('Enrollment Codes')}</h2>
<div> <div>
%if section_data['sales_admin']: %if section_data['sales_admin']:
<span class="code_tip"> <span class="code_tip">
<p>${_('Create one or more pre-paid course enrollment codes. Students can use these codes to enroll in the course.')}</p> <p>${_('Create one or more pre-paid course enrollment codes. Students can use these codes to enroll in the course.')}</p>
<a id="registration_code_generation_link" href="#reg_code_generation_modal" class="add blue-button">${_('Create Enrollment Codes')}</a> <a id="registration_code_generation_link" href="#reg_code_generation_modal" class="add blue-button">${_('Create Enrollment Codes')}</a>
<p></p>
<p>${_('Cancel, restore, or mark an enrollment code as unused.')}</p>
<a id="query_registration_code_link" href="#invalidate_registration_code_modal" rel="leanModal" class="add blue-button">${_('Change Enrollment Code Status')}</a>
</span> </span>
%endif %endif
<p>${_('Download a .csv file of all enrollment codes for this course')}</p> <p>${_('Download a .csv file of all enrollment codes for this course')}</p>
...@@ -466,6 +470,9 @@ import pytz ...@@ -466,6 +470,9 @@ import pytz
$('#course_price_link').click(function () { $('#course_price_link').click(function () {
reset_input_fields(); reset_input_fields();
}); });
$('#query_registration_code_link').click(function () {
reset_input_fields();
});
$('#add_coupon_link').click(function () { $('#add_coupon_link').click(function () {
reset_input_fields(); reset_input_fields();
}); });
...@@ -581,6 +588,8 @@ import pytz ...@@ -581,6 +588,8 @@ import pytz
$("#edit-coupon-modal").attr("aria-hidden", "true"); $("#edit-coupon-modal").attr("aria-hidden", "true");
$(".edit-right").focus(); $(".edit-right").focus();
$("#set-course-mode-price-modal").attr("aria-hidden", "true"); $("#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"); $("#registration_code_generation_modal").attr("aria-hidden", "true");
$("#add_coupon_button").removeAttr('disabled'); $("#add_coupon_button").removeAttr('disabled');
$("#set_course_button").removeAttr('disabled'); $("#set_course_button").removeAttr('disabled');
...@@ -604,10 +613,11 @@ import pytz ...@@ -604,10 +613,11 @@ import pytz
$("#edit-coupon-modal .close-modal").click(onModalClose); $("#edit-coupon-modal .close-modal").click(onModalClose);
$('#registration_code_generation_modal .close-modal').click(onModalClose); $('#registration_code_generation_modal .close-modal').click(onModalClose);
$("#set-course-mode-price-modal .close-modal").click(reset_input_fields); $("#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 // 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; var keyCode = e.keyCode || e.which;
// 27 is the ESC key // 27 is the ESC key
if (keyCode === 27) { if (keyCode === 27) {
...@@ -615,14 +625,20 @@ import pytz ...@@ -615,14 +625,20 @@ import pytz
$("#add-coupon-modal .close-modal").click(); $("#add-coupon-modal .close-modal").click();
$("#set-course-mode-price-modal .close-modal").click(); $("#set-course-mode-price-modal .close-modal").click();
$("#edit-coupon-modal .close-modal").click(); $("#edit-coupon-modal .close-modal").click();
$("#invalidate_registration_code_modal .close-modal").click();
$('#registration_code_generation_modal .close-modal').click(); $('#registration_code_generation_modal .close-modal').click();
} }
}); });
}); });
var reset_input_fields = function () { var reset_input_fields = function () {
$('#error-msg').val(''); $('#error-msg').val('');
$('#error-msg').hide() $('#error-msg').hide();
$('#add_coupon_form #coupon_form_error').attr('style', 'display: none'); $('#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'); $('#set_price_form #course_form_error').attr('style', 'display: none');
$('#generate_codes #registration_code_form_error').attr('style', 'display: none'); $('#generate_codes #registration_code_form_error').attr('style', 'display: none');
$('#add_coupon_form #coupon_form_error').text(); $('#add_coupon_form #coupon_form_error').text();
...@@ -631,6 +647,7 @@ import pytz ...@@ -631,6 +647,7 @@ import pytz
$('input#coupon_discount').val(''); $('input#coupon_discount').val('');
$('textarea#coupon_description').val(''); $('textarea#coupon_description').val('');
$('input[name="company_name"]').val(''); $('input[name="company_name"]').val('');
$('input[name="regcode_code"]').val('');
$('input[name="total_registration_codes"]').val(''); $('input[name="total_registration_codes"]').val('');
$('input[name="address_line_1"]').val(''); $('input[name="address_line_1"]').val('');
$('input[name="address_line_2"]').val(''); $('input[name="address_line_2"]').val('');
......
<table width="100%" class="tb_registration_code_status">
<thead>
<th width="15%"> <%- gettext("Code") %> </th>
<th width="20%"> <%- gettext("Used") %> </th>
<th width="14%"> <%- gettext("Valid") %> </th>
<th> <%- gettext("Actions") %> </th>
</thead>
<tbody>
<tr>
<td> <%- lookup_registration_code %> </td>
<td> <%- is_registration_code_redeemed %> </td>
<td> <%- is_registration_code_valid %> </td>
<td>
<% _.each(actions, function(action){ %>
<a class="registration_code_action_link" data-registration-code="<%= action.registration_code %>" data-action-type="<%= action.action_type %>" href="#" data-endpoint="<%= action.action_url %>">
<%- action.action_name %>
</a>
<% }); %>
</td>
</tr>
</tbody>
</table>
...@@ -77,7 +77,7 @@ from django.core.urlresolvers import reverse ...@@ -77,7 +77,7 @@ from django.core.urlresolvers import reverse
## Include Underscore templates ## Include Underscore templates
<%block name="header_extras"> <%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"]:
<script type="text/template" id="${template_name}-tpl"> <script type="text/template" id="${template_name}-tpl">
<%static:include path="instructor/instructor_dashboard_2/${template_name}.underscore" /> <%static:include path="instructor/instructor_dashboard_2/${template_name}.underscore" />
</script> </script>
......
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%page args="section_data"/>
<section id="invalidate_registration_code_modal" class="modal" role="dialog" tabindex="-1" aria-label="${_('Enrollment Code Status')}">
<div class="inner-wrapper">
<button class="close-modal">
<i class="icon fa fa-remove"></i>
<span class="sr">
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
${_('Close')}
</span>
</button>
<div id="regcode-content">
<header>
<h2>${_("Enrollment Code Status")}</h2>
</header>
<div class="instructions">
<p>
${_("Change the status of an enrollment code.")}</p>
</div>
<form id="set_regcode_status_form">
<div id="regcode_status_form_error" class="modal-form-error"></div>
<div class="success-msgs" id="regcode_status_form_success"></div>
<fieldset class="group group-form group-form-requiredinformation">
<legend class="is-hidden">${_("Required Information")}</legend>
<ol class="list-input">
<li class="field required text" id="set-course-mode-modal-field-price">
<label for="regcode_code">${_("Enrollment Code")}</label>
<input class="field" id="regcode_code" type="text" name="regcode_code" placeholder="Enter an Enrollment Code" aria-required="true">
</li>
</ol>
</fieldset>
<div class="submit">
<input name="submit" type="button" id="lookup_regcode" data-endpoint="${section_data['look_up_registration_code']}" value="${_('Find Enrollment Code')}"/>
</div>
</form>
</div>
</div>
</section>
...@@ -84,6 +84,8 @@ from microsite_configuration import microsite ...@@ -84,6 +84,8 @@ from microsite_configuration import microsite
<td> <td>
% if reg_code_info['is_redeemed']: % if reg_code_info['is_redeemed']:
<span class="red"></M>${_("Used")}</span> <span class="red"></M>${_("Used")}</span>
% elif not reg_code_info['is_valid']:
<span class="red"></M>${_("Invalid")}</span>
% else: % else:
<span class="green"></M>${_("Available")}</span> <span class="green"></M>${_("Available")}</span>
% endif % endif
......
...@@ -237,6 +237,19 @@ if settings.WIKI_ENABLED: ...@@ -237,6 +237,19 @@ if settings.WIKI_ENABLED:
) )
if settings.COURSEWARE_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 += ( urlpatterns += (
url(r'^courses/{}/jump_to/(?P<location>.*)$'.format(settings.COURSE_ID_PATTERN), url(r'^courses/{}/jump_to/(?P<location>.*)$'.format(settings.COURSE_ID_PATTERN),
'courseware.views.jump_to', name="jump_to"), 'courseware.views.jump_to', name="jump_to"),
...@@ -357,6 +370,7 @@ if settings.COURSEWARE_ENABLED: ...@@ -357,6 +370,7 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/{}/get_coupon_info$'.format(settings.COURSE_ID_PATTERN), url(r'^courses/{}/get_coupon_info$'.format(settings.COURSE_ID_PATTERN),
'instructor.views.coupons.get_coupon_info', name="get_coupon_info"), '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 # see ENABLE_INSTRUCTOR_LEGACY_DASHBOARD section for legacy dash urls
# Open Ended grading views # Open Ended grading views
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment