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 (
...
@@ -47,6 +47,8 @@ from student.models import (
)
)
from
student.forms
import
PasswordResetFormNoActive
from
student.forms
import
PasswordResetFormNoActive
from
verify_student.models
import
SoftwareSecurePhotoVerification
from
certificates.models
import
CertificateStatuses
,
certificate_status_for_student
from
certificates.models
import
CertificateStatuses
,
certificate_status_for_student
from
xmodule.course_module
import
CourseDescriptor
from
xmodule.course_module
import
CourseDescriptor
...
@@ -334,6 +336,8 @@ def dashboard(request):
...
@@ -334,6 +336,8 @@ def dashboard(request):
CourseAuthorization
.
instructor_email_enabled
(
course
.
id
)
CourseAuthorization
.
instructor_email_enabled
(
course
.
id
)
)
)
)
)
# Verification Attempts
verification_status
=
SoftwareSecurePhotoVerification
.
user_status
(
user
)
# get info w.r.t ExternalAuthMap
# get info w.r.t ExternalAuthMap
external_auth_map
=
None
external_auth_map
=
None
try
:
try
:
...
@@ -351,6 +355,8 @@ def dashboard(request):
...
@@ -351,6 +355,8 @@ def dashboard(request):
'all_course_modes'
:
course_modes
,
'all_course_modes'
:
course_modes
,
'cert_statuses'
:
cert_statuses
,
'cert_statuses'
:
cert_statuses
,
'show_email_settings_for'
:
show_email_settings_for
,
'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
)
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
...
@@ -26,6 +26,7 @@ from django.conf import settings
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
django.db
import
models
from
django.db
import
models
from
django.contrib.auth.models
import
User
from
django.contrib.auth.models
import
User
from
django.utils.translation
import
ugettext
as
_
from
model_utils.models
import
StatusModel
from
model_utils.models
import
StatusModel
from
model_utils
import
Choices
from
model_utils
import
Choices
...
@@ -175,20 +176,28 @@ class PhotoVerification(StatusModel):
...
@@ -175,20 +176,28 @@ class PhotoVerification(StatusModel):
##### Methods listed in the order you'd typically call them
##### Methods listed in the order you'd typically call them
@classmethod
@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
):
def
user_is_verified
(
cls
,
user
,
earliest_allowed_date
=
None
):
"""
"""
Return whether or not a user has satisfactorily proved their
Return whether or not a user has satisfactorily proved their
identity. Depending on the policy, this can expire after some period of
identity. Depending on the policy, this can expire after some period of
time, so a user might have to renew periodically.
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
(
return
cls
.
objects
.
filter
(
user
=
user
,
user
=
user
,
status
=
"approved"
,
status
=
"approved"
,
created_at__gte
=
earliest_allowed_date
created_at__gte
=
(
earliest_allowed_date
or
cls
.
_earliest_allowed_date
())
)
.
exists
()
)
.
exists
()
@classmethod
@classmethod
...
@@ -201,14 +210,11 @@ class PhotoVerification(StatusModel):
...
@@ -201,14 +210,11 @@ class PhotoVerification(StatusModel):
on the contents of the attempt, and we have not yet received a denial.
on the contents of the attempt, and we have not yet received a denial.
"""
"""
valid_statuses
=
[
'must_retry'
,
'submitted'
,
'approved'
]
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
(
return
cls
.
objects
.
filter
(
user
=
user
,
user
=
user
,
status__in
=
valid_statuses
,
status__in
=
valid_statuses
,
created_at__gte
=
earliest_allowed_date
created_at__gte
=
(
earliest_allowed_date
or
cls
.
_earliest_allowed_date
())
)
.
exists
()
)
.
exists
()
@classmethod
@classmethod
...
@@ -225,6 +231,38 @@ class PhotoVerification(StatusModel):
...
@@ -225,6 +231,38 @@ class PhotoVerification(StatusModel):
else
:
else
:
return
None
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"
)
@status_before_must_be
(
"created"
)
def
upload_face_image
(
self
,
img
):
def
upload_face_image
(
self
,
img
):
raise
NotImplementedError
raise
NotImplementedError
...
@@ -486,6 +524,37 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
...
@@ -486,6 +524,37 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
self
.
status
=
"must_retry"
self
.
status
=
"must_retry"
self
.
save
()
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
):
def
image_url
(
self
,
name
):
"""
"""
We dynamically generate this, since we want it the expiration clock to
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
...
@@ -17,11 +17,11 @@ from util.testing import UrlResetMixin
import
verify_student.models
import
verify_student.models
FAKE_SETTINGS
=
{
FAKE_SETTINGS
=
{
"SOFTWARE_SECURE"
:
{
"SOFTWARE_SECURE"
:
{
"FACE_IMAGE_AES_KEY"
:
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
,
"FACE_IMAGE_AES_KEY"
:
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
,
"API_ACCESS_KEY"
:
"BBBBBBBBBBBBBBBBBBBB"
,
"API_ACCESS_KEY"
:
"BBBBBBBBBBBBBBBBBBBB"
,
"API_SECRET_KEY"
:
"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
,
"API_SECRET_KEY"
:
"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
,
"RSA_PUBLIC_KEY"
:
"""-----BEGIN PUBLIC KEY-----
"RSA_PUBLIC_KEY"
:
"""-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu2fUn20ZQtDpa1TKeCA/
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu2fUn20ZQtDpa1TKeCA/
rDA2cEeFARjEr41AP6jqP/k3O7TeqFX6DgCBkxcjojRCs5IfE8TimBHtv/bcSx9o
rDA2cEeFARjEr41AP6jqP/k3O7TeqFX6DgCBkxcjojRCs5IfE8TimBHtv/bcSx9o
7PANTq/62ZLM9xAMpfCcU6aAd4+CVqQkXSYjj5TUqamzDFBkp67US8IPmw7I2Gaa
7PANTq/62ZLM9xAMpfCcU6aAd4+CVqQkXSYjj5TUqamzDFBkp67US8IPmw7I2Gaa
...
@@ -30,10 +30,10 @@ dyZCM9pBcvcH+60ma+nNg8GVGBAW/oLxILBtg+T3PuXSUvcu/r6lUFMHk55pU94d
...
@@ -30,10 +30,10 @@ dyZCM9pBcvcH+60ma+nNg8GVGBAW/oLxILBtg+T3PuXSUvcu/r6lUFMHk55pU94d
9A/T8ySJm379qU24ligMEetPk1o9CUasdaI96xfXVDyFhrzrntAmdD+HYCSPOQHz
9A/T8ySJm379qU24ligMEetPk1o9CUasdaI96xfXVDyFhrzrntAmdD+HYCSPOQHz
iwIDAQAB
iwIDAQAB
-----END PUBLIC KEY-----"""
,
-----END PUBLIC KEY-----"""
,
"API_URL"
:
"http://localhost/verify_student/fake_endpoint"
,
"API_URL"
:
"http://localhost/verify_student/fake_endpoint"
,
"AWS_ACCESS_KEY"
:
"FAKEACCESSKEY"
,
"AWS_ACCESS_KEY"
:
"FAKEACCESSKEY"
,
"AWS_SECRET_KEY"
:
"FAKESECRETKEY"
,
"AWS_SECRET_KEY"
:
"FAKESECRETKEY"
,
"S3_BUCKET"
:
"fake-bucket"
"S3_BUCKET"
:
"fake-bucket"
}
}
}
}
...
@@ -57,11 +57,13 @@ class MockKey(object):
...
@@ -57,11 +57,13 @@ class MockKey(object):
def
generate_url
(
self
,
duration
):
def
generate_url
(
self
,
duration
):
return
"http://fake-edx-s3.edx.org/"
return
"http://fake-edx-s3.edx.org/"
class
MockBucket
(
object
):
class
MockBucket
(
object
):
"""Mocking a boto S3 Bucket object."""
"""Mocking a boto S3 Bucket object."""
def
__init__
(
self
,
name
):
def
__init__
(
self
,
name
):
self
.
name
=
name
self
.
name
=
name
class
MockS3Connection
(
object
):
class
MockS3Connection
(
object
):
"""Mocking a boto S3 Connection"""
"""Mocking a boto S3 Connection"""
def
__init__
(
self
,
access_key
,
secret_key
):
def
__init__
(
self
,
access_key
,
secret_key
):
...
@@ -165,14 +167,14 @@ class TestPhotoVerification(TestCase):
...
@@ -165,14 +167,14 @@ class TestPhotoVerification(TestCase):
# approved
# approved
assert_raises
(
VerificationException
,
attempt
.
submit
)
assert_raises
(
VerificationException
,
attempt
.
submit
)
attempt
.
approve
()
# no-op
attempt
.
approve
()
# no-op
attempt
.
system_error
(
"System error"
)
# no-op, something processed it without error
attempt
.
system_error
(
"System error"
)
# no-op, something processed it without error
attempt
.
deny
(
DENY_ERROR_MSG
)
attempt
.
deny
(
DENY_ERROR_MSG
)
# denied
# denied
assert_raises
(
VerificationException
,
attempt
.
submit
)
assert_raises
(
VerificationException
,
attempt
.
submit
)
attempt
.
deny
(
DENY_ERROR_MSG
)
# no-op
attempt
.
deny
(
DENY_ERROR_MSG
)
# no-op
attempt
.
system_error
(
"System error"
)
# no-op, something processed it without error
attempt
.
system_error
(
"System error"
)
# no-op, something processed it without error
attempt
.
approve
()
attempt
.
approve
()
def
test_name_freezing
(
self
):
def
test_name_freezing
(
self
):
...
@@ -307,3 +309,49 @@ class TestPhotoVerification(TestCase):
...
@@ -307,3 +309,49 @@ class TestPhotoVerification(TestCase):
attempt
.
save
()
attempt
.
save
()
assert_true
(
SoftwareSecurePhotoVerification
.
user_has_valid_or_pending
(
user
),
status
)
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
...
@@ -14,6 +14,7 @@ from mock import patch, Mock, ANY
from
django.test
import
TestCase
from
django.test
import
TestCase
from
django.test.utils
import
override_settings
from
django.test.utils
import
override_settings
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
django.core.exceptions
import
ObjectDoesNotExist
from
django.core.exceptions
import
ObjectDoesNotExist
...
@@ -98,6 +99,7 @@ class TestReverifyView(TestCase):
...
@@ -98,6 +99,7 @@ class TestReverifyView(TestCase):
self
.
assertIn
(
'photo_reverification'
,
template
)
self
.
assertIn
(
'photo_reverification'
,
template
)
self
.
assertTrue
(
context
[
'error'
])
self
.
assertTrue
(
context
[
'error'
])
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'
:
True
})
def
test_reverify_post_success
(
self
):
def
test_reverify_post_success
(
self
):
url
=
reverse
(
'verify_student_reverify'
)
url
=
reverse
(
'verify_student_reverify'
)
response
=
self
.
client
.
post
(
url
,
{
'face_image'
:
','
,
response
=
self
.
client
.
post
(
url
,
{
'face_image'
:
','
,
...
...
lms/envs/acceptance.py
View file @
d3a4747b
...
@@ -98,6 +98,9 @@ MITX_FEATURES['ENABLE_PAYMENT_FAKE'] = True
...
@@ -98,6 +98,9 @@ MITX_FEATURES['ENABLE_PAYMENT_FAKE'] = True
MITX_FEATURES
[
'ENABLE_INSTRUCTOR_EMAIL'
]
=
True
MITX_FEATURES
[
'ENABLE_INSTRUCTOR_EMAIL'
]
=
True
MITX_FEATURES
[
'REQUIRE_COURSE_EMAIL_AUTH'
]
=
False
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
# Configure the payment processor to use the fake processing page
# Since both the fake payment page and the shoppingcart app are using
# 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
...
@@ -36,10 +36,6 @@ MITX_FEATURES['ENABLE_INSTRUCTOR_BETA_DASHBOARD'] = True
MITX_FEATURES
[
'ENABLE_SHOPPING_CART'
]
=
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.
# Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it.
WIKI_ENABLED
=
True
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