Commit fb8c84a5 by Miles Steele

add analytics proxy endpoint

parent 35ffb1b3
...@@ -5,6 +5,7 @@ Unit tests for instructor.api methods. ...@@ -5,6 +5,7 @@ Unit tests for instructor.api methods.
import unittest import unittest
import json import json
from urllib import quote from urllib import quote
from django.conf import settings
from django.test import TestCase from django.test import TestCase
from nose.tools import raises from nose.tools import raises
from mock import Mock from mock import Mock
...@@ -23,6 +24,7 @@ from student.models import CourseEnrollment ...@@ -23,6 +24,7 @@ from student.models import CourseEnrollment
from courseware.models import StudentModule from courseware.models import StudentModule
from instructor.access import allow_access from instructor.access import allow_access
import instructor.views.api
from instructor.views.api import ( from instructor.views.api import (
_split_input_list, _msk_from_problem_urlname, common_exceptions_400) _split_input_list, _msk_from_problem_urlname, common_exceptions_400)
from instructor_task.api_helper import AlreadyRunningError from instructor_task.api_helper import AlreadyRunningError
...@@ -118,6 +120,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -118,6 +120,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
'list_instructor_tasks', 'list_instructor_tasks',
'list_forum_members', 'list_forum_members',
'update_forum_role_membership', 'update_forum_role_membership',
'proxy_legacy_analytics',
] ]
for endpoint in staff_level_endpoints: for endpoint in staff_level_endpoints:
url = reverse(endpoint, kwargs={'course_id': self.course.id}) url = reverse(endpoint, kwargs={'course_id': self.course.id})
...@@ -753,6 +756,95 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -753,6 +756,95 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.assertEqual(json.loads(response.content), expected_res) self.assertEqual(json.loads(response.content), expected_res)
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
@override_settings(ANALYTICS_SERVER_URL="http://robotanalyticsserver.netbot:900/")
@override_settings(ANALYTICS_API_KEY="robot_api_key")
class TestInstructorAPIAnalyticsProxy(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Test instructor analytics proxy endpoint.
"""
class FakeProxyResponse(object):
""" Fake successful requests response object. """
def __init__(self):
self.status_code = instructor.views.api.codes.OK
self.content = '{"test_content": "robot test content"}'
class FakeBadProxyResponse(object):
""" Fake strange-failed requests response object. """
def __init__(self):
self.status_code = 'notok.'
self.content = '{"test_content": "robot test content"}'
def setUp(self):
self.instructor = AdminFactory.create()
self.course = CourseFactory.create()
self.client.login(username=self.instructor.username, password='test')
def test_analytics_proxy_url(self):
""" Test legacy analytics proxy url generation. """
act = Mock(return_value=self.FakeProxyResponse())
instructor.views.api.requests.get = act
url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id})
response = self.client.get(url, {
'aname': 'ProblemGradeDistribution'
})
print response.content
self.assertEqual(response.status_code, 200)
# check request url
expected_url = "{url}get?aname={aname}&course_id={course_id}&apikey={api_key}".format(
url="http://robotanalyticsserver.netbot:900/",
aname="ProblemGradeDistribution",
course_id=self.course.id,
api_key="robot_api_key",
)
act.assert_called_once_with(expected_url)
def test_analytics_proxy(self):
"""
Test legacy analytics content proxying.
"""
act = Mock(return_value=self.FakeProxyResponse())
instructor.views.api.requests.get = act
url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id})
response = self.client.get(url, {
'aname': 'ProblemGradeDistribution'
})
print response.content
self.assertEqual(response.status_code, 200)
# check response
self.assertTrue(act.called)
expected_res = {'test_content': "robot test content"}
self.assertEqual(json.loads(response.content), expected_res)
def test_analytics_proxy_reqfailed(self):
""" Test proxy when server reponds with failure. """
act = Mock(return_value=self.FakeBadProxyResponse())
instructor.views.api.requests.get = act
url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id})
response = self.client.get(url, {
'aname': 'ProblemGradeDistribution'
})
print response.content
self.assertEqual(response.status_code, 500)
def test_analytics_proxy_missing_param(self):
""" Test proxy when missing the aname query parameter. """
act = Mock(return_value=self.FakeProxyResponse())
instructor.views.api.requests.get = act
url = reverse('proxy_legacy_analytics', kwargs={'course_id': self.course.id})
response = self.client.get(url, {})
print response.content
self.assertEqual(response.status_code, 400)
self.assertFalse(act.called)
class TestInstructorAPIHelpers(TestCase): class TestInstructorAPIHelpers(TestCase):
""" Test helpers for instructor.api """ """ Test helpers for instructor.api """
def test_split_input_list(self): def test_split_input_list(self):
......
...@@ -8,11 +8,15 @@ Many of these GETs may become PUTs in the future. ...@@ -8,11 +8,15 @@ Many of these GETs may become PUTs in the future.
import re import re
import logging import logging
import requests
from requests.status_codes import codes
from collections import OrderedDict
from django.conf import settings
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.views.decorators.cache import cache_control from django.views.decorators.cache import cache_control
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.http import HttpResponseBadRequest, HttpResponseForbidden from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from util.json_request import JsonResponse from util.json_request import JsonResponse
from courseware.access import has_access from courseware.access import has_access
...@@ -725,6 +729,57 @@ def update_forum_role_membership(request, course_id): ...@@ -725,6 +729,57 @@ def update_forum_role_membership(request, course_id):
return JsonResponse(response_payload) return JsonResponse(response_payload)
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff')
@require_query_params(
aname="name of analytic to query",
)
@common_exceptions_400
def proxy_legacy_analytics(request, course_id):
"""
Proxies to the analytics cron job server.
`aname` is a query parameter specifying which analytic to query.
"""
analytics_name = request.GET.get('aname')
# abort if misconfigured
if not (hasattr(settings, 'ANALYTICS_SERVER_URL') and hasattr(settings, 'ANALYTICS_API_KEY')):
return HttpResponse("Analytics service not configured.", status=501)
url = "{}get?aname={}&course_id={}&apikey={}".format(
settings.ANALYTICS_SERVER_URL,
analytics_name,
course_id,
settings.ANALYTICS_API_KEY,
)
try:
res = requests.get(url)
except Exception:
log.exception("Error requesting from analytics server at %s", url)
return HttpResponse("Error requesting from analytics server.", status=500)
if res.status_code is 200:
# return the successful request content
return HttpResponse(res.content, content_type="application/json")
elif res.status_code is 404:
# forward the 404 and content
return HttpResponse(res.content, content_type="application/json", status=404)
else:
# 500 on all other unexpected status codes.
log.error(
"Error fetching {}, code: {}, msg: {}".format(
url, res.status_code, res.content
)
)
return HttpResponse(
"Error from analytics server ({}).".format(res.status_code),
status=500
)
def _split_input_list(str_list): def _split_input_list(str_list):
""" """
Separate out individual student email from the comma, or space separated string. Separate out individual student email from the comma, or space separated string.
......
...@@ -30,4 +30,6 @@ urlpatterns = patterns('', # nopep8 ...@@ -30,4 +30,6 @@ urlpatterns = patterns('', # nopep8
'instructor.views.api.list_forum_members', name="list_forum_members"), 'instructor.views.api.list_forum_members', name="list_forum_members"),
url(r'^update_forum_role_membership$', url(r'^update_forum_role_membership$',
'instructor.views.api.update_forum_role_membership', name="update_forum_role_membership"), 'instructor.views.api.update_forum_role_membership', name="update_forum_role_membership"),
url(r'^proxy_legacy_analytics$',
'instructor.views.api.proxy_legacy_analytics', name="proxy_legacy_analytics"),
) )
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