Commit 2204eb73 by Julia Hansbrough

Fixed merge conflict between release and master.

parents 7e975f78 9b53c809
......@@ -113,6 +113,21 @@ class CourseMode(models.Model):
return None
@classmethod
def verified_mode_for_course(cls, course_id):
"""
Since we have two separate modes that can go through the verify flow,
we want to be able to select the 'correct' verified mode for a given course.
Currently, we prefer to return the professional mode over the verified one
if both exist for the given course.
"""
modes_dict = cls.modes_for_course_dict(course_id)
verified_mode = modes_dict.get('verified', None)
professional_mode = modes_dict.get('professional', None)
# we prefer professional over verify
return professional_mode if professional_mode else verified_mode
@classmethod
def min_course_price_for_verified_for_currency(cls, course_id, currency):
"""
Returns the minimum price of the course int he appropriate currency over all the
......
......@@ -113,3 +113,17 @@ class CourseModeModelTest(TestCase):
modes = CourseMode.modes_for_course(SlashSeparatedCourseKey('TestOrg', 'TestCourse', 'TestRun'))
self.assertEqual([CourseMode.DEFAULT_MODE], modes)
def test_verified_mode_for_course(self):
self.create_mode('verified', 'Verified Certificate')
mode = CourseMode.verified_mode_for_course(self.course_key)
self.assertEqual(mode.slug, 'verified')
# verify that the professional mode is preferred
self.create_mode('professional', 'Professional Education Verified Certificate')
mode = CourseMode.verified_mode_for_course(self.course_key)
self.assertEqual(mode.slug, 'professional')
......@@ -59,8 +59,6 @@ class ChooseModeView(View):
)
)
donation_for_course = request.session.get("donation_for_course", {})
chosen_price = donation_for_course.get(course_key, None)
......@@ -135,11 +133,6 @@ class ChooseModeView(View):
donation_for_course = request.session.get("donation_for_course", {})
donation_for_course[course_key] = amount_value
request.session["donation_for_course"] = donation_for_course
if SoftwareSecurePhotoVerification.user_has_valid_or_pending(request.user):
return redirect(
reverse('verify_student_verified',
kwargs={'course_id': course_key.to_deprecated_string()}) + "?upgrade={}".format(upgrade)
)
return redirect(
reverse('verify_student_show_requirements',
......
......@@ -968,7 +968,7 @@ class CourseEnrollment(models.Model):
"""
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:
if paid_course or self.mode == 'professional':
return True
return False
......
......@@ -701,8 +701,13 @@ class CertificateItem(OrderItem):
item.qty = 1
item.unit_cost = cost
course_name = modulestore().get_course(course_id).display_name
item.line_desc = _("Certificate of Achievement, {mode_name} for course {course}").format(mode_name=mode_info.name,
course=course_name)
# Translators: In this particular case, mode_name refers to a
# particular mode (i.e. Honor Code Certificate, Verified Certificate, etc)
# by which a user could enroll in the given course.
item.line_desc = _("{mode_name} for course {course}").format(
mode_name=mode_info.name,
course=course_name
)
item.currency = currency
order.currency = currency
order.save()
......@@ -725,7 +730,7 @@ class CertificateItem(OrderItem):
@property
def single_item_receipt_template(self):
if self.mode == 'verified':
if self.mode in ('verified', 'professional'):
return 'shoppingcart/verified_cert_receipt.html'
else:
return super(CertificateItem, self).single_item_receipt_template
......
......@@ -222,7 +222,7 @@ class ItemizedPurchaseReportTest(ModuleStoreTestCase):
self.CORRECT_CSV = dedent("""
Purchase Time,Order ID,Status,Quantity,Unit Cost,Total Cost,Currency,Description,Comments
{time_str},1,purchased,1,40,40,usd,Registration for Course: Robot Super Course,Ba\xc3\xbc\xe5\x8c\x85
{time_str},1,purchased,1,40,40,usd,"Certificate of Achievement, verified cert for course Robot Super Course",
{time_str},1,purchased,1,40,40,usd,verified cert for course Robot Super Course,
""".format(time_str=str(self.now)))
def test_purchased_items_btw_dates(self):
......
"""
Integration tests of the payment flow, including course mode selection.
"""
from lxml.html import soupparser
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from student.tests.factories import UserFactory
from course_modes.tests.factories import CourseModeFactory
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from verify_student.models import SoftwareSecurePhotoVerification
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestProfEdVerification(ModuleStoreTestCase):
"""
Integration test for professional ed verification, including course mode selection.
"""
# Choose an uncommon number for the price so we can search for it on the page
MIN_PRICE = 1438
def setUp(self):
self.user = UserFactory.create(username="rusty", password="test")
self.client.login(username="rusty", password="test")
course = CourseFactory.create(org='Robot', number='999', display_name='Test Course')
self.course_key = course.id
CourseModeFactory(
mode_slug="professional",
course_id=self.course_key,
min_price=self.MIN_PRICE,
suggested_prices=''
)
self.urls = {
'course_modes_choose': reverse(
'course_modes_choose',
args=[unicode(self.course_key)]
),
'verify_show_student_requirements': reverse(
'verify_student_show_requirements',
args=[unicode(self.course_key)]
),
'verify_student_verify': reverse(
'verify_student_verify',
args=[unicode(self.course_key)]
),
'verify_student_verified': reverse(
'verify_student_verified',
args=[unicode(self.course_key)]
) + "?upgrade=False",
}
def test_new_user_flow(self):
# Go to the course mode page, expecting a redirect
# to the show requirements page
# because this is a professional ed course
# (otherwise, the student would have the option to choose their track)
resp = self.client.get(self.urls['course_modes_choose'], follow=True)
self.assertRedirects(resp, self.urls['verify_show_student_requirements'])
# On the show requirements page, verify that there's a link to the verify page
# (this is the only action the user is allowed to take)
self.assertContains(resp, self.urls['verify_student_verify'])
# Simulate the user clicking the button by following the link
# to the verified page.
# Since there are no suggested prices for professional ed,
# expect that only one price is displayed.
resp = self.client.get(self.urls['verify_student_verify'])
self.assertEqual(self._prices_on_page(resp.content), [self.MIN_PRICE])
def test_already_verified_user_flow(self):
# Simulate the user already being verified
self._verify_student()
# Go to the course mode page, expecting a redirect to the
# verified (past tense!) page.
resp = self.client.get(self.urls['course_modes_choose'], follow=True)
self.assertRedirects(resp, self.urls['verify_student_verified'])
# Since this is a professional ed course, expect that only
# one price is shown.
self.assertContains(resp, "Your Course Total is $")
self.assertContains(resp, str(self.MIN_PRICE))
# On the verified page, expect that there's a link to payment page
self.assertContains(resp, '/shoppingcart/payment_fake')
def _prices_on_page(self, page_content):
""" Retrieve the available prices on the verify page. """
html = soupparser.fromstring(page_content)
xpath_sel = '//li[@class="field contribution-option"]/span[@class="label-value"]/text()'
return [int(price) for price in html.xpath(xpath_sel)]
def _verify_student(self):
""" Simulate that the student's identity has already been verified. """
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready()
attempt.submit()
attempt.approve()
......@@ -74,18 +74,27 @@ class VerifyView(View):
# bookkeeping-wise just to start over.
progress_state = "start"
modes_dict = CourseMode.modes_for_course_dict(course_id)
verify_mode = modes_dict.get('verified', None)
# we prefer professional over verify
current_mode = CourseMode.verified_mode_for_course(course_id)
# if the course doesn't have a verified mode, we want to kick them
# from the flow
if not verify_mode:
if not current_mode:
return redirect(reverse('dashboard'))
if course_id.to_deprecated_string() in request.session.get("donation_for_course", {}):
chosen_price = request.session["donation_for_course"][course_id.to_deprecated_string()]
else:
chosen_price = verify_mode.min_price
chosen_price = current_mode.min_price
course = modulestore().get_course(course_id)
if current_mode.suggested_prices != '':
suggested_prices = [
decimal.Decimal(price)
for price in current_mode.suggested_prices.split(",")
]
else:
suggested_prices = []
context = {
"progress_state": progress_state,
"user_full_name": request.user.profile.name,
......@@ -95,15 +104,13 @@ class VerifyView(View):
"course_org": course.display_org_with_default,
"course_num": course.display_number_with_default,
"purchase_endpoint": get_purchase_endpoint(),
"suggested_prices": [
decimal.Decimal(price)
for price in verify_mode.suggested_prices.split(",")
],
"currency": verify_mode.currency.upper(),
"suggested_prices": suggested_prices,
"currency": current_mode.currency.upper(),
"chosen_price": chosen_price,
"min_price": verify_mode.min_price,
"min_price": current_mode.min_price,
"upgrade": upgrade == u'True',
"can_audit": "audit" in modes_dict,
"can_audit": CourseMode.mode_for_course(course_id, 'audit') is not None,
"modes_dict": CourseMode.modes_for_course_dict(course_id),
}
return render_to_response('verify_student/photo_verification.html', context)
......@@ -124,19 +131,20 @@ class VerifiedView(View):
if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == ('verified', True):
return redirect(reverse('dashboard'))
modes_dict = CourseMode.modes_for_course_dict(course_id)
verify_mode = modes_dict.get('verified', None)
if verify_mode is None:
return redirect(reverse('dashboard'))
# we prefer professional over verify
current_mode = CourseMode.verified_mode_for_course(course_id)
chosen_price = request.session.get(
"donation_for_course",
{}
).get(
course_id.to_deprecated_string(),
verify_mode.min_price
)
# if the course doesn't have a verified mode, we want to kick them
# from the flow
if not current_mode:
return redirect(reverse('dashboard'))
if course_id.to_deprecated_string() in request.session.get("donation_for_course", {}):
chosen_price = request.session["donation_for_course"][course_id.to_deprecated_string()]
else:
chosen_price = current_mode.min_price
course = modulestore().get_course(course_id)
context = {
......@@ -146,11 +154,12 @@ class VerifiedView(View):
"course_org": course.display_org_with_default,
"course_num": course.display_number_with_default,
"purchase_endpoint": get_purchase_endpoint(),
"currency": verify_mode.currency.upper(),
"currency": current_mode.currency.upper(),
"chosen_price": chosen_price,
"create_order_url": reverse("verify_student_create_order"),
"upgrade": upgrade == u'True',
"can_audit": "audit" in modes_dict,
"modes_dict": modes_dict,
}
return render_to_response('verify_student/verified.html', context)
......@@ -185,19 +194,24 @@ def create_order(request):
donation_for_course[course_id] = amount
request.session['donation_for_course'] = donation_for_course
verified_mode = CourseMode.modes_for_course_dict(course_id).get('verified', None)
# prefer professional mode over verified_mode
current_mode = CourseMode.verified_mode_for_course(course_id)
if current_mode.slug == 'professional':
amount = current_mode.min_price
# make sure this course has a verified mode
if not verified_mode:
if not current_mode:
return HttpResponseBadRequest(_("This course doesn't support verified certificates"))
if amount < verified_mode.min_price:
if amount < current_mode.min_price:
return HttpResponseBadRequest(_("No selected price or selected price is below minimum."))
# I know, we should check this is valid. All kinds of stuff missing here
cart = Order.get_cart_for_user(request.user)
cart.clear()
CertificateItem.add_to_order(cart, course_id, amount, 'verified')
enrollment_mode = current_mode.slug
CertificateItem.add_to_order(cart, course_id, amount, enrollment_mode)
params = get_signed_purchase_params(cart)
......@@ -288,12 +302,20 @@ def show_requirements(request, course_id):
"""
Show the requirements necessary for the verification flow.
"""
# TODO: seems borked for professional; we're told we need to take photos even if there's a pending verification
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
upgrade = request.GET.get('upgrade', False)
if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == ('verified', True):
return redirect(reverse('dashboard'))
if SoftwareSecurePhotoVerification.user_has_valid_or_pending(request.user):
return redirect(
reverse('verify_student_verified',
kwargs={'course_id': course_id.to_deprecated_string()}) + "?upgrade={}".format(upgrade)
)
upgrade = request.GET.get('upgrade', False)
course = modulestore().get_course(course_id)
modes_dict = CourseMode.modes_for_course_dict(course_id)
context = {
"course_id": course_id.to_deprecated_string(),
"course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_id.to_deprecated_string()}),
......@@ -303,6 +325,7 @@ def show_requirements(request, course_id):
"course_num": course.display_number_with_default,
"is_not_active": not request.user.is_active,
"upgrade": upgrade == u'True',
"modes_dict": modes_dict,
}
return render_to_response("verify_student/show_requirements.html", context)
......
......@@ -227,6 +227,13 @@ $verified-color-lvl3: $m-green-l2;
$verified-color-lvl4: $m-green-l3;
$verified-color-lvl5: $m-green-l4;
// STATE: professional ed
$professional-color-lvl1: $m-pink;
$professional-color-lvl2: $m-pink-l1;
$professional-color-lvl3: $m-pink-l2;
$professional-color-lvl4: $m-pink-l3;
$professional-color-lvl5: $m-pink-l4;
// STATE: honor code
$honorcode-color-lvl1: rgb(50, 165, 217);
$honorcode-color-lvl2: tint($honorcode-color-lvl1, 33%);
......
......@@ -495,6 +495,33 @@
// ====================
// CASE: "enrolled as" status - professional ed
&.professional {
// changes to cover
.cover {
border-color: $professional-color-lvl3;
padding: ($baseline/10);
}
// course enrollment status message
.sts-enrollment {
position: absolute;
left: 30px;
width: auto;
.label {
@extend %text-sr;
}
// status message
.sts-enrollment-value {
background: $professional-color-lvl3;
color: tint($professional-color-lvl1, 95%);
}
}
}
// CASE: "enrolled as" status - verified
&.verified {
......
......@@ -56,6 +56,11 @@
<span class="label">${_("Enrolled as: ")}</span>
<span class="sts-enrollment-value">${_("Auditing")}</span>
</span>
% elif enrollment.mode == "professional":
<span class="sts-enrollment" title="${_("You're enrolled as a professional education student")}">
<span class="label">${_("Enrolled as: ")}</span>
<span class="sts-enrollment-value">${_("Professional Ed")}</span>
</span>
% endif
% endif
......
......@@ -177,12 +177,13 @@
<dt class="faq-question">${_("What do you do with this picture?")}</dt>
<dd class="faq-answer">${_("We only use it to verify your identity. It is not displayed anywhere.")}</dd>
<dt class="faq-question">${_("What if my camera isn't working?")}</dt>
%if upgrade:
<dd class="faq-answer">${_("You can always continue to audit the course without verifying.")}</dd>
%else:
<dd class="faq-answer">${_("You can always {a_start} audit the course for free {a_end} without verifying.").format(a_start='<a rel="external" href="{}">'.format(course_modes_choose_url), a_end="</a>")}</dd>
%if "professional" not in modes_dict:
<dt class="faq-question">${_("What if my camera isn't working?")}</dt>
%if upgrade:
<dd class="faq-answer">${_("You can always continue to audit the course without verifying.")}</dd>
%else:
<dd class="faq-answer">${_("You can always {a_start} audit the course for free {a_end} without verifying.").format(a_start='<a rel="external" href="{}">'.format(course_modes_choose_url), a_end="</a>")}</dd>
%endif
%endif
</dl>
</div>
......@@ -366,6 +367,7 @@
</ul>
</li>
%if len(suggested_prices) > 0:
<li class="review-task review-task-contribution">
<h4 class="title">${_("Check Your Contribution Level")}</h4>
......@@ -376,12 +378,28 @@
<%include file="/course_modes/_contribution.html" args="suggested_prices=suggested_prices, currency=currency, chosen_price=chosen_price, min_price=min_price"/>
</li>
%else:
<li class="review-task review-task-contribution">
<h4 class="title">${_("Your Course Total")}</h4>
<div class="copy">
<p>${_("To complete your registration, you will need to pay:")}</p>
</div>
<ul class="list-fields contribution-options">
<li class="field contribution-option">
<span class="deco-denomination">$</span>
<span class="label-value">${chosen_price}</span>
<span class="denomination-name">${currency}</span>
</label>
</li>
</ul>
</li>
%endif
</ol>
</div>
<nav class="nav-wizard">
<div class="prompt-verify">
<h3 class="title">Before proceeding, please confirm that your details match</h3>
<h3 class="title">${_("Before proceeding, please confirm that your details match")}</h3>
<p class="copy"> ${_("Once you verify your details match the requirements, you can move on to step 4, payment on our secure server.")}</p>
......
......@@ -85,8 +85,11 @@ $(document).ready(function() {
</div>
<nav class="nav-wizard is-ready">
%if "professional" in modes_dict:
<span class="help help-inline price-value">${_("Your Course Total is $ ")} <strong>${chosen_price}</strong></span>
%else:
<span class="help help-inline price-value">${_("You have decided to pay $ ")} <strong>${chosen_price}</strong></span>
%endif
<ol class="wizard-steps">
<li class="wizard-step step-proceed">
......
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