From b188430679d7aede87b2bb5d17b52a42191aa5a7 Mon Sep 17 00:00:00 2001
From: stephensanchez
Date: Mon, 22 Dec 2014 20:23:24 +0000
Subject: [PATCH] Enable redeem codes.
Update the redeem code schema
Updating the redeem code schema.
Adding migration file.
Adding course mode support when redeeming a code.
Conflicts:
lms/djangoapps/shoppingcart/views.py
Add sales admin privileges for redeem code generation.
Making sure redeem code URLs work for verified courses.
pep8 violations
Code Review and Test Cleanup changes
Added tests, fixed tests.
Updating the boolean checks in ecommerce template
---
common/djangoapps/course_modes/models.py | 65 ++++--
.../0042_grant_sales_admin_roles.py | 184 +++++++++++++++
common/djangoapps/student/roles.py | 10 +-
common/djangoapps/student/tests/tests.py | 5 +-
lms/djangoapps/instructor/tests/test_api.py | 21 +-
.../instructor/tests/test_ecommerce.py | 22 +-
lms/djangoapps/instructor/views/api.py | 80 ++++++-
.../instructor/views/instructor_dashboard.py | 42 ++--
.../instructor_analytics/tests/test_basic.py | 12 +-
lms/djangoapps/shoppingcart/exceptions.py | 5 +
..._field_courseregistrationcode_mode_slug.py | 218 ++++++++++++++++++
lms/djangoapps/shoppingcart/models.py | 3 +-
.../shoppingcart/tests/test_views.py | 54 ++++-
lms/djangoapps/shoppingcart/views.py | 54 +++--
lms/static/sass/views/_shoppingcart.scss | 2 +-
.../instructor_dashboard_2/e-commerce.html | 8 +-
.../registration_code_receipt.html | 96 ++++++++
.../registration_code_redemption.html | 11 +-
18 files changed, 804 insertions(+), 88 deletions(-)
create mode 100644 common/djangoapps/student/migrations/0042_grant_sales_admin_roles.py
create mode 100644 lms/djangoapps/shoppingcart/migrations/0024_auto__add_field_courseregistrationcode_mode_slug.py
create mode 100644 lms/templates/shoppingcart/registration_code_receipt.html
diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py
index 0fa7b0f33d..c4f73447c5 100644
--- a/common/djangoapps/course_modes/models.py
+++ b/common/djangoapps/course_modes/models.py
@@ -81,17 +81,7 @@ class CourseMode(models.Model):
"""
modes_by_course = defaultdict(list)
for mode in cls.objects.filter(course_id__in=course_id_list):
- modes_by_course[mode.course_id].append(
- Mode(
- mode.mode_slug,
- mode.mode_display_name,
- mode.min_price,
- mode.suggested_prices,
- mode.currency,
- mode.expiration_datetime,
- mode.description
- )
- )
+ modes_by_course[mode.course_id].append(mode.to_tuple())
# Assign default modes if nothing available in the database
missing_courses = set(course_id_list) - set(modes_by_course.keys())
@@ -130,6 +120,31 @@ class CourseMode(models.Model):
return (all_modes, unexpired_modes)
+ @classmethod
+ def paid_modes_for_course(cls, course_id):
+ """
+ Returns a list of non-expired modes for a course ID that have a set minimum price.
+
+ If no modes have been set, returns an empty list.
+
+ Args:
+ course_id (CourseKey): The course to find paid modes for.
+
+ Returns:
+ A list of CourseModes with a minimum price.
+
+ """
+ now = datetime.now(pytz.UTC)
+ found_course_modes = cls.objects.filter(
+ Q(course_id=course_id) &
+ Q(min_price__gt=0) &
+ (
+ Q(expiration_datetime__isnull=True) |
+ Q(expiration_datetime__gte=now)
+ )
+ )
+ return [mode.to_tuple() for mode in found_course_modes]
+
@classmethod
def modes_for_course(cls, course_id):
"""
@@ -141,15 +156,7 @@ class CourseMode(models.Model):
found_course_modes = cls.objects.filter(Q(course_id=course_id) &
(Q(expiration_datetime__isnull=True) |
Q(expiration_datetime__gte=now)))
- modes = ([Mode(
- mode.mode_slug,
- mode.mode_display_name,
- mode.min_price,
- mode.suggested_prices,
- mode.currency,
- mode.expiration_datetime,
- mode.description
- ) for mode in found_course_modes])
+ modes = ([mode.to_tuple() for mode in found_course_modes])
if not modes:
modes = [cls.DEFAULT_MODE]
return modes
@@ -359,6 +366,24 @@ class CourseMode(models.Model):
modes = cls.modes_for_course(course_id)
return min(mode.min_price for mode in modes if mode.currency == currency)
+ def to_tuple(self):
+ """
+ Takes a mode model and turns it into a model named tuple.
+
+ Returns:
+ A 'Model' namedtuple with all the same attributes as the model.
+
+ """
+ return Mode(
+ self.mode_slug,
+ self.mode_display_name,
+ self.min_price,
+ self.suggested_prices,
+ self.currency,
+ self.expiration_datetime,
+ self.description
+ )
+
def __unicode__(self):
return u"{} : {}, min={}, prices={}".format(
self.course_id.to_deprecated_string(), self.mode_slug, self.min_price, self.suggested_prices
diff --git a/common/djangoapps/student/migrations/0042_grant_sales_admin_roles.py b/common/djangoapps/student/migrations/0042_grant_sales_admin_roles.py
new file mode 100644
index 0000000000..ccbb58f7fc
--- /dev/null
+++ b/common/djangoapps/student/migrations/0042_grant_sales_admin_roles.py
@@ -0,0 +1,184 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models, IntegrityError
+
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ """Map all Finance Admins to Sales Admins."""
+ finance_admins = orm['student.courseaccessrole'].objects.filter(role='finance_admin')
+
+ for finance_admin in finance_admins:
+ sales_admin = orm['student.courseaccessrole'](
+ role='sales_admin',
+ user=finance_admin.user,
+ org=finance_admin.org,
+ course_id=finance_admin.course_id,
+ )
+ try:
+ sales_admin.save()
+ except IntegrityError:
+ pass # If sales admin roles exist, continue.
+
+ def backwards(self, orm):
+ """Remove all sales administrators, as they did not exist before this migration. """
+ sales_admins = orm['student.courseaccessrole'].objects.filter(role='sales_admin')
+ sales_admins.delete()
+
+ 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'})
+ },
+ 'student.anonymoususerid': {
+ 'Meta': {'object_name': 'AnonymousUserId'},
+ 'anonymous_user_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+ 'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'student.courseaccessrole': {
+ 'Meta': {'unique_together': "(('user', 'org', 'course_id', 'role'),)", 'object_name': 'CourseAccessRole'},
+ 'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'org': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'blank': 'True'}),
+ 'role': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ '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']"})
+ },
+ 'student.courseenrollmentallowed': {
+ 'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'},
+ 'auto_enroll': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ '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'}),
+ 'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'student.dashboardconfiguration': {
+ 'Meta': {'object_name': 'DashboardConfiguration'},
+ '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'}),
+ 'recent_enrollment_time_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'student.loginfailures': {
+ 'Meta': {'object_name': 'LoginFailures'},
+ 'failure_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'lockout_until': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'student.passwordhistory': {
+ 'Meta': {'object_name': 'PasswordHistory'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'time_set': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'student.pendingemailchange': {
+ 'Meta': {'object_name': 'PendingEmailChange'},
+ 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
+ },
+ 'student.pendingnamechange': {
+ 'Meta': {'object_name': 'PendingNameChange'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
+ },
+ 'student.registration': {
+ 'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
+ 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
+ },
+ 'student.userprofile': {
+ 'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
+ 'allow_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'city': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'null': 'True', 'blank': 'True'}),
+ 'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
+ 'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
+ 'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
+ 'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
+ 'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
+ 'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}),
+ 'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'student.usersignupsource': {
+ 'Meta': {'object_name': 'UserSignupSource'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'site': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'student.userstanding': {
+ 'Meta': {'object_name': 'UserStanding'},
+ 'account_status': ('django.db.models.fields.CharField', [], {'max_length': '31', 'blank': 'True'}),
+ 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'standing_last_changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'standing'", 'unique': 'True', 'to': "orm['auth.User']"})
+ },
+ 'student.usertestgroup': {
+ 'Meta': {'object_name': 'UserTestGroup'},
+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
+ 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
+ }
+ }
+
+ complete_apps = ['student']
+ symmetrical = True
diff --git a/common/djangoapps/student/roles.py b/common/djangoapps/student/roles.py
index 5165966acb..8727aad0cf 100644
--- a/common/djangoapps/student/roles.py
+++ b/common/djangoapps/student/roles.py
@@ -204,13 +204,21 @@ class CourseInstructorRole(CourseRole):
class CourseFinanceAdminRole(CourseRole):
- """A course Instructor"""
+ """A course staff member with privileges to review financial data."""
ROLE = 'finance_admin'
def __init__(self, *args, **kwargs):
super(CourseFinanceAdminRole, self).__init__(self.ROLE, *args, **kwargs)
+class CourseSalesAdminRole(CourseRole):
+ """A course staff member with privileges to perform sales operations. """
+ ROLE = 'sales_admin'
+
+ def __init__(self, *args, **kwargs):
+ super(CourseSalesAdminRole, self).__init__(self.ROLE, *args, **kwargs)
+
+
class CourseBetaTesterRole(CourseRole):
"""A course Beta Tester"""
ROLE = 'beta_testers'
diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py
index 14b5eedf1a..a747c37598 100644
--- a/common/djangoapps/student/tests/tests.py
+++ b/common/djangoapps/student/tests/tests.py
@@ -280,8 +280,9 @@ class DashboardTest(ModuleStoreTestCase):
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 = shoppingcart.models.CourseRegistrationCode(
+ code="abcde", course_id=self.course.id, created_by=self.user, invoice=sale_invoice_1, mode_slug='honor'
+ )
course_reg_code.save()
cart = shoppingcart.models.Order.get_cart_for_user(self.user)
diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py
index e884c693df..10789d3743 100644
--- a/lms/djangoapps/instructor/tests/test_api.py
+++ b/lms/djangoapps/instructor/tests/test_api.py
@@ -13,7 +13,6 @@ import random
import requests
import shutil
import tempfile
-from unittest import TestCase
from urllib import quote
from django.conf import settings
@@ -45,8 +44,8 @@ from shoppingcart.models import (
from student.models import (
CourseEnrollment, CourseEnrollmentAllowed, NonExistentCourseError
)
-from student.tests.factories import UserFactory
-from student.roles import CourseBetaTesterRole
+from student.tests.factories import UserFactory, CourseModeFactory
+from student.roles import CourseBetaTesterRole, CourseSalesAdminRole, CourseFinanceAdminRole
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@@ -1722,7 +1721,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
for i in range(2):
course_registration_code = CourseRegistrationCode(
code='sale_invoice{}'.format(i), course_id=self.course.id.to_deprecated_string(),
- created_by=self.instructor, invoice=self.sale_invoice_1
+ created_by=self.instructor, invoice=self.sale_invoice_1, mode_slug='honor'
)
course_registration_code.save()
@@ -1807,6 +1806,10 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
percentage_discount='10', created_by=self.instructor, is_active=True
)
coupon.save()
+
+ # Coupon Redeem Count only visible for Financial Admins.
+ CourseFinanceAdminRole(self.course.id).add_users(self.instructor)
+
PaidCourseRegistration.add_to_order(self.cart, self.course.id)
# apply the coupon code to the item in the cart
resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': coupon.code})
@@ -1838,7 +1841,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
for i in range(2):
course_registration_code = CourseRegistrationCode(
code='sale_invoice{}'.format(i), course_id=self.course.id.to_deprecated_string(),
- created_by=self.instructor, invoice=self.sale_invoice_1
+ created_by=self.instructor, invoice=self.sale_invoice_1, mode_slug='honor'
)
course_registration_code.save()
@@ -1853,7 +1856,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
for i in range(5):
course_registration_code = CourseRegistrationCode(
code='sale_invoice{}'.format(i), course_id=self.course.id.to_deprecated_string(),
- created_by=self.instructor, invoice=self.sale_invoice_1
+ created_by=self.instructor, invoice=self.sale_invoice_1, mode_slug='honor'
)
course_registration_code.save()
@@ -1872,7 +1875,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
for i in range(5):
course_registration_code = CourseRegistrationCode(
code='qwerty{}'.format(i), course_id=self.course.id.to_deprecated_string(),
- created_by=self.instructor, invoice=self.sale_invoice_1
+ created_by=self.instructor, invoice=self.sale_invoice_1, mode_slug='honor'
)
course_registration_code.save()
@@ -1886,7 +1889,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
for i in range(5):
course_registration_code = CourseRegistrationCode(
code='xyzmn{}'.format(i), course_id=self.course.id.to_deprecated_string(),
- created_by=self.instructor, invoice=sale_invoice_2
+ created_by=self.instructor, invoice=sale_invoice_2, mode_slug='honor'
)
course_registration_code.save()
@@ -2994,8 +2997,10 @@ class TestCourseRegistrationCodes(ModuleStoreTestCase):
Fixtures.
"""
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)
url = reverse('generate_registration_codes',
kwargs={'course_id': self.course.id.to_deprecated_string()})
diff --git a/lms/djangoapps/instructor/tests/test_ecommerce.py b/lms/djangoapps/instructor/tests/test_ecommerce.py
index 98271f510e..7f9dcd4b78 100644
--- a/lms/djangoapps/instructor/tests/test_ecommerce.py
+++ b/lms/djangoapps/instructor/tests/test_ecommerce.py
@@ -50,6 +50,8 @@ class TestECommerceDashboardViews(ModuleStoreTestCase):
"""
response = self.client.get(self.url)
self.assertTrue(self.e_commerce_link in response.content)
+ # Coupons should show up for White Label sites with priced honor modes.
+ self.assertTrue('Coupons' in response.content)
def test_user_has_finance_admin_rights_in_e_commerce_tab(self):
response = self.client.get(self.url)
@@ -190,7 +192,8 @@ 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(), created_by=self.instructor
+ code='Vs23Ws4j', course_id=unicode(self.course.id), created_by=self.instructor,
+ mode_slug='honor'
)
course_registration.save()
@@ -288,3 +291,20 @@ class TestECommerceDashboardViews(ModuleStoreTestCase):
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)
+
+ def test_verified_course(self):
+ """Verify the e-commerce panel shows up for verified courses as well, without Coupons """
+ # Change honor mode to verified.
+ original_mode = CourseMode.objects.get(course_id=self.course.id, mode_slug='honor')
+ original_mode.delete()
+ new_mode = CourseMode(
+ course_id=unicode(self.course.id), mode_slug='verified',
+ mode_display_name='verified', min_price=10, currency='usd'
+ )
+ new_mode.save()
+
+ # Get the response value, ensure the Coupon section is not included.
+ response = self.client.get(self.url)
+ self.assertTrue(self.e_commerce_link in response.content)
+ # Coupons should show up for White Label sites with priced honor modes.
+ self.assertFalse('Coupons List' in response.content)
diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py
index 56f6a80979..a2270e17a9 100644
--- a/lms/djangoapps/instructor/views/api.py
+++ b/lms/djangoapps/instructor/views/api.py
@@ -28,6 +28,8 @@ import string # pylint: disable=deprecated-module
import random
import unicodecsv
import urllib
+from student import auth
+from student.roles import CourseSalesAdminRole
from util.file import store_uploaded_file, course_and_time_based_filename_generator, FileValidationException, UniversalNewlineIterator
import datetime
import pytz
@@ -214,7 +216,7 @@ def require_level(level):
def decorator(func): # pylint: disable=missing-docstring
def wrapped(*args, **kwargs): # pylint: disable=missing-docstring
request = args[0]
- course = get_course_by_id(SlashSeparatedCourseKey.from_deprecated_string(kwargs['course_id']))
+ course = get_course_by_id(CourseKey.from_string(kwargs['course_id']))
if has_access(request.user, level, course):
return func(*args, **kwargs)
@@ -224,6 +226,31 @@ def require_level(level):
return decorator
+def require_sales_admin(func):
+ """
+ Decorator for checking sales administrator access before executing an HTTP endpoint. This decorator
+ is designed to be used for a request based action on a course. It assumes that there will be a
+ request object as well as a course_id attribute to leverage to check course level privileges.
+
+ If the user does not have privileges for this operation, this will return HttpResponseForbidden (403).
+ """
+ def wrapped(request, course_id): # pylint: disable=missing-docstring
+
+ try:
+ course_key = CourseKey.from_string(course_id)
+ except InvalidKeyError:
+ log.error(u"Unable to find course with course key %s", course_id)
+ return HttpResponseNotFound()
+
+ access = auth.has_access(request.user, CourseSalesAdminRole(course_key))
+
+ if access:
+ return func(request, course_id)
+ else:
+ return HttpResponseForbidden()
+ return wrapped
+
+
EMAIL_INDEX = 0
USERNAME_INDEX = 1
NAME_INDEX = 2
@@ -1024,10 +1051,21 @@ def get_coupon_codes(request, course_id): # pylint: disable=unused-argument
return instructor_analytics.csvs.create_csv_response('Coupons.csv', header, data_rows)
-def save_registration_code(user, course_id, invoice=None, order=None):
+def save_registration_code(user, course_id, mode_slug, invoice=None, order=None):
"""
recursive function that generate a new code every time and saves in the Course Registration Table
if validation check passes
+
+ Args:
+ user (User): The user creating the course registration codes.
+ course_id (str): The string representation of the course ID.
+ mode_slug (str): The Course Mode Slug associated with any enrollment made by these codes.
+ invoice (Invoice): (Optional) The associated invoice for this code.
+ order (Order): (Optional) The associated order for this code.
+
+ Returns:
+ The newly created CourseRegistrationCode.
+
"""
code = random_code_generator()
@@ -1038,10 +1076,11 @@ def save_registration_code(user, course_id, invoice=None, order=None):
course_registration = CourseRegistrationCode(
code=code,
- course_id=course_id.to_deprecated_string(),
+ course_id=unicode(course_id),
created_by=user,
invoice=invoice,
- order=order
+ order=order,
+ mode_slug=mode_slug
)
try:
course_registration.save()
@@ -1101,13 +1140,13 @@ def get_registration_codes(request, course_id): # pylint: disable=unused-argume
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
-@require_level('staff')
+@require_sales_admin
@require_POST
def generate_registration_codes(request, course_id):
"""
Respond with csv which contains a summary of all Generated Codes.
"""
- course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
+ course_id = CourseKey.from_string(course_id)
invoice_copy = False
# covert the course registration code number into integer
@@ -1155,15 +1194,32 @@ def generate_registration_codes(request, course_id):
internal_reference=internal_reference,
customer_reference_number=customer_reference_number
)
+
+ course = get_course_by_id(course_id, depth=0)
+ paid_modes = CourseMode.paid_modes_for_course(course_id)
+
+ if len(paid_modes) != 1:
+ msg = (
+ u"Generating Code Redeem Codes for Course '{course_id}', which must have a single paid course mode. "
+ u"This is a configuration issue. Current course modes with payment options: {paid_modes}"
+ ).format(course_id=course_id, paid_modes=paid_modes)
+ log.error(msg)
+ return HttpResponse(
+ status=500,
+ content=_(u"Unable to generate redeem codes because of course misconfiguration.")
+ )
+
+ course_mode = paid_modes[0]
+ course_price = course_mode.min_price
+
registration_codes = []
- for _ in range(course_code_number): # pylint: disable=redefined-outer-name
- generated_registration_code = save_registration_code(request.user, course_id, sale_invoice, order=None)
+ for __ in range(course_code_number): # pylint: disable=redefined-outer-name
+ generated_registration_code = save_registration_code(
+ request.user, course_id, course_mode.slug, sale_invoice, order=None
+ )
registration_codes.append(generated_registration_code)
- site_name = microsite.get_value('SITE_NAME', settings.SITE_NAME)
- course = get_course_by_id(course_id, depth=None)
- course_honor_mode = CourseMode.mode_for_course(course_id, 'honor')
- course_price = course_honor_mode.min_price
+ site_name = microsite.get_value('SITE_NAME', 'localhost')
quantity = course_code_number
discount = (float(quantity * course_price) - float(sale_price))
course_url = '{base_url}{course_about}'.format(
diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py
index a32c864f95..8db5957c14 100644
--- a/lms/djangoapps/instructor/views/instructor_dashboard.py
+++ b/lms/djangoapps/instructor/views/instructor_dashboard.py
@@ -4,6 +4,8 @@ Instructor Dashboard Views
import logging
import datetime
+from opaque_keys import InvalidKeyError
+from opaque_keys.edx.keys import CourseKey
import uuid
import pytz
@@ -15,7 +17,7 @@ from django.views.decorators.cache import cache_control
from edxmako.shortcuts import render_to_response
from django.core.urlresolvers import reverse
from django.utils.html import escape
-from django.http import Http404
+from django.http import Http404, HttpResponseServerError
from django.conf import settings
from util.json_request import JsonResponse
from mock import patch
@@ -33,7 +35,7 @@ from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR
from student.models import CourseEnrollment
from shoppingcart.models import Coupon, PaidCourseRegistration
from course_modes.models import CourseMode, CourseModesArchive
-from student.roles import CourseFinanceAdminRole
+from student.roles import CourseFinanceAdminRole, CourseSalesAdminRole
from class_dashboard.dashboard_data import get_section_display_name, get_array_section_has_problem
from .tools import get_units_with_due_date, title_or_url, bulk_email_is_enabled_for_course
@@ -47,13 +49,19 @@ log = logging.getLogger(__name__)
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def instructor_dashboard_2(request, course_id):
""" Display the instructor dashboard for a course. """
- course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
- course = get_course_by_id(course_key, depth=None)
+ try:
+ course_key = CourseKey.from_string(course_id)
+ except InvalidKeyError:
+ log.error(u"Unable to find course with course key %s while loading the Instructor Dashboard.", course_id)
+ return HttpResponseServerError()
+
+ course = get_course_by_id(course_key, depth=0)
access = {
'admin': request.user.is_staff,
'instructor': has_access(request.user, 'instructor', course),
'finance_admin': CourseFinanceAdminRole(course_key).has_user(request.user),
+ 'sales_admin': CourseSalesAdminRole(course_key).has_user(request.user),
'staff': has_access(request.user, 'staff', course),
'forum_admin': has_forum_access(
request.user, course_key, FORUM_ROLE_ADMINISTRATOR
@@ -72,10 +80,18 @@ def instructor_dashboard_2(request, course_id):
]
#check if there is corresponding entry in the CourseMode Table related to the Instructor Dashboard course
- course_honor_mode = CourseMode.mode_for_course(course_key, 'honor')
course_mode_has_price = False
- if course_honor_mode and course_honor_mode.min_price > 0:
+ paid_modes = CourseMode.paid_modes_for_course(course_key)
+ if len(paid_modes) == 1:
course_mode_has_price = True
+ elif len(paid_modes) > 1:
+ log.error(
+ u"Course %s has %s course modes with payment options. Course must only have "
+ u"one paid course mode to enable eCommerce options.",
+ unicode(course_key), len(paid_modes)
+ )
+
+ is_white_label = CourseMode.is_white_label(course_key)
if (settings.FEATURES.get('INDIVIDUAL_DUE_DATES') and access['instructor']):
sections.insert(3, _section_extensions(course))
@@ -89,8 +105,8 @@ def instructor_dashboard_2(request, course_id):
sections.append(_section_metrics(course, access))
# Gate access to Ecommerce tab
- if course_mode_has_price:
- sections.append(_section_e_commerce(course, access))
+ if course_mode_has_price and (access['finance_admin'] or access['sales_admin']):
+ sections.append(_section_e_commerce(course, access, paid_modes[0], is_white_label))
disable_buttons = not _is_small_course(course_key)
@@ -126,15 +142,13 @@ def instructor_dashboard_2(request, course_id):
## section_display_name will be used to generate link titles in the nav bar.
-def _section_e_commerce(course, access):
+def _section_e_commerce(course, access, paid_mode, coupons_enabled):
""" Provide data for the corresponding dashboard section """
course_key = course.id
coupons = Coupon.objects.filter(course_id=course_key).order_by('-is_active')
- course_price = None
+ course_price = paid_mode.min_price
+
total_amount = None
- course_honor_mode = CourseMode.mode_for_course(course_key, 'honor')
- if course_honor_mode and course_honor_mode.min_price > 0:
- course_price = course_honor_mode.min_price
if access['finance_admin']:
total_amount = PaidCourseRegistration.get_total_amount_of_purchased_item(course_key)
@@ -160,6 +174,8 @@ def _section_e_commerce(course, access):
'set_course_mode_url': reverse('set_course_mode_price', kwargs={'course_id': unicode(course_key)}),
'download_coupon_codes_url': reverse('get_coupon_codes', kwargs={'course_id': unicode(course_key)}),
'coupons': coupons,
+ 'sales_admin': access['sales_admin'],
+ 'coupons_enabled': coupons_enabled,
'course_price': course_price,
'total_amount': total_amount
}
diff --git a/lms/djangoapps/instructor_analytics/tests/test_basic.py b/lms/djangoapps/instructor_analytics/tests/test_basic.py
index 794e4aacc3..a532ccaa35 100644
--- a/lms/djangoapps/instructor_analytics/tests/test_basic.py
+++ b/lms/djangoapps/instructor_analytics/tests/test_basic.py
@@ -6,7 +6,8 @@ import json
from student.models import CourseEnrollment
from django.core.urlresolvers import reverse
from mock import patch
-from student.tests.factories import UserFactory
+from student.roles import CourseSalesAdminRole
+from student.tests.factories import UserFactory, CourseModeFactory
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from shoppingcart.models import (
CourseRegistrationCode, RegistrationCodeRedemption, Order,
@@ -149,7 +150,7 @@ class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase):
for i in range(5):
course_code = CourseRegistrationCode(
code="test_code{}".format(i), course_id=self.course.id.to_deprecated_string(),
- created_by=self.instructor, invoice=sale_invoice
+ created_by=self.instructor, invoice=sale_invoice, mode_slug='honor'
)
course_code.save()
@@ -259,6 +260,13 @@ class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase):
self.course = CourseFactory.create()
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 a paid course mode.
+ mode = CourseModeFactory.create()
+ mode.course_id = self.course.id
+ mode.min_price = 1
+ mode.save()
url = reverse('generate_registration_codes',
kwargs={'course_id': self.course.id.to_deprecated_string()})
diff --git a/lms/djangoapps/shoppingcart/exceptions.py b/lms/djangoapps/shoppingcart/exceptions.py
index c4170e629f..a93c89bcec 100644
--- a/lms/djangoapps/shoppingcart/exceptions.py
+++ b/lms/djangoapps/shoppingcart/exceptions.py
@@ -37,6 +37,11 @@ class MultipleCouponsNotAllowedException(InvalidCartItem):
pass
+class RedemptionCodeError(Exception):
+ """An error occurs while processing redemption codes. """
+ pass
+
+
class ReportException(Exception):
pass
diff --git a/lms/djangoapps/shoppingcart/migrations/0024_auto__add_field_courseregistrationcode_mode_slug.py b/lms/djangoapps/shoppingcart/migrations/0024_auto__add_field_courseregistrationcode_mode_slug.py
new file mode 100644
index 0000000000..105e441fb3
--- /dev/null
+++ b/lms/djangoapps/shoppingcart/migrations/0024_auto__add_field_courseregistrationcode_mode_slug.py
@@ -0,0 +1,218 @@
+# -*- coding: utf-8 -*-
+import 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.mode_slug'
+ db.add_column('shoppingcart_courseregistrationcode', 'mode_slug',
+ self.gf('django.db.models.fields.CharField')(max_length=100, null=True),
+ keep_default=False)
+
+ def backwards(self, orm):
+ # Deleting field 'CourseRegistrationCode.mode_slug'
+ db.delete_column('shoppingcart_courseregistrationcode', 'mode_slug')
+
+ 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, 1, 12, 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, 1, 12, 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'}),
+ '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.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'}),
+ 'address_line_3': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': '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'}),
+ 'customer_reference_number': ('django.db.models.fields.CharField', [], {'max_length': '63', 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'internal_reference': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
+ 'is_valid': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ '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.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, 1, 12, 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']
diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py
index 2e4908cca7..4f592bcefb 100644
--- a/lms/djangoapps/shoppingcart/models.py
+++ b/lms/djangoapps/shoppingcart/models.py
@@ -745,6 +745,7 @@ class CourseRegistrationCode(models.Model):
created_at = models.DateTimeField(default=datetime.now(pytz.utc))
order = models.ForeignKey(Order, db_index=True, null=True, related_name="purchase_order")
invoice = models.ForeignKey(Invoice, null=True)
+ mode_slug = models.CharField(max_length=100, null=True)
class RegistrationCodeRedemption(models.Model):
@@ -1135,7 +1136,7 @@ class CourseRegCodeItem(OrderItem):
# is in another PR (for another feature)
from instructor.views.api import save_registration_code
for i in range(total_registration_codes): # pylint: disable=unused-variable
- save_registration_code(self.user, self.course_id, invoice=None, order=self.order)
+ save_registration_code(self.user, self.course_id, self.mode, invoice=None, order=self.order)
log.info("Enrolled {0} in paid course {1}, paid ${2}"
.format(self.user.email, self.course_id, self.line_cost)) # pylint: disable=no-member
diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py
index 7e203116f3..6f6b72f8c4 100644
--- a/lms/djangoapps/shoppingcart/tests/test_views.py
+++ b/lms/djangoapps/shoppingcart/tests/test_views.py
@@ -28,6 +28,7 @@ from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase, mixed_store_config
)
from xmodule.modulestore.tests.factories import CourseFactory
+from student.roles import CourseSalesAdminRole
from util.date_utils import get_default_time_display
from util.testing import UrlResetMixin
@@ -37,7 +38,7 @@ from shoppingcart.models import (
Coupon, CourseRegistrationCode, RegistrationCodeRedemption,
DonationConfiguration
)
-from student.tests.factories import UserFactory, AdminFactory
+from student.tests.factories import UserFactory, AdminFactory, CourseModeFactory
from courseware.tests.factories import InstructorFactory
from student.models import CourseEnrollment
from course_modes.models import CourseMode
@@ -104,6 +105,10 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
self.cart = Order.get_cart_for_user(self.user)
self.addCleanup(patcher.stop)
+ self.now = datetime.now(pytz.UTC)
+ self.yesterday = self.now - timedelta(days=1)
+ self.tomorrow = self.now + timedelta(days=1)
+
def get_discount(self, cost):
"""
This method simple return the discounted amount
@@ -119,13 +124,27 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
percentage_discount=self.percentage_discount, created_by=self.user, is_active=is_active)
coupon.save()
- def add_reg_code(self, course_key):
+ def add_reg_code(self, course_key, mode_slug='honor'):
"""
add dummy registration code into models
"""
- course_reg_code = CourseRegistrationCode(code=self.reg_code, course_id=course_key, created_by=self.user)
+ course_reg_code = CourseRegistrationCode(
+ code=self.reg_code, course_id=course_key, created_by=self.user, mode_slug=mode_slug
+ )
course_reg_code.save()
+ def _add_course_mode(self, min_price=50, mode_slug='honor', expiration_date=None):
+ """
+ Adds a course mode to the test course.
+ """
+ mode = CourseModeFactory.create()
+ mode.course_id = self.course.id
+ mode.min_price = min_price
+ mode.mode_slug = mode_slug
+ mode.expiration_date = expiration_date
+ mode.save()
+ return mode
+
def add_course_to_user_cart(self, course_key):
"""
adding course to user cart
@@ -392,6 +411,31 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
self.assertEqual(resp.status_code, 404)
self.assertIn("Cart item quantity should not be greater than 1 when applying activation code", resp.content)
+ @ddt.data(True, False)
+ def test_reg_code_uses_associated_mode(self, expired_mode):
+ """Tests the use of reg codes on verified courses, expired or active. """
+ course_key = self.course_key.to_deprecated_string()
+ expiration_date = self.yesterday if expired_mode else self.tomorrow
+ self._add_course_mode(mode_slug='verified', expiration_date=expiration_date)
+ self.add_reg_code(course_key, mode_slug='verified')
+ self.add_course_to_user_cart(self.course_key)
+ resp = self.client.post(reverse('register_code_redemption', args=[self.reg_code]), HTTP_HOST='localhost')
+ self.assertEqual(resp.status_code, 200)
+ self.assertIn(self.course.display_name, resp.content)
+
+ @ddt.data(True, False)
+ def test_reg_code_uses_unknown_mode(self, expired_mode):
+ """Tests the use of reg codes on verified courses, expired or active. """
+ course_key = self.course_key.to_deprecated_string()
+ expiration_date = self.yesterday if expired_mode else self.tomorrow
+ self._add_course_mode(mode_slug='verified', expiration_date=expiration_date)
+ self.add_reg_code(course_key, mode_slug='bananas')
+ self.add_course_to_user_cart(self.course_key)
+ resp = self.client.post(reverse('register_code_redemption', args=[self.reg_code]), HTTP_HOST='localhost')
+ self.assertEqual(resp.status_code, 200)
+ self.assertIn(self.course.display_name, resp.content)
+ self.assertIn("error processing your redeem code", resp.content)
+
def test_course_discount_for_valid_active_coupon_code(self):
self.add_coupon(self.course_key, True, self.coupon_code)
@@ -1472,6 +1516,10 @@ class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase):
cache.clear()
instructor = InstructorFactory(course_key=self.course_key)
self.client.login(username=instructor.username, password='test')
+
+ # Registration Code Generation only available to Sales Admins.
+ CourseSalesAdminRole(self.course.id).add_users(instructor)
+
url = reverse('generate_registration_codes',
kwargs={'course_id': self.course.id.to_deprecated_string()})
diff --git a/lms/djangoapps/shoppingcart/views.py b/lms/djangoapps/shoppingcart/views.py
index f717b182cd..1ad35a7a70 100644
--- a/lms/djangoapps/shoppingcart/views.py
+++ b/lms/djangoapps/shoppingcart/views.py
@@ -10,6 +10,7 @@ from django.http import (
HttpResponseBadRequest, HttpResponseForbidden, Http404
)
from django.utils.translation import ugettext as _
+from course_modes.models import CourseMode
from util.json_request import JsonResponse
from django.views.decorators.http import require_POST, require_http_methods
from django.core.urlresolvers import reverse
@@ -26,12 +27,13 @@ from courseware.courses import get_course_by_id
from courseware.views import registered_for_course
from config_models.decorators import require_config
from shoppingcart.reports import RefundReport, ItemizedPurchaseReport, UniversityRevenueShareReport, CertificateStatusReport
-from student.models import CourseEnrollment
+from student.models import CourseEnrollment, EnrollmentClosedError, CourseFullError, \
+ AlreadyEnrolledError
from .exceptions import (
ItemAlreadyInCartException, AlreadyEnrolledInCourseException,
CourseDoesNotExistException, ReportTypeDoesNotExistException,
MultipleCouponsNotAllowedException, InvalidCartItem,
- ItemNotFoundInCartException
+ ItemNotFoundInCartException, RedemptionCodeError
)
from .models import (
Order, OrderTypes,
@@ -307,7 +309,6 @@ def get_reg_code_validity(registration_code, request, limiter):
@require_http_methods(["GET", "POST"])
@login_required
-@enforce_shopping_cart_enabled
def register_code_redemption(request, registration_code):
"""
This view allows the student to redeem the registration code
@@ -338,8 +339,14 @@ def register_code_redemption(request, registration_code):
elif request.method == "POST":
reg_code_is_valid, reg_code_already_redeemed, course_registration = get_reg_code_validity(registration_code,
request, limiter)
-
course = get_course_by_id(getattr(course_registration, 'course_id'), depth=0)
+ context = {
+ 'reg_code': registration_code,
+ 'site_name': site_name,
+ 'course': course,
+ 'reg_code_is_valid': reg_code_is_valid,
+ 'reg_code_already_redeemed': reg_code_already_redeemed,
+ }
if reg_code_is_valid and not reg_code_already_redeemed:
# remove the course from the cart if it was added there.
cart = Order.get_cart_for_user(request.user)
@@ -355,23 +362,30 @@ def register_code_redemption(request, registration_code):
#now redeem the reg code.
redemption = RegistrationCodeRedemption.create_invoice_generated_registration_redemption(course_registration, request.user)
- redemption.course_enrollment = CourseEnrollment.enroll(request.user, course.id)
- redemption.save()
- context = {
- 'redemption_success': True,
- 'reg_code': registration_code,
- 'site_name': site_name,
- 'course': course,
- }
+ try:
+ kwargs = {}
+ if course_registration.mode_slug is not None:
+ if CourseMode.mode_for_course(course.id, course_registration.mode_slug):
+ kwargs['mode'] = course_registration.mode_slug
+ else:
+ raise RedemptionCodeError()
+ redemption.course_enrollment = CourseEnrollment.enroll(request.user, course.id, **kwargs)
+ redemption.save()
+ context['redemption_success'] = True
+ except RedemptionCodeError:
+ context['redeem_code_error'] = True
+ context['redemption_success'] = False
+ except EnrollmentClosedError:
+ context['enrollment_closed'] = True
+ context['redemption_success'] = False
+ except CourseFullError:
+ context['course_full'] = True
+ context['redemption_success'] = False
+ except AlreadyEnrolledError:
+ context['registered_for_course'] = True
+ context['redemption_success'] = False
else:
- context = {
- 'reg_code_is_valid': reg_code_is_valid,
- 'reg_code_already_redeemed': reg_code_already_redeemed,
- 'redemption_success': False,
- 'reg_code': registration_code,
- 'site_name': site_name,
- 'course': course,
- }
+ context['redemption_success'] = False
return render_to_response(template_to_render, context)
diff --git a/lms/static/sass/views/_shoppingcart.scss b/lms/static/sass/views/_shoppingcart.scss
index 68c96b8cae..f884cdf169 100644
--- a/lms/static/sass/views/_shoppingcart.scss
+++ b/lms/static/sass/views/_shoppingcart.scss
@@ -294,7 +294,7 @@
}
}
- input[type="submit"] {
+ input[type="submit"], button {
text-transform: none;
width: 450px;
height: 70px;
diff --git a/lms/templates/instructor/instructor_dashboard_2/e-commerce.html b/lms/templates/instructor/instructor_dashboard_2/e-commerce.html
index 0930e711bb..b63b6cfa1f 100644
--- a/lms/templates/instructor/instructor_dashboard_2/e-commerce.html
+++ b/lms/templates/instructor/instructor_dashboard_2/e-commerce.html
@@ -12,9 +12,11 @@
${_('Registration Codes')}
+ %if section_data['sales_admin']:
${_('Click to generate Registration Codes')}
${_('Generate Registration Codes')}
+ %endif
${_('Click to generate a CSV file of all Course Registrations Codes:')}
+ %if section_data['coupons_enabled']:
${_("Course Price")}
@@ -53,8 +56,9 @@
+ %endif
- %if section_data['access']['finance_admin'] is True:
+ %if section_data['access']['finance_admin']:
${_("Sales")}
@@ -79,6 +83,7 @@
%endif
+ %if section_data['coupons_enabled']:
${_("Coupons List")}
@@ -132,6 +137,7 @@
+ %endif
diff --git a/lms/templates/shoppingcart/registration_code_receipt.html b/lms/templates/shoppingcart/registration_code_receipt.html
new file mode 100644
index 0000000000..7d291e2538
--- /dev/null
+++ b/lms/templates/shoppingcart/registration_code_receipt.html
@@ -0,0 +1,96 @@
+<%!
+from django.utils.translation import ugettext as _
+from django.core.urlresolvers import reverse
+from courseware.courses import course_image_url, get_course_about_section
+%>
+<%inherit file="../main.html" />
+<%namespace name='static' file='/static_content.html'/>
+<%block name="pagetitle">${_("Confirm Enrollment")}%block>
+
+<%block name="content">
+
+
+
+
+
+
})
+
+
+
${_("Confirm your enrollment for:")}
+
${_("course dates")}
+
+
+
+
+
+
+ ${_("{course_name}").format(course_name=course.display_name)}
+ ${_("{start_date}").format(start_date=course.start_datetime_text())} - ${_("{end_date}").format(end_date=course.end_datetime_text())}
+
+
+
+
+
+ % if reg_code_already_redeemed:
+ <% dashboard_url = reverse('dashboard')%>
+
+ ${_("You've clicked a link for an enrollment code that has already been used."
+ " Check your course dashboard to see if you're enrolled in the course,"
+ " or contact your company's administrator.").format(dashboard_url=dashboard_url)}
+
+ % elif redemption_success:
+
+ ${_("You have successfully enrolled in {course_name}."
+ " This course has now been added to your dashboard.").format(course_name=course.display_name)}
+
+ % elif registered_for_course:
+ <% dashboard_url = reverse('dashboard')%>
+
+ ${_("You're already registered for this course."
+ " Visit your dashboard to see the course.").format(dashboard_url=dashboard_url)}
+
+ % elif course_full:
+ <% dashboard_url = reverse('dashboard')%>
+
+ ${_("The course you are registering for is full.")}
+
+ % elif enrollment_closed:
+ <% dashboard_url = reverse('dashboard')%>
+
+ ${_("The course you are registering for is closed.")}
+
+ % elif redeem_code_error:
+ <% dashboard_url = reverse('dashboard')%>
+
+ ${_("There was an error processing your redeem code.")}
+
+ % else:
+
+ ${_("You're about to activate an enrollment code for {course_name} by {site_name}. "
+ "This code can only be used one time, so you should only activate this code if you're its intended"
+ " recipient.").format(course_name=course.display_name, site_name=site_name)}
+
+ % endif
+
+
+ % if not reg_code_already_redeemed:
+ %if redemption_success:
+ <% course_url = reverse('info', args=[course.id.to_deprecated_string()]) %>
+ ${_("View Course")}
+ %elif not registered_for_course:
+
+ %endif
+ %endif
+
+
+
+%block>
diff --git a/lms/templates/shoppingcart/registration_code_redemption.html b/lms/templates/shoppingcart/registration_code_redemption.html
index f21f0b1b2f..e8c244a0b2 100644
--- a/lms/templates/shoppingcart/registration_code_redemption.html
+++ b/lms/templates/shoppingcart/registration_code_redemption.html
@@ -73,6 +73,10 @@ from courseware.courses import course_image_url, get_course_about_section
link_end='',
)}
+ % elif redeem_code_error:
+
+ ${_( "There was an error processing your redeem code.")}
+
% else:
${_(
@@ -90,12 +94,13 @@ from courseware.courses import course_image_url, get_course_about_section
% if not reg_code_already_redeemed:
%if redemption_success:
- ${_("View Dashboard ▸")}
+ ${_("View Dashboard")}
%elif not registered_for_course:
%endif
%endif