Skip to content

Commit

Permalink
Add support for refreshing objects from API
Browse files Browse the repository at this point in the history
The BaseModel now contains a `pull` method which will refresh the
internal properties from the API, and update the raw field.

I have also pulled the create_FOO functions out of the api module into
a helpers module to make dependencies easier to manage. As this breaks
the interface I will bump the version and re-release.
  • Loading branch information
hugorodgerbrown committed Oct 24, 2016
1 parent 7bb61bd commit 76bf902
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 193 deletions.
73 changes: 9 additions & 64 deletions onfido/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@

import requests

from .models import (
Applicant,
Check,
Report
)
from .settings import (
API_ROOT,
API_KEY
Expand Down Expand Up @@ -52,68 +47,18 @@ def _respond(response):
"""Process common response object."""
if not str(response.status_code).startswith('2'):
raise ApiError(response)
return response.json()
data = response.json()
logger.debug("Onfido API response: %s", data)
return data


def _get(url):
def get(href):
"""Make a GET request and return the response as JSON."""
logger.debug("Making GET request to %s", url)
return _respond(requests.get(url, headers=_headers()))
logger.debug("Onfido API GET request: %s", href)
return _respond(requests.get(_url(href), headers=_headers()))


def _post(url, data):
def post(href, data):
"""Make a POST request and return the response as JSON."""
logger.debug("Making POST request to %s: %s", url, data)
return _respond(requests.post(url, headers=_headers(), json=data))


def create_applicant(user):
"""Create an applicant in the Onfido system."""
data = {
"first_name": user.first_name,
"last_name": user.last_name,
"email": user.email
}
response = _post(_url('applicants'), data)
logger.debug(response)
return Applicant.objects.create_applicant(user, response)


def create_check(applicant, check_type, reports, **kwargs):
"""
Create a new Check (and child Reports).
Args:
applicant: Applicant for whom the checks are being made.
check_type: string, currently only 'standard' is supported.
reports: list of strings, each of which is a valid report type.
Kwargs:
any kwargs passed in are merged into the data dict sent to the API. This
enables support for additional check properties - e.g. redirect_uri,
tags, suppress_form_emails and any other that may change over time. See
https://documentation.onfido.com/#checks for details.
Returns a new Check object, and creates the child Report objects.
"""
assert check_type == 'standard', (
"Invalid check_type '{}', currently only 'standard' "
"checks are supported.".format(check_type)
)
assert isinstance(reports, (list, tuple)), (
"Invalid reports arg '{}', must be a list or tuple "
"if supplied.".format(reports)
)
data = {
"type": check_type,
"reports": [{'name': r} for r in reports],
}
# merge in the additional kwargs
data.update(kwargs)
response = _post(_url('applicants/{}/checks'.format(applicant.onfido_id)), data)
logger.debug(response)
check = Check.objects.create_check(applicant=applicant, raw=response)
for report in response['reports']:
Report.objects.create_report(check=check, raw=report)
return check
logger.debug("Onfido API POST request: %s: %s", href, data)
return _respond(requests.post(_url(href), headers=_headers(), json=data))
58 changes: 58 additions & 0 deletions onfido/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Helper functions
from .api import post
from .models import (
Applicant,
Check,
Report
)


def create_applicant(user):
"""Create an applicant in the Onfido system."""
data = {
"first_name": user.first_name,
"last_name": user.last_name,
"email": user.email
}
response = post('applicants', data)
return Applicant.objects.create_applicant(user, response)


def create_check(applicant, check_type, reports, **kwargs):
"""
Create a new Check (and child Reports).
Args:
applicant: Applicant for whom the checks are being made.
check_type: string, currently only 'standard' is supported.
reports: list of strings, each of which is a valid report type.
Kwargs:
any kwargs passed in are merged into the data dict sent to the API. This
enables support for additional check properties - e.g. redirect_uri,
tags, suppress_form_emails and any other that may change over time. See
https://documentation.onfido.com/#checks for details.
Returns a new Check object, and creates the child Report objects.
"""
assert check_type == 'standard', (
"Invalid check_type '{}', currently only 'standard' "
"checks are supported.".format(check_type)
)
assert isinstance(reports, (list, tuple)), (
"Invalid reports arg '{}', must be a list or tuple "
"if supplied.".format(reports)
)
data = {
"type": check_type,
"reports": [{'name': r} for r in reports],
}
# merge in the additional kwargs
data.update(kwargs)
response = post('applicants/{}/checks'.format(applicant.onfido_id), data)
check = Check.objects.create_check(applicant=applicant, raw=response)
for report in response['reports']:
Report.objects.create_report(check=check, raw=report)
return check
34 changes: 27 additions & 7 deletions onfido/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.db import models
from django.utils.translation import ugettext as _

from .api import get
from .db.fields import JSONField
from .signals import on_status_change, on_completion

Expand Down Expand Up @@ -35,6 +36,14 @@ class BaseModel(models.Model):
class Meta:
abstract = True

def __str__(self):
return unicode(self).encode('utf8')

@property
def href(self):
"""Return the href from the raw JSON field."""
return self.raw['href']

def save(self, *args, **kwargs):
"""Save object and return self (for chaining methods)."""
self.full_clean()
Expand All @@ -48,8 +57,19 @@ def parse(self, raw_json):
self.created_at = date_parse(self.raw['created_at'])
return self

def __str__(self):
return unicode(self).encode('utf8')
def pull(self):
"""
Update the object from the remote API.
Named after the git operation - this will call the API for the
latest JSON representation, and then parse and save the object.
The API url is taken from the self.href property, and will raise
a KeyError if it does not exist.
Returns the updated object.
"""
return self.parse(get(self.href)).save()


class BaseStatusModel(BaseModel):
Expand Down Expand Up @@ -184,7 +204,7 @@ def __unicode__(self):
return u"{}".format(self.user.get_full_name() or self.user.username)

def __repr__(self):
return u"<Applicant id={} user='{}'>".format(
return u"<Applicant id={} user_id={}>".format(
self.id, self.user.id
)

Expand Down Expand Up @@ -233,10 +253,10 @@ def __unicode__(self):
)

def __repr__(self):
return u"<Check id={} type='{}' user='{}'>".format(
return u"<Check id={} type='{}' user_id={}>".format(
self.id,
self.check_type,
self.user
self.user.id
)

def parse(self, raw_json):
Expand Down Expand Up @@ -300,10 +320,10 @@ def __unicode__(self):
)

def __repr__(self):
return u"<Report id={} type='{}' user=%s>".format(
return u"<Report id={} type='{}' user_id={}>".format(
self.id,
self.report_type,
self.user
self.user.id
)

def parse(self, raw_json):
Expand Down
123 changes: 7 additions & 116 deletions onfido/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
# -*- coding: utf-8 -*-
import mock

from dateutil.parser import parse as date_parse

from django.contrib.auth.models import User
from django.test import TestCase

from ..api import (
_get,
_post,
get,
post,
_url,
_headers,
_respond,
ApiError,
create_applicant,
create_check,
# import from api, not models, to reduce dependency issues
Applicant,
Check,
Report
)


Expand Down Expand Up @@ -61,8 +53,8 @@ def test_get(self, mock_headers, mock_get):
response.status_code = 200
headers = mock_headers.return_value
mock_get.return_value = response
self.assertEqual(_get('/'), response.json.return_value)
mock_get.assert_called_once_with('/', headers=headers)
self.assertEqual(get('/'), response.json.return_value)
mock_get.assert_called_once_with(_url('/'), headers=headers)

@mock.patch('requests.post')
@mock.patch('onfido.api._headers')
Expand All @@ -73,106 +65,5 @@ def test_post(self, mock_headers, mock_post):
headers = mock_headers.return_value
data = {"foo": "bar"}
mock_post.return_value = response
self.assertEqual(_post('/', data), response.json.return_value)
mock_post.assert_called_once_with('/', headers=headers, json=data)

@mock.patch('onfido.api._post')
def test_create_applicant(self, mock_post):
"""Test the create_applicant function."""
data = {
"id": "a9acefdf-3dc5-4973-aa78-20bd36825b50",
"created_at": "2016-10-18T16:02:04Z",
"title": "Mr",
"first_name": "Fred",
"last_name": "Flintstone",
"email": "[email protected]",
"country": "gbr",
}
mock_post.return_value = data
user = User.objects.create_user(
username='fred',
first_name='Fred',
last_name='Flintstone',
email='[email protected]'
)
applicant = create_applicant(user)
self.assertEqual(applicant.onfido_id, data['id'])
self.assertEqual(applicant.user, user)
self.assertEqual(applicant.created_at, date_parse(data['created_at']))

@mock.patch('onfido.api._post')
def test_create_check(self, mock_post):
"""Test the create_check function."""
applicant_data = {
"id": "a9acefdf-3dc5-4973-aa78-20bd36825b50",
"created_at": "2016-10-18T16:02:04Z",
"title": "Mr",
"first_name": "Fred",
"last_name": "Flintstone",
"email": "[email protected]",
"country": "gbr",
}
check_data = {
"id": "b2b75f66-fffd-45a4-ba15-b1e77a672a9a",
"created_at": "2016-10-18T16:02:08Z",
"status": "awaiting_applicant",
"redirect_uri": None,
"type": "standard",
"result": None,
# "form_uri": "https://onfido.com/information/b2b75f66-fffd-45a4-ba15-b1e77a672a9a",
"reports": [
{
"created_at": "2016-10-18T16:02:08Z",
"id": "08345559-852c-4f47-bf65-f9852bf59c4b",
"name": "document",
"result": None,
"status": "awaiting_data",
"variant": "standard",
},
{
"created_at": "2016-10-18T16:02:08Z",
"id": "1ffd3e8a-da71-4674-a245-8b52f1492191",
"name": "identity",
"result": None,
"status": "awaiting_data",
"variant": "standard",
}
],
}
mock_post.return_value = check_data
user = User.objects.create_user(
username='fred',
first_name='Fred',
last_name='Flintstone',
email='[email protected]'
)
applicant = Applicant.objects.create_applicant(user, applicant_data)

# 1. use the defaults.
check = create_check(applicant, 'standard', ('identity', 'document'))
mock_post.assert_called_once_with(
'https://api.onfido.com/v2/applicants/a9acefdf-3dc5-4973-aa78-20bd36825b50/checks',
{'reports': [{'name': 'identity'}, {'name': 'document'}], 'type': 'standard'}
)
self.assertEqual(Check.objects.get(), check)
# check we have two reports, and that the raw field matches the JSON
# and that the parse method has run
self.assertEqual(Report.objects.count(), 2)
for r in check_data['reports']:
# this will only work if the JSON has been parsed correctly
report = Report.objects.get(onfido_id=r['id'])
self.assertEqual(report.raw, r)

# confirm that asserts guard input
self.assertRaises(AssertionError, create_check, applicant, 'express', ('identity'))
self.assertRaises(AssertionError, create_check, applicant, 'standard', 'identity')
self.assertRaises(AssertionError, create_check, applicant, 'standard', None)

# confirm that kwargs are merged in correctly
check.delete()
mock_post.reset_mock()
check = create_check(applicant, 'standard', ('identity',), foo='bar')
mock_post.assert_called_once_with(
'https://api.onfido.com/v2/applicants/a9acefdf-3dc5-4973-aa78-20bd36825b50/checks',
{'reports': [{'name': 'identity'}], 'type': 'standard', 'foo': 'bar'}
)
self.assertEqual(post('/', data), response.json.return_value)
mock_post.assert_called_once_with(_url('/'), headers=headers, json=data)
Loading

0 comments on commit 76bf902

Please sign in to comment.