Response to CR
This commit is contained in:
@@ -580,6 +580,11 @@ class CourseEnrollment(models.Model):
|
||||
courseenrollment__is_active=True
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def enrollments_in(cls, course_id):
|
||||
"""Return a queryset of CourseEnrollment for every active enrollment in the course."""
|
||||
return cls.objects.filter(course_id=course_id, is_active=True,)
|
||||
|
||||
def activate(self):
|
||||
"""Makes this `CourseEnrollment` record active. Saves immediately."""
|
||||
self.update_enrollment(is_active=True)
|
||||
|
||||
@@ -10,7 +10,6 @@ from boto.exception import BotoServerError # this is a super-class of SESError
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.mail import send_mail
|
||||
@@ -19,14 +18,11 @@ from django.utils.translation import ugettext as _
|
||||
from django.db import transaction
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from courseware.courses import get_course_by_id
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from student.views import course_from_id
|
||||
from student.models import CourseEnrollment, unenroll_done
|
||||
@@ -34,8 +30,7 @@ from student.models import CourseEnrollment, unenroll_done
|
||||
from verify_student.models import SoftwareSecurePhotoVerification
|
||||
|
||||
from .exceptions import (InvalidCartItem, PurchasedCallbackException, ItemAlreadyInCartException,
|
||||
AlreadyEnrolledInCourseException, CourseDoesNotExistException, ReportException,
|
||||
ReportTypeDoesNotExistException)
|
||||
AlreadyEnrolledInCourseException, CourseDoesNotExistException, ReportException)
|
||||
|
||||
log = logging.getLogger("shoppingcart")
|
||||
|
||||
@@ -45,6 +40,8 @@ ORDER_STATUSES = (
|
||||
('refunded', 'refunded'),
|
||||
)
|
||||
|
||||
|
||||
|
||||
# we need a tuple to represent the primary key of various OrderItem subclasses
|
||||
OrderItemSubclassPK = namedtuple('OrderItemSubclassPK', ['cls', 'pk']) # pylint: disable=C0103
|
||||
|
||||
@@ -571,255 +568,3 @@ class CertificateItem(OrderItem):
|
||||
"Please include your order number in your e-mail. "
|
||||
"Please do NOT include your credit card information.").format(
|
||||
billing_email=settings.PAYMENT_SUPPORT_EMAIL)
|
||||
|
||||
|
||||
class Report(models.Model):
|
||||
"""
|
||||
Base class for making CSV reports related to revenue, enrollments, etc
|
||||
|
||||
To make a different type of report, write a new subclass that implements
|
||||
the methods get_query, csv_report_header_row, and csv_report_row.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def initialize_report(cls, report_type):
|
||||
"""
|
||||
Creates the appropriate type of Report object based on the string report_type.
|
||||
"""
|
||||
if report_type == "refund_report":
|
||||
return RefundReport()
|
||||
elif report_type == "itemized_purchase_report":
|
||||
return ItemizedPurchaseReport()
|
||||
elif report_type == "university_revenue_share":
|
||||
return UniversityRevenueShareReport()
|
||||
elif report_type == "certificate_status":
|
||||
return CertificateStatusReport()
|
||||
else:
|
||||
raise ReportTypeDoesNotExistException
|
||||
|
||||
def get_query(self, start_date, end_date):
|
||||
"""
|
||||
Performs any database queries necessary to obtain the data for the report.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def csv_report_header_row(self):
|
||||
"""
|
||||
Returns the appropriate header based on the report type.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def csv_report_row(self, item):
|
||||
"""
|
||||
Given the results of the query from get_query, this function generates a single row of a csv.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def make_report(cls, report_type, filelike, start_date, end_date):
|
||||
"""
|
||||
Given the string report_type, a file object to write to, and start/end date bounds,
|
||||
generates a CSV report of the appropriate type.
|
||||
"""
|
||||
report = cls.initialize_report(report_type)
|
||||
items = report.get_query(start_date, end_date)
|
||||
writer = unicodecsv.writer(filelike, encoding="utf-8")
|
||||
writer.writerow(report.csv_report_header_row())
|
||||
for item in items:
|
||||
writer.writerow(report.csv_report_row(item))
|
||||
|
||||
|
||||
class RefundReport(Report):
|
||||
"""
|
||||
Subclass of Report, used to generate Refund Reports for finance purposes.
|
||||
"""
|
||||
def get_query(self, start_date, end_date):
|
||||
return CertificateItem.objects.filter(
|
||||
status="refunded",
|
||||
refund_requested_time__gte=start_date,
|
||||
refund_requested_time__lt=end_date,
|
||||
)
|
||||
|
||||
def csv_report_header_row(self):
|
||||
return [
|
||||
"Order Number",
|
||||
"Customer Name",
|
||||
"Date of Original Transaction",
|
||||
"Date of Refund",
|
||||
"Amount of Refund",
|
||||
"Service Fees (if any)",
|
||||
]
|
||||
|
||||
def csv_report_row(self, item):
|
||||
return [
|
||||
item.order_id,
|
||||
item.user.get_full_name(),
|
||||
item.fulfilled_time,
|
||||
item.refund_requested_time, # TODO Change this torefund_fulfilled once we start recording that value
|
||||
item.line_cost,
|
||||
item.service_fee,
|
||||
]
|
||||
|
||||
|
||||
class ItemizedPurchaseReport(Report):
|
||||
"""
|
||||
Subclass of Report, used to generate itemized purchase reports.
|
||||
"""
|
||||
def get_query(self, start_date, end_date):
|
||||
return OrderItem.objects.filter(
|
||||
status="purchased",
|
||||
fulfilled_time__gte=start_date,
|
||||
fulfilled_time__lt=end_date,
|
||||
).order_by("fulfilled_time")
|
||||
|
||||
def csv_report_header_row(self):
|
||||
return [
|
||||
"Purchase Time",
|
||||
"Order ID",
|
||||
"Status",
|
||||
"Quantity",
|
||||
"Unit Cost",
|
||||
"Total Cost",
|
||||
"Currency",
|
||||
"Description",
|
||||
"Comments"
|
||||
]
|
||||
|
||||
def csv_report_row(self, item):
|
||||
return [
|
||||
item.fulfilled_time,
|
||||
item.order_id, # pylint: disable=no-member
|
||||
item.status,
|
||||
item.qty,
|
||||
item.unit_cost,
|
||||
item.line_cost,
|
||||
item.currency,
|
||||
item.line_desc,
|
||||
item.report_comments,
|
||||
]
|
||||
|
||||
|
||||
class CertificateStatusReport(Report):
|
||||
"""
|
||||
Subclass of Report, used to generate Certificate Status Reports for ed services.
|
||||
"""
|
||||
def get_query(self, start_date, end_date):
|
||||
results = []
|
||||
for course_id in settings.COURSE_LISTINGS['default']:
|
||||
cur_course = get_course_by_id(course_id)
|
||||
university = cur_course.org
|
||||
course = cur_course.number + " " + cur_course.display_name # TODO add term (i.e. Fall 2013)?
|
||||
enrollments = CourseEnrollment.objects.filter(course_id=course_id,
|
||||
is_active=True,)
|
||||
total_enrolled = enrollments.count()
|
||||
audit_enrolled = enrollments.filter(mode="audit").count()
|
||||
honor_enrolled = enrollments.filter(mode="honor").count()
|
||||
# Since every verified enrollment has 1 and only 1 cert item, let's just query those
|
||||
verified_enrollments = CertificateItem.objects.filter(course_id=course_id, mode="verified", status="purchased")
|
||||
verified_enrolled = verified_enrollments.count()
|
||||
gross_rev_temp = CertificateItem.objects.filter(course_id=course_id, mode="verified", status="purchased").aggregate(Sum('unit_cost'))
|
||||
gross_rev = gross_rev_temp['unit_cost__sum']
|
||||
gross_rev_over_min = gross_rev - (CourseMode.objects.get(course_id=course_id, mode_slug="verified").min_price * verified_enrolled)
|
||||
refunded_enrollments = CertificateItem.objects.filter(course_id='course_id', mode="verified", status="refunded")
|
||||
number_of_refunds = refunded_enrollments.count()
|
||||
dollars_refunded_temp = refunded_enrollments.aggregate(Sum('unit_cost'))
|
||||
if dollars_refunded_temp['unit_cost__sum'] is None:
|
||||
dollars_refunded = Decimal(0.00)
|
||||
else:
|
||||
dollars_refunded = dollars_refunded_temp['unit_cost__sum']
|
||||
|
||||
result = [
|
||||
university,
|
||||
course,
|
||||
total_enrolled,
|
||||
audit_enrolled,
|
||||
honor_enrolled,
|
||||
verified_enrolled,
|
||||
gross_rev,
|
||||
gross_rev_over_min,
|
||||
number_of_refunds,
|
||||
dollars_refunded
|
||||
]
|
||||
|
||||
results.append(result)
|
||||
return results
|
||||
|
||||
def csv_report_header_row(self):
|
||||
return [
|
||||
"University",
|
||||
"Course",
|
||||
"Total Enrolled",
|
||||
"Audit Enrollment",
|
||||
"Honor Code Enrollment",
|
||||
"Verified Enrollment",
|
||||
"Gross Revenue",
|
||||
"Gross Revenue over the Minimum",
|
||||
"Number of Refunds",
|
||||
"Dollars Refunded",
|
||||
]
|
||||
|
||||
def csv_report_row(self, item):
|
||||
return item
|
||||
|
||||
|
||||
class UniversityRevenueShareReport(Report):
|
||||
"""
|
||||
Subclass of Report, used to generate University Revenue Share Reports for finance purposes.
|
||||
"""
|
||||
def get_query(self, start_date, end_date):
|
||||
results = []
|
||||
for course_id in settings.COURSE_LISTINGS['default']:
|
||||
cur_course = get_course_by_id(course_id)
|
||||
university = cur_course.org
|
||||
course = cur_course.number + " " + cur_course.display_name
|
||||
num_transactions = 0 # TODO clarify with billing what transactions are included in this (purchases? refunds? etc)
|
||||
|
||||
all_paid_certs = CertificateItem.objects.filter(course_id=course_id, status="purchased")
|
||||
|
||||
total_payments_collected_temp = all_paid_certs.aggregate(Sum('unit_cost'))
|
||||
if total_payments_collected_temp['unit_cost__sum'] is None:
|
||||
total_payments_collected = Decimal(0.00)
|
||||
else:
|
||||
total_payments_collected = total_payments_collected_temp['unit_cost__sum']
|
||||
|
||||
total_service_fees_temp = all_paid_certs.aggregate(Sum('service_fee'))
|
||||
if total_service_fees_temp['service_fee__sum'] is None:
|
||||
service_fees = Decimal(0.00)
|
||||
else:
|
||||
service_fees = total_service_fees_temp['service_fee__sum']
|
||||
|
||||
refunded_enrollments = CertificateItem.objects.filter(course_id=course_id, status="refunded")
|
||||
num_refunds = refunded_enrollments.count()
|
||||
|
||||
amount_refunds_temp = refunded_enrollments.aggregate(Sum('unit_cost'))
|
||||
if amount_refunds_temp['unit_cost__sum'] is None:
|
||||
amount_refunds = Decimal(0.00)
|
||||
else:
|
||||
amount_refunds = amount_refunds_temp['unit_cost__sum']
|
||||
|
||||
result = [
|
||||
university,
|
||||
course,
|
||||
num_transactions,
|
||||
total_payments_collected,
|
||||
service_fees,
|
||||
num_refunds,
|
||||
amount_refunds
|
||||
]
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
def csv_report_header_row(self):
|
||||
return [
|
||||
"University",
|
||||
"Course",
|
||||
"Number of Transactions",
|
||||
"Total Payments Collected",
|
||||
"Service Fees (if any)",
|
||||
"Number of Successful Refunds",
|
||||
"Total Amount of Refunds",
|
||||
]
|
||||
|
||||
def csv_report_row(self, item):
|
||||
return item
|
||||
|
||||
249
lms/djangoapps/shoppingcart/reports.py
Normal file
249
lms/djangoapps/shoppingcart/reports.py
Normal file
@@ -0,0 +1,249 @@
|
||||
from shoppingcart.models import CertificateItem, OrderItem
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
import unicodecsv
|
||||
from django.conf import settings
|
||||
from courseware.courses import get_course_by_id
|
||||
from student.models import CourseEnrollment
|
||||
from course_modes.models import CourseMode
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class Report(models.Model):
|
||||
"""
|
||||
Base class for making CSV reports related to revenue, enrollments, etc
|
||||
|
||||
To make a different type of report, write a new subclass that implements
|
||||
the methods get_report_data, csv_report_header_row, and csv_report_row.
|
||||
"""
|
||||
|
||||
def get_report_data(self, start_date, end_date, start_letter=None, end_letter=None):
|
||||
"""
|
||||
Performs database queries necessary for the report. May return either a query result
|
||||
or a list of lists, depending on the particular type of report--see Report subclasses
|
||||
for sample implementations.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def csv_report_header_row(self):
|
||||
"""
|
||||
Returns the appropriate header based on the report type.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def csv_report_row(self, item):
|
||||
"""
|
||||
Given the results of get_report_data, this function generates a single row of a csv.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def make_report(self, filelike, start_date, end_date, start_letter=None, end_letter=None):
|
||||
"""
|
||||
Given the string report_type, a file object to write to, and start/end date bounds,
|
||||
generates a CSV report of the appropriate type.
|
||||
"""
|
||||
items = self.get_report_data(start_date, end_date, start_letter, end_letter)
|
||||
writer = unicodecsv.writer(filelike, encoding="utf-8")
|
||||
writer.writerow(self.csv_report_header_row())
|
||||
for item in items:
|
||||
writer.writerow(self.csv_report_row(item))
|
||||
|
||||
|
||||
class RefundReport(Report):
|
||||
"""
|
||||
Subclass of Report, used to generate Refund Reports for finance purposes.
|
||||
"""
|
||||
def get_report_data(self, start_date, end_date, start_letter=None, end_letter=None):
|
||||
return CertificateItem.objects.filter(
|
||||
status="refunded",
|
||||
refund_requested_time__gte=start_date,
|
||||
refund_requested_time__lt=end_date,
|
||||
)
|
||||
|
||||
def csv_report_header_row(self):
|
||||
return [
|
||||
"Order Number",
|
||||
"Customer Name",
|
||||
"Date of Original Transaction",
|
||||
"Date of Refund",
|
||||
"Amount of Refund",
|
||||
"Service Fees (if any)",
|
||||
]
|
||||
|
||||
def csv_report_row(self, item):
|
||||
return [
|
||||
item.order_id,
|
||||
item.user.get_full_name(),
|
||||
item.fulfilled_time,
|
||||
item.refund_requested_time, # TODO Change this torefund_fulfilled once we start recording that value
|
||||
item.line_cost,
|
||||
item.service_fee,
|
||||
]
|
||||
|
||||
|
||||
class ItemizedPurchaseReport(Report):
|
||||
"""
|
||||
Subclass of Report, used to generate itemized purchase reports.
|
||||
"""
|
||||
def get_report_data(self, start_date, end_date, start_letter=None, end_letter=None):
|
||||
return OrderItem.objects.filter(
|
||||
status="purchased",
|
||||
fulfilled_time__gte=start_date,
|
||||
fulfilled_time__lt=end_date,
|
||||
).order_by("fulfilled_time")
|
||||
|
||||
def csv_report_header_row(self):
|
||||
return [
|
||||
"Purchase Time",
|
||||
"Order ID",
|
||||
"Status",
|
||||
"Quantity",
|
||||
"Unit Cost",
|
||||
"Total Cost",
|
||||
"Currency",
|
||||
"Description",
|
||||
"Comments"
|
||||
]
|
||||
|
||||
def csv_report_row(self, item):
|
||||
return [
|
||||
item.fulfilled_time,
|
||||
item.order_id, # pylint: disable=no-member
|
||||
item.status,
|
||||
item.qty,
|
||||
item.unit_cost,
|
||||
item.line_cost,
|
||||
item.currency,
|
||||
item.line_desc,
|
||||
item.report_comments,
|
||||
]
|
||||
|
||||
|
||||
class CertificateStatusReport(Report):
|
||||
"""
|
||||
Subclass of Report, used to generate Certificate Status Reports for ed services.
|
||||
"""
|
||||
def get_report_data(self, start_date, end_date, start_letter=None, end_letter=None):
|
||||
results = []
|
||||
for course_id in settings.COURSE_LISTINGS['default']:
|
||||
if (start_letter.lower() <= course_id.lower()) and (end_letter.lower() >= course_id.lower()) and (get_course_by_id(course_id) is not None):
|
||||
cur_course = get_course_by_id(course_id)
|
||||
university = cur_course.org
|
||||
course = cur_course.number + " " + cur_course.display_name # TODO add term (i.e. Fall 2013)?
|
||||
enrollments = CourseEnrollment.enrollments_in(course_id)
|
||||
total_enrolled = enrollments.count()
|
||||
audit_enrolled = enrollments.filter(mode="audit").count()
|
||||
honor_enrolled = enrollments.filter(mode="honor").count()
|
||||
# Since every verified enrollment has 1 and only 1 cert item, let's just query those
|
||||
verified_enrollments = CertificateItem.objects.filter(course_id=course_id, mode="verified", status="purchased")
|
||||
verified_enrolled = verified_enrollments.count()
|
||||
gross_rev_temp = CertificateItem.objects.filter(course_id=course_id, mode="verified", status="purchased").aggregate(Sum('unit_cost'))
|
||||
gross_rev = gross_rev_temp['unit_cost__sum']
|
||||
gross_rev_over_min = gross_rev - (CourseMode.objects.get(course_id=course_id, mode_slug="verified").min_price * verified_enrolled)
|
||||
refunded_enrollments = CertificateItem.objects.filter(course_id='course_id', mode="verified", status="refunded")
|
||||
number_of_refunds = refunded_enrollments.count()
|
||||
dollars_refunded_temp = refunded_enrollments.aggregate(Sum('unit_cost'))
|
||||
if dollars_refunded_temp['unit_cost__sum'] is None:
|
||||
dollars_refunded = Decimal(0.00)
|
||||
else:
|
||||
dollars_refunded = dollars_refunded_temp['unit_cost__sum']
|
||||
|
||||
result = [
|
||||
university,
|
||||
course,
|
||||
total_enrolled,
|
||||
audit_enrolled,
|
||||
honor_enrolled,
|
||||
verified_enrolled,
|
||||
gross_rev,
|
||||
gross_rev_over_min,
|
||||
number_of_refunds,
|
||||
dollars_refunded
|
||||
]
|
||||
|
||||
results.append(result)
|
||||
return results
|
||||
|
||||
def csv_report_header_row(self):
|
||||
return [
|
||||
"University",
|
||||
"Course",
|
||||
"Total Enrolled",
|
||||
"Audit Enrollment",
|
||||
"Honor Code Enrollment",
|
||||
"Verified Enrollment",
|
||||
"Gross Revenue",
|
||||
"Gross Revenue over the Minimum",
|
||||
"Number of Refunds",
|
||||
"Dollars Refunded",
|
||||
]
|
||||
|
||||
def csv_report_row(self, item):
|
||||
return item
|
||||
|
||||
|
||||
class UniversityRevenueShareReport(Report):
|
||||
"""
|
||||
Subclass of Report, used to generate University Revenue Share Reports for finance purposes.
|
||||
"""
|
||||
def get_report_data(self, start_date, end_date, start_letter=None, end_letter=None):
|
||||
results = []
|
||||
for course_id in settings.COURSE_LISTINGS['default']:
|
||||
if (start_letter.lower() <= course_id.lower()) and (end_letter.lower() >= course_id.lower()):
|
||||
try:
|
||||
cur_course = get_course_by_id(course_id)
|
||||
except:
|
||||
break
|
||||
university = cur_course.org
|
||||
course = cur_course.number + " " + cur_course.display_name
|
||||
num_transactions = 0 # TODO clarify with billing what transactions are included in this (purchases? refunds? etc)
|
||||
|
||||
all_paid_certs = CertificateItem.objects.filter(course_id=course_id, status="purchased")
|
||||
|
||||
total_payments_collected_temp = all_paid_certs.aggregate(Sum('unit_cost'))
|
||||
if total_payments_collected_temp['unit_cost__sum'] is None:
|
||||
total_payments_collected = Decimal(0.00)
|
||||
else:
|
||||
total_payments_collected = total_payments_collected_temp['unit_cost__sum']
|
||||
|
||||
total_service_fees_temp = all_paid_certs.aggregate(Sum('service_fee'))
|
||||
if total_service_fees_temp['service_fee__sum'] is None:
|
||||
service_fees = Decimal(0.00)
|
||||
else:
|
||||
service_fees = total_service_fees_temp['service_fee__sum']
|
||||
|
||||
refunded_enrollments = CertificateItem.objects.filter(course_id=course_id, status="refunded")
|
||||
num_refunds = refunded_enrollments.count()
|
||||
|
||||
amount_refunds_temp = refunded_enrollments.aggregate(Sum('unit_cost'))
|
||||
if amount_refunds_temp['unit_cost__sum'] is None:
|
||||
amount_refunds = Decimal(0.00)
|
||||
else:
|
||||
amount_refunds = amount_refunds_temp['unit_cost__sum']
|
||||
|
||||
result = [
|
||||
university,
|
||||
course,
|
||||
num_transactions,
|
||||
total_payments_collected,
|
||||
service_fees,
|
||||
num_refunds,
|
||||
amount_refunds
|
||||
]
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
def csv_report_header_row(self):
|
||||
return [
|
||||
"University",
|
||||
"Course",
|
||||
"Number of Transactions",
|
||||
"Total Payments Collected",
|
||||
"Service Fees (if any)",
|
||||
"Number of Successful Refunds",
|
||||
"Total Amount of Refunds",
|
||||
]
|
||||
|
||||
def csv_report_row(self, item):
|
||||
return item
|
||||
@@ -17,14 +17,31 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from shoppingcart.models import (Order, OrderItem, CertificateItem, InvalidCartItem, PaidCourseRegistration,
|
||||
OrderItemSubclassPK, PaidCourseRegistrationAnnotation, Report)
|
||||
OrderItemSubclassPK, PaidCourseRegistrationAnnotation)
|
||||
from shoppingcart.reports import ItemizedPurchaseReport, CertificateStatusReport, UniversityRevenueShareReport, RefundReport
|
||||
from student.tests.factories import UserFactory
|
||||
from student.models import CourseEnrollment
|
||||
from course_modes.models import CourseMode
|
||||
from shoppingcart.exceptions import PurchasedCallbackException
|
||||
from shoppingcart.exceptions import PurchasedCallbackException, ReportTypeDoesNotExistException
|
||||
import pytz
|
||||
import datetime
|
||||
|
||||
REPORT_TYPES = [
|
||||
("refund_report", RefundReport),
|
||||
("itemized_purchase_report", ItemizedPurchaseReport),
|
||||
("university_revenue_share", UniversityRevenueShareReport),
|
||||
("certificate_status", CertificateStatusReport),
|
||||
]
|
||||
|
||||
|
||||
def initialize_report(report_type):
|
||||
"""
|
||||
Creates the appropriate type of Report object based on the string report_type.
|
||||
"""
|
||||
for item in REPORT_TYPES:
|
||||
if report_type in item:
|
||||
return item[1]()
|
||||
raise ReportTypeDoesNotExistException
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
class OrderTest(ModuleStoreTestCase):
|
||||
@@ -355,13 +372,12 @@ class ItemizedPurchaseReportTest(ModuleStoreTestCase):
|
||||
self.now = datetime.datetime.now(pytz.UTC)
|
||||
|
||||
def test_purchased_items_btw_dates(self):
|
||||
report_type = "itemized_purchase_report"
|
||||
report = Report.initialize_report(report_type)
|
||||
purchases = report.get_query(self.now - self.FIVE_MINS, self.now + self.FIVE_MINS)
|
||||
report = initialize_report("itemized_purchase_report")
|
||||
purchases = report.get_report_data(self.now - self.FIVE_MINS, self.now + self.FIVE_MINS)
|
||||
self.assertEqual(len(purchases), 2)
|
||||
self.assertIn(self.reg.orderitem_ptr, purchases)
|
||||
self.assertIn(self.cert_item.orderitem_ptr, purchases)
|
||||
no_purchases = report.get_query(self.now + self.FIVE_MINS, self.now + self.FIVE_MINS + self.FIVE_MINS)
|
||||
no_purchases = report.get_report_data(self.now + self.FIVE_MINS, self.now + self.FIVE_MINS + self.FIVE_MINS)
|
||||
self.assertFalse(no_purchases)
|
||||
|
||||
test_time = datetime.datetime.now(pytz.UTC)
|
||||
@@ -376,14 +392,13 @@ class ItemizedPurchaseReportTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests that a generated purchase report CSV is as we expect
|
||||
"""
|
||||
report_type = "itemized_purchase_report"
|
||||
report = Report.initialize_report(report_type)
|
||||
for item in report.get_query(self.now - self.FIVE_MINS, self.now + self.FIVE_MINS):
|
||||
report = initialize_report("itemized_purchase_report")
|
||||
for item in report.get_report_data(self.now - self.FIVE_MINS, self.now + self.FIVE_MINS):
|
||||
item.fulfilled_time = self.test_time
|
||||
item.save()
|
||||
|
||||
csv_file = StringIO.StringIO()
|
||||
Report.make_report(report_type, csv_file, self.now - self.FIVE_MINS, self.now + self.FIVE_MINS)
|
||||
report.make_report(csv_file, self.now - self.FIVE_MINS, self.now + self.FIVE_MINS)
|
||||
csv = csv_file.getvalue()
|
||||
csv_file.close()
|
||||
# Using excel mode csv, which automatically ends lines with \r\n, so need to convert to \n
|
||||
@@ -405,163 +420,6 @@ class ItemizedPurchaseReportTest(ModuleStoreTestCase):
|
||||
self.assertEqual(unicode(self.annotation), u'{} : {}'.format(self.course_id, self.TEST_ANNOTATION))
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
class ReportTypeTests(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the models used to generate certificate status reports
|
||||
"""
|
||||
FIVE_MINS = datetime.timedelta(minutes=5)
|
||||
|
||||
def setUp(self):
|
||||
# Need to make a *lot* of users for this one
|
||||
self.user1 = UserFactory.create()
|
||||
self.user1.first_name = "John"
|
||||
self.user1.last_name = "Doe"
|
||||
self.user1.save()
|
||||
|
||||
self.user2 = UserFactory.create()
|
||||
self.user2.first_name = "Jane"
|
||||
self.user2.last_name = "Deer"
|
||||
self.user2.save()
|
||||
|
||||
self.user3 = UserFactory.create()
|
||||
self.user3.first_name = "Joe"
|
||||
self.user3.last_name = "Miller"
|
||||
self.user3.save()
|
||||
|
||||
self.user4 = UserFactory.create()
|
||||
self.user4.first_name = "Simon"
|
||||
self.user4.last_name = "Blackquill"
|
||||
self.user4.save()
|
||||
|
||||
self.user5 = UserFactory.create()
|
||||
self.user5.first_name = "Super"
|
||||
self.user5.last_name = "Mario"
|
||||
self.user5.save()
|
||||
|
||||
self.user6 = UserFactory.create()
|
||||
self.user6.first_name = "Princess"
|
||||
self.user6.last_name = "Peach"
|
||||
self.user6.save()
|
||||
|
||||
self.user7 = UserFactory.create()
|
||||
self.user7.first_name = "King"
|
||||
self.user7.last_name = "Bowser"
|
||||
self.user7.save()
|
||||
|
||||
self.user8 = UserFactory.create()
|
||||
self.user8.first_name = "Susan"
|
||||
self.user8.last_name = "Smith"
|
||||
self.user8.save()
|
||||
|
||||
# Two are verified, three are audit, one honor
|
||||
|
||||
self.course_id = "MITx/999/Robot_Super_Course"
|
||||
settings.COURSE_LISTINGS['default'] = [self.course_id]
|
||||
self.cost = 40
|
||||
self.course = CourseFactory.create(org='MITx', number='999', display_name=u'Robot Super Course')
|
||||
course_mode = CourseMode(course_id=self.course_id,
|
||||
mode_slug="honor",
|
||||
mode_display_name="honor cert",
|
||||
min_price=self.cost)
|
||||
course_mode.save()
|
||||
|
||||
course_mode2 = CourseMode(course_id=self.course_id,
|
||||
mode_slug="verified",
|
||||
mode_display_name="verified cert",
|
||||
min_price=self.cost)
|
||||
course_mode2.save()
|
||||
|
||||
# User 1 & 2 will be verified
|
||||
self.cart1 = Order.get_cart_for_user(self.user1)
|
||||
CertificateItem.add_to_order(self.cart1, self.course_id, self.cost, 'verified')
|
||||
self.cart1.purchase()
|
||||
|
||||
self.cart2 = Order.get_cart_for_user(self.user2)
|
||||
CertificateItem.add_to_order(self.cart2, self.course_id, self.cost, 'verified')
|
||||
self.cart2.purchase()
|
||||
|
||||
# Users 3, 4, and 5 are audit
|
||||
CourseEnrollment.enroll(self.user3, self.course_id, "audit")
|
||||
CourseEnrollment.enroll(self.user4, self.course_id, "audit")
|
||||
CourseEnrollment.enroll(self.user5, self.course_id, "audit")
|
||||
|
||||
# User 6 is honor
|
||||
CourseEnrollment.enroll(self.user6, self.course_id, "honor")
|
||||
|
||||
self.now = datetime.datetime.now(pytz.UTC)
|
||||
|
||||
# Users 7 & 8 are refunds
|
||||
self.cart = Order.get_cart_for_user(self.user7)
|
||||
CertificateItem.add_to_order(self.cart, self.course_id, self.cost, 'verified')
|
||||
self.cart.purchase()
|
||||
CourseEnrollment.unenroll(self.user7, self.course_id)
|
||||
|
||||
self.cart = Order.get_cart_for_user(self.user8)
|
||||
CertificateItem.add_to_order(self.cart, self.course_id, self.cost, 'verified')
|
||||
self.cart.purchase(self.user8, self.course_id)
|
||||
CourseEnrollment.unenroll(self.user8, self.course_id)
|
||||
|
||||
self.test_time = datetime.datetime.now(pytz.UTC)
|
||||
self.CORRECT_REFUND_REPORT_CSV = dedent("""
|
||||
Order Number,Customer Name,Date of Original Transaction,Date of Refund,Amount of Refund,Service Fees (if any)
|
||||
3,King Bowser,{time_str},{time_str},40,0
|
||||
4,Susan Smith,{time_str},{time_str},40,0
|
||||
""".format(time_str=str(self.test_time)))
|
||||
|
||||
self.CORRECT_CERT_STATUS_CSV = dedent("""
|
||||
University,Course,Total Enrolled,Audit Enrollment,Honor Code Enrollment,Verified Enrollment,Gross Revenue,Gross Revenue over the Minimum,Number of Refunds,Dollars Refunded
|
||||
MITx,999 Robot Super Course,6,3,1,2,80.00,0.00,0,0
|
||||
""".format(time_str=str(self.test_time)))
|
||||
|
||||
self.CORRECT_UNI_REVENUE_SHARE_CSV = dedent("""
|
||||
University,Course,Number of Transactions,Total Payments Collected,Service Fees (if any),Number of Successful Refunds,Total Amount of Refunds
|
||||
MITx,999 Robot Super Course,0,80.00,0.00,2,80.00
|
||||
""".format(time_str=str(self.test_time)))
|
||||
|
||||
def test_refund_report_get_query(self):
|
||||
report_type = "refund_report"
|
||||
report = Report.initialize_report(report_type)
|
||||
refunded_certs = report.get_query(self.now - self.FIVE_MINS, self.now + self.FIVE_MINS)
|
||||
self.assertEqual(len(refunded_certs), 2)
|
||||
self.assertTrue(CertificateItem.objects.get(user=self.user7, course_id=self.course_id))
|
||||
self.assertTrue(CertificateItem.objects.get(user=self.user8, course_id=self.course_id))
|
||||
|
||||
def test_refund_report_purchased_csv(self):
|
||||
"""
|
||||
Tests that a generated purchase report CSV is as we expect
|
||||
"""
|
||||
report_type = "refund_report"
|
||||
report = Report.initialize_report(report_type)
|
||||
for item in report.get_query(self.now - self.FIVE_MINS, self.now + self.FIVE_MINS):
|
||||
item.fulfilled_time = self.test_time
|
||||
item.refund_requested_time = self.test_time # hm do we want to make these different
|
||||
item.save()
|
||||
|
||||
csv_file = StringIO.StringIO()
|
||||
Report.make_report(report_type, csv_file, self.now - self.FIVE_MINS, self.now + self.FIVE_MINS)
|
||||
csv = csv_file.getvalue()
|
||||
csv_file.close()
|
||||
# Using excel mode csv, which automatically ends lines with \r\n, so need to convert to \n
|
||||
self.assertEqual(csv.replace('\r\n', '\n').strip(), self.CORRECT_REFUND_REPORT_CSV.strip())
|
||||
|
||||
def test_basic_cert_status_csv(self):
|
||||
report_type = "certificate_status"
|
||||
report = Report.initialize_report(report_type)
|
||||
csv_file = StringIO.StringIO()
|
||||
report.make_report(report_type, csv_file, self.now - self.FIVE_MINS, self.now + self.FIVE_MINS)
|
||||
csv = csv_file.getvalue()
|
||||
self.assertEqual(csv.replace('\r\n', '\n').strip(), self.CORRECT_CERT_STATUS_CSV.strip())
|
||||
|
||||
def test_basic_uni_revenue_share_csv(self):
|
||||
report_type = "university_revenue_share"
|
||||
report = Report.initialize_report(report_type)
|
||||
csv_file = StringIO.StringIO()
|
||||
report.make_report(report_type, csv_file, self.now - self.FIVE_MINS, self.now + self.FIVE_MINS)
|
||||
csv = csv_file.getvalue()
|
||||
self.assertEqual(csv.replace('\r\n', '\n').strip(), self.CORRECT_UNI_REVENUE_SHARE_CSV.strip())
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
class CertificateItemTest(ModuleStoreTestCase):
|
||||
"""
|
||||
|
||||
179
lms/djangoapps/shoppingcart/tests/test_reports.py
Normal file
179
lms/djangoapps/shoppingcart/tests/test_reports.py
Normal file
@@ -0,0 +1,179 @@
|
||||
"""
|
||||
Tests for the Shopping Cart Models
|
||||
"""
|
||||
import StringIO
|
||||
from textwrap import dedent
|
||||
|
||||
from django.conf import settings
|
||||
from django.test.utils import override_settings
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from shoppingcart.models import (Order, CertificateItem)
|
||||
from shoppingcart.reports import ItemizedPurchaseReport, CertificateStatusReport, UniversityRevenueShareReport, RefundReport
|
||||
from student.tests.factories import UserFactory
|
||||
from student.models import CourseEnrollment
|
||||
from course_modes.models import CourseMode
|
||||
from shoppingcart.views import initialize_report
|
||||
import pytz
|
||||
import datetime
|
||||
|
||||
REPORT_TYPES = [
|
||||
("refund_report", RefundReport),
|
||||
("itemized_purchase_report", ItemizedPurchaseReport),
|
||||
("university_revenue_share", UniversityRevenueShareReport),
|
||||
("certificate_status", CertificateStatusReport),
|
||||
]
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
class ReportTypeTests(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the models used to generate certificate status reports
|
||||
"""
|
||||
FIVE_MINS = datetime.timedelta(minutes=5)
|
||||
|
||||
def setUp(self):
|
||||
# Need to make a *lot* of users for this one
|
||||
self.user1 = UserFactory.create()
|
||||
self.user1.first_name = "John"
|
||||
self.user1.last_name = "Doe"
|
||||
self.user1.save()
|
||||
|
||||
self.user2 = UserFactory.create()
|
||||
self.user2.first_name = "Jane"
|
||||
self.user2.last_name = "Deer"
|
||||
self.user2.save()
|
||||
|
||||
self.user3 = UserFactory.create()
|
||||
self.user3.first_name = "Joe"
|
||||
self.user3.last_name = "Miller"
|
||||
self.user3.save()
|
||||
|
||||
self.user4 = UserFactory.create()
|
||||
self.user4.first_name = "Simon"
|
||||
self.user4.last_name = "Blackquill"
|
||||
self.user4.save()
|
||||
|
||||
self.user5 = UserFactory.create()
|
||||
self.user5.first_name = "Super"
|
||||
self.user5.last_name = "Mario"
|
||||
self.user5.save()
|
||||
|
||||
self.user6 = UserFactory.create()
|
||||
self.user6.first_name = "Princess"
|
||||
self.user6.last_name = "Peach"
|
||||
self.user6.save()
|
||||
|
||||
self.user7 = UserFactory.create()
|
||||
self.user7.first_name = "King"
|
||||
self.user7.last_name = "Bowser"
|
||||
self.user7.save()
|
||||
|
||||
self.user8 = UserFactory.create()
|
||||
self.user8.first_name = "Susan"
|
||||
self.user8.last_name = "Smith"
|
||||
self.user8.save()
|
||||
|
||||
# Two are verified, three are audit, one honor
|
||||
|
||||
self.course_id = "MITx/999/Robot_Super_Course"
|
||||
settings.COURSE_LISTINGS['default'] = [self.course_id]
|
||||
self.cost = 40
|
||||
self.course = CourseFactory.create(org='MITx', number='999', display_name=u'Robot Super Course')
|
||||
course_mode = CourseMode(course_id=self.course_id,
|
||||
mode_slug="honor",
|
||||
mode_display_name="honor cert",
|
||||
min_price=self.cost)
|
||||
course_mode.save()
|
||||
|
||||
course_mode2 = CourseMode(course_id=self.course_id,
|
||||
mode_slug="verified",
|
||||
mode_display_name="verified cert",
|
||||
min_price=self.cost)
|
||||
course_mode2.save()
|
||||
|
||||
# User 1 & 2 will be verified
|
||||
self.cart1 = Order.get_cart_for_user(self.user1)
|
||||
CertificateItem.add_to_order(self.cart1, self.course_id, self.cost, 'verified')
|
||||
self.cart1.purchase()
|
||||
|
||||
self.cart2 = Order.get_cart_for_user(self.user2)
|
||||
CertificateItem.add_to_order(self.cart2, self.course_id, self.cost, 'verified')
|
||||
self.cart2.purchase()
|
||||
|
||||
# Users 3, 4, and 5 are audit
|
||||
CourseEnrollment.enroll(self.user3, self.course_id, "audit")
|
||||
CourseEnrollment.enroll(self.user4, self.course_id, "audit")
|
||||
CourseEnrollment.enroll(self.user5, self.course_id, "audit")
|
||||
|
||||
# User 6 is honor
|
||||
CourseEnrollment.enroll(self.user6, self.course_id, "honor")
|
||||
|
||||
self.now = datetime.datetime.now(pytz.UTC)
|
||||
|
||||
# Users 7 & 8 are refunds
|
||||
self.cart = Order.get_cart_for_user(self.user7)
|
||||
CertificateItem.add_to_order(self.cart, self.course_id, self.cost, 'verified')
|
||||
self.cart.purchase()
|
||||
CourseEnrollment.unenroll(self.user7, self.course_id)
|
||||
|
||||
self.cart = Order.get_cart_for_user(self.user8)
|
||||
CertificateItem.add_to_order(self.cart, self.course_id, self.cost, 'verified')
|
||||
self.cart.purchase(self.user8, self.course_id)
|
||||
CourseEnrollment.unenroll(self.user8, self.course_id)
|
||||
|
||||
self.test_time = datetime.datetime.now(pytz.UTC)
|
||||
self.CORRECT_REFUND_REPORT_CSV = dedent("""
|
||||
Order Number,Customer Name,Date of Original Transaction,Date of Refund,Amount of Refund,Service Fees (if any)
|
||||
3,King Bowser,{time_str},{time_str},40,0
|
||||
4,Susan Smith,{time_str},{time_str},40,0
|
||||
""".format(time_str=str(self.test_time)))
|
||||
|
||||
self.CORRECT_CERT_STATUS_CSV = dedent("""
|
||||
University,Course,Total Enrolled,Audit Enrollment,Honor Code Enrollment,Verified Enrollment,Gross Revenue,Gross Revenue over the Minimum,Number of Refunds,Dollars Refunded
|
||||
MITx,999 Robot Super Course,6,3,1,2,80.00,0.00,0,0
|
||||
""".format(time_str=str(self.test_time)))
|
||||
|
||||
self.CORRECT_UNI_REVENUE_SHARE_CSV = dedent("""
|
||||
University,Course,Number of Transactions,Total Payments Collected,Service Fees (if any),Number of Successful Refunds,Total Amount of Refunds
|
||||
MITx,999 Robot Super Course,0,80.00,0.00,2,80.00
|
||||
""".format(time_str=str(self.test_time)))
|
||||
|
||||
def test_refund_report_get_report_data(self):
|
||||
report = initialize_report("refund_report")
|
||||
refunded_certs = report.get_report_data(self.now - self.FIVE_MINS, self.now + self.FIVE_MINS)
|
||||
self.assertEqual(len(refunded_certs), 2)
|
||||
self.assertTrue(CertificateItem.objects.get(user=self.user7, course_id=self.course_id))
|
||||
self.assertTrue(CertificateItem.objects.get(user=self.user8, course_id=self.course_id))
|
||||
|
||||
def test_refund_report_purchased_csv(self):
|
||||
"""
|
||||
Tests that a generated purchase report CSV is as we expect
|
||||
"""
|
||||
report = initialize_report("refund_report")
|
||||
for item in report.get_report_data(self.now - self.FIVE_MINS, self.now + self.FIVE_MINS):
|
||||
item.fulfilled_time = self.test_time
|
||||
item.refund_requested_time = self.test_time # hm do we want to make these different
|
||||
item.save()
|
||||
|
||||
csv_file = StringIO.StringIO()
|
||||
report.make_report(csv_file, self.now - self.FIVE_MINS, self.now + self.FIVE_MINS)
|
||||
csv = csv_file.getvalue()
|
||||
csv_file.close()
|
||||
# Using excel mode csv, which automatically ends lines with \r\n, so need to convert to \n
|
||||
self.assertEqual(csv.replace('\r\n', '\n').strip(), self.CORRECT_REFUND_REPORT_CSV.strip())
|
||||
|
||||
def test_basic_cert_status_csv(self):
|
||||
report = initialize_report("certificate_status")
|
||||
csv_file = StringIO.StringIO()
|
||||
report.make_report(csv_file, self.now - self.FIVE_MINS, self.now + self.FIVE_MINS, 'A', 'Z')
|
||||
csv = csv_file.getvalue()
|
||||
self.assertEqual(csv.replace('\r\n', '\n').strip(), self.CORRECT_CERT_STATUS_CSV.strip())
|
||||
|
||||
def test_basic_uni_revenue_share_csv(self):
|
||||
report = initialize_report("university_revenue_share")
|
||||
csv_file = StringIO.StringIO()
|
||||
report.make_report(csv_file, self.now - self.FIVE_MINS, self.now + self.FIVE_MINS, 'A', 'Z')
|
||||
csv = csv_file.getvalue()
|
||||
self.assertEqual(csv.replace('\r\n', '\n').strip(), self.CORRECT_UNI_REVENUE_SHARE_CSV.strip())
|
||||
@@ -14,13 +14,15 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from shoppingcart.views import _can_download_report, _get_date_from_str
|
||||
from shoppingcart.models import Order, CertificateItem, PaidCourseRegistration, Report
|
||||
from shoppingcart.models import Order, CertificateItem, PaidCourseRegistration
|
||||
from student.tests.factories import UserFactory
|
||||
from student.models import CourseEnrollment
|
||||
from course_modes.models import CourseMode
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from shoppingcart.processors import render_purchase_form_html
|
||||
from mock import patch, Mock, sentinel
|
||||
from shoppingcart.reports import ItemizedPurchaseReport
|
||||
from shoppingcart.views import initialize_report
|
||||
|
||||
|
||||
def mock_render_purchase_form_html(*args, **kwargs):
|
||||
@@ -354,7 +356,6 @@ class CSVReportViewsTest(ModuleStoreTestCase):
|
||||
def test_report_csv_bad_date(self):
|
||||
self.login_user()
|
||||
self.add_to_download_group(self.user)
|
||||
report_type = "itemized_purchase_report"
|
||||
response = self.client.post(reverse('payment_csv_report'), {'start_date': 'BAD', 'end_date': 'BAD', 'requested_report': 'itemized_purchase_report'})
|
||||
|
||||
((template, context), unused_kwargs) = render_mock.call_args
|
||||
@@ -386,7 +387,7 @@ class CSVReportViewsTest(ModuleStoreTestCase):
|
||||
|
||||
CORRECT_CSV_NO_DATE_ITEMIZED_PURCHASE = ",1,purchased,1,40,40,usd,Registration for Course: Robot Super Course,"
|
||||
|
||||
def test_report_csv(self):
|
||||
def test_report_csv_itemized(self):
|
||||
report_type = 'itemized_purchase_report'
|
||||
PaidCourseRegistration.add_to_order(self.cart, self.course_id)
|
||||
self.cart.purchase()
|
||||
@@ -396,10 +397,24 @@ class CSVReportViewsTest(ModuleStoreTestCase):
|
||||
'end_date': '2100-01-01',
|
||||
'requested_report': report_type})
|
||||
self.assertEqual(response['Content-Type'], 'text/csv')
|
||||
report = Report.initialize_report(report_type)
|
||||
report = initialize_report(report_type)
|
||||
self.assertIn(",".join(report.csv_report_header_row()), response.content)
|
||||
self.assertIn(self.CORRECT_CSV_NO_DATE_ITEMIZED_PURCHASE, response.content)
|
||||
|
||||
def test_report_csv_university_revenue_share(self):
|
||||
report_type = 'university_revenue_share'
|
||||
self.login_user()
|
||||
self.add_to_download_group(self.user)
|
||||
response = self.client.post(reverse('payment_csv_report'), {'start_date': '1970-01-01',
|
||||
'end_date': '2100-01-01',
|
||||
'start_letter': 'A',
|
||||
'end_letter': 'Z',
|
||||
'requested_report': report_type})
|
||||
self.assertEqual(response['Content-Type'], 'text/csv')
|
||||
report = initialize_report(report_type)
|
||||
self.assertIn(",".join(report.csv_report_header_row()), response.content)
|
||||
# TODO add another test here
|
||||
|
||||
|
||||
class UtilFnsTest(TestCase):
|
||||
"""
|
||||
|
||||
@@ -12,14 +12,31 @@ from django.views.decorators.csrf import csrf_exempt
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from student.models import CourseEnrollment
|
||||
from .models import Order, PaidCourseRegistration, OrderItem, Report
|
||||
from shoppingcart.reports import RefundReport, ItemizedPurchaseReport, UniversityRevenueShareReport, CertificateStatusReport
|
||||
from .models import Order, PaidCourseRegistration, OrderItem
|
||||
from .processors import process_postpay_callback, render_purchase_form_html
|
||||
from .exceptions import ItemAlreadyInCartException, AlreadyEnrolledInCourseException, CourseDoesNotExistException
|
||||
from .exceptions import ItemAlreadyInCartException, AlreadyEnrolledInCourseException, CourseDoesNotExistException, ReportTypeDoesNotExistException
|
||||
|
||||
log = logging.getLogger("shoppingcart")
|
||||
|
||||
EVENT_NAME_USER_UPGRADED = 'edx.course.enrollment.upgrade.succeeded'
|
||||
|
||||
REPORT_TYPES = [
|
||||
("refund_report", RefundReport),
|
||||
("itemized_purchase_report", ItemizedPurchaseReport),
|
||||
("university_revenue_share", UniversityRevenueShareReport),
|
||||
("certificate_status", CertificateStatusReport),
|
||||
]
|
||||
|
||||
|
||||
def initialize_report(report_type):
|
||||
"""
|
||||
Creates the appropriate type of Report object based on the string report_type.
|
||||
"""
|
||||
for item in REPORT_TYPES:
|
||||
if report_type in item:
|
||||
return item[1]()
|
||||
raise ReportTypeDoesNotExistException
|
||||
|
||||
@require_POST
|
||||
def add_course_to_cart(request, course_id):
|
||||
@@ -155,7 +172,7 @@ def _get_date_from_str(date_input):
|
||||
return datetime.datetime.strptime(date_input.strip(), "%Y-%m-%d").replace(tzinfo=pytz.UTC)
|
||||
|
||||
|
||||
def _render_report_form(start_str, end_str, report_type, total_count_error=False, date_fmt_error=False):
|
||||
def _render_report_form(start_str, end_str, start_letter, end_letter, report_type, total_count_error=False, date_fmt_error=False):
|
||||
"""
|
||||
Helper function that renders the purchase form. Reduces repetition
|
||||
"""
|
||||
@@ -164,6 +181,8 @@ def _render_report_form(start_str, end_str, report_type, total_count_error=False
|
||||
'date_fmt_error': date_fmt_error,
|
||||
'start_date': start_str,
|
||||
'end_date': end_str,
|
||||
'start_letter': start_letter,
|
||||
'end_letter': end_letter,
|
||||
'requested_report': report_type,
|
||||
}
|
||||
return render_to_response('shoppingcart/download_report.html', context)
|
||||
@@ -178,34 +197,44 @@ def csv_report(request):
|
||||
if not _can_download_report(request.user):
|
||||
return HttpResponseForbidden(_('You do not have permission to view this page.'))
|
||||
|
||||
# TODO temp filler for start letter, end letter
|
||||
|
||||
if request.method == 'POST':
|
||||
start_str = request.POST.get('start_date', '')
|
||||
end_str = request.POST.get('end_date', '')
|
||||
start_letter = request.POST.get('start_letter', '')
|
||||
end_letter = request.POST.get('end_letter', '')
|
||||
report_type = request.POST.get('requested_report', '')
|
||||
try:
|
||||
start_date = _get_date_from_str(start_str)
|
||||
start_date = _get_date_from_str(start_str) + datetime.timedelta(days=0)
|
||||
end_date = _get_date_from_str(end_str) + datetime.timedelta(days=1)
|
||||
except ValueError:
|
||||
# Error case: there was a badly formatted user-input date string
|
||||
return _render_report_form(start_str, end_str, report_type, date_fmt_error=True)
|
||||
return _render_report_form(start_str, end_str, start_letter, end_letter, report_type, date_fmt_error=True)
|
||||
|
||||
report = Report.initialize_report(report_type)
|
||||
items = report.get_query(start_date, end_date)
|
||||
if items.count() > settings.PAYMENT_REPORT_MAX_ITEMS:
|
||||
report = initialize_report(report_type)
|
||||
items = report.get_report_data(start_date, end_date, start_letter, end_letter)
|
||||
|
||||
# TODO add this back later as a query-est function or something
|
||||
try:
|
||||
if items.count() > settings.PAYMENT_REPORT_MAX_ITEMS:
|
||||
# Error case: too many items would be generated in the report and we're at risk of timeout
|
||||
return _render_report_form(start_str, end_str, report_type, total_count_error=True)
|
||||
return _render_report_form(start_str, end_str, start_letter, end_letter, report_type, total_count_error=True)
|
||||
except:
|
||||
pass
|
||||
|
||||
response = HttpResponse(mimetype='text/csv')
|
||||
filename = "purchases_report_{}.csv".format(datetime.datetime.now(pytz.UTC).strftime("%Y-%m-%d-%H-%M-%S"))
|
||||
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
|
||||
# this flos is a little odd; what's up with report_type being called twice? check later
|
||||
report.make_report(report_type, response, start_date, end_date)
|
||||
report.make_report(response, start_date, end_date, start_letter, end_letter)
|
||||
return response
|
||||
|
||||
elif request.method == 'GET':
|
||||
end_date = datetime.datetime.now(pytz.UTC)
|
||||
start_date = end_date - datetime.timedelta(days=30)
|
||||
return _render_report_form(start_date.strftime("%Y-%m-%d"), end_date.strftime("%Y-%m-%d"), report_type="")
|
||||
start_letter = ""
|
||||
end_letter = ""
|
||||
return _render_report_form(start_date.strftime("%Y-%m-%d"), end_date.strftime("%Y-%m-%d"), start_letter, end_letter, report_type="")
|
||||
|
||||
else:
|
||||
return HttpResponseBadRequest("HTTP Method Not Supported")
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
<input id="start_date" type="text" value="${start_date}" name="start_date"/>
|
||||
<label for="end_date">${_("End Date: ")}</label>
|
||||
<input id="end_date" type="text" value="${end_date}" name="end_date"/>
|
||||
<label for="start_letter">${_("Start Letter: ")}</label>
|
||||
<input id="start_letter" type="text" value="${start_letter}" name="start_letter"/>
|
||||
<label for="end_letter">${_("End Letter: ")}</label>
|
||||
<input id="end_letter" type="text" value="${end_letter}" name="end_letter"/>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}" />
|
||||
<br/>
|
||||
<button type = "submit" name="requested_report" value="itemized_purchase_report">Itemized Purchase Report</button>
|
||||
|
||||
Reference in New Issue
Block a user