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
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