Create Donation order item
Add tax-deductible messaging to the confirmation email and receipt pages. Update receipt page to make the course URL optional
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding model 'Donation'
|
||||
db.create_table('shoppingcart_donation', (
|
||||
('orderitem_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['shoppingcart.OrderItem'], unique=True, primary_key=True)),
|
||||
('donation_type', self.gf('django.db.models.fields.CharField')(default='general', max_length=32)),
|
||||
('course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255, db_index=True)),
|
||||
))
|
||||
db.send_create_signal('shoppingcart', ['Donation'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'Donation'
|
||||
db.delete_table('shoppingcart_donation')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'shoppingcart.certificateitem': {
|
||||
'Meta': {'object_name': 'CertificateItem', '_ormbases': ['shoppingcart.OrderItem']},
|
||||
'course_enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']"}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '128', 'db_index': 'True'}),
|
||||
'mode': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
|
||||
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
|
||||
},
|
||||
'shoppingcart.coupon': {
|
||||
'Meta': {'object_name': 'Coupon'},
|
||||
'code': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255'}),
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 10, 2, 0, 0)'}),
|
||||
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
|
||||
'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'percentage_discount': ('django.db.models.fields.IntegerField', [], {'default': '0'})
|
||||
},
|
||||
'shoppingcart.couponredemption': {
|
||||
'Meta': {'object_name': 'CouponRedemption'},
|
||||
'coupon': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Coupon']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'shoppingcart.courseregistrationcode': {
|
||||
'Meta': {'object_name': 'CourseRegistrationCode'},
|
||||
'code': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 10, 2, 0, 0)'}),
|
||||
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_by_user'", 'to': "orm['auth.User']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'invoice': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Invoice']", 'null': 'True'}),
|
||||
'order': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'purchase_order'", 'null': 'True', 'to': "orm['shoppingcart.Order']"})
|
||||
},
|
||||
'shoppingcart.donation': {
|
||||
'Meta': {'object_name': 'Donation', '_ormbases': ['shoppingcart.OrderItem']},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'donation_type': ('django.db.models.fields.CharField', [], {'default': "'general'", 'max_length': '32'}),
|
||||
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
|
||||
},
|
||||
'shoppingcart.invoice': {
|
||||
'Meta': {'object_name': 'Invoice'},
|
||||
'address_line_1': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'address_line_2': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
|
||||
'address_line_3': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
|
||||
'city': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
|
||||
'company_contact_email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'company_contact_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'company_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'customer_reference_number': ('django.db.models.fields.CharField', [], {'max_length': '63', 'null': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'internal_reference': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
|
||||
'is_valid': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'recipient_email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'recipient_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'state': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
|
||||
'total_amount': ('django.db.models.fields.FloatField', [], {}),
|
||||
'zip': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True'})
|
||||
},
|
||||
'shoppingcart.order': {
|
||||
'Meta': {'object_name': 'Order'},
|
||||
'bill_to_cardtype': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
|
||||
'bill_to_ccnum': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}),
|
||||
'bill_to_city': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||
'bill_to_country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||
'bill_to_first': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||
'bill_to_last': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||
'bill_to_postalcode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}),
|
||||
'bill_to_state': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}),
|
||||
'bill_to_street1': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
|
||||
'bill_to_street2': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
|
||||
'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'processor_reply_dump': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'purchase_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'refunded_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'shoppingcart.orderitem': {
|
||||
'Meta': {'object_name': 'OrderItem'},
|
||||
'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}),
|
||||
'fulfilled_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'line_desc': ('django.db.models.fields.CharField', [], {'default': "'Misc. Item'", 'max_length': '1024'}),
|
||||
'list_price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '30', 'decimal_places': '2'}),
|
||||
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}),
|
||||
'qty': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
|
||||
'refund_requested_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'report_comments': ('django.db.models.fields.TextField', [], {'default': "''"}),
|
||||
'service_fee': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32', 'db_index': 'True'}),
|
||||
'unit_cost': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'shoppingcart.paidcourseregistration': {
|
||||
'Meta': {'object_name': 'PaidCourseRegistration', '_ormbases': ['shoppingcart.OrderItem']},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '128', 'db_index': 'True'}),
|
||||
'mode': ('django.db.models.fields.SlugField', [], {'default': "'honor'", 'max_length': '50'}),
|
||||
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
|
||||
},
|
||||
'shoppingcart.paidcourseregistrationannotation': {
|
||||
'Meta': {'object_name': 'PaidCourseRegistrationAnnotation'},
|
||||
'annotation': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
},
|
||||
'shoppingcart.registrationcoderedemption': {
|
||||
'Meta': {'object_name': 'RegistrationCodeRedemption'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']", 'null': 'True'}),
|
||||
'redeemed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 10, 2, 0, 0)', 'null': 'True'}),
|
||||
'redeemed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
|
||||
'registration_code': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.CourseRegistrationCode']"})
|
||||
},
|
||||
'student.courseenrollment': {
|
||||
'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['shoppingcart']
|
||||
@@ -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")
|
||||
|
||||
@@ -1,31 +1,41 @@
|
||||
"""
|
||||
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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user