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
81e677b7
Commit
81e677b7
authored
Jul 21, 2015
by
Will Daly
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #8959 from edx/will/separate-verification-and-upgrade-deadline
Separate verification deadline from upgrade deadline
parents
081549df
25fa2ffc
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
1182 additions
and
195 deletions
+1182
-195
common/djangoapps/course_modes/admin.py
+146
-20
common/djangoapps/course_modes/models.py
+36
-13
common/djangoapps/course_modes/tests/test_admin.py
+220
-0
common/djangoapps/course_modes/tests/test_views.py
+0
-45
common/djangoapps/student/helpers.py
+13
-18
common/djangoapps/student/tests/test_verification_status.py
+7
-4
common/djangoapps/student/views.py
+2
-6
lms/djangoapps/verify_student/migrations/0011_add_verification_deadline.py
+156
-0
lms/djangoapps/verify_student/migrations/0012_populate_verification_deadlines.py
+180
-0
lms/djangoapps/verify_student/models.py
+118
-2
lms/djangoapps/verify_student/tests/test_models.py
+49
-2
lms/djangoapps/verify_student/tests/test_views.py
+110
-17
lms/djangoapps/verify_student/views.py
+116
-50
lms/templates/verify_student/missed_deadline.html
+29
-0
lms/templates/verify_student/missed_verification_deadline.html
+0
-18
No files found.
common/djangoapps/course_modes/admin.py
View file @
81e677b7
...
@@ -2,16 +2,31 @@
...
@@ -2,16 +2,31 @@
Django admin page for course modes
Django admin page for course modes
"""
"""
from
django.conf
import
settings
from
django.conf
import
settings
from
pytz
import
timezone
,
UTC
from
ratelimitbackend
import
admin
from
course_modes.models
import
CourseMode
from
django
import
forms
from
django
import
forms
from
django.utils.translation
import
ugettext_lazy
as
_
from
django.contrib
import
admin
from
pytz
import
timezone
,
UTC
from
opaque_keys
import
InvalidKeyError
from
xmodule.modulestore.django
import
modulestore
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
from
opaque_keys
import
InvalidKeyError
from
util.date_utils
import
get_time_display
from
util.date_utils
import
get_time_display
from
xmodule.modulestore.django
import
modulestore
from
course_modes.models
import
CourseMode
# Technically, we shouldn't be doing this, since verify_student is defined
# in LMS, and course_modes is defined in common.
#
# Once we move the responsibility for administering course modes into
# the Course Admin tool, we can remove this dependency and expose
# verification deadlines as a separate Django model admin.
#
# The admin page will work in both LMS and Studio,
# but the test suite for Studio will fail because
# the verification deadline table won't exist.
from
verify_student
import
models
as
verification_models
# pylint: disable=import-error
class
CourseModeForm
(
forms
.
ModelForm
):
class
CourseModeForm
(
forms
.
ModelForm
):
...
@@ -26,7 +41,45 @@ class CourseModeForm(forms.ModelForm):
...
@@ -26,7 +41,45 @@ class CourseModeForm(forms.ModelForm):
[(
mode_slug
,
mode_slug
)
for
mode_slug
in
CourseMode
.
CREDIT_MODES
]
[(
mode_slug
,
mode_slug
)
for
mode_slug
in
CourseMode
.
CREDIT_MODES
]
)
)
mode_slug
=
forms
.
ChoiceField
(
choices
=
COURSE_MODE_SLUG_CHOICES
)
mode_slug
=
forms
.
ChoiceField
(
choices
=
COURSE_MODE_SLUG_CHOICES
,
label
=
_
(
"Mode"
))
# The verification deadline is stored outside the course mode in the verify_student app.
# (we used to use the course mode expiration_datetime as both an upgrade and verification deadline).
# In order to make this transition easier, we include the verification deadline as a custom field
# in the course mode admin form. Longer term, we will deprecate the course mode Django admin
# form in favor of an external Course Administration Tool.
verification_deadline
=
forms
.
SplitDateTimeField
(
label
=
_
(
"Verification Deadline"
),
required
=
False
,
help_text
=
_
(
"OPTIONAL: After this date/time, users will no longer be able to submit photos for verification. "
"This appies ONLY to modes that require verification."
),
widget
=
admin
.
widgets
.
AdminSplitDateTime
,
)
def
__init__
(
self
,
*
args
,
**
kwargs
):
super
(
CourseModeForm
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
default_tz
=
timezone
(
settings
.
TIME_ZONE
)
if
self
.
instance
.
expiration_datetime
:
# django admin is using default timezone. To avoid time conversion from db to form
# convert the UTC object to naive and then localize with default timezone.
expiration_datetime
=
self
.
instance
.
expiration_datetime
.
replace
(
tzinfo
=
None
)
self
.
initial
[
"expiration_datetime"
]
=
default_tz
.
localize
(
expiration_datetime
)
# Load the verification deadline
# Since this is stored on a model in verify student, we need to load it from there.
# We need to munge the timezone a bit to get Django admin to display it without converting
# it to the user's timezone. We'll make sure we write it back to the database with the timezone
# set to UTC later.
if
self
.
instance
.
course_id
and
self
.
instance
.
mode_slug
in
CourseMode
.
VERIFIED_MODES
:
deadline
=
verification_models
.
VerificationDeadline
.
deadline_for_course
(
self
.
instance
.
course_id
)
self
.
initial
[
"verification_deadline"
]
=
(
default_tz
.
localize
(
deadline
.
replace
(
tzinfo
=
None
))
if
deadline
is
not
None
else
None
)
def
clean_course_id
(
self
):
def
clean_course_id
(
self
):
course_id
=
self
.
cleaned_data
[
'course_id'
]
course_id
=
self
.
cleaned_data
[
'course_id'
]
...
@@ -43,38 +96,111 @@ class CourseModeForm(forms.ModelForm):
...
@@ -43,38 +96,111 @@ class CourseModeForm(forms.ModelForm):
return
course_key
return
course_key
def
__init__
(
self
,
*
args
,
**
kwargs
):
super
(
CourseModeForm
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
if
self
.
instance
.
expiration_datetime
:
default_tz
=
timezone
(
settings
.
TIME_ZONE
)
# django admin is using default timezone. To avoid time conversion from db to form
# convert the UTC object to naive and then localize with default timezone.
expiration_datetime
=
self
.
instance
.
expiration_datetime
.
replace
(
tzinfo
=
None
)
self
.
initial
[
'expiration_datetime'
]
=
default_tz
.
localize
(
expiration_datetime
)
def
clean_expiration_datetime
(
self
):
def
clean_expiration_datetime
(
self
):
"""changing the tzinfo for a given datetime object"""
"""
Ensure that the expiration datetime we save uses the UTC timezone.
"""
# django admin saving the date with default timezone to avoid time conversion from form to db
# django admin saving the date with default timezone to avoid time conversion from form to db
# changes its tzinfo to UTC
# changes its tzinfo to UTC
if
self
.
cleaned_data
.
get
(
"expiration_datetime"
):
if
self
.
cleaned_data
.
get
(
"expiration_datetime"
):
return
self
.
cleaned_data
.
get
(
"expiration_datetime"
)
.
replace
(
tzinfo
=
UTC
)
return
self
.
cleaned_data
.
get
(
"expiration_datetime"
)
.
replace
(
tzinfo
=
UTC
)
def
clean_verification_deadline
(
self
):
"""
Ensure that the verification deadline we save uses the UTC timezone.
"""
if
self
.
cleaned_data
.
get
(
"verification_deadline"
):
return
self
.
cleaned_data
.
get
(
"verification_deadline"
)
.
replace
(
tzinfo
=
UTC
)
def
clean
(
self
):
"""
Clean the form fields.
This is the place to perform checks that involve multiple form fields.
"""
cleaned_data
=
super
(
CourseModeForm
,
self
)
.
clean
()
mode_slug
=
cleaned_data
.
get
(
"mode_slug"
)
upgrade_deadline
=
cleaned_data
.
get
(
"expiration_datetime"
)
verification_deadline
=
cleaned_data
.
get
(
"verification_deadline"
)
# Allow upgrade deadlines ONLY for the "verified" mode
# This avoids a nasty error condition in which the upgrade deadline is set
# for a professional education course before the enrollment end date.
# When this happens, the course mode expires and students are able to enroll
# in the course for free. To avoid this, we explicitly prevent admins from
# setting an upgrade deadline for any mode except "verified" (which has an upgrade path).
if
upgrade_deadline
is
not
None
and
mode_slug
!=
CourseMode
.
VERIFIED
:
raise
forms
.
ValidationError
(
'Only the "verified" mode can have an upgrade deadline. '
'For other modes, please set the enrollment end date in Studio.'
)
# Verification deadlines are allowed only for verified modes
if
verification_deadline
is
not
None
and
mode_slug
not
in
CourseMode
.
VERIFIED_MODES
:
raise
forms
.
ValidationError
(
"Verification deadline can be set only for verified modes."
)
# Verification deadline must be after the upgrade deadline,
# if an upgrade deadline is set.
# There are cases in which we might want to set a verification deadline,
# but not an upgrade deadline (for example, a professional education course that requires verification).
if
verification_deadline
is
not
None
:
if
upgrade_deadline
is
not
None
and
verification_deadline
<
upgrade_deadline
:
raise
forms
.
ValidationError
(
"Verification deadline must be after the upgrade deadline."
)
return
cleaned_data
def
save
(
self
,
commit
=
True
):
"""
Save the form data.
"""
# Trigger validation so we can access cleaned data
if
self
.
is_valid
():
course_key
=
self
.
cleaned_data
.
get
(
"course_id"
)
verification_deadline
=
self
.
cleaned_data
.
get
(
"verification_deadline"
)
mode_slug
=
self
.
cleaned_data
.
get
(
"mode_slug"
)
# Since the verification deadline is stored in a separate model,
# we need to handle saving this ourselves.
# Note that verification deadline can be `None` here if
# the deadline is being disabled.
if
course_key
is
not
None
and
mode_slug
in
CourseMode
.
VERIFIED_MODES
:
verification_models
.
VerificationDeadline
.
set_deadline
(
course_key
,
verification_deadline
)
return
super
(
CourseModeForm
,
self
)
.
save
(
commit
=
commit
)
class
CourseModeAdmin
(
admin
.
ModelAdmin
):
class
CourseModeAdmin
(
admin
.
ModelAdmin
):
"""Admin for course modes"""
"""Admin for course modes"""
form
=
CourseModeForm
form
=
CourseModeForm
fields
=
(
'course_id'
,
'mode_slug'
,
'mode_display_name'
,
'min_price'
,
'currency'
,
'expiration_datetime'
,
'verification_deadline'
,
'sku'
)
search_fields
=
(
'course_id'
,)
search_fields
=
(
'course_id'
,)
list_display
=
(
list_display
=
(
'id'
,
'course_id'
,
'mode_slug'
,
'mode_display_name'
,
'min_price'
,
'id'
,
'currency'
,
'expiration_date'
,
'expiration_datetime_custom'
,
'sku'
'course_id'
,
'mode_slug'
,
'min_price'
,
'expiration_datetime_custom'
,
'sku'
)
)
exclude
=
(
'suggested_prices'
,)
def
expiration_datetime_custom
(
self
,
obj
):
def
expiration_datetime_custom
(
self
,
obj
):
"""adding custom column to show the expiry_datetime"""
"""adding custom column to show the expiry_datetime"""
if
obj
.
expiration_datetime
:
if
obj
.
expiration_datetime
:
return
get_time_display
(
obj
.
expiration_datetime
,
'
%
B
%
d,
%
Y,
%
H:
%
M
%
p'
)
return
get_time_display
(
obj
.
expiration_datetime
,
'
%
B
%
d,
%
Y,
%
H:
%
M
%
p'
)
expiration_datetime_custom
.
short_description
=
"Expiration Datetime"
# Display a more user-friendly name for the custom expiration datetime field
# in the Django admin list view.
expiration_datetime_custom
.
short_description
=
"Upgrade Deadline"
admin
.
site
.
register
(
CourseMode
,
CourseModeAdmin
)
admin
.
site
.
register
(
CourseMode
,
CourseModeAdmin
)
common/djangoapps/course_modes/models.py
View file @
81e677b7
...
@@ -30,28 +30,45 @@ class CourseMode(models.Model):
...
@@ -30,28 +30,45 @@ class CourseMode(models.Model):
"""
"""
# the course that this mode is attached to
# the course that this mode is attached to
course_id
=
CourseKeyField
(
max_length
=
255
,
db_index
=
True
)
course_id
=
CourseKeyField
(
max_length
=
255
,
db_index
=
True
,
verbose_name
=
_
(
"Course"
)
)
# the reference to this mode that can be used by Enrollments to generate
# the reference to this mode that can be used by Enrollments to generate
# similar behavior for the same slug across courses
# similar behavior for the same slug across courses
mode_slug
=
models
.
CharField
(
max_length
=
100
)
mode_slug
=
models
.
CharField
(
max_length
=
100
,
verbose_name
=
_
(
"Mode"
)
)
# The 'pretty' name that can be translated and displayed
# The 'pretty' name that can be translated and displayed
mode_display_name
=
models
.
CharField
(
max_length
=
255
)
mode_display_name
=
models
.
CharField
(
max_length
=
255
,
verbose_name
=
_
(
"Display Name"
))
# minimum price in USD that we would like to charge for this mode of the course
min_price
=
models
.
IntegerField
(
default
=
0
)
# the suggested prices for this mode
# The price in USD that we would like to charge for this mode of the course
suggested_prices
=
models
.
CommaSeparatedIntegerField
(
max_length
=
255
,
blank
=
True
,
default
=
''
)
# Historical note: We used to allow users to choose from several prices, but later
# switched to using a single price. Although this field is called `min_price`, it is
# really just the price of the course.
min_price
=
models
.
IntegerField
(
default
=
0
,
verbose_name
=
_
(
"Price"
))
# the currency these prices are in, using lower case ISO currency codes
# the currency these prices are in, using lower case ISO currency codes
currency
=
models
.
CharField
(
default
=
"usd"
,
max_length
=
8
)
currency
=
models
.
CharField
(
default
=
"usd"
,
max_length
=
8
)
# turn this mode off after the given expiration date
# The datetime at which the course mode will expire.
# This is used to implement "upgrade" deadlines.
# For example, if there is a verified mode that expires on 1/1/2015,
# then users will be able to upgrade into the verified mode before that date.
# Once the date passes, users will no longer be able to enroll as verified.
expiration_datetime
=
models
.
DateTimeField
(
default
=
None
,
null
=
True
,
blank
=
True
,
verbose_name
=
_
(
u"Upgrade Deadline"
),
help_text
=
_
(
u"OPTIONAL: After this date/time, users will no longer be able to enroll in this mode. "
u"Leave this blank if users can enroll in this mode until enrollment closes for the course."
),
)
# DEPRECATED: the `expiration_date` field has been replaced by `expiration_datetime`
expiration_date
=
models
.
DateField
(
default
=
None
,
null
=
True
,
blank
=
True
)
expiration_date
=
models
.
DateField
(
default
=
None
,
null
=
True
,
blank
=
True
)
expiration_datetime
=
models
.
DateTimeField
(
default
=
None
,
null
=
True
,
blank
=
True
)
# DEPRECATED: the suggested prices for this mode
# We used to allow users to choose from a set of prices, but we now allow only
# a single price. This field has been deprecated by `min_price`
suggested_prices
=
models
.
CommaSeparatedIntegerField
(
max_length
=
255
,
blank
=
True
,
default
=
''
)
# optional description override
# optional description override
# WARNING: will not be localized
# WARNING: will not be localized
...
@@ -63,7 +80,10 @@ class CourseMode(models.Model):
...
@@ -63,7 +80,10 @@ class CourseMode(models.Model):
null
=
True
,
null
=
True
,
blank
=
True
,
blank
=
True
,
verbose_name
=
"SKU"
,
verbose_name
=
"SKU"
,
help_text
=
"This is the SKU (stock keeping unit) of this mode in the external ecommerce service."
help_text
=
_
(
u"OPTIONAL: This is the SKU (stock keeping unit) of this mode in the external ecommerce service. "
u"Leave this blank if the course has not yet been migrated to the ecommerce service."
)
)
)
HONOR
=
'honor'
HONOR
=
'honor'
...
@@ -217,7 +237,7 @@ class CourseMode(models.Model):
...
@@ -217,7 +237,7 @@ class CourseMode(models.Model):
return
modes
return
modes
@classmethod
@classmethod
def
modes_for_course_dict
(
cls
,
course_id
,
modes
=
None
,
only_selectable
=
True
):
def
modes_for_course_dict
(
cls
,
course_id
,
modes
=
None
,
**
kwargs
):
"""Returns the non-expired modes for a particular course.
"""Returns the non-expired modes for a particular course.
Arguments:
Arguments:
...
@@ -228,6 +248,9 @@ class CourseMode(models.Model):
...
@@ -228,6 +248,9 @@ class CourseMode(models.Model):
of course modes. This can be used to avoid an additional
of course modes. This can be used to avoid an additional
database query if you have already loaded the modes list.
database query if you have already loaded the modes list.
include_expired (bool): If True, expired course modes will be included
in the returned values. If False, these modes will be omitted.
only_selectable (bool): If True, include only modes that are shown
only_selectable (bool): If True, include only modes that are shown
to users on the track selection page. (Currently, "credit" modes
to users on the track selection page. (Currently, "credit" modes
aren't available to users until they complete the course, so
aren't available to users until they complete the course, so
...
@@ -238,7 +261,7 @@ class CourseMode(models.Model):
...
@@ -238,7 +261,7 @@ class CourseMode(models.Model):
"""
"""
if
modes
is
None
:
if
modes
is
None
:
modes
=
cls
.
modes_for_course
(
course_id
,
only_selectable
=
only_selectable
)
modes
=
cls
.
modes_for_course
(
course_id
,
**
kwargs
)
return
{
mode
.
slug
:
mode
for
mode
in
modes
}
return
{
mode
.
slug
:
mode
for
mode
in
modes
}
...
...
common/djangoapps/course_modes/tests/test_admin.py
0 → 100644
View file @
81e677b7
"""
Tests for the course modes Django admin interface.
"""
import
unittest
from
datetime
import
datetime
,
timedelta
import
ddt
from
pytz
import
timezone
,
UTC
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
util.date_utils
import
get_time_display
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
student.tests.factories
import
UserFactory
from
course_modes.models
import
CourseMode
from
course_modes.admin
import
CourseModeForm
# Technically, we shouldn't be importing verify_student, since it's
# defined in the LMS and course_modes is in common. However, the benefits
# of putting all this configuration in one place outweigh the downsides.
# Once the course admin tool is deployed, we can remove this dependency.
from
verify_student.models
import
VerificationDeadline
# pylint: disable=import-error
# We can only test this in the LMS because the course modes admin relies
# on verify student, which is not an installed app in Studio, so the verification
# deadline table will not be created.
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
class
AdminCourseModePageTest
(
ModuleStoreTestCase
):
"""
Test the course modes Django admin interface.
"""
def
test_expiration_timezone
(
self
):
# Test that expiration datetimes are saved and retrieved with the timezone set to UTC.
# This verifies the fix for a bug in which the date displayed to users was different
# than the date in Django admin.
user
=
UserFactory
.
create
(
is_staff
=
True
,
is_superuser
=
True
)
user
.
save
()
course
=
CourseFactory
.
create
()
expiration
=
datetime
(
2015
,
10
,
20
,
1
,
10
,
23
,
tzinfo
=
timezone
(
settings
.
TIME_ZONE
))
data
=
{
'course_id'
:
unicode
(
course
.
id
),
'mode_slug'
:
'verified'
,
'mode_display_name'
:
'verified'
,
'min_price'
:
10
,
'currency'
:
'usd'
,
'expiration_datetime_0'
:
expiration
.
date
(),
# due to django admin datetime widget passing as seperate vals
'expiration_datetime_1'
:
expiration
.
time
(),
}
self
.
client
.
login
(
username
=
user
.
username
,
password
=
'test'
)
# Create a new course mode from django admin page
response
=
self
.
client
.
post
(
reverse
(
'admin:course_modes_coursemode_add'
),
data
=
data
)
self
.
assertRedirects
(
response
,
reverse
(
'admin:course_modes_coursemode_changelist'
))
# Verify that datetime is appears on list page
response
=
self
.
client
.
get
(
reverse
(
'admin:course_modes_coursemode_changelist'
))
self
.
assertContains
(
response
,
get_time_display
(
expiration
,
'
%
B
%
d,
%
Y,
%
H:
%
M
%
p'
))
# Verify that on the edit page the datetime value appears as UTC.
resp
=
self
.
client
.
get
(
reverse
(
'admin:course_modes_coursemode_change'
,
args
=
(
1
,)))
self
.
assertContains
(
resp
,
expiration
.
date
())
self
.
assertContains
(
resp
,
expiration
.
time
())
# Verify that the expiration datetime is the same as what we set
# (hasn't changed because of a timezone translation).
course_mode
=
CourseMode
.
objects
.
get
(
pk
=
1
)
self
.
assertEqual
(
course_mode
.
expiration_datetime
.
replace
(
tzinfo
=
None
),
expiration
.
replace
(
tzinfo
=
None
))
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
@ddt.ddt
class
AdminCourseModeFormTest
(
ModuleStoreTestCase
):
"""
Test the course modes Django admin form validation and saving.
"""
UPGRADE_DEADLINE
=
datetime
.
now
(
UTC
)
VERIFICATION_DEADLINE
=
UPGRADE_DEADLINE
+
timedelta
(
days
=
5
)
def
setUp
(
self
):
"""
Create a test course.
"""
super
(
AdminCourseModeFormTest
,
self
)
.
setUp
()
self
.
course
=
CourseFactory
.
create
()
@ddt.data
(
(
"honor"
,
False
),
(
"verified"
,
True
),
(
"professional"
,
True
),
(
"no-id-professional"
,
False
),
(
"credit"
,
False
),
)
@ddt.unpack
def
test_load_verification_deadline
(
self
,
mode
,
expect_deadline
):
# Configure a verification deadline for the course
VerificationDeadline
.
set_deadline
(
self
.
course
.
id
,
self
.
VERIFICATION_DEADLINE
)
# Configure a course mode with both an upgrade and verification deadline
# and load the form to edit it.
deadline
=
self
.
UPGRADE_DEADLINE
if
mode
==
"verified"
else
None
form
=
self
.
_admin_form
(
mode
,
upgrade_deadline
=
deadline
)
# Check that the verification deadline is loaded,
# but ONLY for verified modes.
loaded_deadline
=
form
.
initial
.
get
(
"verification_deadline"
)
if
expect_deadline
:
self
.
assertEqual
(
loaded_deadline
.
replace
(
tzinfo
=
None
),
self
.
VERIFICATION_DEADLINE
.
replace
(
tzinfo
=
None
)
)
else
:
self
.
assertIs
(
loaded_deadline
,
None
)
@ddt.data
(
"verified"
,
"professional"
)
def
test_set_verification_deadline
(
self
,
course_mode
):
# Configure a verification deadline for the course
VerificationDeadline
.
set_deadline
(
self
.
course
.
id
,
self
.
VERIFICATION_DEADLINE
)
# Create the course mode Django admin form
form
=
self
.
_admin_form
(
course_mode
)
# Update the verification deadline form data
# We need to set the date and time fields separately, since they're
# displayed as separate widgets in the form.
new_deadline
=
(
self
.
VERIFICATION_DEADLINE
+
timedelta
(
days
=
1
))
.
replace
(
microsecond
=
0
)
self
.
_set_form_verification_deadline
(
form
,
new_deadline
)
form
.
save
()
# Check that the deadline was updated
updated_deadline
=
VerificationDeadline
.
deadline_for_course
(
self
.
course
.
id
)
self
.
assertEqual
(
updated_deadline
,
new_deadline
)
def
test_disable_verification_deadline
(
self
):
# Configure a verification deadline for the course
VerificationDeadline
.
set_deadline
(
self
.
course
.
id
,
self
.
VERIFICATION_DEADLINE
)
# Create the course mode Django admin form
form
=
self
.
_admin_form
(
"verified"
,
upgrade_deadline
=
self
.
UPGRADE_DEADLINE
)
# Use the form to disable the verification deadline
self
.
_set_form_verification_deadline
(
form
,
None
)
form
.
save
()
# Check that the deadline was disabled
self
.
assertIs
(
VerificationDeadline
.
deadline_for_course
(
self
.
course
.
id
),
None
)
@ddt.data
(
"honor"
,
"professional"
,
"no-id-professional"
,
"credit"
)
def
test_validate_upgrade_deadline_only_for_verified
(
self
,
course_mode
):
# Only the verified mode should have an upgrade deadline, so any other course
# mode that has an upgrade deadline set should cause a validation error
form
=
self
.
_admin_form
(
course_mode
,
upgrade_deadline
=
self
.
UPGRADE_DEADLINE
)
self
.
_assert_form_has_error
(
form
,
(
'Only the "verified" mode can have an upgrade deadline. '
'For other modes, please set the enrollment end date in Studio.'
))
@ddt.data
(
"honor"
,
"no-id-professional"
,
"credit"
)
def
test_validate_verification_deadline_only_for_verified
(
self
,
course_mode
):
# Only the verified mode should have a verification deadline set.
# Any other course mode should raise a validation error if a deadline is set.
form
=
self
.
_admin_form
(
course_mode
)
self
.
_set_form_verification_deadline
(
form
,
self
.
VERIFICATION_DEADLINE
)
self
.
_assert_form_has_error
(
form
,
"Verification deadline can be set only for verified modes."
)
def
test_verification_deadline_after_upgrade_deadline
(
self
):
form
=
self
.
_admin_form
(
"verified"
,
upgrade_deadline
=
self
.
UPGRADE_DEADLINE
)
before_upgrade
=
self
.
UPGRADE_DEADLINE
-
timedelta
(
days
=
1
)
self
.
_set_form_verification_deadline
(
form
,
before_upgrade
)
self
.
_assert_form_has_error
(
form
,
"Verification deadline must be after the upgrade deadline."
)
def
_configure
(
self
,
mode
,
upgrade_deadline
=
None
,
verification_deadline
=
None
):
"""Configure course modes and deadlines. """
course_mode
=
CourseMode
.
objects
.
create
(
mode_slug
=
mode
,
mode_display_name
=
mode
,
)
if
upgrade_deadline
is
not
None
:
course_mode
.
upgrade_deadline
=
upgrade_deadline
course_mode
.
save
()
VerificationDeadline
.
set_deadline
(
self
.
course
.
id
,
verification_deadline
)
return
CourseModeForm
(
instance
=
course_mode
)
def
_admin_form
(
self
,
mode
,
upgrade_deadline
=
None
):
"""Load the course mode admin form. """
course_mode
=
CourseMode
.
objects
.
create
(
course_id
=
self
.
course
.
id
,
mode_slug
=
mode
,
)
return
CourseModeForm
({
"course_id"
:
unicode
(
self
.
course
.
id
),
"mode_slug"
:
mode
,
"mode_display_name"
:
mode
,
"expiration_datetime"
:
upgrade_deadline
,
"currency"
:
"usd"
,
"min_price"
:
10
,
},
instance
=
course_mode
)
def
_set_form_verification_deadline
(
self
,
form
,
deadline
):
"""Set the verification deadline on the course mode admin form. """
date_str
=
deadline
.
strftime
(
"
%
Y-
%
m-
%
d"
)
if
deadline
else
None
time_str
=
deadline
.
strftime
(
"
%
H:
%
M:
%
S"
)
if
deadline
else
None
form
.
data
[
"verification_deadline_0"
]
=
date_str
form
.
data
[
"verification_deadline_1"
]
=
time_str
def
_assert_form_has_error
(
self
,
form
,
error
):
"""Check that a form has a validation error. """
validation_errors
=
form
.
errors
.
get
(
"__all__"
,
[])
self
.
assertIn
(
error
,
validation_errors
)
common/djangoapps/course_modes/tests/test_views.py
View file @
81e677b7
...
@@ -4,12 +4,9 @@ import ddt
...
@@ -4,12 +4,9 @@ import ddt
from
mock
import
patch
from
mock
import
patch
from
django.conf
import
settings
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
pytz
import
timezone
from
datetime
import
datetime
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
util.date_utils
import
get_time_display
from
util.testing
import
UrlResetMixin
from
util.testing
import
UrlResetMixin
from
embargo.test_utils
import
restrict_course
from
embargo.test_utils
import
restrict_course
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
...
@@ -371,45 +368,3 @@ class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase):
...
@@ -371,45 +368,3 @@ class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase):
def
test_embargo_allow
(
self
):
def
test_embargo_allow
(
self
):
response
=
self
.
client
.
get
(
self
.
url
)
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
class
AdminCourseModePageTest
(
ModuleStoreTestCase
):
"""Test the django admin course mode form saving data in db without any conversion
properly converts the tzinfo from default zone to utc
"""
def
test_save_valid_data
(
self
):
user
=
UserFactory
.
create
(
is_staff
=
True
,
is_superuser
=
True
)
user
.
save
()
course
=
CourseFactory
.
create
()
expiration
=
datetime
(
2015
,
10
,
20
,
1
,
10
,
23
,
tzinfo
=
timezone
(
settings
.
TIME_ZONE
))
data
=
{
'course_id'
:
unicode
(
course
.
id
),
'mode_slug'
:
'professional'
,
'mode_display_name'
:
'professional'
,
'min_price'
:
10
,
'currency'
:
'usd'
,
'expiration_datetime_0'
:
expiration
.
date
(),
# due to django admin datetime widget passing as seperate vals
'expiration_datetime_1'
:
expiration
.
time
(),
}
self
.
client
.
login
(
username
=
user
.
username
,
password
=
'test'
)
# creating new course mode from django admin page
response
=
self
.
client
.
post
(
reverse
(
'admin:course_modes_coursemode_add'
),
data
=
data
)
self
.
assertRedirects
(
response
,
reverse
(
'admin:course_modes_coursemode_changelist'
))
# verifying that datetime is appearing on list page
response
=
self
.
client
.
get
(
reverse
(
'admin:course_modes_coursemode_changelist'
))
self
.
assertContains
(
response
,
get_time_display
(
expiration
,
'
%
B
%
d,
%
Y,
%
H:
%
M
%
p'
))
# verifiying the on edit page datetime value appearing without any modifications
resp
=
self
.
client
.
get
(
reverse
(
'admin:course_modes_coursemode_change'
,
args
=
(
1
,)))
self
.
assertContains
(
resp
,
expiration
.
date
())
self
.
assertContains
(
resp
,
expiration
.
time
())
# checking the values in db. comparing values without tzinfo
course_mode
=
CourseMode
.
objects
.
get
(
pk
=
1
)
self
.
assertEqual
(
course_mode
.
expiration_datetime
.
replace
(
tzinfo
=
None
),
expiration
.
replace
(
tzinfo
=
None
))
common/djangoapps/student/helpers.py
View file @
81e677b7
...
@@ -6,7 +6,7 @@ from pytz import UTC
...
@@ -6,7 +6,7 @@ from pytz import UTC
from
django.core.urlresolvers
import
reverse
,
NoReverseMatch
from
django.core.urlresolvers
import
reverse
,
NoReverseMatch
import
third_party_auth
import
third_party_auth
from
verify_student.models
import
SoftwareSecurePhotoVerification
# pylint: disable=import-error
from
verify_student.models
import
VerificationDeadline
,
SoftwareSecurePhotoVerification
# pylint: disable=import-error
from
course_modes.models
import
CourseMode
from
course_modes.models
import
CourseMode
...
@@ -19,7 +19,7 @@ VERIFY_STATUS_MISSED_DEADLINE = "verify_missed_deadline"
...
@@ -19,7 +19,7 @@ VERIFY_STATUS_MISSED_DEADLINE = "verify_missed_deadline"
VERIFY_STATUS_NEED_TO_REVERIFY
=
"verify_need_to_reverify"
VERIFY_STATUS_NEED_TO_REVERIFY
=
"verify_need_to_reverify"
def
check_verify_status_by_course
(
user
,
course_enrollments
,
all_course_modes
):
def
check_verify_status_by_course
(
user
,
course_enrollments
):
"""
"""
Determine the per-course verification statuses for a given user.
Determine the per-course verification statuses for a given user.
...
@@ -44,8 +44,6 @@ def check_verify_status_by_course(user, course_enrollments, all_course_modes):
...
@@ -44,8 +44,6 @@ def check_verify_status_by_course(user, course_enrollments, all_course_modes):
Arguments:
Arguments:
user (User): The currently logged-in user.
user (User): The currently logged-in user.
course_enrollments (list[CourseEnrollment]): The courses the user is enrolled in.
course_enrollments (list[CourseEnrollment]): The courses the user is enrolled in.
all_course_modes (list): List of all course modes for the student's enrolled courses,
including modes that have expired.
Returns:
Returns:
dict: Mapping of course keys verification status dictionaries.
dict: Mapping of course keys verification status dictionaries.
...
@@ -69,24 +67,21 @@ def check_verify_status_by_course(user, course_enrollments, all_course_modes):
...
@@ -69,24 +67,21 @@ def check_verify_status_by_course(user, course_enrollments, all_course_modes):
user
,
queryset
=
verifications
user
,
queryset
=
verifications
)
)
# Retrieve verification deadlines for the enrolled courses
enrolled_course_keys
=
[
enrollment
.
course_id
for
enrollment
in
course_enrollments
]
course_deadlines
=
VerificationDeadline
.
deadlines_for_courses
(
enrolled_course_keys
)
recent_verification_datetime
=
None
recent_verification_datetime
=
None
for
enrollment
in
course_enrollments
:
for
enrollment
in
course_enrollments
:
# Get the verified mode (if any) for this course
# If the user hasn't enrolled as verified, then the course
# We pass in the course modes we have already loaded to avoid
# won't display state related to its verification status.
# another database hit, as well as to ensure that expired
if
enrollment
.
mode
in
CourseMode
.
VERIFIED_MODES
:
# course modes are included in the search.
verified_mode
=
CourseMode
.
verified_mode_for_course
(
# Retrieve the verification deadline associated with the course.
enrollment
.
course_id
,
# This could be None if the course doesn't have a deadline.
modes
=
all_course_modes
[
enrollment
.
course_id
]
deadline
=
course_deadlines
.
get
(
enrollment
.
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
)
relevant_verification
=
SoftwareSecurePhotoVerification
.
verification_for_datetime
(
deadline
,
verifications
)
...
...
common/djangoapps/student/tests/test_verification_status.py
View file @
81e677b7
...
@@ -20,7 +20,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
...
@@ -20,7 +20,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
student.tests.factories
import
UserFactory
,
CourseEnrollmentFactory
from
student.tests.factories
import
UserFactory
,
CourseEnrollmentFactory
from
course_modes.tests.factories
import
CourseModeFactory
from
course_modes.tests.factories
import
CourseModeFactory
from
verify_student.models
import
SoftwareSecurePhotoVerification
# pylint: disable=import-error
from
verify_student.models
import
VerificationDeadline
,
SoftwareSecurePhotoVerification
# pylint: disable=import-error
from
util.testing
import
UrlResetMixin
from
util.testing
import
UrlResetMixin
...
@@ -61,9 +61,11 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
...
@@ -61,9 +61,11 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
mode
=
"verified"
mode
=
"verified"
)
)
# The default course has no verified mode,
# Continue to show the student as needing to verify.
# so no verification status should be displayed
# The student is enrolled as verified, so we might as well let them
self
.
_assert_course_verification_status
(
None
)
# complete verification. We'd need to change their enrollment mode
# anyway to ensure that the student is issued the correct kind of certificate.
self
.
_assert_course_verification_status
(
VERIFY_STATUS_NEED_TO_VERIFY
)
def
test_need_to_verify_no_expiration
(
self
):
def
test_need_to_verify_no_expiration
(
self
):
self
.
_setup_mode_and_enrollment
(
None
,
"verified"
)
self
.
_setup_mode_and_enrollment
(
None
,
"verified"
)
...
@@ -285,6 +287,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
...
@@ -285,6 +287,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
user
=
self
.
user
,
user
=
self
.
user
,
mode
=
enrollment_mode
mode
=
enrollment_mode
)
)
VerificationDeadline
.
set_deadline
(
self
.
course
.
id
,
deadline
)
BANNER_ALT_MESSAGES
=
{
BANNER_ALT_MESSAGES
=
{
None
:
"Honor"
,
None
:
"Honor"
,
...
...
common/djangoapps/student/views.py
View file @
81e677b7
...
@@ -533,7 +533,7 @@ def dashboard(request):
...
@@ -533,7 +533,7 @@ def dashboard(request):
# Retrieve the course modes for each course
# Retrieve the course modes for each course
enrolled_course_ids
=
[
enrollment
.
course_id
for
enrollment
in
course_enrollments
]
enrolled_course_ids
=
[
enrollment
.
course_id
for
enrollment
in
course_enrollments
]
all_course_modes
,
unexpired_course_modes
=
CourseMode
.
all_and_unexpired_modes_for_courses
(
enrolled_course_ids
)
__
,
unexpired_course_modes
=
CourseMode
.
all_and_unexpired_modes_for_courses
(
enrolled_course_ids
)
course_modes_by_course
=
{
course_modes_by_course
=
{
course_id
:
{
course_id
:
{
mode
.
slug
:
mode
mode
.
slug
:
mode
...
@@ -596,11 +596,7 @@ def dashboard(request):
...
@@ -596,11 +596,7 @@ def dashboard(request):
#
#
# If a course is not included in this dictionary,
# If a course is not included in this dictionary,
# there is no verification messaging to display.
# there is no verification messaging to display.
verify_status_by_course
=
check_verify_status_by_course
(
verify_status_by_course
=
check_verify_status_by_course
(
user
,
course_enrollments
)
user
,
course_enrollments
,
all_course_modes
)
cert_statuses
=
{
cert_statuses
=
{
enrollment
.
course_id
:
cert_info
(
request
.
user
,
enrollment
.
course_overview
,
enrollment
.
mode
)
enrollment
.
course_id
:
cert_info
(
request
.
user
,
enrollment
.
course_overview
,
enrollment
.
mode
)
for
enrollment
in
course_enrollments
for
enrollment
in
course_enrollments
...
...
lms/djangoapps/verify_student/migrations/0011_add_verification_deadline.py
0 → 100644
View file @
81e677b7
# -*- coding: utf-8 -*-
from
south.utils
import
datetime_utils
as
datetime
from
south.db
import
db
from
south.v2
import
SchemaMigration
from
django.db
import
models
class
Migration
(
SchemaMigration
):
def
forwards
(
self
,
orm
):
# Adding model 'HistoricalVerificationDeadline'
db
.
create_table
(
'verify_student_historicalverificationdeadline'
,
(
(
'id'
,
self
.
gf
(
'django.db.models.fields.IntegerField'
)(
db_index
=
True
,
blank
=
True
)),
(
'created'
,
self
.
gf
(
'model_utils.fields.AutoCreatedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'modified'
,
self
.
gf
(
'model_utils.fields.AutoLastModifiedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'course_key'
,
self
.
gf
(
'xmodule_django.models.CourseKeyField'
)(
max_length
=
255
,
db_index
=
True
)),
(
'deadline'
,
self
.
gf
(
'django.db.models.fields.DateTimeField'
)()),
(
u'history_id'
,
self
.
gf
(
'django.db.models.fields.AutoField'
)(
primary_key
=
True
)),
(
u'history_date'
,
self
.
gf
(
'django.db.models.fields.DateTimeField'
)()),
(
u'history_user'
,
self
.
gf
(
'django.db.models.fields.related.ForeignKey'
)(
related_name
=
u'+'
,
null
=
True
,
on_delete
=
models
.
SET_NULL
,
to
=
orm
[
'auth.User'
])),
(
u'history_type'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
max_length
=
1
)),
))
db
.
send_create_signal
(
'verify_student'
,
[
'HistoricalVerificationDeadline'
])
# Adding model 'VerificationDeadline'
db
.
create_table
(
'verify_student_verificationdeadline'
,
(
(
'id'
,
self
.
gf
(
'django.db.models.fields.AutoField'
)(
primary_key
=
True
)),
(
'created'
,
self
.
gf
(
'model_utils.fields.AutoCreatedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'modified'
,
self
.
gf
(
'model_utils.fields.AutoLastModifiedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'course_key'
,
self
.
gf
(
'xmodule_django.models.CourseKeyField'
)(
unique
=
True
,
max_length
=
255
,
db_index
=
True
)),
(
'deadline'
,
self
.
gf
(
'django.db.models.fields.DateTimeField'
)()),
))
db
.
send_create_signal
(
'verify_student'
,
[
'VerificationDeadline'
])
def
backwards
(
self
,
orm
):
# Deleting model 'HistoricalVerificationDeadline'
db
.
delete_table
(
'verify_student_historicalverificationdeadline'
)
# Deleting model 'VerificationDeadline'
db
.
delete_table
(
'verify_student_verificationdeadline'
)
models
=
{
'auth.group'
:
{
'Meta'
:
{
'object_name'
:
'Group'
},
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'80'
}),
'permissions'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['auth.Permission']"
,
'symmetrical'
:
'False'
,
'blank'
:
'True'
})
},
'auth.permission'
:
{
'Meta'
:
{
'ordering'
:
"('content_type__app_label', 'content_type__model', 'codename')"
,
'unique_together'
:
"(('content_type', 'codename'),)"
,
'object_name'
:
'Permission'
},
'codename'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'content_type'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['contenttypes.ContentType']"
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'50'
})
},
'auth.user'
:
{
'Meta'
:
{
'object_name'
:
'User'
},
'date_joined'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'email'
:
(
'django.db.models.fields.EmailField'
,
[],
{
'max_length'
:
'75'
,
'blank'
:
'True'
}),
'first_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'30'
,
'blank'
:
'True'
}),
'groups'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['auth.Group']"
,
'symmetrical'
:
'False'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'is_active'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'True'
}),
'is_staff'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'is_superuser'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'last_login'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'last_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'30'
,
'blank'
:
'True'
}),
'password'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'128'
}),
'user_permissions'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['auth.Permission']"
,
'symmetrical'
:
'False'
,
'blank'
:
'True'
}),
'username'
:
(
'django.db.models.fields.CharField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'30'
})
},
'contenttypes.contenttype'
:
{
'Meta'
:
{
'ordering'
:
"('name',)"
,
'unique_together'
:
"(('app_label', 'model'),)"
,
'object_name'
:
'ContentType'
,
'db_table'
:
"'django_content_type'"
},
'app_label'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'model'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
})
},
'verify_student.historicalverificationdeadline'
:
{
'Meta'
:
{
'ordering'
:
"(u'-history_date', u'-history_id')"
,
'object_name'
:
'HistoricalVerificationDeadline'
},
'course_key'
:
(
'xmodule_django.models.CourseKeyField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'deadline'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{}),
u'history_date'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{}),
u'history_id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
u'history_type'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'1'
}),
u'history_user'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"u'+'"
,
'null'
:
'True'
,
'on_delete'
:
'models.SET_NULL'
,
'to'
:
"orm['auth.User']"
}),
'id'
:
(
'django.db.models.fields.IntegerField'
,
[],
{
'db_index'
:
'True'
,
'blank'
:
'True'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
})
},
'verify_student.incoursereverificationconfiguration'
:
{
'Meta'
:
{
'ordering'
:
"('-change_date',)"
,
'object_name'
:
'InCourseReverificationConfiguration'
},
'change_date'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'auto_now_add'
:
'True'
,
'blank'
:
'True'
}),
'changed_by'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['auth.User']"
,
'null'
:
'True'
,
'on_delete'
:
'models.PROTECT'
}),
'enabled'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
})
},
'verify_student.skippedreverification'
:
{
'Meta'
:
{
'unique_together'
:
"(('user', 'course_id'),)"
,
'object_name'
:
'SkippedReverification'
},
'checkpoint'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"'skipped_checkpoint'"
,
'to'
:
"orm['verify_student.VerificationCheckpoint']"
}),
'course_id'
:
(
'xmodule_django.models.CourseKeyField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'created_at'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'auto_now_add'
:
'True'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'user'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['auth.User']"
})
},
'verify_student.softwaresecurephotoverification'
:
{
'Meta'
:
{
'ordering'
:
"['-created_at']"
,
'object_name'
:
'SoftwareSecurePhotoVerification'
},
'created_at'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'auto_now_add'
:
'True'
,
'db_index'
:
'True'
,
'blank'
:
'True'
}),
'display'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'True'
,
'db_index'
:
'True'
}),
'error_code'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'50'
,
'blank'
:
'True'
}),
'error_msg'
:
(
'django.db.models.fields.TextField'
,
[],
{
'blank'
:
'True'
}),
'face_image_url'
:
(
'django.db.models.fields.URLField'
,
[],
{
'max_length'
:
'255'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'blank'
:
'True'
}),
'photo_id_image_url'
:
(
'django.db.models.fields.URLField'
,
[],
{
'max_length'
:
'255'
,
'blank'
:
'True'
}),
'photo_id_key'
:
(
'django.db.models.fields.TextField'
,
[],
{
'max_length'
:
'1024'
}),
'receipt_id'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"'9b14470c-4219-4c69-9a38-d8ff14b60b09'"
,
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'reviewing_service'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'blank'
:
'True'
}),
'reviewing_user'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'default'
:
'None'
,
'related_name'
:
"'photo_verifications_reviewed'"
,
'null'
:
'True'
,
'to'
:
"orm['auth.User']"
}),
'status'
:
(
'model_utils.fields.StatusField'
,
[],
{
'default'
:
"'created'"
,
'max_length'
:
'100'
,
u'no_check_for_status'
:
'True'
}),
'status_changed'
:
(
'model_utils.fields.MonitorField'
,
[],
{
'default'
:
'datetime.datetime.now'
,
u'monitor'
:
"u'status'"
}),
'submitted_at'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'null'
:
'True'
,
'db_index'
:
'True'
}),
'updated_at'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'auto_now'
:
'True'
,
'db_index'
:
'True'
,
'blank'
:
'True'
}),
'user'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['auth.User']"
})
},
'verify_student.verificationcheckpoint'
:
{
'Meta'
:
{
'unique_together'
:
"(('course_id', 'checkpoint_location'),)"
,
'object_name'
:
'VerificationCheckpoint'
},
'checkpoint_location'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
}),
'course_id'
:
(
'xmodule_django.models.CourseKeyField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'photo_verification'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['verify_student.SoftwareSecurePhotoVerification']"
,
'symmetrical'
:
'False'
})
},
'verify_student.verificationdeadline'
:
{
'Meta'
:
{
'object_name'
:
'VerificationDeadline'
},
'course_key'
:
(
'xmodule_django.models.CourseKeyField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'deadline'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
})
},
'verify_student.verificationstatus'
:
{
'Meta'
:
{
'object_name'
:
'VerificationStatus'
},
'checkpoint'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"'checkpoint_status'"
,
'to'
:
"orm['verify_student.VerificationCheckpoint']"
}),
'error'
:
(
'django.db.models.fields.TextField'
,
[],
{
'null'
:
'True'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'response'
:
(
'django.db.models.fields.TextField'
,
[],
{
'null'
:
'True'
,
'blank'
:
'True'
}),
'status'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'32'
,
'db_index'
:
'True'
}),
'timestamp'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'auto_now_add'
:
'True'
,
'blank'
:
'True'
}),
'user'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['auth.User']"
})
}
}
complete_apps
=
[
'verify_student'
]
\ No newline at end of file
lms/djangoapps/verify_student/migrations/0012_populate_verification_deadlines.py
0 → 100644
View file @
81e677b7
# -*- coding: utf-8 -*-
from
south.utils
import
datetime_utils
as
datetime
from
south.db
import
db
from
south.v2
import
DataMigration
from
django.db
import
models
class
Migration
(
DataMigration
):
def
forwards
(
self
,
orm
):
"""
This migration populates the "verification deadline" model with
the "expiration datetime" from the course modes table for verified
courses.
In the past, the course modes expiration (really an upgrade deadline)
and the verification deadline were always set to the same value.
With this change, the verification deadline will now be tracked in a separate
model owned by the verify_student app.
"""
# Retrieve all verified course modes (whether they have expired or not)
# Unfortunately, we don't have access to constants from the application here,
# so we hard-code the names of the course modes that require verification.
verified_modes
=
orm
[
'course_modes.CourseMode'
]
.
objects
.
filter
(
mode_slug__in
=
[
"verified"
,
"professional"
],
expiration_datetime__isnull
=
False
,
)
for
mode
in
verified_modes
:
orm
.
VerificationDeadline
.
objects
.
create
(
course_key
=
mode
.
course_id
,
deadline
=
mode
.
expiration_datetime
,
)
def
backwards
(
self
,
orm
):
"""
Backwards migration deletes all verification deadlines.
"""
orm
.
VerificationDeadline
.
objects
.
all
()
.
delete
()
models
=
{
'auth.group'
:
{
'Meta'
:
{
'object_name'
:
'Group'
},
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'80'
}),
'permissions'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['auth.Permission']"
,
'symmetrical'
:
'False'
,
'blank'
:
'True'
})
},
'auth.permission'
:
{
'Meta'
:
{
'ordering'
:
"('content_type__app_label', 'content_type__model', 'codename')"
,
'unique_together'
:
"(('content_type', 'codename'),)"
,
'object_name'
:
'Permission'
},
'codename'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'content_type'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['contenttypes.ContentType']"
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'50'
})
},
'auth.user'
:
{
'Meta'
:
{
'object_name'
:
'User'
},
'date_joined'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'email'
:
(
'django.db.models.fields.EmailField'
,
[],
{
'max_length'
:
'75'
,
'blank'
:
'True'
}),
'first_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'30'
,
'blank'
:
'True'
}),
'groups'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['auth.Group']"
,
'symmetrical'
:
'False'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'is_active'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'True'
}),
'is_staff'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'is_superuser'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'last_login'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'last_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'30'
,
'blank'
:
'True'
}),
'password'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'128'
}),
'user_permissions'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['auth.Permission']"
,
'symmetrical'
:
'False'
,
'blank'
:
'True'
}),
'username'
:
(
'django.db.models.fields.CharField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'30'
})
},
'contenttypes.contenttype'
:
{
'Meta'
:
{
'ordering'
:
"('name',)"
,
'unique_together'
:
"(('app_label', 'model'),)"
,
'object_name'
:
'ContentType'
,
'db_table'
:
"'django_content_type'"
},
'app_label'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'model'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
})
},
'course_modes.coursemode'
:
{
'Meta'
:
{
'unique_together'
:
"(('course_id', 'mode_slug', 'currency'),)"
,
'object_name'
:
'CourseMode'
},
'course_id'
:
(
'xmodule_django.models.CourseKeyField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'currency'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"'usd'"
,
'max_length'
:
'8'
}),
'description'
:
(
'django.db.models.fields.TextField'
,
[],
{
'null'
:
'True'
,
'blank'
:
'True'
}),
'expiration_date'
:
(
'django.db.models.fields.DateField'
,
[],
{
'default'
:
'None'
,
'null'
:
'True'
,
'blank'
:
'True'
}),
'expiration_datetime'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'default'
:
'None'
,
'null'
:
'True'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'min_price'
:
(
'django.db.models.fields.IntegerField'
,
[],
{
'default'
:
'0'
}),
'mode_display_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
}),
'mode_slug'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'sku'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'null'
:
'True'
,
'blank'
:
'True'
}),
'suggested_prices'
:
(
'django.db.models.fields.CommaSeparatedIntegerField'
,
[],
{
'default'
:
"''"
,
'max_length'
:
'255'
,
'blank'
:
'True'
})
},
'course_modes.coursemodesarchive'
:
{
'Meta'
:
{
'object_name'
:
'CourseModesArchive'
},
'course_id'
:
(
'xmodule_django.models.CourseKeyField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'currency'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"'usd'"
,
'max_length'
:
'8'
}),
'expiration_date'
:
(
'django.db.models.fields.DateField'
,
[],
{
'default'
:
'None'
,
'null'
:
'True'
,
'blank'
:
'True'
}),
'expiration_datetime'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'default'
:
'None'
,
'null'
:
'True'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'min_price'
:
(
'django.db.models.fields.IntegerField'
,
[],
{
'default'
:
'0'
}),
'mode_display_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
}),
'mode_slug'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'suggested_prices'
:
(
'django.db.models.fields.CommaSeparatedIntegerField'
,
[],
{
'default'
:
"''"
,
'max_length'
:
'255'
,
'blank'
:
'True'
})
},
'verify_student.historicalverificationdeadline'
:
{
'Meta'
:
{
'ordering'
:
"(u'-history_date', u'-history_id')"
,
'object_name'
:
'HistoricalVerificationDeadline'
},
'course_key'
:
(
'xmodule_django.models.CourseKeyField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'deadline'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{}),
u'history_date'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{}),
u'history_id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
u'history_type'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'1'
}),
u'history_user'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"u'+'"
,
'null'
:
'True'
,
'on_delete'
:
'models.SET_NULL'
,
'to'
:
"orm['auth.User']"
}),
'id'
:
(
'django.db.models.fields.IntegerField'
,
[],
{
'db_index'
:
'True'
,
'blank'
:
'True'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
})
},
'verify_student.incoursereverificationconfiguration'
:
{
'Meta'
:
{
'ordering'
:
"('-change_date',)"
,
'object_name'
:
'InCourseReverificationConfiguration'
},
'change_date'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'auto_now_add'
:
'True'
,
'blank'
:
'True'
}),
'changed_by'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['auth.User']"
,
'null'
:
'True'
,
'on_delete'
:
'models.PROTECT'
}),
'enabled'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
})
},
'verify_student.skippedreverification'
:
{
'Meta'
:
{
'unique_together'
:
"(('user', 'course_id'),)"
,
'object_name'
:
'SkippedReverification'
},
'checkpoint'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"'skipped_checkpoint'"
,
'to'
:
"orm['verify_student.VerificationCheckpoint']"
}),
'course_id'
:
(
'xmodule_django.models.CourseKeyField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'created_at'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'auto_now_add'
:
'True'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'user'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['auth.User']"
})
},
'verify_student.softwaresecurephotoverification'
:
{
'Meta'
:
{
'ordering'
:
"['-created_at']"
,
'object_name'
:
'SoftwareSecurePhotoVerification'
},
'created_at'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'auto_now_add'
:
'True'
,
'db_index'
:
'True'
,
'blank'
:
'True'
}),
'display'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'True'
,
'db_index'
:
'True'
}),
'error_code'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'50'
,
'blank'
:
'True'
}),
'error_msg'
:
(
'django.db.models.fields.TextField'
,
[],
{
'blank'
:
'True'
}),
'face_image_url'
:
(
'django.db.models.fields.URLField'
,
[],
{
'max_length'
:
'255'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'blank'
:
'True'
}),
'photo_id_image_url'
:
(
'django.db.models.fields.URLField'
,
[],
{
'max_length'
:
'255'
,
'blank'
:
'True'
}),
'photo_id_key'
:
(
'django.db.models.fields.TextField'
,
[],
{
'max_length'
:
'1024'
}),
'receipt_id'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"'6644e0c2-da9b-49a4-9d0c-c19c596c911e'"
,
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'reviewing_service'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'blank'
:
'True'
}),
'reviewing_user'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'default'
:
'None'
,
'related_name'
:
"'photo_verifications_reviewed'"
,
'null'
:
'True'
,
'to'
:
"orm['auth.User']"
}),
'status'
:
(
'model_utils.fields.StatusField'
,
[],
{
'default'
:
"'created'"
,
'max_length'
:
'100'
,
u'no_check_for_status'
:
'True'
}),
'status_changed'
:
(
'model_utils.fields.MonitorField'
,
[],
{
'default'
:
'datetime.datetime.now'
,
u'monitor'
:
"u'status'"
}),
'submitted_at'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'null'
:
'True'
,
'db_index'
:
'True'
}),
'updated_at'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'auto_now'
:
'True'
,
'db_index'
:
'True'
,
'blank'
:
'True'
}),
'user'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['auth.User']"
})
},
'verify_student.verificationcheckpoint'
:
{
'Meta'
:
{
'unique_together'
:
"(('course_id', 'checkpoint_location'),)"
,
'object_name'
:
'VerificationCheckpoint'
},
'checkpoint_location'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
}),
'course_id'
:
(
'xmodule_django.models.CourseKeyField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'photo_verification'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['verify_student.SoftwareSecurePhotoVerification']"
,
'symmetrical'
:
'False'
})
},
'verify_student.verificationdeadline'
:
{
'Meta'
:
{
'object_name'
:
'VerificationDeadline'
},
'course_key'
:
(
'xmodule_django.models.CourseKeyField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'deadline'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
})
},
'verify_student.verificationstatus'
:
{
'Meta'
:
{
'object_name'
:
'VerificationStatus'
},
'checkpoint'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'related_name'
:
"'checkpoint_status'"
,
'to'
:
"orm['verify_student.VerificationCheckpoint']"
}),
'error'
:
(
'django.db.models.fields.TextField'
,
[],
{
'null'
:
'True'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'response'
:
(
'django.db.models.fields.TextField'
,
[],
{
'null'
:
'True'
,
'blank'
:
'True'
}),
'status'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'32'
,
'db_index'
:
'True'
}),
'timestamp'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'auto_now_add'
:
'True'
,
'blank'
:
'True'
}),
'user'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['auth.User']"
})
}
}
complete_apps
=
[
'course_modes'
,
'verify_student'
]
symmetrical
=
True
lms/djangoapps/verify_student/models.py
View file @
81e677b7
...
@@ -24,14 +24,17 @@ from django.conf import settings
...
@@ -24,14 +24,17 @@ from django.conf import settings
from
django.contrib.auth.models
import
User
from
django.contrib.auth.models
import
User
from
django.core.exceptions
import
ObjectDoesNotExist
from
django.core.exceptions
import
ObjectDoesNotExist
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
django.core.cache
import
cache
from
django.dispatch
import
receiver
from
django.db
import
models
from
django.db
import
models
from
django.utils.translation
import
ugettext
as
_
from
django.utils.translation
import
ugettext
as
_
,
ugettext_lazy
from
boto.s3.connection
import
S3Connection
from
boto.s3.connection
import
S3Connection
from
boto.s3.key
import
Key
from
boto.s3.key
import
Key
from
simple_history.models
import
HistoricalRecords
from
config_models.models
import
ConfigurationModel
from
config_models.models
import
ConfigurationModel
from
course_modes.models
import
CourseMode
from
course_modes.models
import
CourseMode
from
model_utils.models
import
StatusModel
from
model_utils.models
import
StatusModel
,
TimeStampedModel
from
model_utils
import
Choices
from
model_utils
import
Choices
from
verify_student.ssencrypt
import
(
from
verify_student.ssencrypt
import
(
random_aes_key
,
encrypt_and_encode
,
random_aes_key
,
encrypt_and_encode
,
...
@@ -884,6 +887,119 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
...
@@ -884,6 +887,119 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
return
'ID Verified'
return
'ID Verified'
class
VerificationDeadline
(
TimeStampedModel
):
"""
Represent a verification deadline for a particular course.
The verification deadline is the datetime after which
users are no longer allowed to submit photos for initial verification
in a course.
Note that this is NOT the same as the "upgrade" deadline, after
which a user is no longer allowed to upgrade to a verified enrollment.
If no verification deadline record exists for a course,
then that course does not have a deadline. This means that users
can submit photos at any time.
"""
course_key
=
CourseKeyField
(
max_length
=
255
,
db_index
=
True
,
unique
=
True
,
help_text
=
ugettext_lazy
(
u"The course for which this deadline applies"
),
)
deadline
=
models
.
DateTimeField
(
help_text
=
ugettext_lazy
(
u"The datetime after which users are no longer allowed "
u"to submit photos for verification."
)
)
# Maintain a history of changes to deadlines for auditing purposes
history
=
HistoricalRecords
()
ALL_DEADLINES_CACHE_KEY
=
"verify_student.all_verification_deadlines"
@classmethod
def
set_deadline
(
cls
,
course_key
,
deadline
):
"""
Configure the verification deadline for a course.
If `deadline` is `None`, then the course will have no verification
deadline. In this case, users will be able to verify for the course
at any time.
Arguments:
course_key (CourseKey): Identifier for the course.
deadline (datetime or None): The verification deadline.
"""
if
deadline
is
None
:
VerificationDeadline
.
objects
.
filter
(
course_key
=
course_key
)
.
delete
()
else
:
record
,
created
=
VerificationDeadline
.
objects
.
get_or_create
(
course_key
=
course_key
,
defaults
=
{
"deadline"
:
deadline
}
)
if
not
created
:
record
.
deadline
=
deadline
record
.
save
()
@classmethod
def
deadlines_for_courses
(
cls
,
course_keys
):
"""
Retrieve verification deadlines for particular courses.
Arguments:
course_keys (list): List of `CourseKey`s.
Returns:
dict: Map of course keys to datetimes (verification deadlines)
"""
all_deadlines
=
cache
.
get
(
cls
.
ALL_DEADLINES_CACHE_KEY
)
if
all_deadlines
is
None
:
all_deadlines
=
{
deadline
.
course_key
:
deadline
.
deadline
for
deadline
in
VerificationDeadline
.
objects
.
all
()
}
cache
.
set
(
cls
.
ALL_DEADLINES_CACHE_KEY
,
all_deadlines
)
return
{
course_key
:
all_deadlines
[
course_key
]
for
course_key
in
course_keys
if
course_key
in
all_deadlines
}
@classmethod
def
deadline_for_course
(
cls
,
course_key
):
"""
Retrieve the verification deadline for a particular course.
Arguments:
course_key (CourseKey): The identifier for the course.
Returns:
datetime or None
"""
try
:
deadline
=
cls
.
objects
.
get
(
course_key
=
course_key
)
return
deadline
.
deadline
except
cls
.
DoesNotExist
:
return
None
@receiver
(
models
.
signals
.
post_save
,
sender
=
VerificationDeadline
)
@receiver
(
models
.
signals
.
post_delete
,
sender
=
VerificationDeadline
)
def
invalidate_deadline_caches
(
sender
,
**
kwargs
):
# pylint: disable=unused-argument
"""Invalidate the cached verification deadline information. """
cache
.
delete
(
VerificationDeadline
.
ALL_DEADLINES_CACHE_KEY
)
class
VerificationCheckpoint
(
models
.
Model
):
class
VerificationCheckpoint
(
models
.
Model
):
"""Represents a point at which a user is asked to re-verify his/her
"""Represents a point at which a user is asked to re-verify his/her
identity.
identity.
...
...
lms/djangoapps/verify_student/tests/test_models.py
View file @
81e677b7
...
@@ -7,6 +7,7 @@ import pytz
...
@@ -7,6 +7,7 @@ import pytz
from
django.conf
import
settings
from
django.conf
import
settings
from
django.db.utils
import
IntegrityError
from
django.db.utils
import
IntegrityError
from
django.test
import
TestCase
from
mock
import
patch
from
mock
import
patch
from
nose.tools
import
assert_is_none
,
assert_equals
,
assert_raises
,
assert_true
,
assert_false
# pylint: disable=no-name-in-module
from
nose.tools
import
assert_is_none
,
assert_equals
,
assert_raises
,
assert_true
,
assert_false
# pylint: disable=no-name-in-module
...
@@ -14,9 +15,13 @@ from student.tests.factories import UserFactory
...
@@ -14,9 +15,13 @@ from student.tests.factories import UserFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
opaque_keys.edx.keys
import
CourseKey
from
verify_student.models
import
(
from
verify_student.models
import
(
SoftwareSecurePhotoVerification
,
VerificationException
,
VerificationCheckpoint
,
VerificationStatus
,
SoftwareSecurePhotoVerification
,
SkippedReverification
VerificationException
,
VerificationCheckpoint
,
VerificationStatus
,
SkippedReverification
,
VerificationDeadline
)
)
FAKE_SETTINGS
=
{
FAKE_SETTINGS
=
{
...
@@ -763,3 +768,45 @@ class SkippedReverificationTest(ModuleStoreTestCase):
...
@@ -763,3 +768,45 @@ class SkippedReverificationTest(ModuleStoreTestCase):
self
.
assertFalse
(
self
.
assertFalse
(
SkippedReverification
.
check_user_skipped_reverification_exists
(
course_id
=
self
.
course
.
id
,
user
=
user2
)
SkippedReverification
.
check_user_skipped_reverification_exists
(
course_id
=
self
.
course
.
id
,
user
=
user2
)
)
)
class
VerificationDeadlineTest
(
TestCase
):
"""
Tests for the VerificationDeadline model.
"""
def
test_caching
(
self
):
deadlines
=
{
CourseKey
.
from_string
(
"edX/DemoX/Fall"
):
datetime
.
now
(
pytz
.
UTC
),
CourseKey
.
from_string
(
"edX/DemoX/Spring"
):
datetime
.
now
(
pytz
.
UTC
)
+
timedelta
(
days
=
1
)
}
course_keys
=
deadlines
.
keys
()
# Initially, no deadlines are set
with
self
.
assertNumQueries
(
1
):
all_deadlines
=
VerificationDeadline
.
deadlines_for_courses
(
course_keys
)
self
.
assertEqual
(
all_deadlines
,
{})
# Create the deadlines
for
course_key
,
deadline
in
deadlines
.
iteritems
():
VerificationDeadline
.
objects
.
create
(
course_key
=
course_key
,
deadline
=
deadline
,
)
# Warm the cache
with
self
.
assertNumQueries
(
1
):
VerificationDeadline
.
deadlines_for_courses
(
course_keys
)
# Load the deadlines from the cache
with
self
.
assertNumQueries
(
0
):
all_deadlines
=
VerificationDeadline
.
deadlines_for_courses
(
course_keys
)
self
.
assertEqual
(
all_deadlines
,
deadlines
)
# Delete the deadlines
VerificationDeadline
.
objects
.
all
()
.
delete
()
# Verify that the deadlines are updated correctly
with
self
.
assertNumQueries
(
1
):
all_deadlines
=
VerificationDeadline
.
deadlines_for_courses
(
course_keys
)
self
.
assertEqual
(
all_deadlines
,
{})
lms/djangoapps/verify_student/tests/test_views.py
View file @
81e677b7
...
@@ -43,8 +43,9 @@ from verify_student.views import (
...
@@ -43,8 +43,9 @@ from verify_student.views import (
_compose_message_reverification_email
_compose_message_reverification_email
)
)
from
verify_student.models
import
(
from
verify_student.models
import
(
SoftwareSecurePhotoVerification
,
VerificationCheckpoint
,
VerificationDeadline
,
SoftwareSecurePhotoVerification
,
InCourseReverificationConfiguration
,
VerificationStatus
VerificationCheckpoint
,
InCourseReverificationConfiguration
,
VerificationStatus
)
)
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
...
@@ -612,15 +613,15 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
...
@@ -612,15 +613,15 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
self
.
_assert_contribution_amount
(
response
,
"12.34"
)
self
.
_assert_contribution_amount
(
response
,
"12.34"
)
def
test_verification_deadline
(
self
):
def
test_verification_deadline
(
self
):
# Set a deadline on the course mode
deadline
=
datetime
(
2999
,
1
,
2
,
tzinfo
=
pytz
.
UTC
)
course
=
self
.
_create_course
(
"verified"
)
course
=
self
.
_create_course
(
"verified"
)
mode
=
CourseMode
.
objects
.
get
(
course_id
=
course
.
id
,
# Set a deadline on the course mode AND on the verification deadline model.
mode_slug
=
"verified"
# This simulates the common case in which the upgrade deadline (course mode expiration)
)
# and the verification deadline are the same.
expiration
=
datetime
(
2999
,
1
,
2
,
tzinfo
=
pytz
.
UTC
)
# NOTE: we used to use the course mode expiration datetime for BOTH of these deadlines,
mode
.
expiration_datetime
=
expiration
# before the VerificationDeadline model was introduced.
mode
.
save
(
)
self
.
_set_deadlines
(
course
.
id
,
upgrade_deadline
=
deadline
,
verification_deadline
=
deadline
)
# Expect that the expiration date is set
# Expect that the expiration date is set
response
=
self
.
_get_page
(
"verify_student_start_flow"
,
course
.
id
)
response
=
self
.
_get_page
(
"verify_student_start_flow"
,
course
.
id
)
...
@@ -628,14 +629,13 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
...
@@ -628,14 +629,13 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
self
.
assertEqual
(
data
[
'verification_deadline'
],
"Jan 02, 2999 at 00:00 UTC"
)
self
.
assertEqual
(
data
[
'verification_deadline'
],
"Jan 02, 2999 at 00:00 UTC"
)
def
test_course_mode_expired
(
self
):
def
test_course_mode_expired
(
self
):
deadline
=
datetime
(
1999
,
1
,
2
,
tzinfo
=
pytz
.
UTC
)
course
=
self
.
_create_course
(
"verified"
)
course
=
self
.
_create_course
(
"verified"
)
mode
=
CourseMode
.
objects
.
get
(
course_id
=
course
.
id
,
# Set the upgrade deadline (course mode expiration) and verification deadline
mode_slug
=
"verified"
# to the same value. This used to be the default when we used the expiration datetime
)
# for BOTH values.
expiration
=
datetime
(
1999
,
1
,
2
,
tzinfo
=
pytz
.
UTC
)
self
.
_set_deadlines
(
course
.
id
,
upgrade_deadline
=
deadline
,
verification_deadline
=
deadline
)
mode
.
expiration_datetime
=
expiration
mode
.
save
()
# Need to be enrolled
# Need to be enrolled
self
.
_enroll
(
course
.
id
,
"verified"
)
self
.
_enroll
(
course
.
id
,
"verified"
)
...
@@ -646,6 +646,66 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
...
@@ -646,6 +646,66 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
self
.
assertContains
(
response
,
"verification deadline"
)
self
.
assertContains
(
response
,
"verification deadline"
)
self
.
assertContains
(
response
,
"Jan 02, 1999 at 00:00 UTC"
)
self
.
assertContains
(
response
,
"Jan 02, 1999 at 00:00 UTC"
)
@ddt.data
(
datetime
(
2999
,
1
,
2
,
tzinfo
=
pytz
.
UTC
),
None
)
def
test_course_mode_expired_verification_deadline_in_future
(
self
,
verification_deadline
):
course
=
self
.
_create_course
(
"verified"
)
# Set the upgrade deadline in the past, but the verification
# deadline in the future.
self
.
_set_deadlines
(
course
.
id
,
upgrade_deadline
=
datetime
(
1999
,
1
,
2
,
tzinfo
=
pytz
.
UTC
),
verification_deadline
=
verification_deadline
,
)
# Try to pay or upgrade.
# We should get an error message since the deadline has passed.
for
page_name
in
[
"verify_student_start_flow"
,
"verify_student_upgrade_and_verify"
]:
response
=
self
.
_get_page
(
page_name
,
course
.
id
)
self
.
assertContains
(
response
,
"Upgrade Deadline Has Passed"
)
# Simulate paying for the course and enrolling
self
.
_enroll
(
course
.
id
,
"verified"
)
# Enter the verification part of the flow
# Expect that we are able to verify
response
=
self
.
_get_page
(
"verify_student_verify_now"
,
course
.
id
)
self
.
assertNotContains
(
response
,
"Verification is no longer available"
)
data
=
self
.
_get_page_data
(
response
)
self
.
assertEqual
(
data
[
'message_key'
],
PayAndVerifyView
.
VERIFY_NOW_MSG
)
# Check that the verification deadline (rather than the upgrade deadline) is displayed
if
verification_deadline
is
not
None
:
self
.
assertEqual
(
data
[
"verification_deadline"
],
"Jan 02, 2999 at 00:00 UTC"
)
else
:
self
.
assertEqual
(
data
[
"verification_deadline"
],
""
)
def
test_course_mode_not_expired_verification_deadline_passed
(
self
):
course
=
self
.
_create_course
(
"verified"
)
# Set the upgrade deadline in the future
# and the verification deadline in the past
# We try not to discourage this with validation rules,
# since it's a bad user experience
# to purchase a verified track and then not be able to verify,
# but if it happens we need to handle it gracefully.
self
.
_set_deadlines
(
course
.
id
,
upgrade_deadline
=
datetime
(
2999
,
1
,
2
,
tzinfo
=
pytz
.
UTC
),
verification_deadline
=
datetime
(
1999
,
1
,
2
,
tzinfo
=
pytz
.
UTC
),
)
# Enroll as verified (simulate purchasing the verified enrollment)
self
.
_enroll
(
course
.
id
,
"verified"
)
# Even though the upgrade deadline is in the future,
# the verification deadline has passed, so we should see an error
# message when we go to verify.
response
=
self
.
_get_page
(
"verify_student_verify_now"
,
course
.
id
)
self
.
assertContains
(
response
,
"verification deadline"
)
self
.
assertContains
(
response
,
"Jan 02, 1999 at 00:00 UTC"
)
@mock.patch.dict
(
settings
.
FEATURES
,
{
'EMBARGO'
:
True
})
@mock.patch.dict
(
settings
.
FEATURES
,
{
'EMBARGO'
:
True
})
def
test_embargo_restrict
(
self
):
def
test_embargo_restrict
(
self
):
course
=
self
.
_create_course
(
"verified"
)
course
=
self
.
_create_course
(
"verified"
)
...
@@ -716,6 +776,30 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
...
@@ -716,6 +776,30 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
attempt
.
created_at
=
datetime
.
now
(
pytz
.
UTC
)
-
timedelta
(
days
=
(
days_good_for
+
1
))
attempt
.
created_at
=
datetime
.
now
(
pytz
.
UTC
)
-
timedelta
(
days
=
(
days_good_for
+
1
))
attempt
.
save
()
attempt
.
save
()
def
_set_deadlines
(
self
,
course_key
,
upgrade_deadline
=
None
,
verification_deadline
=
None
):
"""
Set the upgrade and verification deadlines.
Arguments:
course_key (CourseKey): Identifier for the course.
Keyword Arguments:
upgrade_deadline (datetime): Datetime after which a user cannot
upgrade to a verified mode.
verification_deadline (datetime): Datetime after which a user cannot
submit an initial verification attempt.
"""
# Set the course mode expiration (same as the "upgrade" deadline)
mode
=
CourseMode
.
objects
.
get
(
course_id
=
course_key
,
mode_slug
=
"verified"
)
mode
.
expiration_datetime
=
upgrade_deadline
mode
.
save
()
# Set the verification deadline
VerificationDeadline
.
set_deadline
(
course_key
,
verification_deadline
)
def
_set_contribution
(
self
,
amount
,
course_id
):
def
_set_contribution
(
self
,
amount
,
course_id
):
"""Set the contribution amount pre-filled in a session var. """
"""Set the contribution amount pre-filled in a session var. """
session
=
self
.
client
.
session
session
=
self
.
client
.
session
...
@@ -785,6 +869,15 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
...
@@ -785,6 +869,15 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
"""Retrieve the data attributes rendered on the page. """
"""Retrieve the data attributes rendered on the page. """
soup
=
BeautifulSoup
(
response
.
content
)
soup
=
BeautifulSoup
(
response
.
content
)
pay_and_verify_div
=
soup
.
find
(
id
=
"pay-and-verify-container"
)
pay_and_verify_div
=
soup
.
find
(
id
=
"pay-and-verify-container"
)
self
.
assertIsNot
(
pay_and_verify_div
,
None
,
msg
=
(
"Could not load pay and verify flow data. "
"Maybe this isn't the pay and verify page?"
)
)
return
{
return
{
'full_name'
:
pay_and_verify_div
[
'data-full-name'
],
'full_name'
:
pay_and_verify_div
[
'data-full-name'
],
'course_key'
:
pay_and_verify_div
[
'data-course-key'
],
'course_key'
:
pay_and_verify_div
[
'data-course-key'
],
...
...
lms/djangoapps/verify_student/views.py
View file @
81e677b7
...
@@ -47,6 +47,7 @@ from shoppingcart.processors import (
...
@@ -47,6 +47,7 @@ from shoppingcart.processors import (
)
)
from
verify_student.ssencrypt
import
has_valid_signature
from
verify_student.ssencrypt
import
has_valid_signature
from
verify_student.models
import
(
from
verify_student.models
import
(
VerificationDeadline
,
SoftwareSecurePhotoVerification
,
SoftwareSecurePhotoVerification
,
VerificationCheckpoint
,
VerificationCheckpoint
,
VerificationStatus
,
VerificationStatus
,
...
@@ -194,6 +195,10 @@ class PayAndVerifyView(View):
...
@@ -194,6 +195,10 @@ class PayAndVerifyView(View):
FACE_PHOTO_STEP
:
[
WEBCAM_REQ
],
FACE_PHOTO_STEP
:
[
WEBCAM_REQ
],
}
}
# Deadline types
VERIFICATION_DEADLINE
=
"verification"
UPGRADE_DEADLINE
=
"upgrade"
@method_decorator
(
login_required
)
@method_decorator
(
login_required
)
def
get
(
def
get
(
self
,
request
,
course_id
,
self
,
request
,
course_id
,
...
@@ -201,7 +206,8 @@ class PayAndVerifyView(View):
...
@@ -201,7 +206,8 @@ class PayAndVerifyView(View):
current_step
=
None
,
current_step
=
None
,
message
=
FIRST_TIME_VERIFY_MSG
message
=
FIRST_TIME_VERIFY_MSG
):
):
"""Render the pay/verify requirements page.
"""
Render the payment and verification flow.
Arguments:
Arguments:
request (HttpRequest): The request object.
request (HttpRequest): The request object.
...
@@ -244,28 +250,45 @@ class PayAndVerifyView(View):
...
@@ -244,28 +250,45 @@ class PayAndVerifyView(View):
if
redirect_url
:
if
redirect_url
:
return
redirect
(
redirect_url
)
return
redirect
(
redirect_url
)
expired_verified_course_mode
,
unexpired_paid_course_mode
=
self
.
_get_expired_verified_and_paid_mode
(
course_key
)
# If the verification deadline has passed
# then show the user a message that he/she can't verify.
# Check that the course has an unexpired paid mode
#
if
unexpired_paid_course_mode
is
not
None
:
# We're making the assumptions (enforced in Django admin) that:
if
CourseMode
.
is_verified_mode
(
unexpired_paid_course_mode
):
#
# 1) Only verified modes have verification deadlines.
#
# 2) If set, verification deadlines are always AFTER upgrade deadlines, because why would you
# let someone upgrade into a verified track if they can't complete verification?
#
verification_deadline
=
VerificationDeadline
.
deadline_for_course
(
course
.
id
)
response
=
self
.
_response_if_deadline_passed
(
course
,
self
.
VERIFICATION_DEADLINE
,
verification_deadline
)
if
response
is
not
None
:
log
.
info
(
u"Verification deadline for '
%
s' has passed."
,
course
.
id
)
return
response
# Retrieve the relevant course mode for the payment/verification flow.
#
# WARNING: this is technical debt! A much better way to do this would be to
# separate out the payment flow and use the product SKU to figure out what
# the user is trying to purchase.
#
# Nonethless, for the time being we continue to make the really ugly assumption
# that at some point there was a paid course mode we can query for the price.
relevant_course_mode
=
self
.
_get_paid_mode
(
course_key
)
# If we can find a relevant course mode, then log that we're entering the flow
# Otherwise, this course does not support payment/verification, so respond with a 404.
if
relevant_course_mode
is
not
None
:
if
CourseMode
.
is_verified_mode
(
relevant_course_mode
):
log
.
info
(
log
.
info
(
u"Entering
verified work
flow for user '
%
s', course '
%
s', with current step '
%
s'."
,
u"Entering
payment and verification
flow for user '
%
s', course '
%
s', with current step '
%
s'."
,
request
.
user
.
id
,
course_id
,
current_step
request
.
user
.
id
,
course_id
,
current_step
)
)
elif
expired_verified_course_mode
is
not
None
:
else
:
# Check if there is an *expired* verified course mode;
log
.
info
(
# if so, we should show a message explaining that the verification
u"Entering payment flow for user '
%
s', course '
%
s', with current step '
%
s'"
,
# deadline has passed.
request
.
user
.
id
,
course_id
,
current_step
log
.
info
(
u"Verification deadline for '
%
s' has passed."
,
course_id
)
context
=
{
'course'
:
course
,
'deadline'
:
(
get_default_time_display
(
expired_verified_course_mode
.
expiration_datetime
)
if
expired_verified_course_mode
.
expiration_datetime
else
""
)
)
}
return
render_to_response
(
"verify_student/missed_verification_deadline.html"
,
context
)
else
:
else
:
# Otherwise, there has never been a verified/paid mode,
# Otherwise, there has never been a verified/paid mode,
# so return a page not found response.
# so return a page not found response.
...
@@ -275,14 +298,32 @@ class PayAndVerifyView(View):
...
@@ -275,14 +298,32 @@ class PayAndVerifyView(View):
)
)
raise
Http404
raise
Http404
# If the user is trying to *pay* and the upgrade deadline has passed,
# then they shouldn't be able to enter the flow.
#
# NOTE: This should match the availability dates used by the E-Commerce service
# to determine whether a user can purchase a product. The idea is that if the service
# won't fulfill the order, we shouldn't even let the user get into the payment flow.
#
user_is_trying_to_pay
=
message
in
[
self
.
FIRST_TIME_VERIFY_MSG
,
self
.
UPGRADE_MSG
]
if
user_is_trying_to_pay
:
upgrade_deadline
=
relevant_course_mode
.
expiration_datetime
response
=
self
.
_response_if_deadline_passed
(
course
,
self
.
UPGRADE_DEADLINE
,
upgrade_deadline
)
if
response
is
not
None
:
log
.
info
(
u"Upgrade deadline for '
%
s' has passed."
,
course
.
id
)
return
response
# Check whether the user has verified, paid, and enrolled.
# Check whether the user has verified, paid, and enrolled.
# A user is considered "paid" if he or she has an enrollment
# A user is considered "paid" if he or she has an enrollment
# with a paid course mode (such as "verified").
# with a paid course mode (such as "verified").
# For this reason, every paid user is enrolled, but not
# For this reason, every paid user is enrolled, but not
# every enrolled user is paid.
# every enrolled user is paid.
# If the course mode is not verified(i.e only paid) then already_verified is always True
# If the course mode is not verified(i.e only paid) then already_verified is always True
already_verified
=
self
.
_check_already_verified
(
request
.
user
)
\
already_verified
=
(
if
CourseMode
.
is_verified_mode
(
unexpired_paid_course_mode
)
else
True
self
.
_check_already_verified
(
request
.
user
)
if
CourseMode
.
is_verified_mode
(
relevant_course_mode
)
else
True
)
already_paid
,
is_enrolled
=
self
.
_check_enrollment
(
request
.
user
,
course_key
)
already_paid
,
is_enrolled
=
self
.
_check_enrollment
(
request
.
user
,
course_key
)
# Redirect the user to a more appropriate page if the
# Redirect the user to a more appropriate page if the
...
@@ -302,7 +343,7 @@ class PayAndVerifyView(View):
...
@@ -302,7 +343,7 @@ class PayAndVerifyView(View):
always_show_payment
,
always_show_payment
,
already_verified
,
already_verified
,
already_paid
,
already_paid
,
unexpired_paid
_course_mode
relevant
_course_mode
)
)
requirements
=
self
.
_requirements
(
display_steps
,
request
.
user
.
is_active
)
requirements
=
self
.
_requirements
(
display_steps
,
request
.
user
.
is_active
)
...
@@ -346,7 +387,7 @@ class PayAndVerifyView(View):
...
@@ -346,7 +387,7 @@ class PayAndVerifyView(View):
verification_good_until
=
self
.
_verification_valid_until
(
request
.
user
)
verification_good_until
=
self
.
_verification_valid_until
(
request
.
user
)
# get available payment processors
# get available payment processors
if
unexpired_paid
_course_mode
.
sku
:
if
relevant
_course_mode
.
sku
:
# transaction will be conducted via ecommerce service
# transaction will be conducted via ecommerce service
processors
=
ecommerce_api_client
(
request
.
user
)
.
payment
.
processors
.
get
()
processors
=
ecommerce_api_client
(
request
.
user
)
.
payment
.
processors
.
get
()
else
:
else
:
...
@@ -358,7 +399,7 @@ class PayAndVerifyView(View):
...
@@ -358,7 +399,7 @@ class PayAndVerifyView(View):
'contribution_amount'
:
contribution_amount
,
'contribution_amount'
:
contribution_amount
,
'course'
:
course
,
'course'
:
course
,
'course_key'
:
unicode
(
course_key
),
'course_key'
:
unicode
(
course_key
),
'course_mode'
:
unexpired_paid
_course_mode
,
'course_mode'
:
relevant
_course_mode
,
'courseware_url'
:
courseware_url
,
'courseware_url'
:
courseware_url
,
'current_step'
:
current_step
,
'current_step'
:
current_step
,
'disable_courseware_js'
:
True
,
'disable_courseware_js'
:
True
,
...
@@ -370,8 +411,8 @@ class PayAndVerifyView(View):
...
@@ -370,8 +411,8 @@ class PayAndVerifyView(View):
'requirements'
:
requirements
,
'requirements'
:
requirements
,
'user_full_name'
:
full_name
,
'user_full_name'
:
full_name
,
'verification_deadline'
:
(
'verification_deadline'
:
(
get_default_time_display
(
unexpired_paid_course_mode
.
expiration_datetim
e
)
get_default_time_display
(
verification_deadlin
e
)
if
unexpired_paid_course_mode
.
expiration_datetim
e
else
""
if
verification_deadlin
e
else
""
),
),
'already_verified'
:
already_verified
,
'already_verified'
:
already_verified
,
'verification_good_until'
:
verification_good_until
,
'verification_good_until'
:
verification_good_until
,
...
@@ -449,41 +490,35 @@ class PayAndVerifyView(View):
...
@@ -449,41 +490,35 @@ class PayAndVerifyView(View):
if
url
is
not
None
:
if
url
is
not
None
:
return
redirect
(
url
)
return
redirect
(
url
)
def
_get_expired_verified_and_paid_mode
(
self
,
course_key
):
# pylint: disable=invalid-name
def
_get_paid_mode
(
self
,
course_key
):
"""Retrieve expired verified mode and unexpired paid mode(with min_price>0) for a course.
"""
Retrieve the paid course mode for a course.
The returned course mode may or may not be expired.
Unexpired modes are preferred to expired modes.
Arguments:
Arguments:
course_key (CourseKey): The location of the course.
course_key (CourseKey): The location of the course.
Returns:
Returns:
Tuple of `(expired_verified_mode, unexpired_paid_mode)`. If provided,
CourseMode tuple
`expired_verified_mode` is an *expired* verified mode for the course.
If provided, `unexpired_paid_mode` is an *unexpired* paid(with min_price>0)
mode for the course. Either of these may be None.
"""
"""
# Retrieve all the modes at once to reduce the number of database queries
# Retrieve all the modes at once to reduce the number of database queries
all_modes
,
unexpired_modes
=
CourseMode
.
all_and_unexpired_modes_for_courses
([
course_key
])
all_modes
,
unexpired_modes
=
CourseMode
.
all_and_unexpired_modes_for_courses
([
course_key
])
# Unexpired paid modes
# Retrieve the first unexpired, paid mode, if there is one
unexpired_paid_modes
=
[
mode
for
mode
in
unexpired_modes
[
course_key
]
if
mode
.
min_price
]
for
mode
in
unexpired_modes
[
course_key
]:
if
len
(
unexpired_paid_modes
)
>
1
:
if
mode
.
min_price
>
0
:
# There is more than one paid mode defined,
return
mode
# so choose the first one.
log
.
warn
(
u"More than one paid modes are defined for course '
%
s' choosing the first one
%
s"
,
course_key
,
unexpired_paid_modes
[
0
]
)
unexpired_paid_mode
=
unexpired_paid_modes
[
0
]
if
unexpired_paid_modes
else
None
# Find an unexpired verified mode
# Otherwise, find the first expired mode
verified_mode
=
CourseMode
.
verified_mode_for_course
(
course_key
,
modes
=
unexpired_modes
[
course_key
])
for
mode
in
all_modes
[
course_key
]:
expired_verified_mode
=
None
if
mode
.
min_price
>
0
:
return
mode
if
verified_mode
is
None
:
# Otherwise, return None and so the view knows to respond with a 404.
expired_verified_mode
=
CourseMode
.
verified_mode_for_course
(
course_key
,
modes
=
all_modes
[
course_key
])
return
None
return
(
expired_verified_mode
,
unexpired_paid_mode
)
def
_display_steps
(
self
,
always_show_payment
,
already_verified
,
already_paid
,
course_mode
):
def
_display_steps
(
self
,
always_show_payment
,
already_verified
,
already_paid
,
course_mode
):
"""Determine which steps to display to the user.
"""Determine which steps to display to the user.
...
@@ -610,12 +645,43 @@ class PayAndVerifyView(View):
...
@@ -610,12 +645,43 @@ class PayAndVerifyView(View):
has_paid
=
False
has_paid
=
False
if
enrollment_mode
is
not
None
and
is_active
:
if
enrollment_mode
is
not
None
and
is_active
:
all_modes
=
CourseMode
.
modes_for_course_dict
(
course_key
)
all_modes
=
CourseMode
.
modes_for_course_dict
(
course_key
,
include_expired
=
True
)
course_mode
=
all_modes
.
get
(
enrollment_mode
)
course_mode
=
all_modes
.
get
(
enrollment_mode
)
has_paid
=
(
course_mode
and
course_mode
.
min_price
>
0
)
has_paid
=
(
course_mode
and
course_mode
.
min_price
>
0
)
return
(
has_paid
,
bool
(
is_active
))
return
(
has_paid
,
bool
(
is_active
))
def
_response_if_deadline_passed
(
self
,
course
,
deadline_name
,
deadline_datetime
):
"""
Respond with some error messaging if the deadline has passed.
Arguments:
course (Course): The course the user is trying to enroll in.
deadline_name (str): One of the deadline constants.
deadline_datetime (datetime): The deadline.
Returns: HttpResponse or None
"""
if
deadline_name
not
in
[
self
.
VERIFICATION_DEADLINE
,
self
.
UPGRADE_DEADLINE
]:
log
.
error
(
"Invalid deadline name
%
s. Skipping check for whether the deadline passed."
,
deadline_name
)
return
None
deadline_passed
=
(
deadline_datetime
is
not
None
and
deadline_datetime
<
datetime
.
datetime
.
now
(
UTC
)
)
if
deadline_passed
:
context
=
{
'course'
:
course
,
'deadline_name'
:
deadline_name
,
'deadline'
:
(
get_default_time_display
(
deadline_datetime
)
if
deadline_datetime
else
""
)
}
return
render_to_response
(
"verify_student/missed_deadline.html"
,
context
)
def
checkout_with_ecommerce_service
(
user
,
course_key
,
course_mode
,
processor
):
# pylint: disable=invalid-name
def
checkout_with_ecommerce_service
(
user
,
course_key
,
course_mode
,
processor
):
# pylint: disable=invalid-name
""" Create a new basket and trigger immediate checkout, using the E-Commerce API. """
""" Create a new basket and trigger immediate checkout, using the E-Commerce API. """
...
...
lms/templates/verify_student/missed_deadline.html
0 → 100644
View file @
81e677b7
<
%!
from
django
.
utils
.
translation
import
ugettext
as
_
from
verify_student
.
views
import
PayAndVerifyView
%
>
<
%
namespace
name=
'static'
file=
'../static_content.html'
/>
<
%
inherit
file=
"../main.html"
/>
<
%
block
name=
"pagetitle"
>
% if deadline_name == PayAndVerifyView.VERIFICATION_DEADLINE:
${_("Verification Deadline Has Passed")}
% elif deadline_name == PayAndVerifyView.UPGRADE_DEADLINE:
${_("Upgrade Deadline Has Passed")}
% endif
</
%
block>
<
%
block
name=
"content"
>
<section
class=
"outside-app"
>
<p>
% if deadline_name == PayAndVerifyView.VERIFICATION_DEADLINE:
${_(u"The verification deadline for {course_name} was {date}. Verification is no longer available.").format(
course_name=course.display_name, date=deadline)}
% elif deadline_name == PayAndVerifyView.UPGRADE_DEADLINE:
${_(u"The deadline to upgrade to a verified certificate for this course has passed. You can still earn an honor code certificate.")}
% endif
</p>
</section>
</
%
block>
lms/templates/verify_student/missed_verification_deadline.html
deleted
100644 → 0
View file @
081549df
<
%!
from
django
.
utils
.
translation
import
ugettext
as
_
%
>
<
%
namespace
name=
'static'
file=
'../static_content.html'
/>
<
%
inherit
file=
"../main.html"
/>
<
%
block
name=
"pagetitle"
>
${_("Verification Deadline Has Passed")}
</
%
block>
<
%
block
name=
"content"
>
<section
class=
"outside-app"
>
<p>
${_(
u"The verification deadline for {course_name} was {date}. "
u"Verification is no longer available."
).format(
course_name=course.display_name,
date=deadline
)}
</p>
</section>
</
%
block>
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