Commit 84301e76 by Will Daly

Merge pull request #6070 from edx/will/verified-messaging-per-course

Student verification status
parents 500a33db 962dc221
......@@ -5,7 +5,7 @@ import pytz
from datetime import datetime
from django.db import models
from collections import namedtuple
from collections import namedtuple, defaultdict
from django.utils.translation import ugettext_lazy as _
from django.db.models import Q
......@@ -67,6 +67,70 @@ class CourseMode(models.Model):
unique_together = ('course_id', 'mode_slug', 'currency')
@classmethod
def all_modes_for_courses(cls, course_id_list):
"""Find all modes for a list of course IDs, including expired modes.
Courses that do not have a course mode will be given a default mode.
Arguments:
course_id_list (list): List of `CourseKey`s
Returns:
dict mapping `CourseKey` to lists of `Mode`
"""
modes_by_course = defaultdict(list)
for mode in cls.objects.filter(course_id__in=course_id_list):
modes_by_course[mode.course_id].append(
Mode(
mode.mode_slug,
mode.mode_display_name,
mode.min_price,
mode.suggested_prices,
mode.currency,
mode.expiration_datetime,
mode.description
)
)
# Assign default modes if nothing available in the database
missing_courses = set(course_id_list) - set(modes_by_course.keys())
for course_id in missing_courses:
modes_by_course[course_id] = [cls.DEFAULT_MODE]
return modes_by_course
@classmethod
def all_and_unexpired_modes_for_courses(cls, course_id_list):
"""Retrieve course modes for a list of courses.
To reduce the number of database queries, this function
loads *all* course modes, then creates a second list
of unexpired course modes.
Arguments:
course_id_list (list of `CourseKey`): List of courses for which
to retrieve course modes.
Returns:
Tuple of `(all_course_modes, unexpired_course_modes)`, where
the first is a list of *all* `Mode`s (including expired ones),
and the second is a list of only unexpired `Mode`s.
"""
now = datetime.now(pytz.UTC)
all_modes = cls.all_modes_for_courses(course_id_list)
unexpired_modes = {
course_id: [
mode for mode in modes
if mode.expiration_datetime is None or mode.expiration_datetime >= now
]
for course_id, modes in all_modes.iteritems()
}
return (all_modes, unexpired_modes)
@classmethod
def modes_for_course(cls, course_id):
"""
Returns a list of the non-expired modes for a given course id
......@@ -91,23 +155,48 @@ class CourseMode(models.Model):
return modes
@classmethod
def modes_for_course_dict(cls, course_id):
"""
Returns the non-expired modes for a particular course as a
dictionary with the mode slug as the key
def modes_for_course_dict(cls, course_id, modes=None):
"""Returns the non-expired modes for a particular course.
Arguments:
course_id (CourseKey): Search for course modes for this course.
Keyword Arguments:
modes (list of `Mode`): If provided, search through this list
of course modes. This can be used to avoid an additional
database query if you have already loaded the modes list.
Returns:
dict: Keys are mode slugs, values are lists of `Mode` namedtuples.
"""
return {mode.slug: mode for mode in cls.modes_for_course(course_id)}
if modes is None:
modes = cls.modes_for_course(course_id)
return {mode.slug: mode for mode in modes}
@classmethod
def mode_for_course(cls, course_id, mode_slug):
"""
Returns the mode for the course corresponding to mode_slug.
def mode_for_course(cls, course_id, mode_slug, modes=None):
"""Returns the mode for the course corresponding to mode_slug.
Returns only non-expired modes.
If this particular mode is not set for the course, returns None
Arguments:
course_id (CourseKey): Search for course modes for this course.
mode_slug (str): Search for modes with this slug.
Keyword Arguments:
modes (list of `Mode`): If provided, search through this list
of course modes. This can be used to avoid an additional
database query if you have already loaded the modes list.
Returns:
Mode
"""
modes = cls.modes_for_course(course_id)
if modes is None:
modes = cls.modes_for_course(course_id)
matched = [m for m in modes if m.slug == mode_slug]
if matched:
......@@ -116,15 +205,28 @@ 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,
def verified_mode_for_course(cls, course_id, modes=None):
"""Find a verified mode for a particular course.
Since we have multiple 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.
Arguments:
course_id (CourseKey): Search for course modes for this course.
Keyword Arguments:
modes (list of `Mode`): If provided, search through this list
of course modes. This can be used to avoid an additional
database query if you have already loaded the modes list.
Returns:
Mode or None
"""
modes_dict = cls.modes_for_course_dict(course_id)
modes_dict = cls.modes_for_course_dict(course_id, modes=modes)
verified_mode = modes_dict.get('verified', None)
professional_mode = modes_dict.get('professional', None)
# we prefer professional over verify
......
......@@ -10,6 +10,7 @@ import pytz
import ddt
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys.edx.locator import CourseLocator
from django.test import TestCase
from course_modes.models import CourseMode, Mode
......@@ -163,3 +164,45 @@ class CourseModeModelTest(TestCase):
# Verify that we can or cannot auto enroll
self.assertEqual(CourseMode.can_auto_enroll(self.course_key), can_auto_enroll)
def test_all_modes_for_courses(self):
now = datetime.now(pytz.UTC)
future = now + timedelta(days=1)
past = now - timedelta(days=1)
# Unexpired, no expiration date
CourseMode.objects.create(
course_id=self.course_key,
mode_display_name="Honor No Expiration",
mode_slug="honor_no_expiration",
expiration_datetime=None
)
# Unexpired, expiration date in future
CourseMode.objects.create(
course_id=self.course_key,
mode_display_name="Honor Not Expired",
mode_slug="honor_not_expired",
expiration_datetime=future
)
# Expired
CourseMode.objects.create(
course_id=self.course_key,
mode_display_name="Verified Expired",
mode_slug="verified_expired",
expiration_datetime=past
)
# We should get all of these back when querying for *all* course modes,
# including ones that have expired.
other_course_key = CourseLocator(org="not", course="a", run="course")
all_modes = CourseMode.all_modes_for_courses([self.course_key, other_course_key])
self.assertEqual(len(all_modes[self.course_key]), 3)
self.assertEqual(all_modes[self.course_key][0].name, "Honor No Expiration")
self.assertEqual(all_modes[self.course_key][1].name, "Honor Not Expired")
self.assertEqual(all_modes[self.course_key][2].name, "Verified Expired")
# 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)
"""Helpers for the student app. """
import time
from datetime import datetime
from pytz import UTC
from django.utils.http import cookie_date
from django.conf import settings
from django.core.urlresolvers import reverse
......@@ -9,6 +11,7 @@ from third_party_auth import ( # pylint: disable=unused-import
pipeline, provider,
is_enabled as third_party_auth_enabled
)
from verify_student.models import SoftwareSecurePhotoVerification # pylint: disable=F0401
def auth_pipeline_urls(auth_entry, redirect_url=None, course_id=None):
......@@ -111,3 +114,118 @@ def set_logged_in_cookie(request, response):
def is_logged_in_cookie_set(request):
"""Check whether the request has the logged in cookie set. """
return settings.EDXMKTG_COOKIE_NAME in request.COOKIES
# Enumeration of per-course verification statuses
# we display on the student dashboard.
VERIFY_STATUS_NEED_TO_VERIFY = "verify_need_to_verify"
VERIFY_STATUS_SUBMITTED = "verify_submitted"
VERIFY_STATUS_APPROVED = "verify_approved"
VERIFY_STATUS_MISSED_DEADLINE = "verify_missed_deadline"
def check_verify_status_by_course(user, course_enrollment_pairs, all_course_modes):
"""Determine the per-course verification statuses for a given user.
The possible statuses are:
* VERIFY_STATUS_NEED_TO_VERIFY: The student has not yet submitted photos for verification.
* VERIFY_STATUS_SUBMITTED: The student has submitted photos for verification,
but has have not yet been approved.
* VERIFY_STATUS_APPROVED: The student has been successfully verified.
* VERIFY_STATUS_MISSED_DEADLINE: The student did not submit photos within the course's deadline.
It is is also possible that a course does NOT have a verification status if:
* The user is not enrolled in a verified mode, meaning that the user didn't pay.
* The course does not offer a verified mode.
* The user submitted photos but an error occurred while verifying them.
* The user submitted photos but the verification was denied.
In the last two cases, we rely on messages in the sidebar rather than displaying
messages for each course.
Arguments:
user (User): The currently logged-in user.
course_enrollment_pairs (list): The courses the user is enrolled in.
The list should contain tuples of `(Course, CourseEnrollment)`.
all_course_modes (list): List of all course modes for the student's enrolled courses,
including modes that have expired.
Returns:
dict: Mapping of course keys verification status dictionaries.
If no verification status is applicable to a course, it will not
be included in the dictionary.
The dictionaries have these keys:
* status (str): One of the enumerated status codes.
* days_until_deadline (int): Number of days until the verification deadline.
* verification_good_until (str): Date string for the verification expiration date.
"""
status_by_course = {}
# Retrieve all verifications for the user, sorted in descending
# order by submission datetime
verifications = SoftwareSecurePhotoVerification.objects.filter(user=user)
for course, enrollment in course_enrollment_pairs:
# Get the verified mode (if any) for this course
# We pass in the course modes we have already loaded to avoid
# another database hit, as well as to ensure that expired
# course modes are included in the search.
verified_mode = CourseMode.verified_mode_for_course(
course.id,
modes=all_course_modes[course.id]
)
# If no verified mode has ever been offered, or the user hasn't enrolled
# as verified, then the course won't display state related to its
# verification status.
if verified_mode is not None and enrollment.mode in CourseMode.VERIFIED_MODES:
deadline = verified_mode.expiration_datetime
relevant_verification = SoftwareSecurePhotoVerification.verification_for_datetime(deadline, verifications)
# By default, don't show any status related to verification
status = None
# Check whether the user was approved or is awaiting approval
if relevant_verification is not None:
if relevant_verification.status == "approved":
status = VERIFY_STATUS_APPROVED
elif relevant_verification.status == "submitted":
status = VERIFY_STATUS_SUBMITTED
# If the user didn't submit at all, then tell them they need to verify
# If the deadline has already passed, then tell them they missed it.
# If they submitted but something went wrong (error or denied),
# then don't show any messaging next to the course, since we already
# show messages related to this on the left sidebar.
submitted = (
relevant_verification is not None and
relevant_verification.status not in ["created", "ready"]
)
if status is None and not submitted:
if deadline is None or deadline > datetime.now(UTC):
status = VERIFY_STATUS_NEED_TO_VERIFY
else:
status = VERIFY_STATUS_MISSED_DEADLINE
# Set the status for the course only if we're displaying some kind of message
# Otherwise, leave the course out of the dictionary.
if status is not None:
days_until_deadline = None
verification_good_until = None
now = datetime.now(UTC)
if deadline is not None and deadline > now:
days_until_deadline = (deadline - now).days
if relevant_verification is not None:
verification_good_until = relevant_verification.expiration_datetime.strftime("%m/%d/%Y")
status_by_course[course.id] = {
'status': status,
'days_until_deadline': days_until_deadline,
'verification_good_until': verification_good_until
}
return status_by_course
......@@ -99,7 +99,10 @@ from util.password_policy_validators import (
import third_party_auth
from third_party_auth import pipeline, provider
from student.helpers import auth_pipeline_urls, set_logged_in_cookie
from student.helpers import (
auth_pipeline_urls, set_logged_in_cookie,
check_verify_status_by_course
)
from xmodule.error_module import ErrorDescriptor
from shoppingcart.models import CourseRegistrationCode
from user_api.api import profile as profile_api
......@@ -496,9 +499,14 @@ def dashboard(request):
course_enrollment_pairs.sort(key=lambda x: x[1].created, reverse=True)
# Retrieve the course modes for each course
enrolled_course_ids = [course.id for course, __ in course_enrollment_pairs]
all_course_modes, unexpired_course_modes = CourseMode.all_and_unexpired_modes_for_courses(enrolled_course_ids)
course_modes_by_course = {
course.id: CourseMode.modes_for_course_dict(course.id)
for course, __ in course_enrollment_pairs
course_id: {
mode.slug: mode
for mode in modes
}
for course_id, modes in unexpired_course_modes.iteritems()
}
# Check to see if the student has recently enrolled in a course.
......@@ -538,6 +546,29 @@ def dashboard(request):
for course, enrollment in course_enrollment_pairs
}
# Determine the per-course verification status
# This is a dictionary in which the keys are course locators
# and the values are one of:
#
# VERIFY_STATUS_NEED_TO_VERIFY
# VERIFY_STATUS_SUBMITTED
# VERIFY_STATUS_APPROVED
# VERIFY_STATUS_MISSED_DEADLINE
#
# Each of which correspond to a particular message to display
# next to the course on the dashboard.
#
# If a course is not included in this dictionary,
# there is no verification messaging to display.
if settings.FEATURES.get("SEPARATE_VERIFICATION_FROM_PAYMENT"):
verify_status_by_course = check_verify_status_by_course(
user,
course_enrollment_pairs,
all_course_modes
)
else:
verify_status_by_course = {}
cert_statuses = {
course.id: cert_info(request.user, course)
for course, _enrollment in course_enrollment_pairs
......@@ -616,6 +647,7 @@ def dashboard(request):
'show_email_settings_for': show_email_settings_for,
'reverifications': reverifications,
'verification_status': verification_status,
'verification_status_by_course': verify_status_by_course,
'verification_msg': verification_msg,
'show_refund_option_for': show_refund_option_for,
'block_courses': block_courses,
......
......@@ -188,11 +188,8 @@ class PhotoVerification(StatusModel):
Returns the earliest allowed date given the settings
"""
DAYS_GOOD_FOR = settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]
allowed_date = (
datetime.now(pytz.UTC) - timedelta(days=DAYS_GOOD_FOR)
)
return allowed_date
days_good_for = settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]
return datetime.now(pytz.UTC) - timedelta(days=days_good_for)
@classmethod
def user_is_verified(cls, user, earliest_allowed_date=None, window=None):
......@@ -310,6 +307,66 @@ class PhotoVerification(StatusModel):
return (status, error_msg)
@classmethod
def verification_for_datetime(cls, deadline, candidates):
"""Find a verification in a set that applied during a particular datetime.
A verification is considered "active" during a datetime if:
1) The verification was created before the datetime, and
2) The verification is set to expire after the datetime.
Note that verification status is *not* considered here,
just the start/expire dates.
If multiple verifications were active at the deadline,
returns the most recently created one.
Arguments:
deadline (datetime): The datetime at which the verification applied.
If `None`, then return the most recently created candidate.
candidates (list of `PhotoVerification`s): Potential verifications to search through.
Returns:
PhotoVerification: A photo verification that was active at the deadline.
If no verification was active, return None.
"""
if len(candidates) == 0:
return None
# If there's no deadline, then return the most recently created verification
if deadline is None:
return candidates[0]
# Otherwise, look for a verification that was in effect at the deadline,
# preferring recent verifications.
# If no such verification is found, implicitly return `None`
for verification in candidates:
if verification.active_at_datetime(deadline):
return verification
@property
def expiration_datetime(self):
"""Datetime that the verification will expire. """
days_good_for = settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]
return self.created_at + timedelta(days=days_good_for)
def active_at_datetime(self, deadline):
"""Check whether the verification was active at a particular datetime.
Arguments:
deadline (datetime): The date at which the verification was active
(created before and expired after).
Returns:
bool
"""
return (
self.created_at < deadline and
self.expiration_datetime > deadline
)
def parsed_error_msg(self):
"""
Sometimes, the error message we've received needs to be parsed into
......
......@@ -419,6 +419,83 @@ class TestPhotoVerification(TestCase):
parsed_error_msg = attempt.parsed_error_msg()
self.assertEquals(parsed_error_msg, "There was an error verifying your ID photos.")
def test_active_at_datetime(self):
user = UserFactory.create()
attempt = SoftwareSecurePhotoVerification.objects.create(user=user)
# Not active before the created date
before = attempt.created_at - timedelta(seconds=1)
self.assertFalse(attempt.active_at_datetime(before))
# Active immediately after created date
after_created = attempt.created_at + timedelta(seconds=1)
self.assertTrue(attempt.active_at_datetime(after_created))
# Active immediately before expiration date
expiration = attempt.created_at + timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
before_expiration = expiration - timedelta(seconds=1)
self.assertTrue(attempt.active_at_datetime(before_expiration))
# Not active after the expiration date
after = expiration + timedelta(seconds=1)
self.assertFalse(attempt.active_at_datetime(after))
def test_verification_for_datetime(self):
user = UserFactory.create()
now = datetime.now(pytz.UTC)
# No attempts in the query set, so should return None
query = SoftwareSecurePhotoVerification.objects.filter(user=user)
result = SoftwareSecurePhotoVerification.verification_for_datetime(now, query)
self.assertIs(result, None)
# Should also return None if no deadline specified
query = SoftwareSecurePhotoVerification.objects.filter(user=user)
result = SoftwareSecurePhotoVerification.verification_for_datetime(None, query)
self.assertIs(result, None)
# Make an attempt
attempt = SoftwareSecurePhotoVerification.objects.create(user=user)
# Before the created date, should get no results
before = attempt.created_at - timedelta(seconds=1)
query = SoftwareSecurePhotoVerification.objects.filter(user=user)
result = SoftwareSecurePhotoVerification.verification_for_datetime(before, query)
self.assertIs(result, None)
# Immediately after the created date, should get the attempt
after_created = attempt.created_at + timedelta(seconds=1)
query = SoftwareSecurePhotoVerification.objects.filter(user=user)
result = SoftwareSecurePhotoVerification.verification_for_datetime(after_created, query)
self.assertEqual(result, attempt)
# If no deadline specified, should return first available
query = SoftwareSecurePhotoVerification.objects.filter(user=user)
result = SoftwareSecurePhotoVerification.verification_for_datetime(None, query)
self.assertEqual(result, attempt)
# Immediately before the expiration date, should get the attempt
expiration = attempt.created_at + timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
before_expiration = expiration - timedelta(seconds=1)
query = SoftwareSecurePhotoVerification.objects.filter(user=user)
result = SoftwareSecurePhotoVerification.verification_for_datetime(before_expiration, query)
self.assertEqual(result, attempt)
# Immediately after the expiration date, should not get the attempt
after = expiration + timedelta(seconds=1)
query = SoftwareSecurePhotoVerification.objects.filter(user=user)
result = SoftwareSecurePhotoVerification.verification_for_datetime(after, query)
self.assertIs(result, None)
# Create a second attempt in the same window
second_attempt = SoftwareSecurePhotoVerification.objects.create(user=user)
# Now we should get the newer attempt
deadline = second_attempt.created_at + timedelta(days=1)
query = SoftwareSecurePhotoVerification.objects.filter(user=user)
result = SoftwareSecurePhotoVerification.verification_for_datetime(deadline, query)
self.assertEqual(result, second_attempt)
@override_settings(MODULESTORE=TEST_DATA_MOCK_MODULESTORE)
@patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS)
......
......@@ -296,6 +296,9 @@ FEATURES = {
# Enable display of enrollment counts in instructor and legacy analytics dashboard
'DISPLAY_ANALYTICS_ENROLLMENTS': True,
# Separate the verification flow from the payment flow
'SEPARATE_VERIFICATION_FROM_PAYMENT': False,
}
# Ignore static asset files on import which match this pattern
......
......@@ -488,6 +488,12 @@
@extend %text-sr;
}
.deco-graphic {
position: absolute;
top: -5px;
right: -8px;
}
.sts-enrollment-value {
@extend %ui-depth1;
@extend %copy-badge;
......@@ -893,6 +899,30 @@
}
}
}
.verification-reminder {
width: flex-grid(8, 12);
position: relative;
float: left;
}
.verification-cta {
width: flex-grid(4, 12);
position: relative;
float: left;
.cta {
@include button(simple, $green-d1);
@include box-sizing(border-box);
@include float(right);
border-radius: 3px;
display: block;
font: normal 15px/1.6rem $sans-serif;
letter-spacing: 0;
padding: 6px 32px 7px;
text-align: center;
}
}
}
a.unenroll {
......
......@@ -180,7 +180,8 @@
<% show_refund_option = (course.id in show_refund_option_for) %>
<% is_paid_course = (course.id in enrolled_courses_either_paid) %>
<% is_course_blocked = (course.id in block_courses) %>
<%include file='dashboard/_dashboard_course_listing.html' args="course=course, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option = show_refund_option, is_paid_course = is_paid_course, is_course_blocked = is_course_blocked" />
<% course_verification_status = verification_status_by_course.get(course.id, {}) %>
<%include file='dashboard/_dashboard_course_listing.html' args="course=course, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option = show_refund_option, is_paid_course = is_paid_course, is_course_blocked = is_course_blocked, verification_status=course_verification_status" />
% endfor
</ul>
......
<%page args="course, enrollment, show_courseware_link, cert_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course, is_course_blocked" />
<%page args="course, enrollment, show_courseware_link, cert_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course, is_course_blocked, verification_status" />
<%! from django.utils.translation import ugettext as _ %>
<%!
from django.core.urlresolvers import reverse
from courseware.courses import course_image_url, get_course_about_section
from student.helpers import (
VERIFY_STATUS_NEED_TO_VERIFY,
VERIFY_STATUS_SUBMITTED,
VERIFY_STATUS_APPROVED,
VERIFY_STATUS_MISSED_DEADLINE
)
%>
<%
......@@ -45,28 +51,49 @@
</div>
% endif
% if settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES'):
% if enrollment.mode == "verified":
% if enrollment.mode == "verified":
% if settings.FEATURES.get('SEPARATE_VERIFICATION_FROM_PAYMENT'):
% 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>
<img class="deco-graphic" src="${static.url('images/verified-ribbon.png')}" alt="ID Verified Pending Ribbon/Badge" />
<div class="sts-enrollment-value">${_("Verified Pending")}</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
% else:
<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/vcert-ribbon-s.png')}" alt="ID Verified Ribbon/Badge" />
<span class="sts-enrollment-value">${_("Verified")}</span>
</span>
% elif enrollment.mode == "honor":
<span class="sts-enrollment" title="${_("You're enrolled as an honor code student")}">
<span class="label">${_("Enrolled as: ")}</span>
<span class="sts-enrollment-value">${_("Honor Code")}</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>
% elif enrollment.mode == "audit":
<span class="sts-enrollment" title="${_("You're auditing this course")}">
<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")}">
% endif
% elif enrollment.mode == "honor":
<span class="sts-enrollment" title="${_("You're enrolled as an honor code student")}">
<span class="label">${_("Enrolled as: ")}</span>
<span class="sts-enrollment-value">${_("Professional Ed")}</span>
<div class="sts-enrollment-value">${_("Honor Code")}</div>
</span>
% endif
% elif enrollment.mode == "audit":
<span class="sts-enrollment" title="${_("You're auditing this course")}">
<span class="label">${_("Enrolled as: ")}</span>
<div class="sts-enrollment-value">${_("Auditing")}</div>
</span>
% elif enrollment.mode == "professional":
<span class="sts-enrollment" title="${_("You're enrolled as a professional education student")}">
<span class="label">${_("Enrolled as: ")}</span>
<div class="sts-enrollment-value">${_("Professional Ed")}</div>
</span>
% endif
% endif
<section class="info">
......@@ -100,6 +127,35 @@
<%include file='_dashboard_certificate_information.html' args='cert_status=cert_status,course=course, enrollment=enrollment'/>
% endif
% if settings.FEATURES.get('SEPARATE_VERIFICATION_FROM_PAYMENT'):
% if verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_APPROVED] and not is_course_blocked:
<div class="message message-status is-shown">
% if verification_status['status'] == VERIFY_STATUS_NEED_TO_VERIFY:
<div class="verification-reminder">
% if verification_status['days_until_deadline'] is not None:
<h4 class="message-title">${_('Verification not yet complete.')}</h4>
<p class="message-copy">${_('You only have {days} days left to verify for this course.').format(days=verification_status['days_until_deadline'])}</p>
% else:
<h4 class="message-title">${_('Almost there!')}</h4>
<p class="message-copy">${_('You still need to verify for this course.')}</p>
% endif
</div>
<div class="verification-cta">
<a href="#" class="cta">${_('Verify Now')}</a>
</div>
% elif verification_status['status'] == VERIFY_STATUS_SUBMITTED:
<h4 class="message-title">${_('You have already verified your ID!')}</h4>
<p class="message-copy">${_('Thanks for your patience as we process your request.')}</p>
% elif verification_status['status'] == VERIFY_STATUS_APPROVED:
<h4 class="message-title">${_('You have already verified your ID!')}</h4>
% if verification_status['verification_good_until'] is not None:
<p class="message-copy">${_('Your verification status is good until {date}.').format(date=verification_status['verification_good_until'])}
% endif
% endif
</div>
% endif
% endif
% if course_mode_info['show_upsell'] and not is_course_blocked:
<div class="message message-upsell has-actions is-expandable is-shown">
......
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