Commit 0dc8f482 by Clinton Blackburn

Updated client to support enrollment date ranges

Change-Id: I5c096c47e1ae47902cb2bf1bd3d2e52ec41cbee9
parent c14f3047
...@@ -4,7 +4,7 @@ import requests ...@@ -4,7 +4,7 @@ import requests
import requests.exceptions import requests.exceptions
from analyticsclient.course import Course from analyticsclient.course import Course
from analyticsclient.exceptions import ClientError, InvalidRequestError, NotFoundError from analyticsclient.exceptions import ClientError, InvalidRequestError, NotFoundError, TimeoutError
from analyticsclient.status import Status from analyticsclient.status import Status
...@@ -81,6 +81,7 @@ class Client(object): ...@@ -81,6 +81,7 @@ class Client(object):
except ClientError: except ClientError:
return False return False
# pylint: disable=no-member
def _request(self, resource, timeout=None): def _request(self, resource, timeout=None):
if timeout is None: if timeout is None:
timeout = self.timeout timeout = self.timeout
...@@ -96,14 +97,14 @@ class Client(object): ...@@ -96,14 +97,14 @@ class Client(object):
response = requests.get(uri, headers=headers, timeout=timeout) response = requests.get(uri, headers=headers, timeout=timeout)
status = response.status_code status = response.status_code
if status != requests.codes.ok: # pylint: disable=no-member if status != requests.codes.ok:
message = 'Resource "{0}" returned status code {1}'.format(resource, status) message = 'Resource "{0}" returned status code {1}'.format(resource, status)
error_class = ClientError error_class = ClientError
if status == requests.codes.bad_request: # pylint: disable=no-member if status == requests.codes.bad_request:
message = 'The request to {0} was invalid.'.format(uri) message = 'The request to {0} was invalid.'.format(uri)
error_class = InvalidRequestError error_class = InvalidRequestError
elif status == requests.codes.not_found: # pylint: disable=no-member elif status == requests.codes.not_found:
message = 'Resource {0} was not found on the API server.'.format(uri) message = 'Resource {0} was not found on the API server.'.format(uri)
error_class = NotFoundError error_class = NotFoundError
...@@ -112,6 +113,11 @@ class Client(object): ...@@ -112,6 +113,11 @@ class Client(object):
return response return response
except requests.exceptions.Timeout:
message = "Response from {0} exceeded timeout of {1}s."
log.exception(message)
raise TimeoutError(message)
except requests.exceptions.RequestException: except requests.exceptions.RequestException:
message = 'Unable to retrieve resource' message = 'Unable to retrieve resource'
log.exception(message) log.exception(message)
......
import urllib
import analyticsclient.activity_type as at import analyticsclient.activity_type as at
...@@ -17,14 +18,38 @@ class Course(object): ...@@ -17,14 +18,38 @@ class Course(object):
self.client = client self.client = client
self.course_id = unicode(course_id) self.course_id = unicode(course_id)
def enrollment(self, demographic): def enrollment(self, demographic=None, start_date=None, end_date=None):
""" """
Get course enrollment data grouped by demographic. Get course enrollment data.
Specify a start or end date to retrieve all data for the date range. If no start or end date is specifying, data
for the most-recent date will be returned. All dates are in the UTC timezone and should be formatted as
YYYY-mm-dd (e.g. 2014-01-31).
Specify a demographic to retrieve data grouped by the specified demographic. If no demographic is specified,
data will be across all demographics.
Arguments: Arguments:
demographic (str): Demographic by which enrollment data should be grouped. demographic (str): Demographic by which enrollment data should be grouped.
start_date (str): Minimum date for returned enrollment data
end_date (str): Maxmimum date for returned enrollment data
""" """
return self.client.get('courses/{0}/enrollment/{1}/'.format(self.course_id, demographic)) path = 'courses/{0}/enrollment/'.format(self.course_id)
if demographic:
path += '{0}/'.format(demographic)
params = {}
if start_date:
params['start_date'] = start_date
if end_date:
params['end_date'] = end_date
querystring = urllib.urlencode(params)
if querystring:
path += '?{0}'.format(querystring)
return self.client.get(path)
def recent_activity(self, activity_type=at.ANY): def recent_activity(self, activity_type=at.ANY):
""" """
......
...@@ -3,3 +3,4 @@ ...@@ -3,3 +3,4 @@
BIRTH_YEAR = 'birth_year' BIRTH_YEAR = 'birth_year'
EDUCATION = 'education' EDUCATION = 'education'
GENDER = 'gender' GENDER = 'gender'
LOCATION = 'location'
...@@ -14,3 +14,9 @@ class InvalidRequestError(ClientError): ...@@ -14,3 +14,9 @@ class InvalidRequestError(ClientError):
""" The API request was invalid. """ """ The API request was invalid. """
pass pass
class TimeoutError(ClientError):
""" The API server did not respond before the timeout expired. """
pass
import json import json
import httpretty import httpretty
import mock
import requests.exceptions
from analyticsclient.client import Client from analyticsclient.client import Client
from analyticsclient.exceptions import ClientError from analyticsclient.exceptions import ClientError, TimeoutError
from analyticsclient.tests import ClientTestCase from analyticsclient.tests import ClientTestCase
...@@ -12,33 +14,36 @@ class ClientTests(ClientTestCase): ...@@ -12,33 +14,36 @@ class ClientTests(ClientTestCase):
super(ClientTests, self).setUp() super(ClientTests, self).setUp()
httpretty.enable() httpretty.enable()
self.test_endpoint = 'test' self.test_endpoint = 'test'
self.test_uri = self.get_api_url(self.test_endpoint) self.test_url = self.get_api_url(self.test_endpoint)
def tearDown(self): def tearDown(self):
httpretty.disable() httpretty.disable()
def test_has_resource(self): def test_has_resource(self):
httpretty.register_uri(httpretty.GET, self.test_uri, body='') httpretty.register_uri(httpretty.GET, self.test_url, body='')
self.assertEquals(self.client.has_resource(self.test_endpoint), True) self.assertEquals(self.client.has_resource(self.test_endpoint), True)
def test_missing_resource(self): def test_missing_resource(self):
httpretty.register_uri(httpretty.GET, self.test_uri, body='', status=404) httpretty.register_uri(httpretty.GET, self.test_url, body='', status=404)
self.assertEquals(self.client.has_resource(self.test_endpoint), False) self.assertEquals(self.client.has_resource(self.test_endpoint), False)
def test_failed_authentication(self): def test_failed_authentication(self):
client = Client(base_url=self.api_url, auth_token='atoken') client = Client(base_url=self.api_url, auth_token='atoken')
httpretty.register_uri(httpretty.GET, self.test_uri, body='', status=401) httpretty.register_uri(httpretty.GET, self.test_url, body='', status=401)
self.assertEquals(client.has_resource(self.test_endpoint), False) self.assertEquals(client.has_resource(self.test_endpoint), False)
self.assertEquals(httpretty.last_request().headers['Authorization'], 'Token atoken') self.assertEquals(httpretty.last_request().headers['Authorization'], 'Token atoken')
def test_get(self): def test_get(self):
data = {'foo': 'bar'} data = {'foo': 'bar'}
httpretty.register_uri(httpretty.GET, self.test_uri, body=json.dumps(data)) httpretty.register_uri(httpretty.GET, self.test_url, body=json.dumps(data))
self.assertEquals(self.client.get(self.test_endpoint), data) self.assertEquals(self.client.get(self.test_endpoint), data)
# Bad JSON def test_get_invalid_response_body(self):
httpretty.register_uri(httpretty.GET, self.test_uri, body=json.dumps(data)[:6]) """ Verify that client raises a ClientError if the response body cannot be properly parsed. """
data = {'foo': 'bar'}
httpretty.register_uri(httpretty.GET, self.test_url, body=json.dumps(data)[:6])
with self.assertRaises(ClientError): with self.assertRaises(ClientError):
self.client.get(self.test_endpoint) self.client.get(self.test_endpoint)
...@@ -50,3 +55,17 @@ class ClientTests(ClientTestCase): ...@@ -50,3 +55,17 @@ class ClientTests(ClientTestCase):
url_with_slash = 'http://example.com/' url_with_slash = 'http://example.com/'
client = Client(url_with_slash) client = Client(url_with_slash)
self.assertEqual(client.base_url, url) self.assertEqual(client.base_url, url)
# pylint: disable=protected-access
@mock.patch('requests.get', side_effect=requests.exceptions.Timeout)
def test_request_timeout(self, mock_get):
url = self.test_url
timeout = None
self.assertRaises(TimeoutError, self.client._request, self.test_endpoint, timeout=timeout)
headers = {'Accept': 'application/json'}
mock_get.assert_called_once_with(url, headers=headers, timeout=self.client.timeout)
mock_get.reset_mock()
timeout = 10
self.assertRaises(TimeoutError, self.client._request, self.test_endpoint, timeout=timeout)
mock_get.assert_called_once_with(url, headers=headers, timeout=timeout)
...@@ -20,12 +20,28 @@ class CoursesTests(ClientTestCase): ...@@ -20,12 +20,28 @@ class CoursesTests(ClientTestCase):
super(CoursesTests, self).tearDown() super(CoursesTests, self).tearDown()
httpretty.disable() httpretty.disable()
def assertEnrollmentResponseData(self, course, data, demographic=None): def assertCorrectEnrollmentUrl(self, course, demographic=None):
""" Verifies that the enrollment URL is correct. """
uri = self.get_api_url('courses/{0}/enrollment/'.format(course.course_id)) uri = self.get_api_url('courses/{0}/enrollment/'.format(course.course_id))
if demographic: if demographic:
uri += '%s/' % demographic uri += '%s/' % demographic
httpretty.register_uri(httpretty.GET, uri, body=json.dumps(data))
self.assertDictEqual(data, course.enrollment(demographic)) httpretty.register_uri(httpretty.GET, uri, body='{}')
course.enrollment(demographic)
date = '2014-01-01'
httpretty.reset()
httpretty.register_uri(httpretty.GET, '{0}?start_date={1}'.format(uri, date), body='{}')
course.enrollment(demographic, start_date=date)
httpretty.reset()
httpretty.register_uri(httpretty.GET, '{0}?end_date={1}'.format(uri, date), body='{}')
course.enrollment(demographic, end_date=date)
httpretty.reset()
httpretty.register_uri(httpretty.GET, '{0}?start_date={1}&end_date={1}'.format(uri, date), body='{}')
course.enrollment(demographic, start_date=date, end_date=date)
@httpretty.activate @httpretty.activate
def assertRecentActivityResponseData(self, course, activity_type): def assertRecentActivityResponseData(self, course, activity_type):
...@@ -41,42 +57,6 @@ class CoursesTests(ClientTestCase): ...@@ -41,42 +57,6 @@ class CoursesTests(ClientTestCase):
httpretty.register_uri(httpretty.GET, uri, body=json.dumps(body)) httpretty.register_uri(httpretty.GET, uri, body=json.dumps(body))
self.assertDictEqual(body, self.course.recent_activity(activity_type)) self.assertDictEqual(body, self.course.recent_activity(activity_type))
def test_enrollment_birth_year(self):
data = {
u'birth_years': {
u'1894': 13,
u'1895': 19
}
}
self.assertEnrollmentResponseData(self.course, data, demo.BIRTH_YEAR)
def test_enrollment_education(self):
data = {
u'education_levels': {
u'none': 667,
u'junior_secondary': 6051,
u'primary': 981,
u'associates': 12255,
u'bachelors': 70885,
u'masters': 53216,
u'doctorate': 9940,
u'other': 5722,
u'secondary': 51591
}
}
self.assertEnrollmentResponseData(self.course, data, demo.EDUCATION)
def test_enrollment_gender(self):
data = {
u'genders': {
u'm': 133240,
u'o': 423,
u'f': 77495
}
}
self.assertEnrollmentResponseData(self.course, data, demo.GENDER)
def test_recent_activity(self): def test_recent_activity(self):
self.assertRecentActivityResponseData(self.course, at.ANY) self.assertRecentActivityResponseData(self.course, at.ANY)
self.assertRecentActivityResponseData(self.course, at.ATTEMPTED_PROBLEM) self.assertRecentActivityResponseData(self.course, at.ATTEMPTED_PROBLEM)
...@@ -104,3 +84,10 @@ class CoursesTests(ClientTestCase): ...@@ -104,3 +84,10 @@ class CoursesTests(ClientTestCase):
self.assertRaises(InvalidRequestError, self.course.recent_activity, 'not-a-an-activity-type') self.assertRaises(InvalidRequestError, self.course.recent_activity, 'not-a-an-activity-type')
self.assertRaises(InvalidRequestError, self.course.enrollment, 'not-a-demographic') self.assertRaises(InvalidRequestError, self.course.enrollment, 'not-a-demographic')
def test_enrollment(self):
self.assertCorrectEnrollmentUrl(self.course, None)
self.assertCorrectEnrollmentUrl(self.course, demo.BIRTH_YEAR)
self.assertCorrectEnrollmentUrl(self.course, demo.EDUCATION)
self.assertCorrectEnrollmentUrl(self.course, demo.GENDER)
self.assertCorrectEnrollmentUrl(self.course, demo.LOCATION)
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