Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
ea0ae111
Commit
ea0ae111
authored
Jan 14, 2014
by
Julia Hansbrough
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Response to CR 1-14
parent
a7764760
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
152 additions
and
120 deletions
+152
-120
common/djangoapps/course_modes/models.py
+9
-0
common/djangoapps/student/models.py
+3
-2
common/djangoapps/util/query.py
+6
-0
lms/djangoapps/shoppingcart/models.py
+3
-1
lms/djangoapps/shoppingcart/reports.py
+9
-10
lms/djangoapps/shoppingcart/tests/test_models.py
+1
-98
lms/djangoapps/shoppingcart/tests/test_reports.py
+98
-6
lms/djangoapps/shoppingcart/tests/test_views.py
+0
-1
lms/envs/aws.py
+2
-0
lms/envs/common.py
+2
-0
lms/envs/dev.py
+2
-0
lms/templates/shoppingcart/download_report.html
+17
-2
No files found.
common/djangoapps/course_modes/models.py
View file @
ea0ae111
...
...
@@ -97,6 +97,15 @@ class CourseMode(models.Model):
@classmethod
def
min_course_price_for_verified_for_currency
(
cls
,
course_id
,
currency
):
"""
Returns the minimum price of the course int he appropriate currency over all the
course's *verified*, non-expired modes.
Assuming all verified courses have a minimum price of >0, this value should always
be >0.
If no verified mode is found, 0 is returned.
"""
modes
=
cls
.
modes_for_course
(
course_id
)
for
mode
in
modes
:
if
(
mode
.
currency
==
currency
)
and
(
mode
.
slug
==
'verified'
):
...
...
common/djangoapps/student/models.py
View file @
ea0ae111
...
...
@@ -23,7 +23,7 @@ from django.conf import settings
from
django.contrib.auth.models
import
User
from
django.contrib.auth.signals
import
user_logged_in
,
user_logged_out
from
django.db
import
models
,
IntegrityError
from
django.db.models
import
Sum
,
Count
from
django.db.models
import
Count
from
django.db.models.signals
import
post_save
from
django.dispatch
import
receiver
,
Signal
import
django.dispatch
...
...
@@ -35,6 +35,7 @@ from eventtracking import tracker
from
course_modes.models
import
CourseMode
import
lms.lib.comment_client
as
cc
from
util.query
import
use_read_replica_if_available
unenroll_done
=
Signal
(
providing_args
=
[
"course_enrollment"
])
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -588,7 +589,7 @@ class CourseEnrollment(models.Model):
enrollment count for each individual mode.
"""
# Unfortunately, Django's "group by"-style queries look super-awkward
query
=
cls
.
objects
.
filter
(
course_id
=
course_id
,
is_active
=
True
)
.
values
(
'mode'
)
.
order_by
()
.
annotate
(
Count
(
'mode'
))
query
=
use_read_replica_if_available
(
cls
.
objects
.
filter
(
course_id
=
course_id
,
is_active
=
True
)
.
values
(
'mode'
)
.
order_by
()
.
annotate
(
Count
(
'mode'
)
))
total
=
0
d
=
defaultdict
(
int
)
for
item
in
query
:
...
...
common/djangoapps/util/query.py
View file @
ea0ae111
""" Utility functions related to database queries """
from
django.conf
import
settings
def
use_read_replica_if_available
(
queryset
):
"""
If there is a database called 'read_replica', use that database for the queryset.
"""
return
queryset
.
using
(
"read_replica"
)
if
"read_replica"
in
settings
.
DATABASES
else
queryset
\ No newline at end of file
lms/djangoapps/shoppingcart/models.py
View file @
ea0ae111
""" Models for the shopping cart and assorted purchase types """
from
collections
import
namedtuple
from
datetime
import
datetime
from
decimal
import
Decimal
...
...
@@ -581,7 +583,7 @@ class CertificateItem(OrderItem):
"""
Returns a Decimal indicating the total sum of field_to_aggregate for all verified certificates with a particular status.
Sample usages:
Sample usages:
- status 'refunded' and field_to_aggregate 'unit_cost' will give the total amount of money refunded for course_id
- status 'purchased' and field_to_aggregate 'service_fees' gives the sum of all service fees for purchased certificates
etc
...
...
lms/djangoapps/shoppingcart/reports.py
View file @
ea0ae111
""" Objects and functions related to generating CSV reports """
from
decimal
import
Decimal
import
unicodecsv
import
logging
from
django.db
import
models
from
django.conf
import
settings
from
django.utils.translation
import
ugettext
as
_
from
courseware.courses
import
get_course_by_id
...
...
@@ -11,7 +10,6 @@ from course_modes.models import CourseMode
from
shoppingcart.models
import
CertificateItem
,
OrderItem
from
student.models
import
CourseEnrollment
from
util.query
import
use_read_replica_if_available
from
xmodule.error_module
import
ErrorDescriptor
from
xmodule.modulestore.django
import
modulestore
...
...
@@ -191,10 +189,10 @@ class CertificateStatusReport(Report):
yield
[
university
,
course
,
""
,
""
,
""
,
""
,
course_announce_date
,
course_reg_start_date
,
course_reg_close_date
,
registration_period
,
total_enrolled
,
audit_enrolled
,
honor_enrolled
,
...
...
@@ -267,10 +265,11 @@ class UniversityRevenueShareReport(Report):
_
(
"Total Amount of Refunds"
),
]
def
course_ids_between
(
start_word
,
end_word
):
"""
"""
Returns a list of all valid course_ids that fall alphabetically between start_word and end_word.
These comparisons are unicode-safe.
These comparisons are unicode-safe.
"""
valid_courses
=
[]
...
...
lms/djangoapps/shoppingcart/tests/test_models.py
View file @
ea0ae111
...
...
@@ -18,12 +18,10 @@ 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
)
from
shoppingcart.views
import
initialize_report
,
REPORT_TYPES
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
,
ReportTypeDoesNotExistException
from
shoppingcart.exceptions
import
PurchasedCallbackException
import
pytz
import
datetime
...
...
@@ -326,101 +324,6 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase):
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
class
ItemizedPurchaseReportTest
(
ModuleStoreTestCase
):
"""
Tests for the models used to generate itemized purchase reports
"""
FIVE_MINS
=
datetime
.
timedelta
(
minutes
=
5
)
TEST_ANNOTATION
=
u'Ba
\xfc\u5305
'
def
setUp
(
self
):
self
.
user
=
UserFactory
.
create
()
self
.
course_id
=
"MITx/999/Robot_Super_Course"
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
()
self
.
annotation
=
PaidCourseRegistrationAnnotation
(
course_id
=
self
.
course_id
,
annotation
=
self
.
TEST_ANNOTATION
)
self
.
annotation
.
save
()
self
.
cart
=
Order
.
get_cart_for_user
(
self
.
user
)
self
.
reg
=
PaidCourseRegistration
.
add_to_order
(
self
.
cart
,
self
.
course_id
)
self
.
cert_item
=
CertificateItem
.
add_to_order
(
self
.
cart
,
self
.
course_id
,
self
.
cost
,
'verified'
)
self
.
cart
.
purchase
()
self
.
now
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
paid_reg
=
PaidCourseRegistration
.
objects
.
get
(
course_id
=
self
.
course_id
,
user
=
self
.
user
)
paid_reg
.
fulfilled_time
=
self
.
now
paid_reg
.
refund_requested_time
=
self
.
now
paid_reg
.
save
()
cert
=
CertificateItem
.
objects
.
get
(
course_id
=
self
.
course_id
,
user
=
self
.
user
)
cert
.
fulfilled_time
=
self
.
now
cert
.
refund_requested_time
=
self
.
now
cert
.
save
()
self
.
CORRECT_CSV
=
dedent
(
"""
Purchase Time,Order ID,Status,Quantity,Unit Cost,Total Cost,Currency,Description,Comments
{time_str},1,purchased,1,40,40,usd,Registration for Course: Robot Super Course,Ba
\xc3\xbc\xe5\x8c\x85
{time_str},1,purchased,1,40,40,usd,"Certificate of Achievement, verified cert for course Robot Super Course",
"""
.
format
(
time_str
=
str
(
self
.
now
)))
def
test_purchased_items_btw_dates
(
self
):
report
=
initialize_report
(
"itemized_purchase_report"
,
self
.
now
-
self
.
FIVE_MINS
,
self
.
now
+
self
.
FIVE_MINS
)
purchases
=
report
.
rows
()
# since there's not many purchases, just run through the generator to make sure we've got the right number
num_purchases
=
0
for
item
in
purchases
:
num_purchases
+=
1
self
.
assertEqual
(
num_purchases
,
2
)
#self.assertIn(self.reg.orderitem_ptr, purchases)
#self.assertIn(self.cert_item.orderitem_ptr, purchases)
report
=
initialize_report
(
"itemized_purchase_report"
,
self
.
now
+
self
.
FIVE_MINS
,
self
.
now
+
self
.
FIVE_MINS
+
self
.
FIVE_MINS
)
no_purchases
=
report
.
rows
()
num_purchases
=
0
for
item
in
no_purchases
:
num_purchases
+=
1
self
.
assertEqual
(
num_purchases
,
0
)
def
test_purchased_csv
(
self
):
"""
Tests that a generated purchase report CSV is as we expect
"""
report
=
initialize_report
(
"itemized_purchase_report"
,
self
.
now
-
self
.
FIVE_MINS
,
self
.
now
+
self
.
FIVE_MINS
)
csv_file
=
StringIO
.
StringIO
()
report
.
write_csv
(
csv_file
)
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_CSV
.
strip
())
def
test_csv_report_no_annotation
(
self
):
"""
Fill in gap in test coverage. csv_report_comments for PaidCourseRegistration instance with no
matching annotation
"""
# delete the matching annotation
self
.
annotation
.
delete
()
self
.
assertEqual
(
u""
,
self
.
reg
.
csv_report_comments
)
def
test_paidcourseregistrationannotation_unicode
(
self
):
"""
Fill in gap in test coverage. __unicode__ method of PaidCourseRegistrationAnnotation
"""
self
.
assertEqual
(
unicode
(
self
.
annotation
),
u'{} : {}'
.
format
(
self
.
course_id
,
self
.
TEST_ANNOTATION
))
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
class
CertificateItemTest
(
ModuleStoreTestCase
):
"""
Tests for verifying specific CertificateItem functionality
...
...
lms/djangoapps/shoppingcart/tests/test_reports.py
View file @
ea0ae111
...
...
@@ -13,8 +13,7 @@ from django.test.utils import override_settings
from
course_modes.models
import
CourseMode
from
courseware.tests.tests
import
TEST_DATA_MONGO_MODULESTORE
from
shoppingcart.models
import
(
Order
,
CertificateItem
)
from
shoppingcart.reports
import
ItemizedPurchaseReport
,
CertificateStatusReport
,
UniversityRevenueShareReport
,
RefundReport
from
shoppingcart.models
import
(
Order
,
CertificateItem
,
PaidCourseRegistration
,
PaidCourseRegistrationAnnotation
)
from
shoppingcart.views
import
initialize_report
,
REPORT_TYPES
from
student.tests.factories
import
UserFactory
from
student.models
import
CourseEnrollment
...
...
@@ -56,11 +55,11 @@ class ReportTypeTests(ModuleStoreTestCase):
self
.
honor_user
.
profile
.
save
()
self
.
first_refund_user
=
UserFactory
.
create
()
self
.
first_refund_user
.
profile
.
name
=
"King Bowse
r"
self
.
first_refund_user
.
profile
.
name
=
u"King Bowsé
r"
self
.
first_refund_user
.
profile
.
save
()
self
.
second_refund_user
=
UserFactory
.
create
()
self
.
second_refund_user
.
profile
.
name
=
"Su
san Smith"
self
.
second_refund_user
.
profile
.
name
=
u"Sú
san Smith"
self
.
second_refund_user
.
profile
.
save
()
# Two are verified, three are audit, one honor
...
...
@@ -125,8 +124,8 @@ class ReportTypeTests(ModuleStoreTestCase):
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 Bows
e
r,{time_str},{time_str},40,0
4,S
u
san Smith,{time_str},{time_str},40,0
3,King Bows
é
r,{time_str},{time_str},40,0
4,S
ú
san Smith,{time_str},{time_str},40,0
"""
.
format
(
time_str
=
str
(
self
.
test_time
)))
self
.
CORRECT_CERT_STATUS_CSV
=
dedent
(
"""
...
...
@@ -177,3 +176,96 @@ class ReportTypeTests(ModuleStoreTestCase):
report
.
write_csv
(
csv_file
)
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
ItemizedPurchaseReportTest
(
ModuleStoreTestCase
):
"""
Tests for the models used to generate itemized purchase reports
"""
FIVE_MINS
=
datetime
.
timedelta
(
minutes
=
5
)
TEST_ANNOTATION
=
u'Ba
\xfc\u5305
'
def
setUp
(
self
):
self
.
user
=
UserFactory
.
create
()
self
.
course_id
=
"MITx/999/Robot_Super_Course"
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
()
self
.
annotation
=
PaidCourseRegistrationAnnotation
(
course_id
=
self
.
course_id
,
annotation
=
self
.
TEST_ANNOTATION
)
self
.
annotation
.
save
()
self
.
cart
=
Order
.
get_cart_for_user
(
self
.
user
)
self
.
reg
=
PaidCourseRegistration
.
add_to_order
(
self
.
cart
,
self
.
course_id
)
self
.
cert_item
=
CertificateItem
.
add_to_order
(
self
.
cart
,
self
.
course_id
,
self
.
cost
,
'verified'
)
self
.
cart
.
purchase
()
self
.
now
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
paid_reg
=
PaidCourseRegistration
.
objects
.
get
(
course_id
=
self
.
course_id
,
user
=
self
.
user
)
paid_reg
.
fulfilled_time
=
self
.
now
paid_reg
.
refund_requested_time
=
self
.
now
paid_reg
.
save
()
cert
=
CertificateItem
.
objects
.
get
(
course_id
=
self
.
course_id
,
user
=
self
.
user
)
cert
.
fulfilled_time
=
self
.
now
cert
.
refund_requested_time
=
self
.
now
cert
.
save
()
self
.
CORRECT_CSV
=
dedent
(
"""
Purchase Time,Order ID,Status,Quantity,Unit Cost,Total Cost,Currency,Description,Comments
{time_str},1,purchased,1,40,40,usd,Registration for Course: Robot Super Course,Ba
\xc3\xbc\xe5\x8c\x85
{time_str},1,purchased,1,40,40,usd,"Certificate of Achievement, verified cert for course Robot Super Course",
"""
.
format
(
time_str
=
str
(
self
.
now
)))
def
test_purchased_items_btw_dates
(
self
):
report
=
initialize_report
(
"itemized_purchase_report"
,
self
.
now
-
self
.
FIVE_MINS
,
self
.
now
+
self
.
FIVE_MINS
)
purchases
=
report
.
rows
()
# since there's not many purchases, just run through the generator to make sure we've got the right number
num_purchases
=
0
for
item
in
purchases
:
num_purchases
+=
1
self
.
assertEqual
(
num_purchases
,
2
)
report
=
initialize_report
(
"itemized_purchase_report"
,
self
.
now
+
self
.
FIVE_MINS
,
self
.
now
+
self
.
FIVE_MINS
+
self
.
FIVE_MINS
)
no_purchases
=
report
.
rows
()
num_purchases
=
0
for
item
in
no_purchases
:
num_purchases
+=
1
self
.
assertEqual
(
num_purchases
,
0
)
def
test_purchased_csv
(
self
):
"""
Tests that a generated purchase report CSV is as we expect
"""
report
=
initialize_report
(
"itemized_purchase_report"
,
self
.
now
-
self
.
FIVE_MINS
,
self
.
now
+
self
.
FIVE_MINS
)
csv_file
=
StringIO
.
StringIO
()
report
.
write_csv
(
csv_file
)
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_CSV
.
strip
())
def
test_csv_report_no_annotation
(
self
):
"""
Fill in gap in test coverage. csv_report_comments for PaidCourseRegistration instance with no
matching annotation
"""
# delete the matching annotation
self
.
annotation
.
delete
()
self
.
assertEqual
(
u""
,
self
.
reg
.
csv_report_comments
)
def
test_paidcourseregistrationannotation_unicode
(
self
):
"""
Fill in gap in test coverage. __unicode__ method of PaidCourseRegistrationAnnotation
"""
self
.
assertEqual
(
unicode
(
self
.
annotation
),
u'{} : {}'
.
format
(
self
.
course_id
,
self
.
TEST_ANNOTATION
))
lms/djangoapps/shoppingcart/tests/test_views.py
View file @
ea0ae111
...
...
@@ -21,7 +21,6 @@ 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
...
...
lms/envs/aws.py
View file @
ea0ae111
...
...
@@ -285,6 +285,8 @@ if AWS_SECRET_ACCESS_KEY == "":
AWS_STORAGE_BUCKET_NAME
=
AUTH_TOKENS
.
get
(
'AWS_STORAGE_BUCKET_NAME'
,
'edxuploads'
)
# If there is a database called 'read_replica', you can use the use_read_replica_if_available
# function in util/query.py, which is useful for very large database reads
DATABASES
=
AUTH_TOKENS
[
'DATABASES'
]
XQUEUE_INTERFACE
=
AUTH_TOKENS
[
'XQUEUE_INTERFACE'
]
...
...
lms/envs/common.py
View file @
ea0ae111
...
...
@@ -202,6 +202,8 @@ FEATURES = {
# Give course staff unrestricted access to grade downloads (if set to False,
# only edX superusers can perform the downloads)
'ALLOW_COURSE_STAFF_GRADE_DOWNLOADS'
:
False
,
'ENABLED_PAYMENT_REPORTS'
:
[
"refund_report"
,
"itemized_purchase_report"
,
"university_revenue_share"
,
"certificate_status"
],
}
# Used for A/B testing
...
...
lms/envs/dev.py
View file @
ea0ae111
...
...
@@ -52,6 +52,8 @@ LOGGING = get_logger_config(ENV_ROOT / "log",
dev_env
=
True
,
debug
=
True
)
# If there is a database called 'read_replica', you can use the use_read_replica_if_available
# function in util/query.py, which is useful for very large database reads
DATABASES
=
{
'default'
:
{
'ENGINE'
:
'django.db.backends.sqlite3'
,
...
...
lms/templates/shoppingcart/download_report.html
View file @
ea0ae111
<
%!
from
django
.
utils
.
translation
import
ugettext
as
_
%
>
<
%!
from
django
.
core
.
urlresolvers
import
reverse
%
>
<
%!
from
django
.
conf
import
settings
%
>
<
%
inherit
file=
"../main.html"
/>
<
%
block
name=
"title"
><title>
${_("Download CSV Reports")}
</title></
%
block>
...
...
@@ -19,21 +20,35 @@
<label
for=
"end_date"
>
${_("End Date: ")}
</label>
<input
id=
"end_date"
type=
"text"
value=
"${end_date}"
name=
"end_date"
/>
<br/>
%if "itemized_purchase_report" in settings.FEATURES['ENABLED_PAYMENT_REPORTS']:
<button
type =
"submit"
name=
"requested_report"
value=
"itemized_purchase_report"
>
Itemized Purchase Report
</button>
<br/>
%endif
%if "refund_report" in settings.FEATURES['ENABLED_PAYMENT_REPORTS']:
<button
type =
"submit"
name=
"requested_report"
value=
"refund_report"
>
Refund Report
</button>
<br/>
%endif
<br/>
<br/><br/>
<p>
${_("These reports are delimited alphabetically by university name. i.e., generating a report with 'Start Letter' A and 'End Letter' C will generate reports for all universities starting with A, B, and C.")}
</p>
<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/>
%if "university_revenue_share" in settings.FEATURES['ENABLED_PAYMENT_REPORTS']:
<button
type =
"submit"
name=
"requested_report"
value=
"university_revenue_share"
>
University Revenue Share
</button>
<br/>
%endif
%if "certificate_status" in settings.FEATURES['ENABLED_PAYMENT_REPORTS']:
<button
type=
"submit"
name=
"requested_report"
value=
"certificate_status"
>
Certiciate Status
</button>
<br/>
%endif
</form>
</section>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment