Commit 158a0a0e by Stephen Sanchez

Merge pull request #6374 from edx/sanchez/enable_redeem_codes

Enable redeem codes for Verified Cert Courses
parents 6d184f7b b1884306
......@@ -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())
......@@ -131,6 +121,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):
"""
Returns a list of the non-expired modes for a given 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
......
# -*- 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
......
# -*- 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)
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,
'reg_code': registration_code,
'site_name': site_name,
'course': course,
}
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>
......
<%!
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 &nbsp; &nbsp; &#x25b8;")}</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")} &#x25b8;"
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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment