Commit 8c52c92b by Will Daly

Reverification iOS support and refactor

* Delete reverification templates
* Delete photocapture.js
* Delete unused "name change" end-points
* Rebuild the reverification views using Backbone sub-views
* Stop passing template names to the JavaScript code
* Avoid hard-coding the parent view ID in the webcam view (for getting the capture click sound URL)
parent 86f17d02
...@@ -119,7 +119,6 @@ class ChooseModeView(View): ...@@ -119,7 +119,6 @@ class ChooseModeView(View):
"course_num": course.display_number_with_default, "course_num": course.display_number_with_default,
"chosen_price": chosen_price, "chosen_price": chosen_price,
"error": error, "error": error,
"can_audit": "audit" in modes,
"responsive": True "responsive": True
} }
if "verified" in modes: if "verified" in modes:
......
"""
Unit tests for change_name view of student.
"""
import json
from django.conf import settings
from django.core.urlresolvers import reverse
from django.test.client import Client
from django.test import TestCase
from student.tests.factories import UserFactory
from student.models import UserProfile
import unittest
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TestChangeName(TestCase):
"""
Check the change_name view of student.
"""
def setUp(self):
super(TestChangeName, self).setUp()
self.student = UserFactory.create(password='test')
self.client = Client()
def test_change_name_get_request(self):
"""Get requests are not allowed in this view."""
change_name_url = reverse('change_name')
resp = self.client.get(change_name_url)
self.assertEquals(resp.status_code, 405)
def test_change_name_post_request(self):
"""Name will be changed when provided with proper data."""
self.client.login(username=self.student.username, password='test')
change_name_url = reverse('change_name')
resp = self.client.post(change_name_url, {
'new_name': 'waqas',
'rationale': 'change identity'
})
response_data = json.loads(resp.content)
user = UserProfile.objects.get(user=self.student.id)
meta = json.loads(user.meta)
self.assertEquals(user.name, 'waqas')
self.assertEqual(meta['old_names'][0][1], 'change identity')
self.assertTrue(response_data['success'])
def test_change_name_without_name(self):
"""Empty string for name is not allowed in this view."""
self.client.login(username=self.student.username, password='test')
change_name_url = reverse('change_name')
resp = self.client.post(change_name_url, {
'new_name': '',
'rationale': 'change identity'
})
response_data = json.loads(resp.content)
self.assertFalse(response_data['success'])
def test_unauthenticated(self):
"""Unauthenticated user is not allowed to call this view."""
change_name_url = reverse('change_name')
resp = self.client.post(change_name_url, {
'new_name': 'waqas',
'rationale': 'change identity'
})
self.assertEquals(resp.status_code, 404)
...@@ -2078,66 +2078,6 @@ def confirm_email_change(request, key): # pylint: disable=unused-argument ...@@ -2078,66 +2078,6 @@ def confirm_email_change(request, key): # pylint: disable=unused-argument
raise raise
# TODO: DELETE AFTER NEW ACCOUNT PAGE DONE
@ensure_csrf_cookie
@require_POST
def change_name_request(request):
""" Log a request for a new name. """
if not request.user.is_authenticated():
raise Http404
try:
pnc = PendingNameChange.objects.get(user=request.user.id)
except PendingNameChange.DoesNotExist:
pnc = PendingNameChange()
pnc.user = request.user
pnc.new_name = request.POST['new_name'].strip()
pnc.rationale = request.POST['rationale']
if len(pnc.new_name) < 2:
return JsonResponse({
"success": False,
"error": _('Name required'),
}) # TODO: this should be status code 400 # pylint: disable=fixme
pnc.save()
# The following automatically accepts name change requests. Remove this to
# go back to the old system where it gets queued up for admin approval.
accept_name_change_by_id(pnc.id)
return JsonResponse({"success": True})
# TODO: DELETE AFTER NEW ACCOUNT PAGE DONE
def accept_name_change_by_id(uid):
"""
Accepts the pending name change request for the user represented
by user id `uid`.
"""
try:
pnc = PendingNameChange.objects.get(id=uid)
except PendingNameChange.DoesNotExist:
return JsonResponse({
"success": False,
"error": _('Invalid ID'),
}) # TODO: this should be status code 400 # pylint: disable=fixme
user = pnc.user
u_prof = UserProfile.objects.get(user=user)
# Save old name
meta = u_prof.get_meta()
if 'old_names' not in meta:
meta['old_names'] = []
meta['old_names'].append([u_prof.name, pnc.rationale, datetime.datetime.now(UTC).isoformat()])
u_prof.set_meta(meta)
u_prof.name = pnc.new_name
u_prof.save()
pnc.delete()
return JsonResponse({"success": True})
@require_POST @require_POST
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
......
...@@ -4,13 +4,9 @@ from django.utils.translation import ugettext as _ ...@@ -4,13 +4,9 @@ from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
%> %>
<%block name="bodyclass">register verification-process step-select-track ${'is-upgrading' if upgrade else ''}</%block> <%block name="bodyclass">register verification-process step-select-track</%block>
<%block name="pagetitle"> <%block name="pagetitle">
% if upgrade: ${_("Enroll In {} | Choose Your Track").format(course_name)}
${_("Upgrade Your Enrollment for {} | Choose Your Track").format(course_name)}
% else:
${_("Enroll In {} | Choose Your Track").format(course_name)}
%endif
</%block> </%block>
<%block name="js_extra"> <%block name="js_extra">
...@@ -65,7 +61,11 @@ from django.core.urlresolvers import reverse ...@@ -65,7 +61,11 @@ from django.core.urlresolvers import reverse
<section class="wrapper"> <section class="wrapper">
<div class="wrapper-register-choose wrapper-content-main"> <div class="wrapper-register-choose wrapper-content-main">
<article class="register-choose content-main"> <article class="register-choose content-main">
<%include file="/verify_student/_verification_header.html" args="course_name=course_name" /> <header class="page-header content-main">
<h3 class="title">
${_("Congratulations! You are now enrolled in {course_name}").format(course_name=course_name)}
</h3>
</header>
<form class="form-register-choose" method="post" name="enrollment_mode_form" id="enrollment_mode_form"> <form class="form-register-choose" method="post" name="enrollment_mode_form" id="enrollment_mode_form">
% if "verified" in modes: % if "verified" in modes:
...@@ -129,36 +129,32 @@ from django.core.urlresolvers import reverse ...@@ -129,36 +129,32 @@ from django.core.urlresolvers import reverse
</div> </div>
% endif % endif
% if not upgrade: % if "honor" in modes:
% if "honor" in modes: <span class="deco-divider">
<span class="deco-divider"> <span class="copy">${_("or")}</span>
<span class="copy">${_("or")}</span> </span>
</span>
<div class="register-choice register-choice-audit"> <div class="register-choice register-choice-audit">
<div class="wrapper-copy"> <div class="wrapper-copy">
<span class="deco-ribbon"></span> <span class="deco-ribbon"></span>
<h4 class="title">${_("Audit This Course")}</h4> <h4 class="title">${_("Audit This Course")}</h4>
<div class="copy"> <div class="copy">
<p>${_("Audit this course for free and have complete access to all the course material, activities, tests, and forums. If your work is satisfactory and you abide by the Honor Code, you'll receive a personalized Honor Code Certificate to showcase your achievement.")}</p> <p>${_("Audit this course for free and have complete access to all the course material, activities, tests, and forums. If your work is satisfactory and you abide by the Honor Code, you'll receive a personalized Honor Code Certificate to showcase your achievement.")}</p>
</div>
</div> </div>
<ul class="list-actions">
<li class="action action-select">
<input type="submit" name="honor_mode" value="${_('Audit This Course')}" />
</li>
</ul>
</div> </div>
% endif
<ul class="list-actions">
<li class="action action-select">
<input type="submit" name="honor_mode" value="${_('Audit This Course')}" />
</li>
</ul>
</div>
% endif % endif
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }"> <input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
</form> </form>
</article> </article>
</div> <!-- /wrapper-content-main --> </div> <!-- /wrapper-content-main -->
<%include file="/verify_student/_verification_support.html" />
</section> </section>
</div> </div>
</%block> </%block>
...@@ -41,9 +41,8 @@ from student.models import CourseEnrollment ...@@ -41,9 +41,8 @@ from student.models import CourseEnrollment
from util.date_utils import get_default_time_display from util.date_utils import get_default_time_display
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
from verify_student.views import ( from verify_student.views import (
checkout_with_ecommerce_service, checkout_with_ecommerce_service, render_to_response, PayAndVerifyView,
render_to_response, PayAndVerifyView, EVENT_NAME_USER_ENTERED_INCOURSE_REVERIFY_VIEW, _send_email, _compose_message_reverification_email
EVENT_NAME_USER_SUBMITTED_INCOURSE_REVERIFY, _send_email, _compose_message_reverification_email
) )
from verify_student.models import ( from verify_student.models import (
SoftwareSecurePhotoVerification, VerificationCheckpoint, SoftwareSecurePhotoVerification, VerificationCheckpoint,
...@@ -1584,52 +1583,84 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase): ...@@ -1584,52 +1583,84 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase):
VerificationStatus.add_verification_status(checkpoint, self.user, "submitted") VerificationStatus.add_verification_status(checkpoint, self.user, "submitted")
class TestReverifyView(ModuleStoreTestCase): class TestReverifyView(TestCase):
""" """
Tests for the reverification views. Tests for the reverification view.
Reverification occurs when a verification attempt is denied or expired,
and the student is given the option to resubmit.
""" """
USERNAME = "shaftoe"
PASSWORD = "detachment-2702"
def setUp(self): def setUp(self):
super(TestReverifyView, self).setUp() super(TestReverifyView, self).setUp()
self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD)
success = self.client.login(username=self.USERNAME, password=self.PASSWORD)
self.assertTrue(success, msg="Could not log in")
self.user = UserFactory.create(username="rusty", password="test") def test_reverify_view_can_reverify_denied(self):
self.user.profile.name = u"Røøsty Bøøgins" # User has a denied attempt, so can reverify
self.user.profile.save() attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
self.client.login(username="rusty", password="test") attempt.mark_ready()
self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') attempt.submit()
self.course_key = self.course.id attempt.deny("error")
self._assert_can_reverify()
@patch('verify_student.views.render_to_response', render_mock) def test_reverify_view_can_reverify_expired(self):
def test_reverify_get(self): # User has a verification attempt, but it's expired
url = reverse('verify_student_reverify') attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
response = self.client.get(url) attempt.mark_ready()
self.assertEquals(response.status_code, 200) attempt.submit()
((_template, context), _kwargs) = render_mock.call_args # pylint: disable=unpacking-non-sequence attempt.approve()
self.assertFalse(context['error'])
@patch('verify_student.views.render_to_response', render_mock)
def test_reverify_post_failure(self):
url = reverse('verify_student_reverify')
response = self.client.post(url, {'face_image': '',
'photo_id_image': ''})
self.assertEquals(response.status_code, 200)
((template, context), _kwargs) = render_mock.call_args # pylint: disable=unpacking-non-sequence
self.assertIn('photo_reverification', template)
self.assertTrue(context['error'])
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) days_good_for = settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]
def test_reverify_post_success(self): attempt.created_at = datetime.now(pytz.UTC) - timedelta(days=(days_good_for + 1))
url = reverse('verify_student_reverify') attempt.save()
response = self.client.post(url, {'face_image': ',',
'photo_id_image': ','}) # Allow the student to reverify
self.assertEquals(response.status_code, 302) self._assert_can_reverify()
try:
verification_attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user) def test_reverify_view_cannot_reverify_pending(self):
self.assertIsNotNone(verification_attempt) # User has submitted a verification attempt, but Software Secure has not yet responded
except ObjectDoesNotExist: attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
self.fail('No verification object generated') attempt.mark_ready()
((template, context), _kwargs) = render_mock.call_args # pylint: disable=unpacking-non-sequence attempt.submit()
self.assertIn('photo_reverification', template)
self.assertTrue(context['error']) # Cannot reverify because an attempt has already been submitted.
self._assert_cannot_reverify()
def test_reverify_view_cannot_reverify_approved(self):
# Submitted attempt has been approved
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready()
attempt.submit()
attempt.approve()
# Cannot reverify because the user is already verified.
self._assert_cannot_reverify()
def _get_reverify_page(self):
"""
Retrieve the reverification page and return the response.
"""
url = reverse("verify_student_reverify")
return self.client.get(url)
def _assert_can_reverify(self):
"""
Check that the reverification flow is rendered.
"""
response = self._get_reverify_page()
self.assertContains(response, "reverify-container")
def _assert_cannot_reverify(self):
"""
Check that the user is blocked from reverifying.
"""
response = self._get_reverify_page()
self.assertContains(response, "reverify-blocked")
class TestInCourseReverifyView(ModuleStoreTestCase): class TestInCourseReverifyView(ModuleStoreTestCase):
...@@ -1727,7 +1758,7 @@ class TestInCourseReverifyView(ModuleStoreTestCase): ...@@ -1727,7 +1758,7 @@ class TestInCourseReverifyView(ModuleStoreTestCase):
# submitting the photo verification # submitting the photo verification
self.mock_tracker.track.assert_called_once_with( # pylint: disable=no-member self.mock_tracker.track.assert_called_once_with( # pylint: disable=no-member
self.user.id, # pylint: disable=no-member self.user.id, # pylint: disable=no-member
EVENT_NAME_USER_ENTERED_INCOURSE_REVERIFY_VIEW, 'edx.bi.reverify.started',
{ {
'category': "verification", 'category': "verification",
'label': unicode(self.course_key), 'label': unicode(self.course_key),
...@@ -1781,7 +1812,7 @@ class TestInCourseReverifyView(ModuleStoreTestCase): ...@@ -1781,7 +1812,7 @@ class TestInCourseReverifyView(ModuleStoreTestCase):
# photo verification # photo verification
self.mock_tracker.track.assert_called_once_with( # pylint: disable=no-member self.mock_tracker.track.assert_called_once_with( # pylint: disable=no-member
self.user.id, self.user.id,
EVENT_NAME_USER_SUBMITTED_INCOURSE_REVERIFY, 'edx.bi.reverify.submitted',
{ {
'category': "verification", 'category': "verification",
'label': unicode(self.course_key), 'label': unicode(self.course_key),
......
...@@ -90,28 +90,23 @@ urlpatterns = patterns( ...@@ -90,28 +90,23 @@ urlpatterns = patterns(
), ),
url( url(
r'^reverify$', r'^submit-photos/$',
views.ReverifyView.as_view(), views.submit_photos_for_verification,
name="verify_student_reverify" name="verify_student_submit_photos"
),
url(
r'^reverification_confirmation$',
views.reverification_submission_confirmation,
name="verify_student_reverification_confirmation"
), ),
# End-point for reverification
# Reverification occurs when a user's initial verification attempt
# is denied or expires. The user is allowed to retry by submitting
# new photos. This is different than *in-course* reverification,
# in which a student submits only face photos, which are matched
# against the ID photo from the user's initial verification attempt.
url( url(
r'^reverification_window_expired$', r'^reverify$',
views.reverification_window_expired, views.ReverifyView.as_view(),
name="verify_student_reverification_window_expired" name="verify_student_reverify"
), ),
url(
r'^submit-photos/$',
views.submit_photos_for_verification,
name="verify_student_submit_photos"
),
# Endpoint for in-course reverification # Endpoint for in-course reverification
# Users are sent to this end-point from within courseware # Users are sent to this end-point from within courseware
# to re-verify their identities by re-submitting face photos. # to re-verify their identities by re-submitting face photos.
......
...@@ -6,7 +6,6 @@ import datetime ...@@ -6,7 +6,6 @@ import datetime
import decimal import decimal
import json import json
import logging import logging
from collections import namedtuple
from pytz import UTC from pytz import UTC
from ipware.ip import get_ip from ipware.ip import get_ip
...@@ -14,10 +13,7 @@ from django.conf import settings ...@@ -14,10 +13,7 @@ from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.mail import send_mail from django.core.mail import send_mail
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import ( from django.http import HttpResponse, HttpResponseBadRequest, Http404
HttpResponse, HttpResponseBadRequest,
HttpResponseRedirect, Http404
)
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils import timezone from django.utils import timezone
...@@ -63,8 +59,6 @@ from staticfiles.storage import staticfiles_storage ...@@ -63,8 +59,6 @@ from staticfiles.storage import staticfiles_storage
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
EVENT_NAME_USER_ENTERED_INCOURSE_REVERIFY_VIEW = 'edx.bi.reverify.started'
EVENT_NAME_USER_SUBMITTED_INCOURSE_REVERIFY = 'edx.bi.reverify.submitted'
class PayAndVerifyView(View): class PayAndVerifyView(View):
...@@ -156,43 +150,14 @@ class PayAndVerifyView(View): ...@@ -156,43 +150,14 @@ class PayAndVerifyView(View):
INTRO_STEP, INTRO_STEP,
] ]
Step = namedtuple( STEP_TITLES = {
'Step', INTRO_STEP: ugettext_lazy("Intro"),
[ MAKE_PAYMENT_STEP: ugettext_lazy("Make payment"),
'title', PAYMENT_CONFIRMATION_STEP: ugettext_lazy("Payment confirmation"),
'template_name' FACE_PHOTO_STEP: ugettext_lazy("Take photo"),
] ID_PHOTO_STEP: ugettext_lazy("Take a photo of your ID"),
) REVIEW_PHOTOS_STEP: ugettext_lazy("Review your info"),
ENROLLMENT_CONFIRMATION_STEP: ugettext_lazy("Enrollment confirmation"),
STEP_INFO = {
INTRO_STEP: Step(
title=ugettext_lazy("Intro"),
template_name="intro_step"
),
MAKE_PAYMENT_STEP: Step(
title=ugettext_lazy("Make payment"),
template_name="make_payment_step"
),
PAYMENT_CONFIRMATION_STEP: Step(
title=ugettext_lazy("Payment confirmation"),
template_name="payment_confirmation_step"
),
FACE_PHOTO_STEP: Step(
title=ugettext_lazy("Take photo"),
template_name="face_photo_step"
),
ID_PHOTO_STEP: Step(
title=ugettext_lazy("Take a photo of your ID"),
template_name="id_photo_step"
),
REVIEW_PHOTOS_STEP: Step(
title=ugettext_lazy("Review your info"),
template_name="review_photos_step"
),
ENROLLMENT_CONFIRMATION_STEP: Step(
title=ugettext_lazy("Enrollment confirmation"),
template_name="enrollment_confirmation_step"
),
} }
# Messages # Messages
...@@ -554,8 +519,7 @@ class PayAndVerifyView(View): ...@@ -554,8 +519,7 @@ class PayAndVerifyView(View):
return [ return [
{ {
'name': step, 'name': step,
'title': unicode(self.STEP_INFO[step].title), 'title': unicode(self.STEP_TITLES[step]),
'templateName': self.STEP_INFO[step].template_name
} }
for step in display_steps for step in display_steps
if step not in remove_steps if step not in remove_steps
...@@ -1044,85 +1008,52 @@ def results_callback(request): ...@@ -1044,85 +1008,52 @@ def results_callback(request):
class ReverifyView(View): class ReverifyView(View):
""" """
The main reverification view. Under similar constraints as the main verification view. Reverification occurs when a user's initial verification is denied
Has to perform these functions: or expires. When this happens, users can re-submit photos through
- take new face photo the re-verification flow.
- take new id photo
- submit photos to photo verification service Unlike in-course reverification, this flow requires users to submit
*both* face and ID photos. In contrast, during in-course reverification,
Does not need to be attached to a particular course. students submit only face photos, which are matched against the ID photo
Does not need to worry about pricing the user submitted during initial verification.
""" """
@method_decorator(login_required) @method_decorator(login_required)
def get(self, request): def get(self, request):
""" """
display this view Render the reverification flow.
"""
context = {
"user_full_name": request.user.profile.name,
"error": False,
}
return render_to_response("verify_student/photo_reverification.html", context)
@method_decorator(login_required) Most of the work is done client-side by composing the same
def post(self, request): Backbone views used in the initial verification flow.
"""
submits the reverification to SoftwareSecure
""" """
status, _ = SoftwareSecurePhotoVerification.user_status(request.user)
try: if status in ["must_reverify", "expired"]:
attempt = SoftwareSecurePhotoVerification(user=request.user)
b64_face_image = request.POST['face_image'].split(",")[1]
b64_photo_id_image = request.POST['photo_id_image'].split(",")[1]
attempt.upload_face_image(b64_face_image.decode('base64'))
attempt.upload_photo_id_image(b64_photo_id_image.decode('base64'))
attempt.mark_ready()
# save this attempt
attempt.save()
# then submit it across
attempt.submit()
return HttpResponseRedirect(reverse('verify_student_reverification_confirmation'))
except Exception:
log.exception(
"Could not submit verification attempt for user {}".format(request.user.id)
)
context = { context = {
"user_full_name": request.user.profile.name, "user_full_name": request.user.profile.name,
"error": True, "platform_name": settings.PLATFORM_NAME,
"capture_sound": staticfiles_storage.url("audio/camera_capture.wav"),
} }
return render_to_response("verify_student/photo_reverification.html", context) return render_to_response("verify_student/reverify.html", context)
else:
context = {
@login_required "status": status
def reverification_submission_confirmation(_request): }
""" return render_to_response("verify_student/reverify_not_allowed.html", context)
Shows the user a confirmation page if the submission to SoftwareSecure was successful
"""
return render_to_response("verify_student/reverification_confirmation.html")
@login_required
def reverification_window_expired(_request):
"""
Displays an error page if a student tries to submit a reverification, but the window
for that reverification has already expired.
"""
# TODO need someone to review the copy for this template
return render_to_response("verify_student/reverification_window_expired.html")
class InCourseReverifyView(View): class InCourseReverifyView(View):
""" """
The in-course reverification view. The in-course reverification view.
Needs to perform these functions:
- take new face photo
- retrieve the old id photo
- submit these photos to photo verification service
Does not need to worry about pricing In-course reverification occurs while a student is taking a course.
At points in the course, students are prompted to submit face photos,
which are matched against the ID photos the user submitted during their
initial verification.
Students are prompted to enter this flow from an "In Course Reverification"
XBlock (courseware component) that course authors add to the course.
See https://github.com/edx/edx-reverification-block for more details.
""" """
@method_decorator(login_required) @method_decorator(login_required)
def get(self, request, course_id, usage_id): def get(self, request, course_id, usage_id):
...@@ -1168,9 +1099,7 @@ class InCourseReverifyView(View): ...@@ -1168,9 +1099,7 @@ class InCourseReverifyView(View):
return self._redirect_no_initial_verification(user, course_key) return self._redirect_no_initial_verification(user, course_key)
# emit the reverification event # emit the reverification event
self._track_reverification_events( self._track_reverification_events('edx.bi.reverify.started', user.id, course_id, checkpoint.checkpoint_name)
EVENT_NAME_USER_ENTERED_INCOURSE_REVERIFY_VIEW, user.id, course_id, checkpoint.checkpoint_name
)
context = { context = {
'course_key': unicode(course_key), 'course_key': unicode(course_key),
...@@ -1235,7 +1164,8 @@ class InCourseReverifyView(View): ...@@ -1235,7 +1164,8 @@ class InCourseReverifyView(View):
# emit the reverification event # emit the reverification event
self._track_reverification_events( self._track_reverification_events(
EVENT_NAME_USER_SUBMITTED_INCOURSE_REVERIFY, user.id, course_id, checkpoint.checkpoint_name 'edx.bi.reverify.submitted',
user.id, course_id, checkpoint.checkpoint_name
) )
redirect_url = get_redirect_url(course_key, usage_key) redirect_url = get_redirect_url(course_key, usage_key)
......
...@@ -1310,6 +1310,20 @@ reverify_js = [ ...@@ -1310,6 +1310,20 @@ reverify_js = [
'js/verify_student/views/error_view.js', 'js/verify_student/views/error_view.js',
'js/verify_student/views/image_input_view.js', 'js/verify_student/views/image_input_view.js',
'js/verify_student/views/webcam_photo_view.js', 'js/verify_student/views/webcam_photo_view.js',
'js/verify_student/views/step_view.js',
'js/verify_student/views/face_photo_step_view.js',
'js/verify_student/views/id_photo_step_view.js',
'js/verify_student/views/review_photos_step_view.js',
'js/verify_student/views/reverify_success_step_view.js',
'js/verify_student/models/verification_model.js',
'js/verify_student/views/reverify_view.js',
'js/verify_student/reverify.js',
]
incourse_reverify_js = [
'js/verify_student/views/error_view.js',
'js/verify_student/views/image_input_view.js',
'js/verify_student/views/webcam_photo_view.js',
'js/verify_student/models/reverification_model.js', 'js/verify_student/models/reverification_model.js',
'js/verify_student/views/incourse_reverify_view.js', 'js/verify_student/views/incourse_reverify_view.js',
'js/verify_student/incourse_reverify.js', 'js/verify_student/incourse_reverify.js',
...@@ -1541,6 +1555,10 @@ PIPELINE_JS = { ...@@ -1541,6 +1555,10 @@ PIPELINE_JS = {
'source_filenames': reverify_js, 'source_filenames': reverify_js,
'output_filename': 'js/reverify.js' 'output_filename': 'js/reverify.js'
}, },
'incourse_reverify': {
'source_filenames': incourse_reverify_js,
'output_filename': 'js/incourse_reverify.js'
},
'ccx': { 'ccx': {
'source_filenames': ccx_js, 'source_filenames': ccx_js,
'output_filename': 'js/ccx.js' 'output_filename': 'js/ccx.js'
......
...@@ -61,7 +61,6 @@ ...@@ -61,7 +61,6 @@
// Manually specify LMS files that are not converted to RequireJS // Manually specify LMS files that are not converted to RequireJS
'history': 'js/vendor/history', 'history': 'js/vendor/history',
'js/mustache': 'js/mustache', 'js/mustache': 'js/mustache',
'js/verify_student/photocapture': 'js/verify_student/photocapture',
'js/staff_debug_actions': 'js/staff_debug_actions', 'js/staff_debug_actions': 'js/staff_debug_actions',
'js/vendor/jquery.qubit': 'js/vendor/jquery.qubit', 'js/vendor/jquery.qubit': 'js/vendor/jquery.qubit',
...@@ -284,9 +283,6 @@ ...@@ -284,9 +283,6 @@
exports: 'js/student_profile/profile', exports: 'js/student_profile/profile',
deps: ['jquery', 'underscore', 'backbone', 'gettext', 'jquery.cookie'] deps: ['jquery', 'underscore', 'backbone', 'gettext', 'jquery.cookie']
}, },
'js/verify_student/photocapture': {
exports: 'js/verify_student/photocapture'
},
'js/staff_debug_actions': { 'js/staff_debug_actions': {
exports: 'js/staff_debug_actions', exports: 'js/staff_debug_actions',
deps: ['gettext'] deps: ['gettext']
...@@ -501,6 +497,7 @@ ...@@ -501,6 +497,7 @@
'gettext', 'gettext',
'jquery.cookie', 'jquery.cookie',
'jquery.url', 'jquery.url',
'string_utils',
'js/verify_student/views/step_view', 'js/verify_student/views/step_view',
] ]
}, },
...@@ -550,6 +547,13 @@ ...@@ -550,6 +547,13 @@
'js/verify_student/views/step_view', 'js/verify_student/views/step_view',
] ]
}, },
'js/verify_student/views/reverify_success_step_view': {
exports: 'edx.verify_student.ReverifySuccessStepView',
deps: [
'jquery',
'js/verify_student/views/step_view',
]
},
'js/verify_student/views/pay_and_verify_view': { 'js/verify_student/views/pay_and_verify_view': {
exports: 'edx.verify_student.PayAndVerifyView', exports: 'edx.verify_student.PayAndVerifyView',
deps: [ deps: [
...@@ -567,6 +571,20 @@ ...@@ -567,6 +571,20 @@
'js/verify_student/views/enrollment_confirmation_step_view' 'js/verify_student/views/enrollment_confirmation_step_view'
] ]
}, },
'js/verify_student/views/reverify_view': {
exports: 'edx.verify_student.ReverifyView',
deps: [
'jquery',
'underscore',
'backbone',
'gettext',
'js/verify_student/models/verification_model',
'js/verify_student/views/face_photo_step_view',
'js/verify_student/views/id_photo_step_view',
'js/verify_student/views/enrollment_confirmation_step_view',
'js/verify_student/views/reverify_success_step_view'
]
},
// Student Notes // Student Notes
'annotator_1.2.9': { 'annotator_1.2.9': {
exports: 'Annotator', exports: 'Annotator',
...@@ -583,7 +601,6 @@ ...@@ -583,7 +601,6 @@
'lms/include/js/spec/components/header/header_spec.js', 'lms/include/js/spec/components/header/header_spec.js',
'lms/include/js/spec/components/tabbed/tabbed_view_spec.js', 'lms/include/js/spec/components/tabbed/tabbed_view_spec.js',
'lms/include/js/spec/components/card/card_spec.js', 'lms/include/js/spec/components/card/card_spec.js',
'lms/include/js/spec/photocapture_spec.js',
'lms/include/js/spec/staff_debug_actions_spec.js', 'lms/include/js/spec/staff_debug_actions_spec.js',
'lms/include/js/spec/views/notification_spec.js', 'lms/include/js/spec/views/notification_spec.js',
'lms/include/js/spec/views/file_uploader_spec.js', 'lms/include/js/spec/views/file_uploader_spec.js',
...@@ -610,6 +627,7 @@ ...@@ -610,6 +627,7 @@
'lms/include/js/spec/student_profile/learner_profile_view_spec.js', 'lms/include/js/spec/student_profile/learner_profile_view_spec.js',
'lms/include/js/spec/student_profile/learner_profile_fields_spec.js', 'lms/include/js/spec/student_profile/learner_profile_fields_spec.js',
'lms/include/js/spec/verify_student/pay_and_verify_view_spec.js', 'lms/include/js/spec/verify_student/pay_and_verify_view_spec.js',
'lms/include/js/spec/verify_student/reverify_view_spec.js',
'lms/include/js/spec/verify_student/webcam_photo_view_spec.js', 'lms/include/js/spec/verify_student/webcam_photo_view_spec.js',
'lms/include/js/spec/verify_student/image_input_spec.js', 'lms/include/js/spec/verify_student/image_input_spec.js',
'lms/include/js/spec/verify_student/review_photos_step_view_spec.js', 'lms/include/js/spec/verify_student/review_photos_step_view_spec.js',
......
define(['backbone', 'jquery', 'js/verify_student/photocapture'],
function (Backbone, $) {
describe("Photo Verification", function () {
beforeEach(function () {
setFixtures('<div id="order-error" style="display: none;"></div><input type="radio" name="contribution" value="35" id="contribution-35" checked="checked"><input type="radio" id="contribution-other" name="contribution" value=""><input type="text" size="9" name="contribution-other-amt" id="contribution-other-amt" value="30"><img id="face_image" src="src="data:image/png;base64,dummy"><img id="photo_id_image" src="src="data:image/png;base64,dummy"><button class="payment-button">pay button</button>');
});
it('retake photo', function () {
spyOn(window, "refereshPageMessage").andCallFake(function () {
return;
});
spyOn($, "ajax").andCallFake(function (e) {
e.success({"success": false});
});
submitToPaymentProcessing();
expect(window.refereshPageMessage).toHaveBeenCalled();
});
it('successful submission', function () {
spyOn(window, "submitForm").andCallFake(function () {
return;
});
spyOn($, "ajax").andCallFake(function (e) {
e.success({"success": true});
});
submitToPaymentProcessing();
expect(window.submitForm).toHaveBeenCalled();
expect($(".payment-button")).toHaveClass("is-disabled");
});
it('Error during process', function () {
spyOn(window, "showSubmissionError").andCallFake(function () {
return;
});
spyOn($, "ajax").andCallFake(function (e) {
e.error({});
});
spyOn($.fn, "addClass").andCallThrough();
spyOn($.fn, "removeClass").andCallThrough();
submitToPaymentProcessing();
expect(window.showSubmissionError).toHaveBeenCalled();
// make sure the button isn't disabled
expect($(".payment-button")).not.toHaveClass("is-disabled");
// but also make sure that it was disabled during the ajax call
expect($.fn.addClass).toHaveBeenCalledWith("is-disabled");
expect($.fn.removeClass).toHaveBeenCalledWith("is-disabled");
});
});
});
...@@ -29,7 +29,6 @@ define([ ...@@ -29,7 +29,6 @@ define([
var createView = function( stepDataOverrides ) { var createView = function( stepDataOverrides ) {
var view = new MakePaymentStepView({ var view = new MakePaymentStepView({
el: $( '#current-step-container' ), el: $( '#current-step-container' ),
templateName: 'make_payment_step',
stepData: _.extend( _.clone( STEP_DATA ), stepDataOverrides ), stepData: _.extend( _.clone( STEP_DATA ), stepDataOverrides ),
errorModel: new ( Backbone.Model.extend({}) )() errorModel: new ( Backbone.Model.extend({}) )()
}).render(); }).render();
......
...@@ -18,19 +18,16 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/ ...@@ -18,19 +18,16 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/
]; ];
var INTRO_STEP = { var INTRO_STEP = {
templateName: "intro_step",
name: "intro-step", name: "intro-step",
title: "Intro" title: "Intro"
}; };
var DISPLAY_STEPS_FOR_PAYMENT = [ var DISPLAY_STEPS_FOR_PAYMENT = [
{ {
templateName: "make_payment_step",
name: "make-payment-step", name: "make-payment-step",
title: "Make Payment" title: "Make Payment"
}, },
{ {
templateName: "payment_confirmation_step",
name: "payment-confirmation-step", name: "payment-confirmation-step",
title: "Payment Confirmation" title: "Payment Confirmation"
} }
...@@ -38,22 +35,18 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/ ...@@ -38,22 +35,18 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/
var DISPLAY_STEPS_FOR_VERIFICATION = [ var DISPLAY_STEPS_FOR_VERIFICATION = [
{ {
templateName: "face_photo_step",
name: "face-photo-step", name: "face-photo-step",
title: "Take Face Photo" title: "Take Face Photo"
}, },
{ {
templateName: "id_photo_step",
name: "id-photo-step", name: "id-photo-step",
title: "ID Photo" title: "ID Photo"
}, },
{ {
templateName: "review_photos_step",
name: "review-photos-step", name: "review-photos-step",
title: "Review Photos" title: "Review Photos"
}, },
{ {
templateName: "enrollment_confirmation_step",
name: "enrollment-confirmation-step", name: "enrollment-confirmation-step",
title: "Enrollment Confirmation" title: "Enrollment Confirmation"
} }
...@@ -67,7 +60,7 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/ ...@@ -67,7 +60,7 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/
}).render(); }).render();
}; };
var expectStepRendered = function( stepName, stepNum, numSteps ) { var expectStepRendered = function( stepName ) {
// Expect that the step container div rendered // Expect that the step container div rendered
expect( $( '.' + stepName ).length > 0 ).toBe( true ); expect( $( '.' + stepName ).length > 0 ).toBe( true );
}; };
...@@ -89,27 +82,27 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/ ...@@ -89,27 +82,27 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/
); );
// Verify that the first step rendered // Verify that the first step rendered
expectStepRendered('make-payment-step', 1, 6); expectStepRendered('make-payment-step');
// Iterate through the steps, ensuring that each is rendered // Iterate through the steps, ensuring that each is rendered
view.nextStep(); view.nextStep();
expectStepRendered('payment-confirmation-step', 2, 6); expectStepRendered('payment-confirmation-step');
view.nextStep(); view.nextStep();
expectStepRendered('face-photo-step', 3, 6); expectStepRendered('face-photo-step');
view.nextStep(); view.nextStep();
expectStepRendered('id-photo-step', 4, 6); expectStepRendered('id-photo-step');
view.nextStep(); view.nextStep();
expectStepRendered('review-photos-step', 5, 6); expectStepRendered('review-photos-step');
view.nextStep(); view.nextStep();
expectStepRendered('enrollment-confirmation-step', 6, 6); expectStepRendered('enrollment-confirmation-step');
// Going past the last step stays on the last step // Going past the last step stays on the last step
view.nextStep(); view.nextStep();
expectStepRendered('enrollment-confirmation-step', 6, 6); expectStepRendered('enrollment-confirmation-step');
}); });
it( 'renders intro and verification steps', function() { it( 'renders intro and verification steps', function() {
...@@ -119,20 +112,20 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/ ...@@ -119,20 +112,20 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/
); );
// Verify that the first step rendered // Verify that the first step rendered
expectStepRendered('intro-step', 1, 5); expectStepRendered('intro-step');
// Iterate through the steps, ensuring that each is rendered // Iterate through the steps, ensuring that each is rendered
view.nextStep(); view.nextStep();
expectStepRendered('face-photo-step', 2, 5); expectStepRendered('face-photo-step');
view.nextStep(); view.nextStep();
expectStepRendered('id-photo-step', 3, 5); expectStepRendered('id-photo-step');
view.nextStep(); view.nextStep();
expectStepRendered('review-photos-step', 4, 5); expectStepRendered('review-photos-step');
view.nextStep(); view.nextStep();
expectStepRendered('enrollment-confirmation-step', 5, 5); expectStepRendered('enrollment-confirmation-step');
}); });
it( 'starts from a later step', function() { it( 'starts from a later step', function() {
...@@ -143,11 +136,11 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/ ...@@ -143,11 +136,11 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/
); );
// Verify that we start on the right step // Verify that we start on the right step
expectStepRendered('payment-confirmation-step', 2, 6); expectStepRendered('payment-confirmation-step');
// Try moving to the next step // Try moving to the next step
view.nextStep(); view.nextStep();
expectStepRendered('face-photo-step', 3, 6); expectStepRendered('face-photo-step');
}); });
...@@ -160,7 +153,7 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/ ...@@ -160,7 +153,7 @@ define(['jquery', 'common/js/spec_helpers/template_helpers', 'js/verify_student/
// Jump back to the face photo step // Jump back to the face photo step
view.goToStep('face-photo-step'); view.goToStep('face-photo-step');
expectStepRendered('face-photo-step', 1, 4); expectStepRendered('face-photo-step');
}); });
}); });
......
/**
* Tests for the reverification view.
**/
define(['jquery', 'js/common_helpers/template_helpers', 'js/verify_student/views/reverify_view'],
function( $, TemplateHelpers, ReverifyView ) {
'use strict';
describe( 'edx.verify_student.ReverifyView', function() {
var TEMPLATES = [
"reverify",
"webcam_photo",
"image_input",
"error",
"face_photo_step",
"id_photo_step",
"review_photos_step",
"reverify_success_step"
];
var STEP_INFO = {
'face-photo-step': {
platformName: 'edX',
},
'id-photo-step': {
platformName: 'edX',
},
'review-photos-step': {
fullName: 'John Doe',
platformName: 'edX'
},
'reverify-success-step': {
platformName: 'edX'
}
};
var createView = function() {
return new ReverifyView({stepInfo: STEP_INFO}).render();
};
var expectStepRendered = function( stepName ) {
// Expect that the step container div rendered
expect( $( '.' + stepName ).length > 0 ).toBe( true );
};
beforeEach(function() {
window.analytics = jasmine.createSpyObj('analytics', ['track', 'page', 'trackLink']);
setFixtures('<div id="reverify-container"></div>');
$.each( TEMPLATES, function( index, templateName ) {
TemplateHelpers.installTemplate('templates/verify_student/' + templateName );
});
});
it( 'renders verification steps', function() {
var view = createView();
// Go through the flow, verifying that each step renders
// We rely on other unit tests to check the behavior of these subviews.
expectStepRendered('face-photo-step');
view.nextStep();
expectStepRendered('id-photo-step');
view.nextStep();
expectStepRendered('review-photos-step');
view.nextStep();
expectStepRendered('reverify-success-step');
});
});
}
);
...@@ -21,7 +21,6 @@ define([ ...@@ -21,7 +21,6 @@ define([
var createView = function() { var createView = function() {
return new ReviewPhotosStepView({ return new ReviewPhotosStepView({
el: $( '#current-step-container' ), el: $( '#current-step-container' ),
templateName: 'review_photos_step',
stepData: STEP_DATA, stepData: STEP_DATA,
model: new VerificationModel({ model: new VerificationModel({
faceImage: FACE_IMAGE, faceImage: FACE_IMAGE,
......
...@@ -74,10 +74,12 @@ var edx = edx || {}; ...@@ -74,10 +74,12 @@ var edx = edx || {};
requirements: el.data('requirements') requirements: el.data('requirements')
}, },
'face-photo-step': { 'face-photo-step': {
platformName: el.data('platform-name') platformName: el.data('platform-name'),
captureSoundPath: el.data('capture-sound')
}, },
'id-photo-step': { 'id-photo-step': {
platformName: el.data('platform-name') platformName: el.data('platform-name'),
captureSoundPath: el.data('capture-sound')
}, },
'review-photos-step': { 'review-photos-step': {
fullName: el.data('full-name'), fullName: el.data('full-name'),
......
/**
* Reverification flow.
*
* This flow allows students who have a denied or expired verification
* to re-submit face and ID photos. It re-uses most of the same sub-views
* as the payment/verification flow.
*/
var edx = edx || {};
(function( $, _ ) {
'use strict';
var errorView,
el = $('#reverify-container');
edx.verify_student = edx.verify_student || {};
// Initialize an error view for displaying top-level error messages.
errorView = new edx.verify_student.ErrorView({
el: $('#error-container')
});
// Initialize the base view, passing in information
// from the data attributes on the parent div.
return new edx.verify_student.ReverifyView({
errorModel: errorView.model,
stepInfo: {
'face-photo-step': {
platformName: el.data('platform-name'),
captureSoundPath: el.data('capture-sound')
},
'id-photo-step': {
platformName: el.data('platform-name'),
captureSoundPath: el.data('capture-sound')
},
'review-photos-step': {
fullName: el.data('full-name'),
platformName: el.data('platform-name')
},
'reverify-success-step': {
platformName: el.data('platform-name')
}
}
}).render();
})( jQuery, _ );
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
*/ */
var edx = edx || {}; var edx = edx || {};
(function( $ ) { (function() {
'use strict'; 'use strict';
edx.verify_student = edx.verify_student || {}; edx.verify_student = edx.verify_student || {};
...@@ -12,6 +12,9 @@ var edx = edx || {}; ...@@ -12,6 +12,9 @@ var edx = edx || {};
// Currently, this step does not need to install any event handlers, // Currently, this step does not need to install any event handlers,
// since the displayed information is static. // since the displayed information is static.
edx.verify_student.EnrollmentConfirmationStepView = edx.verify_student.StepView.extend({ edx.verify_student.EnrollmentConfirmationStepView = edx.verify_student.StepView.extend({
templateName: 'enrollment_confirmation_step',
postRender: function() { postRender: function() {
// Track a virtual pageview, for easy funnel reconstruction. // Track a virtual pageview, for easy funnel reconstruction.
window.analytics.page( 'verification', this.templateName ); window.analytics.page( 'verification', this.templateName );
...@@ -27,4 +30,4 @@ var edx = edx || {}; ...@@ -27,4 +30,4 @@ var edx = edx || {};
} }
}); });
})( jQuery ); })();
...@@ -10,6 +10,8 @@ var edx = edx || {}; ...@@ -10,6 +10,8 @@ var edx = edx || {};
edx.verify_student.FacePhotoStepView = edx.verify_student.StepView.extend({ edx.verify_student.FacePhotoStepView = edx.verify_student.StepView.extend({
templateName: "face_photo_step",
defaultContext: function() { defaultContext: function() {
return { return {
platformName: '' platformName: ''
...@@ -22,7 +24,8 @@ var edx = edx || {}; ...@@ -22,7 +24,8 @@ var edx = edx || {};
model: this.model, model: this.model,
modelAttribute: 'faceImage', modelAttribute: 'faceImage',
submitButton: '#next_step_button', submitButton: '#next_step_button',
errorModel: this.errorModel errorModel: this.errorModel,
captureSoundPath: this.stepData.captureSoundPath
}).render(); }).render();
// Track a virtual pageview, for easy funnel reconstruction. // Track a virtual pageview, for easy funnel reconstruction.
......
...@@ -10,6 +10,8 @@ var edx = edx || {}; ...@@ -10,6 +10,8 @@ var edx = edx || {};
edx.verify_student.IDPhotoStepView = edx.verify_student.StepView.extend({ edx.verify_student.IDPhotoStepView = edx.verify_student.StepView.extend({
templateName: "id_photo_step",
defaultContext: function() { defaultContext: function() {
return { return {
platformName: '' platformName: ''
...@@ -22,7 +24,8 @@ var edx = edx || {}; ...@@ -22,7 +24,8 @@ var edx = edx || {};
model: this.model, model: this.model,
modelAttribute: 'identificationImage', modelAttribute: 'identificationImage',
submitButton: '#next_step_button', submitButton: '#next_step_button',
errorModel: this.errorModel errorModel: this.errorModel,
captureSoundPath: this.stepData.captureSoundPath
}).render(); }).render();
// Track a virtual pageview, for easy funnel reconstruction. // Track a virtual pageview, for easy funnel reconstruction.
......
...@@ -10,6 +10,8 @@ var edx = edx || {}; ...@@ -10,6 +10,8 @@ var edx = edx || {};
edx.verify_student.IntroStepView = edx.verify_student.StepView.extend({ edx.verify_student.IntroStepView = edx.verify_student.StepView.extend({
templateName: "intro_step",
defaultContext: function() { defaultContext: function() {
return { return {
introTitle: '', introTitle: '',
......
...@@ -10,6 +10,8 @@ var edx = edx || {}; ...@@ -10,6 +10,8 @@ var edx = edx || {};
edx.verify_student.MakePaymentStepView = edx.verify_student.StepView.extend({ edx.verify_student.MakePaymentStepView = edx.verify_student.StepView.extend({
templateName: "make_payment_step",
defaultContext: function() { defaultContext: function() {
return { return {
isActive: true, isActive: true,
......
...@@ -83,7 +83,6 @@ var edx = edx || {}; ...@@ -83,7 +83,6 @@ var edx = edx || {};
subviewConfig = { subviewConfig = {
errorModel: this.errorModel, errorModel: this.errorModel,
templateName: this.displaySteps[i].templateName,
nextStepTitle: nextStepTitle, nextStepTitle: nextStepTitle,
stepData: stepData stepData: stepData
}; };
...@@ -121,8 +120,6 @@ var edx = edx || {}; ...@@ -121,8 +120,6 @@ var edx = edx || {};
} }
// Render the subview // Render the subview
// Note that this will trigger a GET request for the
// underscore template.
// When the view is rendered, it will overwrite the existing // When the view is rendered, it will overwrite the existing
// step in the DOM. // step in the DOM.
stepName = this.displaySteps[ this.currentStepIndex ].name; stepName = this.displaySteps[ this.currentStepIndex ].name;
......
...@@ -10,6 +10,8 @@ var edx = edx || {}; ...@@ -10,6 +10,8 @@ var edx = edx || {};
edx.verify_student.PaymentConfirmationStepView = edx.verify_student.StepView.extend({ edx.verify_student.PaymentConfirmationStepView = edx.verify_student.StepView.extend({
templateName: "payment_confirmation_step",
defaultContext: function() { defaultContext: function() {
return { return {
courseKey: '', courseKey: '',
......
/**
* Show a message to the student that he/she has successfully
* submitted photos for reverification.
*/
var edx = edx || {};
(function() {
'use strict';
edx.verify_student = edx.verify_student || {};
edx.verify_student.ReverifySuccessStepView = edx.verify_student.StepView.extend({
templateName: 'reverify_success_step'
});
})();
/**
* Reverification flow.
*
* This flow allows students who have a denied or expired verification
* to re-submit face and ID photos. It re-uses most of the same sub-views
* as the payment/verification flow.
*
*/
var edx = edx || {};
(function($, _, Backbone, gettext) {
'use strict';
edx.verify_student = edx.verify_student || {};
edx.verify_student.ReverifyView = Backbone.View.extend({
el: '#reverify-container',
stepOrder: [
"face-photo-step",
"id-photo-step",
"review-photos-step",
"reverify-success-step"
],
stepViews: {},
initialize: function( obj ) {
this.errorModel = obj.errorModel || null;
this.initializeStepViews( obj.stepInfo || {} );
this.currentStepIndex = 0;
},
initializeStepViews: function( stepInfo ) {
var verificationModel, stepViewConstructors, nextStepTitles;
// We need to initialize this here, because
// outside of this method the subview classes
// might not yet have been loaded.
stepViewConstructors = {
'face-photo-step': edx.verify_student.FacePhotoStepView,
'id-photo-step': edx.verify_student.IDPhotoStepView,
'review-photos-step': edx.verify_student.ReviewPhotosStepView,
'reverify-success-step': edx.verify_student.ReverifySuccessStepView
};
nextStepTitles = [
gettext( "Take a photo of your ID" ),
gettext( "Review your info" ),
gettext( "Confirm" ),
""
];
// Create the verification model, which is shared
// among the different steps. This allows
// one step to save photos and another step
// to submit them.
verificationModel = new edx.verify_student.VerificationModel();
_.each(this.stepOrder, function(name, index) {
var stepView = new stepViewConstructors[name]({
errorModel: this.errorModel,
nextStepTitle: nextStepTitles[index],
stepData: stepInfo[name],
model: verificationModel
});
this.listenTo(stepView, 'next-step', this.nextStep);
this.listenTo(stepView, 'go-to-step', this.goToStep);
this.stepViews[name] = stepView;
}, this);
},
render: function() {
this.renderCurrentStep();
return this;
},
renderCurrentStep: function() {
var stepView, stepEl;
// Get or create the step container
stepEl = $("#current-step-container");
if (!stepEl.length) {
stepEl = $('<div id="current-step-container"></div>').appendTo(this.el);
}
// Render the step subview
// When the view is rendered, it will overwrite the existing step in the DOM.
stepView = this.stepViews[ this.stepOrder[ this.currentStepIndex ] ];
stepView.el = stepEl;
stepView.render();
},
nextStep: function() {
this.currentStepIndex = Math.min(
this.currentStepIndex + 1,
this.stepOrder.length - 1
);
this.render();
},
goToStep: function( stepName ) {
var stepIndex = _.indexOf(this.stepOrder, stepName);
if ( stepIndex >= 0 ) {
this.currentStepIndex = stepIndex;
}
this.render();
}
});
})(jQuery, _, Backbone, gettext);
...@@ -10,6 +10,8 @@ var edx = edx || {}; ...@@ -10,6 +10,8 @@ var edx = edx || {};
edx.verify_student.ReviewPhotosStepView = edx.verify_student.StepView.extend({ edx.verify_student.ReviewPhotosStepView = edx.verify_student.StepView.extend({
templateName: "review_photos_step",
defaultContext: function() { defaultContext: function() {
return { return {
platformName: '', platformName: '',
......
...@@ -212,15 +212,11 @@ ...@@ -212,15 +212,11 @@
}, },
initialize: function( obj ) { initialize: function( obj ) {
this.mainContainer = $('#pay-and-verify-container');
if (!this.mainContainer){
this.mainContainer = $('#incourse-reverify-container');
}
this.submitButton = obj.submitButton || ""; this.submitButton = obj.submitButton || "";
this.modelAttribute = obj.modelAttribute || ""; this.modelAttribute = obj.modelAttribute || "";
this.errorModel = obj.errorModel || null; this.errorModel = obj.errorModel || null;
this.backend = this.backends[obj.backendName] || obj.backend; this.backend = this.backends[obj.backendName] || obj.backend;
this.captureSoundPath = this.mainContainer.data('capture-sound'); this.captureSoundPath = obj.captureSoundPath || "";
this.backend.initialize({ this.backend.initialize({
wrapper: "#camera", wrapper: "#camera",
......
// Updates for decoupled verification A/B test // Updates for decoupled verification A/B test
.verification-process { .verification-process {
.pay-and-verify, .incourse-reverify { .pay-and-verify, .incourse-reverify, .reverify {
.review { .review {
.title.center-col { .title.center-col {
padding: 0 calc( ( 100% - 750px ) / 2 ) 10px; padding: 0 calc( ( 100% - 750px ) / 2 ) 10px;
......
<%! from django.utils.translation import ugettext as _ %>
<section id="edit-name" class="modal">
<div class="inner-wrapper">
<header>
<h2>${_("Edit Your Name")}</h2>
<hr />
</header>
<div id="change_name_body">
<form id="course-checklists" class="course-checklists" method="post" action="">
<div role="alert" class="status message submission-error" tabindex="-1">
<p class="message-title">${_("The following error occurred while editing your name:")}
<span class="message-copy"> </span>
</p>
</div>
<p>${_("To uphold the credibility of {platform} certificates, all name changes will be logged and recorded.").format(platform=settings.PLATFORM_NAME)}</p>
<fieldset>
<div class="input-group">
<label for="name">${_('Full Name')}</label>
<input id="name" type="text" name="name" value="" placeholder="${user_full_name}" required aria-required="true" />
<label>${_("Reason for name change:")}</label>
<textarea id="name_rationale_field" value=""></textarea>
</div>
<div class="actions">
<button id="submit" class="action action-primary action-save">${_("Change my name")}</button>
</div>
</form>
</div>
<a href="javascript:void(0)" data-dismiss="leanModal" rel="view" class="action action-close action-editname-close close-modal">
<i class="icon fa fa-times-circle"></i>
<span class="sr">${_("close")}</span>
</a>
</section>
<%! from django.utils.translation import ugettext as _ %>
<div class="wrapper-content-supplementary">
<aside class="content-supplementary">
<ul class="list-help">
<li class="help-item help-item-whyreverify">
<h3 class="title">${_("Why Do I Need to Re-Verify My Identity?")}</h3>
<div class="copy">
<p>${_("You may need to re-verify your identity if an error occurs with your verification or if your verification has expired. All verifications expire after one year. The re-verification process is the same as the original verification process. You need a webcam and a government-issued photo ID.")}</p>
</div>
</li>
<li class="help-item help-item-technical">
<h3 class="title">${_("Having Technical Trouble?")}</h3>
<div class="copy">
<p>${_("Please make sure your browser is updated to the <strong>{a_start}most recent version possible{a_end}</strong>. Also, please make sure your <strong>webcam is plugged in, turned on, and allowed to function in your web browser (commonly adjustable in your browser settings)</strong>").format(a_start='<a rel="external" href="http://browsehappy.com/">', a_end="</a>")}</p>
</div>
</li>
<li class="help-item help-item-questions">
<h3 class="title">${_("Have questions?")}</h3>
<div class="copy">
<p>${_("Please read {a_start}our FAQs to view common questions about our certificates{a_end}.").format(a_start='<a rel="external" href="'+ marketing_link('WHAT_IS_VERIFIED_CERT') + '">', a_end="</a>")}</p>
</div>
</li>
</ul>
</aside>
</div> <!-- /wrapper-content-supplementary -->
<%! from django.utils.translation import ugettext as _ %>
<%namespace name='static' file='../static_content.html'/>
<header class="page-header content-main">
<h3 class="title">
<% course_name_html = '<span class="sts-course-name">' + course_num + '</span>' %>
<% course_display_html = course_org + "'s " + course_num + " " + course_name %>
% if upgrade:
${_("You are upgrading your enrollment for: {course_name}").format(
course_name=course_name_html
)}
% elif reverify:
${_("You are re-verifying for: {course_name}").format(
course_name=course_name_html
)}
% elif modes_dict and "professional" in modes_dict:
${_("You are enrolling in: {course_name}").format(
course_name=course_name_html
)}
% else:
${_("Congratulations! You are now enrolled in {course_display}").format(
course_display=course_display_html
)}
% endif
</h3>
</header>
<%block name="js_extra">
<%static:js group='rwd_header'/>
</%block>
<%! from django.utils.translation import ugettext as _ %>
<div class="wrapper-content-supplementary">
<aside class="content-supplementary">
<ul class="list-help">
<li class="help-item help-item-questions">
<h3 class="title">${_("Have questions?")}</h3>
<div class="copy">
<p>${_("Please read {a_start}our FAQs to view common questions about our certificates{a_end}.").format(a_start='<a rel="external" href="'+ marketing_link('WHAT_IS_VERIFIED_CERT') + '">', a_end="</a>")}</p>
</div>
</li>
<li class="help-item help-item-technical">
<h3 class="title">${_("Technical Requirements")}</h3>
<div class="copy">
<p>${_("Please make sure your browser is updated to the {a_start}most recent version possible{a_end}. Also, please make sure your <strong>webcam is plugged in, turned on, and allowed to function in your web browser (commonly adjustable in your browser settings).</strong>").format(a_start='<strong><a rel="external" href="http://browsehappy.com/">', a_end="</a></strong>")}</p>
</div>
</li>
</ul>
</aside>
</div> <!-- /wrapper-content-supplementary -->
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
<div class="facephoto view"> <div class="facephoto view">
<h2 class="title photo_verification"><%- gettext( "Take Your Photo" ) %></h2> <h3 class="title"><%- gettext( "Take Your Photo" ) %></h2>
<div class="instruction"> <div class="instruction">
<p><%= _.sprintf( gettext( "When your face is in position, use the camera button %(icon)s below to take your photo." ), { icon: '<span class="example">(<i class="icon fa fa-camera" aria-hidden="true"></i><span class="sr">icon</span>)</span>' } ) %></p> <p><%= _.sprintf( gettext( "When your face is in position, use the camera button %(icon)s below to take your photo." ), { icon: '<span class="example">(<i class="icon fa fa-camera" aria-hidden="true"></i><span class="sr">icon</span>)</span>' } ) %></p>
</div> </div>
......
<%! <%!
import json
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from verify_student.views import PayAndVerifyView
%> %>
<%namespace name='static' file='../static_content.html'/> <%namespace name='static' file='../static_content.html'/>
...@@ -25,7 +23,7 @@ from verify_student.views import PayAndVerifyView ...@@ -25,7 +23,7 @@ from verify_student.views import PayAndVerifyView
<script src="${static.url('js/vendor/underscore.string.min.js')}"></script> <script src="${static.url('js/vendor/underscore.string.min.js')}"></script>
<script src="${static.url('js/vendor/backbone-min.js')}"></script> <script src="${static.url('js/vendor/backbone-min.js')}"></script>
<script src="${static.url('js/src/tooltip_manager.js')}"></script> <script src="${static.url('js/src/tooltip_manager.js')}"></script>
<%static:js group='reverify'/> <%static:js group='incourse_reverify'/>
</%block> </%block>
<%block name="content"> <%block name="content">
......
...@@ -24,7 +24,8 @@ from verify_student.views import PayAndVerifyView ...@@ -24,7 +24,8 @@ from verify_student.views import PayAndVerifyView
<% <%
template_names = ( template_names = (
["webcam_photo", "image_input", "error"] + ["webcam_photo", "image_input", "error"] +
[step['templateName'] for step in display_steps] ["intro_step", "make_payment_step", "payment_confirmation_step"] +
["face_photo_step", "id_photo_step", "review_photos_step", "enrollment_confirmation_step"]
) )
%> %>
% for template_name in template_names: % for template_name in template_names:
......
<%! from django.utils.translation import ugettext as _ %>
<h2 class="title">${_("You need to re-verify to continue")}</h2>
<p class='activation-message'>
${_("To continue in the ID Verified track in {course}, you need to re-verify your identity by {date}. Go to URL.").format(email)}
</p>
<%inherit file="../main.html" />
<%namespace name='static' file='/static_content.html'/>
<%!
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
%>
<%block name="bodyclass">register verification-process is-not-verified step-confirmation</%block>
<%block name="pagetitle">${_("Re-Verification Submission Confirmation")}</%block>
<%block name="js_extra">
<script src="${static.url('js/vendor/responsive-carousel/responsive-carousel.js')}"></script>
<script src="${static.url('js/vendor/responsive-carousel/responsive-carousel.keybd.js')}"></script>
</%block>
<%block name="content">
<div class="container">
<section class="wrapper">
<div class="wrapper-progress">
<section class="progress">
<h3 class="sr title">${_("Your Progress")}</h3>
<ol class="progress-steps">
<li class="progress-step is-completed" id="progress-step1">
<span class="wrapper-step-number"><span class="step-number">1</span></span>
<span class="step-name">${_("Re-Take Photo")}</span>
</li>
<li class="progress-step is-completed" id="progress-step2">
<span class="wrapper-step-number"><span class="step-number">2</span></span>
<span class="step-name">${_("Re-Take ID Photo")}</span>
</li>
<li class="progress-step is-completed" id="progress-step3">
<span class="wrapper-step-number"><span class="step-number">3</span></span>
<span class="step-name">${_("Review")}</span>
</li>
<li class="progress-step is-current progress-step-icon" id="progress-step4">
<span class="wrapper-step-number"><span class="step-number">
<i class="icon fa fa-check-square-o"></i>
</span></span>
<span class="step-name"><span class="sr">${_("Current Step: ")}</span>${_("Confirmation")}</span>
</li>
</ol>
<span class="progress-sts">
<span class="progress-sts-value"></span>
</span>
</section>
</div>
<div class="wrapper-content-main">
<article class="content-main">
<section class="content-confirmation">
<div class="wrapper-view">
<div class="view">
<h3 class="title">${_("Your Credentials Have Been Updated")}</h3>
<div class="instruction">
<p>${_("We've captured your re-submitted information and will review it to verify your identity shortly. You should receive an update to your veriication status within 1-2 days. In the meantime, you still have access to all of your course content.")}</p>
</div>
<ol class="list-nav">
<li class="nav-item">
<a class="action action-primary" href="${reverse('dashboard')}">${_("Return to Your Dashboard")}</a>
</li>
</ol>
</div> <!-- /view -->
</div> <!-- /wrapper-view -->
</section>
</article>
</div> <!-- /wrapper-content-main -->
<%include file="_reverification_support.html" />
</section>
</div>
</%block>
<%inherit file="../main.html" />
<%namespace name='static' file='/static_content.html'/>
<%!
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
%>
<%block name="bodyclass">register verification-process is-not-verified step-confirmation</%block>
<%block name="pagetitle">${_("Re-Verification Failed")}</%block>
<%block name="js_extra">
<script src="${static.url('js/vendor/responsive-carousel/responsive-carousel.js')}"></script>
<script src="${static.url('js/vendor/responsive-carousel/responsive-carousel.keybd.js')}"></script>
</%block>
<%block name="content">
<div class="container">
<section class="wrapper">
<div class="wrapper-content-main">
<article class="content-main">
<section class="content-confirmation">
<div class="wrapper-view">
<div class="view">
<h3 class="title">${_("Re-Verification Failed")}</h3>
<div class="instruction">
<p>${_("Your re-verification was submitted after the re-verification deadline, and you can no longer be re-verified.")}</p>
<p>${_("Please contact support if you believe this message to be in error.")}</p>
</div>
<ol class="list-nav">
<li class="nav-item">
<a class="action action-primary" href="${reverse('dashboard')}">${_("Return to Your Dashboard")}</a>
</li>
</ol>
</div> <!-- /view -->
</div> <!-- /wrapper-view -->
</section>
</article>
</div> <!-- /wrapper-content-main -->
<%include file="_reverification_support.html" />
</section>
</div>
</%block>
<%!
from django.utils.translation import ugettext as _
%>
<%namespace name='static' file='../static_content.html'/>
<%inherit file="../main.html" />
<%block name="bodyclass">register verification-process step-requirements</%block>
<%block name="pagetitle">${_("Re-Verification")}</%block>
<%block name="header_extras">
% for template_name in ["webcam_photo", "image_input", "error", "face_photo_step", "id_photo_step", "review_photos_step", "reverify_success_step"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="verify_student/${template_name}.underscore" />
</script>
% endfor
</%block>
<%block name="js_extra">
<%static:js group='rwd_header'/>
<script src="${static.url('js/vendor/underscore-min.js')}"></script>
<script src="${static.url('js/vendor/underscore.string.min.js')}"></script>
<script src="${static.url('js/vendor/backbone-min.js')}"></script>
<script src="${static.url('js/src/tooltip_manager.js')}"></script>
<%static:js group='reverify'/>
</%block>
<%block name="content">
## Top-level wrapper for errors
## JavaScript views may append to this wrapper
<div id="error-container" style="display: none;"></div>
<div class="container">
<section class="wrapper carousel">
## Container for the reverification view.
## The Backbone view renders itself into this <div>.
## The server can pass information to the Backbone view
## by including the information as "data-*" attributes
## of this </div>.
<div id="reverify-container" class="reverify"
data-full-name='${user_full_name}'
data-platform-name='${platform_name}'
data-capture-sound='${capture_sound}'
></div>
</section>
</div>
</%block>
<%!
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
%>
<%inherit file="../main.html" />
<%block name="pagetitle">${_("Identity Verification")}</%block>
<%block name="content">
<div class="reverify-blocked">
<h1 class="title">${_("Identity Verification")}</h1>
<div class="instructions">
<p>
% if status in ["pending", "approved"]:
${_("You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days).")}
% else:
${_("You cannot verify your identity at this time.")}
% endif
</p>
</div>
<div class="wrapper-actions">
<a class="action action-primary" href="${ reverse('dashboard') }">${_("Return to Your Dashboard")}</a>
</div>
</div>
</%block>
<div id="wrapper-reverify-success" tab-index="0" class="wrapper-view reverify-success-step">
<h1 class="title"><%- gettext( "Identity Verification In Progress" ) %></h3>
<div class="instruction">
<p><%- gettext( "We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days). In the meantime, you can still access all available course content." ) %></p>
</div>
<div class="wrapper-actions">
<a class="action action-primary" href="/dashboard"><%- gettext( "Return to Your Dashboard" ) %></a>
</div>
</div>
...@@ -29,7 +29,6 @@ urlpatterns = ( ...@@ -29,7 +29,6 @@ urlpatterns = (
url(r'^admin_dashboard$', 'dashboard.views.dashboard'), url(r'^admin_dashboard$', 'dashboard.views.dashboard'),
url(r'^email_confirm/(?P<key>[^/]*)$', 'student.views.confirm_email_change'), url(r'^email_confirm/(?P<key>[^/]*)$', 'student.views.confirm_email_change'),
url(r'^change_name$', 'student.views.change_name_request', name="change_name"),
url(r'^event$', 'track.views.user_track'), url(r'^event$', 'track.views.user_track'),
url(r'^performance$', 'performance.views.performance_log'), url(r'^performance$', 'performance.views.performance_log'),
url(r'^segmentio/event$', 'track.views.segmentio.segmentio_event'), url(r'^segmentio/event$', 'track.views.segmentio.segmentio_event'),
......
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