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
8d60a8b4
Commit
8d60a8b4
authored
Oct 07, 2014
by
Will Daly
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #5482 from edx/will/cybersource-donations
CyberSource donations (back-end)
parents
324a1da6
26bcfe58
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
312 additions
and
21 deletions
+312
-21
lms/djangoapps/shoppingcart/migrations/0018_auto__add_donation.py
+0
-0
lms/djangoapps/shoppingcart/models.py
+143
-3
lms/djangoapps/shoppingcart/tests/test_models.py
+96
-9
lms/djangoapps/shoppingcart/tests/test_views.py
+67
-7
lms/templates/shoppingcart/receipt.html
+6
-2
No files found.
lms/djangoapps/shoppingcart/migrations/0018_auto__add_donation.py
0 → 100644
View file @
8d60a8b4
This diff is collapsed.
Click to expand it.
lms/djangoapps/shoppingcart/models.py
View file @
8d60a8b4
...
...
@@ -30,9 +30,12 @@ from xmodule_django.models import CourseKeyField
from
verify_student.models
import
SoftwareSecurePhotoVerification
from
.exceptions
import
(
InvalidCartItem
,
PurchasedCallbackException
,
ItemAlreadyInCartException
,
AlreadyEnrolledInCourseException
,
CourseDoesNotExistException
,
MultipleCouponsNotAllowedException
,
RegCodeAlreadyExistException
,
ItemDoesNotExistAgainstRegCodeException
)
from
.exceptions
import
(
InvalidCartItem
,
PurchasedCallbackException
,
ItemAlreadyInCartException
,
AlreadyEnrolledInCourseException
,
CourseDoesNotExistException
,
MultipleCouponsNotAllowedException
,
RegCodeAlreadyExistException
,
ItemDoesNotExistAgainstRegCodeException
)
from
microsite_configuration
import
microsite
...
...
@@ -865,3 +868,140 @@ class CertificateItem(OrderItem):
mode
=
'verified'
,
status
=
'purchased'
,
unit_cost__gt
=
(
CourseMode
.
min_course_price_for_verified_for_currency
(
course_id
,
'usd'
))))
.
count
()
class
Donation
(
OrderItem
):
"""A donation made by a user.
Donations can be made for a specific course or to the organization as a whole.
Users can choose the donation amount.
"""
# Types of donations
DONATION_TYPES
=
(
(
"general"
,
"A general donation"
),
(
"course"
,
"A donation to a particular course"
)
)
# The type of donation
donation_type
=
models
.
CharField
(
max_length
=
32
,
default
=
"general"
,
choices
=
DONATION_TYPES
)
# If a donation is made for a specific course, then store the course ID here.
# If the donation is made to the organization as a whole,
# set this field to CourseKeyField.Empty
course_id
=
CourseKeyField
(
max_length
=
255
,
db_index
=
True
)
@classmethod
@transaction.commit_on_success
def
add_to_order
(
cls
,
order
,
donation_amount
,
course_id
=
None
,
currency
=
'usd'
):
"""Add a donation to an order.
Args:
order (Order): The order to add this donation to.
donation_amount (Decimal): The amount the user is donating.
Keyword Args:
course_id (CourseKey): If provided, associate this donation with a particular course.
currency (str): The currency used for the the donation.
Raises:
InvalidCartItem: The provided course ID is not valid.
Returns:
Donation
"""
# This will validate the currency but won't actually add the item to the order.
super
(
Donation
,
cls
)
.
add_to_order
(
order
,
currency
=
currency
)
# Create a line item description, including the name of the course
# if this is a per-course donation.
# This will raise an exception if the course can't be found.
description
=
cls
.
_line_item_description
(
course_id
=
course_id
)
params
=
{
"order"
:
order
,
"user"
:
order
.
user
,
"status"
:
order
.
status
,
"qty"
:
1
,
"unit_cost"
:
donation_amount
,
"currency"
:
currency
,
"line_desc"
:
description
}
if
course_id
is
not
None
:
params
[
"course_id"
]
=
course_id
params
[
"donation_type"
]
=
"course"
else
:
params
[
"donation_type"
]
=
"general"
return
cls
.
objects
.
create
(
**
params
)
def
purchased_callback
(
self
):
"""Donations do not need to be fulfilled, so this method does nothing."""
pass
def
generate_receipt_instructions
(
self
):
"""Provide information about tax-deductible donations in the receipt.
Returns:
tuple of (Donation, unicode)
"""
return
self
.
pk_with_subclass
,
set
([
self
.
_tax_deduction_msg
()])
@property
def
additional_instruction_text
(
self
):
"""Provide information about tax-deductible donations in the confirmation email.
Returns:
unicode
"""
return
self
.
_tax_deduction_msg
()
def
_tax_deduction_msg
(
self
):
"""Return the translated version of the tax deduction message.
Returns:
unicode
"""
return
_
(
u"This receipt was prepared to support charitable contributions for tax purposes. "
u"Gifts are tax deductible as permitted by law. "
u"We confirm that neither goods nor services were provided in exchange for this gift."
)
@classmethod
def
_line_item_description
(
self
,
course_id
=
None
):
"""Create a line-item description for the donation.
Includes the course display name if provided.
Keyword Arguments:
course_id (CourseKey)
Raises:
InvalidCartItem: The course ID is not valid.
Returns:
unicode
"""
# If a course ID is provided, include the display name of the course
# in the line item description.
if
course_id
is
not
None
:
course
=
modulestore
()
.
get_course
(
course_id
)
if
course
is
None
:
err
=
_
(
u"Could not find a course with the ID '{course_id}'"
)
.
format
(
course_id
=
course_id
)
raise
InvalidCartItem
(
err
)
return
_
(
u"Donation for {course}"
)
.
format
(
course
=
course
.
display_name
)
# The donation is for the organization as a whole, not a specific course
else
:
return
_
(
u"Donation"
)
lms/djangoapps/shoppingcart/tests/test_models.py
View file @
8d60a8b4
"""
Tests for the Shopping Cart Models
"""
from
decimal
import
Decimal
import
datetime
import
smtplib
from
boto.exception
import
BotoServerError
# this is a super-class of SESError and catches connection errors
from
mock
import
patch
,
MagicMock
import
pytz
from
django.core
import
mail
from
django.conf
import
settings
from
django.db
import
DatabaseError
from
django.test
import
TestCase
from
django.test.utils
import
override_settings
from
django.contrib.auth.models
import
AnonymousUser
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
(
ModuleStoreTestCase
,
mixed_store_config
)
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
)
from
shoppingcart.models
import
(
Order
,
OrderItem
,
CertificateItem
,
InvalidCartItem
,
PaidCourseRegistration
,
Donation
,
OrderItemSubclassPK
)
from
student.tests.factories
import
UserFactory
from
student.models
import
CourseEnrollment
from
course_modes.models
import
CourseMode
from
shoppingcart.exceptions
import
PurchasedCallbackException
import
pytz
import
datetime
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
# Since we don't need any XML course fixtures, use a modulestore configuration
# that disables the XML modulestore.
MODULESTORE_CONFIG
=
mixed_store_config
(
settings
.
COMMON_TEST_DATA_ROOT
,
{},
include_xml
=
False
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
@override_settings
(
MODULESTORE
=
MODULESTORE_CONFIG
)
class
OrderTest
(
ModuleStoreTestCase
):
def
setUp
(
self
):
self
.
user
=
UserFactory
.
create
()
...
...
@@ -286,7 +296,7 @@ class OrderItemTest(TestCase):
self
.
assertEquals
(
set
([]),
inst_set
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
@override_settings
(
MODULESTORE
=
MODULESTORE_CONFIG
)
class
PaidCourseRegistrationTest
(
ModuleStoreTestCase
):
def
setUp
(
self
):
self
.
user
=
UserFactory
.
create
()
...
...
@@ -383,7 +393,7 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase):
self
.
assertTrue
(
PaidCourseRegistration
.
contained_in_order
(
cart
,
self
.
course_key
))
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
@override_settings
(
MODULESTORE
=
MODULESTORE_CONFIG
)
class
CertificateItemTest
(
ModuleStoreTestCase
):
"""
Tests for verifying specific CertificateItem functionality
...
...
@@ -547,3 +557,80 @@ class CertificateItemTest(ModuleStoreTestCase):
CourseEnrollment
.
enroll
(
self
.
user
,
self
.
course_key
,
'verified'
)
ret_val
=
CourseEnrollment
.
unenroll
(
self
.
user
,
self
.
course_key
)
self
.
assertFalse
(
ret_val
)
@override_settings
(
MODULESTORE
=
MODULESTORE_CONFIG
)
class
DonationTest
(
ModuleStoreTestCase
):
"""Tests for the donation order item type. """
COST
=
Decimal
(
'23.45'
)
def
setUp
(
self
):
"""Create a test user and order. """
super
(
DonationTest
,
self
)
.
setUp
()
self
.
user
=
UserFactory
.
create
()
self
.
cart
=
Order
.
get_cart_for_user
(
self
.
user
)
def
test_donate_to_org
(
self
):
# No course ID provided, so this is a donation to the entire organization
donation
=
Donation
.
add_to_order
(
self
.
cart
,
self
.
COST
)
self
.
_assert_donation
(
donation
,
donation_type
=
"general"
,
unit_cost
=
self
.
COST
,
line_desc
=
"Donation"
)
def
test_donate_to_course
(
self
):
# Create a test course
course
=
CourseFactory
.
create
(
display_name
=
"Test Course"
)
# Donate to the course
donation
=
Donation
.
add_to_order
(
self
.
cart
,
self
.
COST
,
course_id
=
course
.
id
)
self
.
_assert_donation
(
donation
,
donation_type
=
"course"
,
course_id
=
course
.
id
,
unit_cost
=
self
.
COST
,
line_desc
=
u"Donation for Test Course"
)
def
test_donate_no_such_course
(
self
):
fake_course_id
=
SlashSeparatedCourseKey
(
"edx"
,
"fake"
,
"course"
)
with
self
.
assertRaises
(
InvalidCartItem
):
Donation
.
add_to_order
(
self
.
cart
,
self
.
COST
,
course_id
=
fake_course_id
)
def
test_confirmation_email
(
self
):
# Pay for a donation
Donation
.
add_to_order
(
self
.
cart
,
self
.
COST
)
self
.
cart
.
start_purchase
()
self
.
cart
.
purchase
()
# Check that the tax-deduction information appears in the confirmation email
self
.
assertEqual
(
len
(
mail
.
outbox
),
1
)
email
=
mail
.
outbox
[
0
]
self
.
assertEquals
(
'Order Payment Confirmation'
,
email
.
subject
)
self
.
assertIn
(
"tax deductible"
,
email
.
body
)
def
_assert_donation
(
self
,
donation
,
donation_type
=
None
,
course_id
=
None
,
unit_cost
=
None
,
line_desc
=
None
):
"""Verify the donation fields and that the donation can be purchased. """
self
.
assertEqual
(
donation
.
order
,
self
.
cart
)
self
.
assertEqual
(
donation
.
user
,
self
.
user
)
self
.
assertEqual
(
donation
.
donation_type
,
donation_type
)
self
.
assertEqual
(
donation
.
course_id
,
course_id
)
self
.
assertEqual
(
donation
.
qty
,
1
)
self
.
assertEqual
(
donation
.
unit_cost
,
unit_cost
)
self
.
assertEqual
(
donation
.
currency
,
"usd"
)
self
.
assertEqual
(
donation
.
line_desc
,
line_desc
)
# Verify that the donation is in the cart
self
.
assertTrue
(
self
.
cart
.
has_items
(
item_type
=
Donation
))
self
.
assertEqual
(
self
.
cart
.
total_cost
,
unit_cost
)
# Purchase the item
self
.
cart
.
start_purchase
()
self
.
cart
.
purchase
()
# Verify that the donation is marked as purchased
donation
=
Donation
.
objects
.
get
(
pk
=
donation
.
id
)
self
.
assertEqual
(
donation
.
status
,
"purchased"
)
lms/djangoapps/shoppingcart/tests/test_views.py
View file @
8d60a8b4
...
...
@@ -18,11 +18,16 @@ from pytz import UTC
from
freezegun
import
freeze_time
from
datetime
import
datetime
,
timedelta
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
(
ModuleStoreTestCase
,
mixed_store_config
)
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
,
Coupon
,
CourseRegistrationCode
,
RegistrationCodeRedemption
from
shoppingcart.models
import
(
Order
,
CertificateItem
,
PaidCourseRegistration
,
Coupon
,
CourseRegistrationCode
,
RegistrationCodeRedemption
,
Donation
)
from
student.tests.factories
import
UserFactory
,
AdminFactory
from
courseware.tests.factories
import
InstructorFactory
from
student.models
import
CourseEnrollment
...
...
@@ -33,7 +38,7 @@ from shoppingcart.admin import SoftDeleteCouponAdmin
from
mock
import
patch
,
Mock
from
shoppingcart.views
import
initialize_report
from
decimal
import
Decimal
from
student.tests.factories
import
AdminFactory
def
mock_render_purchase_form_html
(
*
args
,
**
kwargs
):
return
render_purchase_form_html
(
*
args
,
**
kwargs
)
...
...
@@ -48,7 +53,12 @@ render_mock = Mock(side_effect=mock_render_to_response)
postpay_mock
=
Mock
()
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
# Since we don't need any XML course fixtures, use a modulestore configuration
# that disables the XML modulestore.
MODULESTORE_CONFIG
=
mixed_store_config
(
settings
.
COMMON_TEST_DATA_ROOT
,
{},
include_xml
=
False
)
@override_settings
(
MODULESTORE
=
MODULESTORE_CONFIG
)
class
ShoppingCartViewsTests
(
ModuleStoreTestCase
):
def
setUp
(
self
):
patcher
=
patch
(
'student.models.tracker'
)
...
...
@@ -739,7 +749,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
self
.
assertEqual
(
template
,
cert_item
.
single_item_receipt_template
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
@override_settings
(
MODULESTORE
=
MODULESTORE_CONFIG
)
class
RegistrationCodeRedemptionCourseEnrollment
(
ModuleStoreTestCase
):
"""
Test suite for RegistrationCodeRedemption Course Enrollments
...
...
@@ -857,7 +867,57 @@ class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase):
self
.
assertTrue
(
"You've clicked a link for an enrollment code that has already been used."
in
response
.
content
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
@override_settings
(
MODULESTORE
=
MODULESTORE_CONFIG
)
class
DonationReceiptViewTest
(
ModuleStoreTestCase
):
"""Tests for the receipt page when the user pays for a donation. """
COST
=
Decimal
(
'23.45'
)
PASSWORD
=
"password"
def
setUp
(
self
):
"""Create a test user and order. """
super
(
DonationReceiptViewTest
,
self
)
.
setUp
()
# Create and login a user
self
.
user
=
UserFactory
.
create
()
self
.
user
.
set_password
(
self
.
PASSWORD
)
self
.
user
.
save
()
result
=
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
self
.
PASSWORD
)
self
.
assertTrue
(
result
)
# Create an order for the user
self
.
cart
=
Order
.
get_cart_for_user
(
self
.
user
)
def
test_donation_for_org_receipt
(
self
):
# Purchase the donation
Donation
.
add_to_order
(
self
.
cart
,
self
.
COST
)
self
.
cart
.
start_purchase
()
self
.
cart
.
purchase
()
# Verify the receipt page
self
.
_assert_receipt_contains
(
"tax deductible"
)
def
test_donation_for_course_receipt
(
self
):
# Create a test course
self
.
course
=
CourseFactory
.
create
(
display_name
=
"Test Course"
)
# Purchase the donation for the course
Donation
.
add_to_order
(
self
.
cart
,
self
.
COST
,
course_id
=
self
.
course
.
id
)
self
.
cart
.
start_purchase
()
self
.
cart
.
purchase
()
# Verify the receipt page
self
.
_assert_receipt_contains
(
"tax deductible"
)
self
.
_assert_receipt_contains
(
self
.
course
.
display_name
)
def
_assert_receipt_contains
(
self
,
expected_text
):
"""Load the receipt page and verify that it contains the expected text."""
url
=
reverse
(
"shoppingcart.views.show_receipt"
,
kwargs
=
{
"ordernum"
:
self
.
cart
.
id
})
resp
=
self
.
client
.
get
(
url
)
self
.
assertContains
(
resp
,
expected_text
)
@override_settings
(
MODULESTORE
=
MODULESTORE_CONFIG
)
class
CSVReportViewsTest
(
ModuleStoreTestCase
):
"""
Test suite for CSV Purchase Reporting
...
...
lms/templates/shoppingcart/receipt.html
View file @
8d60a8b4
...
...
@@ -46,13 +46,17 @@
</tr>
% for item in order_items:
<
%
course_id =
reverse('info',
args=
[item.course_id.to_deprecated_string()])
%
>
<tr
class=
"order-item"
>
% if item.status == "purchased":
<td>
${item.qty}
</td>
<td>
${item.line_desc}
</td>
<td><a
href=
"${course_id | h}"
class=
"enter-course"
>
${_('View Course')}
</a></td>
<td>
% if item.course_id:
<
%
course_id =
reverse('info',
args=
[item.course_id.to_deprecated_string()])
%
>
<a
href=
"${course_id | h}"
class=
"enter-course"
>
${_('View Course')}
</a></td>
% endif
</td>
<td>
${"{0:0.2f}".format(item.unit_cost)}
% if item.list_price != None:
<span
class=
"old-price"
>
${"{0:0.2f}".format(item.list_price)}
</span>
...
...
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