Commit 066a7648 by Vedran Karačić Committed by GitHub

Merge pull request #1016 from edx/vkaracic/is_verified

Add custom verification status exception.
parents b24d3422 cf834c05
......@@ -6,3 +6,8 @@ class MissingRequestError(Exception):
class SiteConfigurationError(Exception):
""" Raised when SiteConfiguration is invalid. """
pass
class VerificationStatusError(Exception):
""" Raised when the verification fails to connect to LMS. """
pass
import datetime
import hashlib
import logging
from urlparse import urljoin
from analytics import Client as SegmentClient
from dateutil.parser import parse
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.contrib.sites.models import Site
......@@ -10,12 +12,14 @@ from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from edx_rest_api_client.client import EdxRestApiClient
from jsonfield.fields import JSONField
from requests.exceptions import ConnectionError, Timeout
from slumber.exceptions import HttpNotFoundError, SlumberBaseException
from ecommerce.core.exceptions import VerificationStatusError
from ecommerce.core.url_utils import get_lms_url
from ecommerce.courses.utils import mode_for_seat
from ecommerce.extensions.payment.exceptions import ProcessorNotFoundError
......@@ -448,13 +452,14 @@ class User(AbstractUser):
raise
return response
def is_verified(self, request):
def is_verified(self, site):
"""
Check if a user has verified his/her identity.
Calls the LMS verification status API endpoint and returns the verification status information.
The status information is stored in cache, if the user is verified, until the verification expires.
Args:
request (WSGIRequest): The request from which the LMS account API endpoint is created.
site (Site): The site object from which the LMS account API endpoint is created.
Returns:
True if the user is verified, false otherwise.
......@@ -464,20 +469,27 @@ class User(AbstractUser):
establishing a connection with the LMS verification status API endpoint.
"""
try:
api = EdxRestApiClient(
request.site.siteconfiguration.build_lms_url('api/user/v1/'),
oauth_access_token=self.access_token
)
response = api.accounts(self.username).verification_status().get()
return response.get('is_verified', False)
cache_key = 'verification_status_{username}'.format(username=self.username)
cache_key = hashlib.md5(cache_key).hexdigest()
verification = cache.get(cache_key)
if not verification:
api = EdxRestApiClient(
site.siteconfiguration.build_lms_url('api/user/v1/'),
oauth_access_token=self.access_token
)
response = api.accounts(self.username).verification_status().get()
verification = response.get('is_verified', False)
if verification:
cache_timeout = int((parse(response.get('expiration_datetime')) - now()).total_seconds())
cache.set(cache_key, verification, cache_timeout)
return verification
except HttpNotFoundError:
return False
except (ConnectionError, SlumberBaseException, Timeout): # pragma: no cover
log.exception(
'Failed to retrieve verification status details for [%s]',
self.username
)
raise
except (ConnectionError, SlumberBaseException, Timeout):
msg = 'Failed to retrieve verification status details for [{username}]'.format(username=self.username)
log.exception(msg)
raise VerificationStatusError(msg)
class Client(User):
......
......@@ -8,6 +8,7 @@ from django.test import override_settings
from edx_rest_api_client.auth import SuppliedJwtAuth
from requests.exceptions import ConnectionError
from ecommerce.core.exceptions import VerificationStatusError
from ecommerce.core.models import BusinessClient, User, SiteConfiguration, validate_configuration
from ecommerce.core.tests import toggle_switch
from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin
......@@ -132,8 +133,35 @@ class UserTests(CourseCatalogTestMixin, LmsApiMockMixin, TestCase):
def test_user_verification_status(self, status_code, is_verified):
""" Verify the method returns correct response. """
user = self.create_user()
self.mock_verification_status_api(self.request, user, status=status_code, is_verified=is_verified)
self.assertEqual(user.is_verified(self.request), is_verified)
self.mock_verification_status_api(self.site, user, status=status_code, is_verified=is_verified)
self.assertEqual(user.is_verified(self.site), is_verified)
def test_user_verification_connection_error(self):
""" Verify verification status exception is raised for connection issues. """
user = self.create_user()
with self.assertRaises(VerificationStatusError):
user.is_verified(self.site)
@httpretty.activate
def test_user_verification_status_cache(self):
""" Verify the user verification status values are cached. """
user = self.create_user()
self.mock_verification_status_api(self.site, user)
self.assertTrue(user.is_verified(self.site))
httpretty.disable()
self.assertTrue(user.is_verified(self.site))
@httpretty.activate
def test_user_verification_status_not_cached(self):
""" Verify the user verification status values is not cached when user is not verified. """
user = self.create_user()
self.mock_verification_status_api(self.site, user, is_verified=False)
self.assertFalse(user.is_verified(self.site))
httpretty.disable()
with self.assertRaises(VerificationStatusError):
user.is_verified(self.site)
class BusinessClientTests(TestCase):
......
......@@ -23,8 +23,8 @@ def get_range_catalog_query_results(limit, query, site, offset=None):
"""
partner_code = site.siteconfiguration.partner.short_code
cache_key = 'course_runs_{}_{}_{}_{}'.format(query, limit, offset, partner_code)
cache_hash = hashlib.md5(cache_key).hexdigest()
response = cache.get(cache_hash)
cache_key = hashlib.md5(cache_key).hexdigest()
response = cache.get(cache_key)
if not response:
response = site.siteconfiguration.course_catalog_api_client.course_runs.get(
limit=limit,
......@@ -32,7 +32,7 @@ def get_range_catalog_query_results(limit, query, site, offset=None):
q=query,
partner=partner_code
)
cache.set(cache_hash, response, settings.COURSES_API_CACHE_TIMEOUT)
cache.set(cache_key, response, settings.COURSES_API_CACHE_TIMEOUT)
return response
......
......@@ -49,15 +49,15 @@ class UtilsTests(CourseCatalogTestMixin, CourseCatalogMockMixin, TestCase):
self.mock_dynamic_catalog_single_course_runs_api(course)
cache_key = 'courses_api_detail_{}{}'.format(course.id, self.site.siteconfiguration.partner.short_code)
cache_hash = hashlib.md5(cache_key).hexdigest()
cached_course = cache.get(cache_hash)
cache_key = hashlib.md5(cache_key).hexdigest()
cached_course = cache.get(cache_key)
self.assertIsNone(cached_course)
response = get_course_info_from_catalog(self.request.site, course)
self.assertEqual(response['title'], course.name)
cached_course = cache.get(cache_hash)
cached_course = cache.get(cache_key)
self.assertEqual(cached_course, response)
@ddt.data(
......
......@@ -25,11 +25,11 @@ def get_course_info_from_catalog(site, course_key):
api = site.siteconfiguration.course_catalog_api_client
partner_short_code = site.siteconfiguration.partner.short_code
cache_key = 'courses_api_detail_{}{}'.format(course_key, partner_short_code)
cache_hash = hashlib.md5(cache_key).hexdigest()
course_run = cache.get(cache_hash)
cache_key = hashlib.md5(cache_key).hexdigest()
course_run = cache.get(cache_key)
if not course_run: # pragma: no cover
course_run = api.course_runs(course_key).get(partner=partner_short_code)
cache.set(cache_hash, course_run, settings.COURSES_API_CACHE_TIMEOUT)
cache.set(cache_key, course_run, settings.COURSES_API_CACHE_TIMEOUT)
return course_run
......
......@@ -405,13 +405,13 @@ class BasketSummaryViewTests(CourseCatalogTestMixin, CourseCatalogMockMixin, Lms
self.mock_dynamic_catalog_single_course_runs_api(self.course)
cache_key = 'courses_api_detail_{}{}'.format(self.course.id, self.site.siteconfiguration.partner.short_code)
cache_hash = hashlib.md5(cache_key).hexdigest()
cached_course_before = cache.get(cache_hash)
cache_key = hashlib.md5(cache_key).hexdigest()
cached_course_before = cache.get(cache_key)
self.assertIsNone(cached_course_before)
response = self.client.get(self.path)
self.assertEqual(response.status_code, 200)
cached_course_after = cache.get(cache_hash)
cached_course_after = cache.get(cache_key)
self.assertEqual(cached_course_after['title'], self.course.name)
@ddt.data({
......
......@@ -116,8 +116,8 @@ class Range(AbstractRange):
Retrieve the results from running the query contained in catalog_query field.
"""
cache_key = 'catalog_query_contains [{}] [{}]'.format(self.catalog_query, product.course_id)
cache_hash = hashlib.md5(cache_key).hexdigest()
response = cache.get(cache_hash)
cache_key = hashlib.md5(cache_key).hexdigest()
response = cache.get(cache_key)
if not response: # pragma: no cover
request = get_current_request()
try:
......@@ -126,7 +126,7 @@ class Range(AbstractRange):
course_run_ids=product.course_id,
partner=request.site.siteconfiguration.partner.short_code
)
cache.set(cache_hash, response, settings.COURSES_API_CACHE_TIMEOUT)
cache.set(cache_key, response, settings.COURSES_API_CACHE_TIMEOUT)
except: # pylint: disable=bare-except
raise Exception('Could not contact Course Catalog Service.')
......
......@@ -96,14 +96,14 @@ class RangeTests(CouponMixin, CourseCatalogTestMixin, CourseCatalogMockMixin, Te
self.range.catalog_query = 'key:*'
cache_key = 'catalog_query_contains [{}] [{}]'.format('key:*', seat.course_id)
cache_hash = hashlib.md5(cache_key).hexdigest()
cached_response = cache.get(cache_hash)
cache_key = hashlib.md5(cache_key).hexdigest()
cached_response = cache.get(cache_key)
self.assertIsNone(cached_response)
with mock.patch('ecommerce.core.url_utils.get_current_request', mock.Mock(return_value=request)):
response = self.range.run_catalog_query(seat)
self.assertTrue(response['course_runs'][course.id])
cached_response = cache.get(cache_hash)
cached_response = cache.get(cache_key)
self.assertEqual(response, cached_response)
@httpretty.activate
......
......@@ -553,11 +553,11 @@ def get_cached_voucher(code):
Voucher.DoesNotExist: When no vouchers with provided code exist.
"""
cache_key = 'voucher_{code}'.format(code=code)
cache_hash = hashlib.md5(cache_key).hexdigest()
cache_key = hashlib.md5(cache_key).hexdigest()
voucher = cache.get(cache_key)
if not voucher:
voucher = Voucher.objects.get(code=code)
cache.set(cache_hash, voucher, settings.VOUCHER_CACHE_TIMEOUT)
cache.set(cache_key, voucher, settings.VOUCHER_CACHE_TIMEOUT)
return voucher
......
......@@ -382,7 +382,7 @@ class LmsApiMockMixin(object):
)
httpretty.register_uri(httpretty.GET, url, body=json.dumps(eligibility_data), content_type=CONTENT_TYPE)
def mock_verification_status_api(self, request, user, status=200, is_verified=True):
def mock_verification_status_api(self, site, user, status=200, is_verified=True):
""" Mock verification API endpoint. Returns verfication status data. """
verification_data = {
'status': 'approved',
......@@ -390,7 +390,7 @@ class LmsApiMockMixin(object):
'is_verified': is_verified
}
url = '{host}/accounts/{username}/verification_status/'.format(
host=request.site.siteconfiguration.build_lms_url('/api/user/v1'),
host=site.siteconfiguration.build_lms_url('/api/user/v1'),
username=user.username
)
httpretty.register_uri(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment