Commit 4e454cca by Awais Qureshi

Merge pull request #7267 from edx/awais786/ECOM-911-enable-noid-paid-course

Awais786/ecom 911 enable noid paid course
parents cff9a5aa 4bab316b
...@@ -21,7 +21,8 @@ class CourseModeForm(forms.ModelForm): ...@@ -21,7 +21,8 @@ class CourseModeForm(forms.ModelForm):
COURSE_MODE_SLUG_CHOICES = ( COURSE_MODE_SLUG_CHOICES = (
[(CourseMode.DEFAULT_MODE_SLUG, CourseMode.DEFAULT_MODE_SLUG)] + [(CourseMode.DEFAULT_MODE_SLUG, CourseMode.DEFAULT_MODE_SLUG)] +
[(mode_slug, mode_slug) for mode_slug in CourseMode.VERIFIED_MODES] [(mode_slug, mode_slug) for mode_slug in CourseMode.VERIFIED_MODES] +
[(CourseMode.NO_ID_PROFESSIONAL_MODE, CourseMode.NO_ID_PROFESSIONAL_MODE)]
) )
mode_slug = forms.ChoiceField(choices=COURSE_MODE_SLUG_CHOICES) mode_slug = forms.ChoiceField(choices=COURSE_MODE_SLUG_CHOICES)
......
...@@ -65,11 +65,17 @@ class CourseMode(models.Model): ...@@ -65,11 +65,17 @@ class CourseMode(models.Model):
help_text="This is the SKU(stock keeping unit) of this mode in external services." help_text="This is the SKU(stock keeping unit) of this mode in external services."
) )
DEFAULT_MODE = Mode('honor', _('Honor Code Certificate'), 0, '', 'usd', None, None, None) HONOR = 'honor'
DEFAULT_MODE_SLUG = 'honor' PROFESSIONAL = 'professional'
VERIFIED = "verified"
AUDIT = "audit"
NO_ID_PROFESSIONAL_MODE = "no-id-professional"
DEFAULT_MODE = Mode(HONOR, _('Honor Code Certificate'), 0, '', 'usd', None, None, None)
DEFAULT_MODE_SLUG = HONOR
# Modes that allow a student to pursue a verified certificate # Modes that allow a student to pursue a verified certificate
VERIFIED_MODES = ["verified", "professional"] VERIFIED_MODES = [VERIFIED, PROFESSIONAL]
class Meta: class Meta:
""" meta attributes of this model """ """ meta attributes of this model """
...@@ -249,6 +255,23 @@ class CourseMode(models.Model): ...@@ -249,6 +255,23 @@ class CourseMode(models.Model):
return professional_mode if professional_mode else verified_mode return professional_mode if professional_mode else verified_mode
@classmethod @classmethod
def min_course_price_for_verified_for_currency(cls, course_id, currency): # pylint: disable=invalid-name
"""
Returns the minimum price of the course int he appropriate currency over all the
course's *verified*, non-expired modes.
Assuming all verified courses have a minimum price of >0, this value should always
be >0.
If no verified mode is found, 0 is returned.
"""
modes = cls.modes_for_course(course_id)
for mode in modes:
if (mode.currency == currency) and (mode.slug == 'verified'):
return mode.min_price
return 0
@classmethod
def has_verified_mode(cls, course_mode_dict): def has_verified_mode(cls, course_mode_dict):
"""Check whether the modes for a course allow a student to pursue a verfied certificate. """Check whether the modes for a course allow a student to pursue a verfied certificate.
...@@ -265,21 +288,65 @@ class CourseMode(models.Model): ...@@ -265,21 +288,65 @@ class CourseMode(models.Model):
return False return False
@classmethod @classmethod
def min_course_price_for_verified_for_currency(cls, course_id, currency): def has_professional_mode(cls, modes_dict):
""" """
Returns the minimum price of the course int he appropriate currency over all the check the course mode is profession or no-id-professional
course's *verified*, non-expired modes.
Assuming all verified courses have a minimum price of >0, this value should always Args:
be >0. modes_dict (dict): course modes.
If no verified mode is found, 0 is returned. Returns:
bool
""" """
modes = cls.modes_for_course(course_id) return cls.PROFESSIONAL in modes_dict or cls.NO_ID_PROFESSIONAL_MODE in modes_dict
for mode in modes:
if (mode.currency == currency) and (mode.slug == 'verified'): @classmethod
return mode.min_price def is_professional_mode(cls, course_mode_tuple):
return 0 """
checking that tuple is professional mode.
Args:
course_mode_tuple (tuple) : course mode tuple
Returns:
bool
"""
return course_mode_tuple.slug in [cls.PROFESSIONAL, cls.NO_ID_PROFESSIONAL_MODE] if course_mode_tuple else False
@classmethod
def is_professional_slug(cls, slug):
"""checking slug is professional
Args:
slug (str) : course mode string
Return:
bool
"""
return slug in [cls.PROFESSIONAL, cls.NO_ID_PROFESSIONAL_MODE]
@classmethod
def is_verified_mode(cls, course_mode_tuple):
"""Check whether the given modes is_verified or not.
Args:
course_mode_tuple(Mode): Mode tuple
Returns:
bool: True iff the course modes is verified else False.
"""
return course_mode_tuple.slug in cls.VERIFIED_MODES
@classmethod
def is_verified_slug(cls, mode_slug):
"""Check whether the given mode_slug is_verified or not.
Args:
mode_slug(str): Mode Slug
Returns:
bool: True iff the course mode slug is verified else False.
"""
return mode_slug in cls.VERIFIED_MODES
@classmethod @classmethod
def has_payment_options(cls, course_id): def has_payment_options(cls, course_id):
...@@ -325,8 +392,8 @@ class CourseMode(models.Model): ...@@ -325,8 +392,8 @@ class CourseMode(models.Model):
if modes_dict is None: if modes_dict is None:
modes_dict = cls.modes_for_course_dict(course_id) modes_dict = cls.modes_for_course_dict(course_id)
# Professional mode courses are always behind a paywall # Professional and no-id-professional mode courses are always behind a paywall
if "professional" in modes_dict: if cls.has_professional_mode(modes_dict):
return False return False
# White-label uses course mode honor with a price # White-label uses course mode honor with a price
...@@ -335,7 +402,7 @@ class CourseMode(models.Model): ...@@ -335,7 +402,7 @@ class CourseMode(models.Model):
return False return False
# Check that the default mode is available. # Check that the default mode is available.
return ("honor" in modes_dict) return (cls.HONOR in modes_dict)
@classmethod @classmethod
def is_white_label(cls, course_id, modes_dict=None): def is_white_label(cls, course_id, modes_dict=None):
...@@ -360,13 +427,13 @@ class CourseMode(models.Model): ...@@ -360,13 +427,13 @@ class CourseMode(models.Model):
# White-label uses course mode honor with a price # White-label uses course mode honor with a price
# to indicate that the course is behind a paywall. # to indicate that the course is behind a paywall.
if "honor" in modes_dict and len(modes_dict) == 1: if cls.HONOR in modes_dict and len(modes_dict) == 1:
if modes_dict["honor"].min_price > 0 or modes_dict["honor"].suggested_prices != '': if modes_dict["honor"].min_price > 0 or modes_dict["honor"].suggested_prices != '':
return True return True
return False return False
@classmethod @classmethod
def min_course_price_for_currency(cls, course_id, currency): def min_course_price_for_currency(cls, course_id, currency): # pylint: disable=invalid-name
""" """
Returns the minimum price of the course in the appropriate currency over all the course's Returns the minimum price of the course in the appropriate currency over all the course's
non-expired modes. non-expired modes.
...@@ -375,6 +442,96 @@ class CourseMode(models.Model): ...@@ -375,6 +442,96 @@ class CourseMode(models.Model):
modes = cls.modes_for_course(course_id) modes = cls.modes_for_course(course_id)
return min(mode.min_price for mode in modes if mode.currency == currency) return min(mode.min_price for mode in modes if mode.currency == currency)
@classmethod
def enrollment_mode_display(cls, mode, verification_status):
""" Select appropriate display strings and CSS classes.
Uses mode and verification status to select appropriate display strings and CSS classes
for certificate display.
Args:
mode (str): enrollment mode.
verification_status (str) : verification status of student
Returns:
dictionary:
"""
# import inside the function to avoid the circular import
from student.helpers import (
VERIFY_STATUS_NEED_TO_VERIFY,
VERIFY_STATUS_SUBMITTED,
VERIFY_STATUS_APPROVED
)
show_image = False
image_alt = ''
if mode == cls.VERIFIED:
if verification_status in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED]:
enrollment_title = _("Your verification is pending")
enrollment_value = _("Verified: Pending Verification")
show_image = True
image_alt = _("ID verification pending")
elif verification_status == VERIFY_STATUS_APPROVED:
enrollment_title = _("You're enrolled as a verified student")
enrollment_value = _("Verified")
show_image = True
image_alt = _("ID Verified Ribbon/Badge")
else:
enrollment_title = _("You're enrolled as an honor code student")
enrollment_value = _("Honor Code")
elif mode == cls.HONOR:
enrollment_title = _("You're enrolled as an honor code student")
enrollment_value = _("Honor Code")
elif mode == cls.AUDIT:
enrollment_title = _("You're auditing this course")
enrollment_value = _("Auditing")
elif mode in [cls.PROFESSIONAL, cls.NO_ID_PROFESSIONAL_MODE]:
enrollment_title = _("You're enrolled as a professional education student")
enrollment_value = _("Professional Ed")
else:
enrollment_title = ''
enrollment_value = ''
return {
'enrollment_title': unicode(enrollment_title),
'enrollment_value': unicode(enrollment_value),
'show_image': show_image,
'image_alt': unicode(image_alt),
'display_mode': cls._enrollment_mode_display(mode, verification_status)
}
@staticmethod
def _enrollment_mode_display(enrollment_mode, verification_status):
"""Checking enrollment mode and status and returns the display mode
Args:
enrollment_mode (str): enrollment mode.
verification_status (str) : verification status of student
Returns:
display_mode (str) : display mode for certs
"""
# import inside the function to avoid the circular import
from student.helpers import (
VERIFY_STATUS_NEED_TO_VERIFY,
VERIFY_STATUS_SUBMITTED,
VERIFY_STATUS_APPROVED
)
if enrollment_mode == CourseMode.VERIFIED:
if verification_status in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_APPROVED]:
display_mode = "verified"
else:
display_mode = "honor"
elif enrollment_mode in [CourseMode.PROFESSIONAL, CourseMode.NO_ID_PROFESSIONAL_MODE]:
display_mode = "professional"
else:
display_mode = enrollment_mode
return display_mode
def to_tuple(self): def to_tuple(self):
""" """
Takes a mode model and turns it into a model named tuple. Takes a mode model and turns it into a model named tuple.
......
...@@ -150,11 +150,17 @@ class CourseModeModelTest(TestCase): ...@@ -150,11 +150,17 @@ class CourseModeModelTest(TestCase):
honor.save() honor.save()
self.assertTrue(CourseMode.has_payment_options(self.course_key)) self.assertTrue(CourseMode.has_payment_options(self.course_key))
def test_course_has_payment_options_with_no_id_professional(self):
# Has payment options.
self.create_mode('no-id-professional', 'no-id-professional', min_price=5)
self.assertTrue(CourseMode.has_payment_options(self.course_key))
@ddt.data( @ddt.data(
([], True), ([], True),
([("honor", 0), ("audit", 0), ("verified", 100)], True), ([("honor", 0), ("audit", 0), ("verified", 100)], True),
([("honor", 100)], False), ([("honor", 100)], False),
([("professional", 100)], False), ([("professional", 100)], False),
([("no-id-professional", 100)], False),
) )
@ddt.unpack @ddt.unpack
def test_can_auto_enroll(self, modes_and_prices, can_auto_enroll): def test_can_auto_enroll(self, modes_and_prices, can_auto_enroll):
...@@ -206,3 +212,111 @@ class CourseModeModelTest(TestCase): ...@@ -206,3 +212,111 @@ class CourseModeModelTest(TestCase):
# Check that we get a default mode for when no course mode is available # Check that we get a default mode for when no course mode is available
self.assertEqual(len(all_modes[other_course_key]), 1) self.assertEqual(len(all_modes[other_course_key]), 1)
self.assertEqual(all_modes[other_course_key][0], CourseMode.DEFAULT_MODE) self.assertEqual(all_modes[other_course_key][0], CourseMode.DEFAULT_MODE)
@ddt.data('', 'no-id-professional', 'professional', 'verified')
def test_course_has_professional_mode(self, mode):
# check the professional mode.
self.create_mode(mode, 'course mode', 10)
modes_dict = CourseMode.modes_for_course_dict(self.course_key)
if mode in ['professional', 'no-id-professional']:
self.assertTrue(CourseMode.has_professional_mode(modes_dict))
else:
self.assertFalse(CourseMode.has_professional_mode(modes_dict))
@ddt.data('no-id-professional', 'professional', 'verified')
def test_course_is_professional_mode(self, mode):
# check that tuple has professional mode
course_mode, __ = self.create_mode(mode, 'course mode', 10)
if mode in ['professional', 'no-id-professional']:
self.assertTrue(CourseMode.is_professional_mode(course_mode.to_tuple()))
else:
self.assertFalse(CourseMode.is_professional_mode(course_mode.to_tuple()))
def test_course_is_professional_mode_with_invalid_tuple(self):
# check that tuple has professional mode with None
self.assertFalse(CourseMode.is_professional_mode(None))
@ddt.data(
('no-id-professional', False),
('professional', True),
('verified', True),
('honor', False),
('audit', False)
)
@ddt.unpack
def test_is_verified_slug(self, mode_slug, is_verified):
# check that mode slug is verified or not
if is_verified:
self.assertTrue(CourseMode.is_verified_slug(mode_slug))
else:
self.assertFalse(CourseMode.is_verified_slug(mode_slug))
@ddt.data(
("verified", "verify_need_to_verify"),
("verified", "verify_submitted"),
("verified", "verify_approved"),
("verified", 'dummy'),
("verified", None),
('honor', None),
('honor', 'dummy'),
('audit', None),
('professional', None),
('no-id-professional', None),
('no-id-professional', 'dummy')
)
@ddt.unpack
def test_enrollment_mode_display(self, mode, verification_status):
if mode == "verified":
self.assertEqual(
CourseMode.enrollment_mode_display(mode, verification_status),
self._enrollment_display_modes_dicts(verification_status)
)
self.assertEqual(
CourseMode.enrollment_mode_display(mode, verification_status),
self._enrollment_display_modes_dicts(verification_status)
)
self.assertEqual(
CourseMode.enrollment_mode_display(mode, verification_status),
self._enrollment_display_modes_dicts(verification_status)
)
elif mode == "honor":
self.assertEqual(
CourseMode.enrollment_mode_display(mode, verification_status),
self._enrollment_display_modes_dicts(mode)
)
elif mode == "audit":
self.assertEqual(
CourseMode.enrollment_mode_display(mode, verification_status),
self._enrollment_display_modes_dicts(mode)
)
elif mode == "professional":
self.assertEqual(
CourseMode.enrollment_mode_display(mode, verification_status),
self._enrollment_display_modes_dicts(mode)
)
def _enrollment_display_modes_dicts(self, dict_type):
"""
Helper function to generate the enrollment display mode dict.
"""
dict_keys = ['enrollment_title', 'enrollment_value', 'show_image', 'image_alt', 'display_mode']
display_values = {
"verify_need_to_verify": ["Your verification is pending", "Verified: Pending Verification", True,
'ID verification pending', 'verified'],
"verify_approved": ["You're enrolled as a verified student", "Verified", True, 'ID Verified Ribbon/Badge',
'verified'],
"verify_none": ["You're enrolled as an honor code student", "Honor Code", False, '', 'honor'],
"honor": ["You're enrolled as an honor code student", "Honor Code", False, '', 'honor'],
"audit": ["You're auditing this course", "Auditing", False, '', 'audit'],
"professional": ["You're enrolled as a professional education student", "Professional Ed", False, '',
'professional']
}
if dict_type in ['verify_need_to_verify', 'verify_submitted']:
return dict(zip(dict_keys, display_values.get('verify_need_to_verify')))
elif dict_type is None or dict_type == 'dummy':
return dict(zip(dict_keys, display_values.get('verify_none')))
else:
return dict(zip(dict_keys, display_values.get(dict_type)))
...@@ -68,6 +68,25 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -68,6 +68,25 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
else: else:
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
def test_no_id_redirect(self):
# Create the course modes
CourseModeFactory(mode_slug=CourseMode.NO_ID_PROFESSIONAL_MODE, course_id=self.course.id, min_price=100)
# Enroll the user in the test course
CourseEnrollmentFactory(
is_active=False,
mode=CourseMode.NO_ID_PROFESSIONAL_MODE,
course_id=self.course.id,
user=self.user
)
# Configure whether we're upgrading or not
url = reverse('course_modes_choose', args=[unicode(self.course.id)])
response = self.client.get(url)
# Check whether we were correctly redirected
start_flow_url = reverse('verify_student_start_flow', args=[unicode(self.course.id)])
self.assertRedirects(response, start_flow_url)
def test_no_enrollment(self): def test_no_enrollment(self):
# Create the course modes # Create the course modes
for mode in ('audit', 'honor', 'verified'): for mode in ('audit', 'honor', 'verified'):
...@@ -115,9 +134,10 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -115,9 +134,10 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
# TODO: Fix it so that response.templates works w/ mako templates, and then assert # TODO: Fix it so that response.templates works w/ mako templates, and then assert
# that the right template rendered # that the right template rendered
def test_professional_enrollment(self): @ddt.data('professional', 'no-id-professional')
def test_professional_enrollment(self, mode):
# The only course mode is professional ed # The only course mode is professional ed
CourseModeFactory(mode_slug='professional', course_id=self.course.id) CourseModeFactory(mode_slug=mode, course_id=self.course.id, min_price=1)
# Go to the "choose your track" page # Go to the "choose your track" page
choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)]) choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
...@@ -132,7 +152,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -132,7 +152,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
CourseEnrollmentFactory( CourseEnrollmentFactory(
user=self.user, user=self.user,
is_active=True, is_active=True,
mode="professional", mode=mode,
course_id=unicode(self.course.id), course_id=unicode(self.course.id),
) )
...@@ -156,7 +176,8 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -156,7 +176,8 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
def test_choose_mode_redirect(self, course_mode, expected_redirect): def test_choose_mode_redirect(self, course_mode, expected_redirect):
# Create the course modes # Create the course modes
for mode in ('audit', 'honor', 'verified'): for mode in ('audit', 'honor', 'verified'):
CourseModeFactory(mode_slug=mode, course_id=self.course.id) min_price = 0 if course_mode in ["honor", "audit"] else 1
CourseModeFactory(mode_slug=mode, course_id=self.course.id, min_price=min_price)
# Choose the mode (POST request) # Choose the mode (POST request)
choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)]) choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
......
...@@ -72,9 +72,9 @@ class ChooseModeView(View): ...@@ -72,9 +72,9 @@ class ChooseModeView(View):
# We assume that, if 'professional' is one of the modes, it is the *only* mode. # We assume that, if 'professional' is one of the modes, it is the *only* mode.
# If we offer more modes alongside 'professional' in the future, this will need to route # If we offer more modes alongside 'professional' in the future, this will need to route
# to the usual "choose your track" page. # to the usual "choose your track" page same is true for no-id-professional mode.
has_enrolled_professional = (enrollment_mode == "professional" and is_active) has_enrolled_professional = (CourseMode.is_professional_slug(enrollment_mode) and is_active)
if "professional" in modes and not has_enrolled_professional: if CourseMode.has_professional_mode(modes) and not has_enrolled_professional:
return redirect( return redirect(
reverse( reverse(
'verify_student_start_flow', 'verify_student_start_flow',
...@@ -90,7 +90,7 @@ class ChooseModeView(View): ...@@ -90,7 +90,7 @@ class ChooseModeView(View):
return redirect(reverse('dashboard')) return redirect(reverse('dashboard'))
# If a user has already paid, redirect them to the dashboard. # If a user has already paid, redirect them to the dashboard.
if is_active and enrollment_mode in CourseMode.VERIFIED_MODES: if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES + [CourseMode.NO_ID_PROFESSIONAL_MODE]):
return redirect(reverse('dashboard')) return redirect(reverse('dashboard'))
donation_for_course = request.session.get("donation_for_course", {}) donation_for_course = request.session.get("donation_for_course", {})
......
...@@ -38,7 +38,8 @@ class EnrollmentTest(TestCase): ...@@ -38,7 +38,8 @@ class EnrollmentTest(TestCase):
(['honor', 'verified', 'audit'], 'honor'), (['honor', 'verified', 'audit'], 'honor'),
# Check for professional ed happy path. # Check for professional ed happy path.
(['professional'], 'professional') (['professional'], 'professional'),
(['no-id-professional'], 'no-id-professional')
) )
@ddt.unpack @ddt.unpack
def test_enroll(self, course_modes, mode): def test_enroll(self, course_modes, mode):
...@@ -72,7 +73,8 @@ class EnrollmentTest(TestCase): ...@@ -72,7 +73,8 @@ class EnrollmentTest(TestCase):
(['honor', 'verified', 'audit'], 'honor'), (['honor', 'verified', 'audit'], 'honor'),
# Check for professional ed happy path. # Check for professional ed happy path.
(['professional'], 'professional') (['professional'], 'professional'),
(['no-id-professional'], 'no-id-professional')
) )
@ddt.unpack @ddt.unpack
def test_unenroll(self, course_modes, mode): def test_unenroll(self, course_modes, mode):
......
...@@ -1100,9 +1100,8 @@ class CourseEnrollment(models.Model): ...@@ -1100,9 +1100,8 @@ class CourseEnrollment(models.Model):
""" """
Returns True, if course is paid Returns True, if course is paid
""" """
paid_course = CourseMode.objects.filter(Q(course_id=self.course_id) & Q(mode_slug='honor') & paid_course = CourseMode.is_white_label(self.course_id)
(Q(expiration_datetime__isnull=True) | Q(expiration_datetime__gte=datetime.now(pytz.UTC)))).exclude(min_price=0) if paid_course or CourseMode.is_professional_slug(self.mode):
if paid_course or self.mode == 'professional':
return True return True
return False return False
...@@ -1154,6 +1153,12 @@ class CourseEnrollment(models.Model): ...@@ -1154,6 +1153,12 @@ class CourseEnrollment(models.Model):
def course(self): def course(self):
return modulestore().get_course(self.course_id) return modulestore().get_course(self.course_id)
def is_verified_enrollment(self):
"""
Check the course enrollment mode is verified or not
"""
return CourseMode.is_verified_slug(self.mode)
class CourseEnrollmentAllowed(models.Model): class CourseEnrollmentAllowed(models.Model):
""" """
...@@ -1403,6 +1408,9 @@ class LinkedInAddToProfileConfiguration(ConfigurationModel): ...@@ -1403,6 +1408,9 @@ class LinkedInAddToProfileConfiguration(ConfigurationModel):
"honor": ugettext_lazy(u"{platform_name} Honor Code Certificate for {course_name}"), "honor": ugettext_lazy(u"{platform_name} Honor Code Certificate for {course_name}"),
"verified": ugettext_lazy(u"{platform_name} Verified Certificate for {course_name}"), "verified": ugettext_lazy(u"{platform_name} Verified Certificate for {course_name}"),
"professional": ugettext_lazy(u"{platform_name} Professional Certificate for {course_name}"), "professional": ugettext_lazy(u"{platform_name} Professional Certificate for {course_name}"),
"no-id-professional": ugettext_lazy(
u"{platform_name} Professional Certificate for {course_name}"
),
} }
company_identifier = models.TextField( company_identifier = models.TextField(
......
...@@ -55,6 +55,7 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -55,6 +55,7 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase):
# We should NOT be auto-enrolled, because that would be giving # We should NOT be auto-enrolled, because that would be giving
# away an expensive course for free :) # away an expensive course for free :)
(['professional'], 'course_modes_choose', None), (['professional'], 'course_modes_choose', None),
(['no-id-professional'], 'course_modes_choose', None),
) )
@ddt.unpack @ddt.unpack
def test_enroll(self, course_modes, next_url, enrollment_mode): def test_enroll(self, course_modes, next_url, enrollment_mode):
...@@ -113,6 +114,9 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -113,6 +114,9 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase):
(['professional'], 'true'), (['professional'], 'true'),
(['professional'], 'false'), (['professional'], 'false'),
(['professional'], None), (['professional'], None),
(['no-id-professional'], 'true'),
(['no-id-professional'], 'false'),
(['no-id-professional'], None),
) )
@ddt.unpack @ddt.unpack
def test_enroll_with_email_opt_in(self, course_modes, email_opt_in, mock_update_email_opt_in): def test_enroll_with_email_opt_in(self, course_modes, email_opt_in, mock_update_email_opt_in):
......
...@@ -219,7 +219,10 @@ class DashboardTest(ModuleStoreTestCase): ...@@ -219,7 +219,10 @@ class DashboardTest(ModuleStoreTestCase):
attempt.approve() attempt.approve()
response = self.client.get(reverse('dashboard')) response = self.client.get(reverse('dashboard'))
self.assertContains(response, "class=\"course {0}\"".format(mode)) if mode in ['professional', 'no-id-professional']:
self.assertContains(response, 'class="course professional"')
else:
self.assertContains(response, 'class="course {0}"'.format(mode))
self.assertContains(response, value) self.assertContains(response, value)
@patch.dict("django.conf.settings.FEATURES", {'ENABLE_VERIFIED_CERTIFICATES': True}) @patch.dict("django.conf.settings.FEATURES", {'ENABLE_VERIFIED_CERTIFICATES': True})
...@@ -231,6 +234,8 @@ class DashboardTest(ModuleStoreTestCase): ...@@ -231,6 +234,8 @@ class DashboardTest(ModuleStoreTestCase):
self._check_verification_status_on('verified', 'You\'re enrolled as a verified student') self._check_verification_status_on('verified', 'You\'re enrolled as a verified student')
self._check_verification_status_on('honor', 'You\'re enrolled as an honor code student') self._check_verification_status_on('honor', 'You\'re enrolled as an honor code student')
self._check_verification_status_on('audit', 'You\'re auditing this course') self._check_verification_status_on('audit', 'You\'re auditing this course')
self._check_verification_status_on('professional', 'You\'re enrolled as a professional education student')
self._check_verification_status_on('no-id-professional', 'You\'re enrolled as a professional education student')
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def _check_verification_status_off(self, mode, value): def _check_verification_status_off(self, mode, value):
......
...@@ -909,9 +909,9 @@ def change_enrollment(request, check_access=True): ...@@ -909,9 +909,9 @@ def change_enrollment(request, check_access=True):
# If we have more than one course mode or professional ed is enabled, # If we have more than one course mode or professional ed is enabled,
# then send the user to the choose your track page. # then send the user to the choose your track page.
# (In the case of professional ed, this will redirect to a page that # (In the case of no-id-professional/professional ed, this will redirect to a page that
# funnels users directly into the verification / payment flow) # funnels users directly into the verification / payment flow)
if CourseMode.has_verified_mode(available_modes): if CourseMode.has_verified_mode(available_modes) or CourseMode.has_professional_mode(available_modes):
return HttpResponse( return HttpResponse(
reverse("course_modes_choose", kwargs={'course_id': unicode(course_id)}) reverse("course_modes_choose", kwargs={'course_id': unicode(course_id)})
) )
......
...@@ -1746,13 +1746,10 @@ class CertificateItem(OrderItem): ...@@ -1746,13 +1746,10 @@ class CertificateItem(OrderItem):
self.course_enrollment.activate() self.course_enrollment.activate()
def additional_instruction_text(self): def additional_instruction_text(self):
refund_reminder = _( verification_reminder = ""
"You have up to two weeks into the course to unenroll from the Verified Certificate option " is_enrollment_mode_verified = self.course_enrollment.is_verified_enrollment() # pylint: disable=E1101
"and receive a full refund. To receive your refund, contact {billing_email}. "
"Please include your order number in your email. "
"Please do NOT include your credit card information."
).format(billing_email=settings.PAYMENT_SUPPORT_EMAIL)
if is_enrollment_mode_verified:
domain = microsite.get_value('SITE_NAME', settings.SITE_NAME) domain = microsite.get_value('SITE_NAME', settings.SITE_NAME)
path = reverse('verify_student_verify_later', kwargs={'course_id': unicode(self.course_id)}) path = reverse('verify_student_verify_later', kwargs={'course_id': unicode(self.course_id)})
verification_url = "http://{domain}{path}".format(domain=domain, path=path) verification_url = "http://{domain}{path}".format(domain=domain, path=path)
...@@ -1761,6 +1758,15 @@ class CertificateItem(OrderItem): ...@@ -1761,6 +1758,15 @@ class CertificateItem(OrderItem):
"If you haven't verified your identity yet, please start the verification process ({verification_url})." "If you haven't verified your identity yet, please start the verification process ({verification_url})."
).format(verification_url=verification_url) ).format(verification_url=verification_url)
refund_reminder = _(
"You have up to two weeks into the course to unenroll and receive a full refund."
"To receive your refund, contact {billing_email}. "
"Please include your order number in your email. "
"Please do NOT include your credit card information."
).format(
billing_email=settings.PAYMENT_SUPPORT_EMAIL
)
# Need this to be unicode in case the reminder strings # Need this to be unicode in case the reminder strings
# have been translated and contain non-ASCII unicode # have been translated and contain non-ASCII unicode
return u"{verification_reminder} {refund_reminder}".format( return u"{verification_reminder} {refund_reminder}".format(
......
...@@ -801,6 +801,29 @@ class CertificateItemTest(ModuleStoreTestCase): ...@@ -801,6 +801,29 @@ class CertificateItemTest(ModuleStoreTestCase):
ret_val = CourseEnrollment.unenroll(self.user, self.course_key) ret_val = CourseEnrollment.unenroll(self.user, self.course_key)
self.assertFalse(ret_val) self.assertFalse(ret_val)
def test_no_id_prof_confirm_email(self):
# Pay for a no-id-professional course
course_mode = CourseMode(course_id=self.course_key,
mode_slug="no-id-professional",
mode_display_name="No Id Professional Cert",
min_price=self.cost)
course_mode.save()
CourseEnrollment.enroll(self.user, self.course_key)
cart = Order.get_cart_for_user(user=self.user)
CertificateItem.add_to_order(cart, self.course_key, self.cost, 'no-id-professional')
# verify that we are still enrolled
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_key))
self.mock_tracker.reset_mock()
cart.purchase()
enrollment = CourseEnrollment.objects.get(user=self.user, course_id=self.course_key)
self.assertEquals(enrollment.mode, u'no-id-professional')
# 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.assertNotIn("If you haven't verified your identity yet, please start the verification process", email.body)
class DonationTest(ModuleStoreTestCase): class DonationTest(ModuleStoreTestCase):
"""Tests for the donation order item type. """ """Tests for the donation order item type. """
......
...@@ -298,7 +298,7 @@ class StudentAccountLoginAndRegistrationTest(UrlResetMixin, ModuleStoreTestCase) ...@@ -298,7 +298,7 @@ class StudentAccountLoginAndRegistrationTest(UrlResetMixin, ModuleStoreTestCase)
] ]
self._assert_third_party_auth_data(response, current_provider, expected_providers) self._assert_third_party_auth_data(response, current_provider, expected_providers)
@ddt.data([], ["honor"], ["honor", "verified", "audit"], ["professional"]) @ddt.data([], ["honor"], ["honor", "verified", "audit"], ["professional"], ["no-id-professional"])
def test_third_party_auth_course_id_verified(self, modes): def test_third_party_auth_course_id_verified(self, modes):
# Create a course with the specified course modes # Create a course with the specified course modes
course = CourseFactory.create() course = CourseFactory.create()
......
...@@ -98,6 +98,21 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase): ...@@ -98,6 +98,21 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
]) ])
self._assert_upgrade_session_flag(False) self._assert_upgrade_session_flag(False)
@ddt.data("no-id-professional")
def test_start_flow_with_no_id_professional(self, course_mode):
course = self._create_course(course_mode)
# by default enrollment is honor
self._enroll(course.id, "honor")
response = self._get_page('verify_student_start_flow', course.id)
self._assert_displayed_mode(response, course_mode)
self._assert_steps_displayed(
response,
PayAndVerifyView.PAYMENT_STEPS,
PayAndVerifyView.MAKE_PAYMENT_STEP
)
self._assert_messaging(response, PayAndVerifyView.FIRST_TIME_VERIFY_MSG)
self._assert_requirements_displayed(response, [])
@ddt.data("expired", "denied") @ddt.data("expired", "denied")
def test_start_flow_expired_or_denied_verification(self, verification_status): def test_start_flow_expired_or_denied_verification(self, verification_status):
course = self._create_course("verified") course = self._create_course("verified")
...@@ -121,7 +136,8 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase): ...@@ -121,7 +136,8 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
("verified", "submitted"), ("verified", "submitted"),
("verified", "approved"), ("verified", "approved"),
("verified", "error"), ("verified", "error"),
("professional", "submitted") ("professional", "submitted"),
("no-id-professional", None),
) )
@ddt.unpack @ddt.unpack
def test_start_flow_already_verified(self, course_mode, verification_status): def test_start_flow_already_verified(self, course_mode, verification_status):
...@@ -516,6 +532,14 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase): ...@@ -516,6 +532,14 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
expected_status_code=404 expected_status_code=404
) )
@ddt.data([], ["no-id-professional", "professional"], ["honor", "audit"])
def test_no_id_professional_entry_point(self, modes_available):
course = self._create_course(*modes_available)
if "no-id-professional" in modes_available or "professional" in modes_available:
self._get_page("verify_student_start_flow", course.id, expected_status_code=200)
else:
self._get_page("verify_student_start_flow", course.id, expected_status_code=404)
@ddt.data( @ddt.data(
"verify_student_start_flow", "verify_student_start_flow",
"verify_student_verify_now", "verify_student_verify_now",
...@@ -647,7 +671,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase): ...@@ -647,7 +671,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
modulestore().update_item(course, ModuleStoreEnum.UserID.test) modulestore().update_item(course, ModuleStoreEnum.UserID.test)
for course_mode in course_modes: for course_mode in course_modes:
min_price = (self.MIN_PRICE if course_mode != "honor" else 0) min_price = (0 if course_mode in ["honor", "audit"] else self.MIN_PRICE)
CourseModeFactory( CourseModeFactory(
course_id=course.id, course_id=course.id,
mode_slug=course_mode, mode_slug=course_mode,
...@@ -826,8 +850,8 @@ class TestCreateOrder(ModuleStoreTestCase): ...@@ -826,8 +850,8 @@ class TestCreateOrder(ModuleStoreTestCase):
self.user = UserFactory.create(username="test", password="test") self.user = UserFactory.create(username="test", password="test")
self.course = CourseFactory.create() self.course = CourseFactory.create()
for mode in ('audit', 'honor', 'verified'): for mode, min_price in (('audit', 0), ('honor', 0), ('verified', 100)):
CourseModeFactory(mode_slug=mode, course_id=self.course.id) CourseModeFactory(mode_slug=mode, course_id=self.course.id, min_price=min_price)
self.client.login(username="test", password="test") self.client.login(username="test", password="test")
def test_create_order_already_verified(self): def test_create_order_already_verified(self):
...@@ -838,6 +862,7 @@ class TestCreateOrder(ModuleStoreTestCase): ...@@ -838,6 +862,7 @@ class TestCreateOrder(ModuleStoreTestCase):
url = reverse('verify_student_create_order') url = reverse('verify_student_create_order')
params = { params = {
'course_id': unicode(self.course.id), 'course_id': unicode(self.course.id),
'contribution': 100
} }
response = self.client.post(url, params) response = self.client.post(url, params)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -857,7 +882,7 @@ class TestCreateOrder(ModuleStoreTestCase): ...@@ -857,7 +882,7 @@ class TestCreateOrder(ModuleStoreTestCase):
# Create a prof ed course # Create a prof ed course
course = CourseFactory.create() course = CourseFactory.create()
CourseModeFactory(mode_slug="professional", course_id=course.id) CourseModeFactory(mode_slug="professional", course_id=course.id, min_price=10)
# Create an order for a prof ed course # Create an order for a prof ed course
url = reverse('verify_student_create_order') url = reverse('verify_student_create_order')
...@@ -872,6 +897,45 @@ class TestCreateOrder(ModuleStoreTestCase): ...@@ -872,6 +897,45 @@ class TestCreateOrder(ModuleStoreTestCase):
self.assertEqual(data['merchant_defined_data1'], unicode(course.id)) self.assertEqual(data['merchant_defined_data1'], unicode(course.id))
self.assertEqual(data['merchant_defined_data2'], "professional") self.assertEqual(data['merchant_defined_data2'], "professional")
def test_create_order_for_no_id_professional(self):
# Create a no-id-professional ed course
course = CourseFactory.create()
CourseModeFactory(mode_slug="no-id-professional", course_id=course.id, min_price=10)
# Create an order for a prof ed course
url = reverse('verify_student_create_order')
params = {
'course_id': unicode(course.id)
}
response = self.client.post(url, params)
self.assertEqual(response.status_code, 200)
# Verify that the course ID and transaction type are included in "merchant-defined data"
data = json.loads(response.content)
self.assertEqual(data['merchant_defined_data1'], unicode(course.id))
self.assertEqual(data['merchant_defined_data2'], "no-id-professional")
def test_create_order_for_multiple_paid_modes(self):
# Create a no-id-professional ed course
course = CourseFactory.create()
CourseModeFactory(mode_slug="no-id-professional", course_id=course.id, min_price=10)
CourseModeFactory(mode_slug="professional", course_id=course.id, min_price=10)
# Create an order for a prof ed course
url = reverse('verify_student_create_order')
params = {
'course_id': unicode(course.id)
}
response = self.client.post(url, params)
self.assertEqual(response.status_code, 200)
# Verify that the course ID and transaction type are included in "merchant-defined data"
data = json.loads(response.content)
self.assertEqual(data['merchant_defined_data1'], unicode(course.id))
self.assertEqual(data['merchant_defined_data2'], "no-id-professional")
def test_create_order_set_donation_amount(self): def test_create_order_set_donation_amount(self):
# Verify the student so we don't need to submit photos # Verify the student so we don't need to submit photos
self._verify_student() self._verify_student()
...@@ -959,7 +1023,7 @@ class TestCreateOrderView(ModuleStoreTestCase): ...@@ -959,7 +1023,7 @@ class TestCreateOrderView(ModuleStoreTestCase):
photo_id_image=self.IMAGE_DATA, photo_id_image=self.IMAGE_DATA,
expect_status_code=400 expect_status_code=400
) )
self.assertIn('This course doesn\'t support verified certificates', response.content) self.assertIn('This course doesn\'t support paid certificates', response.content)
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
def test_create_order_fail_with_get(self): def test_create_order_fail_with_get(self):
......
...@@ -272,15 +272,16 @@ class PayAndVerifyView(View): ...@@ -272,15 +272,16 @@ class PayAndVerifyView(View):
if redirect_url: if redirect_url:
return redirect(redirect_url) return redirect(redirect_url)
# Check that the course has an unexpired verified mode expired_verified_course_mode, unexpired_paid_course_mode = self._get_expired_verified_and_paid_mode(course_key)
course_mode, expired_course_mode = self._get_verified_modes_for_course(course_key)
if course_mode is not None: # Check that the course has an unexpired paid mode
if unexpired_paid_course_mode is not None:
if CourseMode.is_verified_mode(unexpired_paid_course_mode):
log.info( log.info(
u"Entering verified workflow for user '%s', course '%s', with current step '%s'.", u"Entering verified workflow for user '%s', course '%s', with current step '%s'.",
request.user.id, course_id, current_step request.user.id, course_id, current_step
) )
elif expired_course_mode is not None: elif expired_verified_course_mode is not None:
# Check if there is an *expired* verified course mode; # Check if there is an *expired* verified course mode;
# if so, we should show a message explaining that the verification # if so, we should show a message explaining that the verification
# deadline has passed. # deadline has passed.
...@@ -288,16 +289,16 @@ class PayAndVerifyView(View): ...@@ -288,16 +289,16 @@ class PayAndVerifyView(View):
context = { context = {
'course': course, 'course': course,
'deadline': ( 'deadline': (
get_default_time_display(expired_course_mode.expiration_datetime) get_default_time_display(expired_verified_course_mode.expiration_datetime)
if expired_course_mode.expiration_datetime else "" if expired_verified_course_mode.expiration_datetime else ""
) )
} }
return render_to_response("verify_student/missed_verification_deadline.html", context) return render_to_response("verify_student/missed_verification_deadline.html", context)
else: else:
# Otherwise, there has never been a verified mode, # Otherwise, there has never been a verified/paid mode,
# so return a page not found response. # so return a page not found response.
log.warn( log.warn(
u"No verified course mode found for course '%s' for verification flow request", u"No paid/verified course mode found for course '%s' for verification/payment flow request",
course_id course_id
) )
raise Http404 raise Http404
...@@ -307,7 +308,9 @@ class PayAndVerifyView(View): ...@@ -307,7 +308,9 @@ class PayAndVerifyView(View):
# with a paid course mode (such as "verified"). # with a paid course mode (such as "verified").
# For this reason, every paid user is enrolled, but not # For this reason, every paid user is enrolled, but not
# every enrolled user is paid. # every enrolled user is paid.
already_verified = self._check_already_verified(request.user) # If the course mode is not verified(i.e only paid) then already_verified is always True
already_verified = self._check_already_verified(request.user) \
if CourseMode.is_verified_mode(unexpired_paid_course_mode) else True
already_paid, is_enrolled = self._check_enrollment(request.user, course_key) already_paid, is_enrolled = self._check_enrollment(request.user, course_key)
# Redirect the user to a more appropriate page if the # Redirect the user to a more appropriate page if the
...@@ -326,7 +329,8 @@ class PayAndVerifyView(View): ...@@ -326,7 +329,8 @@ class PayAndVerifyView(View):
display_steps = self._display_steps( display_steps = self._display_steps(
always_show_payment, always_show_payment,
already_verified, already_verified,
already_paid already_paid,
unexpired_paid_course_mode
) )
requirements = self._requirements(display_steps, request.user.is_active) requirements = self._requirements(display_steps, request.user.is_active)
...@@ -371,7 +375,7 @@ class PayAndVerifyView(View): ...@@ -371,7 +375,7 @@ class PayAndVerifyView(View):
'contribution_amount': contribution_amount, 'contribution_amount': contribution_amount,
'course': course, 'course': course,
'course_key': unicode(course_key), 'course_key': unicode(course_key),
'course_mode': course_mode, 'course_mode': unexpired_paid_course_mode,
'courseware_url': courseware_url, 'courseware_url': courseware_url,
'current_step': current_step, 'current_step': current_step,
'disable_courseware_js': True, 'disable_courseware_js': True,
...@@ -383,8 +387,8 @@ class PayAndVerifyView(View): ...@@ -383,8 +387,8 @@ class PayAndVerifyView(View):
'requirements': requirements, 'requirements': requirements,
'user_full_name': full_name, 'user_full_name': full_name,
'verification_deadline': ( 'verification_deadline': (
get_default_time_display(course_mode.expiration_datetime) get_default_time_display(unexpired_paid_course_mode.expiration_datetime)
if course_mode.expiration_datetime else "" if unexpired_paid_course_mode.expiration_datetime else ""
), ),
} }
return render_to_response("verify_student/pay_and_verify.html", context) return render_to_response("verify_student/pay_and_verify.html", context)
...@@ -459,22 +463,33 @@ class PayAndVerifyView(View): ...@@ -459,22 +463,33 @@ class PayAndVerifyView(View):
if url is not None: if url is not None:
return redirect(url) return redirect(url)
def _get_verified_modes_for_course(self, course_key): def _get_expired_verified_and_paid_mode(self, course_key): # pylint: disable=invalid-name
"""Retrieve unexpired and expired verified modes for a course. """Retrieve expired verified mode and unexpired paid mode(with min_price>0) for a course.
Arguments: Arguments:
course_key (CourseKey): The location of the course. course_key (CourseKey): The location of the course.
Returns: Returns:
Tuple of `(verified_mode, expired_verified_mode)`. If provided, Tuple of `(expired_verified_mode, unexpired_paid_mode)`. If provided,
`verified_mode` is an *unexpired* verified mode for the course. `expired_verified_mode` is an *expired* verified mode for the course.
If provided, `expired_verified_mode` is an *expired* verified If provided, `unexpired_paid_mode` is an *unexpired* paid(with min_price>0)
mode for the course. Either of these may be None. mode for the course. Either of these may be None.
""" """
# Retrieve all the modes at once to reduce the number of database queries # Retrieve all the modes at once to reduce the number of database queries
all_modes, unexpired_modes = CourseMode.all_and_unexpired_modes_for_courses([course_key]) all_modes, unexpired_modes = CourseMode.all_and_unexpired_modes_for_courses([course_key])
# Unexpired paid modes
unexpired_paid_modes = [mode for mode in unexpired_modes[course_key] if mode.min_price]
if len(unexpired_paid_modes) > 1:
# There is more than one paid mode defined,
# so choose the first one.
log.warn(
u"More than one paid modes are defined for course '%s' choosing the first one %s",
course_key, unexpired_paid_modes[0]
)
unexpired_paid_mode = unexpired_paid_modes[0] if unexpired_paid_modes else None
# Find an unexpired verified mode # Find an unexpired verified mode
verified_mode = CourseMode.verified_mode_for_course(course_key, modes=unexpired_modes[course_key]) verified_mode = CourseMode.verified_mode_for_course(course_key, modes=unexpired_modes[course_key])
expired_verified_mode = None expired_verified_mode = None
...@@ -482,9 +497,9 @@ class PayAndVerifyView(View): ...@@ -482,9 +497,9 @@ class PayAndVerifyView(View):
if verified_mode is None: if verified_mode is None:
expired_verified_mode = CourseMode.verified_mode_for_course(course_key, modes=all_modes[course_key]) expired_verified_mode = CourseMode.verified_mode_for_course(course_key, modes=all_modes[course_key])
return (verified_mode, expired_verified_mode) return (expired_verified_mode, unexpired_paid_mode)
def _display_steps(self, always_show_payment, already_verified, already_paid): def _display_steps(self, always_show_payment, already_verified, already_paid, course_mode):
"""Determine which steps to display to the user. """Determine which steps to display to the user.
Includes all steps by default, but removes steps Includes all steps by default, but removes steps
...@@ -508,7 +523,7 @@ class PayAndVerifyView(View): ...@@ -508,7 +523,7 @@ class PayAndVerifyView(View):
display_steps = self.ALL_STEPS display_steps = self.ALL_STEPS
remove_steps = set() remove_steps = set()
if already_verified: if already_verified or not CourseMode.is_verified_mode(course_mode):
remove_steps |= set(self.VERIFICATION_STEPS) remove_steps |= set(self.VERIFICATION_STEPS)
if already_paid and not always_show_payment: if already_paid and not always_show_payment:
...@@ -517,7 +532,6 @@ class PayAndVerifyView(View): ...@@ -517,7 +532,6 @@ class PayAndVerifyView(View):
# The "make payment" step doubles as an intro step, # The "make payment" step doubles as an intro step,
# so if we're showing the payment step, hide the intro step. # so if we're showing the payment step, hide the intro step.
remove_steps |= set([self.INTRO_STEP]) remove_steps |= set([self.INTRO_STEP])
return [ return [
{ {
'name': step, 'name': step,
...@@ -642,15 +656,21 @@ def create_order(request): ...@@ -642,15 +656,21 @@ def create_order(request):
except decimal.InvalidOperation: except decimal.InvalidOperation:
return HttpResponseBadRequest(_("Selected price is not valid number.")) return HttpResponseBadRequest(_("Selected price is not valid number."))
# prefer professional mode over verified_mode current_mode = None
current_mode = CourseMode.verified_mode_for_course(course_id) paid_modes = CourseMode.paid_modes_for_course(course_id)
# Check if there are more than 1 paid(mode with min_price>0 e.g verified/professional/no-id-professional) modes
# for course exist then choose the first one
if paid_modes:
if len(paid_modes) > 1:
log.warn(u"Multiple paid course modes found for course '%s' for create order request", course_id)
current_mode = paid_modes[0]
# make sure this course has a verified mode # Make sure this course has a paid mode
if not current_mode: if not current_mode:
log.warn(u"Verification requested for course {course_id} without a verified mode.".format(course_id=course_id)) log.warn(u"Create order requested for course '%s' without a paid mode.", course_id)
return HttpResponseBadRequest(_("This course doesn't support verified certificates")) return HttpResponseBadRequest(_("This course doesn't support paid certificates"))
if current_mode.slug == 'professional': if CourseMode.is_professional_mode(current_mode):
amount = current_mode.min_price amount = current_mode.min_price
if amount < current_mode.min_price: if amount < current_mode.min_price:
......
...@@ -6,6 +6,7 @@ from django.utils.translation import ungettext ...@@ -6,6 +6,7 @@ from django.utils.translation import ungettext
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from markupsafe import escape from markupsafe import escape
from courseware.courses import course_image_url, get_course_about_section from courseware.courses import course_image_url, get_course_about_section
from course_modes.models import CourseMode
from student.helpers import ( from student.helpers import (
VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_NEED_TO_VERIFY,
VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_SUBMITTED,
...@@ -30,17 +31,14 @@ from student.helpers import ( ...@@ -30,17 +31,14 @@ from student.helpers import (
<li class="course-item"> <li class="course-item">
% if settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES'): % if settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES'):
% if enrollment.mode == "verified": <% course_verified_certs = CourseMode.enrollment_mode_display(enrollment.mode, verification_status.get('status')) %>
% if verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_APPROVED]: <%
<% mode_class = " verified" %> mode_class = course_verified_certs.get('display_mode', '')
% else: if mode_class != '':
<% mode_class = " honor" %> mode_class = ' ' + mode_class ;
% endif %>
% else:
<% mode_class = " " + enrollment.mode %>
% endif
% else: % else:
<% mode_class = "" %> <% mode_class = '' %>
% endif % endif
<article class="course${mode_class}"> <article class="course${mode_class}">
...@@ -64,44 +62,15 @@ from student.helpers import ( ...@@ -64,44 +62,15 @@ from student.helpers import (
<img src="${course_image_url(course)}" alt="${_('{course_number} {course_name} Cover Image').format(course_number=course.number, course_name=course.display_name_with_default) | h}" /> <img src="${course_image_url(course)}" alt="${_('{course_number} {course_name} Cover Image').format(course_number=course.number, course_name=course.display_name_with_default) | h}" />
</div> </div>
% endif % endif
% if settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES'): % if settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES'):
% if enrollment.mode == "verified": <span class="sts-enrollment" title="${course_verified_certs.get('enrollment_title')}">
% if verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED]:
<span class="sts-enrollment" title="${_("Your verification is pending")}">
<span class="label">${_("Enrolled as: ")}</span> <span class="label">${_("Enrolled as: ")}</span>
## Translators: This text describes that the student has enrolled for a Verified Certificate, but verification of identity is pending. % if course_verified_certs.get('show_image'):
<img class="deco-graphic" src="${static.url('images/verified-ribbon.png')}" alt="${_("ID verification pending")}" /> <img class="deco-graphic" src="${static.url('images/verified-ribbon.png')}" alt="${course_verified_certs.get('image_alt')}" />
## Translators: The student is enrolled for a Verified Certificate, but verification of identity is pending.
<div class="sts-enrollment-value">${_("Verified: Pending Verification")}</div>
</span>
% elif verification_status.get('status') == VERIFY_STATUS_APPROVED:
<span class="sts-enrollment" title="${_("You're enrolled as a verified student")}">
<span class="label">${_("Enrolled as: ")}</span>
<img class="deco-graphic" src="${static.url('images/verified-ribbon.png')}" alt="${_("ID Verified Ribbon/Badge")}" />
<div class="sts-enrollment-value">${_("Verified")}</div>
</span>
% else:
<span class="sts-enrollment" title="${_("You're enrolled as an honor code student")}">
<span class="label">${_("Enrolled as: ")}</span>
<div class="sts-enrollment-value">${_("Honor Code")}</div>
</span>
% endif % endif
% elif enrollment.mode == "honor": <div class="sts-enrollment-value">${course_verified_certs.get('enrollment_value')}</div>
<span class="sts-enrollment" title="${_("You're enrolled as an honor code student")}">
<span class="label">${_("Enrolled as: ")}</span>
<div class="sts-enrollment-value">${_("Honor Code")}</div>
</span> </span>
% elif enrollment.mode == "audit":
<span class="sts-enrollment" title="${_("You're auditing this course")}">
<span class="label">${_("Enrolled as: ")}</span>
<div class="sts-enrollment-value">${_("Auditing")}</div>
</span>
% elif enrollment.mode == "professional":
<span class="sts-enrollment" title="${_("You're enrolled as a professional education student")}">
<span class="label">${_("Enrolled as: ")}</span>
<div class="sts-enrollment-value">${_("Professional Ed")}</div>
</span>
% endif
% endif % endif
<section class="info"> <section class="info">
......
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