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