Commit 2cc04338 by Muhammad Shoaib Committed by Chris Dodge

Implement mediated bulk purchase epic

Updating EX-60 logic
parent 27c83d51
......@@ -15,6 +15,8 @@ from xmodule.error_module import ErrorDescriptor
from django.test.client import Client
from student.models import CourseEnrollment
from student.views import get_course_enrollment_pairs
import unittest
from django.conf import settings
class TestCourseListing(ModuleStoreTestCase):
......@@ -54,6 +56,7 @@ class TestCourseListing(ModuleStoreTestCase):
self.client.logout()
super(TestCourseListing, self).tearDown()
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_get_course_list(self):
"""
Test getting courses
......
......@@ -32,6 +32,7 @@ from student.tests.factories import UserFactory, CourseModeFactory
from certificates.models import CertificateStatuses
from certificates.tests.factories import GeneratedCertificateFactory
import shoppingcart
from bulk_email.models import Optout
log = logging.getLogger(__name__)
......@@ -266,6 +267,50 @@ class DashboardTest(TestCase):
self.assertFalse(enrollment.refundable())
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@patch('courseware.views.log.warning')
def test_blocked_course_scenario(self, log_warning):
self.client.login(username="jack", password="test")
#create testing invoice 1
sale_invoice_1 = shoppingcart.models.Invoice.objects.create(
total_amount=1234.32, company_name='Test1', company_contact_name='Testw',
company_contact_email='test1@test.com', customer_reference_number='2Fwe23S',
recipient_name='Testw_1', recipient_email='test2@test.com', internal_reference="A",
course_id=self.course.id, is_valid=False
)
course_reg_code = shoppingcart.models.CourseRegistrationCode(code="abcde", course_id=self.course.id,
created_by=self.user, invoice=sale_invoice_1)
course_reg_code.save()
cart = shoppingcart.models.Order.get_cart_for_user(self.user)
shoppingcart.models.PaidCourseRegistration.add_to_order(cart, self.course.id)
resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': course_reg_code.code})
self.assertEqual(resp.status_code, 200)
# freely enroll the user into course
resp = self.client.get(reverse('shoppingcart.views.register_courses'))
self.assertIn('success', resp.content)
response = self.client.get(reverse('dashboard'))
self.assertIn('You can no longer access this course because payment has not yet been received', response.content)
optout_object = Optout.objects.filter(user=self.user, course_id=self.course.id)
self.assertEqual(len(optout_object), 1)
# Direct link to course redirect to user dashboard
self.client.get(reverse('courseware', kwargs={"course_id": self.course.id.to_deprecated_string()}))
log_warning.assert_called_with(
u'User %s cannot access the course %s because payment has not yet been received', self.user, self.course.id.to_deprecated_string())
# Now re-validating the invoice
invoice = shoppingcart.models.Invoice.objects.get(id=sale_invoice_1.id)
invoice.is_valid = True
invoice.save()
response = self.client.get(reverse('dashboard'))
self.assertNotIn('You can no longer access this course because payment has not yet been received', response.content)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_refundable_of_purchased_course(self):
self.client.login(username="jack", password="test")
......@@ -315,6 +360,7 @@ class EnrollInCourseTest(TestCase):
self.mock_tracker = patcher.start()
self.addCleanup(patcher.stop)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_enrollment(self):
user = User.objects.create_user("joe", "joe@joe.com", "password")
course_id = SlashSeparatedCourseKey("edX", "Test101", "2013")
......@@ -419,6 +465,7 @@ class EnrollInCourseTest(TestCase):
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
self.assert_enrollment_event_was_emitted(user, course_id)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_enrollment_by_email(self):
user = User.objects.create(username="jack", email="jack@fake.edx.org")
course_id = SlashSeparatedCourseKey("edX", "Test101", "2013")
......@@ -456,6 +503,7 @@ class EnrollInCourseTest(TestCase):
CourseEnrollment.unenroll_by_email("not_jack@fake.edx.org", course_id)
self.assert_no_events_were_emitted()
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_enrollment_multiple_classes(self):
user = User(username="rusty", email="rusty@fake.edx.org")
course_id1 = SlashSeparatedCourseKey("edX", "Test101", "2013")
......@@ -478,6 +526,7 @@ class EnrollInCourseTest(TestCase):
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id1))
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id2))
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_activation(self):
user = User.objects.create(username="jack", email="jack@fake.edx.org")
course_id = SlashSeparatedCourseKey("edX", "Test101", "2013")
......
......@@ -92,6 +92,7 @@ from util.password_policy_validators import (
from third_party_auth import pipeline, provider
from xmodule.error_module import ErrorDescriptor
from shoppingcart.models import CourseRegistrationCode
import analytics
from eventtracking import tracker
......@@ -431,6 +432,20 @@ def complete_course_mode_info(course_id, enrollment):
return mode_info
def is_course_blocked(request, redeemed_registration_codes, course_key):
"""Checking either registration is blocked or not ."""
blocked = False
for redeemed_registration in redeemed_registration_codes:
if not getattr(redeemed_registration.invoice, 'is_valid'):
blocked = True
# disabling email notifications for unpaid registration courses
Optout.objects.get_or_create(user=request.user, course_id=course_key)
log.info(u"User {0} ({1}) opted out of receiving emails from course {2}".format(request.user.username, request.user.email, course_key))
track.views.server_track(request, "change-email1-settings", {"receive_emails": "no", "course": course_key.to_deprecated_string()}, page='dashboard')
break
return blocked
@login_required
@ensure_csrf_cookie
def dashboard(request):
......@@ -493,6 +508,10 @@ def dashboard(request):
show_refund_option_for = frozenset(course.id for course, _enrollment in course_enrollment_pairs
if _enrollment.refundable())
block_courses = frozenset(course.id for course, enrollment in course_enrollment_pairs
if is_course_blocked(request, CourseRegistrationCode.objects.filter(course_id=course.id, registrationcoderedemption__redeemed_by=request.user), course.id))
enrolled_courses_either_paid = frozenset(course.id for course, _enrollment in course_enrollment_pairs
if _enrollment.is_paid_course())
# get info w.r.t ExternalAuthMap
......@@ -544,6 +563,7 @@ def dashboard(request):
'verification_status': verification_status,
'verification_msg': verification_msg,
'show_refund_option_for': show_refund_option_for,
'block_courses': block_courses,
'denied_banner': denied_banner,
'billing_email': settings.PAYMENT_SUPPORT_EMAIL,
'language_options': language_options,
......
......@@ -46,6 +46,7 @@ from xmodule.modulestore.search import path_to_location
from xmodule.tabs import CourseTabList, StaffGradingTab, PeerGradingTab, OpenEndedGradingTab
from xmodule.x_module import STUDENT_VIEW
import shoppingcart
from shoppingcart.models import CourseRegistrationCode
from opaque_keys import InvalidKeyError
from microsite_configuration import microsite
......@@ -291,6 +292,14 @@ def index(request, course_id, chapter=None, section=None,
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
user = User.objects.prefetch_related("groups").get(id=request.user.id)
# Redirecting to dashboard if the course is blocked due to un payment.
redeemed_registration_codes = CourseRegistrationCode.objects.filter(course_id=course_key, registrationcoderedemption__redeemed_by=request.user)
for redeemed_registration in redeemed_registration_codes:
if not getattr(redeemed_registration.invoice, 'is_valid'):
log.warning(u'User %s cannot access the course %s because payment has not yet been received', user, course_key.to_deprecated_string())
return redirect(reverse('dashboard'))
request.user = user # keep just one instance of User
course = get_course_with_access(user, 'load', course_key, depth=2)
staff_access = has_access(user, 'staff', course)
......
......@@ -175,8 +175,7 @@ class TestECommerceDashboardViews(ModuleStoreTestCase):
self.assertTrue('Please Enter the Integer Value for Coupon Discount' in response.content)
course_registration = CourseRegistrationCode(
code='Vs23Ws4j', course_id=self.course.id.to_deprecated_string(),
transaction_group_name='Test Group', created_by=self.instructor
code='Vs23Ws4j', course_id=self.course.id.to_deprecated_string(), created_by=self.instructor
)
course_registration.save()
......@@ -254,7 +253,7 @@ class TestECommerceDashboardViews(ModuleStoreTestCase):
response = self.client.post(self.url)
self.assertTrue('<td>AS452</td>' in response.content)
data = {
'coupon_id': coupon.id, 'code': 'update_code', 'discount': '12',
'coupon_id': coupon.id, 'code': 'AS452', 'discount': '10', 'description': 'updated_description', # pylint: disable=E1101
'course_id': coupon.course_id.to_deprecated_string()
}
# URL for update_coupon
......@@ -263,43 +262,12 @@ class TestECommerceDashboardViews(ModuleStoreTestCase):
self.assertTrue('coupon with the coupon id ({coupon_id}) updated Successfully'.format(coupon_id=coupon.id)in response.content)
response = self.client.post(self.url)
self.assertTrue('<td>update_code</td>' in response.content)
self.assertTrue('<td>12</td>' in response.content)
self.assertTrue('<td>updated_description</td>' in response.content)
data['coupon_id'] = 1000 # Coupon Not Exist with this ID
response = self.client.post(update_coupon_url, data=data)
self.assertTrue('coupon with the coupon id ({coupon_id}) DoesNotExist'.format(coupon_id=1000) in response.content)
data['coupon_id'] = coupon.id
data['discount'] = 123
response = self.client.post(update_coupon_url, data=data)
self.assertTrue('Please Enter the Coupon Discount Value Less than or Equal to 100' in response.content)
data['discount'] = '25%'
response = self.client.post(update_coupon_url, data=data)
self.assertTrue('Please Enter the Integer Value for Coupon Discount' in response.content)
data['coupon_id'] = '' # Coupon id is not provided
response = self.client.post(update_coupon_url, data=data)
self.assertTrue('coupon id not found' in response.content)
coupon1 = Coupon(
code='11111', description='coupon', course_id=self.course.id.to_deprecated_string(),
percentage_discount=20, created_by=self.instructor
)
coupon1.save()
data = {'coupon_id': coupon.id, 'code': '11111', 'discount': '12'} # pylint: disable=E1101
response = self.client.post(update_coupon_url, data=data)
self.assertTrue('coupon with the coupon id ({coupon_id}) already exist'.format(coupon_id=coupon.id) in response.content) # pylint: disable=E1101
course_registration = CourseRegistrationCode(
code='Vs23Ws4j', course_id=self.course.id.to_deprecated_string(),
transaction_group_name='Test Group', created_by=self.instructor
)
course_registration.save()
data = {'coupon_id': coupon.id, 'code': 'Vs23Ws4j', # pylint: disable=E1101
'discount': '6', 'course_id': coupon.course_id.to_deprecated_string()} # pylint: disable=E1101
response = self.client.post(update_coupon_url, data=data)
self.assertTrue("The code ({code}) that you have tried to define is already in use as a registration code".
format(code=data['code']) in response.content)
"""
This is the UserPreference key for the user's recipient invoice copy
"""
INVOICE_KEY = 'pref-invoice-copy'
......@@ -19,6 +19,12 @@ urlpatterns = patterns('', # nopep8
'instructor.views.api.get_students_features', name="get_students_features"),
url(r'^get_purchase_transaction(?P<csv>/csv)?$',
'instructor.views.api.get_purchase_transaction', name="get_purchase_transaction"),
url(r'^get_user_invoice_preference$',
'instructor.views.api.get_user_invoice_preference', name="get_user_invoice_preference"),
url(r'^get_sale_records(?P<csv>/csv)?$',
'instructor.views.api.get_sale_records', name="get_sale_records"),
url(r'^sale_validation_url$',
'instructor.views.api.sale_validation', name="sale_validation"),
url(r'^get_anon_ids$',
'instructor.views.api.get_anon_ids', name="get_anon_ids"),
url(r'^get_distribution$',
......@@ -68,6 +74,10 @@ urlpatterns = patterns('', # nopep8
url(r'spent_registration_codes$',
'instructor.views.api.spent_registration_codes', name="spent_registration_codes"),
# Coupon Codes..
url(r'get_coupon_codes',
'instructor.views.api.get_coupon_codes', name="get_coupon_codes"),
# spoc gradebook
url(r'^gradebook$',
'instructor.views.api.spoc_gradebook', name='spoc_gradebook'),
......
......@@ -97,31 +97,8 @@ def update_coupon(request, course_id): # pylint: disable=W0613
except ObjectDoesNotExist:
return HttpResponseNotFound(_("coupon with the coupon id ({coupon_id}) DoesNotExist").format(coupon_id=coupon_id))
code = request.POST.get('code')
filtered_coupons = Coupon.objects.filter(~Q(id=coupon_id), code=code, is_active=True)
if filtered_coupons:
return HttpResponseNotFound(_("coupon with the coupon id ({coupon_id}) already exists").format(coupon_id=coupon_id))
# check if the coupon code is in the CourseRegistrationCode Table
course_registration_code = CourseRegistrationCode.objects.filter(code=code)
if course_registration_code:
return HttpResponseNotFound(_(
"The code ({code}) that you have tried to define is already in use as a registration code").format(code=code)
)
description = request.POST.get('description')
course_id = request.POST.get('course_id')
try:
discount = int(request.POST.get('discount'))
except ValueError:
return HttpResponseNotFound(_("Please Enter the Integer Value for Coupon Discount"))
if discount > 100:
return HttpResponseNotFound(_("Please Enter the Coupon Discount Value Less than or Equal to 100"))
coupon.code = code
coupon.description = description
coupon.course_id = course_id
coupon.percentage_discount = discount
coupon.save()
return HttpResponse(_("coupon with the coupon id ({coupon_id}) updated Successfully").format(coupon_id=coupon_id))
......
......@@ -156,15 +156,19 @@ def _section_e_commerce(course_key, access):
'course_id': course_key.to_deprecated_string(),
'ajax_remove_coupon_url': reverse('remove_coupon', kwargs={'course_id': course_key.to_deprecated_string()}),
'ajax_get_coupon_info': reverse('get_coupon_info', kwargs={'course_id': course_key.to_deprecated_string()}),
'get_user_invoice_preference_url': reverse('get_user_invoice_preference', kwargs={'course_id': course_key.to_deprecated_string()}),
'sale_validation_url': reverse('sale_validation', kwargs={'course_id': course_key.to_deprecated_string()}),
'ajax_update_coupon': reverse('update_coupon', kwargs={'course_id': course_key.to_deprecated_string()}),
'ajax_add_coupon': reverse('add_coupon', kwargs={'course_id': course_key.to_deprecated_string()}),
'get_purchase_transaction_url': reverse('get_purchase_transaction', kwargs={'course_id': course_key.to_deprecated_string()}),
'get_sale_records_url': reverse('get_sale_records', kwargs={'course_id': course_key.to_deprecated_string()}),
'instructor_url': reverse('instructor_dashboard', kwargs={'course_id': course_key.to_deprecated_string()}),
'get_registration_code_csv_url': reverse('get_registration_codes', kwargs={'course_id': course_key.to_deprecated_string()}),
'generate_registration_code_csv_url': reverse('generate_registration_codes', kwargs={'course_id': course_key.to_deprecated_string()}),
'active_registration_code_csv_url': reverse('active_registration_codes', kwargs={'course_id': course_key.to_deprecated_string()}),
'spent_registration_code_csv_url': reverse('spent_registration_codes', kwargs={'course_id': course_key.to_deprecated_string()}),
'set_course_mode_url': reverse('set_course_mode_price', kwargs={'course_id': course_key.to_deprecated_string()}),
'download_coupon_codes_url': reverse('get_coupon_codes', kwargs={'course_id': course_key.to_deprecated_string()}),
'coupons': coupons,
'total_amount': total_amount,
'course_price': course_price
......
......@@ -3,7 +3,7 @@ Student and course analytics.
Serve miscellaneous course and student data
"""
from shoppingcart.models import PaidCourseRegistration, CouponRedemption
from shoppingcart.models import PaidCourseRegistration, CouponRedemption, Invoice, RegistrationCodeRedemption
from django.contrib.auth.models import User
import xmodule.graders as xmgraders
from django.core.exceptions import ObjectDoesNotExist
......@@ -15,8 +15,58 @@ PROFILE_FEATURES = ('name', 'language', 'location', 'year_of_birth', 'gender',
ORDER_ITEM_FEATURES = ('list_price', 'unit_cost', 'order_id')
ORDER_FEATURES = ('purchase_time',)
SALE_FEATURES = ('total_amount', 'company_name', 'company_contact_name', 'company_contact_email', 'recipient_name',
'recipient_email', 'customer_reference_number', 'internal_reference')
AVAILABLE_FEATURES = STUDENT_FEATURES + PROFILE_FEATURES
COURSE_REGISTRATION_FEATURES = ('code', 'course_id', 'transaction_group_name', 'created_by')
COURSE_REGISTRATION_FEATURES = ('code', 'course_id', 'created_by', 'created_at')
COUPON_FEATURES = ('course_id', 'percentage_discount', 'description')
def sale_record_features(course_id, features):
"""
Return list of sales features as dictionaries.
sales_records(course_id, ['company_name, total_codes', total_amount])
would return [
{'company_name': 'group_A', 'total_codes': '1', total_amount:'total_amount1 in decimal'.}
{'company_name': 'group_B', 'total_codes': '2', total_amount:'total_amount2 in decimal'.}
{'company_name': 'group_C', 'total_codes': '3', total_amount:'total_amount3 in decimal'.}
]
"""
sales = Invoice.objects.filter(course_id=course_id)
def sale_records_info(sale, features):
""" convert sales records to dictionary """
sale_features = [x for x in SALE_FEATURES if x in features]
course_reg_features = [x for x in COURSE_REGISTRATION_FEATURES if x in features]
# Extracting sale information
sale_dict = dict((feature, getattr(sale, feature))
for feature in sale_features)
total_used_codes = RegistrationCodeRedemption.objects.filter(registration_code__in=sale.courseregistrationcode_set.all()).count()
sale_dict.update({"invoice_number": getattr(sale, 'id')})
sale_dict.update({"total_codes": sale.courseregistrationcode_set.all().count()})
sale_dict.update({'total_used_codes': total_used_codes})
codes = list()
for reg_code in sale.courseregistrationcode_set.all():
codes.append(reg_code.code)
# Extracting registration code information
obj_course_reg_code = sale.courseregistrationcode_set.all()[:1].get()
course_reg_dict = dict((feature, getattr(obj_course_reg_code, feature))
for feature in course_reg_features)
course_reg_dict['course_id'] = course_id.to_deprecated_string()
course_reg_dict.update({'codes': ", ".join(codes)})
sale_dict.update(dict(course_reg_dict.items()))
return sale_dict
return [sale_records_info(sale, features) for sale in sales]
def purchase_transactions(course_id, features):
......@@ -100,6 +150,36 @@ def enrolled_students_features(course_id, features):
return [extract_student(student, features) for student in students]
def coupon_codes_features(features, coupons_list):
"""
Return list of Coupon Codes as dictionaries.
coupon_codes_features
would return [
{'course_id': 'edX/Open_DemoX/edx_demo_course,, 'discount': '213' ..... }
{'course_id': 'edX/Open_DemoX/edx_demo_course,, 'discount': '234' ..... }
]
"""
def extract_coupon(coupon, features):
""" convert coupon_codes to dictionary
:param coupon_codes:
:param features:
"""
coupon_features = [x for x in COUPON_FEATURES if x in features]
coupon_dict = dict((feature, getattr(coupon, feature)) for feature in coupon_features)
coupon_dict['code_redeemed_count'] = coupon.couponredemption_set.all().count()
# we have to capture the redeemed_by value in the case of the downloading and spent registration
# codes csv. In the case of active and generated registration codes the redeemed_by value will be None.
# They have not been redeemed yet
coupon_dict['course_id'] = coupon_dict['course_id'].to_deprecated_string()
return coupon_dict
return [extract_coupon(coupon, features) for coupon in coupons_list]
def course_registration_features(features, registration_codes, csv_type):
"""
Return list of Course Registration Codes as dictionaries.
......@@ -120,14 +200,24 @@ def course_registration_features(features, registration_codes, csv_type):
registration_features = [x for x in COURSE_REGISTRATION_FEATURES if x in features]
course_registration_dict = dict((feature, getattr(registration_code, feature)) for feature in registration_features)
course_registration_dict['company_name'] = None
if registration_code.invoice:
course_registration_dict['company_name'] = getattr(registration_code.invoice, 'company_name')
course_registration_dict['redeemed_by'] = None
if registration_code.invoice:
sale_invoice = Invoice.objects.get(id=registration_code.invoice_id)
course_registration_dict['invoice_id'] = sale_invoice.id
course_registration_dict['purchaser'] = sale_invoice.recipient_name
course_registration_dict['customer_reference_number'] = sale_invoice.customer_reference_number
course_registration_dict['internal_reference'] = sale_invoice.internal_reference
# we have to capture the redeemed_by value in the case of the downloading and spent registration
# codes csv. In the case of active and generated registration codes the redeemed_by value will be None.
# They have not been redeemed yet
if csv_type is not None:
try:
course_registration_dict['redeemed_by'] = getattr(registration_code.registrationcoderedemption_set.get(registration_code=registration_code), 'redeemed_by')
redeemed_by = getattr(registration_code.registrationcoderedemption_set.get(registration_code=registration_code), 'redeemed_by')
course_registration_dict['redeemed_by'] = getattr(redeemed_by, 'email')
except ObjectDoesNotExist:
pass
......
......@@ -4,11 +4,17 @@ Tests for instructor.basic
from django.test import TestCase
from student.models import CourseEnrollment
from django.core.urlresolvers import reverse
from student.tests.factories import UserFactory
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from shoppingcart.models import CourseRegistrationCode, RegistrationCodeRedemption, Order
from shoppingcart.models import CourseRegistrationCode, RegistrationCodeRedemption, Order, Invoice, Coupon
from instructor_analytics.basic import enrolled_students_features, course_registration_features, AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES
from instructor_analytics.basic import (
sale_record_features, enrolled_students_features, course_registration_features, coupon_codes_features,
AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES
)
from courseware.tests.factories import InstructorFactory
from xmodule.modulestore.tests.factories import CourseFactory
class TestAnalyticsBasic(TestCase):
......@@ -19,6 +25,7 @@ class TestAnalyticsBasic(TestCase):
self.users = tuple(UserFactory() for _ in xrange(30))
self.ces = tuple(CourseEnrollment.enroll(user, self.course_key)
for user in self.users)
self.instructor = InstructorFactory(course_key=self.course_key)
def test_enrolled_students_features_username(self):
self.assertIn('username', AVAILABLE_FEATURES)
......@@ -44,20 +51,89 @@ class TestAnalyticsBasic(TestCase):
self.assertEqual(len(AVAILABLE_FEATURES), len(STUDENT_FEATURES + PROFILE_FEATURES))
self.assertEqual(set(AVAILABLE_FEATURES), set(STUDENT_FEATURES + PROFILE_FEATURES))
def test_course_registration_features(self):
query_features = ['code', 'course_id', 'transaction_group_name', 'created_by', 'redeemed_by']
class TestCourseSaleRecordsAnalyticsBasic(TestCase):
""" Test basic course sale records analytics functions. """
def setUp(self):
"""
Fixtures.
"""
self.course = CourseFactory.create()
self.instructor = InstructorFactory(course_key=self.course.id)
self.client.login(username=self.instructor.username, password='test')
def test_course_sale_features(self):
query_features = [
'company_name', 'company_contact_name', 'company_contact_email', 'total_codes', 'total_used_codes',
'total_amount', 'created_at', 'customer_reference_number', 'recipient_name', 'recipient_email',
'created_by', 'internal_reference', 'invoice_number', 'codes', 'course_id'
]
#create invoice
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_1', recipient_email='test2@test.com',
customer_reference_number='2Fwe23S', internal_reference="ABC", course_id=self.course.id
)
for i in range(5):
course_code = CourseRegistrationCode(
code="test_code{}".format(i), course_id=self.course_key.to_deprecated_string(),
transaction_group_name='TestName', created_by=self.users[0]
code="test_code{}".format(i), course_id=self.course.id.to_deprecated_string(),
created_by=self.instructor, invoice=sale_invoice
)
course_code.save()
order = Order(user=self.users[0], status='purchased')
course_sale_records_list = sale_record_features(self.course.id, query_features)
for sale_record in course_sale_records_list:
self.assertEqual(sale_record['total_amount'], sale_invoice.total_amount)
self.assertEqual(sale_record['recipient_email'], sale_invoice.recipient_email)
self.assertEqual(sale_record['recipient_name'], sale_invoice.recipient_name)
self.assertEqual(sale_record['company_name'], sale_invoice.company_name)
self.assertEqual(sale_record['company_contact_name'], sale_invoice.company_contact_name)
self.assertEqual(sale_record['company_contact_email'], sale_invoice.company_contact_email)
self.assertEqual(sale_record['internal_reference'], sale_invoice.internal_reference)
self.assertEqual(sale_record['customer_reference_number'], sale_invoice.customer_reference_number)
self.assertEqual(sale_record['invoice_number'], sale_invoice.id)
self.assertEqual(sale_record['created_by'], self.instructor)
self.assertEqual(sale_record['total_used_codes'], 0)
self.assertEqual(sale_record['total_codes'], 5)
class TestCourseRegistrationCodeAnalyticsBasic(TestCase):
""" Test basic course registration codes analytics functions. """
def setUp(self):
"""
Fixtures.
"""
self.course = CourseFactory.create()
self.instructor = InstructorFactory(course_key=self.course.id)
self.client.login(username=self.instructor.username, password='test')
url = reverse('generate_registration_codes',
kwargs={'course_id': self.course.id.to_deprecated_string()})
data = {
'total_registration_codes': 12, 'company_name': 'Test Group', 'sale_price': 122.45,
'company_contact_name': 'TestName', 'company_contact_email': 'test@company.com', '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, **{'HTTP_HOST': 'localhost'})
self.assertEqual(response.status_code, 200, response.content)
def test_course_registration_features(self):
query_features = [
'code', 'course_id', 'company_name', 'created_by',
'redeemed_by', 'invoice_id', 'purchaser', 'customer_reference_number', 'internal_reference'
]
order = Order(user=self.instructor, status='purchased')
order.save()
registration_code_redemption = RegistrationCodeRedemption(
order=order, registration_code_id=1, redeemed_by=self.users[0]
order=order, registration_code_id=1, redeemed_by=self.instructor
)
registration_code_redemption.save()
registration_codes = CourseRegistrationCode.objects.all()
......@@ -71,6 +147,32 @@ class TestAnalyticsBasic(TestCase):
[registration_code.course_id.to_deprecated_string() for registration_code in registration_codes]
)
self.assertIn(
course_registration['transaction_group_name'],
[registration_code.transaction_group_name for registration_code in registration_codes]
course_registration['company_name'],
[getattr(registration_code.invoice, 'company_name') for registration_code in registration_codes]
)
self.assertIn(
course_registration['invoice_id'],
[registration_code.invoice_id for registration_code in registration_codes]
)
def test_coupon_codes_features(self):
query_features = [
'course_id', 'percentage_discount', 'code_redeemed_count', 'description'
]
for i in range(10):
coupon = Coupon(
code='test_code{0}'.format(i), description='test_description', course_id=self.course.id,
percentage_discount='{0}'.format(i), created_by=self.instructor, is_active=True
)
coupon.save()
active_coupons = Coupon.objects.filter(course_id=self.course.id, is_active=True)
active_coupons_list = coupon_codes_features(query_features, active_coupons)
self.assertEqual(len(active_coupons_list), len(active_coupons))
for active_coupon in active_coupons_list:
self.assertEqual(set(active_coupon.keys()), set(query_features))
self.assertIn(active_coupon['percentage_discount'], [coupon.percentage_discount for coupon in active_coupons])
self.assertIn(active_coupon['description'], [coupon.description for coupon in active_coupons])
self.assertIn(
active_coupon['course_id'],
[coupon.course_id.to_deprecated_string() for coupon in active_coupons]
)
......@@ -317,6 +317,31 @@ class OrderItem(models.Model):
return ''
class Invoice(models.Model):
"""
This table capture all the information needed to support "invoicing"
which is when a user wants to purchase Registration Codes,
but will not do so via a Credit Card transaction.
"""
company_name = models.CharField(max_length=255, db_index=True)
company_contact_name = models.CharField(max_length=255)
company_contact_email = models.CharField(max_length=255)
recipient_name = models.CharField(max_length=255)
recipient_email = models.CharField(max_length=255)
address_line_1 = models.CharField(max_length=255)
address_line_2 = models.CharField(max_length=255, null=True)
address_line_3 = models.CharField(max_length=255, null=True)
city = models.CharField(max_length=255, null=True)
state = models.CharField(max_length=255, null=True)
zip = models.CharField(max_length=15, null=True)
country = models.CharField(max_length=64, null=True)
course_id = CourseKeyField(max_length=255, db_index=True)
total_amount = models.FloatField()
internal_reference = models.CharField(max_length=255, null=True)
customer_reference_number = models.CharField(max_length=63, null=True)
is_valid = models.BooleanField(default=True)
class CourseRegistrationCode(models.Model):
"""
This table contains registration codes
......@@ -324,9 +349,9 @@ class CourseRegistrationCode(models.Model):
"""
code = models.CharField(max_length=32, db_index=True, unique=True)
course_id = CourseKeyField(max_length=255, db_index=True)
transaction_group_name = models.CharField(max_length=255, db_index=True, null=True, blank=True)
created_by = models.ForeignKey(User, related_name='created_by_user')
created_at = models.DateTimeField(default=datetime.now(pytz.utc))
invoice = models.ForeignKey(Invoice, null=True)
@classmethod
@transaction.commit_on_success
......@@ -574,7 +599,7 @@ class PaidCourseRegistration(OrderItem):
Generates instructions when the user has purchased a PaidCourseRegistration.
Basically tells the user to visit the dashboard to see their new classes
"""
notification = (_('Please visit your <a href="{dashboard_link}">dashboard</a> to see your new enrollments.')
notification = (_('Please visit your <a href="{dashboard_link}">dashboard</a> to see your new course.')
.format(dashboard_link=reverse('dashboard')))
return self.pk_with_subclass, set([notification])
......
......@@ -96,8 +96,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
"""
add dummy registration code into models
"""
course_reg_code = CourseRegistrationCode(code=self.reg_code, course_id=course_key,
transaction_group_name='A', created_by=self.user)
course_reg_code = CourseRegistrationCode(code=self.reg_code, course_id=course_key, created_by=self.user)
course_reg_code.save()
def add_course_to_user_cart(self):
......
......@@ -10,19 +10,17 @@ class ECommerce
@$section.data 'wrapper', @
# gather elements
@$list_purchase_csv_btn = @$section.find("input[name='list-purchase-transaction-csv']'")
@$transaction_group_name = @$section.find("input[name='transaction_group_name']'")
@$course_registration_number = @$section.find("input[name='course_registration_code_number']'")
@$download_transaction_group_name = @$section.find("input[name='download_transaction_group_name']'")
@$active_transaction_group_name = @$section.find("input[name='active_transaction_group_name']'")
@$spent_transaction_group_name = @$section.find('input[name="spent_transaction_group_name"]')
@$generate_registration_code_form = @$section.find("form#course_codes_number")
@$list_sale_csv_btn = @$section.find("input[name='list-sale-csv']'")
@$download_company_name = @$section.find("input[name='download_company_name']'")
@$active_company_name = @$section.find("input[name='active_company_name']'")
@$spent_company_name = @$section.find('input[name="spent_company_name"]')
@$download_coupon_codes = @$section.find('input[name="download-coupon-codes-csv"]')
@$download_registration_codes_form = @$section.find("form#download_registration_codes")
@$active_registration_codes_form = @$section.find("form#active_registration_codes")
@$spent_registration_codes_form = @$section.find("form#spent_registration_codes")
@$coupoon_error = @$section.find('#coupon-error')
@$course_code_error = @$section.find('#code-error')
@$error_msg = @$section.find('#error-msg')
# attach click handlers
# this handler binds to both the download
......@@ -31,47 +29,28 @@ class ECommerce
url = @$list_purchase_csv_btn.data 'endpoint'
url += '/csv'
location.href = url
@$list_sale_csv_btn.click (e) =>
url = @$list_sale_csv_btn.data 'endpoint'
url += '/csv'
location.href = url
@$download_coupon_codes.click (e) =>
url = @$download_coupon_codes.data 'endpoint'
location.href = url
@$download_registration_codes_form.submit (e) =>
@$course_code_error.attr('style', 'display: none')
@$coupoon_error.attr('style', 'display: none')
@$error_msg.attr('style', 'display: none')
return true
@$active_registration_codes_form.submit (e) =>
@$course_code_error.attr('style', 'display: none')
@$coupoon_error.attr('style', 'display: none')
@$error_msg.attr('style', 'display: none')
return true
@$spent_registration_codes_form.submit (e) =>
@$course_code_error.attr('style', 'display: none')
@$coupoon_error.attr('style', 'display: none')
@$error_msg.attr('style', 'display: none')
return true
@$generate_registration_code_form.submit (e) =>
@$course_code_error.attr('style', 'display: none')
@$coupoon_error.attr('style', 'display: none')
group_name = @$transaction_group_name.val()
if group_name == ''
@$course_code_error.html('Please Enter the Transaction Group Name').show()
return false
if ($.isNumeric(group_name))
@$course_code_error.html('Please Enter the non-numeric value for Transaction Group Name').show()
return false;
registration_codes = @$course_registration_number.val();
if (isInt(registration_codes) && $.isNumeric(registration_codes))
if (parseInt(registration_codes) > 1000 )
@$course_code_error.html('You can only generate 1000 Registration Codes at a time').show()
return false;
if (parseInt(registration_codes) == 0 )
@$course_code_error.html('Please Enter the Value greater than 0 for Registration Codes').show()
return false;
return true;
else
@$course_code_error.html('Please Enter the Integer Value for Registration Codes').show()
return false;
# handler for when the section title is clicked.
onClickTitle: ->
@clear_display()
......@@ -83,14 +62,10 @@ class ECommerce
onExit: -> @clear_display()
clear_display: ->
@$course_code_error.attr('style', 'display: none')
@$coupoon_error.attr('style', 'display: none')
@$course_registration_number.val('')
@$transaction_group_name.val('')
@$download_transaction_group_name.val('')
@$active_transaction_group_name.val('')
@$spent_transaction_group_name.val('')
@$error_msg.attr('style', 'display: none')
@$download_company_name.val('')
@$active_company_name.val('')
@$spent_company_name.val('')
isInt = (n) -> return n % 1 == 0;
# Clear any generated tables, warning messages, etc.
......
......@@ -831,27 +831,32 @@ input[name="subject"] {
}
#e-commerce{
.coupon-errors {
input {
margin-bottom: 1em;
line-height: 1.3em;
}
.error-msgs {
background: #FFEEF5;color:#B72667;text-align: center;padding: 10px 0px;
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;font-size: 15px;
border-bottom: 1px solid #B72667;
margin-bottom: 20px;
display: none;
}
.success-msgs {
background: #D0F5D5;color:#008801;text-align: center;padding: 10px 0px;
font-family: "Open Sans",Verdana,Geneva,sans-serif,sans-serif;font-size: 15px;
border-bottom: 1px solid #008801;
margin-bottom: 20px;
display: none;
}
.content{
padding: 0 !important;
}
input[name="course_registration_code_number"] {
margin-right: 10px;
height: 34px;
width: 258px;
border-radius: 3px;
}
input[name="transaction_group_name"], input[name="download_transaction_group_name"],
input[name="active_transaction_group_name"], input[name="spent_transaction_group_name"] {
input[name="download_company_name"],
input[name="active_company_name"], input[name="spent_company_name"] {
margin-right: 8px;
height: 36px;
width: 300px;
width: 254px;
border-radius: 3px;
}
.coupons-table {
......@@ -955,12 +960,21 @@ input[name="subject"] {
}
}
}
section#registration_code_generation_modal {
margin-left: -442px;
width: 930px;
}
// coupon edit and add modals
#add-coupon-modal, #edit-coupon-modal, #set-course-mode-price-modal{
#add-coupon-modal, #edit-coupon-modal, #set-course-mode-price-modal, #registration_code_generation_modal{
.inner-wrapper {
background: #fff;
}
span.tip-text {
font-size: 12px;
display: block;
margin-top: 5px;
color: #646464
}
top:-95px !important;
width: 650px;
margin-left: -325px;
......@@ -973,6 +987,10 @@ input[name="subject"] {
@include button(simple, $blue);
@extend .button-reset;
}
input[name="generate-registration-codes-csv"]{
@include button(simple, $blue);
@extend .button-reset;
}
input[type="submit"]#set_course_button{
@include button(simple, $blue);
@extend .button-reset;
......@@ -1014,6 +1032,61 @@ input[name="subject"] {
margin-bottom: 0px !important;
}
}
form#generate_codes ol.list-input{
li{
width: 278px;
float: left;
label.required:after {
content: "*";
margin-left: 5px;
}
min-height: 120px;
}
li.address_fields {
min-height: 45px !important;
}
li#generate-registration-modal-field-city, li#generate-registration-modal-field-state,
li#generate-registration-modal-field-zipcode{
width: 205px;
}
li#generate-registration-modal-field-country {
width: 204px;
margin-left: 15px !important;
margin-bottom: 20px;
}
li:nth-child(even){
margin-left: 0px !important;
}
li:nth-child(3n) {
margin-left: 15px !important;
}
li#generate-registration-modal-field-company-contact-name,
li#generate-registration-modal-field-address-line-3,
li#generate-registration-modal-field-zipcode {
margin-left: 15px !important;
}
li:last-child{
label {
float: right;
margin-top: -5px;
right: 27px;
}
min-height: 5px;
margin-left: 0px !important;
input[type='checkbox'] {
width: auto;
height: auto;
}
}
li#generate-registration-modal-field-country ~ li#generate-registration-modal-field-total-price,
li#generate-registration-modal-field-country ~ li#generate-registration-modal-field-internal-reference {
margin-left: 0px !important;
margin-right: 15px !important;
}
li#generate-registration-modal-field-custom-reference-number {
width: auto;
}
}
li#set-course-mode-modal-field-price{
width: 100%;
label.required:after {
......@@ -1027,7 +1100,13 @@ input[name="subject"] {
width: 100%;
}
}
#coupon-content, #course-content {
#registration-content form .field.text input {
background: #fff;
margin-bottom: 0;
height: 40px;
border-radius: 3px;
}
#coupon-content, #course-content, #registration-content {
padding: 20px;
header {
margin: 0;
......@@ -1086,6 +1165,19 @@ input[name="subject"] {
}
}
}
#registration-content form .group-form {
}
#registration-content form {
.field {
margin: 0;
}
.group-form {
margin: 0;
padding-top: 0;
padding-bottom: 0px;
}
}
}
}
......@@ -1171,5 +1263,47 @@ input[name="subject"] {
float: right;
}
}
span.code_tip {
background: none repeat scroll 0 0 #F8F4EC;
border-bottom: 1px solid #DDDDDD;
border-top: 1px solid #DDDDDD;
color: #3C3C3C;
display: block;
line-height: 30px;
margin-bottom: 6px;
padding: 10px 15px 10px 20px;
.add{
@include button(simple, $blue);
@extend .button-reset;
font-size: em(13);
float: right;
}
}
span.csv_tip {
display: block;
line-height: 30px;
margin-bottom: 6px;
padding: 10px 15px 10px 1px;
.add{
font-size: em(13);
float: right;
}
}
span.invalid_sale {
background: none repeat scroll 0 0 #F8F4EC;
color: #3C3C3C;
display: block;
line-height: 30px;
height: 37px;
margin-bottom: 6px;
padding: 10px 15px 10px 1px;
.add{
@include button(simple, $blue);
@extend .button-reset;
font-size: em(13);
}
}
}
......@@ -1108,4 +1108,61 @@
@extend %t-copy;
}
}
}
p.course-block{
border-style: solid;
border-color: #E3DC86;
padding: 5px;
border-width: 1px;
background: #FDFBE4;
}
.enter-course-blocked{
@include box-sizing(border-box);
display: block;
float: left;
font: normal 15px/1.6rem $sans-serif;
letter-spacing: 0;
padding: 6px 32px 7px;
text-align: center;
margin-top: 16px;
opacity:0.5;
background:#808080;
border:0;
color:white;
box-shadow:none;
&.archived {
@include button(simple, $button-archive-color);
font: normal 15px/1.6rem $sans-serif;
padding: 6px 32px 7px;
&:hover, &:focus {
text-decoration: none;
}
}
}
a.disable-look{
color: #808080;
}
a.disable-look-unregister{
color: #808080;
float: right;
display: block;
font-style: italic;
color: $lighter-base-font-color;
text-decoration: underline;
font-size: .8em;
margin-top: 32px;
}
a.disable-look-settings{
@extend a.disable-look-unregister;
margin-right: 10px;
}
}
a.fade-cover{
@extend .cover;
opacity: 0.5;
}
......@@ -306,7 +306,8 @@
<% course_mode_info = all_course_modes.get(course.id) %>
<% show_refund_option = (course.id in show_refund_option_for) %>
<% is_paid_course = (course.id in enrolled_courses_either_paid) %>
<%include file='dashboard/_dashboard_course_listing.html' args="course=course, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option = show_refund_option, is_paid_course = is_paid_course" />
<% is_course_blocked = (course.id in block_courses) %>
<%include file='dashboard/_dashboard_course_listing.html' args="course=course, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option = show_refund_option, is_paid_course = is_paid_course, is_course_blocked = is_course_blocked" />
% endfor
</ul>
......@@ -521,3 +522,13 @@
</form>
</div>
</section>
<script>
$(function() {
$("#unregister_block_course").click( function(event) {
$("#unenroll_course_id").val( $(event.target).data("course-id") );
$("#unenroll_course_number").text( $(event.target).data("course-number") );
});
});
</script>
<%page args="course, enrollment, show_courseware_link, cert_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course" />
<%page args="course, enrollment, show_courseware_link, cert_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course, is_course_blocked" />
<%! from django.utils.translation import ugettext as _ %>
<%!
......@@ -31,9 +31,15 @@
%>
% if show_courseware_link:
<a href="${course_target}" class="cover">
% if not is_course_blocked:
<a href="${course_target}" class="cover">
<img src="${course_image_url(course)}" alt="${_('{course_number} {course_name} Cover Image').format(course_number=course.number, course_name=course.display_name_with_default) |h}" />
</a>
% else:
<a class="fade-cover">
<img src="${course_image_url(course)}" alt="${_('{course_number} {course_name} Cover Image').format(course_number=course.number, course_name=course.display_name_with_default) |h}" />
</a>
% endif
% else:
<div class="cover">
<img src="${course_image_url(course)}" alt="${_('{course_number} {course_name} Cover Image').format(course_number=course.number, course_name=course.display_name_with_default) | h}" />
......@@ -80,7 +86,11 @@
<h2 class="university">${get_course_about_section(course, 'university')}</h2>
<h3>
% if show_courseware_link:
% if not is_course_blocked:
<a href="${course_target}">${course.display_number_with_default | h} ${course.display_name_with_default}</a>
% else:
<a class="disable-look">${course.display_number_with_default | h} ${course.display_name_with_default}</a>
% endif
% else:
<span>${course.display_number_with_default | h} ${course.display_name_with_default}</span>
% endif
......@@ -91,7 +101,7 @@
<%include file='_dashboard_certificate_information.html' args='cert_status=cert_status,course=course, enrollment=enrollment'/>
% endif
% if course_mode_info['show_upsell']:
% if course_mode_info['show_upsell'] and not is_course_blocked:
<div class="message message-upsell has-actions is-expandable is-shown">
<div class="wrapper-tip">
......@@ -122,52 +132,116 @@
</div>
</div>
%endif
% if is_course_blocked:
<p id="block-course-msg" class="course-block">
${_('You can no longer access this course because payment has not yet been received. you can <a href="#">contact the account holder</a> to request payment, or you can')}
<a id="unregister_block_course" rel="leanModal" data-course-id="${course.id.to_deprecated_string()}" data-course-number="${course.number}" href="#unenroll-modal" > ${_('unregister')} </a>
${_('for this course.')}
</p>
%endif
% if show_courseware_link:
% if course.has_ended():
% if not is_course_blocked:
<a href="${course_target}" class="enter-course archived">${_('View Archived Course')}</a>
% else:
<a class="enter-course-blocked archived">${_('View Archived Course')}</a>
% endif
% else:
% if not is_course_blocked:
<a href="${course_target}" class="enter-course">${_('View Course')}</a>
% else:
<a class="enter-course-blocked">${_('View Course')}</a>
% endif
% endif
% endif
% if is_paid_course and show_refund_option:
## Translators: The course's name will be added to the end of this sentence.
% if not is_course_blocked:
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id.to_deprecated_string()}" data-course-number="${course.number}" onclick="document.getElementById('track-info').innerHTML='${_("Are you sure you want to unregister from the purchased course")}';
document.getElementById('refund-info').innerHTML=gettext('You will be refunded the amount you paid.')">
${_('Unregister')}
</a>
% else:
<a class="disable-look-unregister" data-course-id="${course.id.to_deprecated_string()}" data-course-number="${course.number}" onclick="document.getElementById('track-info').innerHTML='${_("Are you sure you want to unregister from the purchased course")}';
document.getElementById('refund-info').innerHTML=gettext('You will be refunded the amount you paid.')">
${_('Unregister')}
</a>
% endif
% elif is_paid_course and not show_refund_option:
## Translators: The course's name will be added to the end of this sentence.
% if not is_course_blocked:
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id.to_deprecated_string()}" data-course-number="${course.number}" onclick="document.getElementById('track-info').innerHTML='${_("Are you sure you want to unregister from the purchased course")}';
document.getElementById('refund-info').innerHTML=gettext('You will not be refunded the amount you paid.')">
${_('Unregister')}
</a>
% else:
<a class="disable-look-unregister" data-course-id="${course.id.to_deprecated_string()}" data-course-number="${course.number}" onclick="document.getElementById('track-info').innerHTML='${_("Are you sure you want to unregister from the purchased course")}';
document.getElementById('refund-info').innerHTML=gettext('You will not be refunded the amount you paid.')">
${_('Unregister')}
</a>
% endif
% elif enrollment.mode != "verified":
## Translators: The course's name will be added to the end of this sentence.
% if not is_course_blocked:
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id.to_deprecated_string()}" data-course-number="${course.number}" onclick="document.getElementById('track-info').innerHTML='${_("Are you sure you want to unregister from")}'; document.getElementById('refund-info').innerHTML=''">
${_('Unregister')}
</a>
% else:
<a class="disable-look-unregister" data-course-id="${course.id.to_deprecated_string()}" data-course-number="${course.number}" onclick="document.getElementById('track-info').innerHTML='${_("Are you sure you want to unregister from")}'; document.getElementById('refund-info').innerHTML=''">
${_('Unregister')}
</a>
% endif
% elif show_refund_option:
## Translators: The course's name will be added to the end of this sentence.
% if not is_course_blocked:
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id.to_deprecated_string()}" data-course-number="${course.number}" onclick="document.getElementById('track-info').innerHTML='${_("Are you sure you want to unregister from the verified {cert_name_long} track of").format(cert_name_long=cert_name_long)}';
document.getElementById('refund-info').innerHTML=gettext('You will be refunded the amount you paid.')">
${_('Unregister')}
</a>
% else:
<a class="disable-look-unregister" data-course-id="${course.id.to_deprecated_string()}" data-course-number="${course.number}" onclick="document.getElementById('track-info').innerHTML='${_("Are you sure you want to unregister from the verified {cert_name_long} track of").format(cert_name_long=cert_name_long)}';
document.getElementById('refund-info').innerHTML=gettext('You will be refunded the amount you paid.')">
${_('Unregister')}
</a>
% endif
% else:
## Translators: The course's name will be added to the end of this sentence.
% if not is_course_blocked:
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id.to_deprecated_string()}" data-course-number="${course.number}" onclick="document.getElementById('track-info').innerHTML='${_("Are you sure you want to unregister from the verified {cert_name_long} track of").format(cert_name_long=cert_name_long)}';
document.getElementById('refund-info').innerHTML=gettext('The refund deadline for this course has passed, so you will not receive a refund.')">
${_('Unregister')}
</a>
% else:
<a class="disable-look-unregister" data-course-id="${course.id.to_deprecated_string()}" data-course-number="${course.number}" onclick="document.getElementById('track-info').innerHTML='${_("Are you sure you want to unregister from the verified {cert_name_long} track of").format(cert_name_long=cert_name_long)}';
document.getElementById('refund-info').innerHTML=gettext('The refund deadline for this course has passed, so you will not receive a refund.')">
${_('Unregister')}
</a>
% endif
% endif
% if show_email_settings:
% if not is_course_blocked:
<a href="#email-settings-modal" class="email-settings" rel="leanModal" data-course-id="${course.id.to_deprecated_string()}" data-course-number="${course.number}" data-optout="${course.id.to_deprecated_string() in course_optouts}">${_('Email Settings')}</a>
% else:
<a class="disable-look-settings" data-course-id="${course.id.to_deprecated_string()}" data-course-number="${course.number}" data-optout="${course.id.to_deprecated_string() in course_optouts}">${_('Email Settings')}</a>
% endif
% endif
</section>
</article>
</article>
</li>
<script>
$( document ).ready(function() {
if("${is_course_blocked}" == "True"){
$( "#unregister_block_course" ).click(function() {
$('.disable-look-unregister').click();
});
}
});
</script>
\ No newline at end of file
<%! from django.utils.translation import ugettext as _ %>
${_("Professional Education Invoicing Center")}
${_("Portland Street, ")}
${_("9th floor Cambridge, MA,")}
${_("02139 finance@edx.org")}
${_("---------------------------------")}
${_("Bill To:")}
${_("{company_name}").format(company_name=invoice.company_name)}
${_("{purchaser_name}").format(purchaser_name=invoice.company_contact_name)}
% if invoice.customer_reference_number:
${_("Customer Reference Number: {customer_reference_number}").format(customer_reference_number=invoice.customer_reference_number)}
%endif
${_("---------------------------------")}
${_("Registration codes:")}
${_("Please distribute one of the following registration codes to each student who is going to enroll in the class.")}
${_("A student can redeem one of these codes by registering an account on ({site_name}.edx.org), starting the purchase").format(site_name=site_name)}
${_("process on this course by adding it to their shopping cart from this page ({course_url}), and").format(course_url=course_url)}
${_("entering their registration code while in the shopping cart.")}
%for registration_code in registration_codes:
${registration_code.code}
%endfor
${_("---------------------------------")}
${_("Thank you for your purchase! Failure to pay this invoice will result the invalidation of student enrollments")}
${_("that use these codes. All purchases are final. Please refer to the cancellation policy")}
${_("on {site_name} for more information.").format(site_name=site_name)}
${_("---------------------------------")}
${_("Course: {course_name} {start_date} - {end_date}").format(course_name=course.display_name, start_date=course.start_date_text, end_date=course.end_date_text)}
${_("Price: ${price}").format(price=course_price)}
${_("Quantity: ${quantity}").format(quantity=quantity)}
${_("Sub-Total: ${sub_total}").format(sub_total=quantity*course_price)}
${_("Discount: ${discount}").format(discount=discount_price)}
${_("--------- ------")}
${_("Total Due: ${total_price}").format(total_price=sale_price)}
......@@ -29,13 +29,13 @@
<ol class="list-input">
<li class="field required text" id="edit-coupon-modal-field-code">
<label for="edit_coupon_code" class="required">${_("Code")}</label>
<input class="field" id="edit_coupon_code" type="text" name="code" maxlength="16" value="" placeholder="example: A123DS"
aria-required="true"/>
<input class="field readonly" id="edit_coupon_code" type="text" name="code" maxlength="16" value="" placeholder="example: A123DS"
readonly aria-required="true"/>
</li>
<li class="field required text" id="edit-coupon-modal-field-discount">
<label for="edit_coupon_discount" class="required">${_("Percentage Discount")}</label>
<input class="field" id="edit_coupon_discount" type="text" name="discount" value="" maxlength="3"
aria-required="true"/>
<input class="field readonly" id="edit_coupon_discount" type="text" name="discount" value="" maxlength="3"
readonly aria-required="true"/>
</li>
<li class="field" id="edit-coupon-modal-field-description">
......
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%page args="section_data"/>
<section id="registration_code_generation_modal" class="modal" role="dialog" tabindex="-1" aria-label="${_('Generate Registration Code Modal')}">
<div class="inner-wrapper">
<button class="close-modal">
<i class="icon-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="registration-content">
<header>
<h2>${_("Generate Registration Codes")}</h2>
</header>
<div class="instructions">
<p>
${_("Please enter the details below")}</p>
</div>
<form id="generate_codes" action="${section_data['generate_registration_code_csv_url']}" method="post">
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
<div id="registration_code_form_error" class="modal-form-error"></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="generate-registration-modal-field-company-name">
<label for="id_company_name" class="required text">${_("Organization Name")}</label>
<input class="field required" id="id_company_name" type="text" name="company_name"
placeholder="${_('Organization Name')}" aria-required="true" />
<span class="tip-text">
${_("What is the company the seats were sold to?")}
</span>
</li>
<li class="field required text" id="generate-registration-modal-field-company-contact-name">
<label for="id_company_contact_name" class="required text">${_("Organization Contact")}</label>
<input class="field required" id="id_company_contact_name" type="text" name="company_contact_name"
placeholder="${_('Name')}" aria-required="true" />
<span class="tip-text">
${_("Who is the key contact at the company the sale was made to? ")}
</span>
</li>
<li class="field text" id="generate-registration-modal-field-company-email">
<label for="id_company_contact_email" class="text">&nbsp;</label>
<input class="field" id="id_company_contact_email" type="email" value="" name="company_contact_email"
placeholder="${_('Email')}" aria-required="true" />
</li>
<div class="clearfix"></div>
<li class="field required text" id="generate-registration-modal-field-recipient-name">
<label for="id_recipient_name" class="required text">${_("Invoice Recipient")}</label>
<input class="field required" id="id_recipient_name" type="text" name="recipient_name"
placeholder="${_('Name')}" aria-required="true"/>
<span class="tip-text">
${_("Who at the company should the invoice be sent to?")}
</span>
</li>
<li class="field text" id="generate-registration-modal-field-recipient-email">
<label for="id_recipient_email" class="text">&nbsp;</label>
<input class="field required" id="id_recipient_email" type="email" value="" placeholder="${_('Email')}"
name="recipient_email" aria-required="true"/>
</li>
<div class="clearfix"></div>
<li class="field required text address_fields" id="generate-registration-modal-field-address-line-1">
<label for="id_address_line_1" class="required text">${_("Organization Billing Address")}</label>
<input class="field required" id="id_address_line_1" type="text" name="address_line_1"
placeholder="${_('Address Line 1')}" aria-required="true" />
</li>
<li class="field text address_fields" id="generate-registration-modal-field-address-line-2">
<label for="id_address_line_2" class="text">&nbsp;</label>
<input class="field" id="id_address_line_2" type="text" name="address_line_2"
placeholder="${_('Address Line 2')}" aria-required="true" />
</li>
<li class="field text address_fields" id="generate-registration-modal-field-address-line-3">
<label for="id_address_line_3" class="text">&nbsp;</label>
<input class="field" id="id_address_line_3" type="text" name="address_line_3"
placeholder="${_('Address Line 3')}" aria-required="true" />
</li>
<li class="field text address_fields" id="generate-registration-modal-field-city">
<label for="id_city" class="text">&nbsp;</label>
<input class="field" id="id_city" type="text" name="city"
placeholder="${_('City')}" aria-required="true" />
</li>
<li class="field text address_fields" id="generate-registration-modal-field-state">
<label for="id_state" class="text">&nbsp;</label>
<input class="field" id="id_state" type="text" name="state"
placeholder="${_('State/Province')}" aria-required="true" />
</li>
<li class="field text address_fields" id="generate-registration-modal-field-zipcode">
<label for="id_zip" class="text">&nbsp;</label>
<input class="field" id="id_zip" type="text" name="zip"
placeholder="${_('Zip')}" aria-required="true" />
</li>
<li class="field text address_fields" id="generate-registration-modal-field-country">
<label for="id_country" class="text">&nbsp;</label>
<input class="field" id="id_country" type="text" name="country"
placeholder="${_('Country')}" aria-required="true" />
<span class="tip-text">
</span>
</li>
<div class="clearfix"></div>
<li class="field required text" id="generate-registration-modal-field-total-price">
<label for="id_sale_price" class="required text">${_("Price of Sale")}</label>
<input class="field required" id="id_sale_price" type="text" name="sale_price"
aria-required="true" />
<span class="tip-text">
${_("What was the total sale price for all seats sold?")}
</span>
</li>
<li class="field required text" id="generate-registration-modal-field-total-codes">
<label for="id_total_registration_codes" class="required text">${_("Number of registrations")}</label>
<input class="field required" id="id_total_registration_codes" type="text" name="total_registration_codes"
aria-required="true"/>
<span class="tip-text">
${_("Number of codes to generate for the Invoice")}
</span>
</li>
<div class="clearfix"></div>
<li class="field text" id="generate-registration-modal-field-internal-reference">
<label for="id_internal_reference" >${_("(Optional) Sale Label")}</label>
<input class="field" id="id_internal_reference" type="text" name="internal_reference"
aria-required="true" placeholder="BigCorp-IT-Department-1"/>
<span class="tip-text">
${_("Internal tag to associate with the sale")}
</span>
</li>
<li class="field text" id="generate-registration-modal-field-custom-reference-number">
<label for="id_customer_reference_number" >${_("(Optional) Customer Reference Number")}</label>
<input class="field" id="id_customer_reference_number" type="text" name="customer_reference_number"
aria-required="true" placeholder="PO #"/>
<span class="tip-text">
${_("Any reference specific to the purchasing company, e.g. PO #")}
</span>
</li>
<div class="clearfix"></div>
<li class="field text" id="generate-registration-modal-field-invoice-copy">
<label for="id_invoice" >${_("Send me a copy of the invoice")}</label>
<input class="field" id="id_invoice" type="checkbox" name="invoice"
aria-required="true"/>
</li>
</ol>
</fieldset>
<div class="submit">
<input name="generate-registration-codes-csv" type="submit" value="${_('Generate Registration Codes')}" data-csv="true"/>
</div>
</form>
</div>
</div>
</section>
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