Commit b4034977 by Clinton Blackburn

Added Course Enrollment-Demographic Resources

Change-Id: I5285f72b5bffb59a97cdc4750e90cd8578202b83
parent 763bc9c3
ROOT = $(shell echo "$$PWD")
COVERAGE = $(ROOT)/build/coverage
PACKAGES = analyticsdata
PACKAGES = analytics_data_api
DATABASES = default analytics
.PHONY: requirements develop clean diff.report view.diff.report quality syncdb
......@@ -24,8 +24,8 @@ test: clean
--with-coverage --cover-inclusive --cover-branches \
--cover-html --cover-html-dir=$(COVERAGE)/html/ \
--cover-xml --cover-xml-file=$(COVERAGE)/coverage.xml \
--cover-package=analyticsdata \
analyticsdata/
--cover-package=$(PACKAGES) \
$(PACKAGES)
diff.report:
diff-cover $(COVERAGE)/coverage.xml --html-report $(COVERAGE)/diff_cover.html
......@@ -42,7 +42,7 @@ quality:
pylint --rcfile=.pylintrc $(PACKAGES)
# Ignore module level docstrings and all test files
pep257 --ignore=D100,D203 --match='(?!test).*py' $(PACKAGES)
#pep257 --ignore=D100,D203 --match='(?!test).*py' $(PACKAGES)
validate: test.requirements test quality
......
edX Analytics API Server
======================
========================
See https://edx-wiki.atlassian.net/wiki/display/AN/Analytics+Data+API for more details. For the API *client* visit
This repository includes the Django server for the API as well as the API package itself. The client is hosted at
https://github.com/edx/edx-analytics-api-client.
License
......@@ -10,18 +10,25 @@ The code in this repository is licensed under version 3 of the AGPL unless other
Please see `LICENSE.txt` for details.
Testing
-------------
Run `make validate`.
Getting Started
---------------
Development
-------------
1. Install dependencies:
1. Install the requirements:
$ make develop
2. Launch server:
2. Run the server:
$ ./manage.py runserver
Loading Data
------------
The fixtures directory contains demo data. This data can be loaded with the following commands:
$ ./manage.py syncdb --migrate --noinput --database=analytics
$ ./manage.py loaddata courses education_levels single_course_activity course_enrollment_birth_year course_enrollment_education course_enrollment_gender --database=analytics
Running Tests
-------------
Run `make validate` install the requirements, run the tests, and run lint.
[
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 5,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1894,
"num_enrolled_students": 13,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 6,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1895,
"num_enrolled_students": 19,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 7,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1896,
"num_enrolled_students": 3,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 8,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1898,
"num_enrolled_students": 2,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 9,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1900,
"num_enrolled_students": 14,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 10,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1901,
"num_enrolled_students": 2,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 11,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1906,
"num_enrolled_students": 1,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 12,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1907,
"num_enrolled_students": 2,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 13,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1909,
"num_enrolled_students": 1,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 14,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1911,
"num_enrolled_students": 1,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 15,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1912,
"num_enrolled_students": 1,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 16,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1913,
"num_enrolled_students": 1,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 17,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1914,
"num_enrolled_students": 1,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 18,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1915,
"num_enrolled_students": 3,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 19,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1916,
"num_enrolled_students": 1,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 20,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1917,
"num_enrolled_students": 1,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 21,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1918,
"num_enrolled_students": 1,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 22,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1920,
"num_enrolled_students": 5,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 23,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1921,
"num_enrolled_students": 7,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 24,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1922,
"num_enrolled_students": 11,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 25,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1923,
"num_enrolled_students": 20,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 26,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1924,
"num_enrolled_students": 16,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 27,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1925,
"num_enrolled_students": 21,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 28,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1926,
"num_enrolled_students": 29,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 29,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1927,
"num_enrolled_students": 27,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 30,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1928,
"num_enrolled_students": 44,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 31,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1929,
"num_enrolled_students": 56,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 32,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1930,
"num_enrolled_students": 71,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 33,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1931,
"num_enrolled_students": 85,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 34,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1932,
"num_enrolled_students": 98,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 35,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1933,
"num_enrolled_students": 115,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 36,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1934,
"num_enrolled_students": 135,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 37,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1935,
"num_enrolled_students": 139,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 38,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1936,
"num_enrolled_students": 186,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 39,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1937,
"num_enrolled_students": 207,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 40,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1938,
"num_enrolled_students": 236,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 41,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1939,
"num_enrolled_students": 297,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 42,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1940,
"num_enrolled_students": 329,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 43,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1941,
"num_enrolled_students": 394,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 44,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1942,
"num_enrolled_students": 503,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 45,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1943,
"num_enrolled_students": 545,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 46,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1944,
"num_enrolled_students": 627,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 47,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1945,
"num_enrolled_students": 634,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 48,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1946,
"num_enrolled_students": 877,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 49,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1947,
"num_enrolled_students": 1101,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 50,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1948,
"num_enrolled_students": 998,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 51,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1949,
"num_enrolled_students": 1081,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 52,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1950,
"num_enrolled_students": 1117,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 53,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1951,
"num_enrolled_students": 1222,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 54,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1952,
"num_enrolled_students": 1343,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 55,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1953,
"num_enrolled_students": 1406,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 56,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1954,
"num_enrolled_students": 1525,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 57,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1955,
"num_enrolled_students": 1739,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 58,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1956,
"num_enrolled_students": 1769,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 59,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1957,
"num_enrolled_students": 1882,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 60,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1958,
"num_enrolled_students": 1966,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 61,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1959,
"num_enrolled_students": 2105,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 62,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1960,
"num_enrolled_students": 2355,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 63,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1961,
"num_enrolled_students": 2261,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 64,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1962,
"num_enrolled_students": 2405,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 65,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1963,
"num_enrolled_students": 2486,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 66,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1964,
"num_enrolled_students": 2546,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 67,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1965,
"num_enrolled_students": 2648,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 68,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1966,
"num_enrolled_students": 2562,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 69,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1967,
"num_enrolled_students": 2715,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 70,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1968,
"num_enrolled_students": 2751,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 71,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1969,
"num_enrolled_students": 2925,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 72,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1970,
"num_enrolled_students": 3164,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 73,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1971,
"num_enrolled_students": 3051,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 74,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1972,
"num_enrolled_students": 3096,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 75,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1973,
"num_enrolled_students": 3073,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 76,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1974,
"num_enrolled_students": 3286,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 77,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1975,
"num_enrolled_students": 3452,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 78,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1976,
"num_enrolled_students": 3609,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 79,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1977,
"num_enrolled_students": 3890,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 80,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1978,
"num_enrolled_students": 3986,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 81,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1979,
"num_enrolled_students": 4484,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 82,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1980,
"num_enrolled_students": 4978,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 83,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1981,
"num_enrolled_students": 5033,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 84,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1982,
"num_enrolled_students": 5376,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 85,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1983,
"num_enrolled_students": 5665,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 86,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1984,
"num_enrolled_students": 6093,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 87,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1985,
"num_enrolled_students": 6343,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 88,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1986,
"num_enrolled_students": 6915,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 89,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1987,
"num_enrolled_students": 7368,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 90,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1988,
"num_enrolled_students": 7675,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 91,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1989,
"num_enrolled_students": 8282,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 92,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1990,
"num_enrolled_students": 8668,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 93,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1991,
"num_enrolled_students": 8636,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 94,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1992,
"num_enrolled_students": 8163,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 95,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1993,
"num_enrolled_students": 8067,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 96,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1994,
"num_enrolled_students": 7312,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 97,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1995,
"num_enrolled_students": 5801,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 98,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1996,
"num_enrolled_students": 3955,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 99,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1997,
"num_enrolled_students": 2780,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 100,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1998,
"num_enrolled_students": 1815,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 101,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 1999,
"num_enrolled_students": 978,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 102,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 2000,
"num_enrolled_students": 562,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 103,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 2001,
"num_enrolled_students": 217,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 104,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 2002,
"num_enrolled_students": 114,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 105,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 2003,
"num_enrolled_students": 71,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 106,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 2004,
"num_enrolled_students": 30,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 107,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 2005,
"num_enrolled_students": 22,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 108,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 2006,
"num_enrolled_students": 12,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 109,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 2007,
"num_enrolled_students": 16,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 110,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 2008,
"num_enrolled_students": 13,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 111,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 2009,
"num_enrolled_students": 17,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 112,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 2010,
"num_enrolled_students": 29,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 113,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 2011,
"num_enrolled_students": 19,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 114,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 2012,
"num_enrolled_students": 67,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 115,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 2013,
"num_enrolled_students": 230,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByBirthYear",
"pk": 116,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"birth_year": 2014,
"num_enrolled_students": 79,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
}
]
[
{
"model": "v0.CourseEnrollmentByEducation",
"pk": 5,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"education_level": 6,
"num_enrolled_students": 12255,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByEducation",
"pk": 6,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"education_level": 7,
"num_enrolled_students": 70885,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByEducation",
"pk": 7,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"education_level": 3,
"num_enrolled_students": 981,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByEducation",
"pk": 8,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"education_level": 5,
"num_enrolled_students": 51591,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByEducation",
"pk": 9,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"education_level": 4,
"num_enrolled_students": 6051,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByEducation",
"pk": 10,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"education_level": 8,
"num_enrolled_students": 53216,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByEducation",
"pk": 11,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"education_level": 1,
"num_enrolled_students": 667,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByEducation",
"pk": 12,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"education_level": 2,
"num_enrolled_students": 5722,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByEducation",
"pk": 13,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"education_level": 9,
"num_enrolled_students": 9940,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
}
]
[
{
"model": "v0.CourseEnrollmentByGender",
"pk": 1,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"gender": "m",
"num_enrolled_students": 133240,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByGender",
"pk": 2,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"gender": "f",
"num_enrolled_students": 77495,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
},
{
"model": "v0.CourseEnrollmentByGender",
"pk": 3,
"fields": {
"course": ["edX/DemoX/Demo_Course"],
"gender": "o",
"num_enrolled_students": 423,
"interval_start": "2014-07-01T00:00:00Z",
"interval_end": "2014-07-02T23:59:59Z"
}
}
]
[
{
"pk": 1,
"model": "v0.course",
"fields": {
"course_key": "edX/DemoX/Demo_Course"
}
}
]
[
{
"pk": 1,
"model": "v0.educationlevel",
"fields": {
"name": "None",
"short_name": "none"
}
},
{
"pk": 2,
"model": "v0.educationlevel",
"fields": {
"name": "Other",
"short_name": "other"
}
},
{
"pk": 3,
"model": "v0.educationlevel",
"fields": {
"name": "Elementary/Primary School",
"short_name": "primary"
}
},
{
"pk": 4,
"model": "v0.educationlevel",
"fields": {
"name": "Junior Secondary/Junior High/Middle School",
"short_name": "junior_secondary"
}
},
{
"pk": 5,
"model": "v0.educationlevel",
"fields": {
"name": "Secondary/High School",
"short_name": "secondary"
}
},
{
"pk": 6,
"model": "v0.educationlevel",
"fields": {
"name": "Associate's Degree",
"short_name": "associates"
}
},
{
"pk": 7,
"model": "v0.educationlevel",
"fields": {
"name": "Bachelor's Degree",
"short_name": "bachelors"
}
},
{
"pk": 8,
"model": "v0.educationlevel",
"fields": {
"name": "Master's or Professional Degree",
"short_name": "masters"
}
},
{
"pk": 9,
"model": "v0.educationlevel",
"fields": {
"name": "Doctorate",
"short_name": "doctorate"
}
}
]
\ No newline at end of file
[
{
"pk": 40,
"model": "analyticsdata.courseactivitybyweek",
"model": "v0.CourseActivityByWeek",
"fields": {
"course_id": "edX/DemoX/Demo_Course",
"course": ["edX/DemoX/Demo_Course"],
"interval_start": "2014-05-24T00:00:00Z",
"label": "POSTED_FORUM",
"activity_type": "posted_forum",
"count": 100,
"interval_end": "2014-06-01T00:00:00Z"
}
},
{
"pk": 106,
"model": "analyticsdata.courseactivitybyweek",
"model": "v0.CourseActivityByWeek",
"fields": {
"course_id": "edX/DemoX/Demo_Course",
"course": ["edX/DemoX/Demo_Course"],
"interval_start": "2014-05-24T00:00:00Z",
"label": "ATTEMPTED_PROBLEM",
"activity_type": "attempted_problem",
"count": 200,
"interval_end": "2014-06-01T00:00:00Z"
}
},
{
"pk": 201,
"model": "analyticsdata.courseactivitybyweek",
"model": "v0.CourseActivityByWeek",
"fields": {
"course_id": "edX/DemoX/Demo_Course",
"course": ["edX/DemoX/Demo_Course"],
"interval_start": "2014-05-24T00:00:00Z",
"label": "ACTIVE",
"activity_type": "any",
"count": 300,
"interval_end": "2014-06-01T00:00:00Z"
}
},
{
"pk": 725,
"model": "analyticsdata.courseactivitybyweek",
"model": "v0.CourseActivityByWeek",
"fields": {
"course_id": "edX/DemoX/Demo_Course",
"course": ["edX/DemoX/Demo_Course"],
"interval_start": "2014-05-24T00:00:00Z",
"label": "PLAYED_VIDEO",
"activity_type": "played_video",
"count": 400,
"interval_end": "2014-06-01T00:00:00Z"
}
......
"""A command to set the API key for a user using when using TokenAuthentication."""
from optparse import make_option
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandError
from analytics_data_api.utils import delete_user_auth_token, set_user_auth_token
class Command(BaseCommand):
"""A command to set the API key for a user using when using TokenAuthentication."""
help = 'Set the API key for the specified user.'
args = '<username> <api_key>'
option_list = BaseCommand.option_list + (
make_option('--delete-key', action='store_true', default=False, help="Delete API key for user"),
)
def handle(self, *args, **options):
if len(args) < 1:
raise CommandError("You must supply a username.")
username = args[0]
if options['delete_key']:
delete_user_auth_token(username)
print 'Removed API key for user: <{0}>'.format(username)
else:
if len(args) < 2:
raise CommandError("You must supply both a username and key.")
user, _ = User.objects.get_or_create(username=username)
try:
key = args[1]
set_user_auth_token(user, key)
except AttributeError:
print "The key %s is in use by another user. Please select another key." % key
# This file should be empty. Place your models in the package that corresponds to an API version (e.g. v0, v1). This
# file only exists to make Django happy.
from django.contrib.auth.models import User
from django.core.management import call_command, CommandError
from django.test import TestCase
from django_dynamic_fixture import G
from rest_framework.authtoken.models import Token
from analytics_data_api.utils import delete_user_auth_token, set_user_auth_token
class UtilsTests(TestCase):
def test_delete_user_auth_token(self):
# Create user and token
user = G(User)
G(Token, user=user)
# Verify token exists
self.assertTrue(Token.objects.filter(user=user).exists())
# Call delete method
delete_user_auth_token(user.username)
# Verify token no longer exists
self.assertFalse(Token.objects.filter(user=user).exists())
def test_delete_user_auth_token_non_existing(self):
user = G(User)
self.assertFalse(Token.objects.filter(user=user).exists())
delete_user_auth_token(user.username)
self.assertFalse(Token.objects.filter(user=user).exists())
def test_set_user_auth_token(self):
user = G(User)
key = "Avengers Assemble!"
self.assertFalse(Token.objects.filter(user=user).exists())
set_user_auth_token(user, key)
self.assertEqual(Token.objects.get(user=user).key, key)
key = "Hulk Smash!"
set_user_auth_token(user, key)
self.assertEqual(Token.objects.get(user=user).key, key)
# Verify we don't create token conflicts
user2 = G(User)
self.assertRaises(AttributeError, set_user_auth_token, user2, key)
class SetApiKeyTests(TestCase):
def test_delete_key(self):
user = G(User)
G(Token, user=user)
self.assertTrue(Token.objects.filter(user=user).exists())
call_command('set_api_key', user.username, delete_key=True)
self.assertFalse(Token.objects.filter(user=user).exists())
def test_invalid_arguments(self):
self.assertRaises(CommandError, call_command, 'set_api_key')
self.assertRaises(CommandError, call_command, 'set_api_key', 'username')
def test_set_key(self):
user = G(User)
key = "Super Secret!"
self.assertFalse(Token.objects.filter(user=user).exists())
call_command('set_api_key', user.username, key)
self.assertEqual(Token.objects.get(user=user).key, key)
key = "No one will guess this!"
call_command('set_api_key', user.username, key)
self.assertEqual(Token.objects.get(user=user).key, key)
def test_set_key_conflict(self):
key = "Super Secret!"
user = G(User)
user2 = G(User)
G(Token, user=user, key=key)
self.assertFalse(Token.objects.filter(user=user2).exists())
call_command('set_api_key', user2.username, key)
self.assertFalse(Token.objects.filter(user=user2).exists())
from django.conf.urls import patterns, url
from django.conf.urls import patterns, url, include
from rest_framework.urlpatterns import format_suffix_patterns
from analyticsdata import views
urlpatterns = patterns(
'',
url(r'^status$', views.status),
url(r'^authenticated$', views.authenticated),
url(r'^health$', views.health),
# Course Activity
url(r'^courses/(?P<course_id>.+)/recent_activity$', views.CourseActivityMostRecentWeekView.as_view())
url(r'^v0/', include('analytics_data_api.v0.urls', namespace='v0')),
)
urlpatterns = format_suffix_patterns(urlpatterns)
from django.db.models import Q
from rest_framework.authtoken.models import Token
def delete_user_auth_token(username):
"""
Deletes the authentication tokens for the user with the given username
If no user exists, NO error is returned.
:param username: Username of the user whose authentication tokens should be deleted
:return: None
"""
Token.objects.filter(user__username=username).delete()
def set_user_auth_token(user, key):
"""
Sets the authentication for the given User.
Raises an AttributeError if *different* User with the specified key already exists.
:param user: User whose authentication is being set
:param key: New authentication key
:return: None
"""
# Check that no other user has the same key
if Token.objects.filter(~Q(user=user), key=key).exists():
raise AttributeError("The key %s is already in use by another user.", key)
Token.objects.filter(user=user).delete()
Token.objects.create(user=user, key=key)
print "Set API key for user %s to %s" % (user, key)
from django.db import models
class CourseManager(models.Manager):
def get_by_natural_key(self, course_key):
return self.get(course_key=course_key)
from django.db import models
from analytics_data_api.v0.managers import CourseManager
class Course(models.Model):
objects = CourseManager() # pylint: disable=no-value-for-parameter
course_key = models.CharField(unique=True, max_length=255)
class Meta(object):
db_table = 'courses'
class CourseActivityByWeek(models.Model):
"""A count of unique users who performed a particular action during a week."""
class Meta(object):
db_table = 'course_activity'
course = models.ForeignKey(Course, null=False)
interval_start = models.DateTimeField()
interval_end = models.DateTimeField()
activity_type = models.CharField(db_index=True, max_length=255)
count = models.IntegerField()
@classmethod
def get_most_recent(cls, course_key, activity_type):
"""Activity for the week that was mostly recently computed."""
return cls.objects.filter(course__course_key=course_key, activity_type=activity_type).latest('interval_end')
class BaseCourseEnrollment(models.Model):
course = models.ForeignKey(Course, null=False)
interval_start = models.DateTimeField(null=False)
interval_end = models.DateTimeField(null=False)
num_enrolled_students = models.IntegerField(null=False)
class Meta(object):
abstract = True
class CourseEnrollmentByBirthYear(BaseCourseEnrollment):
birth_year = models.IntegerField(null=False)
class Meta(object):
db_table = 'course_enrollment_birth_year'
ordering = ('course', 'birth_year')
class EducationLevel(models.Model):
name = models.CharField(max_length=255, null=False, unique=True)
short_name = models.CharField(max_length=255, null=False, unique=True)
class Meta(object):
db_table = 'education_levels'
class CourseEnrollmentByEducation(BaseCourseEnrollment):
education_level = models.ForeignKey(EducationLevel)
class Meta(object):
db_table = 'course_enrollment_education_level'
ordering = ('course', 'education_level')
class CourseEnrollmentByGender(BaseCourseEnrollment):
gender = models.CharField(max_length=255, null=False)
class Meta(object):
db_table = 'course_enrollment_gender'
ordering = ('course', 'gender')
from rest_framework import serializers
from analytics_data_api.v0.models import CourseActivityByWeek
class CourseActivityByWeekSerializer(serializers.ModelSerializer):
"""
Representation of CourseActivityByWeek that excludes the id field.
This table is managed by the data pipeline, and records can be removed and added at any time. The id for a
particular record is likely to change unexpectedly so we avoid exposing it.
"""
course_key = serializers.SerializerMethodField('get_course_key')
def get_course_key(self, obj):
return obj.course.course_key
class Meta(object):
model = CourseActivityByWeek
fields = ('interval_start', 'interval_end', 'activity_type', 'count', 'course_key')
# NOTE: Full URLs are used throughout these tests to ensure that the API contract is fulfilled. The URLs should *not*
# 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.
from contextlib import contextmanager
from datetime import datetime
from functools import partial
import random
from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.db.utils import ConnectionHandler, DatabaseError
from django.test import TestCase
from django.test.utils import override_settings
from django_dynamic_fixture import G
from mock import patch, Mock
import mock
import pytz
from rest_framework.authtoken.models import Token
from analytics_data_api.v0.models import CourseEnrollmentByBirthYear, CourseEnrollmentByEducation, EducationLevel, \
CourseEnrollmentByGender, CourseActivityByWeek, Course
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):
def setUp(self):
super(CourseActivityLastWeekTest, self).setUp()
self.course_key = 'edX/DemoX/Demo_Course'
self.course = G(Course, course_key=self.course_key)
interval_start = '2014-05-24T00:00:00Z'
interval_end = '2014-06-01T00:00:00Z'
G(CourseActivityByWeek, course=self.course, interval_start=interval_start, interval_end=interval_end,
activity_type='posted_forum', count=100)
G(CourseActivityByWeek, course=self.course, interval_start=interval_start, interval_end=interval_end,
activity_type='attempted_problem', count=200)
G(CourseActivityByWeek, course=self.course, interval_start=interval_start, interval_end=interval_end,
activity_type='any', count=300)
G(CourseActivityByWeek, course=self.course, interval_start=interval_start, interval_end=interval_end,
activity_type='played_video', count=400)
def test_activity(self):
response = self.authenticated_get('/api/v0/courses/{0}/recent_activity'.format(self.course_key))
self.assertEquals(response.status_code, 200)
self.assertEquals(response.data, self.get_activity_record())
@staticmethod
def get_activity_record(**kwargs):
default = {
'course_key': 'edX/DemoX/Demo_Course',
'interval_start': datetime(2014, 5, 24, 0, 0, tzinfo=pytz.utc),
'interval_end': datetime(2014, 6, 1, 0, 0, tzinfo=pytz.utc),
'activity_type': 'any',
'count': 300,
}
default.update(kwargs)
return default
def test_activity_auth(self):
response = self.client.get('/api/v0/courses/{0}/recent_activity'.format(self.course_key))
self.assertEquals(response.status_code, 401)
def test_url_encoded_course_key(self):
response = self.authenticated_get('/api/v0/courses/edX%2FDemoX%2FDemo_Course/recent_activity')
self.assertEquals(response.status_code, 200)
self.assertEquals(response.data, self.get_activity_record())
def test_video_activity(self):
activity_type = 'played_video'
response = self.authenticated_get('/api/v0/courses/{0}/recent_activity?activity_type={1}'.format(
self.course_key, activity_type))
self.assertEquals(response.status_code, 200)
self.assertEquals(response.data, self.get_activity_record(activity_type=activity_type, count=400))
def test_unknown_activity(self):
activity_type = 'missing_activity_type'
response = self.authenticated_get('/api/v0/courses/{0}/recent_activity?activity_type={1}'.format(
self.course_key, activity_type))
self.assertEquals(response.status_code, 404)
def test_unknown_course_key(self):
response = self.authenticated_get('/api/v0/courses/{0}/recent_activity'.format('foo'))
self.assertEquals(response.status_code, 404)
def test_missing_course_key(self):
response = self.authenticated_get('/api/v0/courses/recent_activity')
self.assertEquals(response.status_code, 404)
class CourseEnrollmentViewTestCase(object):
model = None
path = None
def _get_non_existent_course_key(self):
course_key = random.randint(100, 9999)
if not Course.objects.filter(course_key=course_key).exists():
return course_key
return self._get_non_existent_course_key()
def test_get_not_found(self):
'''
Requests made against non-existent courses should return a 404
'''
course_key = self._get_non_existent_course_key()
self.assertFalse(self.model.objects.filter(course__course_key=course_key).exists()) # pylint: disable=no-member
response = self.authenticated_get('/api/v0/courses/%s%s' % (course_key, self.path)) # pylint: disable=no-member
self.assertEquals(response.status_code, 404) # pylint: disable=no-member
def test_get(self):
raise NotImplementedError
class CourseEnrollmentByBirthYearViewTests(TestCaseWithAuthentication, CourseEnrollmentViewTestCase):
path = '/enrollment/birth_year'
model = CourseEnrollmentByBirthYear
@classmethod
def setUpClass(cls):
cls.course = G(Course)
cls.ce1 = G(CourseEnrollmentByBirthYear, course=cls.course, birth_year=1956)
cls.ce2 = G(CourseEnrollmentByBirthYear, course=cls.course, birth_year=1986)
def test_get(self):
response = self.authenticated_get('/api/v0/courses/%s%s' % (self.course.course_key, self.path,))
self.assertEquals(response.status_code, 200)
expected = {
self.ce1.birth_year: self.ce1.num_enrolled_students,
self.ce2.birth_year: self.ce2.num_enrolled_students,
}
actual = response.data['birth_years']
self.assertEquals(actual, expected)
class CourseEnrollmentByEducationViewTests(TestCaseWithAuthentication, CourseEnrollmentViewTestCase):
path = '/enrollment/education'
model = CourseEnrollmentByEducation
@classmethod
def setUpClass(cls):
cls.el1 = G(EducationLevel, name='Doctorate', short_name='doctorate')
cls.el2 = G(EducationLevel, name='Top Secret', short_name='top_secret')
cls.course = G(Course)
cls.ce1 = G(CourseEnrollmentByEducation, course=cls.course, education_level=cls.el1)
cls.ce2 = G(CourseEnrollmentByEducation, course=cls.course, education_level=cls.el2)
def test_get(self):
response = self.authenticated_get('/api/v0/courses/%s%s' % (self.course.course_key, self.path,))
self.assertEquals(response.status_code, 200)
expected = {
self.ce1.education_level.short_name: self.ce1.num_enrolled_students,
self.ce2.education_level.short_name: self.ce2.num_enrolled_students,
}
actual = response.data['education_levels']
self.assertEquals(actual, expected)
class CourseEnrollmentByGenderViewTests(TestCaseWithAuthentication, CourseEnrollmentViewTestCase):
path = '/enrollment/gender'
model = CourseEnrollmentByGender
@classmethod
def setUpClass(cls):
cls.course = G(Course)
cls.ce1 = G(CourseEnrollmentByGender, course=cls.course, gender='m')
cls.ce2 = G(CourseEnrollmentByGender, course=cls.course, gender='f')
def test_get(self):
response = self.authenticated_get('/api/v0/courses/%s%s' % (self.course.course_key, self.path,))
self.assertEquals(response.status_code, 200)
expected = {
self.ce1.gender: self.ce1.num_enrolled_students,
self.ce2.gender: self.ce2.num_enrolled_students,
}
actual = response.data['genders']
self.assertEquals(actual, expected)
class CourseManagerTests(TestCase):
def test_get_by_natural_key(self):
course_key = 'edX/DemoX/Demo_Course'
self.assertRaises(ObjectDoesNotExist, Course.objects.get_by_natural_key, course_key)
course = G(Course, course_key=course_key)
self.assertEqual(course, Course.objects.get_by_natural_key(course_key))
from django.conf.urls import patterns, url, include
from analytics_data_api.v0.views import operational
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')),
)
import re
from django.conf.urls import patterns, url
from analytics_data_api.v0.views.courses import CourseActivityMostRecentWeekView, CourseEnrollmentByEducationView, \
CourseEnrollmentByBirthYearView, CourseEnrollmentByGenderView
COURSE_URLS = [
('recent_activity', CourseActivityMostRecentWeekView, 'recent_activity'),
('enrollment/birth_year', CourseEnrollmentByBirthYearView, 'enrollment_by_birth_year'),
('enrollment/education', CourseEnrollmentByEducationView, 'enrollment_by_education'),
('enrollment/gender', CourseEnrollmentByGenderView, 'enrollment_by_gender'),
]
urlpatterns = patterns(
'',
# url(r'^$', CourseDetailView.as_view(), name='detail')
)
for path, view, name in COURSE_URLS:
urlpatterns += patterns('', url(r'^(?P<course_key>.+)/' + re.escape(path) + r'$', view.as_view(), name=name))
from django.core.exceptions import ObjectDoesNotExist
from django.http import Http404
from rest_framework import generics
from rest_framework.response import Response
from rest_framework.views import APIView
from analytics_data_api.v0.models import CourseActivityByWeek, CourseEnrollmentByBirthYear, \
CourseEnrollmentByEducation, CourseEnrollmentByGender
from analytics_data_api.v0.serializers import CourseActivityByWeekSerializer
class CourseActivityMostRecentWeekView(generics.RetrieveAPIView):
"""
Counts of users who performed various actions at least once during the most recently computed week.
The default is all users who performed <strong>any</strong> action in the course.
The representation has the following fields:
<ul>
<li>course_key: The ID of the course whose activity is described (e.g. edX/DemoX/Demo_Course).</li>
- interval_start: All data from this timestamp up to the `interval_end` was considered when computing this data
point.
- interval_end: All data from `interval_start` up to this timestamp was considered when computing this data point.
Note that data produced at exactly this time is **not** included.
- activity_type: The type of activity requested. Possible values are:
- ANY: The number of unique users who performed any action within the course, including actions not
enumerated below.
- PLAYED_VIDEO: The number of unique users who started watching any video in the course.
- ATTEMPTED_PROBLEM: The number of unique users who answered any loncapa based question in the course.
- POSTED_FORUM: The number of unique users who created a new post, responded to a post, or submitted a comment
on any forum in the course.
- count: The number of users who performed the activity indicated by the `activity_type`.
</ul>
activity_type -- The type of activity. (Defaults to "any".)
"""
serializer_class = CourseActivityByWeekSerializer
def get_object(self, queryset=None):
"""Select the activity report for the given course and activity type."""
course_key = self.kwargs.get('course_key')
activity_type = self.request.QUERY_PARAMS.get('activity_type', 'any')
activity_type = activity_type.lower()
try:
print CourseActivityByWeek.objects.all()
return CourseActivityByWeek.get_most_recent(course_key, activity_type)
except ObjectDoesNotExist:
raise Http404
class AbstractCourseEnrollmentView(APIView):
model = None
def render_data(self, data):
"""
Render view data
"""
raise NotImplementedError('Subclasses must define a render_data method!')
def get(self, request, *args, **kwargs): # pylint: disable=unused-argument
if not self.model:
raise NotImplementedError('Subclasses must specify a model!')
course_key = self.kwargs['course_key']
data = self.model.objects.filter(course__course_key=course_key)
if not data:
raise Http404
return Response(self.render_data(data))
class CourseEnrollmentByBirthYearView(AbstractCourseEnrollmentView):
"""
Course enrollment broken down by user birth year
Returns the enrollment of a course with users binned by their birth years.
"""
model = CourseEnrollmentByBirthYear
def render_data(self, data):
return {
'birth_years': dict(data.values_list('birth_year', 'num_enrolled_students'))
}
class CourseEnrollmentByEducationView(AbstractCourseEnrollmentView):
"""
Course enrollment broken down by user level of education
Returns the enrollment of a course with users binned by their education levels.
"""
model = CourseEnrollmentByEducation
def render_data(self, data):
return {
'education_levels': dict(data.values_list('education_level__short_name', 'num_enrolled_students'))
}
class CourseEnrollmentByGenderView(AbstractCourseEnrollmentView):
"""
Course enrollment broken down by user gender
Returns the enrollment of a course with users binned by their genders.
Genders:
m - male
f - female
o - other
"""
model = CourseEnrollmentByGender
def render_data(self, data):
return {
'genders': dict(data.values_list('gender', 'num_enrolled_students'))
}
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)
"""A command to set the API key for a user using when using TokenAuthentication."""
from optparse import make_option
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.core.management.base import BaseCommand, CommandError
from rest_framework.authtoken.models import Token
class Command(BaseCommand):
"""A command to set the API key for a user using when using TokenAuthentication."""
help = 'Set the API key for the specified user.'
args = '<username> <api_key>'
option_list = BaseCommand.option_list + (
make_option('--create-user', action='store_true', default=False,
help="Create a user if it doesn't exists"),
make_option('--delete-key', action='store_true', default=False,
help="Delete API key for user")
)
def handle(self, *args, **options):
"""Default Django BaseCommand handler."""
if options['delete_key']:
self._delete_user(*args)
else:
self._set_api_key(*args, **options)
def _delete_user(self, *args):
if len(args) != 1:
raise CommandError('Invalid or misssing arguments')
username = args[0]
tokens = Token.objects.filter(user__username=username)
if tokens.exists():
tokens.delete()
self.stdout.write('Removed API key for user: <{0}>\n'.format(username))
else:
self.stdout.write('Unknown user or user without an API key: <{0}>\n'.format(username))
def _set_api_key(self, *args, **options):
if len(args) != 2:
raise CommandError('Invalid or missing arguments')
username, key = args[0], args[1]
user = self._get_user(username, options['create_user'])
self._set_token(user, key)
def _get_user(self, username, create_user=False):
if create_user:
user, created = User.objects.get_or_create(username=username)
if created:
self.stdout.write('Created user: <{0}>\n'.format(user))
else:
try:
user = User.objects.get(username=username)
except ObjectDoesNotExist:
raise CommandError('Unknown user: <{0}>'.format(username))
return user
def _set_token(self, user, key):
# Check that no other user has the same key
tokens = Token.objects.filter(key=key)
if tokens.exists() and tokens[0].user != user:
raise CommandError('Key already in use.')
# Get and update the user key
_, created = Token.objects.get_or_create(user=user)
count = Token.objects.filter(user=user).update(key=key)
if count:
action = 'Created' if created else 'Updated'
self.stdout.write('{0} API key for user: <{1}>\n'.format(action, user))
else:
raise CommandError('Something went wrong.')
from django.db import models
from rest_framework import serializers
class CourseActivityByWeek(models.Model):
"""A count of unique users who performed a particular action during a week."""
db_from_setting = 'ANALYTICS_DATABASE'
class Meta: # pylint: disable=old-style-class
db_table = 'course_activity'
course_id = models.CharField(db_index=True, max_length=255)
interval_start = models.DateTimeField()
interval_end = models.DateTimeField()
label = models.CharField(db_index=True, max_length=255)
count = models.IntegerField()
@classmethod
def get_most_recent(cls, course_id, label):
"""Activity for the week that was mostly recently computed."""
return cls.objects.filter(course_id=course_id, label=label).latest('interval_end')
class CourseActivityByWeekSerializer(serializers.ModelSerializer):
"""
Representation of CourseActivityByWeek that excludes the id field.
This table is managed by the data pipeline, and records can be removed and added at any time. The id for a
particular record is likely to change unexpectedly so we avoid exposing it.
"""
class Meta: # pylint: disable=old-style-class
model = CourseActivityByWeek
fields = ('course_id', 'interval_start', 'interval_end', 'label', 'count')
from contextlib import contextmanager
from datetime import datetime
from functools import partial
from django.conf import settings
from django.contrib.auth.models import User
from django.db.utils import ConnectionHandler
from django.test import TestCase
from django.test.utils import override_settings
from mock import patch
import pytz
from rest_framework.authtoken.models import Token
from analyticsdata.views import handle_internal_server_error, handle_missing_resource_error
# NOTE: Full URLs are used throughout these tests to ensure that the API contract is fulfilled. The URLs should *not*
# 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.
class TestCaseWithAutenticatation(TestCase):
def setUp(self):
super(TestCaseWithAutenticatation, 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)
class OperationalEndpointsTest(TestCaseWithAutenticatation):
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)
def test_database_down(self):
databases = {
"default": {}
}
with self.override_database_connections(databases):
self.assert_database_health('UNAVAILABLE')
@staticmethod
@contextmanager
def override_database_connections(databases):
with patch('analyticsdata.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')
class ErrorHandlingTest(TestCase):
def test_internal_server_error_handling(self):
response = handle_internal_server_error(None)
self.validate_error_response(response, 500)
def validate_error_response(self, response, status_code):
self.assertEquals(response.content, '{{"status": {0}}}'.format(status_code))
self.assertEquals(response.status_code, status_code)
self.assertEquals(response.get('Content-Type'), 'application/json; charset=utf-8')
def test_missing_resource_handling(self):
response = handle_missing_resource_error(None)
self.validate_error_response(response, 404)
class CourseActivityLastWeekTest(TestCaseWithAutenticatation):
fixtures = ['single_course_activity']
COURSE_ID = 'edX/DemoX/Demo_Course'
def test_activity(self):
response = self.authenticated_get('/api/v0/courses/{0}/recent_activity'.format(self.COURSE_ID))
self.assertEquals(response.status_code, 200)
self.assertEquals(response.data, self.get_activity_record())
@staticmethod
def get_activity_record(**kwargs):
default = {
'course_id': 'edX/DemoX/Demo_Course',
'interval_start': datetime(2014, 5, 24, 0, 0, tzinfo=pytz.utc),
'interval_end': datetime(2014, 6, 1, 0, 0, tzinfo=pytz.utc),
'label': 'ACTIVE',
'count': 300,
}
default.update(kwargs)
return default
def test_activity_auth(self):
response = self.client.get('/api/v0/courses/{0}/recent_activity'.format(self.COURSE_ID))
self.assertEquals(response.status_code, 401)
def test_url_encoded_course_id(self):
response = self.authenticated_get('/api/v0/courses/edX%2FDemoX%2FDemo_Course/recent_activity')
self.assertEquals(response.status_code, 200)
self.assertEquals(response.data, self.get_activity_record())
def test_video_activity(self):
label = 'PLAYED_VIDEO'
response = self.authenticated_get('/api/v0/courses/{0}/recent_activity?label={1}'.format(self.COURSE_ID, label))
self.assertEquals(response.status_code, 200)
self.assertEquals(response.data, self.get_activity_record(label=label, count=400))
def test_unknown_activity(self):
label = 'MISSING_ACTIVITY_TYPE'
response = self.authenticated_get('/api/v0/courses/{0}/recent_activity?label={1}'.format(self.COURSE_ID, label))
self.assertEquals(response.status_code, 404)
def test_unknown_course_id(self):
response = self.authenticated_get('/api/v0/courses/{0}/recent_activity'.format('foo'))
self.assertEquals(response.status_code, 404)
def test_missing_course_id(self):
response = self.authenticated_get('/api/v0/courses/recent_activity')
self.assertEquals(response.status_code, 404)
from rest_framework import generics
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.db import connections
from django.http import HttpResponse, Http404
from analyticsdata.models import CourseActivityByWeek
from analyticsdata.models import CourseActivityByWeekSerializer
@api_view(['GET'])
@permission_classes((AllowAny, ))
def status(_request):
"""
A very quick check to see if the application server is alive and processing requests.
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.
"""
return Response()
@api_view(['GET'])
def authenticated(_request):
"""
Validate provided credentials.
Return no data, a simple 200 OK status code is sufficient to indicate that the credentials are valid.
"""
return Response()
@api_view(['GET'])
@permission_classes((AllowAny, ))
def health(_request):
"""
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".
"""
overall_status = 'OK'
db_conn_status = 'OK'
connection_name = getattr(settings, 'ANALYTICS_DATABASE', 'default')
try:
cursor = connections[connection_name].cursor()
try:
cursor.execute("SELECT 1")
cursor.fetchone()
finally:
cursor.close()
except Exception: # pylint: disable=broad-except
overall_status = 'UNAVAILABLE'
db_conn_status = 'UNAVAILABLE'
response = {
"overall_status": overall_status,
"detailed_status": {
'database_connection': db_conn_status
}
}
return Response(response)
def handle_internal_server_error(_request):
"""Notify the client that an error occurred processing the request without providing any detail."""
return _handle_error(500)
def handle_missing_resource_error(_request):
"""Notify the client that the requested resource could not be found."""
return _handle_error(404)
def _handle_error(status_code):
info = {
'status': status_code
}
renderer = JSONRenderer()
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)
class CourseActivityMostRecentWeekView(generics.RetrieveAPIView):
"""
Counts of users who performed various actions at least once during the most recently computed week.
The default is all users who performed *any* action in the course.
The representation has the following fields:
- course_id: The ID of the course whose activity is described.
- interval_start: All data from this timestamp up to the `interval_end` was considered when computing this data
point.
- interval_end: All data from `interval_start` up to this timestamp was considered when computing this data point.
Note that data produced at exactly this time is **not** included.
- label: The type of activity requested. Possible values are:
- ACTIVE: The number of unique users who performed any action within the course, including actions not
enumerated below.
- PLAYED_VIDEO: The number of unique users who started watching any video in the course.
- ATTEMPTED_PROBLEM: The number of unique users who answered any loncapa based question in the course.
- POSTED_FORUM: The number of unique users who created a new post, responded to a post, or submitted a comment
on any forum in the course.
- count: The number of users who performed the activity indicated by the `label`.
Parameters:
- course_id (string): Unique identifier for the course.
- label (string): The type of activity. Defaults to `ACTIVE`. Possible values:
- `ACTIVE`
- `PLAYED_VIDEO`
- `ATTEMPTED_PROBLEM`
- `POSTED_FORUM`
"""
serializer_class = CourseActivityByWeekSerializer
def get_object(self): # pylint: disable=arguments-differ
"""Select the activity report for the given course and label."""
course_id = self.kwargs.get('course_id')
label = self.request.QUERY_PARAMS.get('label', 'ACTIVE')
try:
return CourseActivityByWeek.get_most_recent(course_id, label)
except ObjectDoesNotExist:
raise Http404
from django.conf import settings
class DatabaseFromSettingRouter(object):
def db_for_read(self, model, **hints):
return self._get_database(model)
def _get_database(self, model):
if model._meta.app_label == 'v0':
return getattr(settings, 'ANALYTICS_DATABASE', 'default')
if getattr(model, 'db_from_setting', None):
return getattr(settings, model.db_from_setting, 'default')
else:
return None
def db_for_write(self, model, **hints):
......
......@@ -185,10 +185,12 @@ THIRD_PARTY_APPS = (
'south',
'rest_framework',
'rest_framework.authtoken',
'rest_framework_swagger',
)
LOCAL_APPS = (
'analyticsdata',
'analytics_data_api',
'analytics_data_api.v0',
)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
......@@ -251,6 +253,7 @@ REST_FRAMEWORK = {
}
########## END REST FRAMEWORK CONFIGURATION
########## ANALYTICS DATA API CONFIGURATION
ANALYTICS_DATABASE = 'default'
......
......@@ -58,6 +58,7 @@ CACHES = {
# See: https://github.com/django-debug-toolbar/django-debug-toolbar#installation
INSTALLED_APPS += (
'debug_toolbar',
'django_nose',
)
# See: https://github.com/django-debug-toolbar/django-debug-toolbar#installation
......@@ -70,7 +71,6 @@ MIDDLEWARE_CLASSES += (
# See: https://github.com/django-debug-toolbar/django-debug-toolbar#installation
DEBUG_TOOLBAR_CONFIG = {
'INTERCEPT_REDIRECTS': False,
'SHOW_TEMPLATE_CONTEXT': True,
}
########## END TOOLBAR CONFIGURATION
......@@ -81,3 +81,5 @@ ANALYTICS_DATABASE = 'analytics'
ENABLE_ADMIN_SITE = True
########## END ANALYTICS DATA API CONFIGURATION
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
from django.conf import settings
from django.conf.urls import patterns, include, url
from django.contrib import admin
from django.views.generic import RedirectView
urlpatterns = patterns(
'',
url(r'^$', RedirectView.as_view(url='/docs')),
# Support logging in to the browseable API
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
......@@ -12,15 +15,13 @@ urlpatterns = patterns(
url(r'^api-token-auth/', 'rest_framework.authtoken.views.obtain_auth_token'),
# Route all reports URLs to this endpoint
# TODO: come up with a better way to handle versioning, maybe a decorator?
url(r'^api/v0/', include('analyticsdata.urls')),
url(r'^api/', include('analytics_data_api.urls', namespace='api')),
url(r'^docs/', include('rest_framework_swagger.urls')),
)
if settings.ENABLE_ADMIN_SITE:
admin.autodiscover()
urlpatterns += patterns('', url(r'^site/admin/', include(admin.site.urls)))
handler500 = 'analyticsdata.views.handle_internal_server_error' # pylint: disable=invalid-name
handler404 = 'analyticsdata.views.handle_missing_resource_error' # pylint: disable=invalid-name
handler500 = 'analyticsdataserver.views.handle_internal_server_error' # pylint: disable=invalid-name
handler404 = 'analyticsdataserver.views.handle_missing_resource_error' # pylint: disable=invalid-name
from django.http import HttpResponse
from rest_framework.renderers import JSONRenderer
def handle_internal_server_error(_request):
"""Notify the client that an error occurred processing the request without providing any detail."""
return _handle_error(500)
def handle_missing_resource_error(_request):
"""Notify the client that the requested resource could not be found."""
return _handle_error(404)
def _handle_error(status_code):
info = {
'status': status_code
}
renderer = JSONRenderer()
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)
Django==1.4.12
Django==1.6.5
Markdown==2.4.1
South==0.8.1
django-model-utils==1.4.0
djangorestframework==2.3.5
ipython==2.1.0
django-rest-swagger==0.1.14
# Local development dependencies go here
-r base.txt
django-debug-toolbar==0.9.4
django-debug-toolbar==1.2.1
......@@ -2,12 +2,11 @@
-r base.txt
coverage==3.7
diff-cover >= 0.2.1
django-dynamic-fixture==1.7.0
django-nose==1.2
httpretty==0.8.0
mock==1.0.1
nose==1.3.3
pep257==0.3.2
pep8==1.5.7
pylint==1.2.1
pytz==2012h
requests==2.3.0
#header form#api_selector .input a#explore {
color: white;
background-color: #126F9E;
box-shadow: 0 2px 1px 0 #0a4a67;
}
#header form#api_selector .input a#explore:hover {
background-color: #1790c7;
}
#header {
border-bottom: 1px solid #8a8c8f;
box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.1);
background: #fff;
}
#header a#logo {
background: transparent url(../images/header-logo.png) no-repeat left center;
padding-left: 100px;
}
body {
background: #f5f5f5;
}
#swagger-ui-container, #message-bar {
background: white;
}
#swagger-ui-container {
padding-bottom: 20px;
}
{% load staticfiles %}
<!DOCTYPE html>
<html>
<head>
<title>edX Analytics API</title>
<link href='//fonts.googleapis.com/css?family=Droid+Sans:400,700' rel='stylesheet' type='text/css'/>
<link href='{% static "rest_framework_swagger/css/highlight.default.css" %}' media='screen' rel='stylesheet' type='text/css'/>
<link href='{% static "rest_framework_swagger/css/rest_framework_swagger.css" %}' media='screen' rel='stylesheet' type='text/css'/>
<link href='{% static "rest_framework_swagger/css/screen.css" %}' media='screen' rel='stylesheet' type='text/css'/>
<link href='{% static "css/edx-swagger.css" %}' media='screen' rel='stylesheet' type='text/css'/>
<script type="text/javascript" src="{% static 'rest_framework_swagger/lib/shred.bundle.js' %}"></script>
<script src='{% static "rest_framework_swagger/lib/jquery-1.8.0.min.js" %}' type='text/javascript'></script>
<script src='{% static "rest_framework_swagger/lib/jquery.slideto.min.js" %}' type='text/javascript'></script>
<script src='{% static "rest_framework_swagger/lib/jquery.wiggle.min.js" %}' type='text/javascript'></script>
<script src='{% static "rest_framework_swagger/lib/jquery.ba-bbq.min.js" %}' type='text/javascript'></script>
<script src='{% static "rest_framework_swagger/lib/jquery.cookie.js" %}' type='text/javascript'></script>
<script src='{% static "rest_framework_swagger/lib/handlebars-1.0.0.js" %}' type='text/javascript'></script>
<script src='{% static "rest_framework_swagger/lib/underscore-min.js" %}' type='text/javascript'></script>
<script src='{% static "rest_framework_swagger/lib/backbone-min.js" %}' type='text/javascript'></script>
<script src='{% static "rest_framework_swagger/lib/swagger.js" %}' type='text/javascript'></script>
<script src='{% static "rest_framework_swagger/swagger-ui.min.js" %}' type='text/javascript'></script>
<script src='{% static "rest_framework_swagger/lib/highlight.7.3.pack.js" %}' type='text/javascript'></script>
<link rel="icon" type="image/x-icon" href='{% static "images/favicon.ico" %}' />
<script type="text/javascript">
$(function () {
window.swaggerUi = new SwaggerUi({
url: "{{ swagger_settings.discovery_url }}",
apiKey: "{{ swagger_settings.api_key }}",
dom_id: "swagger-ui-container",
supportedSubmitMethods: {{ swagger_settings.enabled_methods }},
onComplete: function(swaggerApi, swaggerUi){
if(console) {
console.log("Loaded SwaggerUI")
}
$('pre code').each(function(i, e) {hljs.highlightBlock(e)});
},
onFailure: function(data) {
if(console) {
console.log("Unable to Load SwaggerUI");
console.log(data);
}
},
docExpansion: "none"
});
$('#input_apiKey').change(function() {
var key = $('#input_apiKey')[0].value;
console.log("key: " + key);
if(key && key.trim() != "") {
console.log("added key " + key);
window.authorizations.add("key", new ApiKeyAuthorization("Authorization", "Token " + key, "header"));
}
})
{% if swagger_settings.api_key %}
window.authorizations.add("key", new ApiKeyAuthorization("Authorization", "Token " + "{{ swagger_settings.api_key }}", "header"));
{% endif %}
window.swaggerUi.load();
});
</script>
</head>
<body>
<div id='header'>
<div class="swagger-ui-wrap">
<a id="logo" class="edx-logo" href="http://code.edx.org/"></a>
<form id='api_selector'>
<div class='input'><input placeholder="http://example.com/api" id="input_baseUrl" name="baseUrl" type="text"/></div>
<div class='input'><input placeholder="API Key" id="input_apiKey" name="apiKey" type="text"/></div>
<div class='input'><a id="explore" href="#">Explore</a></div>
</form>
</div>
</div>
<div id="message-bar" class="swagger-ui-wrap">
&nbsp;
</div>
<div id="swagger-ui-container" class="swagger-ui-wrap">
</div>
</body>
</html>
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