Executive Summary Report
This commit is contained in:
@@ -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_calculate_report_csv_already_running(self, report_type, instructor_api_endpoint, task_api_endpoint, extra_instructor_api_kwargs):
|
||||
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_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
|
||||
)
|
||||
|
||||
|
||||
@@ -199,6 +200,18 @@ def enrollment_report_features_csv(entry_id, 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 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):
|
||||
"""
|
||||
|
||||
@@ -6,6 +6,7 @@ running state of a course.
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from django.conf import settings
|
||||
from eventtracking import tracker
|
||||
from itertools import chain
|
||||
from time import time
|
||||
@@ -19,7 +20,13 @@ from django.core.files.storage import DefaultStorage
|
||||
from django.db import transaction, reset_queries
|
||||
import dogstats_wrapper as dog_stats_api
|
||||
from pytz import UTC
|
||||
from StringIO import StringIO
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from instructor.paidcourse_enrollment_report import PaidCourseEnrollmentReportProvider
|
||||
from shoppingcart.models import (
|
||||
PaidCourseRegistration, CourseRegCodeItem, InvoiceTransaction,
|
||||
Invoice, CouponRedemption, RegistrationCodeRedemption, CourseRegistrationCode
|
||||
)
|
||||
|
||||
from track.views import task_track
|
||||
from util.file import course_filename_prefix_generator, UniversalNewlineIterator
|
||||
@@ -41,7 +48,7 @@ from openedx.core.djangoapps.course_groups.models import CourseUserGroup
|
||||
from openedx.core.djangoapps.content.course_structures.models import CourseStructure
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort, is_course_cohorted
|
||||
from student.models import CourseEnrollment
|
||||
from student.models import CourseEnrollment, CourseAccessRole
|
||||
from verify_student.models import SoftwareSecurePhotoVerification
|
||||
|
||||
|
||||
@@ -50,7 +57,7 @@ TASK_LOG = logging.getLogger('edx.celery.task')
|
||||
|
||||
# define value to use when no task_id is provided:
|
||||
UNKNOWN_TASK_ID = 'unknown-task_id'
|
||||
|
||||
FILTERED_OUT_ROLES = ['staff', 'instructor', 'finance_admin', 'sales_admin']
|
||||
# define values for update functions to use to return status to perform_module_state_update
|
||||
UPDATE_STATUS_SUCCEEDED = 'succeeded'
|
||||
UPDATE_STATUS_FAILED = 'failed'
|
||||
@@ -560,6 +567,36 @@ def upload_csv_to_report_store(rows, csv_name, course_id, timestamp, config_name
|
||||
tracker.emit(REPORT_REQUESTED_EVENT_NAME, {"report_type": csv_name, })
|
||||
|
||||
|
||||
def upload_exec_summary_to_store(data_dict, report_name, course_id, generated_at, config_name='FINANCIAL_REPORTS'):
|
||||
"""
|
||||
Upload Executive Summary Html file using ReportStore.
|
||||
|
||||
Arguments:
|
||||
data_dict: containing executive report data.
|
||||
report_name: Name of the resulting Html File.
|
||||
course_id: ID of the course
|
||||
"""
|
||||
report_store = ReportStore.from_config(config_name)
|
||||
|
||||
# Use the data dict and html template to generate the output buffer
|
||||
output_buffer = StringIO(render_to_string("instructor/instructor_dashboard_2/executive_summary.html", data_dict))
|
||||
|
||||
report_store.store(
|
||||
course_id,
|
||||
u"{course_prefix}_{report_name}_{timestamp_str}.html".format(
|
||||
course_prefix=course_filename_prefix_generator(course_id),
|
||||
report_name=report_name,
|
||||
timestamp_str=generated_at.strftime("%Y-%m-%d-%H%M")
|
||||
),
|
||||
output_buffer,
|
||||
config={
|
||||
'content_type': 'text/html',
|
||||
'content_encoding': None,
|
||||
}
|
||||
)
|
||||
tracker.emit(REPORT_REQUESTED_EVENT_NAME, {"report_type": report_name})
|
||||
|
||||
|
||||
def upload_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name): # pylint: disable=too-many-statements
|
||||
"""
|
||||
For a given `course_id`, generate a grades CSV file for all students that
|
||||
@@ -1023,6 +1060,149 @@ def upload_may_enroll_csv(_xmodule_instance_args, _entry_id, course_id, task_inp
|
||||
return task_progress.update_task_state(extra_meta=current_step)
|
||||
|
||||
|
||||
def get_executive_report(course_id):
|
||||
"""
|
||||
Returns dict containing information about the course executive summary.
|
||||
"""
|
||||
single_purchase_total = PaidCourseRegistration.get_total_amount_of_purchased_item(course_id)
|
||||
bulk_purchase_total = CourseRegCodeItem.get_total_amount_of_purchased_item(course_id)
|
||||
paid_invoices_total = InvoiceTransaction.get_total_amount_of_paid_course_invoices(course_id)
|
||||
gross_revenue = single_purchase_total + bulk_purchase_total + paid_invoices_total
|
||||
|
||||
all_invoices_total = Invoice.get_invoice_total_amount_for_course(course_id)
|
||||
gross_pending_revenue = all_invoices_total - float(paid_invoices_total)
|
||||
|
||||
refunded_self_purchased_seats = PaidCourseRegistration.get_self_purchased_seat_count(
|
||||
course_id, status='refunded'
|
||||
)
|
||||
refunded_bulk_purchased_seats = CourseRegCodeItem.get_bulk_purchased_seat_count(
|
||||
course_id, status='refunded'
|
||||
)
|
||||
total_seats_refunded = refunded_self_purchased_seats + refunded_bulk_purchased_seats
|
||||
|
||||
self_purchased_refunds = PaidCourseRegistration.get_total_amount_of_purchased_item(
|
||||
course_id,
|
||||
status='refunded'
|
||||
)
|
||||
bulk_purchase_refunds = CourseRegCodeItem.get_total_amount_of_purchased_item(course_id, status='refunded')
|
||||
total_amount_refunded = self_purchased_refunds + bulk_purchase_refunds
|
||||
|
||||
top_discounted_codes = CouponRedemption.get_top_discount_codes_used(course_id)
|
||||
total_coupon_codes_purchases = CouponRedemption.get_total_coupon_code_purchases(course_id)
|
||||
|
||||
bulk_purchased_codes = CourseRegistrationCode.order_generated_registration_codes(course_id)
|
||||
|
||||
unused_registration_codes = 0
|
||||
for registration_code in bulk_purchased_codes:
|
||||
if not RegistrationCodeRedemption.is_registration_code_redeemed(registration_code.code):
|
||||
unused_registration_codes += 1
|
||||
|
||||
self_purchased_seat_count = PaidCourseRegistration.get_self_purchased_seat_count(course_id)
|
||||
bulk_purchased_seat_count = CourseRegCodeItem.get_bulk_purchased_seat_count(course_id)
|
||||
total_invoiced_seats = CourseRegistrationCode.invoice_generated_registration_codes(course_id).count()
|
||||
|
||||
total_seats = self_purchased_seat_count + bulk_purchased_seat_count + total_invoiced_seats
|
||||
|
||||
self_purchases_percentage = 0.0
|
||||
bulk_purchases_percentage = 0.0
|
||||
invoice_purchases_percentage = 0.0
|
||||
avg_price_paid = 0.0
|
||||
|
||||
if total_seats != 0:
|
||||
self_purchases_percentage = (float(self_purchased_seat_count) / float(total_seats)) * 100
|
||||
bulk_purchases_percentage = (float(bulk_purchased_seat_count) / float(total_seats)) * 100
|
||||
invoice_purchases_percentage = (float(total_invoiced_seats) / float(total_seats)) * 100
|
||||
avg_price_paid = gross_revenue / total_seats
|
||||
|
||||
course = get_course_by_id(course_id, depth=0)
|
||||
currency = settings.PAID_COURSE_REGISTRATION_CURRENCY[1]
|
||||
|
||||
return {
|
||||
'display_name': course.display_name,
|
||||
'start_date': course.start.strftime("%Y-%m-%d") if course.start is not None else 'N/A',
|
||||
'end_date': course.end.strftime("%Y-%m-%d") if course.end is not None else 'N/A',
|
||||
'total_seats': total_seats,
|
||||
'currency': currency,
|
||||
'gross_revenue': float(gross_revenue),
|
||||
'gross_pending_revenue': gross_pending_revenue,
|
||||
'total_seats_refunded': total_seats_refunded,
|
||||
'total_amount_refunded': float(total_amount_refunded),
|
||||
'average_paid_price': float(avg_price_paid),
|
||||
'discount_codes_data': top_discounted_codes,
|
||||
'total_seats_using_discount_codes': total_coupon_codes_purchases,
|
||||
'total_self_purchase_seats': self_purchased_seat_count,
|
||||
'total_bulk_purchase_seats': bulk_purchased_seat_count,
|
||||
'total_invoiced_seats': total_invoiced_seats,
|
||||
'unused_bulk_purchase_code_count': unused_registration_codes,
|
||||
'self_purchases_percentage': self_purchases_percentage,
|
||||
'bulk_purchases_percentage': bulk_purchases_percentage,
|
||||
'invoice_purchases_percentage': invoice_purchases_percentage,
|
||||
}
|
||||
|
||||
|
||||
def upload_exec_summary_report(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name): # pylint: disable=too-many-statements
|
||||
"""
|
||||
For a given `course_id`, generate a html report containing information,
|
||||
which provides a snapshot of how the course is doing.
|
||||
"""
|
||||
start_time = time()
|
||||
report_generation_date = datetime.now(UTC)
|
||||
status_interval = 100
|
||||
|
||||
enrolled_users = CourseEnrollment.objects.users_enrolled_in(course_id)
|
||||
true_enrollment_count = 0
|
||||
for user in enrolled_users:
|
||||
if not user.is_staff and not CourseAccessRole.objects.filter(
|
||||
user=user, course_id=course_id, role__in=FILTERED_OUT_ROLES
|
||||
).exists():
|
||||
true_enrollment_count += 1
|
||||
|
||||
task_progress = TaskProgress(action_name, true_enrollment_count, start_time)
|
||||
|
||||
fmt = u'Task: {task_id}, InstructorTask ID: {entry_id}, Course: {course_id}, Input: {task_input}'
|
||||
task_info_string = fmt.format(
|
||||
task_id=_xmodule_instance_args.get('task_id') if _xmodule_instance_args is not None else None,
|
||||
entry_id=_entry_id,
|
||||
course_id=course_id,
|
||||
task_input=_task_input
|
||||
)
|
||||
|
||||
TASK_LOG.info(u'%s, Task type: %s, Starting task execution', task_info_string, action_name)
|
||||
current_step = {'step': 'Gathering executive summary report information'}
|
||||
|
||||
TASK_LOG.info(
|
||||
u'%s, Task type: %s, Current step: %s, generating executive summary report',
|
||||
task_info_string,
|
||||
action_name,
|
||||
current_step
|
||||
)
|
||||
|
||||
if task_progress.attempted % status_interval == 0:
|
||||
task_progress.update_task_state(extra_meta=current_step)
|
||||
task_progress.attempted += 1
|
||||
|
||||
# get the course executive summary report information.
|
||||
data_dict = get_executive_report(course_id)
|
||||
data_dict.update(
|
||||
{
|
||||
'total_enrollments': true_enrollment_count,
|
||||
'report_generation_date': report_generation_date.strftime("%Y-%m-%d"),
|
||||
}
|
||||
)
|
||||
|
||||
# By this point, we've got the data that we need to generate html report.
|
||||
current_step = {'step': 'Uploading executive summary report HTML file'}
|
||||
task_progress.update_task_state(extra_meta=current_step)
|
||||
TASK_LOG.info(u'%s, Task type: %s, Current step: %s', task_info_string, action_name, current_step)
|
||||
|
||||
# Perform the actual upload
|
||||
upload_exec_summary_to_store(data_dict, 'executive_report', course_id, report_generation_date)
|
||||
task_progress.succeeded += 1
|
||||
# One last update before we close out...
|
||||
TASK_LOG.info(u'%s, Task type: %s, Finalizing executive summary report task', task_info_string, action_name)
|
||||
return task_progress.update_task_state(extra_meta=current_step)
|
||||
|
||||
|
||||
def cohort_students_and_upload(_xmodule_instance_args, _entry_id, course_id, task_input, action_name):
|
||||
"""
|
||||
Within a given course, cohort students in bulk, then upload the results
|
||||
|
||||
@@ -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
|
||||
@@ -714,6 +711,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):
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
@@ -1363,6 +1426,13 @@ class PaidCourseRegistration(OrderItem):
|
||||
mode = models.SlugField(default=CourseMode.DEFAULT_MODE_SLUG)
|
||||
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):
|
||||
"""
|
||||
@@ -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']
|
||||
@@ -1533,6 +1605,19 @@ class CourseRegCodeItem(OrderItem):
|
||||
course_id = CourseKeyField(max_length=128, db_index=True)
|
||||
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):
|
||||
"""
|
||||
@@ -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']
|
||||
|
||||
@@ -19,6 +19,7 @@ from django.conf import settings
|
||||
from django.db import DatabaseError
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
@@ -28,8 +29,8 @@ from shoppingcart.models import (
|
||||
InvalidCartItem, CourseRegistrationCode, PaidCourseRegistration, CourseRegCodeItem,
|
||||
Donation, OrderItemSubclassPK,
|
||||
Invoice, CourseRegistrationCodeInvoiceItem, InvoiceTransaction, InvoiceHistory,
|
||||
RegistrationCodeRedemption
|
||||
)
|
||||
RegistrationCodeRedemption,
|
||||
Coupon, CouponRedemption)
|
||||
from student.tests.factories import UserFactory
|
||||
from student.models import CourseEnrollment
|
||||
from course_modes.models import CourseMode
|
||||
@@ -431,11 +432,17 @@ class OrderItemTest(TestCase):
|
||||
self.assertEquals(set([]), inst_set)
|
||||
|
||||
|
||||
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
|
||||
class PaidCourseRegistrationTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Paid Course Registration Tests.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(PaidCourseRegistrationTest, self).setUp()
|
||||
|
||||
self.user = UserFactory.create()
|
||||
self.user.set_password('password')
|
||||
self.user.save()
|
||||
self.cost = 40
|
||||
self.course = CourseFactory.create()
|
||||
self.course_key = self.course.id
|
||||
@@ -444,8 +451,20 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase):
|
||||
mode_display_name="honor cert",
|
||||
min_price=self.cost)
|
||||
self.course_mode.save()
|
||||
self.percentage_discount = 20.0
|
||||
self.cart = Order.get_cart_for_user(self.user)
|
||||
|
||||
def test_get_total_amount_of_purchased_items(self):
|
||||
"""
|
||||
Test to check the total amount of the
|
||||
purchased items.
|
||||
"""
|
||||
PaidCourseRegistration.add_to_order(self.cart, self.course_key)
|
||||
self.cart.purchase()
|
||||
|
||||
total_amount = PaidCourseRegistration.get_total_amount_of_purchased_item(course_key=self.course_key)
|
||||
self.assertEqual(total_amount, 40.00)
|
||||
|
||||
def test_add_to_order(self):
|
||||
reg1 = PaidCourseRegistration.add_to_order(self.cart, self.course_key)
|
||||
|
||||
@@ -462,6 +481,109 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase):
|
||||
|
||||
self.assertEqual(self.cart.total_cost, self.cost)
|
||||
|
||||
def test_order_generated_registration_codes(self):
|
||||
"""
|
||||
Test to check for the order generated registration
|
||||
codes.
|
||||
"""
|
||||
self.cart.order_type = 'business'
|
||||
self.cart.save()
|
||||
item = CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2)
|
||||
self.cart.purchase()
|
||||
registration_codes = CourseRegistrationCode.order_generated_registration_codes(self.course_key)
|
||||
self.assertEqual(registration_codes.count(), item.qty)
|
||||
|
||||
def add_coupon(self, course_key, is_active, code):
|
||||
"""
|
||||
add dummy coupon into models
|
||||
"""
|
||||
Coupon.objects.create(
|
||||
code=code,
|
||||
description='testing code',
|
||||
course_id=course_key,
|
||||
percentage_discount=self.percentage_discount,
|
||||
created_by=self.user,
|
||||
is_active=is_active
|
||||
)
|
||||
|
||||
def login_user(self, username):
|
||||
"""
|
||||
login the user to the platform.
|
||||
"""
|
||||
self.client.login(username=username, password="password")
|
||||
|
||||
def test_get_top_discount_codes_used(self):
|
||||
"""
|
||||
Test to check for the top coupon codes used.
|
||||
"""
|
||||
self.login_user(self.user.username)
|
||||
self.add_coupon(self.course_key, True, 'Ad123asd')
|
||||
self.add_coupon(self.course_key, True, '32213asd')
|
||||
self.purchases_using_coupon_codes()
|
||||
top_discounted_codes = CouponRedemption.get_top_discount_codes_used(self.course_key)
|
||||
self.assertTrue(top_discounted_codes[0]['coupon__code'], 'Ad123asd')
|
||||
self.assertTrue(top_discounted_codes[0]['coupon__used_count'], 1)
|
||||
self.assertTrue(top_discounted_codes[1]['coupon__code'], '32213asd')
|
||||
self.assertTrue(top_discounted_codes[1]['coupon__used_count'], 2)
|
||||
|
||||
def test_get_total_coupon_code_purchases(self):
|
||||
"""
|
||||
Test to assert the number of coupon code purchases.
|
||||
"""
|
||||
self.login_user(self.user.username)
|
||||
self.add_coupon(self.course_key, True, 'Ad123asd')
|
||||
self.add_coupon(self.course_key, True, '32213asd')
|
||||
self.purchases_using_coupon_codes()
|
||||
|
||||
total_coupon_code_purchases = CouponRedemption.get_total_coupon_code_purchases(self.course_key)
|
||||
self.assertTrue(total_coupon_code_purchases['coupon__count'], 3)
|
||||
|
||||
def test_get_self_purchased_seat_count(self):
|
||||
"""
|
||||
Test to assert the number of seats
|
||||
purchased using individual purchases.
|
||||
"""
|
||||
PaidCourseRegistration.add_to_order(self.cart, self.course_key)
|
||||
self.cart.purchase()
|
||||
|
||||
test_student = UserFactory.create()
|
||||
test_student.set_password('password')
|
||||
test_student.save()
|
||||
|
||||
self.cart = Order.get_cart_for_user(test_student)
|
||||
PaidCourseRegistration.add_to_order(self.cart, self.course_key)
|
||||
self.cart.purchase()
|
||||
|
||||
total_seats_count = PaidCourseRegistration.get_self_purchased_seat_count(course_key=self.course_key)
|
||||
self.assertTrue(total_seats_count, 2)
|
||||
|
||||
def purchases_using_coupon_codes(self):
|
||||
"""
|
||||
helper method that uses coupon codes when purchasing courses.
|
||||
"""
|
||||
self.cart.order_type = 'business'
|
||||
self.cart.save()
|
||||
CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2)
|
||||
resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': 'Ad123asd'})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.cart.purchase()
|
||||
|
||||
self.cart.clear()
|
||||
self.cart = Order.get_cart_for_user(self.user)
|
||||
self.cart.order_type = 'business'
|
||||
self.cart.save()
|
||||
CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2)
|
||||
resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': 'Ad123asd'})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.cart.purchase()
|
||||
|
||||
self.cart.clear()
|
||||
self.cart = Order.get_cart_for_user(self.user)
|
||||
PaidCourseRegistration.add_to_order(self.cart, self.course_key)
|
||||
resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': '32213asd'})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.cart.purchase()
|
||||
|
||||
def test_cart_type_business(self):
|
||||
self.cart.order_type = 'business'
|
||||
self.cart.save()
|
||||
@@ -469,7 +591,8 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase):
|
||||
self.cart.purchase()
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_key))
|
||||
# check that the registration codes are generated against the order
|
||||
self.assertEqual(len(CourseRegistrationCode.objects.filter(order=self.cart)), item.qty)
|
||||
registration_codes = CourseRegistrationCode.order_generated_registration_codes(self.course_key)
|
||||
self.assertEqual(registration_codes.count(), item.qty)
|
||||
|
||||
def test_regcode_redemptions(self):
|
||||
"""
|
||||
@@ -480,7 +603,7 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase):
|
||||
CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2)
|
||||
self.cart.purchase()
|
||||
|
||||
reg_code = CourseRegistrationCode.objects.filter(order=self.cart)[0]
|
||||
reg_code = CourseRegistrationCode.order_generated_registration_codes(self.course_key)[0]
|
||||
|
||||
enrollment = CourseEnrollment.enroll(self.user, self.course_key)
|
||||
|
||||
@@ -505,7 +628,7 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase):
|
||||
CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2)
|
||||
self.cart.purchase()
|
||||
|
||||
reg_codes = CourseRegistrationCode.objects.filter(order=self.cart)
|
||||
reg_codes = CourseRegistrationCode.order_generated_registration_codes(self.course_key)
|
||||
|
||||
self.assertEqual(len(reg_codes), 2)
|
||||
|
||||
@@ -984,10 +1107,34 @@ class InvoiceHistoryTest(TestCase):
|
||||
super(InvoiceHistoryTest, self).setUp()
|
||||
invoice_data = copy.copy(self.INVOICE_INFO)
|
||||
invoice_data.update(self.CONTACT_INFO)
|
||||
self.invoice = Invoice.objects.create(total_amount="123.45", **invoice_data)
|
||||
self.course_key = CourseLocator('edX', 'DemoX', 'Demo_Course')
|
||||
self.invoice = Invoice.objects.create(total_amount="123.45", course_id=self.course_key, **invoice_data)
|
||||
self.user = UserFactory.create()
|
||||
|
||||
def test_get_invoice_total_amount(self):
|
||||
"""
|
||||
test to check the total amount
|
||||
of the invoices for the course.
|
||||
"""
|
||||
total_amount = Invoice.get_invoice_total_amount_for_course(self.course_key)
|
||||
self.assertEqual(total_amount, 123.45)
|
||||
|
||||
def test_get_total_amount_of_paid_invoices(self):
|
||||
"""
|
||||
Test to check the Invoice Transactions amount.
|
||||
"""
|
||||
InvoiceTransaction.objects.create(
|
||||
invoice=self.invoice,
|
||||
amount='123.45',
|
||||
currency='usd',
|
||||
comments='test comments',
|
||||
status='completed',
|
||||
created_by=self.user,
|
||||
last_modified_by=self.user
|
||||
)
|
||||
total_amount_paid = InvoiceTransaction.get_total_amount_of_paid_course_invoices(self.course_key)
|
||||
self.assertEqual(float(total_amount_paid), 123.45)
|
||||
|
||||
def test_invoice_contact_info_history(self):
|
||||
self._assert_history_invoice_info(
|
||||
is_valid=True,
|
||||
@@ -998,6 +1145,30 @@ class InvoiceHistoryTest(TestCase):
|
||||
self._assert_history_items([])
|
||||
self._assert_history_transactions([])
|
||||
|
||||
def test_invoice_generated_registration_codes(self):
|
||||
"""
|
||||
test filter out the registration codes
|
||||
that were generated via Invoice.
|
||||
"""
|
||||
invoice_item = CourseRegistrationCodeInvoiceItem.objects.create(
|
||||
invoice=self.invoice,
|
||||
qty=5,
|
||||
unit_price='123.45',
|
||||
course_id=self.course_key
|
||||
)
|
||||
for i in range(5):
|
||||
CourseRegistrationCode.objects.create(
|
||||
code='testcode{counter}'.format(counter=i),
|
||||
course_id=self.course_key,
|
||||
created_by=self.user,
|
||||
invoice=self.invoice,
|
||||
invoice_item=invoice_item,
|
||||
mode_slug='honor'
|
||||
)
|
||||
|
||||
registration_codes = CourseRegistrationCode.invoice_generated_registration_codes(self.course_key)
|
||||
self.assertEqual(registration_codes.count(), 5)
|
||||
|
||||
def test_invoice_history_items(self):
|
||||
# Create an invoice item
|
||||
CourseRegistrationCodeInvoiceItem.objects.create(
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<%! 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>
|
||||
Reference in New Issue
Block a user