This repository has been archived by the owner on Jan 28, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #403 from mitodl/feature/skm/166_xanalytics
import xanalytics data [delivers #96970016]
- Loading branch information
Showing
10 changed files
with
411 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
""" | ||
Tests for hitting the xanalytics Celery tasks. | ||
""" | ||
|
||
from __future__ import unicode_literals | ||
|
||
import json | ||
import logging | ||
|
||
from django.conf import settings | ||
import responses | ||
|
||
from importer.tasks import populate_xanalytics_fields, check_for_results | ||
from learningresources.tests.base import LoreTestCase | ||
|
||
log = logging.getLogger(__name__) | ||
# pylint: disable=no-self-use | ||
|
||
|
||
class TestAPI(LoreTestCase): | ||
""" | ||
Tests using mocks to avoid actual HTTP requests. | ||
""" | ||
def setUp(self): | ||
"""Override setting.""" | ||
super(TestAPI, self).setUp() | ||
self.original_url = settings.XANALYTICS_URL | ||
settings.XANALYTICS_URL = "http://example.com" | ||
|
||
def tearDown(self): | ||
"""Restore original setting.""" | ||
super(TestAPI, self).tearDown() | ||
settings.XANALYTICS_URL = self.original_url | ||
|
||
@responses.activate | ||
def test_populate_xanalytics_fields(self): | ||
"""Test kicking off a request.""" | ||
responses.add( | ||
responses.POST, | ||
'http://example.com/create', | ||
body='{"token": "abcde"}', | ||
content_type="application/json" | ||
) | ||
# We must mock the /status endpoint as well, | ||
# because a status check is queued when /create is called. | ||
reply = {"status": "still busy"} | ||
responses.add( | ||
responses.POST, | ||
'http://example.com/status', | ||
body=json.dumps(reply), | ||
content_type="application/json" | ||
) | ||
populate_xanalytics_fields(1234) | ||
|
||
@responses.activate | ||
def test_check_for_results_waiting(self): | ||
"""Test kicking off a request.""" | ||
reply = {"status": "still busy"} | ||
responses.add( | ||
responses.POST, | ||
'http://example.com/status', | ||
body=json.dumps(reply), | ||
content_type="application/json" | ||
) | ||
check_for_results("abcde", 10, 1) | ||
|
||
@responses.activate | ||
def test_check_for_results_done(self): | ||
"""Test kicking off a request.""" | ||
file_reply = { | ||
"course_id": "123", | ||
"module_medata": [ | ||
{ | ||
"module_id": "1", | ||
"xa_nr_views": "3", | ||
"xa_nr_attempts": "25" | ||
}, | ||
{ | ||
"module_id": "2", | ||
"xa_nr_views": "7", | ||
"xa_nr_attempts": "99" | ||
|
||
} | ||
] | ||
} | ||
responses.add( | ||
responses.GET, | ||
'http://example.com/foo.json', | ||
body=json.dumps(file_reply), | ||
content_type="application/json" | ||
) | ||
|
||
reply = {"status": "complete", "url": "http://example.com/foo.json"} | ||
responses.add( | ||
responses.POST, | ||
'http://example.com/status', | ||
body=json.dumps(reply), | ||
content_type="application/json" | ||
) | ||
check_for_results("abcde", 10, 1) | ||
|
||
@responses.activate | ||
def test_check_for_results_bad(self): | ||
"""Test kicking off a request.""" | ||
responses.add( | ||
responses.GET, | ||
'http://example.com/foo.json', | ||
body='{"invalid": "JSON", "because": "developer comma",}', | ||
content_type="application/json" | ||
) | ||
reply = {"status": "complete", "url": "http://example.com/foo.json"} | ||
responses.add( | ||
responses.POST, | ||
'http://example.com/status', | ||
body=json.dumps(reply), | ||
content_type="application/json" | ||
) | ||
self.assertRaises( | ||
ValueError, | ||
check_for_results("abcde", 10, 1) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
""" | ||
Tests for utilities within the API. | ||
""" | ||
|
||
import logging | ||
|
||
from learningresources.tests.base import LoreTestCase | ||
|
||
from learningresources.api import update_xanalytics | ||
from learningresources.models import LearningResource | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
class TestXanalyticsData(LoreTestCase): | ||
""" | ||
Test good and bad data. | ||
""" | ||
def test_good(self): | ||
"""Reasonable data.""" | ||
self.resource.uuid = "1" | ||
self.resource.save() | ||
resource = LearningResource.objects.get(id=self.resource.id) | ||
|
||
self.assertEqual(resource.xa_nr_views, 0) | ||
self.assertEqual(resource.xa_nr_attempts, 0) | ||
|
||
reasonable = { | ||
"course_id": self.course.course_number, | ||
"module_medata": [ | ||
{ | ||
"module_id": "1", | ||
"xa_nr_views": "3", | ||
"xa_nr_attempts": "25" | ||
}, | ||
] | ||
} | ||
|
||
self.assertEqual(update_xanalytics(reasonable), 1) | ||
resource = LearningResource.objects.get(id=self.resource.id) | ||
self.assertEqual(resource.xa_nr_views, 3) | ||
self.assertEqual(resource.xa_nr_attempts, 25) | ||
|
||
def test_empty(self): | ||
"""Empty dict.""" | ||
self.assertEqual(update_xanalytics({}), 0) | ||
|
||
def test_course_id(self): | ||
"""Missing/bad course ID.""" | ||
data = { | ||
"module_medata": [ | ||
{ | ||
"module_id": "1", | ||
"xa_nr_views": "3", | ||
"xa_nr_attempts": "25" | ||
}, | ||
] | ||
} | ||
self.assertEqual(update_xanalytics(data), 0) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,3 +17,4 @@ ipdb | |
ipython | ||
urltools | ||
semantic_version | ||
responses |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
""" | ||
Functions for communicating with the xanalytics API. | ||
""" | ||
import logging | ||
import os | ||
from tempfile import mkstemp | ||
|
||
from django.conf import settings | ||
from requests.exceptions import ConnectionError as RequestsConnectionError | ||
import requests | ||
|
||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
def _call(url, data): | ||
""" | ||
Make call via requests, trapping common errors. | ||
Args: | ||
url (unicode): URL | ||
data (dict): POST parameters | ||
Returns: | ||
result (dict): Results read as JSON | ||
""" | ||
try: | ||
resp = requests.post(url=url, data=data) | ||
try: | ||
return resp.json() | ||
except ValueError as ex: | ||
log.error("Bad JSON returned: %s", ex.args) | ||
except RequestsConnectionError as ex: | ||
log.error("Unable to connect to xanalytics server: %s", ex.args) | ||
|
||
# Fallback in case things fail. | ||
return {} | ||
|
||
|
||
def send_request(url, course_id): | ||
""" | ||
Send initial request to xanalytics API. | ||
Args: | ||
url (string): URL of xanalytics API | ||
course_id (int): Course primary key. | ||
Returns: | ||
token (unicode): Job token from xanalytics. | ||
""" | ||
return _call(url=url, data={"course_id": course_id}) | ||
|
||
|
||
def get_result(url, token): | ||
""" | ||
Get result from xanalytics for token. | ||
Args: | ||
token (unicode): Token from xanalytics job. | ||
Returns: | ||
data (dict): Dictionary from JSON object provided by xanalytics. | ||
""" | ||
return _call(url=url, data={"token": token}) |
Oops, something went wrong.