Merge pull request #1529 from edx/flowerhack/feature/refunds-for-certificates
Flowerhack/feature/refunds for certificates
This commit is contained in:
@@ -11,7 +11,6 @@ from django.db.models import Q
|
||||
|
||||
Mode = namedtuple('Mode', ['slug', 'name', 'min_price', 'suggested_prices', 'currency', 'expiration_date'])
|
||||
|
||||
|
||||
class CourseMode(models.Model):
|
||||
"""
|
||||
We would like to offer a course in a variety of modes.
|
||||
@@ -72,8 +71,8 @@ class CourseMode(models.Model):
|
||||
@classmethod
|
||||
def modes_for_course_dict(cls, course_id):
|
||||
"""
|
||||
Returns the modes for a particular course as a dictionary with
|
||||
the mode slug as the key
|
||||
Returns the non-expired modes for a particular course as a
|
||||
dictionary with the mode slug as the key
|
||||
"""
|
||||
return {mode.slug: mode for mode in cls.modes_for_course(course_id)}
|
||||
|
||||
@@ -82,6 +81,8 @@ class CourseMode(models.Model):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
modes = cls.modes_for_course(course_id)
|
||||
@@ -95,7 +96,8 @@ class CourseMode(models.Model):
|
||||
@classmethod
|
||||
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
|
||||
"""
|
||||
modes = cls.modes_for_course(course_id)
|
||||
|
||||
@@ -11,3 +11,4 @@ class CourseModeFactory(DjangoModelFactory):
|
||||
mode_display_name = 'audit course'
|
||||
min_price = 0
|
||||
currency = 'usd'
|
||||
expiration_date = None
|
||||
|
||||
@@ -31,7 +31,7 @@ class CourseModeModelTest(TestCase):
|
||||
mode_slug=mode_slug,
|
||||
min_price=min_price,
|
||||
suggested_prices=suggested_prices,
|
||||
currency=currency
|
||||
currency=currency,
|
||||
)
|
||||
|
||||
def test_modes_for_course_empty(self):
|
||||
|
||||
@@ -17,18 +17,20 @@ import json
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
import django.dispatch
|
||||
from django.forms import ModelForm, forms
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
import comment_client as cc
|
||||
from pytz import UTC
|
||||
|
||||
unenroll_done = django.dispatch.Signal(providing_args=["course_enrollment"])
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
AUDIT_LOG = logging.getLogger("audit")
|
||||
@@ -825,6 +827,7 @@ class CourseEnrollment(models.Model):
|
||||
record = CourseEnrollment.objects.get(user=user, course_id=course_id)
|
||||
record.is_active = False
|
||||
record.save()
|
||||
unenroll_done.send(sender=cls, course_enrollment=record)
|
||||
except cls.DoesNotExist:
|
||||
err_msg = u"Tried to unenroll student {} from {} but they were not enrolled"
|
||||
log.error(err_msg.format(user, course_id))
|
||||
@@ -924,6 +927,18 @@ class CourseEnrollment(models.Model):
|
||||
self.is_active = False
|
||||
self.save()
|
||||
|
||||
def refundable(self):
|
||||
"""
|
||||
For paid/verified certificates, students may receive a refund IFF they have
|
||||
a verified certificate and the deadline for refunds has not yet passed.
|
||||
"""
|
||||
course_mode = CourseMode.mode_for_course(self.course_id, 'verified')
|
||||
if course_mode is None:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
|
||||
class CourseEnrollmentAllowed(models.Model):
|
||||
"""
|
||||
|
||||
@@ -256,6 +256,22 @@ class DashboardTest(TestCase):
|
||||
self.assertFalse(course_mode_info['show_upsell'])
|
||||
self.assertIsNone(course_mode_info['days_for_upsell'])
|
||||
|
||||
def test_refundable(self):
|
||||
verified_mode = CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
mode_slug='verified',
|
||||
mode_display_name='Verified',
|
||||
expiration_date=(datetime.now(pytz.UTC) + timedelta(days=1)).date()
|
||||
)
|
||||
enrollment = CourseEnrollment.enroll(self.user, self.course.id, mode='verified')
|
||||
|
||||
self.assertTrue(enrollment.refundable())
|
||||
|
||||
verified_mode.expiration_date = (datetime.now(pytz.UTC) - timedelta(days=1)).date()
|
||||
verified_mode.save()
|
||||
self.assertFalse(enrollment.refundable())
|
||||
|
||||
|
||||
|
||||
class EnrollInCourseTest(TestCase):
|
||||
"""Tests enrolling and unenrolling in courses."""
|
||||
|
||||
@@ -47,7 +47,6 @@ from student.models import (
|
||||
from student.forms import PasswordResetFormNoActive
|
||||
|
||||
from verify_student.models import SoftwareSecurePhotoVerification
|
||||
|
||||
from certificates.models import CertificateStatuses, certificate_status_for_student
|
||||
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
@@ -73,6 +72,7 @@ from pytz import UTC
|
||||
|
||||
from util.json_request import JsonResponse
|
||||
|
||||
|
||||
log = logging.getLogger("mitx.student")
|
||||
AUDIT_LOG = logging.getLogger("audit")
|
||||
|
||||
@@ -296,13 +296,13 @@ def complete_course_mode_info(course_id, enrollment):
|
||||
def dashboard(request):
|
||||
user = request.user
|
||||
|
||||
# Build our courses list for the user, but ignore any courses that no longer
|
||||
# exist (because the course IDs have changed). Still, we don't delete those
|
||||
# Build our (course, enorllment) list for the user, but ignore any courses that no
|
||||
# longer exist (because the course IDs have changed). Still, we don't delete those
|
||||
# enrollments, because it could have been a data push snafu.
|
||||
courses = []
|
||||
course_enrollment_pairs = []
|
||||
for enrollment in CourseEnrollment.enrollments_for_user(user):
|
||||
try:
|
||||
courses.append((course_from_id(enrollment.course_id), enrollment))
|
||||
course_enrollment_pairs.append((course_from_id(enrollment.course_id), enrollment))
|
||||
except ItemNotFoundError:
|
||||
log.error("User {0} enrolled in non-existent course {1}"
|
||||
.format(user.username, enrollment.course_id))
|
||||
@@ -321,22 +321,27 @@ def dashboard(request):
|
||||
staff_access = True
|
||||
errored_courses = modulestore().get_errored_courses()
|
||||
|
||||
show_courseware_links_for = frozenset(course.id for course, _enrollment in courses
|
||||
show_courseware_links_for = frozenset(course.id for course, _enrollment in course_enrollment_pairs
|
||||
if has_access(request.user, course, 'load'))
|
||||
|
||||
course_modes = {course.id: complete_course_mode_info(course.id, enrollment) for course, enrollment in courses}
|
||||
cert_statuses = {course.id: cert_info(request.user, course) for course, _enrollment in courses}
|
||||
course_modes = {course.id: complete_course_mode_info(course.id, enrollment) for course, enrollment in course_enrollment_pairs}
|
||||
cert_statuses = {course.id: cert_info(request.user, course) for course, _enrollment in course_enrollment_pairs}
|
||||
|
||||
# only show email settings for Mongo course and when bulk email is turned on
|
||||
show_email_settings_for = frozenset(
|
||||
course.id for course, _enrollment in courses if (
|
||||
course.id for course, _enrollment in course_enrollment_pairs if (
|
||||
settings.MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and
|
||||
modulestore().get_modulestore_type(course.id) == MONGO_MODULESTORE_TYPE and
|
||||
CourseAuthorization.instructor_email_enabled(course.id)
|
||||
)
|
||||
)
|
||||
|
||||
# Verification Attempts
|
||||
verification_status, verification_msg = SoftwareSecurePhotoVerification.user_status(user)
|
||||
|
||||
show_refund_option_for = frozenset(course.id for course, _enrollment in course_enrollment_pairs
|
||||
if _enrollment.refundable())
|
||||
|
||||
# get info w.r.t ExternalAuthMap
|
||||
external_auth_map = None
|
||||
try:
|
||||
@@ -344,7 +349,7 @@ def dashboard(request):
|
||||
except ExternalAuthMap.DoesNotExist:
|
||||
pass
|
||||
|
||||
context = {'courses': courses,
|
||||
context = {'course_enrollment_pairs': course_enrollment_pairs,
|
||||
'course_optouts': course_optouts,
|
||||
'message': message,
|
||||
'external_auth_map': external_auth_map,
|
||||
@@ -356,6 +361,7 @@ def dashboard(request):
|
||||
'show_email_settings_for': show_email_settings_for,
|
||||
'verification_status': verification_status,
|
||||
'verification_msg': verification_msg,
|
||||
'show_refund_option_for': show_refund_option_for,
|
||||
}
|
||||
|
||||
return render_to_response('dashboard.html', context)
|
||||
@@ -463,20 +469,17 @@ def change_enrollment(request):
|
||||
)
|
||||
|
||||
elif action == "unenroll":
|
||||
try:
|
||||
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:
|
||||
if not CourseEnrollment.is_enrolled(user, course_id):
|
||||
return HttpResponseBadRequest(_("You are not enrolled in this course"))
|
||||
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()
|
||||
else:
|
||||
return HttpResponseBadRequest(_("Enrollment action is invalid"))
|
||||
|
||||
@@ -891,7 +894,7 @@ def create_account(request, post_override=None):
|
||||
subject = ''.join(subject.splitlines())
|
||||
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')):
|
||||
try:
|
||||
if settings.MITX_FEATURES.get('REROUTE_ACTIVATION_EMAIL'):
|
||||
@@ -1512,4 +1515,4 @@ def change_email_settings(request):
|
||||
log.info(u"User {0} ({1}) opted out of receiving emails from course {2}".format(user.username, user.email, course_id))
|
||||
track.views.server_track(request, "change-email-settings", {"receive_emails": "no", "course": course_id}, page='dashboard')
|
||||
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
@@ -1,15 +1,21 @@
|
||||
from mock import Mock
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
import courseware.access as access
|
||||
from courseware.tests.tests import TEST_DATA_MIXED_MODULESTORE
|
||||
from .factories import CourseEnrollmentAllowedFactory
|
||||
import datetime
|
||||
from django.utils.timezone import UTC
|
||||
import pytz
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class AccessTestCase(TestCase):
|
||||
"""
|
||||
Tests for the various access controls on the student dashboard
|
||||
"""
|
||||
def test__has_global_staff_access(self):
|
||||
u = Mock(is_staff=False)
|
||||
self.assertFalse(access._has_global_staff_access(u))
|
||||
@@ -71,7 +77,7 @@ class AccessTestCase(TestCase):
|
||||
# TODO: override DISABLE_START_DATES and test the start date branch of the method
|
||||
u = Mock()
|
||||
d = Mock()
|
||||
d.start = datetime.datetime.now(UTC()) - datetime.timedelta(days=1) # make sure the start time is in the past
|
||||
d.start = datetime.datetime.now(pytz.utc) - datetime.timedelta(days=1) # make sure the start time is in the past
|
||||
|
||||
# Always returns true because DISABLE_START_DATES is set in test.py
|
||||
self.assertTrue(access._has_access_descriptor(u, d, 'load'))
|
||||
@@ -79,8 +85,8 @@ class AccessTestCase(TestCase):
|
||||
|
||||
def test__has_access_course_desc_can_enroll(self):
|
||||
u = Mock()
|
||||
yesterday = datetime.datetime.now(UTC()) - datetime.timedelta(days=1)
|
||||
tomorrow = datetime.datetime.now(UTC()) + datetime.timedelta(days=1)
|
||||
yesterday = datetime.datetime.now(pytz.utc) - datetime.timedelta(days=1)
|
||||
tomorrow = datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1)
|
||||
c = Mock(enrollment_start=yesterday, enrollment_end=tomorrow, enrollment_domain='')
|
||||
|
||||
# User can enroll if it is between the start and end dates
|
||||
|
||||
@@ -7,6 +7,7 @@ from model_utils.managers import InheritanceManager
|
||||
from collections import namedtuple
|
||||
from boto.exception import BotoServerError # this is a super-class of SESError and catches connection errors
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
@@ -23,7 +24,8 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from course_modes.models import CourseMode
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
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 .exceptions import (InvalidCartItem, PurchasedCallbackException, ItemAlreadyInCartException,
|
||||
@@ -34,7 +36,7 @@ log = logging.getLogger("shoppingcart")
|
||||
ORDER_STATUSES = (
|
||||
('cart', 'cart'),
|
||||
('purchased', 'purchased'),
|
||||
('refunded', 'refunded'), # Not used for now
|
||||
('refunded', 'refunded'),
|
||||
)
|
||||
|
||||
# we need a tuple to represent the primary key of various OrderItem subclasses
|
||||
@@ -398,6 +400,49 @@ class CertificateItem(OrderItem):
|
||||
course_enrollment = models.ForeignKey(CourseEnrollment)
|
||||
mode = models.SlugField()
|
||||
|
||||
@receiver(unenroll_done, sender=CourseEnrollment)
|
||||
def refund_cert_callback(sender, course_enrollment=None, **kwargs):
|
||||
"""
|
||||
When a CourseEnrollment object calls its unenroll method, this function checks to see if that unenrollment
|
||||
occurred in a verified certificate that was within the refund deadline. If so, it actually performs the
|
||||
refund.
|
||||
|
||||
Returns the refunded certificate on a successful refund; else, it returns nothing.
|
||||
"""
|
||||
|
||||
# Only refund verified cert unenrollments that are within bounds of the expiration date
|
||||
if not course_enrollment.refundable():
|
||||
return
|
||||
|
||||
target_certs = CertificateItem.objects.filter(course_id=course_enrollment.course_id, user_id=course_enrollment.user, status='purchased', mode='verified')
|
||||
try:
|
||||
target_cert = target_certs[0]
|
||||
except IndexError:
|
||||
log.error("Matching CertificateItem not found while trying to refund. User %s, Course %s", course_enrollment.user, course_enrollment.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=course_enrollment.user,
|
||||
user_email=course_enrollment.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)
|
||||
except (smtplib.SMTPException, BotoServerError):
|
||||
err_str = 'Failed sending email to billing request a refund for verified certiciate (User {user}, Course {course}, CourseEnrollmentID {ce_id}, Order #{order})'
|
||||
log.error(err_str.format(
|
||||
user=course_enrollment.user,
|
||||
course=course_enrollment.course_id,
|
||||
ce_id=course_enrollment.id,
|
||||
order=order_number))
|
||||
|
||||
return target_cert
|
||||
|
||||
@classmethod
|
||||
@transaction.commit_on_success
|
||||
def add_to_order(cls, order, course_id, cost, mode, currency='usd'):
|
||||
|
||||
@@ -20,6 +20,8 @@ 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
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
@@ -360,3 +362,89 @@ class CertificateItemTest(ModuleStoreTestCase):
|
||||
cert_item = CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor')
|
||||
self.assertEquals(cert_item.single_item_receipt_template,
|
||||
'shoppingcart/receipt.html')
|
||||
|
||||
def test_refund_cert_callback_no_expiration(self):
|
||||
# When there is no expiration date on a verified mode, the user can always get a refund
|
||||
CourseEnrollment.enroll(self.user, self.course_id, 'verified')
|
||||
cart = Order.get_cart_for_user(user=self.user)
|
||||
CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified')
|
||||
cart.purchase()
|
||||
|
||||
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')
|
||||
self.assertTrue(target_certs[0])
|
||||
|
||||
def test_refund_cert_callback_before_expiration(self):
|
||||
# If the expiration date has not yet passed on a verified mode, the user can be refunded
|
||||
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(pytz.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()
|
||||
|
||||
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):
|
||||
# If there's an error sending an email to billing, we need to log this error
|
||||
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(pytz.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()
|
||||
|
||||
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):
|
||||
# If the expiration date has passed, the user cannot get a refund
|
||||
course_id = "refund_after_expiration/test/two"
|
||||
many_days = datetime.timedelta(days=60)
|
||||
|
||||
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.expiration_date = (datetime.datetime.now(pytz.utc).date() - many_days)
|
||||
course_mode.save()
|
||||
|
||||
CourseEnrollment.unenroll(self.user, course_id)
|
||||
target_certs = CertificateItem.objects.filter(course_id=course_id, user_id=self.user, status='refunded', mode='verified')
|
||||
self.assertEqual(len(target_certs), 0)
|
||||
|
||||
def test_refund_cert_no_cert_exists(self):
|
||||
# If there is no paid certificate, the refund callback should return nothing
|
||||
CourseEnrollment.enroll(self.user, self.course_id, 'verified')
|
||||
ret_val = CourseEnrollment.unenroll(self.user, self.course_id)
|
||||
self.assertFalse(ret_val)
|
||||
|
||||
@@ -183,14 +183,15 @@
|
||||
<h2>${_("Current Courses")}</h2>
|
||||
</header>
|
||||
|
||||
% if len(courses) > 0:
|
||||
% if len(course_enrollment_pairs) > 0:
|
||||
<ul class="listing-courses">
|
||||
% for course, enrollment in courses:
|
||||
% for course, enrollment in course_enrollment_pairs:
|
||||
<% show_courseware_link = (course.id in show_courseware_links_for) %>
|
||||
<% cert_status = cert_statuses.get(course.id) %>
|
||||
<% show_email_settings = (course.id in show_email_settings_for) %>
|
||||
<% course_mode_info = all_course_modes.get(course.id) %>
|
||||
<%include file='dashboard/_dashboard_course_listing.html' args="course=course, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info" />
|
||||
<% show_refund_option = (course.id in show_refund_option_for) %>
|
||||
<%include file='dashboard/_dashboard_course_listing.html' args="course=course, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option = show_refund_option" />
|
||||
% endfor
|
||||
|
||||
</ul>
|
||||
@@ -244,26 +245,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="unenroll-modal" class="modal unenroll-modal" aria-hidden="true">
|
||||
<div class="inner-wrapper" role="alertdialog" aria-labelledy="unenrollment-modal-title">
|
||||
<button class="close-modal">✕ <span class="sr">${_('Close Modal')}</span></button>
|
||||
|
||||
<header>
|
||||
<h2 id="unenrollment-modal-title">${_('Are you sure you want to unregister from {course_number}?').format(course_number='<span id="unenroll_course_number"></span>')}<span class="sr">, ${_("modal open")}</span></h2>
|
||||
<hr/>
|
||||
</header>
|
||||
|
||||
<div id="unenroll_error" class="modal-form-error"></div>
|
||||
|
||||
<form id="unenroll_form" method="post" data-remote="true" action="${reverse('change_enrollment')}">
|
||||
<input name="course_id" id="unenroll_course_id" type="hidden" />
|
||||
<input name="enrollment_action" type="hidden" value="unenroll" />
|
||||
<div class="submit">
|
||||
<input name="submit" type="submit" value="${_('Unregister')}" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="password_reset_complete" class="modal" aria-hidden="true">
|
||||
<div class="inner-wrapper" role="dialog" aria-labelledby="password-reset-email">
|
||||
@@ -341,3 +323,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="unenroll-modal" class="modal unenroll-modal" aria-hidden="true">
|
||||
<div class="inner-wrapper" role="alertdialog" aria-labelledy="unenrollment-modal-title">
|
||||
<button class="close-modal">✕ <span class="sr">${_('Close Modal')}</span></button>
|
||||
<header>
|
||||
<h2 id="unenrollment-modal-title">${_('<span id="track-info"></span> {course_number}? <span id="refund-info"></span>').format(course_number='<span id="unenroll_course_number"></span>')}<span class="sr">, ${_("modal open")}</span></h2>
|
||||
refund
|
||||
<hr/>
|
||||
</header>
|
||||
<div id="unenroll_error" class="modal-form-error"></div>
|
||||
<form id="unenroll_form" method="post" data-remote="true" action="${reverse('change_enrollment')}">
|
||||
<input name="course_id" id="unenroll_course_id" type="hidden" />
|
||||
<input name="enrollment_action" type="hidden" value="unenroll" />
|
||||
<div class="submit">
|
||||
<input name="submit" type="submit" value="${_('Unregister')}" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,4 +1,4 @@
|
||||
<%page args="course, enrollment, show_courseware_link, cert_status, show_email_settings, course_mode_info" />
|
||||
<%page args="course, enrollment, show_courseware_link, cert_status, show_email_settings, course_mode_info, show_refund_option" />
|
||||
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%!
|
||||
@@ -141,12 +141,20 @@
|
||||
% endif
|
||||
% endif
|
||||
|
||||
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}">${_('Unregister')}</a>
|
||||
% 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('track-info').innerHTML='Are you sure you want to unregister from'; document.getElementById('refund-info').innerHTML=''">${_('Unregister')}</a>
|
||||
% 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('track-info').innerHTML='Are you sure you want to unregister from the verified certificate track of'; document.getElementById('refund-info').innerHTML=gettext('You will be refunded the amount you paid.')">${_('Unregister')}</a>
|
||||
% else:
|
||||
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}" onclick="document.getElementById('track-info').innerHTML='Are you sure you want to unregister from the verified certificate track of'; document.getElementById('refund-info').innerHTML=gettext('The refund deadline for this course has passed, so you will not receive a refund.')">${_('Unregister')}</a>
|
||||
% endif
|
||||
|
||||
% if show_email_settings:
|
||||
<a href="#email-settings-modal" class="email-settings" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}" data-optout="${course.id in course_optouts}">${_('Email Settings')}</a>
|
||||
% endif
|
||||
|
||||
|
||||
|
||||
</section>
|
||||
</article>
|
||||
</li>
|
||||
</li>
|
||||
Reference in New Issue
Block a user