Commit 5b5825b0 by Clinton Blackburn

Merge pull request #9 from edx/enrollment-trends

Updated client to support enrollment date ranges
parents c14f3047 0dc8f482
......@@ -4,7 +4,7 @@ import requests
import requests.exceptions
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
......@@ -81,6 +81,7 @@ class Client(object):
except ClientError:
return False
# pylint: disable=no-member
def _request(self, resource, timeout=None):
if timeout is None:
timeout = self.timeout
......@@ -96,14 +97,14 @@ class Client(object):
response = requests.get(uri, headers=headers, timeout=timeout)
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)
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)
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)
error_class = NotFoundError
......@@ -112,6 +113,11 @@ class Client(object):
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:
message = 'Unable to retrieve resource'
log.exception(message)
......
import urllib
import analyticsclient.activity_type as at
......@@ -17,14 +18,38 @@ class Course(object):
self.client = client
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:
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):
"""
......
......@@ -3,3 +3,4 @@
BIRTH_YEAR = 'birth_year'
EDUCATION = 'education'
GENDER = 'gender'
LOCATION = 'location'
......@@ -14,3 +14,9 @@ class InvalidRequestError(ClientError):
""" The API request was invalid. """
pass
class TimeoutError(ClientError):
""" The API server did not respond before the timeout expired. """
pass
import json
import httpretty
import mock
import requests.exceptions
from analyticsclient.client import Client
from analyticsclient.exceptions import ClientError
from analyticsclient.exceptions import ClientError, TimeoutError
from analyticsclient.tests import ClientTestCase
......@@ -12,33 +14,36 @@ class ClientTests(ClientTestCase):
super(ClientTests, self).setUp()
httpretty.enable()
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):
httpretty.disable()
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)
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)
def test_failed_authentication(self):
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(httpretty.last_request().headers['Authorization'], 'Token atoken')
def test_get(self):
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)
# Bad JSON
httpretty.register_uri(httpretty.GET, self.test_uri, body=json.dumps(data)[:6])
def test_get_invalid_response_body(self):
""" 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):
self.client.get(self.test_endpoint)
......@@ -50,3 +55,17 @@ class ClientTests(ClientTestCase):
url_with_slash = 'http://example.com/'
client = Client(url_with_slash)
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):
super(CoursesTests, self).tearDown()
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))
if 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
def assertRecentActivityResponseData(self, course, activity_type):
......@@ -41,42 +57,6 @@ class CoursesTests(ClientTestCase):
httpretty.register_uri(httpretty.GET, uri, body=json.dumps(body))
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):
self.assertRecentActivityResponseData(self.course, at.ANY)
self.assertRecentActivityResponseData(self.course, at.ATTEMPTED_PROBLEM)
......@@ -104,3 +84,10 @@ class CoursesTests(ClientTestCase):
self.assertRaises(InvalidRequestError, self.course.recent_activity, 'not-a-an-activity-type')
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