Commit 0569a770 by Afzal Wali Committed by Chris Dodge

Executive Summary Report

parent 40b4bc65
......@@ -105,6 +105,16 @@ REPORTS_DATA = (
}
)
# ddt data for test cases involving executive summary report
EXECUTIVE_SUMMARY_DATA = (
{
'report_type': 'executive summary',
'instructor_api_endpoint': 'get_exec_summary_report',
'task_api_endpoint': 'instructor_task.api.submit_executive_summary_report',
'extra_instructor_api_kwargs': {}
},
)
@common_exceptions_400
def view_success(request): # pylint: disable=unused-argument
......@@ -215,6 +225,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
('get_students_features', {}),
('get_enrollment_report', {}),
('get_students_who_may_enroll', {}),
('get_exec_summary_report', {}),
]
# Endpoints that only Instructors can access
self.instructor_level_endpoints = [
......@@ -2544,9 +2555,36 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
success_status = "Your {report_type} report is being generated! You can view the status of the generation task in the 'Pending Instructor Tasks' section.".format(report_type=report_type)
self.assertIn(success_status, response.content)
@ddt.data(*REPORTS_DATA)
@ddt.data(*EXECUTIVE_SUMMARY_DATA)
@ddt.unpack
def test_executive_summary_report_success(
self,
report_type,
instructor_api_endpoint,
task_api_endpoint,
extra_instructor_api_kwargs
):
kwargs = {'course_id': unicode(self.course.id)}
kwargs.update(extra_instructor_api_kwargs)
url = reverse(instructor_api_endpoint, kwargs=kwargs)
CourseFinanceAdminRole(self.course.id).add_users(self.instructor)
with patch(task_api_endpoint):
response = self.client.get(url, {})
success_status = "Your {report_type} report is being created." \
" To view the status of the report, see the 'Pending Instructor Tasks'" \
" section.".format(report_type=report_type)
self.assertIn(success_status, response.content)
@ddt.data(*EXECUTIVE_SUMMARY_DATA)
@ddt.unpack
def test_calculate_report_csv_already_running(self, report_type, instructor_api_endpoint, task_api_endpoint, extra_instructor_api_kwargs):
def test_executive_summary_report_already_running(
self,
report_type,
instructor_api_endpoint,
task_api_endpoint,
extra_instructor_api_kwargs
):
kwargs = {'course_id': unicode(self.course.id)}
kwargs.update(extra_instructor_api_kwargs)
url = reverse(instructor_api_endpoint, kwargs=kwargs)
......@@ -2555,7 +2593,11 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
with patch(task_api_endpoint) as mock:
mock.side_effect = AlreadyRunningError()
response = self.client.get(url, {})
already_running_status = "{report_type} report generation task is already in progress. Check the 'Pending Instructor Tasks' table for the status of the task. When completed, the report will be available for download in the table below.".format(report_type=report_type)
already_running_status = "An {report_type} report is currently in progress." \
" To view the status of the report, see the 'Pending Instructor Tasks' section." \
" When completed, the report will be available for download in the table below." \
" You will be able to download the" \
" report when it is complete.".format(report_type=report_type)
self.assertIn(already_running_status, response.content)
def test_get_distribution_no_feature(self):
......
......@@ -1228,6 +1228,31 @@ def get_enrollment_report(request, course_id):
})
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff')
@require_finance_admin
def get_exec_summary_report(request, course_id):
"""
get the executive summary report for the particular course.
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
try:
instructor_task.api.submit_executive_summary_report(request, course_key)
status_response = _("Your executive summary report is being created. "
"To view the status of the report, see the 'Pending Instructor Tasks' section.")
except AlreadyRunningError:
status_response = _(
"An executive summary report is currently in progress. "
"To view the status of the report, see the 'Pending Instructor Tasks' section. "
"When completed, the report will be available for download in the table below. "
"You will be able to download the report when it is complete."
)
return JsonResponse({
"status": status_response
})
def save_registration_code(user, course_id, mode_slug, invoice=None, order=None, invoice_item=None):
"""
recursive function that generate a new code every time and saves in the Course Registration Table
......
......@@ -109,7 +109,8 @@ urlpatterns = patterns(
# Reports..
url(r'get_enrollment_report$',
'instructor.views.api.get_enrollment_report', name="get_enrollment_report"),
url(r'get_exec_summary_report$',
'instructor.views.api.get_exec_summary_report', name="get_exec_summary_report"),
# Coupon Codes..
url(r'get_coupon_codes',
......
......@@ -202,6 +202,7 @@ def _section_e_commerce(course, access, paid_mode, coupons_enabled, reports_enab
'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)}),
'enrollment_report_url': reverse('get_enrollment_report', kwargs={'course_id': unicode(course_key)}),
'exec_summary_report_url': reverse('get_exec_summary_report', kwargs={'course_id': unicode(course_key)}),
'list_financial_report_downloads_url': reverse('list_financial_report_downloads',
kwargs={'course_id': unicode(course_key)}),
'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': unicode(course_key)}),
......
......@@ -24,6 +24,7 @@ from instructor_task.tasks import (
cohort_students,
enrollment_report_features_csv,
calculate_may_enroll_csv,
exec_summary_report_csv
)
from instructor_task.api_helper import (
......@@ -392,6 +393,20 @@ def submit_calculate_may_enroll_csv(request, course_key, features):
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
def submit_executive_summary_report(request, course_key): # pylint: disable=invalid-name
"""
Submits a task to generate a HTML File containing the executive summary report.
Raises AlreadyRunningError if HTML File is already being updated.
"""
task_type = 'exec_summary_report'
task_class = exec_summary_report_csv
task_input = {}
task_key = ""
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
def submit_cohort_students(request, course_key, file_name):
"""
Request to have students cohorted in bulk.
......
......@@ -275,7 +275,7 @@ class S3ReportStore(ReportStore):
return key
def store(self, course_id, filename, buff):
def store(self, course_id, filename, buff, config=None):
"""
Store the contents of `buff` in a directory determined by hashing
`course_id`, and name the file `filename`. `buff` is typically a
......@@ -288,10 +288,15 @@ class S3ReportStore(ReportStore):
"""
key = self.key_for(course_id, filename)
_config = config if config else {}
content_type = _config.get('content_type', 'text/csv')
content_encoding = _config.get('content_encoding', 'gzip')
data = buff.getvalue()
key.size = len(data)
key.content_encoding = "gzip"
key.content_type = "text/csv"
key.content_encoding = content_encoding
key.content_type = content_type
# Just setting the content encoding and type above should work
# according to the docs, but when experimenting, this was necessary for
......@@ -299,9 +304,9 @@ class S3ReportStore(ReportStore):
key.set_contents_from_string(
data,
headers={
"Content-Encoding": "gzip",
"Content-Encoding": content_encoding,
"Content-Length": len(data),
"Content-Type": "text/csv",
"Content-Type": content_type,
}
)
......@@ -371,7 +376,7 @@ class LocalFSReportStore(ReportStore):
"""Return the full path to a given file for a given course."""
return os.path.join(self.root_path, urllib.quote(course_id.to_deprecated_string(), safe=''), filename)
def store(self, course_id, filename, buff):
def store(self, course_id, filename, buff, config=None): # pylint: disable=unused-argument
"""
Given the `course_id` and `filename`, store the contents of `buff` in
that file. Overwrite anything that was there previously. `buff` is
......
......@@ -40,6 +40,7 @@ from instructor_task.tasks_helper import (
cohort_students_and_upload,
upload_enrollment_report,
upload_may_enroll_csv,
upload_exec_summary_report
)
......@@ -200,6 +201,18 @@ def enrollment_report_features_csv(entry_id, xmodule_instance_args):
@task(base=BaseInstructorTask, routing_key=settings.GRADES_DOWNLOAD_ROUTING_KEY) # pylint: disable=not-callable
def exec_summary_report_csv(entry_id, xmodule_instance_args):
"""
Compute executive summary report for a course and upload the
Html generated report to an S3 bucket for download.
"""
# Translators: This is a past-tense verb that is inserted into task progress messages as {action}.
action_name = 'generating_exec_summary_report'
task_fn = partial(upload_exec_summary_report, xmodule_instance_args)
return run_main_task(entry_id, task_fn, action_name)
@task(base=BaseInstructorTask, routing_key=settings.GRADES_DOWNLOAD_ROUTING_KEY) # pylint: disable=not-callable
def calculate_may_enroll_csv(entry_id, xmodule_instance_args):
"""
Compute information about invited students who have not enrolled
......
......@@ -18,6 +18,7 @@ from instructor_task.api import (
submit_cohort_students,
submit_detailed_enrollment_features_csv,
submit_calculate_may_enroll_csv,
submit_executive_summary_report
)
from instructor_task.api_helper import AlreadyRunningError
......@@ -214,6 +215,12 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa
self.course.id)
self._test_resubmission(api_call)
def test_submit_executive_summary_report(self):
api_call = lambda: submit_executive_summary_report(
self.create_task_request(self.instructor), self.course.id
)
self._test_resubmission(api_call)
def test_submit_calculate_may_enroll(self):
api_call = lambda: submit_calculate_may_enroll_csv(
self.create_task_request(self.instructor),
......
......@@ -18,19 +18,16 @@ from course_modes.models import CourseMode
from courseware.tests.factories import InstructorFactory
from instructor_task.models import ReportStore
from instructor_task.tasks_helper import cohort_students_and_upload, upload_grades_csv, upload_students_csv, \
upload_enrollment_report
upload_enrollment_report, upload_exec_summary_report
from instructor_task.tests.test_base import InstructorTaskCourseTestCase, TestReportMixin, InstructorTaskModuleTestCase
from openedx.core.djangoapps.course_groups.models import CourseUserGroupPartitionGroup
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
import openedx.core.djangoapps.user_api.course_tag.api as course_tag_api
from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartitionScheme
from shoppingcart.models import Order, PaidCourseRegistration, CourseRegistrationCode, Invoice, \
CourseRegistrationCodeInvoiceItem, InvoiceTransaction
from student.tests.factories import UserFactory
from student.models import (
CourseEnrollment, CourseEnrollmentAllowed, ManualEnrollmentAudit,
ALLOWEDTOENROLL_TO_ENROLLED
)
CourseRegistrationCodeInvoiceItem, InvoiceTransaction, Coupon
from student.tests.factories import UserFactory, CourseModeFactory
from student.models import CourseEnrollment, CourseEnrollmentAllowed, ManualEnrollmentAudit, ALLOWEDTOENROLL_TO_ENROLLED
from verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.partitions.partitions import Group, UserPartition
......@@ -715,6 +712,132 @@ class TestProblemReportCohortedContent(TestReportMixin, ContentGroupTestCase, In
@ddt.ddt
class TestExecutiveSummaryReport(TestReportMixin, InstructorTaskCourseTestCase):
"""
Tests that Executive Summary report generation works.
"""
def setUp(self):
super(TestExecutiveSummaryReport, self).setUp()
self.course = CourseFactory.create()
CourseModeFactory.create(course_id=self.course.id, min_price=50)
self.instructor = InstructorFactory(course_key=self.course.id)
self.student1 = UserFactory()
self.student2 = UserFactory()
self.student1_cart = Order.get_cart_for_user(self.student1)
self.student2_cart = Order.get_cart_for_user(self.student2)
self.sale_invoice_1 = Invoice.objects.create(
total_amount=1234.32, company_name='Test1', company_contact_name='TestName',
company_contact_email='Test@company.com',
recipient_name='Testw', recipient_email='test1@test.com', customer_reference_number='2Fwe23S',
internal_reference="A", course_id=self.course.id, is_valid=True
)
InvoiceTransaction.objects.create(
invoice=self.sale_invoice_1,
amount=self.sale_invoice_1.total_amount,
status='completed',
created_by=self.instructor,
last_modified_by=self.instructor
)
self.invoice_item = CourseRegistrationCodeInvoiceItem.objects.create(
invoice=self.sale_invoice_1,
qty=10,
unit_price=1234.32,
course_id=self.course.id
)
for i in range(5):
coupon = Coupon(
code='coupon{0}'.format(i), description='test_description', course_id=self.course.id,
percentage_discount='{0}'.format(i), created_by=self.instructor, is_active=True,
)
coupon.save()
def test_successfully_generate_executive_summary_report(self):
"""
Test that successfully generates the executive summary report.
"""
task_input = {'features': []}
with patch('instructor_task.tasks_helper._get_current_task'):
result = upload_exec_summary_report(
None, None, self.course.id,
task_input, 'generating executive summary report'
)
ReportStore.from_config(config_name='FINANCIAL_REPORTS')
self.assertDictContainsSubset({'attempted': 1, 'succeeded': 1, 'failed': 0}, result)
def students_purchases(self):
"""
Students purchases the courses using enrollment
and coupon codes.
"""
self.client.login(username=self.student1.username, password='test')
paid_course_reg_item = PaidCourseRegistration.add_to_order(self.student1_cart, self.course.id)
# update the quantity of the cart item paid_course_reg_item
resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), {
'ItemId': paid_course_reg_item.id, 'qty': '4'
})
self.assertEqual(resp.status_code, 200)
# apply the coupon code to the item in the cart
resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': 'coupon1'})
self.assertEqual(resp.status_code, 200)
self.student1_cart.purchase()
course_reg_codes = CourseRegistrationCode.objects.filter(order=self.student1_cart)
redeem_url = reverse('register_code_redemption', args=[course_reg_codes[0].code])
response = self.client.get(redeem_url)
self.assertEquals(response.status_code, 200)
# check button text
self.assertTrue('Activate Course Enrollment' in response.content)
response = self.client.post(redeem_url)
self.assertEquals(response.status_code, 200)
self.client.login(username=self.student2.username, password='test')
PaidCourseRegistration.add_to_order(self.student2_cart, self.course.id)
# apply the coupon code to the item in the cart
resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': 'coupon1'})
self.assertEqual(resp.status_code, 200)
self.student2_cart.purchase()
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
def test_generate_executive_summary_report(self):
"""
test to generate executive summary report
and then test the report authenticity.
"""
self.students_purchases()
task_input = {'features': []}
with patch('instructor_task.tasks_helper._get_current_task'):
result = upload_exec_summary_report(
None, None, self.course.id,
task_input, 'generating executive summary report'
)
report_store = ReportStore.from_config(config_name='FINANCIAL_REPORTS')
expected_data = [
'Gross Revenue Collected', '$1481.82',
'Gross Revenue Pending', '$0.00',
'Average Price per Seat', '$296.36',
'Number of seats purchased using coupon codes', '<td>2</td>'
]
self.assertDictContainsSubset({'attempted': 1, 'succeeded': 1, 'failed': 0}, result)
self._verify_html_file_report(report_store, expected_data)
def _verify_html_file_report(self, report_store, expected_data):
"""
Verify grade report data.
"""
report_html_filename = report_store.links_for(self.course.id)[0][0]
with open(report_store.path_to(self.course.id, report_html_filename)) as html_file:
html_file_data = html_file.read()
for data in expected_data:
self.assertTrue(data in html_file_data)
@ddt.ddt
class TestStudentReport(TestReportMixin, InstructorTaskCourseTestCase):
"""
Tests that CSV student profile report generation works.
......
......@@ -22,7 +22,7 @@ from django.core.mail import send_mail
from django.contrib.auth.models import User
from django.utils.translation import ugettext as _, ugettext_lazy
from django.db import transaction
from django.db.models import Sum
from django.db.models import Sum, Count
from django.db.models.signals import post_save, post_delete
from django.core.urlresolvers import reverse
......@@ -834,6 +834,15 @@ class Invoice(TimeStampedModel):
)
is_valid = models.BooleanField(default=True)
@classmethod
def get_invoice_total_amount_for_course(cls, course_key):
"""
returns the invoice total amount generated by course.
"""
result = cls.objects.filter(course_id=course_key, is_valid=True).aggregate(total=Sum('total_amount')) # pylint: disable=no-member
return result.get('total', 0)
def generate_pdf_invoice(self, course, course_price, quantity, sale_price):
"""
Generates the pdf invoice for the given course
......@@ -995,6 +1004,17 @@ class InvoiceTransaction(TimeStampedModel):
except InvoiceTransaction.DoesNotExist:
return None
@classmethod
def get_total_amount_of_paid_course_invoices(cls, course_key):
"""
returns the total amount of the paid invoices.
"""
result = cls.objects.filter(amount__gt=0, invoice__course_id=course_key, status='completed').aggregate(
total=Sum('amount')
) # pylint: disable=no-member
return result.get('total', 0)
def snapshot(self):
"""Create a snapshot of the invoice transaction.
......@@ -1169,6 +1189,22 @@ class CourseRegistrationCode(models.Model):
invoice = models.ForeignKey(Invoice, null=True)
invoice_item = models.ForeignKey(CourseRegistrationCodeInvoiceItem, null=True)
@classmethod
def order_generated_registration_codes(cls, course_id):
"""
Returns the registration codes that were generated
via bulk purchase scenario.
"""
return cls.objects.filter(order__isnull=False, course_id=course_id)
@classmethod
def invoice_generated_registration_codes(cls, course_id):
"""
Returns the registration codes that were generated
via invoice.
"""
return cls.objects.filter(invoice__isnull=False, course_id=course_id)
class RegistrationCodeRedemption(models.Model):
"""
......@@ -1354,6 +1390,33 @@ class CouponRedemption(models.Model):
return is_redemption_applied
@classmethod
def get_top_discount_codes_used(cls, course_id):
"""
Returns the top discount codes used.
QuerySet = [
{
'coupon__percentage_discount': 22,
'coupon__code': '12',
'coupon__used_count': '2',
},
{
...
}
]
"""
return cls.objects.filter(order__status='purchased', coupon__course_id=course_id).values(
'coupon__code', 'coupon__percentage_discount'
).annotate(coupon__used_count=Count('coupon__code'))
@classmethod
def get_total_coupon_code_purchases(cls, course_id):
"""
returns total seats purchases using coupon codes
"""
return cls.objects.filter(order__status='purchased', coupon__course_id=course_id).aggregate(Count('coupon'))
class PaidCourseRegistration(OrderItem):
"""
......@@ -1364,6 +1427,13 @@ class PaidCourseRegistration(OrderItem):
course_enrollment = models.ForeignKey(CourseEnrollment, null=True)
@classmethod
def get_self_purchased_seat_count(cls, course_key, status='purchased'):
"""
returns the count of paid_course items filter by course_id and status.
"""
return cls.objects.filter(course_id=course_key, status=status).count()
@classmethod
def get_course_item_for_user_enrollment(cls, user, course_id, course_enrollment):
"""
Returns PaidCourseRegistration object if user has payed for
......@@ -1387,12 +1457,14 @@ class PaidCourseRegistration(OrderItem):
]
@classmethod
def get_total_amount_of_purchased_item(cls, course_key):
def get_total_amount_of_purchased_item(cls, course_key, status='purchased'):
"""
This will return the total amount of money that a purchased course generated
"""
total_cost = 0
result = cls.objects.filter(course_id=course_key, status='purchased').aggregate(total=Sum('unit_cost', field='qty * unit_cost')) # pylint: disable=no-member
result = cls.objects.filter(course_id=course_key, status=status).aggregate(
total=Sum('unit_cost', field='qty * unit_cost')
) # pylint: disable=no-member
if result['total'] is not None:
total_cost = result['total']
......@@ -1534,6 +1606,19 @@ class CourseRegCodeItem(OrderItem):
mode = models.SlugField(default=CourseMode.DEFAULT_MODE_SLUG)
@classmethod
def get_bulk_purchased_seat_count(cls, course_key, status='purchased'):
"""
returns the sum of bulk purchases seats.
"""
total = 0
result = cls.objects.filter(course_id=course_key, status=status).aggregate(total=Sum('qty'))
if result['total'] is not None:
total = result['total']
return total
@classmethod
def contained_in_order(cls, order, course_id):
"""
Is the course defined by course_id contained in the order?
......@@ -1545,12 +1630,14 @@ class CourseRegCodeItem(OrderItem):
]
@classmethod
def get_total_amount_of_purchased_item(cls, course_key):
def get_total_amount_of_purchased_item(cls, course_key, status='purchased'):
"""
This will return the total amount of money that a purchased course generated
"""
total_cost = 0
result = cls.objects.filter(course_id=course_key, status='purchased').aggregate(total=Sum('unit_cost', field='qty * unit_cost')) # pylint: disable=no-member
result = cls.objects.filter(course_id=course_key, status=status).aggregate(
total=Sum('unit_cost', field='qty * unit_cost')
) # pylint: disable=no-member
if result['total'] is not None:
total_cost = result['total']
......
......@@ -367,7 +367,7 @@ class ReportDownloads
minWidth: 150
cssClass: "file-download-link"
formatter: (row, cell, value, columnDef, dataContext) ->
'<a href="' + dataContext['url'] + '">' + dataContext['name'] + '</a>'
'<a target="_blank" href="' + dataContext['url'] + '">' + dataContext['name'] + '</a>'
]
$table_placeholder = $ '<div/>', class: 'slickgrid'
......
......@@ -36,22 +36,39 @@ var edx = edx || {};
minDate: 0
});
var view = new edx.instructor_dashboard.ecommerce.ExpiryCouponView();
var request_response = $('.reports .request-response');
var request_response_error = $('.reports .request-response-error');
$('input[name="user-enrollment-report"]').click(function(){
var url = $(this).data('endpoint');
$.ajax({
dataType: "json",
url: url,
success: function (data) {
request_response.text(data['status']);
return $(".reports .msg-confirm").css({
$('#enrollment-report-request-response').text(data['status']);
return $("#enrollment-report-request-response").css({
"display": "block"
});
},
error: function(std_ajax_err) {
request_response_error.text(gettext('Error generating grades. Please try again.'));
return $(".reports .msg-error").css({
$('#enrollment-report-request-response-error').text(gettext('There was a problem creating the report. Select "Create Executive Summary" to try again.'));
return $("#enrollment-report-request-response-error").css({
"display": "block"
});
}
});
});
$('input[name="exec-summary-report"]').click(function(){
var url = $(this).data('endpoint');
$.ajax({
dataType: "json",
url: url,
success: function (data) {
$("#exec-summary-report-request-response").text(data['status']);
return $("#exec-summary-report-request-response").css({
"display": "block"
});
},
error: function(std_ajax_err) {
$('#exec-summary-report-request-response-error').text(gettext('There was a problem creating the report. Select "Create Executive Summary" to try again.'));
return $("#exec-summary-report-request-response-error").css({
"display": "block"
});
}
......
......@@ -96,11 +96,20 @@ import pytz
<div>
<span class="csv_tip">
<div>
<p>${_("Download a .csv file for all credit card purchases or for all invoices, regardless of status.")}</p>
<input type="button" class="add blue-button" name="user-enrollment-report" value="${_("Download Enrollment Report")}" data-endpoint="${ section_data['enrollment_report_url'] }">
<p>${_("Create a .csv file that contains enrollment information for your course.")}</p>
<input type="button" class="add blue-button" name="user-enrollment-report" value="${_("Create Enrollment Report")}" data-endpoint="${ section_data['enrollment_report_url'] }">
</div>
<div class="request-response msg msg-confirm copy" id="report-request-response"></div>
<div class="request-response-error msg msg-warning copy" id="report-request-response-error"></div>
<div class="request-response msg msg-confirm copy" id="enrollment-report-request-response"></div>
<div class="request-response-error msg msg-warning copy" id="enrollment-report-request-response-error"></div>
<br>
</span>
<span class="csv_tip">
<div>
<p>${_("Create an HTML file that contains an executive summary for this course.")}</p>
<input type="button" class="add blue-button" name="exec-summary-report" value="${_("Create Executive Summary")}" data-endpoint="${ section_data['exec_summary_report_url'] }">
</div>
<div class="request-response msg msg-confirm copy" id="exec-summary-report-request-response"></div>
<div class="request-response-error msg msg-warning copy" id="exec-summary-report-request-response-error"></div>
<br>
</span>
<div class="reports-download-container action-type-container">
......
<%! from django.utils.translation import ugettext as _ %>
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Executive Summary</title>
<style type="text/css">
body {
font-family: Arial, Helvetica, sans-serif;
font-size:14px;
line-height:22px;
margin: 10px;
}
.box-bg {
background:#f1f1f1;
padding:10px;
}
th {
padding:5px;
background:#ccc;
}
h2 {
margin-top:0
}
</style>
</head>
<body>
<table width="650" border="0" cellspacing="5" cellpadding="5">
<tr>
<td align="left" valign="top" class="box-bg"><h2>${_("Executive Summary for {display_name}".format(display_name=display_name))}</h2>
<table width="100%">
<tr>
<td width="300">${_("Course Start Date")}</td>
<td align="right"> ${start_date}</td>
</tr>
<tr>
<td width="300">${_("Course End Date")}</td>
<td align="right"> ${end_date}</td>
</tr>
<tr>
<td width="300">${_("Report Creation Date")}</td>
<td align="right"> ${report_generation_date}</td>
</tr>
<tr>
<td width="300">${_("Number of Seats")}</td>
<td align="right">${total_seats}</td>
</tr>
<tr>
<td width="300">${_("Number of Enrollments")}</td>
<td align="right">${total_enrollments}</td>
</tr>
<tr>
<td>${_("Gross Revenue Collected")}</td>
<td align="right">${currency}${"{0:0.2f}".format(gross_revenue)}</td>
</tr>
<tr>
<td>${_("Gross Revenue Pending")}</td>
<td align="right">${currency}${"{0:0.2f}".format(gross_pending_revenue)}</td>
</tr>
<tr>
<td>${_("Number of Enrollment Refunds")}</td>
<td align="right">${total_seats_refunded}</td>
</tr>
<tr>
<td>${_("Amount Refunded")}</td>
<td align="right">${currency}${"{0:0.2f}".format(total_amount_refunded)}</td>
</tr>
<tr>
<td>${_("Average Price per Seat")}</td>
<td align="right">${currency}${"{0:0.2f}".format(average_paid_price)}</td>
</tr>
</table></td>
</tr>
<tr>
<td align="left" valign="top" class="box-bg"><h3>${_("Frequently Used Coupon Codes")}</h3>
<table width="500">
<tr>
<td>${_("Number of seats purchased using coupon codes")}</td>
<td>${total_seats_using_discount_codes['coupon__count']}</td>
</tr>
</table>
<table width="100%">
<th>${_("Rank")}</th>
<th>${_("Coupon Code")}</th>
<th>${_("Percent Discount")}</th>
<th>${_("Times Used")}</th>
%for i, discount_code_data in enumerate(discount_codes_data):
<tr>
<td align="center">${i+1}</td>
<td align="center">${discount_code_data['coupon__code']}</td>
<td align="center">${discount_code_data['coupon__percentage_discount']}</td>
<td align="center">${discount_code_data['coupon__used_count']}</td>
</tr>
%endfor
</table></td>
</tr>
<tr>
<td align="left" valign="top" class="box-bg"><h3>${_("Bulk and Single Seat Purchases")}</h3>
<table width="100%">
<tr>
<td>${_("Number of seats purchased individually")}</td>
<td align="right">${total_self_purchase_seats}</td>
</tr>
<tr>
<td>${_("Number of seats purchased in bulk")}</td>
<td align="right">${total_bulk_purchase_seats}</td>
</tr>
<tr>
<td>${_("Number of seats purchased with invoices")}</td>
<td align="right">${total_invoiced_seats}</td>
</tr>
<tr>
<td>${_("Unused bulk purchase seats (revenue at risk)")}</td>
<td align="right">${unused_bulk_purchase_code_count}</td>
</tr>
<tr>
<td>${_("Percentage of seats purchased individually")}</td>
<td align="right">${"{0:0.2f}".format(self_purchases_percentage)}%</td>
</tr>
<tr>
<td>${_("Percentage of seats purchased in bulk")}</td>
<td align="right">${"{0:0.2f}".format(bulk_purchases_percentage)}%</td>
</tr>
<tr>
<td>${_("Percentage of seats purchased with invoices")}</td>
<td align="right">${"{0:0.2f}".format(invoice_purchases_percentage)}%</td>
</tr>
</table></td>
</tr>
</table>
</body>
</html>
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