Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
84301e76
Commit
84301e76
authored
Dec 02, 2014
by
Will Daly
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #6070 from edx/will/verified-messaging-per-course
Student verification status
parents
500a33db
962dc221
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
809 additions
and
41 deletions
+809
-41
common/djangoapps/course_modes/models.py
+116
-14
common/djangoapps/course_modes/tests/test_models.py
+43
-0
common/djangoapps/student/helpers.py
+118
-0
common/djangoapps/student/tests/test_verification_status.py
+249
-0
common/djangoapps/student/views.py
+35
-3
lms/djangoapps/verify_student/models.py
+62
-5
lms/djangoapps/verify_student/tests/test_models.py
+77
-0
lms/envs/common.py
+3
-0
lms/static/sass/multicourse/_dashboard.scss
+30
-0
lms/templates/dashboard.html
+2
-1
lms/templates/dashboard/_dashboard_course_listing.html
+74
-18
No files found.
common/djangoapps/course_modes/models.py
View file @
84301e76
...
...
@@ -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
...
...
common/djangoapps/course_modes/tests/test_models.py
View file @
84301e76
...
...
@@ -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
)
common/djangoapps/student/helpers.py
View file @
84301e76
"""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
common/djangoapps/student/tests/test_verification_status.py
0 → 100644
View file @
84301e76
This diff is collapsed.
Click to expand it.
common/djangoapps/student/views.py
View file @
84301e76
...
...
@@ -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
,
...
...
lms/djangoapps/verify_student/models.py
View file @
84301e76
...
...
@@ -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
...
...
lms/djangoapps/verify_student/tests/test_models.py
View file @
84301e76
...
...
@@ -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
)
...
...
lms/envs/common.py
View file @
84301e76
...
...
@@ -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
...
...
lms/static/sass/multicourse/_dashboard.scss
View file @
84301e76
...
...
@@ -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
{
...
...
lms/templates/dashboard.html
View file @
84301e76
...
...
@@ -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>
...
...
lms/templates/dashboard/_dashboard_course_listing.html
View file @
84301e76
<
%
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"
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment