Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-analytics-data-api
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-analytics-data-api
Commits
1c69bab7
Commit
1c69bab7
authored
Jul 10, 2014
by
Clinton Blackburn
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Moved status URLs to root
Change-Id: Ib52b7d76a7bf6b4046d7df181044ca6647c4b301
parent
91a5b8bf
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
179 additions
and
176 deletions
+179
-176
.coveragerc
+4
-0
Makefile
+3
-3
analytics_data_api/v0/tests.py
+2
-83
analytics_data_api/v0/urls/__init__.py
+0
-5
analytics_data_api/v0/views/operational.py
+0
-73
analyticsdataserver/router.py
+6
-6
analyticsdataserver/tests.py
+83
-0
analyticsdataserver/urls.py
+7
-6
analyticsdataserver/views.py
+73
-0
requirements/test.txt
+1
-0
No files found.
.coveragerc
View file @
1c69bab7
[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 =
...
...
Makefile
View file @
1c69bab7
ROOT
=
$(
shell
echo
"
$$
PWD"
)
ROOT
=
$(
shell
echo
"
$$
PWD"
)
COVERAGE
=
$(ROOT)
/build/coverage
COVERAGE
=
$(ROOT)
/build/coverage
PACKAGES
=
analytics_data_api
PACKAGES
=
analytics
dataserver 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
:
...
...
analytics_data_api/v0/tests.py
View file @
1c69bab7
...
@@ -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
...
...
analytics_data_api/v0/urls/__init__.py
View file @
1c69bab7
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'
)),
)
)
analytics_data_api/v0/views/operational.py
deleted
100644 → 0
View file @
91a5b8bf
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
)
analyticsdataserver/router.py
View file @
1c69bab7
...
@@ -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
,
d
b
,
model
):
def
allow_syncdb
(
self
,
d
atabase
,
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
d
b
==
dest_db
return
d
atabase
==
dest_db
else
:
else
:
return
None
return
None
analyticsdataserver/tests.py
0 → 100644
View file @
1c69bab7
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'
)
analyticsdataserver/urls.py
View file @
1c69bab7
...
@@ -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
)))
...
...
analyticsdataserver/views.py
View file @
1c69bab7
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
)
requirements/test.txt
View file @
1c69bab7
...
@@ -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
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment