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):
COURSE_MODE_SLUG_CHOICES = (
[(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)
......
......@@ -65,11 +65,17 @@ class CourseMode(models.Model):
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)
DEFAULT_MODE_SLUG = 'honor'
HONOR = '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
VERIFIED_MODES = ["verified", "professional"]
VERIFIED_MODES = [VERIFIED, PROFESSIONAL]
class Meta:
""" meta attributes of this model """
......@@ -249,6 +255,23 @@ class CourseMode(models.Model):
return professional_mode if professional_mode else verified_mode
@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):
"""Check whether the modes for a course allow a student to pursue a verfied certificate.
......@@ -265,21 +288,65 @@ class CourseMode(models.Model):
return False
@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
course's *verified*, non-expired modes.
check the course mode is profession or no-id-professional
Assuming all verified courses have a minimum price of >0, this value should always
be >0.
Args:
modes_dict (dict): course modes.
If no verified mode is found, 0 is returned.
Returns:
bool
"""
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
return cls.PROFESSIONAL in modes_dict or cls.NO_ID_PROFESSIONAL_MODE in modes_dict
@classmethod
def is_professional_mode(cls, course_mode_tuple):
"""
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
def has_payment_options(cls, course_id):
......@@ -325,8 +392,8 @@ class CourseMode(models.Model):
if modes_dict is None:
modes_dict = cls.modes_for_course_dict(course_id)
# Professional mode courses are always behind a paywall
if "professional" in modes_dict:
# Professional and no-id-professional mode courses are always behind a paywall
if cls.has_professional_mode(modes_dict):
return False
# White-label uses course mode honor with a price
......@@ -335,7 +402,7 @@ class CourseMode(models.Model):
return False
# Check that the default mode is available.
return ("honor" in modes_dict)
return (cls.HONOR in modes_dict)
@classmethod
def is_white_label(cls, course_id, modes_dict=None):
......@@ -360,13 +427,13 @@ class CourseMode(models.Model):
# White-label uses course mode honor with a price
# 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 != '':
return True
return False
@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
non-expired modes.
......@@ -375,6 +442,96 @@ class CourseMode(models.Model):
modes = cls.modes_for_course(course_id)
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):
"""
Takes a mode model and turns it into a model named tuple.
......
......@@ -150,11 +150,17 @@ class CourseModeModelTest(TestCase):
honor.save()
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(
([], True),
([("honor", 0), ("audit", 0), ("verified", 100)], True),
([("honor", 100)], False),
([("professional", 100)], False),
([("no-id-professional", 100)], False),
)
@ddt.unpack
def test_can_auto_enroll(self, modes_and_prices, can_auto_enroll):
......@@ -206,3 +212,111 @@ class CourseModeModelTest(TestCase):
# 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(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):
else:
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):
# Create the course modes
for mode in ('audit', 'honor', 'verified'):
......@@ -115,9 +134,10 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
# TODO: Fix it so that response.templates works w/ mako templates, and then assert
# 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
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
choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
......@@ -132,7 +152,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
CourseEnrollmentFactory(
user=self.user,
is_active=True,
mode="professional",
mode=mode,
course_id=unicode(self.course.id),
)
......@@ -156,7 +176,8 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
def test_choose_mode_redirect(self, course_mode, expected_redirect):
# Create the course modes
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_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
......
......@@ -72,9 +72,9 @@ class ChooseModeView(View):
# 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
# to the usual "choose your track" page.
has_enrolled_professional = (enrollment_mode == "professional" and is_active)
if "professional" in modes and not has_enrolled_professional:
# to the usual "choose your track" page same is true for no-id-professional mode.
has_enrolled_professional = (CourseMode.is_professional_slug(enrollment_mode) and is_active)
if CourseMode.has_professional_mode(modes) and not has_enrolled_professional:
return redirect(
reverse(
'verify_student_start_flow',
......@@ -90,7 +90,7 @@ class ChooseModeView(View):
return redirect(reverse('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'))
donation_for_course = request.session.get("donation_for_course", {})
......
......@@ -38,7 +38,8 @@ class EnrollmentTest(TestCase):
(['honor', 'verified', 'audit'], 'honor'),
# Check for professional ed happy path.
(['professional'], 'professional')
(['professional'], 'professional'),
(['no-id-professional'], 'no-id-professional')
)
@ddt.unpack
def test_enroll(self, course_modes, mode):
......@@ -72,7 +73,8 @@ class EnrollmentTest(TestCase):
(['honor', 'verified', 'audit'], 'honor'),
# Check for professional ed happy path.
(['professional'], 'professional')
(['professional'], 'professional'),
(['no-id-professional'], 'no-id-professional')
)
@ddt.unpack
def test_unenroll(self, course_modes, mode):
......
......@@ -1100,9 +1100,8 @@ class CourseEnrollment(models.Model):
"""
Returns True, if course is paid
"""
paid_course = CourseMode.objects.filter(Q(course_id=self.course_id) & Q(mode_slug='honor') &
(Q(expiration_datetime__isnull=True) | Q(expiration_datetime__gte=datetime.now(pytz.UTC)))).exclude(min_price=0)
if paid_course or self.mode == 'professional':
paid_course = CourseMode.is_white_label(self.course_id)
if paid_course or CourseMode.is_professional_slug(self.mode):
return True
return False
......@@ -1154,6 +1153,12 @@ class CourseEnrollment(models.Model):
def course(self):
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):
"""
......@@ -1403,6 +1408,9 @@ class LinkedInAddToProfileConfiguration(ConfigurationModel):
"honor": ugettext_lazy(u"{platform_name} Honor Code 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}"),
"no-id-professional": ugettext_lazy(
u"{platform_name} Professional Certificate for {course_name}"
),
}
company_identifier = models.TextField(
......
......@@ -55,6 +55,7 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase):
# We should NOT be auto-enrolled, because that would be giving
# away an expensive course for free :)
(['professional'], 'course_modes_choose', None),
(['no-id-professional'], 'course_modes_choose', None),
)
@ddt.unpack
def test_enroll(self, course_modes, next_url, enrollment_mode):
......@@ -113,6 +114,9 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase):
(['professional'], 'true'),
(['professional'], 'false'),
(['professional'], None),
(['no-id-professional'], 'true'),
(['no-id-professional'], 'false'),
(['no-id-professional'], None),
)
@ddt.unpack
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):
attempt.approve()
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)
@patch.dict("django.conf.settings.FEATURES", {'ENABLE_VERIFIED_CERTIFICATES': True})
......@@ -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('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('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')
def _check_verification_status_off(self, mode, value):
......
......@@ -909,9 +909,9 @@ def change_enrollment(request, check_access=True):
# If we have more than one course mode or professional ed is enabled,
# 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)
if CourseMode.has_verified_mode(available_modes):
if CourseMode.has_verified_mode(available_modes) or CourseMode.has_professional_mode(available_modes):
return HttpResponse(
reverse("course_modes_choose", kwargs={'course_id': unicode(course_id)})
)
......
......@@ -1746,20 +1746,26 @@ class CertificateItem(OrderItem):
self.course_enrollment.activate()
def additional_instruction_text(self):
verification_reminder = ""
is_enrollment_mode_verified = self.course_enrollment.is_verified_enrollment() # pylint: disable=E1101
if is_enrollment_mode_verified:
domain = microsite.get_value('SITE_NAME', settings.SITE_NAME)
path = reverse('verify_student_verify_later', kwargs={'course_id': unicode(self.course_id)})
verification_url = "http://{domain}{path}".format(domain=domain, path=path)
verification_reminder = _(
"If you haven't verified your identity yet, please start the verification process ({verification_url})."
).format(verification_url=verification_url)
refund_reminder = _(
"You have up to two weeks into the course to unenroll from the Verified Certificate option "
"and receive a full refund. To receive your refund, contact {billing_email}. "
"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)
domain = microsite.get_value('SITE_NAME', settings.SITE_NAME)
path = reverse('verify_student_verify_later', kwargs={'course_id': unicode(self.course_id)})
verification_url = "http://{domain}{path}".format(domain=domain, path=path)
verification_reminder = _(
"If you haven't verified your identity yet, please start the verification process ({verification_url})."
).format(verification_url=verification_url)
).format(
billing_email=settings.PAYMENT_SUPPORT_EMAIL
)
# Need this to be unicode in case the reminder strings
# have been translated and contain non-ASCII unicode
......
......@@ -801,6 +801,29 @@ class CertificateItemTest(ModuleStoreTestCase):
ret_val = CourseEnrollment.unenroll(self.user, self.course_key)
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):
"""Tests for the donation order item type. """
......
......@@ -298,7 +298,7 @@ class StudentAccountLoginAndRegistrationTest(UrlResetMixin, ModuleStoreTestCase)
]
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):
# Create a course with the specified course modes
course = CourseFactory.create()
......
......@@ -98,6 +98,21 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
])
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")
def test_start_flow_expired_or_denied_verification(self, verification_status):
course = self._create_course("verified")
......@@ -121,7 +136,8 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
("verified", "submitted"),
("verified", "approved"),
("verified", "error"),
("professional", "submitted")
("professional", "submitted"),
("no-id-professional", None),
)
@ddt.unpack
def test_start_flow_already_verified(self, course_mode, verification_status):
......@@ -516,6 +532,14 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
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(
"verify_student_start_flow",
"verify_student_verify_now",
......@@ -647,7 +671,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
modulestore().update_item(course, ModuleStoreEnum.UserID.test)
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(
course_id=course.id,
mode_slug=course_mode,
......@@ -826,8 +850,8 @@ class TestCreateOrder(ModuleStoreTestCase):
self.user = UserFactory.create(username="test", password="test")
self.course = CourseFactory.create()
for mode in ('audit', 'honor', 'verified'):
CourseModeFactory(mode_slug=mode, course_id=self.course.id)
for mode, min_price in (('audit', 0), ('honor', 0), ('verified', 100)):
CourseModeFactory(mode_slug=mode, course_id=self.course.id, min_price=min_price)
self.client.login(username="test", password="test")
def test_create_order_already_verified(self):
......@@ -838,6 +862,7 @@ class TestCreateOrder(ModuleStoreTestCase):
url = reverse('verify_student_create_order')
params = {
'course_id': unicode(self.course.id),
'contribution': 100
}
response = self.client.post(url, params)
self.assertEqual(response.status_code, 200)
......@@ -857,7 +882,7 @@ class TestCreateOrder(ModuleStoreTestCase):
# Create a prof ed course
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
url = reverse('verify_student_create_order')
......@@ -872,6 +897,45 @@ class TestCreateOrder(ModuleStoreTestCase):
self.assertEqual(data['merchant_defined_data1'], unicode(course.id))
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):
# Verify the student so we don't need to submit photos
self._verify_student()
......@@ -959,7 +1023,7 @@ class TestCreateOrderView(ModuleStoreTestCase):
photo_id_image=self.IMAGE_DATA,
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})
def test_create_order_fail_with_get(self):
......
......@@ -272,15 +272,16 @@ class PayAndVerifyView(View):
if redirect_url:
return redirect(redirect_url)
# Check that the course has an unexpired verified mode
course_mode, expired_course_mode = self._get_verified_modes_for_course(course_key)
if course_mode is not None:
log.info(
u"Entering verified workflow for user '%s', course '%s', with current step '%s'.",
request.user.id, course_id, current_step
)
elif expired_course_mode is not None:
expired_verified_course_mode, unexpired_paid_course_mode = self._get_expired_verified_and_paid_mode(course_key)
# 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(
u"Entering verified workflow for user '%s', course '%s', with current step '%s'.",
request.user.id, course_id, current_step
)
elif expired_verified_course_mode is not None:
# Check if there is an *expired* verified course mode;
# if so, we should show a message explaining that the verification
# deadline has passed.
......@@ -288,16 +289,16 @@ class PayAndVerifyView(View):
context = {
'course': course,
'deadline': (
get_default_time_display(expired_course_mode.expiration_datetime)
if expired_course_mode.expiration_datetime else ""
get_default_time_display(expired_verified_course_mode.expiration_datetime)
if expired_verified_course_mode.expiration_datetime else ""
)
}
return render_to_response("verify_student/missed_verification_deadline.html", context)
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.
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
)
raise Http404
......@@ -307,7 +308,9 @@ class PayAndVerifyView(View):
# with a paid course mode (such as "verified").
# For this reason, every paid user is enrolled, but not
# 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)
# Redirect the user to a more appropriate page if the
......@@ -326,7 +329,8 @@ class PayAndVerifyView(View):
display_steps = self._display_steps(
always_show_payment,
already_verified,
already_paid
already_paid,
unexpired_paid_course_mode
)
requirements = self._requirements(display_steps, request.user.is_active)
......@@ -371,7 +375,7 @@ class PayAndVerifyView(View):
'contribution_amount': contribution_amount,
'course': course,
'course_key': unicode(course_key),
'course_mode': course_mode,
'course_mode': unexpired_paid_course_mode,
'courseware_url': courseware_url,
'current_step': current_step,
'disable_courseware_js': True,
......@@ -383,8 +387,8 @@ class PayAndVerifyView(View):
'requirements': requirements,
'user_full_name': full_name,
'verification_deadline': (
get_default_time_display(course_mode.expiration_datetime)
if course_mode.expiration_datetime else ""
get_default_time_display(unexpired_paid_course_mode.expiration_datetime)
if unexpired_paid_course_mode.expiration_datetime else ""
),
}
return render_to_response("verify_student/pay_and_verify.html", context)
......@@ -459,22 +463,33 @@ class PayAndVerifyView(View):
if url is not None:
return redirect(url)
def _get_verified_modes_for_course(self, course_key):
"""Retrieve unexpired and expired verified modes for a course.
def _get_expired_verified_and_paid_mode(self, course_key): # pylint: disable=invalid-name
"""Retrieve expired verified mode and unexpired paid mode(with min_price>0) for a course.
Arguments:
course_key (CourseKey): The location of the course.
Returns:
Tuple of `(verified_mode, expired_verified_mode)`. If provided,
`verified_mode` is an *unexpired* verified mode for the course.
If provided, `expired_verified_mode` is an *expired* verified
Tuple of `(expired_verified_mode, unexpired_paid_mode)`. If provided,
`expired_verified_mode` is an *expired* verified mode for the course.
If provided, `unexpired_paid_mode` is an *unexpired* paid(with min_price>0)
mode for the course. Either of these may be None.
"""
# 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])
# 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
verified_mode = CourseMode.verified_mode_for_course(course_key, modes=unexpired_modes[course_key])
expired_verified_mode = None
......@@ -482,9 +497,9 @@ class PayAndVerifyView(View):
if verified_mode is None:
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.
Includes all steps by default, but removes steps
......@@ -508,7 +523,7 @@ class PayAndVerifyView(View):
display_steps = self.ALL_STEPS
remove_steps = set()
if already_verified:
if already_verified or not CourseMode.is_verified_mode(course_mode):
remove_steps |= set(self.VERIFICATION_STEPS)
if already_paid and not always_show_payment:
......@@ -517,7 +532,6 @@ class PayAndVerifyView(View):
# The "make payment" step doubles as an intro step,
# so if we're showing the payment step, hide the intro step.
remove_steps |= set([self.INTRO_STEP])
return [
{
'name': step,
......@@ -642,15 +656,21 @@ def create_order(request):
except decimal.InvalidOperation:
return HttpResponseBadRequest(_("Selected price is not valid number."))
# prefer professional mode over verified_mode
current_mode = CourseMode.verified_mode_for_course(course_id)
current_mode = None
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:
log.warn(u"Verification requested for course {course_id} without a verified mode.".format(course_id=course_id))
return HttpResponseBadRequest(_("This course doesn't support verified certificates"))
log.warn(u"Create order requested for course '%s' without a paid mode.", course_id)
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
if amount < current_mode.min_price:
......
......@@ -6,6 +6,7 @@ from django.utils.translation import ungettext
from django.core.urlresolvers import reverse
from markupsafe import escape
from courseware.courses import course_image_url, get_course_about_section
from course_modes.models import CourseMode
from student.helpers import (
VERIFY_STATUS_NEED_TO_VERIFY,
VERIFY_STATUS_SUBMITTED,
......@@ -30,18 +31,15 @@ from student.helpers import (
<li class="course-item">
% if settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES'):
% if enrollment.mode == "verified":
% if verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_APPROVED]:
<% mode_class = " verified" %>
% else:
<% mode_class = " honor" %>
% endif
% else:
<% mode_class = " " + enrollment.mode %>
% endif
<% course_verified_certs = CourseMode.enrollment_mode_display(enrollment.mode, verification_status.get('status')) %>
<%
mode_class = course_verified_certs.get('display_mode', '')
if mode_class != '':
mode_class = ' ' + mode_class ;
%>
% else:
<% mode_class = "" %>
% endif
<% mode_class = '' %>
% endif
<article class="course${mode_class}">
......@@ -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}" />
</div>
% endif
% if settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES'):
% if enrollment.mode == "verified":
% 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>
## Translators: This text describes that the student has enrolled for a Verified Certificate, but verification of identity is pending.
<img class="deco-graphic" src="${static.url('images/verified-ribbon.png')}" alt="${_("ID verification pending")}" />
## 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
% elif enrollment.mode == "honor":
<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>
% 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="sts-enrollment" title="${course_verified_certs.get('enrollment_title')}">
<span class="label">${_("Enrolled as: ")}</span>
<div class="sts-enrollment-value">${_("Professional Ed")}</div>
% if course_verified_certs.get('show_image'):
<img class="deco-graphic" src="${static.url('images/verified-ribbon.png')}" alt="${course_verified_certs.get('image_alt')}" />
% endif
<div class="sts-enrollment-value">${course_verified_certs.get('enrollment_value')}</div>
</span>
% endif
% endif
<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