Merge pull request #6374 from edx/sanchez/enable_redeem_codes
Enable redeem codes for Verified Cert Courses
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()})
|
||||
|
||||
@@ -37,6 +37,11 @@ class MultipleCouponsNotAllowedException(InvalidCartItem):
|
||||
pass
|
||||
|
||||
|
||||
class RedemptionCodeError(Exception):
|
||||
"""An error occurs while processing redemption codes. """
|
||||
pass
|
||||
|
||||
|
||||
class ReportException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@@ -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']
|
||||
@@ -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
|
||||
|
||||
@@ -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()})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -294,7 +294,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
input[type="submit"], button {
|
||||
text-transform: none;
|
||||
width: 450px;
|
||||
height: 70px;
|
||||
|
||||
@@ -12,9 +12,11 @@
|
||||
<div class="wrap">
|
||||
<h2>${_('Registration Codes')}</h2>
|
||||
<div>
|
||||
%if section_data['sales_admin']:
|
||||
<span class="code_tip">${_('Click to generate Registration Codes')}
|
||||
<a id="registration_code_generation_link" href="#reg_code_generation_modal" class="add blue-button">${_('Generate Registration Codes')}</a>
|
||||
</span>
|
||||
%endif
|
||||
<p>${_('Click to generate a CSV file of all Course Registrations Codes:')}</p>
|
||||
<p>
|
||||
<form action="${ section_data['get_registration_code_csv_url'] }" id="download_registration_codes" method="post">
|
||||
@@ -43,6 +45,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- end wrap -->
|
||||
%if section_data['coupons_enabled']:
|
||||
<div class="wrap">
|
||||
<h2>${_("Course Price")}</h2>
|
||||
<div>
|
||||
@@ -53,8 +56,9 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
%endif
|
||||
<!-- end wrap -->
|
||||
%if section_data['access']['finance_admin'] is True:
|
||||
%if section_data['access']['finance_admin']:
|
||||
<div class="wrap">
|
||||
<h2>${_("Sales")}</h2>
|
||||
<div>
|
||||
@@ -79,6 +83,7 @@
|
||||
</div>
|
||||
</div><!-- end wrap -->
|
||||
%endif
|
||||
%if section_data['coupons_enabled']:
|
||||
<div class="wrap">
|
||||
<h2>${_("Coupons List")}</h2>
|
||||
<div>
|
||||
@@ -132,6 +137,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
%endif
|
||||
<!-- end wrap -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
96
lms/templates/shoppingcart/registration_code_receipt.html
Normal file
96
lms/templates/shoppingcart/registration_code_receipt.html
Normal file
@@ -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">
|
||||
<div class="container">
|
||||
<section class="wrapper confirm-enrollment">
|
||||
<header class="page-header">
|
||||
<h1 class="title">
|
||||
${_("{site_name} - Confirm Enrollment").format(site_name=site_name)}
|
||||
</h1>
|
||||
</header>
|
||||
<section>
|
||||
<div class="course-image">
|
||||
<img style="width: 100%; height: auto;" src="${course_image_url(course)}"
|
||||
alt="${course.display_number_with_default | h} ${get_course_about_section(course, 'title')} Cover Image"/>
|
||||
</div>
|
||||
<div class="enrollment-details">
|
||||
<div class="sub-title">${_("Confirm your enrollment for:")}
|
||||
<span class="course-date-label">${_("course dates")}</span>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="course-title">
|
||||
<h1>
|
||||
${_("{course_name}").format(course_name=course.display_name)}
|
||||
<span class="course-dates">${_("{start_date}").format(start_date=course.start_datetime_text())} - ${_("{end_date}").format(end_date=course.end_datetime_text())}
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
<hr>
|
||||
<div>
|
||||
% if reg_code_already_redeemed:
|
||||
<% dashboard_url = reverse('dashboard')%>
|
||||
<p class="enrollment-text">
|
||||
${_("You've clicked a link for an enrollment code that has already been used."
|
||||
" Check your <a href={dashboard_url}>course dashboard</a> to see if you're enrolled in the course,"
|
||||
" or contact your company's administrator.").format(dashboard_url=dashboard_url)}
|
||||
</p>
|
||||
% elif redemption_success:
|
||||
<p class="enrollment-text">
|
||||
${_("You have successfully enrolled in {course_name}."
|
||||
" This course has now been added to your dashboard.").format(course_name=course.display_name)}
|
||||
</p>
|
||||
% elif registered_for_course:
|
||||
<% dashboard_url = reverse('dashboard')%>
|
||||
<p class="enrollment-text">
|
||||
${_("You're already registered for this course."
|
||||
" Visit your <a href={dashboard_url}>dashboard</a> to see the course.").format(dashboard_url=dashboard_url)}
|
||||
</p>
|
||||
% elif course_full:
|
||||
<% dashboard_url = reverse('dashboard')%>
|
||||
<p class="enrollment-text">
|
||||
${_("The course you are registering for is full.")}
|
||||
</p>
|
||||
% elif enrollment_closed:
|
||||
<% dashboard_url = reverse('dashboard')%>
|
||||
<p class="enrollment-text">
|
||||
${_("The course you are registering for is closed.")}
|
||||
</p>
|
||||
% elif redeem_code_error:
|
||||
<% dashboard_url = reverse('dashboard')%>
|
||||
<p class="enrollment-text">
|
||||
${_("There was an error processing your redeem code.")}
|
||||
</p>
|
||||
% else:
|
||||
<p class="enrollment-text">
|
||||
${_("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)}
|
||||
</p>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
% if not reg_code_already_redeemed:
|
||||
%if redemption_success:
|
||||
<% course_url = reverse('info', args=[course.id.to_deprecated_string()]) %>
|
||||
<a href="${course_url}" class="link-button course-link-bg-color">${_("View Course")} <i class="icon fa fa-caret-right"></i></a>
|
||||
%elif not registered_for_course:
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
|
||||
<button type="submit" id="id_active_course_enrollment"
|
||||
name="active_course_enrollment">${_("Activate Course Enrollment")} <i class="icon fa fa-caret-right"></i></button>
|
||||
</form>
|
||||
%endif
|
||||
%endif
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</%block>
|
||||
@@ -73,6 +73,10 @@ from courseware.courses import course_image_url, get_course_about_section
|
||||
link_end='</a>',
|
||||
)}
|
||||
</p>
|
||||
% elif redeem_code_error:
|
||||
<p class="enrollment-text">
|
||||
${_( "There was an error processing your redeem code.")}
|
||||
</p>
|
||||
% else:
|
||||
<p class="enrollment-text">
|
||||
${_(
|
||||
@@ -90,12 +94,13 @@ from courseware.courses import course_image_url, get_course_about_section
|
||||
</div>
|
||||
% if not reg_code_already_redeemed:
|
||||
%if redemption_success:
|
||||
<a href="${reverse('dashboard')}" class="link-button course-link-bg-color">${_("View Dashboard ▸")}</a>
|
||||
<a href="${reverse('dashboard')}" class="link-button course-link-bg-color">${_("View Dashboard")} <i class="icon fa fa-caret-right"></i></a>
|
||||
%elif not registered_for_course:
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
|
||||
<input type="submit" value="${_("Activate Course Enrollment")} ▸"
|
||||
id="id_active_course_enrollment" name="active_course_enrollment">
|
||||
<button type="submit"
|
||||
id="id_active_course_enrollment"
|
||||
name="active_course_enrollment">${_("Activate Course Enrollment")} <i class="icon fa fa-caret-right"></i></button>
|
||||
</form>
|
||||
%endif
|
||||
%endif
|
||||
|
||||
Reference in New Issue
Block a user