Commit 464dfcfa by Will Daly

Show student verification status on the dashboard.

parent 7b3602e3
...@@ -5,7 +5,7 @@ import pytz ...@@ -5,7 +5,7 @@ import pytz
from datetime import datetime from datetime import datetime
from django.db import models from django.db import models
from collections import namedtuple from collections import namedtuple, defaultdict
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db.models import Q from django.db.models import Q
...@@ -67,6 +67,70 @@ class CourseMode(models.Model): ...@@ -67,6 +67,70 @@ class CourseMode(models.Model):
unique_together = ('course_id', 'mode_slug', 'currency') unique_together = ('course_id', 'mode_slug', 'currency')
@classmethod @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): def modes_for_course(cls, course_id):
""" """
Returns a list of the non-expired modes for a given course id Returns a list of the non-expired modes for a given course id
...@@ -91,23 +155,48 @@ class CourseMode(models.Model): ...@@ -91,23 +155,48 @@ class CourseMode(models.Model):
return modes return modes
@classmethod @classmethod
def modes_for_course_dict(cls, course_id): def modes_for_course_dict(cls, course_id, modes=None):
""" """Returns the non-expired modes for a particular course.
Returns the non-expired modes for a particular course as a
dictionary with the mode slug as the key 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 @classmethod
def mode_for_course(cls, course_id, mode_slug): def mode_for_course(cls, course_id, mode_slug, modes=None):
""" """Returns the mode for the course corresponding to mode_slug.
Returns the mode for the course corresponding to mode_slug.
Returns only non-expired modes. Returns only non-expired modes.
If this particular mode is not set for the course, returns None 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] matched = [m for m in modes if m.slug == mode_slug]
if matched: if matched:
...@@ -116,15 +205,28 @@ class CourseMode(models.Model): ...@@ -116,15 +205,28 @@ class CourseMode(models.Model):
return None return None
@classmethod @classmethod
def verified_mode_for_course(cls, course_id): def verified_mode_for_course(cls, course_id, modes=None):
""" """Find a verified mode for a particular course.
Since we have two separate modes that can go through the verify flow,
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. 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 Currently, we prefer to return the professional mode over the verified one
if both exist for the given course. 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) verified_mode = modes_dict.get('verified', None)
professional_mode = modes_dict.get('professional', None) professional_mode = modes_dict.get('professional', None)
# we prefer professional over verify # we prefer professional over verify
......
...@@ -10,6 +10,7 @@ import pytz ...@@ -10,6 +10,7 @@ import pytz
import ddt import ddt
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys.edx.locator import CourseLocator
from django.test import TestCase from django.test import TestCase
from course_modes.models import CourseMode, Mode from course_modes.models import CourseMode, Mode
...@@ -163,3 +164,45 @@ class CourseModeModelTest(TestCase): ...@@ -163,3 +164,45 @@ class CourseModeModelTest(TestCase):
# Verify that we can or cannot auto enroll # Verify that we can or cannot auto enroll
self.assertEqual(CourseMode.can_auto_enroll(self.course_key), can_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. """ """Helpers for the student app. """
import time import time
from datetime import datetime
from pytz import UTC
from django.utils.http import cookie_date from django.utils.http import cookie_date
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -9,6 +11,7 @@ from third_party_auth import ( # pylint: disable=W0611 ...@@ -9,6 +11,7 @@ from third_party_auth import ( # pylint: disable=W0611
pipeline, provider, pipeline, provider,
is_enabled as third_party_auth_enabled 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): def auth_pipeline_urls(auth_entry, redirect_url=None, course_id=None):
...@@ -111,3 +114,118 @@ def set_logged_in_cookie(request, response): ...@@ -111,3 +114,118 @@ def set_logged_in_cookie(request, response):
def is_logged_in_cookie_set(request): def is_logged_in_cookie_set(request):
"""Check whether the request has the logged in cookie set. """ """Check whether the request has the logged in cookie set. """
return settings.EDXMKTG_COOKIE_NAME in request.COOKIES 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 ( ...@@ -99,7 +99,10 @@ from util.password_policy_validators import (
import third_party_auth import third_party_auth
from third_party_auth import pipeline, provider 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 xmodule.error_module import ErrorDescriptor
from shoppingcart.models import CourseRegistrationCode from shoppingcart.models import CourseRegistrationCode
...@@ -495,9 +498,14 @@ def dashboard(request): ...@@ -495,9 +498,14 @@ def dashboard(request):
course_enrollment_pairs.sort(key=lambda x: x[1].created, reverse=True) course_enrollment_pairs.sort(key=lambda x: x[1].created, reverse=True)
# Retrieve the course modes for each course # 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_modes_by_course = {
course.id: CourseMode.modes_for_course_dict(course.id) course_id: {
for course, __ in course_enrollment_pairs 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. # Check to see if the student has recently enrolled in a course.
...@@ -537,6 +545,29 @@ def dashboard(request): ...@@ -537,6 +545,29 @@ def dashboard(request):
for course, enrollment in course_enrollment_pairs 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 = { cert_statuses = {
course.id: cert_info(request.user, course) course.id: cert_info(request.user, course)
for course, _enrollment in course_enrollment_pairs for course, _enrollment in course_enrollment_pairs
...@@ -615,6 +646,7 @@ def dashboard(request): ...@@ -615,6 +646,7 @@ def dashboard(request):
'show_email_settings_for': show_email_settings_for, 'show_email_settings_for': show_email_settings_for,
'reverifications': reverifications, 'reverifications': reverifications,
'verification_status': verification_status, 'verification_status': verification_status,
'verification_status_by_course': verify_status_by_course,
'verification_msg': verification_msg, 'verification_msg': verification_msg,
'show_refund_option_for': show_refund_option_for, 'show_refund_option_for': show_refund_option_for,
'block_courses': block_courses, 'block_courses': block_courses,
......
...@@ -188,11 +188,8 @@ class PhotoVerification(StatusModel): ...@@ -188,11 +188,8 @@ class PhotoVerification(StatusModel):
Returns the earliest allowed date given the settings Returns the earliest allowed date given the settings
""" """
DAYS_GOOD_FOR = settings.VERIFY_STUDENT["DAYS_GOOD_FOR"] days_good_for = settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]
allowed_date = ( return datetime.now(pytz.UTC) - timedelta(days=days_good_for)
datetime.now(pytz.UTC) - timedelta(days=DAYS_GOOD_FOR)
)
return allowed_date
@classmethod @classmethod
def user_is_verified(cls, user, earliest_allowed_date=None, window=None): def user_is_verified(cls, user, earliest_allowed_date=None, window=None):
...@@ -310,6 +307,66 @@ class PhotoVerification(StatusModel): ...@@ -310,6 +307,66 @@ class PhotoVerification(StatusModel):
return (status, error_msg) 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): def parsed_error_msg(self):
""" """
Sometimes, the error message we've received needs to be parsed into Sometimes, the error message we've received needs to be parsed into
......
...@@ -417,6 +417,83 @@ class TestPhotoVerification(TestCase): ...@@ -417,6 +417,83 @@ class TestPhotoVerification(TestCase):
parsed_error_msg = attempt.parsed_error_msg() parsed_error_msg = attempt.parsed_error_msg()
self.assertEquals(parsed_error_msg, "There was an error verifying your ID photos.") 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_MONGO_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
@patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS) @patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS)
......
...@@ -293,6 +293,9 @@ FEATURES = { ...@@ -293,6 +293,9 @@ FEATURES = {
# Enable display of enrollment counts in instructor and legacy analytics dashboard # Enable display of enrollment counts in instructor and legacy analytics dashboard
'DISPLAY_ANALYTICS_ENROLLMENTS': True, '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 # Ignore static asset files on import which match this pattern
......
...@@ -180,7 +180,8 @@ ...@@ -180,7 +180,8 @@
<% show_refund_option = (course.id in show_refund_option_for) %> <% show_refund_option = (course.id in show_refund_option_for) %>
<% is_paid_course = (course.id in enrolled_courses_either_paid) %> <% is_paid_course = (course.id in enrolled_courses_either_paid) %>
<% is_course_blocked = (course.id in block_courses) %> <% 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 % endfor
</ul> </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.utils.translation import ugettext as _ %>
<%! <%!
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from courseware.courses import course_image_url, get_course_about_section 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 @@ ...@@ -45,28 +51,49 @@
</div> </div>
% endif % endif
% if settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES'): % 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/vcert-ribbon-s.png')}" alt="ID Verified Pending Ribbon/Badge" />
<span class="sts-enrollment-value">${_("Verified Pending")}</span>
</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/vcert-ribbon-s.png')}" alt="ID Verified Ribbon/Badge" />
<span class="sts-enrollment-value">${_("Verified")}</span>
</span>
% else:
<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>
</span>
% endif
% else:
<span class="sts-enrollment" title="${_("You're enrolled as a verified student")}"> <span class="sts-enrollment" title="${_("You're enrolled as a verified student")}">
<span class="label">${_("Enrolled as: ")}</span> <span class="label">${_("Enrolled as: ")}</span>
<img class="deco-graphic" src="${static.url('images/vcert-ribbon-s.png')}" alt="ID Verified Ribbon/Badge" /> <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 class="sts-enrollment-value">${_("Verified")}</span>
</span> </span>
% elif enrollment.mode == "honor": % endif
<span class="sts-enrollment" title="${_("You're enrolled as an honor code student")}"> % elif enrollment.mode == "honor":
<span class="label">${_("Enrolled as: ")}</span> <span class="sts-enrollment" title="${_("You're enrolled as an honor code student")}">
<span class="sts-enrollment-value">${_("Honor Code")}</span>
</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")}">
<span class="label">${_("Enrolled as: ")}</span> <span class="label">${_("Enrolled as: ")}</span>
<span class="sts-enrollment-value">${_("Professional Ed")}</span> <span class="sts-enrollment-value">${_("Honor Code")}</span>
</span> </span>
% endif % 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")}">
<span class="label">${_("Enrolled as: ")}</span>
<span class="sts-enrollment-value">${_("Professional Ed")}</span>
</span>
% endif
% endif % endif
<section class="info"> <section class="info">
...@@ -100,6 +127,32 @@ ...@@ -100,6 +127,32 @@
<%include file='_dashboard_certificate_information.html' args='cert_status=cert_status,course=course, enrollment=enrollment'/> <%include file='_dashboard_certificate_information.html' args='cert_status=cert_status,course=course, enrollment=enrollment'/>
% endif % 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:
% 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
## TODO: style this button
<p>${_('Verify Now')}</p>
% 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: % if course_mode_info['show_upsell'] and not is_course_blocked:
<div class="message message-upsell has-actions is-expandable is-shown"> <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