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
d3a4747b
Commit
d3a4747b
authored
Oct 04, 2013
by
Diana Huang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add new user_status functionality to PhotoVerification.
parent
f3149651
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
150 additions
and
26 deletions
+150
-26
common/djangoapps/student/views.py
+6
-0
lms/djangoapps/verify_student/models.py
+79
-10
lms/djangoapps/verify_student/tests/test_models.py
+60
-12
lms/djangoapps/verify_student/tests/test_views.py
+2
-0
lms/envs/acceptance.py
+3
-0
lms/envs/test.py
+0
-4
No files found.
common/djangoapps/student/views.py
View file @
d3a4747b
...
...
@@ -47,6 +47,8 @@ from student.models import (
)
from
student.forms
import
PasswordResetFormNoActive
from
verify_student.models
import
SoftwareSecurePhotoVerification
from
certificates.models
import
CertificateStatuses
,
certificate_status_for_student
from
xmodule.course_module
import
CourseDescriptor
...
...
@@ -334,6 +336,8 @@ def dashboard(request):
CourseAuthorization
.
instructor_email_enabled
(
course
.
id
)
)
)
# Verification Attempts
verification_status
=
SoftwareSecurePhotoVerification
.
user_status
(
user
)
# get info w.r.t ExternalAuthMap
external_auth_map
=
None
try
:
...
...
@@ -351,6 +355,8 @@ def dashboard(request):
'all_course_modes'
:
course_modes
,
'cert_statuses'
:
cert_statuses
,
'show_email_settings_for'
:
show_email_settings_for
,
'verification_status'
:
verification_status
[
0
],
'verification_msg'
:
verification_status
[
1
],
}
return
render_to_response
(
'dashboard.html'
,
context
)
...
...
lms/djangoapps/verify_student/models.py
View file @
d3a4747b
...
...
@@ -26,6 +26,7 @@ from django.conf import settings
from
django.core.urlresolvers
import
reverse
from
django.db
import
models
from
django.contrib.auth.models
import
User
from
django.utils.translation
import
ugettext
as
_
from
model_utils.models
import
StatusModel
from
model_utils
import
Choices
...
...
@@ -175,20 +176,28 @@ class PhotoVerification(StatusModel):
##### Methods listed in the order you'd typically call them
@classmethod
def
_earliest_allowed_date
(
cls
):
"""
Returns the earliest allowed date given the settings
"""
allowed_date
=
(
datetime
.
now
(
pytz
.
UTC
)
-
timedelta
(
days
=
cls
.
DAYS_GOOD_FOR
)
)
return
allowed_date
@classmethod
def
user_is_verified
(
cls
,
user
,
earliest_allowed_date
=
None
):
"""
Return whether or not a user has satisfactorily proved their
identity. Depending on the policy, this can expire after some period of
time, so a user might have to renew periodically.
"""
earliest_allowed_date
=
(
earliest_allowed_date
or
datetime
.
now
(
pytz
.
UTC
)
-
timedelta
(
days
=
cls
.
DAYS_GOOD_FOR
)
)
return
cls
.
objects
.
filter
(
user
=
user
,
status
=
"approved"
,
created_at__gte
=
earliest_allowed_date
created_at__gte
=
(
earliest_allowed_date
or
cls
.
_earliest_allowed_date
())
)
.
exists
()
@classmethod
...
...
@@ -201,14 +210,11 @@ class PhotoVerification(StatusModel):
on the contents of the attempt, and we have not yet received a denial.
"""
valid_statuses
=
[
'must_retry'
,
'submitted'
,
'approved'
]
earliest_allowed_date
=
(
earliest_allowed_date
or
datetime
.
now
(
pytz
.
UTC
)
-
timedelta
(
days
=
cls
.
DAYS_GOOD_FOR
)
)
return
cls
.
objects
.
filter
(
user
=
user
,
status__in
=
valid_statuses
,
created_at__gte
=
earliest_allowed_date
created_at__gte
=
(
earliest_allowed_date
or
cls
.
_earliest_allowed_date
())
)
.
exists
()
@classmethod
...
...
@@ -225,6 +231,38 @@ class PhotoVerification(StatusModel):
else
:
return
None
@classmethod
def
user_status
(
cls
,
user
):
"""
Returns the status of the user based on their latest verification attempt
If no such verification exists, returns 'none'
If verification has expired, returns 'expired'
"""
try
:
attempts
=
cls
.
objects
.
filter
(
user
=
user
)
.
order_by
(
'-updated_at'
)
attempt
=
attempts
[
0
]
except
IndexError
:
return
(
'none'
,
''
)
if
attempt
.
created_at
<
cls
.
_earliest_allowed_date
():
return
(
'expired'
,
''
)
error_msg
=
attempt
.
error_msg
if
attempt
.
error_msg
:
error_msg
=
attempt
.
parse_error_msg
()
return
(
attempt
.
status
,
error_msg
)
def
parse_error_msg
(
self
):
"""
Sometimes, the error message we've received needs to be parsed into
something more human readable
The default behavior is to return the current error message as is.
"""
return
self
.
error_msg
@status_before_must_be
(
"created"
)
def
upload_face_image
(
self
,
img
):
raise
NotImplementedError
...
...
@@ -486,6 +524,37 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
self
.
status
=
"must_retry"
self
.
save
()
def
parse_error_msg
(
self
):
"""
Parse the error messages we receive from SoftwareSecure
Error messages are written in the form:
`[{"photoIdReasons": ["Not provided"]}]`
Returns a list of error messages
"""
# Translates the category names into something more human readable
category_dict
=
{
"photoIdReasons"
:
_
(
"Photo ID Issues: "
),
"generalReasons"
:
u""
}
try
:
msg_json
=
json
.
loads
(
self
.
error_msg
)
msg_dict
=
msg_json
[
0
]
msg
=
[]
for
category
in
msg_dict
:
# translate the category into a human-readable name
category_name
=
category_dict
[
category
]
msg
.
append
(
category_name
+
u", "
.
join
(
msg_dict
[
category
]))
return
u", "
.
join
(
msg
)
except
(
ValueError
,
KeyError
):
# if we can't parse the message as JSON or the category doesn't
# match one of our known categories, show a generic error
return
_
(
"There was an error verifying your ID photos."
)
def
image_url
(
self
,
name
):
"""
We dynamically generate this, since we want it the expiration clock to
...
...
lms/djangoapps/verify_student/tests/test_models.py
View file @
d3a4747b
...
...
@@ -17,11 +17,11 @@ from util.testing import UrlResetMixin
import
verify_student.models
FAKE_SETTINGS
=
{
"SOFTWARE_SECURE"
:
{
"SOFTWARE_SECURE"
:
{
"FACE_IMAGE_AES_KEY"
:
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
,
"API_ACCESS_KEY"
:
"BBBBBBBBBBBBBBBBBBBB"
,
"API_SECRET_KEY"
:
"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
,
"RSA_PUBLIC_KEY"
:
"""-----BEGIN PUBLIC KEY-----
"API_ACCESS_KEY"
:
"BBBBBBBBBBBBBBBBBBBB"
,
"API_SECRET_KEY"
:
"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
,
"RSA_PUBLIC_KEY"
:
"""-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu2fUn20ZQtDpa1TKeCA/
rDA2cEeFARjEr41AP6jqP/k3O7TeqFX6DgCBkxcjojRCs5IfE8TimBHtv/bcSx9o
7PANTq/62ZLM9xAMpfCcU6aAd4+CVqQkXSYjj5TUqamzDFBkp67US8IPmw7I2Gaa
...
...
@@ -30,10 +30,10 @@ dyZCM9pBcvcH+60ma+nNg8GVGBAW/oLxILBtg+T3PuXSUvcu/r6lUFMHk55pU94d
9A/T8ySJm379qU24ligMEetPk1o9CUasdaI96xfXVDyFhrzrntAmdD+HYCSPOQHz
iwIDAQAB
-----END PUBLIC KEY-----"""
,
"API_URL"
:
"http://localhost/verify_student/fake_endpoint"
,
"AWS_ACCESS_KEY"
:
"FAKEACCESSKEY"
,
"AWS_SECRET_KEY"
:
"FAKESECRETKEY"
,
"S3_BUCKET"
:
"fake-bucket"
"API_URL"
:
"http://localhost/verify_student/fake_endpoint"
,
"AWS_ACCESS_KEY"
:
"FAKEACCESSKEY"
,
"AWS_SECRET_KEY"
:
"FAKESECRETKEY"
,
"S3_BUCKET"
:
"fake-bucket"
}
}
...
...
@@ -57,11 +57,13 @@ class MockKey(object):
def
generate_url
(
self
,
duration
):
return
"http://fake-edx-s3.edx.org/"
class
MockBucket
(
object
):
"""Mocking a boto S3 Bucket object."""
def
__init__
(
self
,
name
):
self
.
name
=
name
class
MockS3Connection
(
object
):
"""Mocking a boto S3 Connection"""
def
__init__
(
self
,
access_key
,
secret_key
):
...
...
@@ -165,14 +167,14 @@ class TestPhotoVerification(TestCase):
# approved
assert_raises
(
VerificationException
,
attempt
.
submit
)
attempt
.
approve
()
# no-op
attempt
.
system_error
(
"System error"
)
# no-op, something processed it without error
attempt
.
approve
()
# no-op
attempt
.
system_error
(
"System error"
)
# no-op, something processed it without error
attempt
.
deny
(
DENY_ERROR_MSG
)
# denied
assert_raises
(
VerificationException
,
attempt
.
submit
)
attempt
.
deny
(
DENY_ERROR_MSG
)
# no-op
attempt
.
system_error
(
"System error"
)
# no-op, something processed it without error
attempt
.
deny
(
DENY_ERROR_MSG
)
# no-op
attempt
.
system_error
(
"System error"
)
# no-op, something processed it without error
attempt
.
approve
()
def
test_name_freezing
(
self
):
...
...
@@ -307,3 +309,49 @@ class TestPhotoVerification(TestCase):
attempt
.
save
()
assert_true
(
SoftwareSecurePhotoVerification
.
user_has_valid_or_pending
(
user
),
status
)
def
test_user_status
(
self
):
# test for correct status when no error returned
user
=
UserFactory
.
create
()
status
=
SoftwareSecurePhotoVerification
.
user_status
(
user
)
self
.
assertEquals
(
status
,
(
'none'
,
''
))
# test for when one has been created
attempt
=
SoftwareSecurePhotoVerification
(
user
=
user
)
attempt
.
status
=
'approved'
attempt
.
save
()
status
=
SoftwareSecurePhotoVerification
.
user_status
(
user
)
self
.
assertEquals
(
status
,
(
attempt
.
status
,
''
))
# create another one for the same user, make sure the right one is
# returned
attempt2
=
SoftwareSecurePhotoVerification
(
user
=
user
)
attempt2
.
status
=
'denied'
attempt2
.
error_msg
=
'[{"photoIdReasons": ["Not provided"]}]'
attempt2
.
save
()
status
=
SoftwareSecurePhotoVerification
.
user_status
(
user
)
self
.
assertEquals
(
status
,
(
attempt2
.
status
,
"Photo ID Issues: Not provided"
))
def
test_parse_error_msg_success
(
self
):
user
=
UserFactory
.
create
()
attempt
=
SoftwareSecurePhotoVerification
(
user
=
user
)
attempt
.
status
=
'denied'
attempt
.
error_msg
=
'[{"photoIdReasons": ["Not provided"]}]'
parsed_error_msg
=
attempt
.
parse_error_msg
()
self
.
assertEquals
(
"Photo ID Issues: Not provided"
,
parsed_error_msg
)
def
test_parse_error_msg_failure
(
self
):
user
=
UserFactory
.
create
()
attempt
=
SoftwareSecurePhotoVerification
(
user
=
user
)
attempt
.
status
=
'denied'
# when we can't parse into json
bad_messages
=
{
'Not Provided'
,
'[{"IdReasons": ["Not provided"]}]'
,
'{"IdReasons": ["Not provided"]}'
,
}
for
msg
in
bad_messages
:
attempt
.
error_msg
=
msg
parsed_error_msg
=
attempt
.
parse_error_msg
()
self
.
assertEquals
(
parsed_error_msg
,
"There was an error verifying your ID photos."
)
lms/djangoapps/verify_student/tests/test_views.py
View file @
d3a4747b
...
...
@@ -14,6 +14,7 @@ from mock import patch, Mock, ANY
from
django.test
import
TestCase
from
django.test.utils
import
override_settings
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
django.core.exceptions
import
ObjectDoesNotExist
...
...
@@ -98,6 +99,7 @@ class TestReverifyView(TestCase):
self
.
assertIn
(
'photo_reverification'
,
template
)
self
.
assertTrue
(
context
[
'error'
])
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'
:
True
})
def
test_reverify_post_success
(
self
):
url
=
reverse
(
'verify_student_reverify'
)
response
=
self
.
client
.
post
(
url
,
{
'face_image'
:
','
,
...
...
lms/envs/acceptance.py
View file @
d3a4747b
...
...
@@ -98,6 +98,9 @@ MITX_FEATURES['ENABLE_PAYMENT_FAKE'] = True
MITX_FEATURES
[
'ENABLE_INSTRUCTOR_EMAIL'
]
=
True
MITX_FEATURES
[
'REQUIRE_COURSE_EMAIL_AUTH'
]
=
False
# Don't actually send any requests to Software Secure for student identity
# verification.
MITX_FEATURES
[
'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'
]
=
True
# Configure the payment processor to use the fake processing page
# Since both the fake payment page and the shoppingcart app are using
...
...
lms/envs/test.py
View file @
d3a4747b
...
...
@@ -36,10 +36,6 @@ MITX_FEATURES['ENABLE_INSTRUCTOR_BETA_DASHBOARD'] = True
MITX_FEATURES
[
'ENABLE_SHOPPING_CART'
]
=
True
# Don't actually send any requests to Software Secure for student identity
# verification.
MITX_FEATURES
[
'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'
]
=
True
# Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it.
WIKI_ENABLED
=
True
...
...
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