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
...
@@ -30,9 +30,12 @@ from xmodule_django.models import CourseKeyField
from
verify_student.models
import
SoftwareSecurePhotoVerification
from
verify_student.models
import
SoftwareSecurePhotoVerification
from
.exceptions
import
(
InvalidCartItem
,
PurchasedCallbackException
,
ItemAlreadyInCartException
,
from
.exceptions
import
(
AlreadyEnrolledInCourseException
,
CourseDoesNotExistException
,
InvalidCartItem
,
PurchasedCallbackException
,
ItemAlreadyInCartException
,
MultipleCouponsNotAllowedException
,
RegCodeAlreadyExistException
,
ItemDoesNotExistAgainstRegCodeException
)
AlreadyEnrolledInCourseException
,
CourseDoesNotExistException
,
MultipleCouponsNotAllowedException
,
RegCodeAlreadyExistException
,
ItemDoesNotExistAgainstRegCodeException
)
from
microsite_configuration
import
microsite
from
microsite_configuration
import
microsite
...
@@ -865,3 +868,140 @@ class CertificateItem(OrderItem):
...
@@ -865,3 +868,140 @@ class CertificateItem(OrderItem):
mode
=
'verified'
,
mode
=
'verified'
,
status
=
'purchased'
,
status
=
'purchased'
,
unit_cost__gt
=
(
CourseMode
.
min_course_price_for_verified_for_currency
(
course_id
,
'usd'
))))
.
count
()
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
Tests for the Shopping Cart Models
"""
"""
from
decimal
import
Decimal
import
datetime
import
smtplib
import
smtplib
from
boto.exception
import
BotoServerError
# this is a super-class of SESError and catches connection errors
from
boto.exception
import
BotoServerError
# this is a super-class of SESError and catches connection errors
from
mock
import
patch
,
MagicMock
from
mock
import
patch
,
MagicMock
import
pytz
from
django.core
import
mail
from
django.core
import
mail
from
django.conf
import
settings
from
django.conf
import
settings
from
django.db
import
DatabaseError
from
django.db
import
DatabaseError
from
django.test
import
TestCase
from
django.test
import
TestCase
from
django.test.utils
import
override_settings
from
django.test.utils
import
override_settings
from
django.contrib.auth.models
import
AnonymousUser
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
xmodule.modulestore.tests.factories
import
CourseFactory
from
courseware.tests.tests
import
TEST_DATA_MONGO_MODULESTORE
from
shoppingcart.models
import
(
from
shoppingcart.models
import
(
Order
,
OrderItem
,
CertificateItem
,
InvalidCartItem
,
PaidCourseRegistration
,
Order
,
OrderItem
,
CertificateItem
,
OrderItemSubclassPK
)
InvalidCartItem
,
PaidCourseRegistration
,
Donation
,
OrderItemSubclassPK
)
from
student.tests.factories
import
UserFactory
from
student.tests.factories
import
UserFactory
from
student.models
import
CourseEnrollment
from
student.models
import
CourseEnrollment
from
course_modes.models
import
CourseMode
from
course_modes.models
import
CourseMode
from
shoppingcart.exceptions
import
PurchasedCallbackException
from
shoppingcart.exceptions
import
PurchasedCallbackException
import
pytz
import
datetime
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
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
):
class
OrderTest
(
ModuleStoreTestCase
):
def
setUp
(
self
):
def
setUp
(
self
):
self
.
user
=
UserFactory
.
create
()
self
.
user
=
UserFactory
.
create
()
...
@@ -286,7 +296,7 @@ class OrderItemTest(TestCase):
...
@@ -286,7 +296,7 @@ class OrderItemTest(TestCase):
self
.
assertEquals
(
set
([]),
inst_set
)
self
.
assertEquals
(
set
([]),
inst_set
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
@override_settings
(
MODULESTORE
=
MODULESTORE_CONFIG
)
class
PaidCourseRegistrationTest
(
ModuleStoreTestCase
):
class
PaidCourseRegistrationTest
(
ModuleStoreTestCase
):
def
setUp
(
self
):
def
setUp
(
self
):
self
.
user
=
UserFactory
.
create
()
self
.
user
=
UserFactory
.
create
()
...
@@ -383,7 +393,7 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase):
...
@@ -383,7 +393,7 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase):
self
.
assertTrue
(
PaidCourseRegistration
.
contained_in_order
(
cart
,
self
.
course_key
))
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
):
class
CertificateItemTest
(
ModuleStoreTestCase
):
"""
"""
Tests for verifying specific CertificateItem functionality
Tests for verifying specific CertificateItem functionality
...
@@ -547,3 +557,80 @@ class CertificateItemTest(ModuleStoreTestCase):
...
@@ -547,3 +557,80 @@ class CertificateItemTest(ModuleStoreTestCase):
CourseEnrollment
.
enroll
(
self
.
user
,
self
.
course_key
,
'verified'
)
CourseEnrollment
.
enroll
(
self
.
user
,
self
.
course_key
,
'verified'
)
ret_val
=
CourseEnrollment
.
unenroll
(
self
.
user
,
self
.
course_key
)
ret_val
=
CourseEnrollment
.
unenroll
(
self
.
user
,
self
.
course_key
)
self
.
assertFalse
(
ret_val
)
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
...
@@ -18,11 +18,16 @@ from pytz import UTC
from
freezegun
import
freeze_time
from
freezegun
import
freeze_time
from
datetime
import
datetime
,
timedelta
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
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.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
student.tests.factories
import
UserFactory
,
AdminFactory
from
courseware.tests.factories
import
InstructorFactory
from
courseware.tests.factories
import
InstructorFactory
from
student.models
import
CourseEnrollment
from
student.models
import
CourseEnrollment
...
@@ -33,7 +38,7 @@ from shoppingcart.admin import SoftDeleteCouponAdmin
...
@@ -33,7 +38,7 @@ from shoppingcart.admin import SoftDeleteCouponAdmin
from
mock
import
patch
,
Mock
from
mock
import
patch
,
Mock
from
shoppingcart.views
import
initialize_report
from
shoppingcart.views
import
initialize_report
from
decimal
import
Decimal
from
decimal
import
Decimal
from
student.tests.factories
import
AdminFactory
def
mock_render_purchase_form_html
(
*
args
,
**
kwargs
):
def
mock_render_purchase_form_html
(
*
args
,
**
kwargs
):
return
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)
...
@@ -48,7 +53,12 @@ render_mock = Mock(side_effect=mock_render_to_response)
postpay_mock
=
Mock
()
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
):
class
ShoppingCartViewsTests
(
ModuleStoreTestCase
):
def
setUp
(
self
):
def
setUp
(
self
):
patcher
=
patch
(
'student.models.tracker'
)
patcher
=
patch
(
'student.models.tracker'
)
...
@@ -739,7 +749,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
...
@@ -739,7 +749,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
self
.
assertEqual
(
template
,
cert_item
.
single_item_receipt_template
)
self
.
assertEqual
(
template
,
cert_item
.
single_item_receipt_template
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
@override_settings
(
MODULESTORE
=
MODULESTORE_CONFIG
)
class
RegistrationCodeRedemptionCourseEnrollment
(
ModuleStoreTestCase
):
class
RegistrationCodeRedemptionCourseEnrollment
(
ModuleStoreTestCase
):
"""
"""
Test suite for RegistrationCodeRedemption Course Enrollments
Test suite for RegistrationCodeRedemption Course Enrollments
...
@@ -857,7 +867,57 @@ class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase):
...
@@ -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
)
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
):
class
CSVReportViewsTest
(
ModuleStoreTestCase
):
"""
"""
Test suite for CSV Purchase Reporting
Test suite for CSV Purchase Reporting
...
...
lms/templates/shoppingcart/receipt.html
View file @
8d60a8b4
...
@@ -46,13 +46,17 @@
...
@@ -46,13 +46,17 @@
</tr>
</tr>
% for item in order_items:
% for item in order_items:
<
%
course_id =
reverse('info',
args=
[item.course_id.to_deprecated_string()])
%
>
<tr
class=
"order-item"
>
<tr
class=
"order-item"
>
% if item.status == "purchased":
% if item.status == "purchased":
<td>
${item.qty}
</td>
<td>
${item.qty}
</td>
<td>
${item.line_desc}
</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)}
<td>
${"{0:0.2f}".format(item.unit_cost)}
% if item.list_price != None:
% if item.list_price != None:
<span
class=
"old-price"
>
${"{0:0.2f}".format(item.list_price)}
</span>
<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