Commit 33d5b49e by Muhammad Shoaib Committed by Chris Dodge

added course price set/view/edit functionality

added CoursmodeArchive model to save the old prices history

analytics -> instructor_analytics

analytics -> instructor_analytics
parent dc46170f
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'CourseModesArchive'
db.create_table('course_modes_coursemodesarchive', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255, db_index=True)),
('mode_slug', self.gf('django.db.models.fields.CharField')(max_length=100)),
('mode_display_name', self.gf('django.db.models.fields.CharField')(max_length=255)),
('min_price', self.gf('django.db.models.fields.IntegerField')(default=0)),
('suggested_prices', self.gf('django.db.models.fields.CommaSeparatedIntegerField')(default='', max_length=255, blank=True)),
('currency', self.gf('django.db.models.fields.CharField')(default='usd', max_length=8)),
('expiration_date', self.gf('django.db.models.fields.DateField')(default=None, null=True, blank=True)),
('expiration_datetime', self.gf('django.db.models.fields.DateTimeField')(default=None, null=True, blank=True)),
))
db.send_create_signal('course_modes', ['CourseModesArchive'])
# Changing field 'CourseMode.course_id'
db.alter_column('course_modes_coursemode', 'course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255))
def backwards(self, orm):
# Deleting model 'CourseModesArchive'
db.delete_table('course_modes_coursemodesarchive')
# Changing field 'CourseMode.course_id'
db.alter_column('course_modes_coursemode', 'course_id', self.gf('django.db.models.fields.CharField')(max_length=255))
models = {
'course_modes.coursemode': {
'Meta': {'unique_together': "(('course_id', 'mode_slug', 'currency'),)", 'object_name': 'CourseMode'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}),
'expiration_date': ('django.db.models.fields.DateField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'expiration_datetime': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'min_price': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'mode_display_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'mode_slug': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'suggested_prices': ('django.db.models.fields.CommaSeparatedIntegerField', [], {'default': "''", 'max_length': '255', 'blank': 'True'})
},
'course_modes.coursemodesarchive': {
'Meta': {'object_name': 'CourseModesArchive'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}),
'expiration_date': ('django.db.models.fields.DateField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'expiration_datetime': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'min_price': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'mode_display_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'mode_slug': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'suggested_prices': ('django.db.models.fields.CommaSeparatedIntegerField', [], {'default': "''", 'max_length': '255', 'blank': 'True'})
}
}
complete_apps = ['course_modes']
\ No newline at end of file
......@@ -128,3 +128,33 @@ class CourseMode(models.Model):
return u"{} : {}, min={}, prices={}".format(
self.course_id.to_deprecated_string(), self.mode_slug, self.min_price, self.suggested_prices
)
class CourseModesArchive(models.Model):
"""
Store the CourseModesArchives in this model
"""
# the course that this mode is attached to
course_id = CourseKeyField(max_length=255, db_index=True)
# the reference to this mode that can be used by Enrollments to generate
# similar behavior for the same slug across courses
mode_slug = models.CharField(max_length=100)
# The 'pretty' name that can be translated and displayed
mode_display_name = models.CharField(max_length=255)
# minimum price in USD that we would like to charge for this mode of the course
min_price = models.IntegerField(default=0)
# the suggested prices for this mode
suggested_prices = models.CommaSeparatedIntegerField(max_length=255, blank=True, default='')
# the currency these prices are in, using lower case ISO currency codes
currency = models.CharField(default="usd", max_length=8)
# turn this mode off after the given expiration date
expiration_date = models.DateField(default=None, null=True, blank=True)
expiration_datetime = models.DateTimeField(default=None, null=True, blank=True)
......@@ -16,12 +16,10 @@ from mock import patch
from student.roles import CourseFinanceAdminRole
# pylint: disable=E1101
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestECommerceDashboardViews(ModuleStoreTestCase):
"""
Check for email view on the new instructor dashboard
for Mongo-backed courses
Check for E-commerce view on the new instructor dashboard
"""
def setUp(self):
self.course = CourseFactory.create()
......@@ -71,6 +69,75 @@ class TestECommerceDashboardViews(ModuleStoreTestCase):
self.assertFalse('Download All e-Commerce Purchase' in response.content)
self.assertFalse('<span>Total Amount: <span>$' + str(total_amount) + '</span></span>' in response.content)
def test_user_view_course_price(self):
"""
test to check if the user views the set price button and price in
the instructor dashboard
"""
response = self.client.get(self.url)
self.assertTrue(self.e_commerce_link in response.content)
# Total amount html should render in e-commerce page, total amount will be 0
course_honor_mode = CourseMode.mode_for_course(self.course.id, 'honor')
price = course_honor_mode.min_price
self.assertTrue('Course Price: <span>$' + str(price) + '</span>' in response.content)
self.assertFalse('+ Set Price</a></span>' in response.content)
# removing the course finance_admin role of login user
CourseFinanceAdminRole(self.course.id).remove_users(self.instructor)
# total amount should not be visible in e-commerce page if the user is not finance admin
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
response = self.client.get(url)
self.assertFalse('+ Set Price</a></span>' in response.content)
def test_update_course_price_check(self):
price = 200
# course B
course2 = CourseFactory.create(org='EDX', display_name='test_course', number='100')
mode = CourseMode(
course_id=course2.id.to_deprecated_string(), mode_slug='honor',
mode_display_name='honor', min_price=30, currency='usd'
)
mode.save()
# course A update
CourseMode.objects.filter(course_id=self.course.id).update(min_price=price)
set_course_price_url = reverse('set_course_mode_price', kwargs={'course_id': self.course.id.to_deprecated_string()})
data = {'course_price': price, 'currency': 'usd'}
response = self.client.post(set_course_price_url, data)
self.assertTrue('CourseMode price updated successfully' in response.content)
# Course A updated total amount should be visible in e-commerce page if the user is finance admin
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
response = self.client.get(url)
self.assertTrue('Course Price: <span>$' + str(price) + '</span>' in response.content)
def test_user_admin_set_course_price(self):
"""
test to set the course price related functionality.
test al the scenarios for setting a new course price
"""
set_course_price_url = reverse('set_course_mode_price', kwargs={'course_id': self.course.id.to_deprecated_string()})
data = {'course_price': '12%', 'currency': 'usd'}
# Value Error course price should be a numeric value
response = self.client.post(set_course_price_url, data)
self.assertTrue("Please Enter the numeric value for the course price" in response.content)
# validation check passes and course price is successfully added
data['course_price'] = 100
response = self.client.post(set_course_price_url, data)
self.assertTrue("CourseMode price updated successfully" in response.content)
course_honor_mode = CourseMode.objects.get(mode_slug='honor')
course_honor_mode.delete()
# Course Mode not exist with mode slug honor
response = self.client.post(set_course_price_url, data)
self.assertTrue("CourseMode with the mode slug({mode_slug}) DoesNotExist".format(mode_slug='honor') in response.content)
def test_add_coupon(self):
"""
Test Add Coupon Scenarios. Handle all the HttpResponses return by add_coupon view
......@@ -221,9 +288,9 @@ class TestECommerceDashboardViews(ModuleStoreTestCase):
percentage_discount=20, created_by=self.instructor
)
coupon1.save()
data = {'coupon_id': coupon.id, 'code': '11111', 'discount': '12'}
data = {'coupon_id': coupon.id, 'code': '11111', 'discount': '12'} # pylint: disable=E1101
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)
self.assertTrue('coupon with the coupon id ({coupon_id}) already exist'.format(coupon_id=coupon.id) in response.content) # pylint: disable=E1101
course_registration = CourseRegistrationCode(
code='Vs23Ws4j', course_id=self.course.id.to_deprecated_string(),
......@@ -231,8 +298,8 @@ class TestECommerceDashboardViews(ModuleStoreTestCase):
)
course_registration.save()
data = {'coupon_id': coupon.id, 'code': 'Vs23Ws4j',
'discount': '6', 'course_id': coupon.course_id.to_deprecated_string()}
data = {'coupon_id': coupon.id, 'code': 'Vs23Ws4j', # pylint: disable=E1101
'discount': '6', 'course_id': coupon.course_id.to_deprecated_string()} # pylint: disable=E1101
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)
......@@ -576,8 +576,8 @@ def get_purchase_transaction(request, course_id, csv=False): # pylint: disable=
}
return JsonResponse(response_payload)
else:
header, datarows = analytics.csvs.format_dictlist(student_data, query_features)
return analytics.csvs.create_csv_response("e-commerce_purchase_transactions.csv", header, datarows)
header, datarows = instructor_analytics.csvs.format_dictlist(student_data, query_features)
return instructor_analytics.csvs.create_csv_response("e-commerce_purchase_transactions.csv", header, datarows)
@ensure_csrf_cookie
......@@ -671,7 +671,7 @@ def registration_codes_csv(file_name, codes_list, csv_type=None):
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)
return instructor_analytics.csvs.create_csv_response(file_name, header, data_rows)
def random_code_generator():
......
"""
Instructor Dashboard Views
"""
from django.views.decorators.http import require_POST
from django.contrib.auth.decorators import login_required
import logging
import datetime
import pytz
from django.utils.translation import ugettext as _
from django_future.csrf import ensure_csrf_cookie
from django.views.decorators.cache import cache_control
......@@ -11,7 +14,7 @@ from edxmako.shortcuts import render_to_response
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.utils.html import escape
from django.http import Http404
from django.http import Http404, HttpResponse, HttpResponseNotFound
from django.conf import settings
from lms.lib.xblock.runtime import quote_slashes
......@@ -27,7 +30,7 @@ from django_comment_client.utils import has_forum_access
from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR
from student.models import CourseEnrollment
from shoppingcart.models import Coupon, PaidCourseRegistration
from course_modes.models import CourseMode
from course_modes.models import CourseMode, CourseModesArchive
from student.roles import CourseFinanceAdminRole
from bulk_email.models import CourseAuthorization
......@@ -130,6 +133,10 @@ def _section_e_commerce(course_key, access):
""" Provide data for the corresponding dashboard section """
coupons = Coupon.objects.filter(course_id=course_key).order_by('-is_active')
total_amount = None
course_price = None
course_honor_mode = CourseMode.mode_for_course(course_key, 'honor')
if course_honor_mode and course_honor_mode.min_price > 0:
course_price = course_honor_mode.min_price
if access['finance_admin']:
total_amount = PaidCourseRegistration.get_total_amount_of_purchased_item(course_key)
......@@ -148,12 +155,46 @@ def _section_e_commerce(course_key, access):
'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()}),
'set_course_mode_url': reverse('set_course_mode_price', kwargs={'course_id': course_key.to_deprecated_string()}),
'coupons': coupons,
'total_amount': total_amount,
'course_price': course_price
}
return section_data
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_POST
@login_required
def set_course_mode_price(request, course_id):
"""
set the new course price and add new entry in the CourseModesArchive Table
"""
try:
course_price = int(request.POST['course_price'])
except ValueError:
return HttpResponseNotFound(_("Please Enter the numeric value for the course price"))
currency = request.POST['currency']
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course_honor_mode = CourseMode.objects.filter(mode_slug='honor', course_id=course_key)
if not course_honor_mode:
return HttpResponseNotFound(
_("CourseMode with the mode slug({mode_slug}) DoesNotExist").format(mode_slug='honor')
)
CourseModesArchive.objects.create(
course_id=course_id, mode_slug='honor', mode_display_name='Honor Code Certificate',
min_price=getattr(course_honor_mode[0], 'min_price'), currency=getattr(course_honor_mode[0], 'currency'),
expiration_datetime=datetime.datetime.now(pytz.utc), expiration_date=datetime.date.today()
)
course_honor_mode.update(
min_price=course_price,
currency=currency
)
return HttpResponse(_("CourseMode price updated successfully"))
def _section_course_info(course_key, access):
""" Provide data for the corresponding dashboard section """
course = get_course_by_id(course_key, depth=None)
......
......@@ -11,10 +11,10 @@ class ECommerce
# 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"]')
@$course_registration_number = @$section.find("input[name='course_registration_code_number']'")
@$download_transaction_group_name = @$section.find("input[name='download_transaction_group_name']'")
@$active_transaction_group_name = @$section.find("input[name='active_transaction_group_name']'")
@$spent_transaction_group_name = @$section.find('input[name="spent_transaction_group_name"]')
@$generate_registration_code_form = @$section.find("form#course_codes_number")
@$download_registration_codes_form = @$section.find("form#download_registration_codes")
......
......@@ -945,7 +945,7 @@ input[name="subject"] {
}
// coupon edit and add modals
#add-coupon-modal, #edit-coupon-modal{
#add-coupon-modal, #edit-coupon-modal, #set-course-mode-price-modal{
.inner-wrapper {
background: #fff;
}
......@@ -961,6 +961,10 @@ input[name="subject"] {
@include button(simple, $blue);
@extend .button-reset;
}
input[type="submit"]#set_course_button{
@include button(simple, $blue);
@extend .button-reset;
}
.modal-form-error {
box-shadow: inset 0 -1px 2px 0 #f3d9db;
-webkit-box-sizing: border-box;
......@@ -997,9 +1001,21 @@ input[name="subject"] {
li:last-child{
margin-bottom: 0px !important;
}
}
#coupon-content {
li#set-course-mode-modal-field-price{
width: 100%;
label.required:after {
content: "*";
margin-left: 5px;
}
}
li#set-course-mode-modal-field-currency{
margin-left: 0px !important;
select {
width: 100%;
}
}
#coupon-content, #course-content {
padding: 20px;
header {
margin: 0;
......
......@@ -2,6 +2,7 @@
<%page args="section_data"/>
<%include file="add_coupon_modal.html" args="section_data=section_data" />
<%include file="edit_coupon_modal.html" args="section_data=section_data" />
<%include file="set_course_mode_price_modal.html" args="section_data=section_data" />
<div class="ecommerce-wrapper">
<h3 class="coupon-errors" id="code-error"></h3>
......@@ -43,17 +44,23 @@
</form>
</p>
<hr>
<h2>${_("Course Price")}</h2>
<span class="tip">${_("Course Price: ")}<span>$${section_data['course_price']}</span>
%if section_data['access']['finance_admin'] is True:
<a id="course_price_link" href="#set-course-mode-price-modal" rel="leanModal" class="add blue-button">+ Set Price</a>
%endif
</span>
<hr>
%if section_data['access']['finance_admin'] is True:
<h2>${_("Transactions")}</h2>
%if section_data['total_amount'] is not None:
<span>${_("Total Amount: ")}<span>$${section_data['total_amount']}</span></span>
%endif
<h2>${_("Transactions")}</h2>
%if section_data['total_amount'] is not None:
<span>${_("Total Amount: ")}<span>$${section_data['total_amount']}</span></span>
%endif
<p>${_("Click to generate a CSV file for all purchase transactions in this course")}</p>
<p>${_("Click to generate a CSV file for all purchase transactions in this course")}</p>
<p><input type="button" name="list-purchase-transaction-csv" value="${_("Download All e-Commerce Purchases")}" data-endpoint="${ section_data['get_purchase_transaction_url'] }" data-csv="true"></p>
%endif
<p><input type="button" name="list-purchase-transaction-csv" value="${_("Download All e-Commerce Purchases")}" data-endpoint="${ section_data['get_purchase_transaction_url'] }" data-csv="true"></p>
%endif
<h2>${_("Coupons List")}</h2>
......@@ -202,9 +209,38 @@
return false;
}
});
$('#course_price_link').click(function () {
reset_input_fields();
});
$('#add_coupon_link').click(function () {
reset_input_fields();
});
$('#set_price_form').submit(function () {
$("#set_course_button").attr('disabled', true);
// Get the Code and Discount value and trim it
var course_price = $.trim($('#mode_price').val());
var currency = $.trim($('#course_mode_currency').val());
// Check if empty of not
if (course_price === '') {
$('#set_price_form #course_form_error').attr('style', 'display: block !important');
$('#set_price_form #course_form_error').text("${_('Please Enter the Course Price')}");
$("#set_course_button").removeAttr('disabled');
return false;
}
if (!$.isNumeric(course_price)) {
$("#set_course_button").removeAttr('disabled');
$('#set_price_form #course_form_error').attr('style', 'display: block !important');
$('#set_price_form #course_form_error').text("${_('Please Enter the Numeric value for Discount')}");
return false;
}
if (currency == '') {
$('#set_price_form #course_form_error').attr('style', 'display: block !important');
$('#set_price_form #course_form_error').text("${_('Please Select the Currency')}");
$("#set_course_button").removeAttr('disabled');
return false;
}
});
$('#add_coupon_form').submit(function () {
$("#add_coupon_button").attr('disabled', true);
// Get the Code and Discount value and trim it
......@@ -238,6 +274,16 @@
}
});
$('#set_price_form').on('ajax:complete', function (event, xhr) {
if (xhr.status == 200) {
location.reload(true);
} else {
$("#set_course_button").removeAttr('disabled');
$('#set_price_form #course_form_error').attr('style', 'display: block !important');
$('#set_price_form #course_form_error').text(xhr.responseText);
}
});
$('#add_coupon_form').on('ajax:complete', function (event, xhr) {
if (xhr.status == 200) {
location.reload(true);
......@@ -261,6 +307,7 @@
$('.close-modal').click(function (e) {
$("#update_coupon_button").removeAttr('disabled');
$("#add_coupon_button").removeAttr('disabled');
$("#set_course_button").removeAttr('disabled');
reset_input_fields();
e.preventDefault();
});
......@@ -270,7 +317,9 @@
$(".remove_coupon").focus();
$("#edit-coupon-modal").attr("aria-hidden", "true");
$(".edit-right").focus();
$("#set-course-mode-price-modal").attr("aria-hidden", "true");
$("#add_coupon_button").removeAttr('disabled');
$("#set_course_button").removeAttr('disabled');
$("#update_coupon_button").removeAttr('disabled');
reset_input_fields();
};
......@@ -288,16 +337,17 @@
$("#add-coupon-modal .close-modal").click(onModalClose);
$("#edit-coupon-modal .close-modal").click(onModalClose);
$("#add-coupon-modal .close-modal").click(reset_input_fields);
$("#set-course-mode-price-modal .close-modal").click(reset_input_fields);
// Hitting the ESC key will exit the modal
$("#add-coupon-modal, #edit-coupon-modal").on("keydown", function (e) {
$("#add-coupon-modal, #edit-coupon-modal, #set-course-mode-price-modal").on("keydown", function (e) {
var keyCode = e.keyCode || e.which;
// 27 is the ESC key
if (keyCode === 27) {
e.preventDefault();
$("#add-coupon-modal .close-modal").click();
$("#set-course-mode-price-modal .close-modal").click();
$("#edit-coupon-modal .close-modal").click();
}
});
......@@ -306,7 +356,9 @@
$('#coupon-error').val('');
$('#coupon-error').attr('style', 'display: none');
$('#add_coupon_form #coupon_form_error').attr('style', 'display: none');
$('#set_price_form #course_form_error').attr('style', 'display: none');
$('#add_coupon_form #coupon_form_error').text();
$('input#mode_price').val('');
$('input#coupon_code').val('');
$('input#coupon_discount').val('');
$('textarea#coupon_description').val('');
......
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%page args="section_data"/>
<section id="set-course-mode-price-modal" class="modal" role="dialog" tabindex="-1" aria-label="${_('Set Course Mode Price')}">
<div class="inner-wrapper">
<button class="close-modal">
<i class="icon-remove"></i>
<span class="sr">
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
${_('Close')}
</span>
</button>
<div id="course-content">
<header>
<h2>${_("Set Course Mode Price")}</h2>
</header>
<div class="instructions">
<p>
${_("Please enter Course Mode detail below")}</p>
</div>
<form id="set_price_form" action="${section_data['set_course_mode_url']}" method="post" data-remote="true">
<div id="course_form_error" class="modal-form-error"></div>
<fieldset class="group group-form group-form-requiredinformation">
<legend class="is-hidden">${_("Required Information")}</legend>
<ol class="list-input">
<li class="field required text" id="set-course-mode-modal-field-price">
<label for="mode_price" class="required">${_("Course Price")}</label>
<input class="field" id="mode_price" type="text" name="course_price" placeholder="${section_data['course_price']}" aria-required="true">
</li>
<li class="field required text" id="set-course-mode-modal-field-currency">
<label for="course_mode_currency" class="required text">${_("Currency")}</label>
<select class="field required" id="course_mode_currency" name="currency">
<option value="usd">USD</option>
</select>
</li>
</ol>
</fieldset>
<div class="submit">
<input name="submit" type="submit" id="set_course_button" value="${_('Set Price')}"/>
</div>
</form>
</div>
</div>
</section>
......@@ -280,6 +280,8 @@ if settings.COURSEWARE_ENABLED:
# For the instructor
url(r'^courses/{}/instructor$'.format(settings.COURSE_ID_PATTERN),
'instructor.views.instructor_dashboard.instructor_dashboard_2', name="instructor_dashboard"),
url(r'^courses/{}/set_course_mode_price$'.format(settings.COURSE_ID_PATTERN),
'instructor.views.instructor_dashboard.set_course_mode_price', name="set_course_mode_price"),
url(r'^courses/{}/instructor/api/'.format(settings.COURSE_ID_PATTERN),
include('instructor.views.api_urls')),
url(r'^courses/{}/remove_coupon$'.format(settings.COURSE_ID_PATTERN),
......
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