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
464dfcfa
Commit
464dfcfa
authored
Nov 25, 2014
by
Will Daly
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Show student verification status on the dashboard.
parent
7b3602e3
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
774 additions
and
39 deletions
+774
-39
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/templates/dashboard.html
+2
-1
lms/templates/dashboard/_dashboard_course_listing.html
+69
-16
No files found.
common/djangoapps/course_modes/models.py
View file @
464dfcfa
...
...
@@ -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 @
464dfcfa
...
...
@@ -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 @
464dfcfa
"""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=W0611
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 @
464dfcfa
This diff is collapsed.
Click to expand it.
common/djangoapps/student/views.py
View file @
464dfcfa
...
...
@@ -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
...
...
@@ -495,9 +498,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.
...
...
@@ -537,6 +545,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
...
...
@@ -615,6 +646,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 @
464dfcfa
...
...
@@ -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 @
464dfcfa
...
...
@@ -417,6 +417,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_MONGO_MODULESTORE
)
@patch.dict
(
settings
.
VERIFY_STUDENT
,
FAKE_SETTINGS
)
...
...
lms/envs/common.py
View file @
464dfcfa
...
...
@@ -293,6 +293,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/templates/dashboard.html
View file @
464dfcfa
...
...
@@ -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 @
464dfcfa
<
%
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/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=
"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>
</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>
<span
class=
"sts-enrollment-value"
>
${_("
Honor Code
")}
</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
<section
class=
"info"
>
...
...
@@ -100,6 +127,32 @@
<
%
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:
% 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:
<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