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
acefe9fc
Commit
acefe9fc
authored
Jul 16, 2014
by
Clinton Blackburn
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Updated enrollment resources to support historical data
Change-Id: I88bf4ebefd9742f4647b0e9383fdbac9f97ac805
parent
dbde3921
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
332 additions
and
181 deletions
+332
-181
analytics_data_api/fixtures/course_enrollment_education.json
+90
-0
analytics_data_api/fixtures/course_enrollment_gender.json
+30
-0
analytics_data_api/v0/serializers.py
+40
-15
analytics_data_api/v0/tests/test_views.py
+86
-85
analytics_data_api/v0/urls/courses.py
+1
-1
analytics_data_api/v0/views/courses.py
+81
-80
analyticsdataserver/settings/local.py
+4
-0
No files found.
analytics_data_api/fixtures/course_enrollment_education.json
View file @
acefe9fc
...
...
@@ -88,5 +88,95 @@
"count"
:
9940
,
"date"
:
"2014-07-01"
}
},
{
"model"
:
"v0.CourseEnrollmentByEducation"
,
"pk"
:
14
,
"fields"
:
{
"course"
:
[
"edX/DemoX/Demo_Course"
],
"education_level"
:
6
,
"count"
:
12295
,
"date"
:
"2014-07-02"
}
},
{
"model"
:
"v0.CourseEnrollmentByEducation"
,
"pk"
:
6
,
"fields"
:
{
"course"
:
[
"edX/DemoX/Demo_Course"
],
"education_level"
:
7
,
"count"
:
70885
,
"date"
:
"2014-07-01"
}
},
{
"model"
:
"v0.CourseEnrollmentByEducation"
,
"pk"
:
7
,
"fields"
:
{
"course"
:
[
"edX/DemoX/Demo_Course"
],
"education_level"
:
3
,
"count"
:
981
,
"date"
:
"2014-07-01"
}
},
{
"model"
:
"v0.CourseEnrollmentByEducation"
,
"pk"
:
8
,
"fields"
:
{
"course"
:
[
"edX/DemoX/Demo_Course"
],
"education_level"
:
5
,
"count"
:
51591
,
"date"
:
"2014-07-01"
}
},
{
"model"
:
"v0.CourseEnrollmentByEducation"
,
"pk"
:
9
,
"fields"
:
{
"course"
:
[
"edX/DemoX/Demo_Course"
],
"education_level"
:
4
,
"count"
:
6051
,
"date"
:
"2014-07-01"
}
},
{
"model"
:
"v0.CourseEnrollmentByEducation"
,
"pk"
:
10
,
"fields"
:
{
"course"
:
[
"edX/DemoX/Demo_Course"
],
"education_level"
:
8
,
"count"
:
53216
,
"date"
:
"2014-07-01"
}
},
{
"model"
:
"v0.CourseEnrollmentByEducation"
,
"pk"
:
11
,
"fields"
:
{
"course"
:
[
"edX/DemoX/Demo_Course"
],
"education_level"
:
1
,
"count"
:
667
,
"date"
:
"2014-07-01"
}
},
{
"model"
:
"v0.CourseEnrollmentByEducation"
,
"pk"
:
12
,
"fields"
:
{
"course"
:
[
"edX/DemoX/Demo_Course"
],
"education_level"
:
2
,
"count"
:
5722
,
"date"
:
"2014-07-01"
}
},
{
"model"
:
"v0.CourseEnrollmentByEducation"
,
"pk"
:
13
,
"fields"
:
{
"course"
:
[
"edX/DemoX/Demo_Course"
],
"education_level"
:
9
,
"count"
:
9940
,
"date"
:
"2014-07-01"
}
}
]
analytics_data_api/fixtures/course_enrollment_gender.json
View file @
acefe9fc
...
...
@@ -28,5 +28,35 @@
"count"
:
423
,
"date"
:
"2014-07-01"
}
},
{
"model"
:
"v0.CourseEnrollmentByGender"
,
"pk"
:
4
,
"fields"
:
{
"course"
:
[
"edX/DemoX/Demo_Course"
],
"gender"
:
"m"
,
"count"
:
1332
,
"date"
:
"2014-07-02"
}
},
{
"model"
:
"v0.CourseEnrollmentByGender"
,
"pk"
:
5
,
"fields"
:
{
"course"
:
[
"edX/DemoX/Demo_Course"
],
"gender"
:
"f"
,
"count"
:
77445
,
"date"
:
"2014-07-02"
}
},
{
"model"
:
"v0.CourseEnrollmentByGender"
,
"pk"
:
6
,
"fields"
:
{
"course"
:
[
"edX/DemoX/Demo_Course"
],
"gender"
:
"o"
,
"count"
:
34
,
"date"
:
"2014-07-02"
}
}
]
analytics_data_api/v0/serializers.py
View file @
acefe9fc
from
django.conf
import
settings
from
rest_framework
import
serializers
from
analytics_data_api.v0.models
import
CourseActivityByWeek
,
ProblemResponseAnswerDistribution
,
\
CourseEnrollmentDaily
,
CourseEnrollmentByCountry
,
Country
from
analytics_data_api.v0
import
models
class
CourseIdMixin
(
object
):
...
...
@@ -24,7 +23,7 @@ class CourseActivityByWeekSerializer(serializers.ModelSerializer, CourseIdMixin)
course_id
=
RequiredSerializerMethodField
(
'get_course_id'
)
class
Meta
(
object
):
model
=
CourseActivityByWeek
model
=
models
.
CourseActivityByWeek
fields
=
(
'interval_start'
,
'interval_end'
,
'activity_type'
,
'count'
,
'course_id'
)
...
...
@@ -37,7 +36,7 @@ class ProblemResponseAnswerDistributionSerializer(serializers.ModelSerializer):
"""
class
Meta
(
object
):
model
=
ProblemResponseAnswerDistribution
model
=
models
.
ProblemResponseAnswerDistribution
fields
=
(
'course_id'
,
'module_id'
,
...
...
@@ -52,30 +51,56 @@ class ProblemResponseAnswerDistributionSerializer(serializers.ModelSerializer):
)
class
CourseEnrollmentDailySerializer
(
serializers
.
ModelSerializer
,
CourseIdMixin
):
"""
Representation of course enrollment for a single day and course.
"""
class
BaseCourseEnrollmentModelSerializer
(
serializers
.
ModelSerializer
,
CourseIdMixin
):
course_id
=
RequiredSerializerMethodField
(
'get_course_id'
)
date
=
serializers
.
DateField
(
format
=
settings
.
DATE_FORMAT
)
class
CourseEnrollmentDailySerializer
(
BaseCourseEnrollmentModelSerializer
):
""" Representation of course enrollment for a single day and course. """
class
Meta
(
object
):
model
=
CourseEnrollmentDaily
model
=
models
.
CourseEnrollmentDaily
fields
=
(
'course_id'
,
'date'
,
'count'
)
# pylint: disable=no-value-for-parameter
class
CountrySerializer
(
serializers
.
ModelSerializer
):
class
Meta
(
object
):
model
=
Country
model
=
models
.
Country
fields
=
(
'code'
,
'name'
)
class
CourseEnrollmentByCountrySerializer
(
serializers
.
ModelSerializer
,
CourseIdMixin
):
course_id
=
RequiredSerializerMethodField
(
'get_course_id'
)
# pylint: disable=no-value-for-parameter
class
EducationLevelSerializer
(
serializers
.
ModelSerializer
):
class
Meta
(
object
):
model
=
models
.
EducationLevel
fields
=
(
'name'
,
'short_name'
)
class
CourseEnrollmentByCountrySerializer
(
BaseCourseEnrollmentModelSerializer
):
country
=
CountrySerializer
()
date
=
serializers
.
DateField
(
format
=
settings
.
DATE_FORMAT
)
class
Meta
(
object
):
model
=
CourseEnrollmentByCountry
model
=
models
.
CourseEnrollmentByCountry
fields
=
(
'date'
,
'course_id'
,
'country'
,
'count'
)
class
CourseEnrollmentByGenderSerializer
(
BaseCourseEnrollmentModelSerializer
):
class
Meta
(
object
):
model
=
models
.
CourseEnrollmentByGender
fields
=
(
'course_id'
,
'date'
,
'gender'
,
'count'
)
class
CourseEnrollmentByEducationSerializer
(
BaseCourseEnrollmentModelSerializer
):
education_level
=
EducationLevelSerializer
()
class
Meta
(
object
):
model
=
models
.
CourseEnrollmentByEducation
fields
=
(
'course_id'
,
'date'
,
'education_level'
,
'count'
)
class
CourseEnrollmentByBirthYearSerializer
(
BaseCourseEnrollmentModelSerializer
):
class
Meta
(
object
):
model
=
models
.
CourseEnrollmentByBirthYear
fields
=
(
'course_id'
,
'date'
,
'birth_year'
,
'count'
)
analytics_data_api/v0/tests/test_views.py
View file @
acefe9fc
...
...
@@ -81,6 +81,7 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
self
.
assertEquals
(
response
.
status_code
,
404
)
# pylint: disable=no-member
class
CourseEnrollmentViewTestCase
(
object
):
model
=
None
path
=
None
...
...
@@ -93,15 +94,49 @@ class CourseEnrollmentViewTestCase(object):
return
self
.
_get_non_existent_course_id
()
def
get_expected_response
(
self
,
*
args
):
raise
NotImplementedError
def
test_get_not_found
(
self
):
""" Requests made against non-existent courses should return a 404 """
course_id
=
self
.
_get_non_existent_course_id
()
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
self
.
assertEquals
(
response
.
status_code
,
404
)
# pylint: disable=no-member
self
.
assertFalse
(
self
.
model
.
objects
.
filter
(
course__course_id
=
course_id
)
.
exists
())
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s'
%
(
course_id
,
self
.
path
))
self
.
assertEquals
(
response
.
status_code
,
404
)
def
test_get
(
self
):
raise
NotImplementedError
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s'
%
(
self
.
course
.
course_id
,
self
.
path
,))
self
.
assertEquals
(
response
.
status_code
,
200
)
expected
=
self
.
get_expected_response
(
*
self
.
model
.
objects
.
filter
(
date
=
self
.
date
))
self
.
assertEquals
(
response
.
data
,
expected
)
def
test_get_with_intervals
(
self
):
expected
=
self
.
get_expected_response
(
*
self
.
model
.
objects
.
filter
(
date
=
self
.
date
))
self
.
assertIntervalFilteringWorks
(
expected
,
self
.
date
,
self
.
date
+
datetime
.
timedelta
(
days
=
1
))
def
assertIntervalFilteringWorks
(
self
,
expected_response
,
start_date
,
end_date
):
course
=
self
.
course
# If start date is after date of existing data, no data should be returned
date
=
(
start_date
+
datetime
.
timedelta
(
days
=
30
))
.
strftime
(
settings
.
DATE_FORMAT
)
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s?start_date=
%
s'
%
(
course
.
course_id
,
self
.
path
,
date
))
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertListEqual
([],
response
.
data
)
# If end date is before date of existing data, no data should be returned
date
=
(
start_date
-
datetime
.
timedelta
(
days
=
30
))
.
strftime
(
settings
.
DATE_FORMAT
)
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s?end_date=
%
s'
%
(
course
.
course_id
,
self
.
path
,
date
))
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertListEqual
([],
response
.
data
)
# If data falls in date range, data should be returned
start_date
=
start_date
.
strftime
(
settings
.
DATE_FORMAT
)
end_date
=
end_date
.
strftime
(
settings
.
DATE_FORMAT
)
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s?start_date=
%
s&end_date=
%
s'
%
(
course
.
course_id
,
self
.
path
,
start_date
,
end_date
))
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertListEqual
(
response
.
data
,
expected_response
)
class
CourseEnrollmentByBirthYearViewTests
(
TestCaseWithAuthentication
,
CourseEnrollmentViewTestCase
):
...
...
@@ -111,23 +146,30 @@ class CourseEnrollmentByBirthYearViewTests(TestCaseWithAuthentication, CourseEnr
@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
)
cls
.
date
=
datetime
.
date
(
2014
,
1
,
1
)
G
(
cls
.
model
,
course
=
cls
.
course
,
date
=
cls
.
date
,
birth_year
=
1956
)
G
(
cls
.
model
,
course
=
cls
.
course
,
date
=
cls
.
date
,
birth_year
=
1986
)
G
(
cls
.
model
,
course
=
cls
.
course
,
date
=
cls
.
date
-
datetime
.
timedelta
(
days
=
10
),
birth_year
=
1956
)
G
(
cls
.
model
,
course
=
cls
.
course
,
date
=
cls
.
date
-
datetime
.
timedelta
(
days
=
10
),
birth_year
=
1986
)
def
get_expected_response
(
self
,
*
args
):
return
[{
'course_id'
:
ce
.
course
.
course_id
,
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
'birth_year'
:
ce
.
birth_year
}
for
ce
in
args
]
def
test_get
(
self
):
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s'
%
(
self
.
course
.
course_id
,
self
.
path
,))
self
.
assertEquals
(
response
.
status_code
,
200
)
expected
=
{
self
.
ce1
.
birth_year
:
self
.
ce1
.
count
,
self
.
ce2
.
birth_year
:
self
.
ce2
.
count
,
}
actual
=
response
.
data
[
'birth_years'
]
self
.
assert
Equals
(
actual
,
expected
)
expected
=
self
.
get_expected_response
(
*
self
.
model
.
objects
.
filter
(
date
=
self
.
date
))
self
.
assertEquals
(
response
.
data
,
expected
)
def
test_get_with_intervals
(
self
):
expected
=
self
.
get_expected_response
(
*
self
.
model
.
objects
.
filter
(
date
=
self
.
date
))
self
.
assert
IntervalFilteringWorks
(
expected
,
self
.
date
,
self
.
date
+
datetime
.
timedelta
(
days
=
1
)
)
class
CourseEnrollmentByEducationViewTests
(
TestCaseWithAuthentication
,
CourseEnrollmentViewTestCase
):
path
=
'/enrollment/education'
path
=
'/enrollment/education
/
'
model
=
CourseEnrollmentByEducation
@classmethod
...
...
@@ -135,41 +177,33 @@ class CourseEnrollmentByEducationViewTests(TestCaseWithAuthentication, CourseEnr
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_id
,
self
.
path
,))
self
.
assertEquals
(
response
.
status_code
,
200
)
cls
.
date
=
datetime
.
date
(
2014
,
1
,
1
)
G
(
cls
.
model
,
course
=
cls
.
course
,
date
=
cls
.
date
,
education_level
=
cls
.
el1
)
G
(
cls
.
model
,
course
=
cls
.
course
,
date
=
cls
.
date
,
education_level
=
cls
.
el2
)
G
(
cls
.
model
,
course
=
cls
.
course
,
date
=
cls
.
date
-
datetime
.
timedelta
(
days
=
2
),
education_level
=
cls
.
el2
)
expected
=
{
self
.
ce1
.
education_level
.
short_name
:
self
.
ce1
.
count
,
self
.
ce2
.
education_level
.
short_name
:
self
.
ce2
.
count
,
}
actual
=
response
.
data
[
'education_levels'
]
self
.
assertEquals
(
actual
,
expected
)
def
get_expected_response
(
self
,
*
args
):
return
[{
'course_id'
:
ce
.
course
.
course_id
,
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
'education_level'
:
{
'name'
:
ce
.
education_level
.
name
,
'short_name'
:
ce
.
education_level
.
short_name
}}
for
ce
in
args
]
class
CourseEnrollmentByGenderViewTests
(
TestCaseWithAuthentication
,
CourseEnrollmentViewTestCase
):
path
=
'/enrollment/gender'
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_id
,
self
.
path
,))
self
.
assertEquals
(
response
.
status_code
,
200
)
cls
.
date
=
datetime
.
date
(
2014
,
1
,
1
)
G
(
cls
.
model
,
course
=
cls
.
course
,
gender
=
'm'
,
date
=
cls
.
date
,
count
=
34
)
G
(
cls
.
model
,
course
=
cls
.
course
,
gender
=
'f'
,
date
=
cls
.
date
,
count
=
45
)
G
(
cls
.
model
,
course
=
cls
.
course
,
gender
=
'f'
,
date
=
cls
.
date
-
datetime
.
timedelta
(
days
=
2
),
count
=
45
)
expected
=
{
self
.
ce1
.
gender
:
self
.
ce1
.
count
,
self
.
ce2
.
gender
:
self
.
ce2
.
count
,
}
actual
=
response
.
data
[
'genders'
]
self
.
assertEquals
(
actual
,
expected
)
def
get_expected_response
(
self
,
*
args
):
return
[{
'course_id'
:
ce
.
course
.
course_id
,
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
'gender'
:
ce
.
gender
}
for
ce
in
args
]
# pylint: disable=no-member,no-value-for-parameter
...
...
@@ -203,20 +237,20 @@ class AnswerDistributionTests(TestCaseWithAuthentication):
self
.
assertEquals
(
response
.
status_code
,
404
)
class
CourseEnrollment
Latest
ViewTests
(
TestCaseWithAuthentication
,
CourseEnrollmentViewTestCase
):
class
CourseEnrollmentViewTests
(
TestCaseWithAuthentication
,
CourseEnrollmentViewTestCase
):
model
=
CourseEnrollmentDaily
path
=
'/enrollment'
@classmethod
def
setUpClass
(
cls
):
cls
.
course
=
G
(
Course
)
cls
.
ce
=
G
(
CourseEnrollmentDaily
,
course
=
cls
.
course
,
date
=
datetime
.
date
(
2014
,
1
,
1
),
count
=
203
)
cls
.
date
=
datetime
.
date
(
2014
,
1
,
1
)
G
(
cls
.
model
,
course
=
cls
.
course
,
date
=
cls
.
date
,
count
=
203
)
G
(
cls
.
model
,
course
=
cls
.
course
,
date
=
cls
.
date
-
datetime
.
timedelta
(
days
=
5
),
count
=
203
)
def
test_get
(
self
):
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s'
%
(
self
.
course
.
course_id
,
self
.
path
,))
self
.
assertEquals
(
response
.
status_code
,
200
)
expected
=
{
'course_id'
:
self
.
ce
.
course
.
course_id
,
'count'
:
self
.
ce
.
count
,
'date'
:
self
.
ce
.
date
}
self
.
assertDictEqual
(
response
.
data
,
expected
)
def
get_expected_response
(
self
,
*
args
):
return
[{
'course_id'
:
ce
.
course
.
course_id
,
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
)}
for
ce
in
args
]
class
CourseEnrollmentByLocationViewTests
(
TestCaseWithAuthentication
,
CourseEnrollmentViewTestCase
):
...
...
@@ -227,43 +261,10 @@ class CourseEnrollmentByLocationViewTests(TestCaseWithAuthentication, CourseEnro
return
[{
'course_id'
:
ce
.
course
.
course_id
,
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
'country'
:
{
'code'
:
ce
.
country
.
code
,
'name'
:
ce
.
country
.
name
}}
for
ce
in
args
]
def
test_get
(
self
):
course
=
G
(
Course
)
date1
=
datetime
.
date
(
2014
,
1
,
1
)
date2
=
datetime
.
date
(
2013
,
1
,
1
)
ce1
=
G
(
CourseEnrollmentByCountry
,
course
=
course
,
country
=
G
(
Country
),
count
=
455
,
date
=
date1
)
ce2
=
G
(
CourseEnrollmentByCountry
,
course
=
course
,
country
=
G
(
Country
),
count
=
356
,
date
=
date1
)
# This should not be returned as the view should return only the latest data when no interval is supplied.
G
(
CourseEnrollmentByCountry
,
course
=
course
,
country
=
G
(
Country
),
count
=
12
,
date
=
date2
)
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s'
%
(
course
.
course_id
,
self
.
path
,))
self
.
assertEquals
(
response
.
status_code
,
200
)
expected
=
self
.
get_expected_response
(
ce1
,
ce2
)
self
.
assertListEqual
(
response
.
data
,
expected
)
def
test_get_with_intervals
(
self
):
course
=
G
(
Course
)
country1
=
G
(
Country
)
country2
=
G
(
Country
)
date
=
datetime
.
date
(
2014
,
1
,
1
)
ce1
=
G
(
CourseEnrollmentByCountry
,
course
=
course
,
country
=
country1
,
date
=
date
)
ce2
=
G
(
CourseEnrollmentByCountry
,
course
=
course
,
country
=
country2
,
date
=
date
)
# If start date is after date of existing data, no data should be returned
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s?start_date=2014-02-01'
%
(
course
.
course_id
,
self
.
path
,))
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertListEqual
([],
response
.
data
)
# If end date is before date of existing data, no data should be returned
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s?end_date=2013-02-01'
%
(
course
.
course_id
,
self
.
path
,))
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertListEqual
([],
response
.
data
)
# If data falls in date range, data should be returned
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s?start_date=2013-02-01&end_date=2014-02-01'
%
(
course
.
course_id
,
self
.
path
,))
self
.
assertEquals
(
response
.
status_code
,
200
)
expected
=
self
.
get_expected_response
(
ce1
,
ce2
)
self
.
assertListEqual
(
response
.
data
,
expected
)
@classmethod
def
setUpClass
(
cls
):
cls
.
course
=
G
(
Course
)
cls
.
date
=
datetime
.
date
(
2014
,
1
,
1
)
G
(
cls
.
model
,
course
=
cls
.
course
,
country
=
G
(
Country
),
count
=
455
,
date
=
cls
.
date
)
G
(
cls
.
model
,
course
=
cls
.
course
,
country
=
G
(
Country
),
count
=
356
,
date
=
cls
.
date
)
G
(
cls
.
model
,
course
=
cls
.
course
,
country
=
G
(
Country
),
count
=
12
,
date
=
cls
.
date
-
datetime
.
timedelta
(
days
=
29
))
analytics_data_api/v0/urls/courses.py
View file @
acefe9fc
...
...
@@ -7,7 +7,7 @@ from analytics_data_api.v0.views import courses as views
COURSE_URLS
=
[
(
'recent_activity'
,
views
.
CourseActivityMostRecentWeekView
,
'recent_activity'
),
(
'enrollment'
,
views
.
CourseEnrollment
Latest
View
,
'enrollment_latest'
),
(
'enrollment'
,
views
.
CourseEnrollmentView
,
'enrollment_latest'
),
(
'enrollment/birth_year'
,
views
.
CourseEnrollmentByBirthYearView
,
'enrollment_by_birth_year'
),
(
'enrollment/education'
,
views
.
CourseEnrollmentByEducationView
,
'enrollment_by_education'
),
(
'enrollment/gender'
,
views
.
CourseEnrollmentByGenderView
,
'enrollment_by_gender'
),
...
...
analytics_data_api/v0/views/courses.py
View file @
acefe9fc
import
datetime
from
django.conf
import
settings
from
django.core.exceptions
import
ObjectDoesNotExist
from
django.db.models
import
Max
from
django.http
import
Http404
from
rest_framework
import
generics
from
rest_framework.generics
import
RetrieveAPIView
,
get_object_or_404
from
rest_framework.response
import
Response
from
rest_framework.views
import
APIView
from
rest_framework.generics
import
get_object_or_404
from
analytics_data_api.v0.models
import
CourseActivityByWeek
,
CourseEnrollmentByBirthYear
,
\
CourseEnrollmentByEducation
,
CourseEnrollmentByGender
,
CourseEnrollmentByCountry
,
CourseEnrollmentDaily
,
Course
from
analytics_data_api.v0.serializers
import
CourseActivityByWeekSerializer
,
CourseEnrollmentByCountrySerializer
,
\
CourseEnrollmentDailySerializer
from
analytics_data_api.v0
import
models
,
serializers
class
CourseActivityMostRecentWeekView
(
generics
.
RetrieveAPIView
):
...
...
@@ -42,7 +38,7 @@ class CourseActivityMostRecentWeekView(generics.RetrieveAPIView):
"""
serializer_class
=
CourseActivityByWeekSerializer
serializer_class
=
serializers
.
CourseActivityByWeekSerializer
def
get_object
(
self
,
queryset
=
None
):
"""Select the activity report for the given course and activity type."""
...
...
@@ -51,63 +47,78 @@ class CourseActivityMostRecentWeekView(generics.RetrieveAPIView):
activity_type
=
activity_type
.
lower
()
try
:
return
CourseActivityByWeek
.
get_most_recent
(
course_id
,
activity_type
)
return
models
.
CourseActivityByWeek
.
get_most_recent
(
course_id
,
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!'
)
class
BaseCourseEnrollmentView
(
generics
.
ListAPIView
):
def
get_course_or_404
(
self
):
return
get_object_or_404
(
models
.
Course
,
course_id
=
self
.
kwargs
.
get
(
'course_id'
))
course_id
=
self
.
kwargs
[
'course_id'
]
data
=
self
.
model
.
objects
.
filter
(
course__course_id
=
course_id
)
def
apply_date_filtering
(
self
,
queryset
):
if
'start_date'
in
self
.
request
.
QUERY_PARAMS
or
'end_date'
in
self
.
request
.
QUERY_PARAMS
:
# Filter by start/end date
start_date
=
self
.
request
.
QUERY_PARAMS
.
get
(
'start_date'
)
if
start_date
:
start_date
=
datetime
.
datetime
.
strptime
(
start_date
,
settings
.
DATE_FORMAT
)
queryset
=
queryset
.
filter
(
date__gte
=
start_date
)
if
not
data
:
raise
Http404
end_date
=
self
.
request
.
QUERY_PARAMS
.
get
(
'end_date'
)
if
end_date
:
end_date
=
datetime
.
datetime
.
strptime
(
end_date
,
settings
.
DATE_FORMAT
)
queryset
=
queryset
.
filter
(
date__lt
=
end_date
)
else
:
# No date filter supplied, so only return data for the latest date
latest_date
=
queryset
.
aggregate
(
Max
(
'date'
))
if
latest_date
:
latest_date
=
latest_date
[
'date__max'
]
queryset
=
queryset
.
filter
(
date
=
latest_date
)
return
queryset
return
Response
(
self
.
render_data
(
data
))
def
get_queryset
(
self
):
course
=
self
.
get_course_or_404
()
queryset
=
self
.
model
.
objects
.
filter
(
course
=
course
)
queryset
=
self
.
apply_date_filtering
(
queryset
)
return
queryset
class
CourseEnrollmentByBirthYearView
(
Abstract
CourseEnrollmentView
):
class
CourseEnrollmentByBirthYearView
(
Base
CourseEnrollmentView
):
"""
Course enrollment broken down by user birth year
Returns the enrollment of a course with users binned by their birth years.
"""
model
=
CourseEnrollmentByBirthYear
If no start or end dates are passed, the data for the latest date is returned. All dates should are in the UTC zone.
def
render_data
(
self
,
data
):
return
{
'birth_years'
:
dict
(
data
.
values_list
(
'birth_year'
,
'count'
))
}
Date format: YYYY-mm-dd (e.g. 2014-01-31)
start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive)
"""
serializer_class
=
serializers
.
CourseEnrollmentByBirthYearSerializer
model
=
models
.
CourseEnrollmentByBirthYear
class
CourseEnrollmentByEducationView
(
Abstract
CourseEnrollmentView
):
class
CourseEnrollmentByEducationView
(
Base
CourseEnrollmentView
):
"""
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'
,
'count'
))
}
If no start or end dates are passed, the data for the latest date is returned. All dates should are in the UTC zone.
Date format: YYYY-mm-dd (e.g. 2014-01-31)
class
CourseEnrollmentByGenderView
(
AbstractCourseEnrollmentView
):
start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive)
"""
serializer_class
=
serializers
.
CourseEnrollmentByEducationSerializer
model
=
models
.
CourseEnrollmentByEducation
class
CourseEnrollmentByGenderView
(
BaseCourseEnrollmentView
):
"""
Course enrollment broken down by user gender
...
...
@@ -117,66 +128,56 @@ class CourseEnrollmentByGenderView(AbstractCourseEnrollmentView):
m - male
f - female
o - other
If no start or end dates are passed, the data for the latest date is returned. All dates should are in the UTC zone.
Date format: YYYY-mm-dd (e.g. 2014-01-31)
start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive)
"""
model
=
CourseEnrollmentByGender
serializer_class
=
serializers
.
CourseEnrollmentByGenderSerializer
model
=
models
.
CourseEnrollmentByGender
def
render_data
(
self
,
data
):
return
{
'genders'
:
dict
(
data
.
values_list
(
'gender'
,
'count'
))
}
class
CourseEnrollmentView
(
BaseCourseEnrollmentView
):
"""
Returns the enrollment count for the specified course.
class
CourseEnrollmentLatestView
(
RetrieveAPIView
):
""" Returns the latest enrollment count for the specified course. """
model
=
CourseEnrollmentDaily
serializer_class
=
CourseEnrollmentDailySerializer
If no start or end dates are passed, the data for the latest date is returned. All dates should are in the UTC zone.
def
get_object
(
self
,
queryset
=
None
):
try
:
course_id
=
self
.
kwargs
[
'course_id'
]
return
CourseEnrollmentDaily
.
objects
.
filter
(
course__course_id
=
course_id
)
.
order_by
(
'-date'
)[
0
]
except
IndexError
:
raise
Http404
Date format: YYYY-mm-dd (e.g. 2014-01-31)
start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive)
"""
serializer_class
=
serializers
.
CourseEnrollmentDailySerializer
model
=
models
.
CourseEnrollmentDaily
# pylint: disable=line-too-long
class
CourseEnrollmentByLocationView
(
generics
.
ListAPI
View
):
class
CourseEnrollmentByLocationView
(
BaseCourseEnrollment
View
):
"""
Course enrollment broken down by user location
Returns the enrollment of a course with users binned by their location. Location is calculated based on the user's
IP address.
If no start or end dates are passed, the data for the latest date is returned.
IP address.
Countries are denoted by their <a href="http://www.iso.org/iso/country_codes/country_codes" target="_blank">ISO 3166 country code</a>.
If no start or end dates are passed, the data for the latest date is returned. All dates should are in the UTC zone.
Date format: YYYY-mm-dd (e.g. 2014-01-31)
start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive)
"""
serializer_class
=
CourseEnrollmentByCountrySerializer
serializer_class
=
serializers
.
CourseEnrollmentByCountrySerializer
def
get_queryset
(
self
):
course
=
get_object_or_404
(
Course
,
course_id
=
self
.
kwargs
.
get
(
'course_id'
))
queryset
=
CourseEnrollmentByCountry
.
objects
.
filter
(
course
=
course
)
if
'start_date'
in
self
.
request
.
QUERY_PARAMS
or
'end_date'
in
self
.
request
.
QUERY_PARAMS
:
# Filter by start/end date
start_date
=
self
.
request
.
QUERY_PARAMS
.
get
(
'start_date'
)
if
start_date
:
start_date
=
datetime
.
datetime
.
strptime
(
start_date
,
settings
.
DATE_FORMAT
)
queryset
=
queryset
.
filter
(
date__gte
=
start_date
)
end_date
=
self
.
request
.
QUERY_PARAMS
.
get
(
'end_date'
)
if
end_date
:
end_date
=
datetime
.
datetime
.
strptime
(
end_date
,
settings
.
DATE_FORMAT
)
queryset
=
queryset
.
filter
(
date__lt
=
end_date
)
else
:
# No date filter supplied, so only return data for the latest date
latest_date
=
queryset
.
aggregate
(
Max
(
'date'
))
if
latest_date
:
latest_date
=
latest_date
[
'date__max'
]
queryset
=
queryset
.
filter
(
date
=
latest_date
)
course
=
self
.
get_course_or_404
()
queryset
=
models
.
CourseEnrollmentByCountry
.
objects
.
filter
(
course
=
course
)
queryset
=
self
.
apply_date_filtering
(
queryset
)
return
queryset
analyticsdataserver/settings/local.py
View file @
acefe9fc
...
...
@@ -83,3 +83,7 @@ ENABLE_ADMIN_SITE = True
########## END ANALYTICS DATA API CONFIGURATION
TEST_RUNNER
=
'django_nose.NoseTestSuiteRunner'
SWAGGER_SETTINGS
=
{
'api_key'
:
'analytics'
}
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