From fb8c84a5162758ee578a4b85706cde8effd4a317 Mon Sep 17 00:00:00 2001 From: Miles Steele Date: Thu, 1 Aug 2013 10:56:47 -0400 Subject: [PATCH] add analytics proxy endpoint --- lms/djangoapps/instructor/tests/test_api.py | 92 +++++++++++++++++++++ lms/djangoapps/instructor/views/api.py | 57 ++++++++++++- lms/djangoapps/instructor/views/api_urls.py | 2 + 3 files changed, 150 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index cc2e23e8fe..32682d2b61 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -5,6 +5,7 @@ Unit tests for instructor.api methods. import unittest import json from urllib import quote +from django.conf import settings from django.test import TestCase from nose.tools import raises from mock import Mock @@ -23,6 +24,7 @@ from student.models import CourseEnrollment from courseware.models import StudentModule from instructor.access import allow_access +import instructor.views.api from instructor.views.api import ( _split_input_list, _msk_from_problem_urlname, common_exceptions_400) from instructor_task.api_helper import AlreadyRunningError @@ -118,6 +120,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase): 'list_instructor_tasks', 'list_forum_members', 'update_forum_role_membership', + 'proxy_legacy_analytics', ] for endpoint in staff_level_endpoints: url = reverse(endpoint, kwargs={'course_id': self.course.id}) @@ -753,6 +756,95 @@ class TestInstructorAPITaskLists(ModuleStoreTestCase, LoginEnrollmentTestCase): 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): """ Test helpers for instructor.api """ def test_split_input_list(self): diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 7655fd5b13..e3d060e57a 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -8,11 +8,15 @@ Many of these GETs may become PUTs in the future. import re 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.views.decorators.cache import cache_control from django.core.urlresolvers import reverse 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 courseware.access import has_access @@ -725,6 +729,57 @@ def update_forum_role_membership(request, course_id): 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): """ Separate out individual student email from the comma, or space separated string. diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index 8515b60524..8c67c24a77 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -30,4 +30,6 @@ urlpatterns = patterns('', # nopep8 'instructor.views.api.list_forum_members', name="list_forum_members"), url(r'^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"), )