Skip to content
Snippets Groups Projects
Commit 5a9fc036 authored by asadiqbal08's avatar asadiqbal08 Committed by Chris Dodge
Browse files

Course Instructor as financeadmin role, will be able to download all purchase...

Course Instructor as financeadmin role, will be able to download all purchase transactions in the course as a CSV file
parent 44c41020
No related merge requests found
......@@ -44,6 +44,8 @@ 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 course_modes.models import CourseMode
from .test_tools import msk_from_problem_urlname, get_extended_due
......@@ -1330,13 +1332,91 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
"""
def setUp(self):
self.course = CourseFactory.create()
self.course_mode = CourseMode(course_id=self.course.id,
mode_slug="honor",
mode_display_name="honor cert",
min_price=40)
self.course_mode.save()
self.instructor = InstructorFactory(course_key=self.course.id)
self.client.login(username=self.instructor.username, password='test')
self.cart = Order.get_cart_for_user(self.instructor)
self.coupon_code = 'abcde'
self.coupon = Coupon(code=self.coupon_code, description='testing code', course_id=self.course.id,
percentage_discount=10, created_by=self.instructor, is_active=True)
self.coupon.save()
self.students = [UserFactory() for _ in xrange(6)]
for student in self.students:
CourseEnrollment.enroll(student, self.course.id)
def test_get_ecommerce_purchase_features_csv(self):
"""
Test that the response from get_purchase_transaction is in csv format.
"""
PaidCourseRegistration.add_to_order(self.cart, self.course.id)
self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123')
url = reverse('get_purchase_transaction', kwargs={'course_id': self.course.id.to_deprecated_string()})
response = self.client.get(url + '/csv', {})
self.assertEqual(response['Content-Type'], 'text/csv')
def test_get_ecommerce_purchase_features_with_coupon_info(self):
"""
Test that some minimum of information is formatted
correctly in the response to get_purchase_transaction.
"""
PaidCourseRegistration.add_to_order(self.cart, self.course.id)
url = reverse('get_purchase_transaction', kwargs={'course_id': self.course.id.to_deprecated_string()})
# using coupon code
resp = self.client.post(reverse('shoppingcart.views.use_coupon'), {'coupon_code': self.coupon_code})
self.assertEqual(resp.status_code, 200)
self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123')
response = self.client.get(url, {})
res_json = json.loads(response.content)
self.assertIn('students', res_json)
for res in res_json['students']:
self.validate_purchased_transaction_response(res, self.cart, self.instructor, self.coupon_code)
def test_get_ecommerce_purchases_features_without_coupon_info(self):
"""
Test that some minimum of information is formatted
correctly in the response to get_purchase_transaction.
"""
url = reverse('get_purchase_transaction', kwargs={'course_id': self.course.id.to_deprecated_string()})
carts, instructors = ([] for i in range(2))
# purchasing the course by different users
for _ in xrange(3):
test_instructor = InstructorFactory(course_key=self.course.id)
self.client.login(username=test_instructor.username, password='test')
cart = Order.get_cart_for_user(test_instructor)
carts.append(cart)
instructors.append(test_instructor)
PaidCourseRegistration.add_to_order(cart, self.course.id)
cart.purchase(first='FirstNameTesting123', street1='StreetTesting123')
response = self.client.get(url, {})
res_json = json.loads(response.content)
self.assertIn('students', res_json)
for res, i in zip(res_json['students'], xrange(3)):
self.validate_purchased_transaction_response(res, carts[i], instructors[i], 'None')
def validate_purchased_transaction_response(self, res, cart, user, code):
"""
validate purchased transactions attribute values with the response object
"""
item = cart.orderitem_set.all().select_subclasses()[0]
self.assertEqual(res['coupon_code'], code)
self.assertEqual(res['username'], user.username)
self.assertEqual(res['email'], user.email)
self.assertEqual(res['list_price'], item.list_price)
self.assertEqual(res['unit_cost'], item.unit_cost)
self.assertEqual(res['order_id'], cart.id)
self.assertEqual(res['orderitem_id'], item.id)
def test_get_students_features(self):
"""
Test that some minimum of information is formatted
......
......@@ -59,6 +59,7 @@ class TestECommerceDashboardViews(ModuleStoreTestCase):
# Total amount html should render in e-commerce page, total amount will be 0
total_amount = PaidCourseRegistration.get_total_amount_of_purchased_item(self.course.id)
self.assertTrue('<span>Total Amount: <span>$' + str(total_amount) + '</span></span>' in response.content)
self.assertTrue('Download All e-Commerce Purchase' in response.content)
# removing the course finance_admin role of login user
CourseFinanceAdminRole(self.course.id).remove_users(self.instructor)
......@@ -67,6 +68,7 @@ class TestECommerceDashboardViews(ModuleStoreTestCase):
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
response = self.client.post(url)
total_amount = PaidCourseRegistration.get_total_amount_of_purchased_item(self.course.id)
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_add_coupon(self):
......
......@@ -547,6 +547,34 @@ def get_grading_config(request, course_id):
return JsonResponse(response_payload)
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff')
def get_purchase_transaction(request, course_id, csv=False): # pylint: disable=W0613, W0621
"""
return the summary of all purchased transactions for a particular course
"""
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
query_features = [
'id', 'username', 'email', 'course_id', 'list_price', 'coupon_code',
'unit_cost', 'purchase_time', 'orderitem_id',
'order_id',
]
student_data = analytics.basic.purchase_transactions(course_id, query_features)
if not csv:
response_payload = {
'course_id': course_id.to_deprecated_string(),
'students': student_data,
'queried_features': query_features
}
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)
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff')
......
......@@ -17,6 +17,8 @@ urlpatterns = patterns('', # nopep8
'instructor.views.api.get_grading_config', name="get_grading_config"),
url(r'^get_students_features(?P<csv>/csv)?$',
'instructor.views.api.get_students_features', name="get_students_features"),
url(r'^get_purchase_transaction(?P<csv>/csv)?$',
'instructor.views.api.get_purchase_transaction', name="get_purchase_transaction"),
url(r'^get_anon_ids$',
'instructor.views.api.get_anon_ids', name="get_anon_ids"),
url(r'^get_distribution$',
......
......@@ -142,6 +142,7 @@ def _section_e_commerce(course_key, access):
'ajax_get_coupon_info': reverse('get_coupon_info', kwargs={'course_id': course_key.to_deprecated_string()}),
'ajax_update_coupon': reverse('update_coupon', kwargs={'course_id': course_key.to_deprecated_string()}),
'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()}),
'coupons': coupons,
'total_amount': total_amount,
......
......@@ -3,7 +3,7 @@ Student and course analytics.
Serve miscellaneous course and student data
"""
from shoppingcart.models import PaidCourseRegistration, CouponRedemption
from django.contrib.auth.models import User
import xmodule.graders as xmgraders
......@@ -11,9 +11,60 @@ import xmodule.graders as xmgraders
STUDENT_FEATURES = ('id', 'username', 'first_name', 'last_name', 'is_staff', 'email')
PROFILE_FEATURES = ('name', 'language', 'location', 'year_of_birth', 'gender',
'level_of_education', 'mailing_address', 'goals')
ORDER_ITEM_FEATURES = ('list_price', 'unit_cost', 'order_id')
ORDER_FEATURES = ('purchase_time',)
AVAILABLE_FEATURES = STUDENT_FEATURES + PROFILE_FEATURES
def purchase_transactions(course_id, features):
"""
Return list of purchased transactions features as dictionaries.
purchase_transactions(course_id, ['username, email', unit_cost])
would return [
{'username': 'username1', 'email': 'email1', unit_cost:'cost1 in decimal'.}
{'username': 'username2', 'email': 'email2', unit_cost:'cost2 in decimal'.}
{'username': 'username3', 'email': 'email3', unit_cost:'cost3 in decimal'.}
]
"""
purchased_courses = PaidCourseRegistration.objects.filter(course_id=course_id, status='purchased')
def purchase_transactions_info(purchased_course, features):
""" convert purchase transactions to dictionary """
coupon_code_dict = dict()
student_features = [x for x in STUDENT_FEATURES if x in features]
order_features = [x for x in ORDER_FEATURES if x in features]
order_item_features = [x for x in ORDER_ITEM_FEATURES if x in features]
# Extracting user information
student_dict = dict((feature, getattr(purchased_course.user, feature))
for feature in student_features)
# Extracting Order information
order_dict = dict((feature, getattr(purchased_course.order, feature))
for feature in order_features)
# Extracting OrderItem information
order_item_dict = dict((feature, getattr(purchased_course, feature))
for feature in order_item_features)
order_item_dict.update({"orderitem_id": getattr(purchased_course, 'id')})
try:
coupon_redemption = CouponRedemption.objects.select_related('coupon').get(order_id=purchased_course.order_id)
except CouponRedemption.DoesNotExist:
coupon_code_dict = {'coupon_code': 'None'}
else:
coupon_code_dict = {'coupon_code': coupon_redemption.coupon.code}
student_dict.update(dict(order_dict.items() + order_item_dict.items() + coupon_code_dict.items()))
student_dict.update({'course_id': course_id.to_deprecated_string()})
return student_dict
return [purchase_transactions_info(purchased_course, features) for purchased_course in purchased_courses]
def enrolled_students_features(course_id, features):
"""
Return list of student features as dictionaries.
......
###
E-Commerce Download Section
###
# Ecommerce Purchase Download Section
class ECommerce
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']'")
# attach click handlers
# this handler binds to both the download
# and the csv button
@$list_purchase_csv_btn.click (e) =>
url = @$list_purchase_csv_btn.data 'endpoint'
url += '/csv'
location.href = url
# handler for when the section title is clicked.
onClickTitle: ->
@clear_display()
clear_display: ->
# 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
......@@ -155,6 +155,9 @@ setup_instructor_dashboard_sections = (idash_content) ->
,
constructor: window.InstructorDashboard.sections.DataDownload
$element: idash_content.find ".#{CSS_IDASH_SECTION}#data_download"
,
constructor: window.InstructorDashboard.sections.ECommerce
$element: idash_content.find ".#{CSS_IDASH_SECTION}#e-commerce"
,
constructor: window.InstructorDashboard.sections.Membership
$element: idash_content.find ".#{CSS_IDASH_SECTION}#membership"
......
......@@ -931,7 +931,7 @@ input[name="subject"] {
}
}
// coupon edit and add modals
// coupon edit and add modals
#add-coupon-modal, #edit-coupon-modal{
.inner-wrapper {
background: #fff;
......@@ -1046,4 +1046,88 @@ input[name="subject"] {
}
}
}
}
.profile-distribution-widget {
margin-bottom: $baseline * 2;
.display-text {}
.display-graph .graph-placeholder {
width: 750px;
height: 250px;
}
.display-table {
.slickgrid {
height: 250px;
}
}
}
.grade-distributions-widget {
margin-bottom: $baseline * 2;
.last-updated {
line-height: 2.2em;
@include font-size(12);
}
.display-graph .graph-placeholder {
width: 750px;
height: 200px;
}
.display-text {
line-height: 2em;
}
}
input[name="subject"] {
width:600px;
}
.enrollment-wrapper {
margin-bottom: $baseline * 2;
.count {
color: green;
font-weight: bold;
}
}
.ecommerce-wrapper{
h2{
height: 26px;
line-height: 26px;
span{
float: right;
font-size: 16px;
font-weight: bold;
span{
background: #ddd;
padding: 2px 9px;
border-radius: 2px;
float: none;
font-weight: 400;
}
}
}
span.tip{
padding: 10px 15px;
display: block;
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
background: #f8f4ec;
color: #3c3c3c;
line-height: 30px;
.add{
@include button(simple, $blue);
@extend .button-reset;
font-size: em(13);
float: right;
}
}
}
......@@ -3,6 +3,12 @@
<%include file="add_coupon_modal.html" args="section_data=section_data" />
<%include file="edit_coupon_modal.html" args="section_data=section_data" />
%if section_data['access']['finance_admin'] is True:
<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 Purchase")}" data-endpoint="${ section_data['get_purchase_transaction_url'] }" data-csv="true"></p>
%endif
<div class="ecommerce-wrapper">
<h2>${_("Coupons List")}
%if section_data['total_amount'] is not None:
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment