Adds retry logic for software secure

This commit is contained in:
Awais Jibran
2020-04-08 11:47:22 +05:00
parent 20fe068a01
commit 804fac9358
16 changed files with 486 additions and 280 deletions

View File

@@ -1899,6 +1899,12 @@ derived('HELP_TOKENS_LANGUAGE_CODE', 'HELP_TOKENS_VERSION')
RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS = 5
RETRY_ACTIVATION_EMAIL_TIMEOUT = 0.5
# Software Secure request retry settings
# Time in seconds before a retry of the task should be 60 mints.
SOFTWARE_SECURE_REQUEST_RETRY_DELAY = 60 * 60
# Maximum of 6 retries before giving up.
SOFTWARE_SECURE_RETRY_MAX_ATTEMPTS = 6
############## DJANGO-USER-TASKS ##############
# How long until database records about the outcome of a task and its artifacts get deleted?
@@ -1951,6 +1957,8 @@ RECALCULATE_GRADES_ROUTING_KEY = DEFAULT_PRIORITY_QUEUE
# Queue to use for updating grades due to grading policy change
POLICY_CHANGE_GRADES_ROUTING_KEY = 'edx.lms.core.default'
SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY = 'edx.lms.core.default'
# Rate limit for regrading tasks that a grading policy change can kick off
POLICY_CHANGE_TASK_RATE_LIMIT = '300/h'

View File

@@ -504,6 +504,11 @@ POLICY_CHANGE_GRADES_ROUTING_KEY = ENV_TOKENS.get('POLICY_CHANGE_GRADES_ROUTING_
# Rate limit for regrading tasks that a grading policy change can kick off
POLICY_CHANGE_TASK_RATE_LIMIT = ENV_TOKENS.get('POLICY_CHANGE_TASK_RATE_LIMIT', POLICY_CHANGE_TASK_RATE_LIMIT)
SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY = ENV_TOKENS.get(
'SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY',
HIGH_PRIORITY_QUEUE
)
# Event tracking
TRACKING_BACKENDS.update(AUTH_TOKENS.get("TRACKING_BACKENDS", {}))
EVENT_TRACKING_BACKENDS['tracking_logs']['OPTIONS']['backends'].update(AUTH_TOKENS.get("EVENT_TRACKING_BACKENDS", {}))

View File

@@ -32,7 +32,7 @@ from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from lms.djangoapps.certificates.models import CertificateStatuses # pylint: disable=import-error
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory # pylint: disable=import-error
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from lms.djangoapps.verify_student.tests import TestVerificationBase
from openedx.core.djangoapps.catalog.tests.factories import CourseFactory as CatalogCourseFactory
from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory, ProgramFactory, generate_course_run_key
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
@@ -291,7 +291,7 @@ class CourseEndingTest(TestCase):
@ddt.ddt
class DashboardTest(ModuleStoreTestCase):
class DashboardTest(ModuleStoreTestCase, TestVerificationBase):
"""
Tests for dashboard utility functions
"""
@@ -314,9 +314,7 @@ class DashboardTest(ModuleStoreTestCase):
if mode == 'verified':
# Simulate a successful verification attempt
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready()
attempt.submit()
attempt = self.create_and_submit_attempt_for_user(self.user)
attempt.approve()
response = self.client.get(reverse('dashboard'))
@@ -351,9 +349,7 @@ class DashboardTest(ModuleStoreTestCase):
if mode == 'verified':
# Simulate a successful verification attempt
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready()
attempt.submit()
attempt = self.create_and_submit_attempt_for_user(self.user)
attempt.approve()
response = self.client.get(reverse('dashboard'))

View File

@@ -4,17 +4,14 @@ Tests for django admin commands in the verify_student module
Lots of imports from verify_student's model tests, since they cover similar ground
"""
import boto
from django.conf import settings
from django.core.management import call_command
from django.core.management.base import CommandError
from django.test import TestCase
from mock import patch
from testfixtures import LogCapture
from common.test.utils import MockS3BotoMixin
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, SSPVerificationRetryConfig
from lms.djangoapps.verify_student.tests import TestVerificationBase
from lms.djangoapps.verify_student.tests.test_models import (
FAKE_SETTINGS,
mock_software_secure_post,
@@ -28,22 +25,10 @@ LOGGER_NAME = 'retry_photo_verification'
# Lots of patching to stub in our own settings, and HTTP posting
@patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS)
@patch('lms.djangoapps.verify_student.models.requests.post', new=mock_software_secure_post)
class TestVerifyStudentCommand(MockS3BotoMixin, TestCase):
class TestVerifyStudentCommand(MockS3BotoMixin, TestVerificationBase):
"""
Tests for django admin commands in the verify_student module
"""
def create_and_submit(self, username):
"""
Helper method that lets us create new SoftwareSecurePhotoVerifications
"""
user = UserFactory.create()
attempt = SoftwareSecurePhotoVerification(user=user)
user.profile.name = username
attempt.upload_face_image("Fake Data")
attempt.upload_photo_id_image("More Fake Data")
attempt.mark_ready()
attempt.submit()
return attempt
def test_retry_failed_photo_verifications(self):
"""
@@ -51,55 +36,58 @@ class TestVerifyStudentCommand(MockS3BotoMixin, TestCase):
and re-submit them executes successfully
"""
# set up some fake data to use...
self.create_and_submit("SuccessfulSally")
self.create_upload_and_submit_attempt_for_user()
with patch('lms.djangoapps.verify_student.models.requests.post', new=mock_software_secure_post_error):
self.create_and_submit("RetryRoger")
with patch('lms.djangoapps.verify_student.models.requests.post', new=mock_software_secure_post_error):
self.create_and_submit("RetryRick")
self.create_upload_and_submit_attempt_for_user()
self.create_upload_and_submit_attempt_for_user()
# check to make sure we had two successes and two failures; otherwise we've got problems elsewhere
assert len(SoftwareSecurePhotoVerification.objects.filter(status="submitted")) == 1
assert len(SoftwareSecurePhotoVerification.objects.filter(status='must_retry')) == 2
call_command('retry_failed_photo_verifications')
self.assertEqual(SoftwareSecurePhotoVerification.objects.filter(status="submitted").count(), 1)
self.assertEqual(SoftwareSecurePhotoVerification.objects.filter(status='must_retry').count(), 2)
with self.immediate_on_commit():
call_command('retry_failed_photo_verifications')
attempts_to_retry = SoftwareSecurePhotoVerification.objects.filter(status='must_retry')
assert not attempts_to_retry
def test_args_from_database(self):
"""Test management command arguments injected from config model."""
# Nothing in the database, should default to disabled
with LogCapture(LOGGER_NAME) as log:
call_command('retry_failed_photo_verifications', '--args-from-database')
log.check_present(
(
LOGGER_NAME, 'WARNING',
u"SSPVerificationRetryConfig is disabled or empty, but --args-from-database was requested."
),
)
# Add a config
def add_test_config_for_retry_verification(self):
"""Setups verification retry configuration."""
config = SSPVerificationRetryConfig.current()
config.arguments = '--verification-ids 1 2 3'
config.enabled = True
config.save()
def test_args_from_database(self):
"""Test management command arguments injected from config model."""
# Nothing in the database, should default to disabled
with LogCapture(LOGGER_NAME) as log:
call_command('retry_failed_photo_verifications', '--args-from-database')
log.check_present(
(
LOGGER_NAME, 'WARNING',
'SSPVerificationRetryConfig is disabled or empty, but --args-from-database was requested.'
),
)
self.add_test_config_for_retry_verification()
with patch('lms.djangoapps.verify_student.models.requests.post', new=mock_software_secure_post_error):
self.create_and_submit("RetryRoger")
self.create_upload_and_submit_attempt_for_user()
with LogCapture(LOGGER_NAME) as log:
call_command('retry_failed_photo_verifications')
with self.immediate_on_commit():
call_command('retry_failed_photo_verifications')
log.check_present(
(
LOGGER_NAME, 'INFO',
'Attempting to retry {0} failed PhotoVerification submissions'.format(1)
),
)
with LogCapture(LOGGER_NAME) as log:
with self.immediate_on_commit():
call_command('retry_failed_photo_verifications', '--args-from-database')
log.check_present(
(
LOGGER_NAME, 'INFO',
u"Attempting to retry {0} failed PhotoVerification submissions".format(1)
'Fetching retry verification ids from config model'
),
)
with LogCapture(LOGGER_NAME) as log:
call_command('retry_failed_photo_verifications', '--args-from-database')
log.check_present(
(
LOGGER_NAME, 'INFO',
u"Fetching retry verification ids from config model"
),
)

View File

@@ -9,7 +9,6 @@ of a student over a period of time. Right now, the only models are the abstract
photo verification process as generic as possible.
"""
import base64
import codecs
import functools
@@ -21,13 +20,12 @@ from datetime import timedelta
from email.utils import formatdate
import requests
import simplejson
import six
from config_models.models import ConfigurationModel
from django.conf import settings
from django.contrib.auth.models import User
from django.core.files.base import ContentFile
from django.db import models
from django.db import models, transaction
from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import cached_property
@@ -46,7 +44,7 @@ from lms.djangoapps.verify_student.ssencrypt import (
from openedx.core.djangoapps.signals.signals import LEARNER_NOW_VERIFIED
from openedx.core.storage import get_storage
from .utils import earliest_allowed_verification_date
from .utils import auto_verify_for_testing_enabled, earliest_allowed_verification_date
log = logging.getLogger(__name__)
@@ -393,7 +391,7 @@ class PhotoVerification(IDVerificationAttempt):
# student dashboard. But at this point, we lock the value into the
# attempt.
self.name = self.user.profile.name
self.status = "ready"
self.status = self.STATUS.ready
self.save()
@status_before_must_be("must_retry", "submitted", "approved", "denied")
@@ -426,7 +424,7 @@ class PhotoVerification(IDVerificationAttempt):
logs. This should be a relatively rare occurrence.
"""
# If someone approves an outdated version of this, the first one wins
if self.status == "approved":
if self.status == self.STATUS.approved:
return
log.info(u"Verification for user '{user_id}' approved by '{reviewer}'.".format(
@@ -436,7 +434,7 @@ class PhotoVerification(IDVerificationAttempt):
self.error_code = "" # reset, in case this attempt was denied before
self.reviewing_user = user_id
self.reviewing_service = service
self.status = "approved"
self.status = self.STATUS.approved
self.save()
# Emit signal to find and generate eligible certificates
LEARNER_NOW_VERIFIED.send_robust(
@@ -447,6 +445,51 @@ class PhotoVerification(IDVerificationAttempt):
message = u'LEARNER_NOW_VERIFIED signal fired for {user} from PhotoVerification'
log.info(message.format(user=self.user.username))
@status_before_must_be("ready", "must_retry")
def mark_submit(self):
"""
Submit this attempt.
Valid attempt statuses when calling this method:
`ready`, `must_retry`
Status after method completes: `submitted`
State Transitions:
→ → → `must_retry`
↑ ↑ ↓
`ready` → `submitted`
"""
self.submitted_at = now()
self.status = self.STATUS.submitted
self.save()
@status_before_must_be("ready", "must_retry", "submitted")
def mark_must_retry(self, error=""):
"""
Set the attempt status to `must_retry`.
Mark that this attempt could not be completed because of a system error.
Status should be moved to `must_retry`. For example, if Software Secure
service is down and we couldn't process the request even after retrying.
Valid attempt statuses when calling this method:
`ready`, `submitted`
Status after method completes: `must_retry`
State Transitions:
→ → → `must_retry`
↑ ↑ ↓
`ready` → `submitted`
"""
if self.status == self.STATUS.must_retry:
return
self.status = self.STATUS.must_retry
self.error_msg = error
self.save()
@status_before_must_be("must_retry", "submitted", "approved", "denied")
def deny(self,
error_msg,
@@ -490,7 +533,7 @@ class PhotoVerification(IDVerificationAttempt):
self.error_code = error_code
self.reviewing_user = reviewing_user
self.reviewing_service = reviewing_service
self.status = "denied"
self.status = self.STATUS.denied
self.save()
@status_before_must_be("must_retry", "submitted", "approved", "denied")
@@ -505,14 +548,14 @@ class PhotoVerification(IDVerificationAttempt):
reported to us that they couldn't process our submission because they
couldn't decrypt the image we sent.
"""
if self.status in ["approved", "denied"]:
if self.status in [self.STATUS.approved, self.STATUS.denied]:
return # If we were already approved or denied, just leave it.
self.error_msg = error_msg
self.error_code = error_code
self.reviewing_user = reviewing_user
self.reviewing_service = reviewing_service
self.status = "must_retry"
self.status = self.STATUS.must_retry
self.save()
@classmethod
@@ -641,7 +684,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
# developing and aren't interested in working on student identity
# verification functionality. If you do want to work on it, you have to
# explicitly enable these in your private settings.
if settings.FEATURES.get('AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'):
if auto_verify_for_testing_enabled():
return
aes_key_str = settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["FACE_IMAGE_AES_KEY"]
@@ -671,7 +714,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
# developing and aren't interested in working on student identity
# verification functionality. If you do want to work on it, you have to
# explicitly enable these in your private settings.
if settings.FEATURES.get('AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'):
if auto_verify_for_testing_enabled():
# fake photo id key is set only for initial verification
self.photo_id_key = 'fake-photo-id-key'
self.save()
@@ -703,27 +746,28 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
Keyword Arguments:
copy_id_photo_from (SoftwareSecurePhotoVerification): If provided, re-send the ID photo
data from this attempt. This is used for reverification, in which new face photos
data from this attempt. This is used for re-verification, in which new face photos
are sent with previously-submitted ID photos.
"""
try:
response = self.send_request(copy_id_photo_from=copy_id_photo_from)
if response.ok:
self.submitted_at = now()
self.status = "submitted"
self.save()
else:
self.status = "must_retry"
self.error_msg = response.text
self.save()
except Exception: # pylint: disable=broad-except
log.exception(
u'Software Secure submission failed for user %s, setting status to must_retry',
self.user.username
from .tasks import send_request_to_ss_for_user
if auto_verify_for_testing_enabled():
self.mark_submit()
fake_response = requests.Response()
fake_response.status_code = 200
return fake_response
if copy_id_photo_from is not None:
log.info(
('Software Secure attempt for user: %r and receipt ID: %r used the same photo ID data as the '
'receipt with ID %r.'),
self.user.username,
self.receipt_id,
copy_id_photo_from.receipt_id,
)
self.status = "must_retry"
self.save()
transaction.on_commit(
lambda:
send_request_to_ss_for_user.delay(user_verification_id=self.id, copy_id_photo_from=copy_id_photo_from)
)
def parsed_error_msg(self):
"""
@@ -766,7 +810,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
parsed_errors.append(parsed_error)
else:
log.debug(u'Ignoring photo verification error message: %s', message)
except Exception: # pylint: disable=broad-except
except Exception: # pylint: disable=broad-except
log.exception(u'Failed to parse error message for SoftwareSecurePhotoVerification %d', self.pk)
return parsed_errors
@@ -845,7 +889,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
Keyword Arguments:
copy_id_photo_from (SoftwareSecurePhotoVerification): If provided, re-send the ID photo
data from this attempt. This is used for reverification, in which new face photos
data from this attempt. This is used for re-verification, in which new face photos
are sent with previously-submitted ID photos.
Returns:
@@ -911,55 +955,6 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
return header_txt + "\n\n" + body_txt
def send_request(self, copy_id_photo_from=None):
"""
Assembles a submission to Software Secure and sends it via HTTPS.
Keyword Arguments:
copy_id_photo_from (SoftwareSecurePhotoVerification): If provided, re-send the ID photo
data from this attempt. This is used for reverification, in which new face photos
are sent with previously-submitted ID photos.
Returns:
request.Response
"""
# If AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING is True, we want to
# skip posting anything to Software Secure. We actually don't even
# create the message because that would require encryption and message
# signing that rely on settings.VERIFY_STUDENT values that aren't set
# in dev. So we just pretend like we successfully posted
if settings.FEATURES.get('AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'):
fake_response = requests.Response()
fake_response.status_code = 200
return fake_response
headers, body = self.create_request(copy_id_photo_from=copy_id_photo_from)
response = requests.post(
settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["API_URL"],
headers=headers,
data=simplejson.dumps(body, indent=2, sort_keys=True, ensure_ascii=False).encode('utf-8'),
verify=False
)
log.info(u"Sent request to Software Secure for receipt ID %s.", self.receipt_id)
if copy_id_photo_from is not None:
log.info(
(
u"Software Secure attempt with receipt ID %s used the same photo ID "
u"data as the receipt with ID %s"
),
self.receipt_id, copy_id_photo_from.receipt_id
)
log.debug("Headers:\n{}\n\n".format(headers))
log.debug("Body:\n{}\n\n".format(body))
log.debug(u"Return code: {}".format(response.status_code))
log.debug(u"Return message:\n\n{}\n\n".format(response.text))
return response
def should_display_status_to_user(self):
"""Whether or not the status from this attempt should be displayed to the user."""
return True

View File

@@ -2,7 +2,6 @@
Implementation of abstraction layer for other parts of the system to make queries related to ID Verification.
"""
import logging
from itertools import chain
@@ -148,9 +147,11 @@ class IDVerificationService(object):
'created_at__gte': earliest_allowed_verification_date()
}
return (SoftwareSecurePhotoVerification.objects.filter(**filter_kwargs).exists() or
SSOVerification.objects.filter(**filter_kwargs).exists() or
ManualVerification.objects.filter(**filter_kwargs).exists())
return (
SoftwareSecurePhotoVerification.objects.filter(**filter_kwargs).exists() or
SSOVerification.objects.filter(**filter_kwargs).exists() or
ManualVerification.objects.filter(**filter_kwargs).exists()
)
@classmethod
def user_status(cls, user):

View File

@@ -2,20 +2,70 @@
Django Celery tasks for service status app
"""
import logging
from smtplib import SMTPException
from celery import task
import requests
import simplejson
from celery import Task, task
from celery.states import FAILURE
from django.conf import settings
from django.core.mail import EmailMessage
from edxmako.shortcuts import render_to_string
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
ACE_ROUTING_KEY = getattr(settings, 'ACE_ROUTING_KEY', None)
SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY = getattr(settings, 'SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY', None)
log = logging.getLogger(__name__)
class BaseSoftwareSecureTask(Task):
"""
Base task class for use with Software Secure request.
Permits updating information about user attempt in correspondence to submitting
request to software secure.
"""
abstract = True
def on_success(self, response, task_id, args, kwargs):
"""
Update SoftwareSecurePhotoVerification object corresponding to this
task with info about success.
Updates user verification attempt to "submitted" if the response was ok otherwise
set it to "must_retry".
"""
user_verification_id = kwargs['user_verification_id']
user_verification = SoftwareSecurePhotoVerification.objects.get(id=user_verification_id)
if response.ok:
user_verification.mark_submit()
log.info(
'Sent request to Software Secure for user: %r and receipt ID %r.',
user_verification.user.username,
user_verification.receipt_id,
)
return user_verification
user_verification.mark_must_retry(response.text)
def after_return(self, status, retval, task_id, args, kwargs, einfo):
"""
If max retries have reached and task status is still failing, mark user submission
with "must_retry" so that it can be retried latter.
"""
if self.max_retries == self.request.retries and status == FAILURE:
user_verification_id = kwargs['user_verification_id']
user_verification = SoftwareSecurePhotoVerification.objects.get(id=user_verification_id)
user_verification.mark_must_retry()
log.error(
'Software Secure submission failed for user %r, setting status to must_retry',
user_verification.user.username,
exc_info=True
)
@task(routing_key=ACE_ROUTING_KEY)
def send_verification_status_email(context):
"""
@@ -35,3 +85,48 @@ def send_verification_status_email(context):
msg.send(fail_silently=False)
except SMTPException:
log.warning(u"Failure in sending verification status e-mail to %s", dest_addr)
@task(
base=BaseSoftwareSecureTask,
bind=True,
default_retry_delay=settings.SOFTWARE_SECURE_REQUEST_RETRY_DELAY,
max_retries=settings.SOFTWARE_SECURE_RETRY_MAX_ATTEMPTS,
routing_key=SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY,
)
def send_request_to_ss_for_user(self, user_verification_id, copy_id_photo_from):
"""
Assembles a submission to Software Secure.
Keyword Arguments:
user_verification_id (int) SoftwareSecurePhotoVerification model object identifier.
copy_id_photo_from (SoftwareSecurePhotoVerification): If provided, re-send the ID photo
data from this attempt. This is used for re-verification, in which new face photos
are sent with previously-submitted ID photos.
Returns:
request.Response
"""
log.info('=>New Verification Task Received') # todo -- remove before merge.
user_verification = SoftwareSecurePhotoVerification.objects.get(id=user_verification_id)
try:
headers, body = user_verification.create_request(copy_id_photo_from)
response = requests.post(
settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["API_URL"],
headers=headers,
data=simplejson.dumps(body, indent=2, sort_keys=True, ensure_ascii=False).encode('utf-8'),
verify=False
)
return response
except Exception as exc: # pylint: disable=bare-except
log.error(
(
'Retrying sending request to Software Secure for user: %r, Receipt ID: %r '
'attempt#: %s of %s'
),
user_verification.user.username,
user_verification.receipt_id,
self.request.retries,
settings.SOFTWARE_SECURE_RETRY_MAX_ATTEMPTS,
)
log.error(str(exc))
self.retry()

View File

@@ -0,0 +1,92 @@
from contextlib import contextmanager
from datetime import timedelta
from unittest import mock
from django.conf import settings
from django.db import DEFAULT_DB_ALIAS
from django.test import TestCase
from django.utils.timezone import now
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from student.tests.factories import UserFactory
class TestVerificationBase(TestCase):
"""
Common tests across all types of Verifications (e.g., SoftwareSecurePhotoVerification, SSOVerification)
"""
@contextmanager
def immediate_on_commit(self, using=None):
"""
Context manager executing transaction.on_commit() hooks immediately as
if the connection was in auto-commit mode. This is required when
using a subclass of django.test.TestCase as all tests are wrapped in
a transaction that never gets committed.
TODO: Remove when immediate_on_commit function is actually implemented
Django Ticket #: 30456, Link: https://code.djangoproject.com/ticket/30457#no1
"""
immediate_using = DEFAULT_DB_ALIAS if using is None else using
def on_commit(func, using=None):
using = DEFAULT_DB_ALIAS if using is None else using
if using == immediate_using:
func()
with mock.patch('django.db.transaction.on_commit', side_effect=on_commit) as patch:
yield patch
def verification_active_at_datetime(self, attempt):
"""
Tests to ensure the Verification is active or inactive at the appropriate datetimes.
"""
# Not active before the created date
before = attempt.created_at - timedelta(seconds=1)
self.assertFalse(attempt.active_at_datetime(before))
# Active immediately after created date
after_created = attempt.created_at + timedelta(seconds=1)
self.assertTrue(attempt.active_at_datetime(after_created))
# Active immediately before expiration date
expiration = attempt.created_at + timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
before_expiration = expiration - timedelta(seconds=1)
self.assertTrue(attempt.active_at_datetime(before_expiration))
# Not active after the expiration date
attempt.created_at = attempt.created_at - timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
attempt.save()
self.assertFalse(attempt.active_at_datetime(now() + timedelta(days=1)))
def submit_attempt(self, attempt):
with self.immediate_on_commit():
attempt.submit()
attempt.refresh_from_db()
return attempt
def create_and_submit_attempt_for_user(self, user=None):
"""
Create photo verification attempt without uploading photos
for a user.
"""
if not user:
user = UserFactory.create()
attempt = SoftwareSecurePhotoVerification.objects.create(user=user)
attempt.mark_ready()
return self.submit_attempt(attempt)
def create_upload_and_submit_attempt_for_user(self, user=None):
"""
Helper method to create a generic submission with photos for
a user and send it.
"""
if not user:
user = UserFactory.create()
attempt = SoftwareSecurePhotoVerification(user=user)
user.profile.name = u"Rust\u01B4"
attempt.upload_face_image("Just pretend this is image data")
attempt.upload_photo_id_image("Hey, we're a photo ID")
attempt.mark_ready()
return self.submit_attempt(attempt)

View File

@@ -1,30 +1,29 @@
# -*- coding: utf-8 -*-
import base64
import simplejson as json
from datetime import datetime, timedelta
import ddt
import mock
import requests.exceptions
import simplejson as json
from django.conf import settings
from django.test import TestCase
from django.utils.timezone import now
from freezegun import freeze_time
from mock import patch
from six.moves import range
from student.tests.factories import UserFactory
from testfixtures import LogCapture
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from common.test.utils import MockS3BotoMixin
from lms.djangoapps.verify_student.models import (
ManualVerification,
PhotoVerification,
SoftwareSecurePhotoVerification,
SSOVerification,
ManualVerification,
VerificationException,
VerificationException
)
from student.tests.factories import UserFactory
from verify_student.tests import TestVerificationBase
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
FAKE_SETTINGS = {
"SOFTWARE_SECURE": {
@@ -90,39 +89,11 @@ def mock_software_secure_post_unavailable(url, headers=None, data=None, **kwargs
raise requests.exceptions.ConnectionError
class TestVerification(TestCase):
"""
Common tests across all types of Verications (e.g., SoftwareSecurePhotoVerication, SSOVerification)
"""
def verification_active_at_datetime(self, attempt):
"""
Tests to ensure the Verification is active or inactive at the appropriate datetimes.
"""
# Not active before the created date
before = attempt.created_at - timedelta(seconds=1)
self.assertFalse(attempt.active_at_datetime(before))
# Active immediately after created date
after_created = attempt.created_at + timedelta(seconds=1)
self.assertTrue(attempt.active_at_datetime(after_created))
# Active immediately before expiration date
expiration = attempt.created_at + timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
before_expiration = expiration - timedelta(seconds=1)
self.assertTrue(attempt.active_at_datetime(before_expiration))
# Not active after the expiration date
attempt.created_at = attempt.created_at - timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
attempt.save()
self.assertFalse(attempt.active_at_datetime(now() + timedelta(days=1)))
# Lots of patching to stub in our own settings, and HTTP posting
@patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS)
@patch('lms.djangoapps.verify_student.models.requests.post', new=mock_software_secure_post)
@ddt.ddt
class TestPhotoVerification(TestVerification, MockS3BotoMixin, ModuleStoreTestCase):
class TestPhotoVerification(TestVerificationBase, MockS3BotoMixin, ModuleStoreTestCase):
def test_state_transitions(self):
"""
@@ -138,44 +109,59 @@ class TestPhotoVerification(TestVerification, MockS3BotoMixin, ModuleStoreTestCa
"""
user = UserFactory.create()
attempt = SoftwareSecurePhotoVerification(user=user)
self.assertEqual(attempt.status, "created")
self.assertEqual(attempt.status, PhotoVerification.STATUS.created)
# These should all fail because we're in the wrong starting state.
self.assertRaises(VerificationException, attempt.submit)
self.assertRaises(VerificationException, attempt.approve)
self.assertRaises(VerificationException, attempt.deny)
self.assertRaises(VerificationException, attempt.mark_must_retry)
self.assertRaises(VerificationException, attempt.mark_submit)
# Now let's fill in some values so that we can pass the mark_ready() call
attempt.mark_ready()
self.assertEqual(attempt.status, "ready")
self.assertEqual(attempt.status, PhotoVerification.STATUS.ready)
# ready (can't approve or deny unless it's "submitted")
self.assertRaises(VerificationException, attempt.approve)
self.assertRaises(VerificationException, attempt.deny)
attempt.mark_must_retry()
attempt.mark_submit()
DENY_ERROR_MSG = '[{"photoIdReasons": ["Not provided"]}]'
# must_retry
attempt.status = "must_retry"
attempt.status = PhotoVerification.STATUS.must_retry
attempt.system_error("System error")
attempt.mark_must_retry() # no-op
attempt.mark_submit()
attempt.approve()
attempt.status = "must_retry"
attempt.status = PhotoVerification.STATUS.must_retry
attempt.deny(DENY_ERROR_MSG)
# submitted
attempt.status = "submitted"
attempt.status = PhotoVerification.STATUS.submitted
attempt.deny(DENY_ERROR_MSG)
attempt.status = "submitted"
attempt.status = PhotoVerification.STATUS.submitted
attempt.mark_must_retry()
attempt.status = PhotoVerification.STATUS.submitted
attempt.approve()
# approved
self.assertRaises(VerificationException, attempt.submit)
self.assertRaises(VerificationException, attempt.mark_must_retry)
self.assertRaises(VerificationException, attempt.mark_submit)
attempt.approve() # no-op
attempt.system_error("System error") # no-op, something processed it without error
attempt.deny(DENY_ERROR_MSG)
# denied
self.assertRaises(VerificationException, attempt.submit)
self.assertRaises(VerificationException, attempt.mark_must_retry)
self.assertRaises(VerificationException, attempt.mark_submit)
attempt.deny(DENY_ERROR_MSG) # no-op
attempt.system_error("System error") # no-op, something processed it without error
attempt.approve()
@@ -198,39 +184,21 @@ class TestPhotoVerification(TestVerification, MockS3BotoMixin, ModuleStoreTestCa
self.assertEqual(u"Clyde \u01B4", attempt.name)
def create_and_submit(self):
"""Helper method to create a generic submission and send it."""
user = UserFactory.create()
attempt = SoftwareSecurePhotoVerification(user=user)
user.profile.name = u"Rust\u01B4"
attempt.upload_face_image("Just pretend this is image data")
attempt.upload_photo_id_image("Hey, we're a photo ID")
attempt.mark_ready()
attempt.submit()
return attempt
def test_submissions(self):
"""Test that we set our status correctly after a submission."""
# Basic case, things go well.
attempt = self.create_and_submit()
self.assertEqual(attempt.status, "submitted")
attempt = self.create_upload_and_submit_attempt_for_user()
self.assertEqual(attempt.status, PhotoVerification.STATUS.submitted)
# We post, but Software Secure doesn't like what we send for some reason
with patch('lms.djangoapps.verify_student.models.requests.post', new=mock_software_secure_post_error):
attempt = self.create_and_submit()
self.assertEqual(attempt.status, "must_retry")
with patch('lms.djangoapps.verify_student.tasks.requests.post', new=mock_software_secure_post_error):
attempt = self.create_upload_and_submit_attempt_for_user()
self.assertEqual(attempt.status, PhotoVerification.STATUS.must_retry)
# We try to post, but run into an error (in this case a network connection error)
with patch('lms.djangoapps.verify_student.models.requests.post', new=mock_software_secure_post_unavailable):
with LogCapture('lms.djangoapps.verify_student.models') as logger:
attempt = self.create_and_submit()
self.assertEqual(attempt.status, "must_retry")
logger.check(
('lms.djangoapps.verify_student.models', 'ERROR',
u'Software Secure submission failed for user %s, setting status to must_retry'
% attempt.user.username))
with patch('lms.djangoapps.verify_student.tasks.requests.post', new=mock_software_secure_post_unavailable):
attempt = self.create_upload_and_submit_attempt_for_user()
self.assertEqual(attempt.status, PhotoVerification.STATUS.must_retry)
@mock.patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
def test_submission_while_testing_flag_is_true(self):
@@ -238,21 +206,14 @@ class TestPhotoVerification(TestVerification, MockS3BotoMixin, ModuleStoreTestCa
initial verification when the feature flag 'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'
is enabled.
"""
user = UserFactory.create()
attempt = SoftwareSecurePhotoVerification(user=user)
user.profile.name = "test-user"
attempt.upload_photo_id_image("Image data")
attempt.mark_ready()
attempt.submit()
attempt = self.create_upload_and_submit_attempt_for_user()
self.assertEqual(attempt.photo_id_key, "fake-photo-id-key")
# pylint: disable=line-too-long
def test_parse_error_msg_success(self):
user = UserFactory.create()
attempt = SoftwareSecurePhotoVerification(user=user)
attempt.status = 'denied'
attempt.status = PhotoVerification.STATUS.denied
attempt.error_msg = '[{"userPhotoReasons": ["Face out of view"]}, {"photoIdReasons": ["Photo hidden/No photo", "ID name not provided"]}]'
parsed_error_msg = attempt.parsed_error_msg()
self.assertEqual(
@@ -288,7 +249,7 @@ class TestPhotoVerification(TestVerification, MockS3BotoMixin, ModuleStoreTestCa
# Make an initial verification with 'photo_id_key'
attempt = SoftwareSecurePhotoVerification(user=user, photo_id_key="dummy_photo_id_key")
attempt.status = 'approved'
attempt.status = PhotoVerification.STATUS.approved
attempt.save()
# Check that method 'get_initial_verification' returns the correct
@@ -298,7 +259,7 @@ class TestPhotoVerification(TestVerification, MockS3BotoMixin, ModuleStoreTestCa
# Now create a second verification without 'photo_id_key'
attempt = SoftwareSecurePhotoVerification(user=user)
attempt.status = 'submitted'
attempt.status = PhotoVerification.STATUS.submitted
attempt.save()
# Test method 'get_initial_verification' still returns the correct
@@ -332,7 +293,7 @@ class TestPhotoVerification(TestVerification, MockS3BotoMixin, ModuleStoreTestCa
# Populate Record
attempt.mark_ready()
attempt.status = "submitted"
attempt.status = PhotoVerification.STATUS.submitted
attempt.photo_id_image_url = "https://example.com/test/image/img.jpg"
attempt.face_image_url = "https://example.com/test/face/img.jpg"
attempt.photo_id_key = 'there_was_an_attempt'
@@ -379,7 +340,7 @@ class TestPhotoVerification(TestVerification, MockS3BotoMixin, ModuleStoreTestCa
for _ in range(2):
# Make an approved verification
attempt = SoftwareSecurePhotoVerification(user=user)
attempt.status = 'approved'
attempt.status = PhotoVerification.STATUS.approved
attempt.expiry_date = datetime.now()
attempt.save()
@@ -400,7 +361,7 @@ class TestPhotoVerification(TestVerification, MockS3BotoMixin, ModuleStoreTestCa
for _ in range(2):
# Make an approved verification
attempt = SoftwareSecurePhotoVerification(user=user)
attempt.status = 'approved'
attempt.status = PhotoVerification.STATUS.approved
attempt.save()
# Test method 'get_recent_verification' returns None
@@ -428,7 +389,7 @@ class TestPhotoVerification(TestVerification, MockS3BotoMixin, ModuleStoreTestCa
user = UserFactory.create()
verification = SoftwareSecurePhotoVerification(user=user)
verification.expiry_date = now() - timedelta(days=FAKE_SETTINGS['DAYS_GOOD_FOR'])
verification.status = 'approved'
verification.status = PhotoVerification.STATUS.approved
verification.save()
self.assertIsNone(verification.expiry_email_date)
@@ -439,7 +400,7 @@ class TestPhotoVerification(TestVerification, MockS3BotoMixin, ModuleStoreTestCa
self.assertIsNotNone(result.expiry_email_date)
class SSOVerificationTest(TestVerification):
class SSOVerificationTest(TestVerificationBase):
"""
Tests for the SSOVerification model
"""
@@ -450,7 +411,7 @@ class SSOVerificationTest(TestVerification):
self.verification_active_at_datetime(attempt)
class ManualVerificationTest(TestVerification):
class ManualVerificationTest(TestVerificationBase):
"""
Tests for the ManualVerification model
"""

View File

@@ -0,0 +1,38 @@
# Lots of patching to stub in our own settings, and HTTP posting
import ddt
import mock
from django.conf import settings
from mock import patch
from common.test.utils import MockS3BotoMixin
from verify_student.tests import TestVerificationBase
from verify_student.tests.test_models import FAKE_SETTINGS, mock_software_secure_post_unavailable
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
LOGGER_NAME = 'lms.djangoapps.verify_student.tasks'
@patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS)
@ddt.ddt
class TestPhotoVerificationTasks(TestVerificationBase, MockS3BotoMixin, ModuleStoreTestCase):
@mock.patch('lms.djangoapps.verify_student.tasks.log')
def test_logs_for_retry_until_failure(self, mock_log):
retry_max_attempts = settings.SOFTWARE_SECURE_RETRY_MAX_ATTEMPTS
with mock.patch('lms.djangoapps.verify_student.tasks.requests.post', new=mock_software_secure_post_unavailable):
attempt = self.create_and_submit_attempt_for_user()
username = attempt.user.username
mock_log.error.assert_called_with(
'Software Secure submission failed for user %r, setting status to must_retry',
username,
exc_info=True
)
for current_attempt in range(retry_max_attempts):
mock_log.error.assert_any_call(
('Retrying sending request to Software Secure for user: %r, Receipt ID: %r '
'attempt#: %s of %s'),
username,
attempt.receipt_id,
current_attempt,
settings.SOFTWARE_SECURE_RETRY_MAX_ATTEMPTS,
)

View File

@@ -3,7 +3,6 @@
Tests of verify_student views.
"""
from datetime import timedelta
from uuid import uuid4
@@ -45,6 +44,7 @@ from shoppingcart.models import CertificateItem, Order
from student.models import CourseEnrollment
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from util.testing import UrlResetMixin
from verify_student.tests import TestVerificationBase
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@@ -54,6 +54,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
def mock_render_to_response(*args, **kwargs):
return render_to_response(*args, **kwargs)
render_mock = Mock(side_effect=mock_render_to_response)
PAYMENT_DATA_KEYS = {'payment_processor_name', 'payment_page_url', 'payment_form_data'}
@@ -64,6 +65,7 @@ class StartView(TestCase):
This view is for the first time student is
attempting a Photo Verification.
"""
def start_url(self, course_id=""):
return "/verify_student/{0}".format(six.moves.urllib.parse.quote(course_id))
@@ -80,7 +82,7 @@ class StartView(TestCase):
@ddt.ddt
class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin, TestVerificationBase):
"""
Tests for the payment and verification flow views.
"""
@@ -897,7 +899,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin):
if status in ["submitted", "approved", "expired", "denied", "error"]:
attempt.mark_ready()
attempt.submit()
attempt = self.submit_attempt(attempt)
if status in ["approved", "expired"]:
attempt.approve()
@@ -1112,6 +1114,7 @@ class CheckoutTestMixin(object):
compatibility, the effect of using this endpoint is to choose a specific product
(i.e. course mode) and trigger immediate checkout.
"""
def setUp(self):
""" Create a user and course. """
super(CheckoutTestMixin, self).setUp()
@@ -1123,12 +1126,12 @@ class CheckoutTestMixin(object):
self.client.login(username="test", password="test")
def _assert_checked_out(
self,
post_params,
patched_create_order,
expected_course_key,
expected_mode_slug,
expected_status_code=200
self,
post_params,
patched_create_order,
expected_course_key,
expected_mode_slug,
expected_status_code=200
):
"""
DRY helper.
@@ -1413,7 +1416,7 @@ class TestCreateOrderView(ModuleStoreTestCase):
@ddt.ddt
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
class TestSubmitPhotosForVerification(MockS3BotoMixin, TestCase):
class TestSubmitPhotosForVerification(MockS3BotoMixin, TestVerificationBase):
"""
Tests for submitting photos for verification.
"""
@@ -1571,6 +1574,7 @@ class TestSubmitPhotosForVerification(MockS3BotoMixin, TestCase):
# Now the request should succeed
self._submit_photos(face_image=self.IMAGE_DATA)
#
def _submit_photos(self, face_image=None, photo_id_image=None, full_name=None, expected_status_code=200):
"""Submit photos for verification.
@@ -1596,7 +1600,8 @@ class TestSubmitPhotosForVerification(MockS3BotoMixin, TestCase):
if full_name is not None:
params['full_name'] = full_name
response = self.client.post(url, params)
with self.immediate_on_commit():
response = self.client.post(url, params)
self.assertEqual(response.status_code, expected_status_code)
return response
@@ -1637,10 +1642,11 @@ class TestSubmitPhotosForVerification(MockS3BotoMixin, TestCase):
return json.loads(last_request.body)
class TestPhotoVerificationResultsCallback(ModuleStoreTestCase):
class TestPhotoVerificationResultsCallback(ModuleStoreTestCase, TestVerificationBase):
"""
Tests for the results_callback view.
"""
def setUp(self):
super(TestPhotoVerificationResultsCallback, self).setUp()
@@ -1750,9 +1756,7 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase):
expiry_date = now() + timedelta(
days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]
)
verification = SoftwareSecurePhotoVerification.objects.create(user=self.user)
verification.mark_ready()
verification.submit()
verification = self.create_and_submit_attempt_for_user(self.user)
verification.approve()
verification.expiry_date = now()
verification.expiry_email_date = now()
@@ -1899,11 +1903,11 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase):
self.assertContains(response, 'Result Unknown not understood', status_code=400)
class TestReverifyView(TestCase):
class TestReverifyView(TestVerificationBase):
"""
Tests for the reverification view.
Tests for the re-verification view.
Reverification occurs when a verification attempt is denied or expired,
Re-verification occurs when a verification attempt is denied or expired,
and the student is given the option to resubmit.
"""
@@ -1918,30 +1922,26 @@ class TestReverifyView(TestCase):
def test_reverify_view_can_do_initial_verification(self):
"""
Test that a User can use reverify link for initial verification.
Test that a User can use re-verify link for initial verification.
"""
self._assert_can_reverify()
def test_reverify_view_can_reverify_denied(self):
# User has a denied attempt, so can reverify
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready()
attempt.submit()
# User has a denied attempt, so can re-verify
attempt = self.create_and_submit_attempt_for_user(self.user)
attempt.deny("error")
self._assert_can_reverify()
def test_reverify_view_can_reverify_expired(self):
# User has a verification attempt, but it's expired
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready()
attempt.submit()
attempt = self.create_and_submit_attempt_for_user(self.user)
attempt.approve()
days_good_for = settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]
attempt.created_at = now() - timedelta(days=(days_good_for + 1))
attempt.save()
# Allow the student to reverify
# Allow the student to re-verify
self._assert_can_reverify()
def test_reverify_view_can_reverify_pending(self):
@@ -1954,21 +1954,17 @@ class TestReverifyView(TestCase):
"""
# User has submitted a verification attempt, but Software Secure has not yet responded
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready()
attempt.submit()
attempt = self.create_and_submit_attempt_for_user(self.user)
# Can re-verify because an attempt has already been submitted.
self._assert_can_reverify()
def test_reverify_view_cannot_reverify_approved(self):
# Submitted attempt has been approved
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready()
attempt.submit()
attempt = self.create_and_submit_attempt_for_user(self.user)
attempt.approve()
# Cannot reverify because the user is already verified.
# Cannot re-verify because the user is already verified.
self._assert_cannot_reverify()
@override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10})
@@ -1979,10 +1975,7 @@ class TestReverifyView(TestCase):
and learner can submit photos if verification is set to expire in
EXPIRING_SOON_WINDOW(i.e here it is 10 days) or less days.
"""
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
attempt.mark_ready()
attempt.submit()
attempt = self.create_and_submit_attempt_for_user(self.user)
attempt.approve()
# Can re-verify because verification is set to expired soon.
@@ -1990,21 +1983,21 @@ class TestReverifyView(TestCase):
def _get_reverify_page(self):
"""
Retrieve the reverification page and return the response.
Retrieve the re-verification page and return the response.
"""
url = reverse("verify_student_reverify")
return self.client.get(url)
def _assert_can_reverify(self):
"""
Check that the reverification flow is rendered.
Check that the re-verification flow is rendered.
"""
response = self._get_reverify_page()
self.assertContains(response, "reverify-container")
def _assert_cannot_reverify(self):
"""
Check that the user is blocked from reverifying.
Check that the user is blocked from re-verifying.
"""
response = self._get_reverify_page()
self.assertContains(response, "reverify-blocked")

View File

@@ -101,3 +101,18 @@ def most_recent_verification(photo_id_verifications, sso_id_verifications, manua
}
return max(verifications_map, key=lambda k: verifications_map[k]) if verifications_map else None
def auto_verify_for_testing_enabled(override=None):
"""
If AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING is True, we want to skip posting
anything to Software Secure.
Bypass posting anything to Software Secure if auto verify feature for testing is enabled.
We actually don't even create the message because that would require encryption and message
signing that rely on settings.VERIFY_STUDENT values that aren't set in dev. So we just
pretend like we successfully posted.
"""
if override is not None:
return override
return settings.FEATURES.get('AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING')

View File

@@ -239,7 +239,7 @@ class PayAndVerifyView(View):
# Verify that the course exists
if course is None:
log.warn(u"Could not find course with ID %s.", course_id)
log.warning(u"Could not find course with ID %s.", course_id)
raise Http404
# Check whether the user has access to this course
@@ -295,7 +295,7 @@ class PayAndVerifyView(View):
else:
# Otherwise, there has never been a verified/paid mode,
# so return a page not found response.
log.warn(
log.warning(
u"No paid/verified course mode found for course '%s' for verification/payment flow request",
course_id
)
@@ -818,12 +818,12 @@ def create_order(request):
paid_modes = CourseMode.paid_modes_for_course(course_id)
if paid_modes:
if len(paid_modes) > 1:
log.warn(u"Multiple paid course modes found for course '%s' for create order request", course_id)
log.warning(u"Multiple paid course modes found for course '%s' for create order request", course_id)
current_mode = paid_modes[0]
# Make sure this course has a paid mode
if not current_mode:
log.warn(u"Create order requested for course '%s' without a paid mode.", course_id)
log.warning(u"Create order requested for course '%s' without a paid mode.", course_id)
return HttpResponseBadRequest(_("This course doesn't support paid certificates"))
if CourseMode.is_professional_mode(current_mode):
@@ -899,7 +899,7 @@ class SubmitPhotosView(View):
# Retrieve the image data
# Validation ensures that we'll have a face image, but we may not have
# a photo ID image if this is a reverification.
# a photo ID image if this is a re-verification.
face_image, photo_id_image, response = self._decode_image_data(
params["face_image"], params.get("photo_id_image")
)

View File

@@ -511,6 +511,13 @@ XQUEUE_INTERFACE = {
# Used with Email sending
RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS = 5
RETRY_ACTIVATION_EMAIL_TIMEOUT = 0.5
# Software Secure request retry settings
# Time in seconds before a retry of the task should be 60 mints.
SOFTWARE_SECURE_REQUEST_RETRY_DELAY = 60 * 60
# Maximum of 6 retries before giving up.
SOFTWARE_SECURE_RETRY_MAX_ATTEMPTS = 6
PASSWORD_RESET_EMAIL_RATE_LIMIT = {
'no_of_emails': 1,
'per_seconds': 60
@@ -2154,7 +2161,6 @@ CELERY_BROKER_TRANSPORT = 'amqp'
CELERY_BROKER_HOSTNAME = 'localhost'
CELERY_BROKER_USER = 'celery'
CELERY_BROKER_PASSWORD = 'celery'
CELERY_TIMEZONE = 'UTC'
################################ Block Structures ###################################
@@ -2837,6 +2843,8 @@ POLICY_CHANGE_GRADES_ROUTING_KEY = 'edx.lms.core.default'
RECALCULATE_GRADES_ROUTING_KEY = 'edx.lms.core.default'
SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY = 'edx.lms.core.default'
GRADES_DOWNLOAD = {
'STORAGE_CLASS': 'django.core.files.storage.FileSystemStorage',
'STORAGE_KWARGS': {

View File

@@ -51,7 +51,7 @@ CONFIG_FILE = get_env_setting('LMS_CFG')
with codecs.open(CONFIG_FILE, encoding='utf-8') as f:
__config__ = yaml.safe_load(f)
# ENV_TOKENS and AUTH_TOKENS are included for reverse compatability.
# ENV_TOKENS and AUTH_TOKENS are included for reverse compatibility.
# Removing them may break plugins that rely on them.
ENV_TOKENS = __config__
AUTH_TOKENS = __config__
@@ -966,6 +966,10 @@ CREDENTIALS_GENERATION_ROUTING_KEY = ENV_TOKENS.get('CREDENTIALS_GENERATION_ROUT
# Queue to use for award program certificates
PROGRAM_CERTIFICATES_ROUTING_KEY = ENV_TOKENS.get('PROGRAM_CERTIFICATES_ROUTING_KEY', DEFAULT_PRIORITY_QUEUE)
SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY = ENV_TOKENS.get(
'SOFTWARE_SECURE_VERIFICATION_ROUTING_KEY',
HIGH_PRIORITY_QUEUE
)
API_ACCESS_MANAGER_EMAIL = ENV_TOKENS.get('API_ACCESS_MANAGER_EMAIL')
API_ACCESS_FROM_EMAIL = ENV_TOKENS.get('API_ACCESS_FROM_EMAIL')

View File

@@ -123,3 +123,10 @@ RETIREMENT_SERVICE_WORKER_USERNAME = 'RETIREMENT_SERVICE_USER'
RETIRED_USERNAME_PREFIX = 'retired__user_'
PROCTORING_SETTINGS = {}
# Software Secure request retry settings
# Time in seconds before a retry of the task should be 60 mints.
SOFTWARE_SECURE_REQUEST_RETRY_DELAY = 60 * 60
# Maximum of 6 retries before giving up.
SOFTWARE_SECURE_RETRY_MAX_ATTEMPTS = 6