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): ...@@ -81,17 +81,7 @@ class CourseMode(models.Model):
""" """
modes_by_course = defaultdict(list) modes_by_course = defaultdict(list)
for mode in cls.objects.filter(course_id__in=course_id_list): for mode in cls.objects.filter(course_id__in=course_id_list):
modes_by_course[mode.course_id].append( modes_by_course[mode.course_id].append(mode.to_tuple())
Mode(
mode.mode_slug,
mode.mode_display_name,
mode.min_price,
mode.suggested_prices,
mode.currency,
mode.expiration_datetime,
mode.description
)
)
# Assign default modes if nothing available in the database # Assign default modes if nothing available in the database
missing_courses = set(course_id_list) - set(modes_by_course.keys()) missing_courses = set(course_id_list) - set(modes_by_course.keys())
...@@ -131,6 +121,31 @@ class CourseMode(models.Model): ...@@ -131,6 +121,31 @@ class CourseMode(models.Model):
return (all_modes, unexpired_modes) return (all_modes, unexpired_modes)
@classmethod @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): def modes_for_course(cls, course_id):
""" """
Returns a list of the non-expired modes for a given course id Returns a list of the non-expired modes for a given course id
...@@ -141,15 +156,7 @@ class CourseMode(models.Model): ...@@ -141,15 +156,7 @@ class CourseMode(models.Model):
found_course_modes = cls.objects.filter(Q(course_id=course_id) & found_course_modes = cls.objects.filter(Q(course_id=course_id) &
(Q(expiration_datetime__isnull=True) | (Q(expiration_datetime__isnull=True) |
Q(expiration_datetime__gte=now))) Q(expiration_datetime__gte=now)))
modes = ([Mode( modes = ([mode.to_tuple() for mode in found_course_modes])
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])
if not modes: if not modes:
modes = [cls.DEFAULT_MODE] modes = [cls.DEFAULT_MODE]
return modes return modes
...@@ -359,6 +366,24 @@ class CourseMode(models.Model): ...@@ -359,6 +366,24 @@ class CourseMode(models.Model):
modes = cls.modes_for_course(course_id) modes = cls.modes_for_course(course_id)
return min(mode.min_price for mode in modes if mode.currency == currency) 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): def __unicode__(self):
return u"{} : {}, min={}, prices={}".format( return u"{} : {}, min={}, prices={}".format(
self.course_id.to_deprecated_string(), self.mode_slug, self.min_price, self.suggested_prices self.course_id.to_deprecated_string(), self.mode_slug, self.min_price, self.suggested_prices
......
...@@ -204,13 +204,21 @@ class CourseInstructorRole(CourseRole): ...@@ -204,13 +204,21 @@ class CourseInstructorRole(CourseRole):
class CourseFinanceAdminRole(CourseRole): class CourseFinanceAdminRole(CourseRole):
"""A course Instructor""" """A course staff member with privileges to review financial data."""
ROLE = 'finance_admin' ROLE = 'finance_admin'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(CourseFinanceAdminRole, self).__init__(self.ROLE, *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): class CourseBetaTesterRole(CourseRole):
"""A course Beta Tester""" """A course Beta Tester"""
ROLE = 'beta_testers' ROLE = 'beta_testers'
......
...@@ -280,8 +280,9 @@ class DashboardTest(ModuleStoreTestCase): ...@@ -280,8 +280,9 @@ class DashboardTest(ModuleStoreTestCase):
recipient_name='Testw_1', recipient_email='test2@test.com', internal_reference="A", recipient_name='Testw_1', recipient_email='test2@test.com', internal_reference="A",
course_id=self.course.id, is_valid=False course_id=self.course.id, is_valid=False
) )
course_reg_code = shoppingcart.models.CourseRegistrationCode(code="abcde", course_id=self.course.id, course_reg_code = shoppingcart.models.CourseRegistrationCode(
created_by=self.user, invoice=sale_invoice_1) code="abcde", course_id=self.course.id, created_by=self.user, invoice=sale_invoice_1, mode_slug='honor'
)
course_reg_code.save() course_reg_code.save()
cart = shoppingcart.models.Order.get_cart_for_user(self.user) cart = shoppingcart.models.Order.get_cart_for_user(self.user)
......
...@@ -13,7 +13,6 @@ import random ...@@ -13,7 +13,6 @@ import random
import requests import requests
import shutil import shutil
import tempfile import tempfile
from unittest import TestCase
from urllib import quote from urllib import quote
from django.conf import settings from django.conf import settings
...@@ -45,8 +44,8 @@ from shoppingcart.models import ( ...@@ -45,8 +44,8 @@ from shoppingcart.models import (
from student.models import ( from student.models import (
CourseEnrollment, CourseEnrollmentAllowed, NonExistentCourseError CourseEnrollment, CourseEnrollmentAllowed, NonExistentCourseError
) )
from student.tests.factories import UserFactory from student.tests.factories import UserFactory, CourseModeFactory
from student.roles import CourseBetaTesterRole from student.roles import CourseBetaTesterRole, CourseSalesAdminRole, CourseFinanceAdminRole
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
...@@ -1722,7 +1721,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa ...@@ -1722,7 +1721,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
for i in range(2): for i in range(2):
course_registration_code = CourseRegistrationCode( course_registration_code = CourseRegistrationCode(
code='sale_invoice{}'.format(i), course_id=self.course.id.to_deprecated_string(), 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() course_registration_code.save()
...@@ -1807,6 +1806,10 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa ...@@ -1807,6 +1806,10 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
percentage_discount='10', created_by=self.instructor, is_active=True percentage_discount='10', created_by=self.instructor, is_active=True
) )
coupon.save() 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) PaidCourseRegistration.add_to_order(self.cart, self.course.id)
# apply the coupon code to the item in the cart # apply the coupon code to the item in the cart
resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': coupon.code}) resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': coupon.code})
...@@ -1838,7 +1841,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa ...@@ -1838,7 +1841,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
for i in range(2): for i in range(2):
course_registration_code = CourseRegistrationCode( course_registration_code = CourseRegistrationCode(
code='sale_invoice{}'.format(i), course_id=self.course.id.to_deprecated_string(), 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() course_registration_code.save()
...@@ -1853,7 +1856,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa ...@@ -1853,7 +1856,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
for i in range(5): for i in range(5):
course_registration_code = CourseRegistrationCode( course_registration_code = CourseRegistrationCode(
code='sale_invoice{}'.format(i), course_id=self.course.id.to_deprecated_string(), 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() course_registration_code.save()
...@@ -1872,7 +1875,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa ...@@ -1872,7 +1875,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
for i in range(5): for i in range(5):
course_registration_code = CourseRegistrationCode( course_registration_code = CourseRegistrationCode(
code='qwerty{}'.format(i), course_id=self.course.id.to_deprecated_string(), 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() course_registration_code.save()
...@@ -1886,7 +1889,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa ...@@ -1886,7 +1889,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
for i in range(5): for i in range(5):
course_registration_code = CourseRegistrationCode( course_registration_code = CourseRegistrationCode(
code='xyzmn{}'.format(i), course_id=self.course.id.to_deprecated_string(), 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() course_registration_code.save()
...@@ -2994,8 +2997,10 @@ class TestCourseRegistrationCodes(ModuleStoreTestCase): ...@@ -2994,8 +2997,10 @@ class TestCourseRegistrationCodes(ModuleStoreTestCase):
Fixtures. Fixtures.
""" """
self.course = CourseFactory.create() self.course = CourseFactory.create()
CourseModeFactory.create(course_id=self.course.id, min_price=50)
self.instructor = InstructorFactory(course_key=self.course.id) self.instructor = InstructorFactory(course_key=self.course.id)
self.client.login(username=self.instructor.username, password='test') self.client.login(username=self.instructor.username, password='test')
CourseSalesAdminRole(self.course.id).add_users(self.instructor)
url = reverse('generate_registration_codes', url = reverse('generate_registration_codes',
kwargs={'course_id': self.course.id.to_deprecated_string()}) kwargs={'course_id': self.course.id.to_deprecated_string()})
......
...@@ -50,6 +50,8 @@ class TestECommerceDashboardViews(ModuleStoreTestCase): ...@@ -50,6 +50,8 @@ class TestECommerceDashboardViews(ModuleStoreTestCase):
""" """
response = self.client.get(self.url) response = self.client.get(self.url)
self.assertTrue(self.e_commerce_link in response.content) 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): def test_user_has_finance_admin_rights_in_e_commerce_tab(self):
response = self.client.get(self.url) response = self.client.get(self.url)
...@@ -190,7 +192,8 @@ class TestECommerceDashboardViews(ModuleStoreTestCase): ...@@ -190,7 +192,8 @@ class TestECommerceDashboardViews(ModuleStoreTestCase):
self.assertTrue('Please Enter the Integer Value for Coupon Discount' in response.content) self.assertTrue('Please Enter the Integer Value for Coupon Discount' in response.content)
course_registration = CourseRegistrationCode( 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() course_registration.save()
...@@ -288,3 +291,20 @@ class TestECommerceDashboardViews(ModuleStoreTestCase): ...@@ -288,3 +291,20 @@ class TestECommerceDashboardViews(ModuleStoreTestCase):
data['coupon_id'] = '' # Coupon id is not provided data['coupon_id'] = '' # Coupon id is not provided
response = self.client.post(update_coupon_url, data=data) response = self.client.post(update_coupon_url, data=data)
self.assertTrue('coupon id not found' in response.content) 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 ...@@ -28,6 +28,8 @@ import string # pylint: disable=deprecated-module
import random import random
import unicodecsv import unicodecsv
import urllib 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 from util.file import store_uploaded_file, course_and_time_based_filename_generator, FileValidationException, UniversalNewlineIterator
import datetime import datetime
import pytz import pytz
...@@ -214,7 +216,7 @@ def require_level(level): ...@@ -214,7 +216,7 @@ def require_level(level):
def decorator(func): # pylint: disable=missing-docstring def decorator(func): # pylint: disable=missing-docstring
def wrapped(*args, **kwargs): # pylint: disable=missing-docstring def wrapped(*args, **kwargs): # pylint: disable=missing-docstring
request = args[0] 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): if has_access(request.user, level, course):
return func(*args, **kwargs) return func(*args, **kwargs)
...@@ -224,6 +226,31 @@ def require_level(level): ...@@ -224,6 +226,31 @@ def require_level(level):
return decorator 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 EMAIL_INDEX = 0
USERNAME_INDEX = 1 USERNAME_INDEX = 1
NAME_INDEX = 2 NAME_INDEX = 2
...@@ -1024,10 +1051,21 @@ def get_coupon_codes(request, course_id): # pylint: disable=unused-argument ...@@ -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) 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 recursive function that generate a new code every time and saves in the Course Registration Table
if validation check passes 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() code = random_code_generator()
...@@ -1038,10 +1076,11 @@ def save_registration_code(user, course_id, invoice=None, order=None): ...@@ -1038,10 +1076,11 @@ def save_registration_code(user, course_id, invoice=None, order=None):
course_registration = CourseRegistrationCode( course_registration = CourseRegistrationCode(
code=code, code=code,
course_id=course_id.to_deprecated_string(), course_id=unicode(course_id),
created_by=user, created_by=user,
invoice=invoice, invoice=invoice,
order=order order=order,
mode_slug=mode_slug
) )
try: try:
course_registration.save() course_registration.save()
...@@ -1101,13 +1140,13 @@ def get_registration_codes(request, course_id): # pylint: disable=unused-argume ...@@ -1101,13 +1140,13 @@ def get_registration_codes(request, course_id): # pylint: disable=unused-argume
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff') @require_sales_admin
@require_POST @require_POST
def generate_registration_codes(request, course_id): def generate_registration_codes(request, course_id):
""" """
Respond with csv which contains a summary of all Generated Codes. 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 invoice_copy = False
# covert the course registration code number into integer # covert the course registration code number into integer
...@@ -1155,15 +1194,32 @@ def generate_registration_codes(request, course_id): ...@@ -1155,15 +1194,32 @@ def generate_registration_codes(request, course_id):
internal_reference=internal_reference, internal_reference=internal_reference,
customer_reference_number=customer_reference_number 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 = [] registration_codes = []
for _ in range(course_code_number): # pylint: disable=redefined-outer-name 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) generated_registration_code = save_registration_code(
request.user, course_id, course_mode.slug, sale_invoice, order=None
)
registration_codes.append(generated_registration_code) registration_codes.append(generated_registration_code)
site_name = microsite.get_value('SITE_NAME', settings.SITE_NAME) site_name = microsite.get_value('SITE_NAME', 'localhost')
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
quantity = course_code_number quantity = course_code_number
discount = (float(quantity * course_price) - float(sale_price)) discount = (float(quantity * course_price) - float(sale_price))
course_url = '{base_url}{course_about}'.format( course_url = '{base_url}{course_about}'.format(
......
...@@ -4,6 +4,8 @@ Instructor Dashboard Views ...@@ -4,6 +4,8 @@ Instructor Dashboard Views
import logging import logging
import datetime import datetime
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
import uuid import uuid
import pytz import pytz
...@@ -15,7 +17,7 @@ from django.views.decorators.cache import cache_control ...@@ -15,7 +17,7 @@ from django.views.decorators.cache import cache_control
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.html import escape from django.utils.html import escape
from django.http import Http404 from django.http import Http404, HttpResponseServerError
from django.conf import settings from django.conf import settings
from util.json_request import JsonResponse from util.json_request import JsonResponse
from mock import patch from mock import patch
...@@ -33,7 +35,7 @@ from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR ...@@ -33,7 +35,7 @@ from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR
from student.models import CourseEnrollment from student.models import CourseEnrollment
from shoppingcart.models import Coupon, PaidCourseRegistration from shoppingcart.models import Coupon, PaidCourseRegistration
from course_modes.models import CourseMode, CourseModesArchive 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 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 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__) ...@@ -47,13 +49,19 @@ log = logging.getLogger(__name__)
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def instructor_dashboard_2(request, course_id): def instructor_dashboard_2(request, course_id):
""" Display the instructor dashboard for a course. """ """ Display the instructor dashboard for a course. """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) try:
course = get_course_by_id(course_key, depth=None) 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 = { access = {
'admin': request.user.is_staff, 'admin': request.user.is_staff,
'instructor': has_access(request.user, 'instructor', course), 'instructor': has_access(request.user, 'instructor', course),
'finance_admin': CourseFinanceAdminRole(course_key).has_user(request.user), '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), 'staff': has_access(request.user, 'staff', course),
'forum_admin': has_forum_access( 'forum_admin': has_forum_access(
request.user, course_key, FORUM_ROLE_ADMINISTRATOR request.user, course_key, FORUM_ROLE_ADMINISTRATOR
...@@ -72,10 +80,18 @@ def instructor_dashboard_2(request, course_id): ...@@ -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 #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 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 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']): if (settings.FEATURES.get('INDIVIDUAL_DUE_DATES') and access['instructor']):
sections.insert(3, _section_extensions(course)) sections.insert(3, _section_extensions(course))
...@@ -89,8 +105,8 @@ def instructor_dashboard_2(request, course_id): ...@@ -89,8 +105,8 @@ def instructor_dashboard_2(request, course_id):
sections.append(_section_metrics(course, access)) sections.append(_section_metrics(course, access))
# Gate access to Ecommerce tab # Gate access to Ecommerce tab
if course_mode_has_price: if course_mode_has_price and (access['finance_admin'] or access['sales_admin']):
sections.append(_section_e_commerce(course, access)) sections.append(_section_e_commerce(course, access, paid_modes[0], is_white_label))
disable_buttons = not _is_small_course(course_key) disable_buttons = not _is_small_course(course_key)
...@@ -126,15 +142,13 @@ def instructor_dashboard_2(request, course_id): ...@@ -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. ## 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 """ """ Provide data for the corresponding dashboard section """
course_key = course.id course_key = course.id
coupons = Coupon.objects.filter(course_id=course_key).order_by('-is_active') coupons = Coupon.objects.filter(course_id=course_key).order_by('-is_active')
course_price = None course_price = paid_mode.min_price
total_amount = None 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']: if access['finance_admin']:
total_amount = PaidCourseRegistration.get_total_amount_of_purchased_item(course_key) total_amount = PaidCourseRegistration.get_total_amount_of_purchased_item(course_key)
...@@ -160,6 +174,8 @@ def _section_e_commerce(course, access): ...@@ -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)}), '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)}), 'download_coupon_codes_url': reverse('get_coupon_codes', kwargs={'course_id': unicode(course_key)}),
'coupons': coupons, 'coupons': coupons,
'sales_admin': access['sales_admin'],
'coupons_enabled': coupons_enabled,
'course_price': course_price, 'course_price': course_price,
'total_amount': total_amount 'total_amount': total_amount
} }
......
...@@ -6,7 +6,8 @@ import json ...@@ -6,7 +6,8 @@ import json
from student.models import CourseEnrollment from student.models import CourseEnrollment
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from mock import patch 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 opaque_keys.edx.locations import SlashSeparatedCourseKey
from shoppingcart.models import ( from shoppingcart.models import (
CourseRegistrationCode, RegistrationCodeRedemption, Order, CourseRegistrationCode, RegistrationCodeRedemption, Order,
...@@ -149,7 +150,7 @@ class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase): ...@@ -149,7 +150,7 @@ class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase):
for i in range(5): for i in range(5):
course_code = CourseRegistrationCode( course_code = CourseRegistrationCode(
code="test_code{}".format(i), course_id=self.course.id.to_deprecated_string(), 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() course_code.save()
...@@ -259,6 +260,13 @@ class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase): ...@@ -259,6 +260,13 @@ class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase):
self.course = CourseFactory.create() self.course = CourseFactory.create()
self.instructor = InstructorFactory(course_key=self.course.id) self.instructor = InstructorFactory(course_key=self.course.id)
self.client.login(username=self.instructor.username, password='test') 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', url = reverse('generate_registration_codes',
kwargs={'course_id': self.course.id.to_deprecated_string()}) kwargs={'course_id': self.course.id.to_deprecated_string()})
......
...@@ -37,6 +37,11 @@ class MultipleCouponsNotAllowedException(InvalidCartItem): ...@@ -37,6 +37,11 @@ class MultipleCouponsNotAllowedException(InvalidCartItem):
pass pass
class RedemptionCodeError(Exception):
"""An error occurs while processing redemption codes. """
pass
class ReportException(Exception): class ReportException(Exception):
pass pass
......
...@@ -745,6 +745,7 @@ class CourseRegistrationCode(models.Model): ...@@ -745,6 +745,7 @@ class CourseRegistrationCode(models.Model):
created_at = models.DateTimeField(default=datetime.now(pytz.utc)) created_at = models.DateTimeField(default=datetime.now(pytz.utc))
order = models.ForeignKey(Order, db_index=True, null=True, related_name="purchase_order") order = models.ForeignKey(Order, db_index=True, null=True, related_name="purchase_order")
invoice = models.ForeignKey(Invoice, null=True) invoice = models.ForeignKey(Invoice, null=True)
mode_slug = models.CharField(max_length=100, null=True)
class RegistrationCodeRedemption(models.Model): class RegistrationCodeRedemption(models.Model):
...@@ -1135,7 +1136,7 @@ class CourseRegCodeItem(OrderItem): ...@@ -1135,7 +1136,7 @@ class CourseRegCodeItem(OrderItem):
# is in another PR (for another feature) # is in another PR (for another feature)
from instructor.views.api import save_registration_code from instructor.views.api import save_registration_code
for i in range(total_registration_codes): # pylint: disable=unused-variable 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}" log.info("Enrolled {0} in paid course {1}, paid ${2}"
.format(self.user.email, self.course_id, self.line_cost)) # pylint: disable=no-member .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 ( ...@@ -28,6 +28,7 @@ from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase, mixed_store_config ModuleStoreTestCase, mixed_store_config
) )
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from student.roles import CourseSalesAdminRole
from util.date_utils import get_default_time_display from util.date_utils import get_default_time_display
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
...@@ -37,7 +38,7 @@ from shoppingcart.models import ( ...@@ -37,7 +38,7 @@ from shoppingcart.models import (
Coupon, CourseRegistrationCode, RegistrationCodeRedemption, Coupon, CourseRegistrationCode, RegistrationCodeRedemption,
DonationConfiguration DonationConfiguration
) )
from student.tests.factories import UserFactory, AdminFactory from student.tests.factories import UserFactory, AdminFactory, CourseModeFactory
from courseware.tests.factories import InstructorFactory from courseware.tests.factories import InstructorFactory
from student.models import CourseEnrollment from student.models import CourseEnrollment
from course_modes.models import CourseMode from course_modes.models import CourseMode
...@@ -104,6 +105,10 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -104,6 +105,10 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
self.cart = Order.get_cart_for_user(self.user) self.cart = Order.get_cart_for_user(self.user)
self.addCleanup(patcher.stop) 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): def get_discount(self, cost):
""" """
This method simple return the discounted amount This method simple return the discounted amount
...@@ -119,13 +124,27 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -119,13 +124,27 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
percentage_discount=self.percentage_discount, created_by=self.user, is_active=is_active) percentage_discount=self.percentage_discount, created_by=self.user, is_active=is_active)
coupon.save() 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 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() 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): def add_course_to_user_cart(self, course_key):
""" """
adding course to user cart adding course to user cart
...@@ -392,6 +411,31 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -392,6 +411,31 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
self.assertEqual(resp.status_code, 404) self.assertEqual(resp.status_code, 404)
self.assertIn("Cart item quantity should not be greater than 1 when applying activation code", resp.content) 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): def test_course_discount_for_valid_active_coupon_code(self):
self.add_coupon(self.course_key, True, self.coupon_code) self.add_coupon(self.course_key, True, self.coupon_code)
...@@ -1472,6 +1516,10 @@ class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase): ...@@ -1472,6 +1516,10 @@ class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase):
cache.clear() cache.clear()
instructor = InstructorFactory(course_key=self.course_key) instructor = InstructorFactory(course_key=self.course_key)
self.client.login(username=instructor.username, password='test') 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', url = reverse('generate_registration_codes',
kwargs={'course_id': self.course.id.to_deprecated_string()}) kwargs={'course_id': self.course.id.to_deprecated_string()})
......
...@@ -10,6 +10,7 @@ from django.http import ( ...@@ -10,6 +10,7 @@ from django.http import (
HttpResponseBadRequest, HttpResponseForbidden, Http404 HttpResponseBadRequest, HttpResponseForbidden, Http404
) )
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from course_modes.models import CourseMode
from util.json_request import JsonResponse from util.json_request import JsonResponse
from django.views.decorators.http import require_POST, require_http_methods from django.views.decorators.http import require_POST, require_http_methods
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -26,12 +27,13 @@ from courseware.courses import get_course_by_id ...@@ -26,12 +27,13 @@ from courseware.courses import get_course_by_id
from courseware.views import registered_for_course from courseware.views import registered_for_course
from config_models.decorators import require_config from config_models.decorators import require_config
from shoppingcart.reports import RefundReport, ItemizedPurchaseReport, UniversityRevenueShareReport, CertificateStatusReport from shoppingcart.reports import RefundReport, ItemizedPurchaseReport, UniversityRevenueShareReport, CertificateStatusReport
from student.models import CourseEnrollment from student.models import CourseEnrollment, EnrollmentClosedError, CourseFullError, \
AlreadyEnrolledError
from .exceptions import ( from .exceptions import (
ItemAlreadyInCartException, AlreadyEnrolledInCourseException, ItemAlreadyInCartException, AlreadyEnrolledInCourseException,
CourseDoesNotExistException, ReportTypeDoesNotExistException, CourseDoesNotExistException, ReportTypeDoesNotExistException,
MultipleCouponsNotAllowedException, InvalidCartItem, MultipleCouponsNotAllowedException, InvalidCartItem,
ItemNotFoundInCartException ItemNotFoundInCartException, RedemptionCodeError
) )
from .models import ( from .models import (
Order, OrderTypes, Order, OrderTypes,
...@@ -307,7 +309,6 @@ def get_reg_code_validity(registration_code, request, limiter): ...@@ -307,7 +309,6 @@ def get_reg_code_validity(registration_code, request, limiter):
@require_http_methods(["GET", "POST"]) @require_http_methods(["GET", "POST"])
@login_required @login_required
@enforce_shopping_cart_enabled
def register_code_redemption(request, registration_code): def register_code_redemption(request, registration_code):
""" """
This view allows the student to redeem the registration code This view allows the student to redeem the registration code
...@@ -338,8 +339,14 @@ def register_code_redemption(request, registration_code): ...@@ -338,8 +339,14 @@ def register_code_redemption(request, registration_code):
elif request.method == "POST": elif request.method == "POST":
reg_code_is_valid, reg_code_already_redeemed, course_registration = get_reg_code_validity(registration_code, reg_code_is_valid, reg_code_already_redeemed, course_registration = get_reg_code_validity(registration_code,
request, limiter) request, limiter)
course = get_course_by_id(getattr(course_registration, 'course_id'), depth=0) 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: if reg_code_is_valid and not reg_code_already_redeemed:
# remove the course from the cart if it was added there. # remove the course from the cart if it was added there.
cart = Order.get_cart_for_user(request.user) cart = Order.get_cart_for_user(request.user)
...@@ -355,23 +362,30 @@ def register_code_redemption(request, registration_code): ...@@ -355,23 +362,30 @@ def register_code_redemption(request, registration_code):
#now redeem the reg code. #now redeem the reg code.
redemption = RegistrationCodeRedemption.create_invoice_generated_registration_redemption(course_registration, request.user) redemption = RegistrationCodeRedemption.create_invoice_generated_registration_redemption(course_registration, request.user)
redemption.course_enrollment = CourseEnrollment.enroll(request.user, course.id) try:
redemption.save() kwargs = {}
context = { if course_registration.mode_slug is not None:
'redemption_success': True, if CourseMode.mode_for_course(course.id, course_registration.mode_slug):
'reg_code': registration_code, kwargs['mode'] = course_registration.mode_slug
'site_name': site_name, else:
'course': course, 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: else:
context = { context['redemption_success'] = False
'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,
}
return render_to_response(template_to_render, context) return render_to_response(template_to_render, context)
......
...@@ -294,7 +294,7 @@ ...@@ -294,7 +294,7 @@
} }
} }
input[type="submit"] { input[type="submit"], button {
text-transform: none; text-transform: none;
width: 450px; width: 450px;
height: 70px; height: 70px;
......
...@@ -12,9 +12,11 @@ ...@@ -12,9 +12,11 @@
<div class="wrap"> <div class="wrap">
<h2>${_('Registration Codes')}</h2> <h2>${_('Registration Codes')}</h2>
<div> <div>
%if section_data['sales_admin']:
<span class="code_tip">${_('Click to generate Registration Codes')} <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> <a id="registration_code_generation_link" href="#reg_code_generation_modal" class="add blue-button">${_('Generate Registration Codes')}</a>
</span> </span>
%endif
<p>${_('Click to generate a CSV file of all Course Registrations Codes:')}</p> <p>${_('Click to generate a CSV file of all Course Registrations Codes:')}</p>
<p> <p>
<form action="${ section_data['get_registration_code_csv_url'] }" id="download_registration_codes" method="post"> <form action="${ section_data['get_registration_code_csv_url'] }" id="download_registration_codes" method="post">
...@@ -43,6 +45,7 @@ ...@@ -43,6 +45,7 @@
</div> </div>
</div> </div>
<!-- end wrap --> <!-- end wrap -->
%if section_data['coupons_enabled']:
<div class="wrap"> <div class="wrap">
<h2>${_("Course Price")}</h2> <h2>${_("Course Price")}</h2>
<div> <div>
...@@ -53,8 +56,9 @@ ...@@ -53,8 +56,9 @@
</span> </span>
</div> </div>
</div> </div>
%endif
<!-- end wrap --> <!-- end wrap -->
%if section_data['access']['finance_admin'] is True: %if section_data['access']['finance_admin']:
<div class="wrap"> <div class="wrap">
<h2>${_("Sales")}</h2> <h2>${_("Sales")}</h2>
<div> <div>
...@@ -79,6 +83,7 @@ ...@@ -79,6 +83,7 @@
</div> </div>
</div><!-- end wrap --> </div><!-- end wrap -->
%endif %endif
%if section_data['coupons_enabled']:
<div class="wrap"> <div class="wrap">
<h2>${_("Coupons List")}</h2> <h2>${_("Coupons List")}</h2>
<div> <div>
...@@ -132,6 +137,7 @@ ...@@ -132,6 +137,7 @@
</div> </div>
</div> </div>
</div> </div>
%endif
<!-- end wrap --> <!-- end wrap -->
</div> </div>
</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 ...@@ -73,6 +73,10 @@ from courseware.courses import course_image_url, get_course_about_section
link_end='</a>', link_end='</a>',
)} )}
</p> </p>
% elif redeem_code_error:
<p class="enrollment-text">
${_( "There was an error processing your redeem code.")}
</p>
% else: % else:
<p class="enrollment-text"> <p class="enrollment-text">
${_( ${_(
...@@ -90,12 +94,13 @@ from courseware.courses import course_image_url, get_course_about_section ...@@ -90,12 +94,13 @@ from courseware.courses import course_image_url, get_course_about_section
</div> </div>
% if not reg_code_already_redeemed: % if not reg_code_already_redeemed:
%if redemption_success: %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: %elif not registered_for_course:
<form method="post"> <form method="post">
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }"> <input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
<input type="submit" value="${_("Activate Course Enrollment")} &#x25b8;" <button type="submit"
id="id_active_course_enrollment" name="active_course_enrollment"> id="id_active_course_enrollment"
name="active_course_enrollment">${_("Activate Course Enrollment")} <i class="icon fa fa-caret-right"></i></button>
</form> </form>
%endif %endif
%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