added registration-codes generation functionality
rebased and resolve conficts with cdoge/registration_codes feature enhancement request: added transaction group name text field to the download buttons as an extra optional query paramerter
This commit is contained in:
committed by
Chris Dodge
parent
08ff030521
commit
4333e53997
@@ -44,7 +44,7 @@ import instructor.views.api
|
||||
from instructor.views.api import _split_input_list, common_exceptions_400
|
||||
from instructor_task.api_helper import AlreadyRunningError
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from shoppingcart.models import Order, PaidCourseRegistration, Coupon
|
||||
from shoppingcart.models import CourseRegistrationCode, RegistrationCodeRedemption, Order, PaidCourseRegistration, Coupon
|
||||
from course_modes.models import CourseMode
|
||||
|
||||
from .test_tools import msk_from_problem_urlname, get_extended_due
|
||||
@@ -2301,3 +2301,192 @@ class TestDueDateExtensions(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
u'header': [u'Unit', u'Extended Due Date'],
|
||||
u'title': u'Due date extensions for %s (%s)' % (
|
||||
self.user1.profile.name, self.user1.username)})
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
@override_settings(REGISTRATION_CODE_LENGTH=8)
|
||||
class TestCourseRegistrationCodes(ModuleStoreTestCase):
|
||||
"""
|
||||
Test data dumps for E-commerce Course Registration Codes.
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Fixtures.
|
||||
"""
|
||||
self.course = CourseFactory.create()
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
|
||||
# Active Registration Codes
|
||||
for i in range(12):
|
||||
course_registration_code = CourseRegistrationCode(
|
||||
code='MyCode0{}'.format(i), course_id=self.course.id.to_deprecated_string(),
|
||||
transaction_group_name='Test Group', created_by=self.instructor
|
||||
)
|
||||
course_registration_code.save()
|
||||
|
||||
for i in range(5):
|
||||
order = Order(user=self.instructor, status='purchased')
|
||||
order.save()
|
||||
|
||||
# Spent(used) Registration Codes
|
||||
for i in range(5):
|
||||
i += 1
|
||||
registration_code_redemption = RegistrationCodeRedemption(
|
||||
order_id=i, registration_code_id=i, redeemed_by=self.instructor
|
||||
)
|
||||
registration_code_redemption.save()
|
||||
|
||||
def test_generate_course_registration_codes_csv(self):
|
||||
"""
|
||||
Test to generate a response of all the generated course registration codes
|
||||
"""
|
||||
url = reverse('generate_registration_codes',
|
||||
kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
|
||||
data = {'course_registration_code_number': 15.0, 'transaction_group_name': 'Test Group'}
|
||||
|
||||
response = self.client.post(url, data)
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
self.assertEqual(response['Content-Type'], 'text/csv')
|
||||
body = response.content.replace('\r', '')
|
||||
self.assertTrue(body.startswith('"code","course_id","transaction_group_name","created_by","redeemed_by"'))
|
||||
self.assertEqual(len(body.split('\n')), 17)
|
||||
|
||||
@patch.object(instructor.views.api, 'random_code_generator', Mock(side_effect=['first', 'second', 'third', 'fourth']))
|
||||
def test_generate_course_registration_codes_matching_existing_coupon_code(self):
|
||||
"""
|
||||
Test the generated course registration code is already in the Coupon Table
|
||||
"""
|
||||
url = reverse('generate_registration_codes',
|
||||
kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
|
||||
coupon = Coupon(code='first', course_id=self.course.id.to_deprecated_string(), created_by=self.instructor)
|
||||
coupon.save()
|
||||
data = {'course_registration_code_number': 3, 'transaction_group_name': 'Test Group'}
|
||||
|
||||
response = self.client.post(url, data)
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
self.assertEqual(response['Content-Type'], 'text/csv')
|
||||
body = response.content.replace('\r', '')
|
||||
self.assertTrue(body.startswith('"code","course_id","transaction_group_name","created_by","redeemed_by"'))
|
||||
self.assertEqual(len(body.split('\n')), 5) # 1 for headers, 1 for new line at the end and 3 for the actual data
|
||||
|
||||
@patch.object(instructor.views.api, 'random_code_generator', Mock(side_effect=['first', 'first', 'second', 'third']))
|
||||
def test_generate_course_registration_codes_integrity_error(self):
|
||||
"""
|
||||
Test for the Integrity error against the generated code
|
||||
"""
|
||||
url = reverse('generate_registration_codes',
|
||||
kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
|
||||
data = {'course_registration_code_number': 2, 'transaction_group_name': 'Test Group'}
|
||||
|
||||
response = self.client.post(url, data)
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
self.assertEqual(response['Content-Type'], 'text/csv')
|
||||
body = response.content.replace('\r', '')
|
||||
self.assertTrue(body.startswith('"code","course_id","transaction_group_name","created_by","redeemed_by"'))
|
||||
self.assertEqual(len(body.split('\n')), 4)
|
||||
|
||||
def test_spent_course_registration_codes_csv(self):
|
||||
"""
|
||||
Test to generate a response of all the spent course registration codes
|
||||
"""
|
||||
url = reverse('spent_registration_codes',
|
||||
kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
|
||||
data = {'spent_transaction_group_name': ''}
|
||||
response = self.client.post(url, data)
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
self.assertEqual(response['Content-Type'], 'text/csv')
|
||||
body = response.content.replace('\r', '')
|
||||
self.assertTrue(body.startswith('"code","course_id","transaction_group_name","created_by","redeemed_by"'))
|
||||
self.assertEqual(len(body.split('\n')), 7)
|
||||
|
||||
for i in range(9):
|
||||
course_registration_code = CourseRegistrationCode(
|
||||
code='TestCode{}'.format(i), course_id=self.course.id.to_deprecated_string(),
|
||||
transaction_group_name='Group Alpha', created_by=self.instructor
|
||||
)
|
||||
course_registration_code.save()
|
||||
|
||||
for i in range(9):
|
||||
order = Order(user=self.instructor, status='purchased')
|
||||
order.save()
|
||||
|
||||
# Spent(used) Registration Codes
|
||||
for i in range(9):
|
||||
i += 13
|
||||
registration_code_redemption = RegistrationCodeRedemption(
|
||||
order_id=i, registration_code_id=i, redeemed_by=self.instructor
|
||||
)
|
||||
registration_code_redemption.save()
|
||||
|
||||
data = {'spent_transaction_group_name': 'Group Alpha'}
|
||||
response = self.client.post(url, data)
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
self.assertEqual(response['Content-Type'], 'text/csv')
|
||||
body = response.content.replace('\r', '')
|
||||
self.assertTrue(body.startswith('"code","course_id","transaction_group_name","created_by","redeemed_by"'))
|
||||
self.assertEqual(len(body.split('\n')), 11)
|
||||
|
||||
def test_active_course_registration_codes_csv(self):
|
||||
"""
|
||||
Test to generate a response of all the active course registration codes
|
||||
"""
|
||||
url = reverse('active_registration_codes',
|
||||
kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
|
||||
data = {'active_transaction_group_name': ''}
|
||||
response = self.client.post(url, data)
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
self.assertEqual(response['Content-Type'], 'text/csv')
|
||||
body = response.content.replace('\r', '')
|
||||
self.assertTrue(body.startswith('"code","course_id","transaction_group_name","created_by","redeemed_by"'))
|
||||
self.assertEqual(len(body.split('\n')), 9)
|
||||
|
||||
for i in range(9):
|
||||
course_registration_code = CourseRegistrationCode(
|
||||
code='TestCode{}'.format(i), course_id=self.course.id.to_deprecated_string(),
|
||||
transaction_group_name='Group Alpha', created_by=self.instructor
|
||||
)
|
||||
course_registration_code.save()
|
||||
|
||||
data = {'active_transaction_group_name': 'Group Alpha'}
|
||||
response = self.client.post(url, data)
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
self.assertEqual(response['Content-Type'], 'text/csv')
|
||||
body = response.content.replace('\r', '')
|
||||
self.assertTrue(body.startswith('"code","course_id","transaction_group_name","created_by","redeemed_by"'))
|
||||
self.assertEqual(len(body.split('\n')), 11)
|
||||
|
||||
def test_get_all_course_registration_codes_csv(self):
|
||||
"""
|
||||
Test to generate a response of all the course registration codes
|
||||
"""
|
||||
url = reverse('get_registration_codes',
|
||||
kwargs={'course_id': self.course.id.to_deprecated_string()})
|
||||
|
||||
data = {'download_transaction_group_name': ''}
|
||||
response = self.client.post(url, data)
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
self.assertEqual(response['Content-Type'], 'text/csv')
|
||||
body = response.content.replace('\r', '')
|
||||
self.assertTrue(body.startswith('"code","course_id","transaction_group_name","created_by","redeemed_by"'))
|
||||
self.assertEqual(len(body.split('\n')), 14)
|
||||
|
||||
for i in range(9):
|
||||
course_registration_code = CourseRegistrationCode(
|
||||
code='TestCode{}'.format(i), course_id=self.course.id.to_deprecated_string(),
|
||||
transaction_group_name='Group Alpha', created_by=self.instructor
|
||||
)
|
||||
course_registration_code.save()
|
||||
|
||||
data = {'download_transaction_group_name': 'Group Alpha'}
|
||||
response = self.client.post(url, data)
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
self.assertEqual(response['Content-Type'], 'text/csv')
|
||||
body = response.content.replace('\r', '')
|
||||
self.assertTrue(body.startswith('"code","course_id","transaction_group_name","created_by","redeemed_by"'))
|
||||
self.assertEqual(len(body.split('\n')), 11)
|
||||
|
||||
@@ -11,7 +11,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from shoppingcart.models import Coupon, PaidCourseRegistration
|
||||
from shoppingcart.models import Coupon, PaidCourseRegistration, CourseRegistrationCode
|
||||
from mock import patch
|
||||
from student.roles import CourseFinanceAdminRole
|
||||
|
||||
@@ -107,6 +107,17 @@ class TestECommerceDashboardViews(ModuleStoreTestCase):
|
||||
response = self.client.post(add_coupon_url, data=data)
|
||||
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(),
|
||||
transaction_group_name='Test Group', created_by=self.instructor
|
||||
)
|
||||
course_registration.save()
|
||||
|
||||
data['code'] = 'Vs23Ws4j'
|
||||
response = self.client.post(add_coupon_url, data)
|
||||
self.assertTrue("The code ({code}) that you have tried to define is already in use as a registration code"
|
||||
.format(code=data['code']) in response.content)
|
||||
|
||||
def test_delete_coupon(self):
|
||||
"""
|
||||
Test Delete Coupon Scenarios. Handle all the HttpResponses return by remove_coupon view
|
||||
@@ -213,3 +224,15 @@ class TestECommerceDashboardViews(ModuleStoreTestCase):
|
||||
data = {'coupon_id': coupon.id, 'code': '11111', 'discount': '12'}
|
||||
response = self.client.post(update_coupon_url, data=data)
|
||||
self.assertTrue('coupon with the coupon id ({coupon_id}) already exist'.format(coupon_id=coupon.id) in response.content)
|
||||
|
||||
course_registration = CourseRegistrationCode(
|
||||
code='Vs23Ws4j', course_id=self.course.id.to_deprecated_string(),
|
||||
transaction_group_name='Test Group', created_by=self.instructor
|
||||
)
|
||||
course_registration.save()
|
||||
|
||||
data = {'coupon_id': coupon.id, 'code': 'Vs23Ws4j',
|
||||
'discount': '6', 'course_id': coupon.course_id.to_deprecated_string()}
|
||||
response = self.client.post(update_coupon_url, data=data)
|
||||
self.assertTrue("The code ({code}) that you have tried to define is already in use as a registration code".
|
||||
format(code=data['code']) in response.content)
|
||||
|
||||
@@ -5,6 +5,7 @@ JSON views which the instructor dashboard requests.
|
||||
|
||||
Many of these GETs may become PUTs in the future.
|
||||
"""
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
import json
|
||||
import logging
|
||||
@@ -14,11 +15,14 @@ from django.conf import settings
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import IntegrityError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.validators import validate_email
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.utils.html import strip_tags
|
||||
import string # pylint: disable=W0402
|
||||
import random
|
||||
from util.json_request import JsonResponse
|
||||
from instructor.views.instructor_task_helpers import extract_email_features, extract_task_features
|
||||
|
||||
@@ -34,6 +38,7 @@ from django_comment_common.models import (
|
||||
)
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from courseware.models import StudentModule
|
||||
from shoppingcart.models import Coupon, CourseRegistrationCode, RegistrationCodeRedemption
|
||||
from student.models import CourseEnrollment, unique_id_for_user, anonymous_id_for_user
|
||||
import instructor_task.api
|
||||
from instructor_task.api_helper import AlreadyRunningError
|
||||
@@ -561,7 +566,7 @@ def get_purchase_transaction(request, course_id, csv=False): # pylint: disable=
|
||||
'order_id',
|
||||
]
|
||||
|
||||
student_data = analytics.basic.purchase_transactions(course_id, query_features)
|
||||
student_data = instructor_analytics.basic.purchase_transactions(course_id, query_features)
|
||||
|
||||
if not csv:
|
||||
response_payload = {
|
||||
@@ -630,6 +635,155 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=W06
|
||||
return instructor_analytics.csvs.create_csv_response("enrolled_profiles.csv", header, datarows)
|
||||
|
||||
|
||||
def save_registration_codes(request, course_id, generated_codes_list, group_name):
|
||||
"""
|
||||
recursive function that generate a new code every time and saves in the Course Registration Table
|
||||
if validation check passes
|
||||
"""
|
||||
code = random_code_generator()
|
||||
|
||||
# check if the generated code is in the Coupon Table
|
||||
matching_coupons = Coupon.objects.filter(code=code, is_active=True)
|
||||
if matching_coupons:
|
||||
return save_registration_codes(request, course_id, generated_codes_list, group_name)
|
||||
|
||||
course_registration = CourseRegistrationCode(
|
||||
code=code, course_id=course_id.to_deprecated_string(),
|
||||
transaction_group_name=group_name, created_by=request.user
|
||||
)
|
||||
try:
|
||||
course_registration.save()
|
||||
generated_codes_list.append(course_registration)
|
||||
except IntegrityError:
|
||||
return save_registration_codes(request, course_id, generated_codes_list, group_name)
|
||||
|
||||
|
||||
def registration_codes_csv(file_name, codes_list, csv_type=None):
|
||||
"""
|
||||
Respond with the csv headers and data rows
|
||||
given a dict of codes list
|
||||
:param file_name:
|
||||
:param codes_list:
|
||||
:param csv_type:
|
||||
"""
|
||||
# csv headers
|
||||
query_features = ['code', 'course_id', 'transaction_group_name', 'created_by', 'redeemed_by']
|
||||
|
||||
registration_codes = instructor_analytics.basic.course_registration_features(query_features, codes_list, csv_type)
|
||||
header, data_rows = instructor_analytics.csvs.format_dictlist(registration_codes, query_features)
|
||||
return analytics.csvs.create_csv_response(file_name, header, data_rows)
|
||||
|
||||
|
||||
def random_code_generator():
|
||||
"""
|
||||
generate a random alphanumeric code of length defined in
|
||||
REGISTRATION_CODE_LENGTH settings
|
||||
"""
|
||||
chars = string.ascii_uppercase + string.digits + string.ascii_lowercase
|
||||
code_length = getattr(settings, 'REGISTRATION_CODE_LENGTH', 8)
|
||||
return string.join((random.choice(chars) for _ in range(code_length)), '')
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@require_POST
|
||||
def get_registration_codes(request, course_id): # pylint: disable=W0613
|
||||
"""
|
||||
Respond with csv which contains a summary of all Registration Codes.
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
|
||||
#filter all the course registration codes
|
||||
registration_codes = CourseRegistrationCode.objects.filter(course_id=course_id).order_by('transaction_group_name')
|
||||
|
||||
group_name = request.POST['download_transaction_group_name']
|
||||
if group_name:
|
||||
registration_codes = registration_codes.filter(transaction_group_name=group_name)
|
||||
|
||||
csv_type = 'download'
|
||||
return registration_codes_csv("Registration_Codes.csv", registration_codes, csv_type)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@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_registration_codes = []
|
||||
|
||||
# covert the course registration code number into integer
|
||||
try:
|
||||
course_code_number = int(request.POST['course_registration_code_number'])
|
||||
except ValueError:
|
||||
course_code_number = int(float(request.POST['course_registration_code_number']))
|
||||
|
||||
group_name = request.POST['transaction_group_name']
|
||||
|
||||
for _ in range(course_code_number): # pylint: disable=W0621
|
||||
save_registration_codes(request, course_id, course_registration_codes, group_name)
|
||||
|
||||
return registration_codes_csv("Registration_Codes.csv", course_registration_codes)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@require_POST
|
||||
def active_registration_codes(request, course_id): # pylint: disable=W0613
|
||||
"""
|
||||
Respond with csv which contains a summary of all Active Registration Codes.
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
|
||||
# find all the registration codes in this course
|
||||
registration_codes_list = CourseRegistrationCode.objects.filter(course_id=course_id).order_by('transaction_group_name')
|
||||
|
||||
group_name = request.POST['active_transaction_group_name']
|
||||
if group_name:
|
||||
registration_codes_list = registration_codes_list.filter(transaction_group_name=group_name)
|
||||
# find the redeemed registration codes if any exist in the db
|
||||
code_redemption_set = RegistrationCodeRedemption.objects.select_related('registration_code').filter(registration_code__course_id=course_id)
|
||||
if code_redemption_set.exists():
|
||||
redeemed_registration_codes = [code.registration_code.code for code in code_redemption_set]
|
||||
# exclude the redeemed registration codes from the registration codes list and you will get
|
||||
# all the registration codes that are active
|
||||
registration_codes_list = registration_codes_list.exclude(code__in=redeemed_registration_codes)
|
||||
|
||||
return registration_codes_csv("Active_Registration_Codes.csv", registration_codes_list)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
@require_POST
|
||||
def spent_registration_codes(request, course_id): # pylint: disable=W0613
|
||||
"""
|
||||
Respond with csv which contains a summary of all Spent(used) Registration Codes.
|
||||
"""
|
||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
|
||||
# find the redeemed registration codes if any exist in the db
|
||||
code_redemption_set = RegistrationCodeRedemption.objects.select_related('registration_code').filter(registration_code__course_id=course_id)
|
||||
spent_codes_list = []
|
||||
if code_redemption_set.exists():
|
||||
redeemed_registration_codes = [code.registration_code.code for code in code_redemption_set]
|
||||
# filter the Registration Codes by course id and the redeemed codes and
|
||||
# you will get a list of all the spent(Redeemed) Registration Codes
|
||||
spent_codes_list = CourseRegistrationCode.objects.filter(course_id=course_id, code__in=redeemed_registration_codes).order_by('transaction_group_name')
|
||||
|
||||
group_name = request.POST['spent_transaction_group_name']
|
||||
if group_name:
|
||||
spent_codes_list = spent_codes_list.filter(transaction_group_name=group_name) # pylint: disable=E1103
|
||||
|
||||
csv_type = 'spent'
|
||||
return registration_codes_csv("Spent_Registration_Codes.csv", spent_codes_list, csv_type)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
|
||||
@@ -58,6 +58,16 @@ urlpatterns = patterns('', # nopep8
|
||||
url(r'calculate_grades_csv$',
|
||||
'instructor.views.api.calculate_grades_csv', name="calculate_grades_csv"),
|
||||
|
||||
# Registration Codes..
|
||||
url(r'get_registration_codes$',
|
||||
'instructor.views.api.get_registration_codes', name="get_registration_codes"),
|
||||
url(r'generate_registration_codes$',
|
||||
'instructor.views.api.generate_registration_codes', name="generate_registration_codes"),
|
||||
url(r'active_registration_codes$',
|
||||
'instructor.views.api.active_registration_codes', name="active_registration_codes"),
|
||||
url(r'spent_registration_codes$',
|
||||
'instructor.views.api.spent_registration_codes', name="spent_registration_codes"),
|
||||
|
||||
# spoc gradebook
|
||||
url(r'^gradebook$',
|
||||
'instructor.views.api.spoc_gradebook', name='spoc_gradebook'),
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.views.decorators.http import require_POST
|
||||
from django.utils.translation import ugettext as _
|
||||
from util.json_request import JsonResponse
|
||||
from django.http import HttpResponse, HttpResponseNotFound
|
||||
from shoppingcart.models import Coupon
|
||||
from shoppingcart.models import Coupon, CourseRegistrationCode
|
||||
|
||||
import logging
|
||||
|
||||
@@ -59,6 +59,13 @@ def add_coupon(request, course_id): # pylint: disable=W0613
|
||||
if coupon:
|
||||
return HttpResponseNotFound(_("coupon with the coupon code ({code}) already exist").format(code=code))
|
||||
|
||||
# check if the coupon code is in the CourseRegistrationCode Table
|
||||
course_registration_code = CourseRegistrationCode.objects.filter(code=code)
|
||||
if course_registration_code:
|
||||
return HttpResponseNotFound(_(
|
||||
"The code ({code}) that you have tried to define is already in use as a registration code").format(code=code)
|
||||
)
|
||||
|
||||
description = request.POST.get('description')
|
||||
course_id = request.POST.get('course_id')
|
||||
try:
|
||||
@@ -96,6 +103,13 @@ def update_coupon(request, course_id): # pylint: disable=W0613
|
||||
if filtered_coupons:
|
||||
return HttpResponseNotFound(_("coupon with the coupon id ({coupon_id}) already exists").format(coupon_id=coupon_id))
|
||||
|
||||
# check if the coupon code is in the CourseRegistrationCode Table
|
||||
course_registration_code = CourseRegistrationCode.objects.filter(code=code)
|
||||
if course_registration_code:
|
||||
return HttpResponseNotFound(_(
|
||||
"The code ({code}) that you have tried to define is already in use as a registration code").format(code=code)
|
||||
)
|
||||
|
||||
description = request.POST.get('description')
|
||||
course_id = request.POST.get('course_id')
|
||||
try:
|
||||
|
||||
@@ -144,6 +144,10 @@ def _section_e_commerce(course_key, access):
|
||||
'ajax_add_coupon': reverse('add_coupon', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'get_purchase_transaction_url': reverse('get_purchase_transaction', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'instructor_url': reverse('instructor_dashboard', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'get_registration_code_csv_url': reverse('get_registration_codes', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'generate_registration_code_csv_url': reverse('generate_registration_codes', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'active_registration_code_csv_url': reverse('active_registration_codes', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'spent_registration_code_csv_url': reverse('spent_registration_codes', kwargs={'course_id': course_key.to_deprecated_string()}),
|
||||
'coupons': coupons,
|
||||
'total_amount': total_amount,
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ Serve miscellaneous course and student data
|
||||
from shoppingcart.models import PaidCourseRegistration, CouponRedemption
|
||||
from django.contrib.auth.models import User
|
||||
import xmodule.graders as xmgraders
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
|
||||
STUDENT_FEATURES = ('id', 'username', 'first_name', 'last_name', 'is_staff', 'email')
|
||||
@@ -15,6 +16,7 @@ ORDER_ITEM_FEATURES = ('list_price', 'unit_cost', 'order_id')
|
||||
ORDER_FEATURES = ('purchase_time',)
|
||||
|
||||
AVAILABLE_FEATURES = STUDENT_FEATURES + PROFILE_FEATURES
|
||||
COURSE_REGISTRATION_FEATURES = ('code', 'course_id', 'transaction_group_name', 'created_by')
|
||||
|
||||
|
||||
def purchase_transactions(course_id, features):
|
||||
@@ -98,6 +100,42 @@ def enrolled_students_features(course_id, features):
|
||||
return [extract_student(student, features) for student in students]
|
||||
|
||||
|
||||
def course_registration_features(features, registration_codes, csv_type):
|
||||
"""
|
||||
Return list of Course Registration Codes as dictionaries.
|
||||
|
||||
course_registration_features
|
||||
would return [
|
||||
{'code': 'code1', 'course_id': 'edX/Open_DemoX/edx_demo_course, ..... }
|
||||
{'code': 'code2', 'course_id': 'edX/Open_DemoX/edx_demo_course, ..... }
|
||||
]
|
||||
"""
|
||||
|
||||
def extract_course_registration(registration_code, features, csv_type):
|
||||
""" convert registration_code to dictionary
|
||||
:param registration_code:
|
||||
:param features:
|
||||
:param csv_type:
|
||||
"""
|
||||
registration_features = [x for x in COURSE_REGISTRATION_FEATURES if x in features]
|
||||
|
||||
course_registration_dict = dict((feature, getattr(registration_code, feature)) for feature in registration_features)
|
||||
course_registration_dict['redeemed_by'] = None
|
||||
|
||||
# we have to capture the redeemed_by value in the case of the downloading and spent registration
|
||||
# codes csv. In the case of active and generated registration codes the redeemed_by value will be None.
|
||||
# They have not been redeemed yet
|
||||
if csv_type is not None:
|
||||
try:
|
||||
course_registration_dict['redeemed_by'] = getattr(registration_code.registrationcoderedemption_set.get(registration_code=registration_code), 'redeemed_by')
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
course_registration_dict['course_id'] = course_registration_dict['course_id'].to_deprecated_string()
|
||||
return course_registration_dict
|
||||
return [extract_course_registration(code, features, csv_type) for code in registration_codes]
|
||||
|
||||
|
||||
def dump_grading_context(course):
|
||||
"""
|
||||
Render information about course grading context
|
||||
|
||||
@@ -6,8 +6,9 @@ from django.test import TestCase
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from shoppingcart.models import CourseRegistrationCode, RegistrationCodeRedemption, Order
|
||||
|
||||
from instructor_analytics.basic import enrolled_students_features, AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES
|
||||
from instructor_analytics.basic import enrolled_students_features, course_registration_features, AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES
|
||||
|
||||
|
||||
class TestAnalyticsBasic(TestCase):
|
||||
@@ -42,3 +43,34 @@ class TestAnalyticsBasic(TestCase):
|
||||
def test_available_features(self):
|
||||
self.assertEqual(len(AVAILABLE_FEATURES), len(STUDENT_FEATURES + PROFILE_FEATURES))
|
||||
self.assertEqual(set(AVAILABLE_FEATURES), set(STUDENT_FEATURES + PROFILE_FEATURES))
|
||||
|
||||
def test_course_registration_features(self):
|
||||
query_features = ['code', 'course_id', 'transaction_group_name', 'created_by', 'redeemed_by']
|
||||
for i in range(5):
|
||||
course_code = CourseRegistrationCode(
|
||||
code="test_code{}".format(i), course_id=self.course_key.to_deprecated_string(),
|
||||
transaction_group_name='TestName', created_by=self.users[0]
|
||||
)
|
||||
course_code.save()
|
||||
|
||||
order = Order(user=self.users[0], status='purchased')
|
||||
order.save()
|
||||
|
||||
registration_code_redemption = RegistrationCodeRedemption(
|
||||
order=order, registration_code_id=1, redeemed_by=self.users[0]
|
||||
)
|
||||
registration_code_redemption.save()
|
||||
registration_codes = CourseRegistrationCode.objects.all()
|
||||
course_registration_list = course_registration_features(query_features, registration_codes, csv_type='download')
|
||||
self.assertEqual(len(course_registration_list), len(registration_codes))
|
||||
for course_registration in course_registration_list:
|
||||
self.assertEqual(set(course_registration.keys()), set(query_features))
|
||||
self.assertIn(course_registration['code'], [registration_code.code for registration_code in registration_codes])
|
||||
self.assertIn(
|
||||
course_registration['course_id'],
|
||||
[registration_code.course_id.to_deprecated_string() for registration_code in registration_codes]
|
||||
)
|
||||
self.assertIn(
|
||||
course_registration['transaction_group_name'],
|
||||
[registration_code.transaction_group_name for registration_code in registration_codes]
|
||||
)
|
||||
|
||||
@@ -429,3 +429,6 @@ ADVANCED_SECURITY_CONFIG = ENV_TOKENS.get('ADVANCED_SECURITY_CONFIG', {})
|
||||
##### GOOGLE ANALYTICS IDS #####
|
||||
GOOGLE_ANALYTICS_ACCOUNT = AUTH_TOKENS.get('GOOGLE_ANALYTICS_ACCOUNT')
|
||||
GOOGLE_ANALYTICS_LINKEDIN = AUTH_TOKENS.get('GOOGLE_ANALYTICS_LINKEDIN')
|
||||
|
||||
#### Course Registration Code length ####
|
||||
REGISTRATION_CODE_LENGTH = ENV_TOKENS.get('REGISTRATION_CODE_LENGTH', 8)
|
||||
|
||||
@@ -291,6 +291,9 @@ FEATURES['ENABLE_SHOPPING_CART'] = True
|
||||
### This enables the Metrics tab for the Instructor dashboard ###########
|
||||
FEATURES['CLASS_DASHBOARD'] = True
|
||||
|
||||
### This settings is for the course registration code length ############
|
||||
REGISTRATION_CODE_LENGTH = 8
|
||||
|
||||
#####################################################################
|
||||
# Lastly, see if the developer has any local overrides.
|
||||
try:
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
###
|
||||
E-Commerce Download Section
|
||||
E-Commerce Section
|
||||
###
|
||||
|
||||
# Ecommerce Purchase Download Section
|
||||
class ECommerce
|
||||
# E-Commerce Section
|
||||
constructor: (@$section) ->
|
||||
# attach self to html so that instructor_dashboard.coffee can find
|
||||
# this object to call event handlers like 'onClickTitle'
|
||||
@$section.data 'wrapper', @
|
||||
|
||||
# gather elements
|
||||
@$list_purchase_csv_btn = @$section.find("input[name='list-purchase-transaction-csv']'")
|
||||
@$transaction_group_name = @$section.find("input[name='transaction_group_name']'")
|
||||
@$transaction_group_name = @$section.find("input[name='transaction_group_name']'")
|
||||
@$download_transaction_group_name = @$section.find("input[name='transaction_group_name']'")
|
||||
@$active_transaction_group_name = @$section.find("input[name='transaction_group_name']'")
|
||||
@$spent_transaction_group_name = @$section.find('input[name="course_registration_code_number"]')
|
||||
|
||||
@$generate_registration_code_form = @$section.find("form#course_codes_number")
|
||||
@$download_registration_codes_form = @$section.find("form#download_registration_codes")
|
||||
@$active_registration_codes_form = @$section.find("form#active_registration_codes")
|
||||
@$spent_registration_codes_form = @$section.find("form#spent_registration_codes")
|
||||
|
||||
@$coupoon_error = @$section.find('#coupon-error')
|
||||
@$course_code_error = @$section.find('#code-error')
|
||||
|
||||
# attach click handlers
|
||||
# this handler binds to both the download
|
||||
# and the csv button
|
||||
@@ -20,17 +32,72 @@ class ECommerce
|
||||
url += '/csv'
|
||||
location.href = url
|
||||
|
||||
@$download_registration_codes_form.submit (e) =>
|
||||
@$course_code_error.attr('style', 'display: none')
|
||||
@$coupoon_error.attr('style', 'display: none')
|
||||
return true
|
||||
|
||||
@$active_registration_codes_form.submit (e) =>
|
||||
@$course_code_error.attr('style', 'display: none')
|
||||
@$coupoon_error.attr('style', 'display: none')
|
||||
return true
|
||||
|
||||
@$spent_registration_codes_form.submit (e) =>
|
||||
@$course_code_error.attr('style', 'display: none')
|
||||
@$coupoon_error.attr('style', 'display: none')
|
||||
return true
|
||||
|
||||
@$generate_registration_code_form.submit (e) =>
|
||||
@$course_code_error.attr('style', 'display: none')
|
||||
@$coupoon_error.attr('style', 'display: none')
|
||||
group_name = @$transaction_group_name.val()
|
||||
if group_name == ''
|
||||
@$course_code_error.html('Please Enter the Transaction Group Name').show()
|
||||
return false
|
||||
|
||||
if ($.isNumeric(group_name))
|
||||
@$course_code_error.html('Please Enter the non-numeric value for Transaction Group Name').show()
|
||||
return false;
|
||||
|
||||
registration_codes = @$course_registration_number.val();
|
||||
if (isInt(registration_codes) && $.isNumeric(registration_codes))
|
||||
if (parseInt(registration_codes) > 1000 )
|
||||
@$course_code_error.html('You can only generate 1000 Registration Codes at a time').show()
|
||||
return false;
|
||||
if (parseInt(registration_codes) == 0 )
|
||||
@$course_code_error.html('Please Enter the Value greater than 0 for Registration Codes').show()
|
||||
return false;
|
||||
return true;
|
||||
else
|
||||
@$course_code_error.html('Please Enter the Integer Value for Registration Codes').show()
|
||||
return false;
|
||||
|
||||
# handler for when the section title is clicked.
|
||||
onClickTitle: ->
|
||||
@clear_display()
|
||||
|
||||
clear_display: ->
|
||||
# handler for when the section title is clicked.
|
||||
onClickTitle: -> @clear_display()
|
||||
|
||||
# handler for when the section is closed
|
||||
onExit: -> @clear_display()
|
||||
|
||||
clear_display: ->
|
||||
@$course_code_error.attr('style', 'display: none')
|
||||
@$coupoon_error.attr('style', 'display: none')
|
||||
@$course_registration_number.val('')
|
||||
@$transaction_group_name.val('')
|
||||
@$download_transaction_group_name.val('')
|
||||
@$active_transaction_group_name.val('')
|
||||
@$spent_transaction_group_name.val('')
|
||||
|
||||
|
||||
isInt = (n) -> return n % 1 == 0;
|
||||
# Clear any generated tables, warning messages, etc.
|
||||
|
||||
# export for use
|
||||
# create parent namespaces if they do not already exist.
|
||||
_.defaults window, InstructorDashboard: {}
|
||||
_.defaults window.InstructorDashboard, sections: {}
|
||||
_.defaults window.InstructorDashboard.sections,
|
||||
ECommerce: ECommerce
|
||||
ECommerce: ECommerce
|
||||
@@ -829,6 +829,19 @@ input[name="subject"] {
|
||||
.content{
|
||||
padding: 0 !important;
|
||||
}
|
||||
input[name="course_registration_code_number"] {
|
||||
margin-right: 10px;
|
||||
height: 34px;
|
||||
width: 258px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
input[name="transaction_group_name"], input[name="download_transaction_group_name"],
|
||||
input[name="active_transaction_group_name"], input[name="spent_transaction_group_name"] {
|
||||
margin-right: 8px;
|
||||
height: 36px;
|
||||
width: 300px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.coupons-table {
|
||||
width: 100%;
|
||||
tr:nth-child(even){
|
||||
|
||||
@@ -4,8 +4,46 @@
|
||||
<%include file="edit_coupon_modal.html" args="section_data=section_data" />
|
||||
|
||||
<div class="ecommerce-wrapper">
|
||||
<h3 class="coupon-errors" id="code-error"></h3>
|
||||
<h2>Registration Codes</h2>
|
||||
<p>Enter the transaction group name and number of registration codes that you want to generate. Click to generate a CSV :</p>
|
||||
<p>
|
||||
<form action="${ section_data['generate_registration_code_csv_url'] }" id="course_codes_number" method="post">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
|
||||
<input type="text" name="transaction_group_name" placeholder="Transaction Group Name"/>
|
||||
<input type="text" name="course_registration_code_number" placeholder="Number of Registration Codes" maxlength="4"/>
|
||||
<input type="submit" name="generate-registration-codes-csv" value="${_("Generate Registration Codes")}" data-csv="true">
|
||||
</form>
|
||||
</p>
|
||||
|
||||
%if section_data['access']['finance_admin'] is True:
|
||||
<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">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
|
||||
<input type="text" name="download_transaction_group_name" placeholder="Transaction Group Name (Optional)"/>
|
||||
<input type="submit" name="list-registration-codes-csv" value="${_("Download Registration Codes")}" data-csv="true">
|
||||
</form>
|
||||
</p>
|
||||
|
||||
<p>Click to generate a CSV file of all Active Course Registrations Codes:</p>
|
||||
<p>
|
||||
<form action="${ section_data['active_registration_code_csv_url'] }" id="active_registration_codes" method="post">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
|
||||
<input type="text" name="active_transaction_group_name" placeholder="Transaction Group Name (Optional)"/>
|
||||
<input type="submit" name="active-registration-codes-csv" value="${_("Active Registration Codes")}" data-csv="true">
|
||||
</form>
|
||||
</p>
|
||||
|
||||
<p>Click to generate a CSV file of all Spent Course Registrations Codes:</p>
|
||||
<p>
|
||||
<form action="${ section_data['spent_registration_code_csv_url'] }" id="spent_registration_codes" method="post">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
|
||||
<input type="text" name="spent_transaction_group_name" placeholder="Transaction Group Name (Optional)"/>
|
||||
<input type="submit" name="spent-registration-codes-csv" value="${_("Spent Registration Codes")}" data-csv="true">
|
||||
</form>
|
||||
</p>
|
||||
<hr>
|
||||
%if section_data['access']['finance_admin'] is True:
|
||||
|
||||
<h2>${_("Transactions")}</h2>
|
||||
%if section_data['total_amount'] is not None:
|
||||
|
||||
Reference in New Issue
Block a user