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}))
|
||||
Reference in New Issue
Block a user