Commit 7ab8142e by Julia Hansbrough

Response to Dave & Diana's CR

parent bf916b66
...@@ -11,7 +11,6 @@ from django.db.models import Q ...@@ -11,7 +11,6 @@ from django.db.models import Q
Mode = namedtuple('Mode', ['slug', 'name', 'min_price', 'suggested_prices', 'currency', 'expiration_date']) Mode = namedtuple('Mode', ['slug', 'name', 'min_price', 'suggested_prices', 'currency', 'expiration_date'])
class CourseMode(models.Model): class CourseMode(models.Model):
""" """
We would like to offer a course in a variety of modes. We would like to offer a course in a variety of modes.
...@@ -72,8 +71,8 @@ class CourseMode(models.Model): ...@@ -72,8 +71,8 @@ class CourseMode(models.Model):
@classmethod @classmethod
def modes_for_course_dict(cls, course_id): def modes_for_course_dict(cls, course_id):
""" """
Returns the modes for a particular course as a dictionary with Returns the non-expired modes for a particular course as a
the mode slug as the key dictionary with the mode slug as the key
""" """
return {mode.slug: mode for mode in cls.modes_for_course(course_id)} return {mode.slug: mode for mode in cls.modes_for_course(course_id)}
...@@ -82,6 +81,8 @@ class CourseMode(models.Model): ...@@ -82,6 +81,8 @@ class CourseMode(models.Model):
""" """
Returns the mode for the course corresponding to mode_slug. Returns the mode for the course corresponding to mode_slug.
Returns only non-expired modes.
If this particular mode is not set for the course, returns None If this particular mode is not set for the course, returns None
""" """
modes = cls.modes_for_course(course_id) modes = cls.modes_for_course(course_id)
...@@ -95,7 +96,8 @@ class CourseMode(models.Model): ...@@ -95,7 +96,8 @@ class CourseMode(models.Model):
@classmethod @classmethod
def min_course_price_for_currency(cls, course_id, currency): def min_course_price_for_currency(cls, course_id, currency):
""" """
Returns the minimum price of the course in the appropriate currency over all the course's modes. Returns the minimum price of the course in the appropriate currency over all the course's
non-expired modes.
If there is no mode found, will return the price of DEFAULT_MODE, which is 0 If there is no mode found, will return the price of DEFAULT_MODE, which is 0
""" """
modes = cls.modes_for_course(course_id) modes = cls.modes_for_course(course_id)
......
...@@ -11,3 +11,4 @@ class CourseModeFactory(DjangoModelFactory): ...@@ -11,3 +11,4 @@ class CourseModeFactory(DjangoModelFactory):
mode_display_name = 'audit course' mode_display_name = 'audit course'
min_price = 0 min_price = 0
currency = 'usd' currency = 'usd'
expiration_date = None
...@@ -23,14 +23,13 @@ from django.contrib.auth.signals import user_logged_in, user_logged_out ...@@ -23,14 +23,13 @@ from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.db import models from django.db import models
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
import django.dispatch
from django.forms import ModelForm, forms from django.forms import ModelForm, forms
import comment_client as cc import comment_client as cc
from pytz import UTC from pytz import UTC
import django.dispatch unenroll_done = django.dispatch.Signal(providing_args=["course_enrollment"])
verified_unenroll_done = django.dispatch.Signal(providing_args=["user", "user_email", "course_id"])
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
AUDIT_LOG = logging.getLogger("audit") AUDIT_LOG = logging.getLogger("audit")
...@@ -823,10 +822,12 @@ class CourseEnrollment(models.Model): ...@@ -823,10 +822,12 @@ class CourseEnrollment(models.Model):
`course_id` is our usual course_id string (e.g. "edX/Test101/2013_Fall) `course_id` is our usual course_id string (e.g. "edX/Test101/2013_Fall)
""" """
refund_error = "Refund Error"
try: try:
record = CourseEnrollment.objects.get(user=user, course_id=course_id) record = CourseEnrollment.objects.get(user=user, course_id=course_id)
record.is_active = False record.is_active = False
record.save() record.save()
unenroll_done.send(sender=cls, course_enrollment=record)
except cls.DoesNotExist: except cls.DoesNotExist:
err_msg = u"Tried to unenroll student {} from {} but they were not enrolled" err_msg = u"Tried to unenroll student {} from {} but they were not enrolled"
log.error(err_msg.format(user, course_id)) log.error(err_msg.format(user, course_id))
......
...@@ -36,8 +36,6 @@ from student.tests.test_email import mock_render_to_string ...@@ -36,8 +36,6 @@ from student.tests.test_email import mock_render_to_string
import shoppingcart import shoppingcart
from course_modes.models import CourseMode
COURSE_1 = 'edX/toy/2012_Fall' COURSE_1 = 'edX/toy/2012_Fall'
COURSE_2 = 'edx/full/6.002_Spring_2012' COURSE_2 = 'edx/full/6.002_Spring_2012'
...@@ -424,50 +422,3 @@ class PaidRegistrationTest(ModuleStoreTestCase): ...@@ -424,50 +422,3 @@ class PaidRegistrationTest(ModuleStoreTestCase):
self.assertEqual(response.content, reverse('shoppingcart.views.show_cart')) self.assertEqual(response.content, reverse('shoppingcart.views.show_cart'))
self.assertTrue(shoppingcart.models.PaidCourseRegistration.contained_in_order( self.assertTrue(shoppingcart.models.PaidCourseRegistration.contained_in_order(
shoppingcart.models.Order.get_cart_for_user(self.user), self.course.id)) shoppingcart.models.Order.get_cart_for_user(self.user), self.course.id))
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class RefundUnenrollmentTests(ModuleStoreTestCase):
"""
Tests views for unenrollment with refunds
"""
# test data
COURSE_SLUG = "100"
COURSE_NAME = "test_course"
COURSE_ORG = "EDX"
def setUp(self):
# Create course, user, and enroll them as a verified student
self.user = UserFactory.create()
self.course_id = "org/test/Test_Course"
self.cost = 40
CourseFactory.create(org='org', number='test', run='course', display_name='Test 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_mode = CourseMode(course_id=self.course_id,
mode_slug="verified",
mode_display_name="verified cert",
min_price=self.cost)
course_mode.save()
self.req_factory = RequestFactory()
course_enrollment = CourseEnrollment.create_enrollment(self.user, self.course_id, 'verified', is_active=True)
course_enrollment.save()
# Student is verified and paid; we should be able to refund them
def test_unenroll_and_refund(self):
request = self.req_factory.post(reverse('change_enrollment'), {'course_id': self.course_id, 'enrollment_action': 'unenroll'})
request.user = self.user
response = change_enrollment(request)
self.assertEqual(response.status_code, 200)
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_id))
def test_unenroll_but_no_course(self):
request = self.req_factory.post(reverse('change_enrollment'), {'course_id': 'non/existent/course', 'enrollment_action': 'unenroll'})
request.user = self.user
response = change_enrollment(request)
self.assertEqual(response.status_code, 400)
...@@ -43,7 +43,6 @@ from student.models import ( ...@@ -43,7 +43,6 @@ from student.models import (
TestCenterRegistration, TestCenterRegistrationForm, PendingNameChange, TestCenterRegistration, TestCenterRegistrationForm, PendingNameChange,
PendingEmailChange, CourseEnrollment, unique_id_for_user, PendingEmailChange, CourseEnrollment, unique_id_for_user,
get_testcenter_registration, CourseEnrollmentAllowed, UserStanding, get_testcenter_registration, CourseEnrollmentAllowed, UserStanding,
verified_unenroll_done
) )
from student.forms import PasswordResetFormNoActive from student.forms import PasswordResetFormNoActive
...@@ -472,29 +471,18 @@ def change_enrollment(request): ...@@ -472,29 +471,18 @@ def change_enrollment(request):
elif action == "unenroll": elif action == "unenroll":
try: try:
course = course_from_id(course_id) CourseEnrollment.enrollment_mode_for_user(user, course_id)
enrollment_mode = CourseEnrollment.enrollment_mode_for_user(user, course_id)
# did they sign up for verified certs?
if(enrollment_mode == 'verified'):
# If the user is allowed a refund, do so
if has_access(user, course, 'refund'):
# triggers the callback to mark the certificate as refunded
verified_unenroll_done.send(sender=request, user=user, user_email=user.email, course_id=course_id)
CourseEnrollment.unenroll(user, course_id)
org, course_num, run = course_id.split("/")
dog_stats_api.increment(
"common.student.unenrollment",
tags=["org:{0}".format(org),
"course:{0}".format(course_num),
"run:{0}".format(run)]
)
return HttpResponse()
except CourseEnrollment.DoesNotExist: except CourseEnrollment.DoesNotExist:
return HttpResponseBadRequest(_("You are not enrolled in this course")) return HttpResponseBadRequest(_("You are not enrolled in this course"))
except ItemNotFoundError: CourseEnrollment.unenroll(user, course_id)
log.warning("User {0} tried to unenroll from non-existent course {1}".format(user.username, course_id)) org, course_num, run = course_id.split("/")
return HttpResponseBadRequest(_("Course id is invalid")) dog_stats_api.increment(
"common.student.unenrollment",
tags=["org:{0}".format(org),
"course:{0}".format(course_num),
"run:{0}".format(run)]
)
return HttpResponse()
else: else:
return HttpResponseBadRequest(_("Enrollment action is invalid")) return HttpResponseBadRequest(_("Enrollment action is invalid"))
...@@ -909,7 +897,7 @@ def create_account(request, post_override=None): ...@@ -909,7 +897,7 @@ def create_account(request, post_override=None):
subject = ''.join(subject.splitlines()) subject = ''.join(subject.splitlines())
message = render_to_string('emails/activation_email.txt', d) message = render_to_string('emails/activation_email.txt', d)
# dont send email if we are doing load testing or random user generation for some reason # don't send email if we are doing load testing or random user generation for some reason
if not (settings.MITX_FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING')): if not (settings.MITX_FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING')):
try: try:
if settings.MITX_FEATURES.get('REROUTE_ACTIVATION_EMAIL'): if settings.MITX_FEATURES.get('REROUTE_ACTIVATION_EMAIL'):
......
...@@ -14,6 +14,7 @@ from xmodule.modulestore import Location ...@@ -14,6 +14,7 @@ from xmodule.modulestore import Location
from xmodule.x_module import XModule, XModuleDescriptor from xmodule.x_module import XModule, XModuleDescriptor
from student.models import CourseEnrollmentAllowed from student.models import CourseEnrollmentAllowed
from course_modes.models import CourseMode
from external_auth.models import ExternalAuthMap from external_auth.models import ExternalAuthMap
from courseware.masquerade import is_masquerading_as_student from courseware.masquerade import is_masquerading_as_student
from django.utils.timezone import UTC from django.utils.timezone import UTC
...@@ -209,18 +210,11 @@ def _has_access_course_desc(user, course, action): ...@@ -209,18 +210,11 @@ def _has_access_course_desc(user, course, action):
or not that deadline has passed; checking whether the student actually *purchased* or not that deadline has passed; checking whether the student actually *purchased*
a paid/verified certificate must be done elsewhere. a paid/verified certificate must be done elsewhere.
""" """
now = datetime.now(UTC()) course_mode = CourseMode.mode_for_course(course.id, 'verified')
course_start = course.enrollment_start if course_mode is None:
# If there *is* no start date, user can be refunded return False
if course_start is None: else:
return True
# Presently, refunds are only allowed up to two weeks after the course
# start date.
grace_period = timedelta(days=14)
refund_end = course_start + grace_period
if (now.date() <= refund_end.date()):
return True return True
return False
checkers = { checkers = {
......
...@@ -12,10 +12,14 @@ from django.utils.timezone import UTC ...@@ -12,10 +12,14 @@ from django.utils.timezone import UTC
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from course_modes.tests.factories import CourseModeFactory
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class AccessTestCase(TestCase): class AccessTestCase(TestCase):
"""
Tests for the various access controls on the student dashboard
"""
def test__has_global_staff_access(self): def test__has_global_staff_access(self):
u = Mock(is_staff=False) u = Mock(is_staff=False)
self.assertFalse(access._has_global_staff_access(u)) self.assertFalse(access._has_global_staff_access(u))
...@@ -114,19 +118,26 @@ class AccessTestCase(TestCase): ...@@ -114,19 +118,26 @@ class AccessTestCase(TestCase):
# Non-staff cannot enroll outside the open enrollment period if not specifically allowed # Non-staff cannot enroll outside the open enrollment period if not specifically allowed
def test__has_access_refund(self): def test__has_access_refund(self):
user = UserFactory.create()
course = CourseFactory.create(org='org', number='test', run='course', display_name='Test Course')
today = datetime.datetime.now(UTC()) today = datetime.datetime.now(UTC())
grace_period = datetime.timedelta(days=14)
one_day_extra = datetime.timedelta(days=1) one_day_extra = datetime.timedelta(days=1)
user = UserFactory.create()
course_nonrefundable_id = 'nonrefundable/test/Test_Course'
course_nonrefundable = CourseFactory.create(org='nonrefundable', number='test', run='course', display_name='Test Course')
course_mode_nonrefundable = CourseModeFactory.create(course_id=course_nonrefundable_id,
mode_slug='verified',
expiration_date=(today - one_day_extra))
course_mode_nonrefundable.save()
# User is allowed to receive refund if it is within two weeks of course start date course_refundable_id = 'refundable/test/Test_Course'
course.enrollment_start = (today - one_day_extra) course_refundable = CourseFactory.create(org='refundable', number='test', run='course', display_name='Test Course')
self.assertTrue(access._has_access_course_desc(user, course, 'refund')) course_mode_refundable = CourseModeFactory.create(course_id=course_refundable_id,
mode_slug='verified',
expiration_date=(today + one_day_extra))
course_mode_refundable.save()
course.enrollment_start = (today - grace_period) # User cannot receive a refund one day after the expiration date
self.assertTrue(access._has_access_course_desc(user, course, 'refund')) self.assertFalse(access._has_access_course_desc(user, course_nonrefundable, 'refund'))
# After two weeks, user may no longer receive a refund # After two weeks, user may no longer receive a refund
course.enrollment_start = (today - grace_period - one_day_extra) self.assertTrue(access._has_access_course_desc(user, course_refundable, 'refund'))
self.assertFalse(access._has_access_course_desc(user, course, 'refund'))
...@@ -8,8 +8,6 @@ from collections import namedtuple ...@@ -8,8 +8,6 @@ from collections import namedtuple
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 django.dispatch import receiver from django.dispatch import receiver
from student.models import verified_unenroll_done
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
...@@ -26,7 +24,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError ...@@ -26,7 +24,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from course_modes.models import CourseMode from course_modes.models import CourseMode
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
from student.views import course_from_id from student.views import course_from_id
from student.models import CourseEnrollment from student.models import CourseEnrollment, unenroll_done
from verify_student.models import SoftwareSecurePhotoVerification from verify_student.models import SoftwareSecurePhotoVerification
...@@ -402,38 +400,48 @@ class CertificateItem(OrderItem): ...@@ -402,38 +400,48 @@ class CertificateItem(OrderItem):
course_enrollment = models.ForeignKey(CourseEnrollment) course_enrollment = models.ForeignKey(CourseEnrollment)
mode = models.SlugField() mode = models.SlugField()
@classmethod @receiver(unenroll_done, sender=CourseEnrollment)
@receiver(verified_unenroll_done)
def refund_cert_callback(sender, **kwargs): def refund_cert_callback(sender, **kwargs):
""" """
When a CourseEnrollment object whose mode is 'verified' has its is_active field set to false (i.e. when a student When a CourseEnrollment object calls its unenroll method, this function checks to see if that unenrollment
is unenrolled), this callback ensures that the associated CertificateItem is marked as refunded, and that an occurred in a verified certificate that was within the refund deadline. If so, it actually performs the
appropriate email is sent to billing. refund.
Returns the refunded certificate on a successful refund. If no refund is necessary, it returns nothing.
If an error that the user should see occurs, it returns a string specifying the error.
""" """
try: mode = kwargs['course_enrollment'].mode
course_id = kwargs['course_id'] course_id = kwargs['course_enrollment'].course_id
user = kwargs['user'] user = kwargs['course_enrollment'].user
# If there's duplicate entries, just grab the first one and refund it (though in most cases we should only get one) # Only refund verified cert unenrollments that are within bounds of the expiration date
target_certs = CertificateItem.objects.filter(course_id=course_id, user_id=user, status='purchased', mode='verified') if (mode != 'verified'):
target_cert = target_certs[0] return
target_cert.status = 'refunded'
target_cert.save()
order_number = target_cert.order_id if (CourseMode.mode_for_course(course_id, 'verified') is None):
return
# send billing an email so they can handle refunding target_certs = CertificateItem.objects.filter(course_id=course_id, user_id=user, status='purchased', mode='verified')
subject = _("[Refund] User-Requested Refund") try:
message = "User " + str(user) + "(" + str(user.email) + ") has requested a refund on Order #" + str(order_number) + "." target_cert = target_certs[0]
to_email = [settings.PAYMENT_SUPPORT_EMAIL] except IndexError:
from_email = "support@edx.org" log.error("Matching CertificateItem not found while trying to refund. User %s, Course %s", user, course_id)
return
target_cert.status = 'refunded'
target_cert.save()
order_number = target_cert.order_id
# send billing an email so they can handle refunding
subject = _("[Refund] User-Requested Refund")
message = "User {user} ({user_email}) has requested a refund on Order #{order_number}.".format(user=user, user_email=user.email, order_number=order_number)
to_email = [settings.PAYMENT_SUPPORT_EMAIL]
from_email = [settings.PAYMENT_SUPPORT_EMAIL]
try:
send_mail(subject, message, from_email, to_email, fail_silently=False) send_mail(subject, message, from_email, to_email, fail_silently=False)
except (smtplib.SMTPException, BotoServerError):
log.error('Failed sending email to billing request a refund for verified certiciate (User %s, Course %s)', user, course_id)
return target_cert return target_cert
except IndexError:
log.exception("Matching CertificateItem not found while trying to refund. User %s, Course %s", user, course_id)
raise IndexError
@classmethod @classmethod
@transaction.commit_on_success @transaction.commit_on_success
......
...@@ -17,9 +17,11 @@ from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE ...@@ -17,9 +17,11 @@ from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from shoppingcart.models import (Order, OrderItem, CertificateItem, InvalidCartItem, PaidCourseRegistration, from shoppingcart.models import (Order, OrderItem, CertificateItem, InvalidCartItem, PaidCourseRegistration,
OrderItemSubclassPK) OrderItemSubclassPK)
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from student.models import CourseEnrollment, verified_unenroll_done 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
from django.utils.timezone import UTC
import datetime
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
...@@ -361,17 +363,85 @@ class CertificateItemTest(ModuleStoreTestCase): ...@@ -361,17 +363,85 @@ class CertificateItemTest(ModuleStoreTestCase):
self.assertEquals(cert_item.single_item_receipt_template, self.assertEquals(cert_item.single_item_receipt_template,
'shoppingcart/receipt.html') 'shoppingcart/receipt.html')
def test_refund_cert_callback(self): def test_refund_cert_callback_no_expiration(self):
# enroll and buy; dup from test_existing_enrollment # enroll and buy; dup from test_existing_enrollment
CourseEnrollment.enroll(self.user, self.course_id) CourseEnrollment.enroll(self.user, self.course_id, 'verified')
cart = Order.get_cart_for_user(user=self.user) cart = Order.get_cart_for_user(user=self.user)
CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified') CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified')
cart.purchase() cart.purchase()
# now that it's there, let's try refunding it # now that it's there, let's try refunding it
verified_unenroll_done.send(sender=self, user=self.user, user_email=self.user.email, course_id=self.course_id) CourseEnrollment.unenroll(self.user, self.course_id)
target_certs = CertificateItem.objects.filter(course_id=self.course_id, user_id=self.user, status='refunded', mode='verified') target_certs = CertificateItem.objects.filter(course_id=self.course_id, user_id=self.user, status='refunded', mode='verified')
self.assertTrue(target_certs[0]) self.assertTrue(target_certs[0])
def test_refund_cert_callback_before_expiration(self):
# enroll and buy; dup from test_existing_enrollment
course_id = "refund_before_expiration/test/one"
many_days = datetime.timedelta(days=60)
CourseFactory.create(org='refund_before_expiration', number='test', run='course', display_name='one')
course_mode = CourseMode(course_id=course_id,
mode_slug="verified",
mode_display_name="verified cert",
min_price=self.cost,
expiration_date=(datetime.datetime.now(UTC()).date() + many_days))
course_mode.save()
CourseEnrollment.enroll(self.user, course_id, 'verified')
cart = Order.get_cart_for_user(user=self.user)
CertificateItem.add_to_order(cart, course_id, self.cost, 'verified')
cart.purchase()
# now that it's there, let's try refunding it
CourseEnrollment.unenroll(self.user, course_id)
target_certs = CertificateItem.objects.filter(course_id=course_id, user_id=self.user, status='refunded', mode='verified')
self.assertTrue(target_certs[0])
@patch('shoppingcart.models.log.error')
def test_refund_cert_callback_before_expiration_email_error(self, error_logger):
course_id = "refund_before_expiration/test/one"
many_days = datetime.timedelta(days=60)
CourseFactory.create(org='refund_before_expiration', number='test', run='course', display_name='one')
course_mode = CourseMode(course_id=course_id,
mode_slug="verified",
mode_display_name="verified cert",
min_price=self.cost,
expiration_date=(datetime.datetime.now(UTC()).date() + many_days))
course_mode.save()
CourseEnrollment.enroll(self.user, course_id, 'verified')
cart = Order.get_cart_for_user(user=self.user)
CertificateItem.add_to_order(cart, course_id, self.cost, 'verified')
cart.purchase()
# now that it's there, let's try refunding it
with patch('shoppingcart.models.send_mail', side_effect=smtplib.SMTPException):
CourseEnrollment.unenroll(self.user, course_id)
self.assertTrue(error_logger.called)
def test_refund_cert_callback_after_expiration(self):
# Enroll and buy
course_id = "refund_after_expiration/test/two"
CourseFactory.create(org='refund_after_expiration', number='test', run='course', display_name='two')
course_mode = CourseMode(course_id=course_id,
mode_slug="verified",
mode_display_name="verified cert",
min_price=self.cost,)
course_mode.save()
CourseEnrollment.enroll(self.user, course_id, 'verified')
cart = Order.get_cart_for_user(user=self.user)
CertificateItem.add_to_order(cart, course_id, self.cost, 'verified')
cart.purchase()
course_mode.save()
# now that it's there, let's try refunding it
CourseEnrollment.unenroll(self.user, course_id)
target_certs = CertificateItem.objects.filter(course_id=course_id, user_id=self.user, status='refunded', mode='verified')
self.assertTrue(target_certs[0])
def test_refund_cert_no_cert_exists(self): def test_refund_cert_no_cert_exists(self):
with self.assertRaises(IndexError): CourseEnrollment.enroll(self.user, self.course_id, 'verified')
verified_unenroll_done.send(sender=self, user=self.user, user_email=self.user.email, course_id=self.course_id) ret_val = CourseEnrollment.unenroll(self.user, self.course_id)
self.assertFalse(ret_val)
...@@ -19,7 +19,7 @@ DEBUG = True ...@@ -19,7 +19,7 @@ DEBUG = True
USE_I18N = True USE_I18N = True
TEMPLATE_DEBUG = True TEMPLATE_DEBUG = True
MITX_FEATURES['ENABLE_PAYMENT_FAKE'] = True
MITX_FEATURES['DISABLE_START_DATES'] = False MITX_FEATURES['DISABLE_START_DATES'] = False
MITX_FEATURES['ENABLE_SQL_TRACKING_LOGS'] = True MITX_FEATURES['ENABLE_SQL_TRACKING_LOGS'] = True
MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = False # Enable to test subdomains--otherwise, want all courses to show up MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = False # Enable to test subdomains--otherwise, want all courses to show up
...@@ -269,7 +269,7 @@ if SEGMENT_IO_LMS_KEY: ...@@ -269,7 +269,7 @@ if SEGMENT_IO_LMS_KEY:
CC_PROCESSOR['CyberSource']['SHARED_SECRET'] = os.environ.get('CYBERSOURCE_SHARED_SECRET', '') CC_PROCESSOR['CyberSource']['SHARED_SECRET'] = os.environ.get('CYBERSOURCE_SHARED_SECRET', '')
CC_PROCESSOR['CyberSource']['MERCHANT_ID'] = os.environ.get('CYBERSOURCE_MERCHANT_ID', '') CC_PROCESSOR['CyberSource']['MERCHANT_ID'] = os.environ.get('CYBERSOURCE_MERCHANT_ID', '')
CC_PROCESSOR['CyberSource']['SERIAL_NUMBER'] = os.environ.get('CYBERSOURCE_SERIAL_NUMBER', '') CC_PROCESSOR['CyberSource']['SERIAL_NUMBER'] = os.environ.get('CYBERSOURCE_SERIAL_NUMBER', '')
CC_PROCESSOR['CyberSource']['PURCHASE_ENDPOINT'] = os.environ.get('CYBERSOURCE_PURCHASE_ENDPOINT', '') CC_PROCESSOR['CyberSource']['PURCHASE_ENDPOINT'] = '/shoppingcart/payment_fake/'
########################## USER API ######################## ########################## USER API ########################
......
...@@ -144,9 +144,9 @@ ...@@ -144,9 +144,9 @@
% if enrollment.mode != "verified": % if enrollment.mode != "verified":
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}" onclick="document.getElementById('refund-info').innerHTML=''">${_('Unregister')}</a> <a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}" onclick="document.getElementById('refund-info').innerHTML=''">${_('Unregister')}</a>
% elif show_refund_option: % elif show_refund_option:
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}" onclick="document.getElementById('refund-info').innerHTML=gettext('You will be refunded for the amount you paid')">${_('Unregister')}</a> <a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}" onclick="document.getElementById('refund-info').innerHTML=gettext('You will be refunded the amount you paid.')">${_('Unregister')}</a>
% else: % else:
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}" onclick="document.getElementById('refund-info').innerHTML=gettext('The refund deadline for this course has passed, so you will not receive money back')">${_('Unregister')}</a> <a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}" onclick="document.getElementById('refund-info').innerHTML=gettext('The refund deadline for this course has passed, so you will not receive a refund.')">${_('Unregister')}</a>
% endif % endif
% if show_email_settings: % if show_email_settings:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment