Merge pull request #1529 from edx/flowerhack/feature/refunds-for-certificates

Flowerhack/feature/refunds for certificates
This commit is contained in:
Julia Hansbrough
2013-11-04 13:33:10 -08:00
11 changed files with 247 additions and 62 deletions

View File

@@ -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)

View File

@@ -11,3 +11,4 @@ class CourseModeFactory(DjangoModelFactory):
mode_display_name = 'audit course'
min_price = 0
currency = 'usd'
expiration_date = None

View File

@@ -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):

View File

@@ -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):
"""

View File

@@ -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."""

View File

@@ -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}))