Commit b1884306 by stephensanchez

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
parent 70b38fc3
......@@ -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
......
......@@ -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
......
......@@ -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>
......
<%!
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