Added columns to the CC purchases report. (added Qty and Total Discount. Moved the Total Amount to the last index). Coupon code report.
454 lines
22 KiB
Python
454 lines
22 KiB
Python
"""
|
|
Tests for instructor.basic
|
|
"""
|
|
|
|
import json
|
|
from student.models import CourseEnrollment, CourseEnrollmentAllowed
|
|
from django.core.urlresolvers import reverse
|
|
from mock import patch
|
|
from student.roles import CourseSalesAdminRole
|
|
from student.tests.factories import UserFactory, CourseModeFactory
|
|
from shoppingcart.models import (
|
|
CourseRegistrationCode, RegistrationCodeRedemption, Order,
|
|
Invoice, Coupon, CourseRegCodeItem, CouponRedemption, CourseRegistrationCodeInvoiceItem
|
|
)
|
|
from course_modes.models import CourseMode
|
|
from instructor_analytics.basic import (
|
|
sale_record_features, sale_order_record_features, enrolled_students_features,
|
|
course_registration_features, coupon_codes_features, list_may_enroll,
|
|
AVAILABLE_FEATURES, STUDENT_FEATURES, PROFILE_FEATURES
|
|
)
|
|
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
|
|
from courseware.tests.factories import InstructorFactory
|
|
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
|
from xmodule.modulestore.tests.factories import CourseFactory
|
|
|
|
import datetime
|
|
from django.db.models import Q
|
|
import pytz
|
|
|
|
|
|
class TestAnalyticsBasic(ModuleStoreTestCase):
|
|
""" Test basic analytics functions. """
|
|
|
|
def setUp(self):
|
|
super(TestAnalyticsBasic, self).setUp()
|
|
self.course_key = self.store.make_course_key('robot', 'course', 'id')
|
|
self.users = tuple(UserFactory() for _ in xrange(30))
|
|
self.ces = tuple(CourseEnrollment.enroll(user, self.course_key)
|
|
for user in self.users)
|
|
self.instructor = InstructorFactory(course_key=self.course_key)
|
|
for user in self.users:
|
|
user.profile.meta = json.dumps({
|
|
"position": "edX expert {}".format(user.id),
|
|
"company": "Open edX Inc {}".format(user.id),
|
|
})
|
|
user.profile.save()
|
|
self.students_who_may_enroll = list(self.users) + [UserFactory() for _ in range(5)]
|
|
for student in self.students_who_may_enroll:
|
|
CourseEnrollmentAllowed.objects.create(
|
|
email=student.email, course_id=self.course_key
|
|
)
|
|
|
|
def test_enrolled_students_features_username(self):
|
|
self.assertIn('username', AVAILABLE_FEATURES)
|
|
userreports = enrolled_students_features(self.course_key, ['username'])
|
|
self.assertEqual(len(userreports), len(self.users))
|
|
for userreport in userreports:
|
|
self.assertEqual(userreport.keys(), ['username'])
|
|
self.assertIn(userreport['username'], [user.username for user in self.users])
|
|
|
|
def test_enrolled_students_features_keys(self):
|
|
query_features = ('username', 'name', 'email')
|
|
for feature in query_features:
|
|
self.assertIn(feature, AVAILABLE_FEATURES)
|
|
with self.assertNumQueries(1):
|
|
userreports = enrolled_students_features(self.course_key, query_features)
|
|
self.assertEqual(len(userreports), len(self.users))
|
|
for userreport in userreports:
|
|
self.assertEqual(set(userreport.keys()), set(query_features))
|
|
self.assertIn(userreport['username'], [user.username for user in self.users])
|
|
self.assertIn(userreport['email'], [user.email for user in self.users])
|
|
self.assertIn(userreport['name'], [user.profile.name for user in self.users])
|
|
|
|
def test_enrolled_students_meta_features_keys(self):
|
|
"""
|
|
Assert that we can query individual fields in the 'meta' field in the UserProfile
|
|
"""
|
|
query_features = ('meta.position', 'meta.company')
|
|
with self.assertNumQueries(1):
|
|
userreports = enrolled_students_features(self.course_key, query_features)
|
|
self.assertEqual(len(userreports), len(self.users))
|
|
for userreport in userreports:
|
|
self.assertEqual(set(userreport.keys()), set(query_features))
|
|
self.assertIn(userreport['meta.position'], ["edX expert {}".format(user.id) for user in self.users])
|
|
self.assertIn(userreport['meta.company'], ["Open edX Inc {}".format(user.id) for user in self.users])
|
|
|
|
def test_enrolled_students_features_keys_cohorted(self):
|
|
course = CourseFactory.create(org="test", course="course1", display_name="run1")
|
|
course.cohort_config = {'cohorted': True, 'auto_cohort': True, 'auto_cohort_groups': ['cohort']}
|
|
self.store.update_item(course, self.instructor.id)
|
|
cohort = CohortFactory.create(name='cohort', course_id=course.id)
|
|
cohorted_students = [UserFactory.create() for _ in xrange(10)]
|
|
cohorted_usernames = [student.username for student in cohorted_students]
|
|
non_cohorted_student = UserFactory.create()
|
|
for student in cohorted_students:
|
|
cohort.users.add(student)
|
|
CourseEnrollment.enroll(student, course.id)
|
|
CourseEnrollment.enroll(non_cohorted_student, course.id)
|
|
instructor = InstructorFactory(course_key=course.id)
|
|
self.client.login(username=instructor.username, password='test')
|
|
|
|
query_features = ('username', 'cohort')
|
|
# There should be a constant of 2 SQL queries when calling
|
|
# enrolled_students_features. The first query comes from the call to
|
|
# User.objects.filter(...), and the second comes from
|
|
# prefetch_related('course_groups').
|
|
with self.assertNumQueries(2):
|
|
userreports = enrolled_students_features(course.id, query_features)
|
|
self.assertEqual(len([r for r in userreports if r['username'] in cohorted_usernames]), len(cohorted_students))
|
|
self.assertEqual(len([r for r in userreports if r['username'] == non_cohorted_student.username]), 1)
|
|
for report in userreports:
|
|
self.assertEqual(set(report.keys()), set(query_features))
|
|
if report['username'] in cohorted_usernames:
|
|
self.assertEqual(report['cohort'], cohort.name)
|
|
else:
|
|
self.assertEqual(report['cohort'], '[unassigned]')
|
|
|
|
def test_available_features(self):
|
|
self.assertEqual(len(AVAILABLE_FEATURES), len(STUDENT_FEATURES + PROFILE_FEATURES))
|
|
self.assertEqual(set(AVAILABLE_FEATURES), set(STUDENT_FEATURES + PROFILE_FEATURES))
|
|
|
|
def test_list_may_enroll(self):
|
|
may_enroll = list_may_enroll(self.course_key, ['email'])
|
|
self.assertEqual(len(may_enroll), len(self.students_who_may_enroll) - len(self.users))
|
|
email_adresses = [student.email for student in self.students_who_may_enroll]
|
|
for student in may_enroll:
|
|
self.assertEqual(student.keys(), ['email'])
|
|
self.assertIn(student['email'], email_adresses)
|
|
|
|
|
|
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
|
|
class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase):
|
|
""" Test basic course sale records analytics functions. """
|
|
def setUp(self):
|
|
"""
|
|
Fixtures.
|
|
"""
|
|
super(TestCourseSaleRecordsAnalyticsBasic, self).setUp()
|
|
self.course = CourseFactory.create()
|
|
self.cost = 40
|
|
self.course_mode = CourseMode(
|
|
course_id=self.course.id, mode_slug="honor",
|
|
mode_display_name="honor cert", min_price=self.cost
|
|
)
|
|
self.course_mode.save()
|
|
self.instructor = InstructorFactory(course_key=self.course.id)
|
|
self.client.login(username=self.instructor.username, password='test')
|
|
|
|
def test_course_sale_features(self):
|
|
|
|
query_features = [
|
|
'company_name', 'company_contact_name', 'company_contact_email', 'total_codes', 'total_used_codes',
|
|
'total_amount', 'created_at', 'customer_reference_number', 'recipient_name', 'recipient_email',
|
|
'created_by', 'internal_reference', 'invoice_number', 'codes', 'course_id'
|
|
]
|
|
|
|
#create invoice
|
|
sale_invoice = Invoice.objects.create(
|
|
total_amount=1234.32, company_name='Test1', company_contact_name='TestName',
|
|
company_contact_email='test@company.com', recipient_name='Testw_1', recipient_email='test2@test.com',
|
|
customer_reference_number='2Fwe23S', internal_reference="ABC", course_id=self.course.id
|
|
)
|
|
invoice_item = CourseRegistrationCodeInvoiceItem.objects.create(
|
|
invoice=sale_invoice,
|
|
qty=1,
|
|
unit_price=1234.32,
|
|
course_id=self.course.id
|
|
)
|
|
for i in range(5):
|
|
course_code = CourseRegistrationCode(
|
|
code="test_code{}".format(i), course_id=self.course.id.to_deprecated_string(),
|
|
created_by=self.instructor, invoice=sale_invoice, invoice_item=invoice_item, mode_slug='honor'
|
|
)
|
|
course_code.save()
|
|
|
|
course_sale_records_list = sale_record_features(self.course.id, query_features)
|
|
|
|
for sale_record in course_sale_records_list:
|
|
self.assertEqual(sale_record['total_amount'], sale_invoice.total_amount)
|
|
self.assertEqual(sale_record['recipient_email'], sale_invoice.recipient_email)
|
|
self.assertEqual(sale_record['recipient_name'], sale_invoice.recipient_name)
|
|
self.assertEqual(sale_record['company_name'], sale_invoice.company_name)
|
|
self.assertEqual(sale_record['company_contact_name'], sale_invoice.company_contact_name)
|
|
self.assertEqual(sale_record['company_contact_email'], sale_invoice.company_contact_email)
|
|
self.assertEqual(sale_record['internal_reference'], sale_invoice.internal_reference)
|
|
self.assertEqual(sale_record['customer_reference_number'], sale_invoice.customer_reference_number)
|
|
self.assertEqual(sale_record['invoice_number'], sale_invoice.id)
|
|
self.assertEqual(sale_record['created_by'], self.instructor)
|
|
self.assertEqual(sale_record['total_used_codes'], 0)
|
|
self.assertEqual(sale_record['total_codes'], 5)
|
|
|
|
def test_sale_order_features_with_discount(self):
|
|
"""
|
|
Test Order Sales Report CSV
|
|
"""
|
|
query_features = [
|
|
('id', 'Order Id'),
|
|
('company_name', 'Company Name'),
|
|
('company_contact_name', 'Company Contact Name'),
|
|
('company_contact_email', 'Company Contact Email'),
|
|
('total_amount', 'Total Amount'),
|
|
('total_codes', 'Total Codes'),
|
|
('total_used_codes', 'Total Used Codes'),
|
|
('logged_in_username', 'Login Username'),
|
|
('logged_in_email', 'Login User Email'),
|
|
('purchase_time', 'Date of Sale'),
|
|
('customer_reference_number', 'Customer Reference Number'),
|
|
('recipient_name', 'Recipient Name'),
|
|
('recipient_email', 'Recipient Email'),
|
|
('bill_to_street1', 'Street 1'),
|
|
('bill_to_street2', 'Street 2'),
|
|
('bill_to_city', 'City'),
|
|
('bill_to_state', 'State'),
|
|
('bill_to_postalcode', 'Postal Code'),
|
|
('bill_to_country', 'Country'),
|
|
('order_type', 'Order Type'),
|
|
('status', 'Order Item Status'),
|
|
('coupon_code', 'Coupon Code'),
|
|
('unit_cost', 'Unit Price'),
|
|
('list_price', 'List Price'),
|
|
('codes', 'Registration Codes'),
|
|
('course_id', 'Course Id')
|
|
]
|
|
# add the coupon code for the course
|
|
coupon = Coupon(
|
|
code='test_code',
|
|
description='test_description',
|
|
course_id=self.course.id,
|
|
percentage_discount='10',
|
|
created_by=self.instructor,
|
|
is_active=True
|
|
)
|
|
coupon.save()
|
|
order = Order.get_cart_for_user(self.instructor)
|
|
order.order_type = 'business'
|
|
order.save()
|
|
order.add_billing_details(
|
|
company_name='Test Company',
|
|
company_contact_name='Test',
|
|
company_contact_email='test@123',
|
|
recipient_name='R1', recipient_email='',
|
|
customer_reference_number='PO#23'
|
|
)
|
|
CourseRegCodeItem.add_to_order(order, self.course.id, 4)
|
|
# apply the coupon code to the item in the cart
|
|
resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': coupon.code})
|
|
self.assertEqual(resp.status_code, 200)
|
|
order.purchase()
|
|
|
|
# get the updated item
|
|
item = order.orderitem_set.all().select_subclasses()[0]
|
|
# get the redeemed coupon information
|
|
coupon_redemption = CouponRedemption.objects.select_related('coupon').filter(order=order)
|
|
|
|
db_columns = [x[0] for x in query_features]
|
|
sale_order_records_list = sale_order_record_features(self.course.id, db_columns)
|
|
|
|
for sale_order_record in sale_order_records_list:
|
|
self.assertEqual(sale_order_record['recipient_email'], order.recipient_email)
|
|
self.assertEqual(sale_order_record['recipient_name'], order.recipient_name)
|
|
self.assertEqual(sale_order_record['company_name'], order.company_name)
|
|
self.assertEqual(sale_order_record['company_contact_name'], order.company_contact_name)
|
|
self.assertEqual(sale_order_record['company_contact_email'], order.company_contact_email)
|
|
self.assertEqual(sale_order_record['customer_reference_number'], order.customer_reference_number)
|
|
self.assertEqual(sale_order_record['unit_cost'], item.unit_cost)
|
|
self.assertEqual(sale_order_record['list_price'], item.list_price)
|
|
self.assertEqual(sale_order_record['status'], item.status)
|
|
self.assertEqual(sale_order_record['coupon_code'], coupon_redemption[0].coupon.code)
|
|
|
|
def test_sale_order_features_without_discount(self):
|
|
"""
|
|
Test Order Sales Report CSV
|
|
"""
|
|
query_features = [
|
|
('id', 'Order Id'),
|
|
('company_name', 'Company Name'),
|
|
('company_contact_name', 'Company Contact Name'),
|
|
('company_contact_email', 'Company Contact Email'),
|
|
('total_amount', 'Total Amount'),
|
|
('total_codes', 'Total Codes'),
|
|
('total_used_codes', 'Total Used Codes'),
|
|
('logged_in_username', 'Login Username'),
|
|
('logged_in_email', 'Login User Email'),
|
|
('purchase_time', 'Date of Sale'),
|
|
('customer_reference_number', 'Customer Reference Number'),
|
|
('recipient_name', 'Recipient Name'),
|
|
('recipient_email', 'Recipient Email'),
|
|
('bill_to_street1', 'Street 1'),
|
|
('bill_to_street2', 'Street 2'),
|
|
('bill_to_city', 'City'),
|
|
('bill_to_state', 'State'),
|
|
('bill_to_postalcode', 'Postal Code'),
|
|
('bill_to_country', 'Country'),
|
|
('order_type', 'Order Type'),
|
|
('status', 'Order Item Status'),
|
|
('coupon_code', 'Coupon Code'),
|
|
('unit_cost', 'Unit Price'),
|
|
('list_price', 'List Price'),
|
|
('codes', 'Registration Codes'),
|
|
('course_id', 'Course Id'),
|
|
('quantity', 'Quantity'),
|
|
('total_discount', 'Total Discount'),
|
|
('total_amount', 'Total Amount Paid'),
|
|
]
|
|
# add the coupon code for the course
|
|
order = Order.get_cart_for_user(self.instructor)
|
|
order.order_type = 'business'
|
|
order.save()
|
|
order.add_billing_details(
|
|
company_name='Test Company',
|
|
company_contact_name='Test',
|
|
company_contact_email='test@123',
|
|
recipient_name='R1', recipient_email='',
|
|
customer_reference_number='PO#23'
|
|
)
|
|
CourseRegCodeItem.add_to_order(order, self.course.id, 4)
|
|
order.purchase()
|
|
|
|
# get the updated item
|
|
item = order.orderitem_set.all().select_subclasses()[0]
|
|
|
|
db_columns = [x[0] for x in query_features]
|
|
sale_order_records_list = sale_order_record_features(self.course.id, db_columns)
|
|
|
|
for sale_order_record in sale_order_records_list:
|
|
self.assertEqual(sale_order_record['recipient_email'], order.recipient_email)
|
|
self.assertEqual(sale_order_record['recipient_name'], order.recipient_name)
|
|
self.assertEqual(sale_order_record['company_name'], order.company_name)
|
|
self.assertEqual(sale_order_record['company_contact_name'], order.company_contact_name)
|
|
self.assertEqual(sale_order_record['company_contact_email'], order.company_contact_email)
|
|
self.assertEqual(sale_order_record['customer_reference_number'], order.customer_reference_number)
|
|
self.assertEqual(sale_order_record['unit_cost'], item.unit_cost)
|
|
# Make sure list price is not None and matches the unit price since no discount was applied.
|
|
self.assertIsNotNone(sale_order_record['list_price'])
|
|
self.assertEqual(sale_order_record['list_price'], item.unit_cost)
|
|
self.assertEqual(sale_order_record['status'], item.status)
|
|
self.assertEqual(sale_order_record['coupon_code'], 'N/A')
|
|
self.assertEqual(sale_order_record['total_amount'], item.unit_cost * item.qty)
|
|
self.assertEqual(sale_order_record['total_discount'], 0)
|
|
self.assertEqual(sale_order_record['quantity'], item.qty)
|
|
|
|
|
|
class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase):
|
|
""" Test basic course registration codes analytics functions. """
|
|
|
|
def setUp(self):
|
|
"""
|
|
Fixtures.
|
|
"""
|
|
super(TestCourseRegistrationCodeAnalyticsBasic, self).setUp()
|
|
self.course = CourseFactory.create()
|
|
self.instructor = InstructorFactory(course_key=self.course.id)
|
|
self.client.login(username=self.instructor.username, password='test')
|
|
CourseSalesAdminRole(self.course.id).add_users(self.instructor)
|
|
|
|
# Create a paid course mode.
|
|
mode = CourseModeFactory.create()
|
|
mode.course_id = self.course.id
|
|
mode.min_price = 1
|
|
mode.save()
|
|
|
|
url = reverse('generate_registration_codes',
|
|
kwargs={'course_id': self.course.id.to_deprecated_string()})
|
|
|
|
data = {
|
|
'total_registration_codes': 12, 'company_name': 'Test Group', 'unit_price': 122.45,
|
|
'company_contact_name': 'TestName', 'company_contact_email': 'test@company.com', 'recipient_name': 'Test123',
|
|
'recipient_email': 'test@123.com', 'address_line_1': 'Portland Street', 'address_line_2': '',
|
|
'address_line_3': '', 'city': '', 'state': '', 'zip': '', 'country': '',
|
|
'customer_reference_number': '123A23F', 'internal_reference': '', 'invoice': ''
|
|
}
|
|
|
|
response = self.client.post(url, data, **{'HTTP_HOST': 'localhost'})
|
|
self.assertEqual(response.status_code, 200, response.content)
|
|
|
|
def test_course_registration_features(self):
|
|
query_features = [
|
|
'code', 'redeem_code_url', 'course_id', 'company_name', 'created_by',
|
|
'redeemed_by', 'invoice_id', 'purchaser', 'customer_reference_number', 'internal_reference'
|
|
]
|
|
order = Order(user=self.instructor, status='purchased')
|
|
order.save()
|
|
|
|
registration_code_redemption = RegistrationCodeRedemption(
|
|
registration_code_id=1, redeemed_by=self.instructor
|
|
)
|
|
registration_code_redemption.save()
|
|
registration_codes = CourseRegistrationCode.objects.all()
|
|
course_registration_list = course_registration_features(query_features, registration_codes, csv_type='download')
|
|
self.assertEqual(len(course_registration_list), len(registration_codes))
|
|
for course_registration in course_registration_list:
|
|
self.assertEqual(set(course_registration.keys()), set(query_features))
|
|
self.assertIn(course_registration['code'], [registration_code.code for registration_code in registration_codes])
|
|
self.assertIn(
|
|
course_registration['course_id'],
|
|
[registration_code.course_id.to_deprecated_string() for registration_code in registration_codes]
|
|
)
|
|
self.assertIn(
|
|
course_registration['company_name'],
|
|
[
|
|
getattr(registration_code.invoice_item.invoice, 'company_name')
|
|
for registration_code in registration_codes
|
|
]
|
|
)
|
|
self.assertIn(
|
|
course_registration['invoice_id'],
|
|
[
|
|
registration_code.invoice_item.invoice_id
|
|
for registration_code in registration_codes
|
|
]
|
|
)
|
|
|
|
def test_coupon_codes_features(self):
|
|
query_features = [
|
|
'course_id', 'percentage_discount', 'code_redeemed_count', 'description', 'expiration_date',
|
|
'total_discounted_amount', 'total_discounted_seats'
|
|
]
|
|
for i in range(10):
|
|
coupon = Coupon(
|
|
code='test_code{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()
|
|
#now create coupons with the expiration dates
|
|
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,
|
|
expiration_date=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=2)
|
|
)
|
|
coupon.save()
|
|
|
|
active_coupons = Coupon.objects.filter(
|
|
Q(course_id=self.course.id),
|
|
Q(is_active=True),
|
|
Q(expiration_date__gt=datetime.datetime.now(pytz.UTC)) |
|
|
Q(expiration_date__isnull=True)
|
|
)
|
|
active_coupons_list = coupon_codes_features(query_features, active_coupons, self.course.id)
|
|
self.assertEqual(len(active_coupons_list), len(active_coupons))
|
|
for active_coupon in active_coupons_list:
|
|
self.assertEqual(set(active_coupon.keys()), set(query_features))
|
|
self.assertIn(active_coupon['percentage_discount'], [coupon.percentage_discount for coupon in active_coupons])
|
|
self.assertIn(active_coupon['description'], [coupon.description for coupon in active_coupons])
|
|
if active_coupon['expiration_date']:
|
|
self.assertIn(active_coupon['expiration_date'], [coupon.display_expiry_date for coupon in active_coupons])
|
|
self.assertIn(
|
|
active_coupon['course_id'],
|
|
[coupon.course_id.to_deprecated_string() for coupon in active_coupons]
|
|
)
|