Commit 1c69bab7 by Clinton Blackburn

Moved status URLs to root

Change-Id: Ib52b7d76a7bf6b4046d7df181044ca6647c4b301
parent 91a5b8bf
[run]
omit = analyticsdataserver/settings*
*wsgi.py
[report] [report]
# Regexes for lines to exclude from consideration # Regexes for lines to exclude from consideration
exclude_lines = exclude_lines =
......
ROOT = $(shell echo "$$PWD") ROOT = $(shell echo "$$PWD")
COVERAGE = $(ROOT)/build/coverage COVERAGE = $(ROOT)/build/coverage
PACKAGES = analytics_data_api PACKAGES = analyticsdataserver analytics_data_api
DATABASES = default analytics DATABASES = default analytics
.PHONY: requirements develop clean diff.report view.diff.report quality syncdb .PHONY: requirements develop clean diff.report view.diff.report quality syncdb
...@@ -21,10 +21,10 @@ clean: ...@@ -21,10 +21,10 @@ clean:
test: clean test: clean
. ./.test_env && ./manage.py test --settings=analyticsdataserver.settings.test \ . ./.test_env && ./manage.py test --settings=analyticsdataserver.settings.test \
--with-coverage --cover-inclusive --cover-branches \ --exclude-dir=analyticsdataserver/settings --with-coverage --cover-inclusive --cover-branches \
--cover-html --cover-html-dir=$(COVERAGE)/html/ \ --cover-html --cover-html-dir=$(COVERAGE)/html/ \
--cover-xml --cover-xml-file=$(COVERAGE)/coverage.xml \ --cover-xml --cover-xml-file=$(COVERAGE)/coverage.xml \
--cover-package=$(PACKAGES) \ $(foreach package,$(PACKAGES),--cover-package=$(package)) \
$(PACKAGES) $(PACKAGES)
diff.report: diff.report:
......
...@@ -2,98 +2,17 @@ ...@@ -2,98 +2,17 @@
# change for versions greater than 1.0.0. Tests target a specific version of the API, additional tests should be added # change for versions greater than 1.0.0. Tests target a specific version of the API, additional tests should be added
# for subsequent versions if there are breaking changes introduced in those versions. # for subsequent versions if there are breaking changes introduced in those versions.
from contextlib import contextmanager
from datetime import datetime from datetime import datetime
from functools import partial
import random import random
from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.utils import ConnectionHandler, DatabaseError
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings
from django_dynamic_fixture import G from django_dynamic_fixture import G
from mock import patch, Mock
import mock
import pytz import pytz
from rest_framework.authtoken.models import Token
from analytics_data_api.v0.models import CourseEnrollmentByBirthYear, CourseEnrollmentByEducation, EducationLevel, \ from analytics_data_api.v0.models import CourseEnrollmentByBirthYear, CourseEnrollmentByEducation, EducationLevel, \
CourseEnrollmentByGender, CourseActivityByWeek, Course CourseEnrollmentByGender, CourseActivityByWeek, Course
from analyticsdataserver.tests import TestCaseWithAuthentication
class TestCaseWithAuthentication(TestCase):
def setUp(self):
super(TestCaseWithAuthentication, self).setUp()
test_user = User.objects.create_user('tester', 'test@example.com', 'testpassword')
token = Token.objects.create(user=test_user)
self.authenticated_get = partial(self.client.get, HTTP_AUTHORIZATION='Token ' + token.key)
@contextmanager
def no_database():
cursor_mock = Mock(side_effect=DatabaseError)
with mock.patch('django.db.backends.util.CursorWrapper', cursor_mock):
yield
class OperationalEndpointsTest(TestCaseWithAuthentication):
def test_status(self):
response = self.client.get('/api/v0/status')
self.assertEquals(response.status_code, 200)
def test_authentication_check_failure(self):
response = self.client.get('/api/v0/authenticated')
self.assertEquals(response.status_code, 401)
def test_authentication_check_success(self):
response = self.authenticated_get('/api/v0/authenticated')
self.assertEquals(response.status_code, 200)
def test_health(self):
self.assert_database_health('OK')
def assert_database_health(self, status):
response = self.client.get('/api/v0/health')
self.assertEquals(
response.data,
{
'overall_status': status,
'detailed_status': {
'database_connection': status
}
}
)
self.assertEquals(response.status_code, 200)
@staticmethod
@contextmanager
def override_database_connections(databases):
with patch('analytics_data_api.v0.views.operational.connections', ConnectionHandler(databases)):
yield
@override_settings(ANALYTICS_DATABASE='reporting')
def test_read_setting(self):
databases = dict(settings.DATABASES)
databases['reporting'] = {}
with self.override_database_connections(databases):
self.assert_database_health('UNAVAILABLE')
# Workaround to remove a setting from django settings. It has to be used in override_settings and then deleted.
@override_settings(ANALYTICS_DATABASE='reporting')
def test_default_setting(self):
del settings.ANALYTICS_DATABASE
databases = dict(settings.DATABASES)
databases['reporting'] = {}
with self.override_database_connections(databases):
# This would normally return UNAVAILABLE, however we have deleted the settings so it will use the default
# connection which should be OK.
self.assert_database_health('OK')
class CourseActivityLastWeekTest(TestCaseWithAuthentication): class CourseActivityLastWeekTest(TestCaseWithAuthentication):
...@@ -179,7 +98,7 @@ class CourseEnrollmentViewTestCase(object): ...@@ -179,7 +98,7 @@ class CourseEnrollmentViewTestCase(object):
course_id = self._get_non_existent_course_id() course_id = self._get_non_existent_course_id()
self.assertFalse(self.model.objects.filter(course__course_id=course_id).exists()) # pylint: disable=no-member self.assertFalse(self.model.objects.filter(course__course_id=course_id).exists()) # pylint: disable=no-member
response = self.authenticated_get('/api/v0/courses/%s%s' % (course_id, self.path)) # pylint: disable=no-member response = self.authenticated_get('/api/v0/courses/%s%s' % (course_id, self.path)) # pylint: disable=no-member
self.assertEquals(response.status_code, 404) # pylint: disable=no-member self.assertEquals(response.status_code, 404) # pylint: disable=no-member
def test_get(self): def test_get(self):
raise NotImplementedError raise NotImplementedError
......
from django.conf.urls import patterns, url, include from django.conf.urls import patterns, url, include
from analytics_data_api.v0.views import operational
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'^status$', operational.StatusView.as_view()),
url(r'^authenticated$', operational.AuthenticationTestView.as_view()),
url(r'^health$', operational.HealthView.as_view()),
url(r'^courses/', include('analytics_data_api.v0.urls.courses', namespace='courses')), url(r'^courses/', include('analytics_data_api.v0.urls.courses', namespace='courses')),
) )
from rest_framework import permissions
from rest_framework.response import Response
from django.conf import settings
from django.db import connections
from rest_framework.views import APIView
class StatusView(APIView):
"""
Simple check to determine if the server is alive
Return no data, a simple 200 OK status code is sufficient to indicate that the server is alive. This endpoint is
public and does not require an authentication token to access it.
"""
permission_classes = (permissions.AllowAny,)
def get(self, request, *args, **kwargs): # pylint: disable=unused-argument
return Response({})
class AuthenticationTestView(APIView):
"""
Verifies that the client is authenticated
Returns HTTP 200 if client is authenticated, HTTP 401 if not authenticated
"""
def get(self, request, *args, **kwargs): # pylint: disable=unused-argument
return Response({})
class HealthView(APIView):
"""
A more comprehensive check to see if the system is fully operational.
This endpoint is public and does not require an authentication token to access it.
The returned structure contains the following fields:
- overall_status: Can be either "OK" or "UNAVAILABLE".
- detailed_status: More detailed information about the status of the system.
- database_connection: Status of the database connection. Can be either "OK" or "UNAVAILABLE".
"""
permission_classes = (permissions.AllowAny,)
def get(self, request, *args, **kwargs): # pylint: disable=unused-argument
overall_status = 'UNAVAILABLE'
db_conn_status = 'UNAVAILABLE'
try:
connection_name = getattr(settings, 'ANALYTICS_DATABASE', 'default')
cursor = connections[connection_name].cursor()
try:
cursor.execute("SELECT 1")
cursor.fetchone()
overall_status = 'OK'
db_conn_status = 'OK'
finally:
cursor.close()
except Exception: # pylint: disable=broad-except
pass
response = {
"overall_status": overall_status,
"detailed_status": {
'database_connection': db_conn_status
}
}
return Response(response)
...@@ -2,11 +2,11 @@ from django.conf import settings ...@@ -2,11 +2,11 @@ from django.conf import settings
class DatabaseFromSettingRouter(object): class DatabaseFromSettingRouter(object):
def db_for_read(self, model, **hints): def db_for_read(self, model, **hints): # pylint: disable=unused-argument
return self._get_database(model) return self._get_database(model)
def _get_database(self, model): def _get_database(self, model):
if model._meta.app_label == 'v0': if model._meta.app_label == 'v0': # pylint: disable=protected-access
return getattr(settings, 'ANALYTICS_DATABASE', 'default') return getattr(settings, 'ANALYTICS_DATABASE', 'default')
if getattr(model, 'db_from_setting', None): if getattr(model, 'db_from_setting', None):
...@@ -14,15 +14,15 @@ class DatabaseFromSettingRouter(object): ...@@ -14,15 +14,15 @@ class DatabaseFromSettingRouter(object):
return None return None
def db_for_write(self, model, **hints): def db_for_write(self, model, **hints): # pylint: disable=unused-argument
return self._get_database(model) return self._get_database(model)
def allow_relation(self, obj1, obj2, **hints): def allow_relation(self, obj1, obj2, **hints): # pylint: disable=unused-argument
return self._get_database(obj1) == self._get_database(obj2) return self._get_database(obj1) == self._get_database(obj2)
def allow_syncdb(self, db, model): def allow_syncdb(self, database, model):
dest_db = self._get_database(model) dest_db = self._get_database(model)
if dest_db is not None: if dest_db is not None:
return db == dest_db return database == dest_db
else: else:
return None return None
from contextlib import contextmanager
from functools import partial
from django.conf import settings
from django.contrib.auth.models import User
from django.db.utils import ConnectionHandler, DatabaseError
from django.test import TestCase
from django.test.utils import override_settings
from mock import patch, Mock
import mock
from rest_framework.authtoken.models import Token
class TestCaseWithAuthentication(TestCase):
def setUp(self):
super(TestCaseWithAuthentication, self).setUp()
test_user = User.objects.create_user('tester', 'test@example.com', 'testpassword')
token = Token.objects.create(user=test_user)
self.authenticated_get = partial(self.client.get, HTTP_AUTHORIZATION='Token ' + token.key)
@contextmanager
def no_database():
cursor_mock = Mock(side_effect=DatabaseError)
with mock.patch('django.db.backends.util.CursorWrapper', cursor_mock):
yield
class OperationalEndpointsTest(TestCaseWithAuthentication):
def test_status(self):
response = self.client.get('/status', follow=True)
self.assertEquals(response.status_code, 200)
def test_authentication_check_failure(self):
response = self.client.get('/authenticated', follow=True)
self.assertEquals(response.status_code, 401)
def test_authentication_check_success(self):
response = self.authenticated_get('/authenticated', follow=True)
self.assertEquals(response.status_code, 200)
def test_health(self):
self.assert_database_health('OK')
def assert_database_health(self, status):
response = self.client.get('/health', follow=True)
self.assertEquals(
response.data,
{
'overall_status': status,
'detailed_status': {
'database_connection': status
}
}
)
self.assertEquals(response.status_code, 200)
@staticmethod
@contextmanager
def override_database_connections(databases):
with patch('analyticsdataserver.views.connections', ConnectionHandler(databases)):
yield
@override_settings(ANALYTICS_DATABASE='reporting')
def test_read_setting(self):
databases = dict(settings.DATABASES)
databases['reporting'] = {}
with self.override_database_connections(databases):
self.assert_database_health('UNAVAILABLE')
# Workaround to remove a setting from django settings. It has to be used in override_settings and then deleted.
@override_settings(ANALYTICS_DATABASE='reporting')
def test_default_setting(self):
del settings.ANALYTICS_DATABASE
databases = dict(settings.DATABASES)
databases['reporting'] = {}
with self.override_database_connections(databases):
# This would normally return UNAVAILABLE, however we have deleted the settings so it will use the default
# connection which should be OK.
self.assert_database_health('OK')
...@@ -2,24 +2,25 @@ from django.conf import settings ...@@ -2,24 +2,25 @@ from django.conf import settings
from django.conf.urls import patterns, include, url from django.conf.urls import patterns, include, url
from django.contrib import admin from django.contrib import admin
from django.views.generic import RedirectView from django.views.generic import RedirectView
from analyticsdataserver import views
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'^$', RedirectView.as_view(url='/docs')), url(r'^$', RedirectView.as_view(url='/docs')), # pylint: disable=no-value-for-parameter
# Support logging in to the browseable API
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
# Support generating tokens using a POST request
url(r'^api-token-auth/', 'rest_framework.authtoken.views.obtain_auth_token'), url(r'^api-token-auth/', 'rest_framework.authtoken.views.obtain_auth_token'),
# Route all reports URLs to this endpoint
url(r'^api/', include('analytics_data_api.urls', namespace='api')), url(r'^api/', include('analytics_data_api.urls', namespace='api')),
url(r'^docs/', include('rest_framework_swagger.urls')), url(r'^docs/', include('rest_framework_swagger.urls')),
url(r'^status/$', views.StatusView.as_view()),
url(r'^authenticated/$', views.AuthenticationTestView.as_view()),
url(r'^health/$', views.HealthView.as_view()),
) )
if settings.ENABLE_ADMIN_SITE: if settings.ENABLE_ADMIN_SITE: # pragma: no cover
admin.autodiscover() admin.autodiscover()
urlpatterns += patterns('', url(r'^site/admin/', include(admin.site.urls))) urlpatterns += patterns('', url(r'^site/admin/', include(admin.site.urls)))
......
from django.http import HttpResponse from django.http import HttpResponse
from rest_framework.renderers import JSONRenderer from rest_framework.renderers import JSONRenderer
from rest_framework import permissions
from rest_framework.response import Response
from django.conf import settings
from django.db import connections
from rest_framework.views import APIView
def handle_internal_server_error(_request): def handle_internal_server_error(_request):
...@@ -20,3 +25,71 @@ def _handle_error(status_code): ...@@ -20,3 +25,71 @@ def _handle_error(status_code):
renderer = JSONRenderer() renderer = JSONRenderer()
content_type = '{media}; charset={charset}'.format(media=renderer.media_type, charset=renderer.charset) content_type = '{media}; charset={charset}'.format(media=renderer.media_type, charset=renderer.charset)
return HttpResponse(renderer.render(info), content_type=content_type, status=status_code) return HttpResponse(renderer.render(info), content_type=content_type, status=status_code)
class StatusView(APIView):
"""
Simple check to determine if the server is alive
Return no data, a simple 200 OK status code is sufficient to indicate that the server is alive. This endpoint is
public and does not require an authentication token to access it.
"""
permission_classes = (permissions.AllowAny,)
def get(self, request, *args, **kwargs): # pylint: disable=unused-argument
return Response({})
class AuthenticationTestView(APIView):
"""
Verifies that the client is authenticated
Returns HTTP 200 if client is authenticated, HTTP 401 if not authenticated
"""
def get(self, request, *args, **kwargs): # pylint: disable=unused-argument
return Response({})
class HealthView(APIView):
"""
A more comprehensive check to see if the system is fully operational.
This endpoint is public and does not require an authentication token to access it.
The returned structure contains the following fields:
- overall_status: Can be either "OK" or "UNAVAILABLE".
- detailed_status: More detailed information about the status of the system.
- database_connection: Status of the database connection. Can be either "OK" or "UNAVAILABLE".
"""
permission_classes = (permissions.AllowAny,)
def get(self, request, *args, **kwargs): # pylint: disable=unused-argument
overall_status = 'UNAVAILABLE'
db_conn_status = 'UNAVAILABLE'
try:
connection_name = getattr(settings, 'ANALYTICS_DATABASE', 'default')
cursor = connections[connection_name].cursor()
try:
cursor.execute("SELECT 1")
cursor.fetchone()
overall_status = 'OK'
db_conn_status = 'OK'
finally:
cursor.close()
except Exception: # pylint: disable=broad-except
pass
response = {
"overall_status": overall_status,
"detailed_status": {
'database_connection': db_conn_status
}
}
return Response(response)
...@@ -6,6 +6,7 @@ django-dynamic-fixture==1.7.0 ...@@ -6,6 +6,7 @@ django-dynamic-fixture==1.7.0
django-nose==1.2 django-nose==1.2
mock==1.0.1 mock==1.0.1
nose==1.3.3 nose==1.3.3
nose-exclude==0.2.0
pep257==0.3.2 pep257==0.3.2
pep8==1.5.7 pep8==1.5.7
pylint==1.2.1 pylint==1.2.1
......
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