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
Hide 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 @@
...
@@ -88,5 +88,95 @@
"count"
:
9940
,
"count"
:
9940
,
"date"
:
"2014-07-01"
"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 @@
...
@@ -28,5 +28,35 @@
"count"
:
423
,
"count"
:
423
,
"date"
:
"2014-07-01"
"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
django.conf
import
settings
from
rest_framework
import
serializers
from
rest_framework
import
serializers
from
analytics_data_api.v0.models
import
CourseActivityByWeek
,
ProblemResponseAnswerDistribution
,
\
from
analytics_data_api.v0
import
models
CourseEnrollmentDaily
,
CourseEnrollmentByCountry
,
Country
class
CourseIdMixin
(
object
):
class
CourseIdMixin
(
object
):
...
@@ -24,7 +23,7 @@ class CourseActivityByWeekSerializer(serializers.ModelSerializer, CourseIdMixin)
...
@@ -24,7 +23,7 @@ class CourseActivityByWeekSerializer(serializers.ModelSerializer, CourseIdMixin)
course_id
=
RequiredSerializerMethodField
(
'get_course_id'
)
course_id
=
RequiredSerializerMethodField
(
'get_course_id'
)
class
Meta
(
object
):
class
Meta
(
object
):
model
=
CourseActivityByWeek
model
=
models
.
CourseActivityByWeek
fields
=
(
'interval_start'
,
'interval_end'
,
'activity_type'
,
'count'
,
'course_id'
)
fields
=
(
'interval_start'
,
'interval_end'
,
'activity_type'
,
'count'
,
'course_id'
)
...
@@ -37,7 +36,7 @@ class ProblemResponseAnswerDistributionSerializer(serializers.ModelSerializer):
...
@@ -37,7 +36,7 @@ class ProblemResponseAnswerDistributionSerializer(serializers.ModelSerializer):
"""
"""
class
Meta
(
object
):
class
Meta
(
object
):
model
=
ProblemResponseAnswerDistribution
model
=
models
.
ProblemResponseAnswerDistribution
fields
=
(
fields
=
(
'course_id'
,
'course_id'
,
'module_id'
,
'module_id'
,
...
@@ -52,30 +51,56 @@ class ProblemResponseAnswerDistributionSerializer(serializers.ModelSerializer):
...
@@ -52,30 +51,56 @@ class ProblemResponseAnswerDistributionSerializer(serializers.ModelSerializer):
)
)
class
CourseEnrollmentDailySerializer
(
serializers
.
ModelSerializer
,
CourseIdMixin
):
class
BaseCourseEnrollmentModelSerializer
(
serializers
.
ModelSerializer
,
CourseIdMixin
):
"""
Representation of course enrollment for a single day and course.
"""
course_id
=
RequiredSerializerMethodField
(
'get_course_id'
)
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
):
class
Meta
(
object
):
model
=
CourseEnrollmentDaily
model
=
models
.
CourseEnrollmentDaily
fields
=
(
'course_id'
,
'date'
,
'count'
)
fields
=
(
'course_id'
,
'date'
,
'count'
)
# pylint: disable=no-value-for-parameter
# pylint: disable=no-value-for-parameter
class
CountrySerializer
(
serializers
.
ModelSerializer
):
class
CountrySerializer
(
serializers
.
ModelSerializer
):
class
Meta
(
object
):
class
Meta
(
object
):
model
=
Country
model
=
models
.
Country
fields
=
(
'code'
,
'name'
)
fields
=
(
'code'
,
'name'
)
class
CourseEnrollmentByCountrySerializer
(
serializers
.
ModelSerializer
,
CourseIdMixin
):
# pylint: disable=no-value-for-parameter
course_id
=
RequiredSerializerMethodField
(
'get_course_id'
)
class
EducationLevelSerializer
(
serializers
.
ModelSerializer
):
class
Meta
(
object
):
model
=
models
.
EducationLevel
fields
=
(
'name'
,
'short_name'
)
class
CourseEnrollmentByCountrySerializer
(
BaseCourseEnrollmentModelSerializer
):
country
=
CountrySerializer
()
country
=
CountrySerializer
()
date
=
serializers
.
DateField
(
format
=
settings
.
DATE_FORMAT
)
class
Meta
(
object
):
class
Meta
(
object
):
model
=
CourseEnrollmentByCountry
model
=
models
.
CourseEnrollmentByCountry
fields
=
(
'date'
,
'course_id'
,
'country'
,
'count'
)
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):
...
@@ -81,6 +81,7 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
self
.
assertEquals
(
response
.
status_code
,
404
)
self
.
assertEquals
(
response
.
status_code
,
404
)
# pylint: disable=no-member
class
CourseEnrollmentViewTestCase
(
object
):
class
CourseEnrollmentViewTestCase
(
object
):
model
=
None
model
=
None
path
=
None
path
=
None
...
@@ -93,15 +94,49 @@ class CourseEnrollmentViewTestCase(object):
...
@@ -93,15 +94,49 @@ class CourseEnrollmentViewTestCase(object):
return
self
.
_get_non_existent_course_id
()
return
self
.
_get_non_existent_course_id
()
def
get_expected_response
(
self
,
*
args
):
raise
NotImplementedError
def
test_get_not_found
(
self
):
def
test_get_not_found
(
self
):
""" Requests made against non-existent courses should return a 404 """
""" Requests made against non-existent courses should return a 404 """
course_id
=
self
.
_get_non_existent_course_id
()
course_id
=
self
.
_get_non_existent_course_id
()
self
.
assertFalse
(
self
.
model
.
objects
.
filter
(
course__course_id
=
course_id
)
.
exists
())
# pylint: disable=no-member
self
.
assertFalse
(
self
.
model
.
objects
.
filter
(
course__course_id
=
course_id
)
.
exists
())
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s'
%
(
course_id
,
self
.
path
))
# pylint: disable=no-member
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s'
%
(
course_id
,
self
.
path
))
self
.
assertEquals
(
response
.
status_code
,
404
)
# pylint: disable=no-member
self
.
assertEquals
(
response
.
status_code
,
404
)
def
test_get
(
self
):
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
):
class
CourseEnrollmentByBirthYearViewTests
(
TestCaseWithAuthentication
,
CourseEnrollmentViewTestCase
):
...
@@ -111,23 +146,30 @@ class CourseEnrollmentByBirthYearViewTests(TestCaseWithAuthentication, CourseEnr
...
@@ -111,23 +146,30 @@ class CourseEnrollmentByBirthYearViewTests(TestCaseWithAuthentication, CourseEnr
@classmethod
@classmethod
def
setUpClass
(
cls
):
def
setUpClass
(
cls
):
cls
.
course
=
G
(
Course
)
cls
.
course
=
G
(
Course
)
cls
.
ce1
=
G
(
CourseEnrollmentByBirthYear
,
course
=
cls
.
course
,
birth_year
=
1956
)
cls
.
date
=
datetime
.
date
(
2014
,
1
,
1
)
cls
.
ce2
=
G
(
CourseEnrollmentByBirthYear
,
course
=
cls
.
course
,
birth_year
=
1986
)
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
):
def
test_get
(
self
):
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s'
%
(
self
.
course
.
course_id
,
self
.
path
,))
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s'
%
(
self
.
course
.
course_id
,
self
.
path
,))
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
status_code
,
200
)
expected
=
{
expected
=
self
.
get_expected_response
(
*
self
.
model
.
objects
.
filter
(
date
=
self
.
date
))
self
.
ce1
.
birth_year
:
self
.
ce1
.
count
,
self
.
assertEquals
(
response
.
data
,
expected
)
self
.
ce2
.
birth_year
:
self
.
ce2
.
count
,
}
def
test_get_with_intervals
(
self
):
actual
=
response
.
data
[
'birth_years'
]
expected
=
self
.
get_expected_response
(
*
self
.
model
.
objects
.
filter
(
date
=
self
.
date
))
self
.
assert
Equals
(
actual
,
expected
)
self
.
assert
IntervalFilteringWorks
(
expected
,
self
.
date
,
self
.
date
+
datetime
.
timedelta
(
days
=
1
)
)
class
CourseEnrollmentByEducationViewTests
(
TestCaseWithAuthentication
,
CourseEnrollmentViewTestCase
):
class
CourseEnrollmentByEducationViewTests
(
TestCaseWithAuthentication
,
CourseEnrollmentViewTestCase
):
path
=
'/enrollment/education'
path
=
'/enrollment/education
/
'
model
=
CourseEnrollmentByEducation
model
=
CourseEnrollmentByEducation
@classmethod
@classmethod
...
@@ -135,41 +177,33 @@ class CourseEnrollmentByEducationViewTests(TestCaseWithAuthentication, CourseEnr
...
@@ -135,41 +177,33 @@ class CourseEnrollmentByEducationViewTests(TestCaseWithAuthentication, CourseEnr
cls
.
el1
=
G
(
EducationLevel
,
name
=
'Doctorate'
,
short_name
=
'doctorate'
)
cls
.
el1
=
G
(
EducationLevel
,
name
=
'Doctorate'
,
short_name
=
'doctorate'
)
cls
.
el2
=
G
(
EducationLevel
,
name
=
'Top Secret'
,
short_name
=
'top_secret'
)
cls
.
el2
=
G
(
EducationLevel
,
name
=
'Top Secret'
,
short_name
=
'top_secret'
)
cls
.
course
=
G
(
Course
)
cls
.
course
=
G
(
Course
)
cls
.
ce1
=
G
(
CourseEnrollmentByEducation
,
course
=
cls
.
course
,
education_level
=
cls
.
el1
)
cls
.
date
=
datetime
.
date
(
2014
,
1
,
1
)
cls
.
ce2
=
G
(
CourseEnrollmentByEducation
,
course
=
cls
.
course
,
education_level
=
cls
.
el2
)
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
)
def
test_get
(
self
):
G
(
cls
.
model
,
course
=
cls
.
course
,
date
=
cls
.
date
-
datetime
.
timedelta
(
days
=
2
),
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s'
%
(
self
.
course
.
course_id
,
self
.
path
,))
education_level
=
cls
.
el2
)
self
.
assertEquals
(
response
.
status_code
,
200
)
expected
=
{
def
get_expected_response
(
self
,
*
args
):
self
.
ce1
.
education_level
.
short_name
:
self
.
ce1
.
count
,
return
[{
'course_id'
:
ce
.
course
.
course_id
,
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
self
.
ce2
.
education_level
.
short_name
:
self
.
ce2
.
count
,
'education_level'
:
{
'name'
:
ce
.
education_level
.
name
,
'short_name'
:
ce
.
education_level
.
short_name
}}
for
}
ce
in
args
]
actual
=
response
.
data
[
'education_levels'
]
self
.
assertEquals
(
actual
,
expected
)
class
CourseEnrollmentByGenderViewTests
(
TestCaseWithAuthentication
,
CourseEnrollmentViewTestCase
):
class
CourseEnrollmentByGenderViewTests
(
TestCaseWithAuthentication
,
CourseEnrollmentViewTestCase
):
path
=
'/enrollment/gender'
path
=
'/enrollment/gender
/
'
model
=
CourseEnrollmentByGender
model
=
CourseEnrollmentByGender
@classmethod
@classmethod
def
setUpClass
(
cls
):
def
setUpClass
(
cls
):
cls
.
course
=
G
(
Course
)
cls
.
course
=
G
(
Course
)
cls
.
ce1
=
G
(
CourseEnrollmentByGender
,
course
=
cls
.
course
,
gender
=
'm'
)
cls
.
date
=
datetime
.
date
(
2014
,
1
,
1
)
cls
.
ce2
=
G
(
CourseEnrollmentByGender
,
course
=
cls
.
course
,
gender
=
'f'
)
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
)
def
test_get
(
self
):
G
(
cls
.
model
,
course
=
cls
.
course
,
gender
=
'f'
,
date
=
cls
.
date
-
datetime
.
timedelta
(
days
=
2
),
count
=
45
)
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s'
%
(
self
.
course
.
course_id
,
self
.
path
,))
self
.
assertEquals
(
response
.
status_code
,
200
)
expected
=
{
def
get_expected_response
(
self
,
*
args
):
self
.
ce1
.
gender
:
self
.
ce1
.
count
,
return
[{
'course_id'
:
ce
.
course
.
course_id
,
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
self
.
ce2
.
gender
:
self
.
ce2
.
count
,
'gender'
:
ce
.
gender
}
for
ce
in
args
]
}
actual
=
response
.
data
[
'genders'
]
self
.
assertEquals
(
actual
,
expected
)
# pylint: disable=no-member,no-value-for-parameter
# pylint: disable=no-member,no-value-for-parameter
...
@@ -203,20 +237,20 @@ class AnswerDistributionTests(TestCaseWithAuthentication):
...
@@ -203,20 +237,20 @@ class AnswerDistributionTests(TestCaseWithAuthentication):
self
.
assertEquals
(
response
.
status_code
,
404
)
self
.
assertEquals
(
response
.
status_code
,
404
)
class
CourseEnrollment
Latest
ViewTests
(
TestCaseWithAuthentication
,
CourseEnrollmentViewTestCase
):
class
CourseEnrollmentViewTests
(
TestCaseWithAuthentication
,
CourseEnrollmentViewTestCase
):
model
=
CourseEnrollmentDaily
model
=
CourseEnrollmentDaily
path
=
'/enrollment'
path
=
'/enrollment'
@classmethod
@classmethod
def
setUpClass
(
cls
):
def
setUpClass
(
cls
):
cls
.
course
=
G
(
Course
)
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
):
def
get_expected_response
(
self
,
*
args
):
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s'
%
(
self
.
course
.
course_id
,
self
.
path
,))
return
[{
'course_id'
:
ce
.
course
.
course_id
,
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
)}
self
.
assertEquals
(
response
.
status_code
,
200
)
for
ce
in
args
]
expected
=
{
'course_id'
:
self
.
ce
.
course
.
course_id
,
'count'
:
self
.
ce
.
count
,
'date'
:
self
.
ce
.
date
}
self
.
assertDictEqual
(
response
.
data
,
expected
)
class
CourseEnrollmentByLocationViewTests
(
TestCaseWithAuthentication
,
CourseEnrollmentViewTestCase
):
class
CourseEnrollmentByLocationViewTests
(
TestCaseWithAuthentication
,
CourseEnrollmentViewTestCase
):
...
@@ -227,43 +261,10 @@ class CourseEnrollmentByLocationViewTests(TestCaseWithAuthentication, CourseEnro
...
@@ -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
),
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
]
'country'
:
{
'code'
:
ce
.
country
.
code
,
'name'
:
ce
.
country
.
name
}}
for
ce
in
args
]
def
test_get
(
self
):
@classmethod
course
=
G
(
Course
)
def
setUpClass
(
cls
):
date1
=
datetime
.
date
(
2014
,
1
,
1
)
cls
.
course
=
G
(
Course
)
date2
=
datetime
.
date
(
2013
,
1
,
1
)
cls
.
date
=
datetime
.
date
(
2014
,
1
,
1
)
ce1
=
G
(
CourseEnrollmentByCountry
,
course
=
course
,
country
=
G
(
Country
),
count
=
455
,
date
=
date1
)
G
(
cls
.
model
,
course
=
cls
.
course
,
country
=
G
(
Country
),
count
=
455
,
date
=
cls
.
date
)
ce2
=
G
(
CourseEnrollmentByCountry
,
course
=
course
,
country
=
G
(
Country
),
count
=
356
,
date
=
date1
)
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
))
# 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
)
analytics_data_api/v0/urls/courses.py
View file @
acefe9fc
...
@@ -7,7 +7,7 @@ from analytics_data_api.v0.views import courses as views
...
@@ -7,7 +7,7 @@ from analytics_data_api.v0.views import courses as views
COURSE_URLS
=
[
COURSE_URLS
=
[
(
'recent_activity'
,
views
.
CourseActivityMostRecentWeekView
,
'recent_activity'
),
(
'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/birth_year'
,
views
.
CourseEnrollmentByBirthYearView
,
'enrollment_by_birth_year'
),
(
'enrollment/education'
,
views
.
CourseEnrollmentByEducationView
,
'enrollment_by_education'
),
(
'enrollment/education'
,
views
.
CourseEnrollmentByEducationView
,
'enrollment_by_education'
),
(
'enrollment/gender'
,
views
.
CourseEnrollmentByGenderView
,
'enrollment_by_gender'
),
(
'enrollment/gender'
,
views
.
CourseEnrollmentByGenderView
,
'enrollment_by_gender'
),
...
...
analytics_data_api/v0/views/courses.py
View file @
acefe9fc
import
datetime
import
datetime
from
django.conf
import
settings
from
django.conf
import
settings
from
django.core.exceptions
import
ObjectDoesNotExist
from
django.core.exceptions
import
ObjectDoesNotExist
from
django.db.models
import
Max
from
django.db.models
import
Max
from
django.http
import
Http404
from
django.http
import
Http404
from
rest_framework
import
generics
from
rest_framework
import
generics
from
rest_framework.generics
import
RetrieveAPIView
,
get_object_or_404
from
rest_framework.generics
import
get_object_or_404
from
rest_framework.response
import
Response
from
rest_framework.views
import
APIView
from
analytics_data_api.v0.models
import
CourseActivityByWeek
,
CourseEnrollmentByBirthYear
,
\
from
analytics_data_api.v0
import
models
,
serializers
CourseEnrollmentByEducation
,
CourseEnrollmentByGender
,
CourseEnrollmentByCountry
,
CourseEnrollmentDaily
,
Course
from
analytics_data_api.v0.serializers
import
CourseActivityByWeekSerializer
,
CourseEnrollmentByCountrySerializer
,
\
CourseEnrollmentDailySerializer
class
CourseActivityMostRecentWeekView
(
generics
.
RetrieveAPIView
):
class
CourseActivityMostRecentWeekView
(
generics
.
RetrieveAPIView
):
...
@@ -42,7 +38,7 @@ 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
):
def
get_object
(
self
,
queryset
=
None
):
"""Select the activity report for the given course and activity type."""
"""Select the activity report for the given course and activity type."""
...
@@ -51,63 +47,78 @@ class CourseActivityMostRecentWeekView(generics.RetrieveAPIView):
...
@@ -51,63 +47,78 @@ class CourseActivityMostRecentWeekView(generics.RetrieveAPIView):
activity_type
=
activity_type
.
lower
()
activity_type
=
activity_type
.
lower
()
try
:
try
:
return
CourseActivityByWeek
.
get_most_recent
(
course_id
,
activity_type
)
return
models
.
CourseActivityByWeek
.
get_most_recent
(
course_id
,
activity_type
)
except
ObjectDoesNotExist
:
except
ObjectDoesNotExist
:
raise
Http404
raise
Http404
class
AbstractCourseEnrollmentView
(
APIView
):
class
BaseCourseEnrollmentView
(
generics
.
ListAPIView
):
model
=
None
def
get_course_or_404
(
self
):
return
get_object_or_404
(
models
.
Course
,
course_id
=
self
.
kwargs
.
get
(
'course_id'
))
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_id
=
self
.
kwargs
[
'course_id'
]
def
apply_date_filtering
(
self
,
queryset
):
data
=
self
.
model
.
objects
.
filter
(
course__course_id
=
course_id
)
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
:
end_date
=
self
.
request
.
QUERY_PARAMS
.
get
(
'end_date'
)
raise
Http404
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
Course enrollment broken down by user birth year
Returns the enrollment of a course with users binned by their birth years.
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.
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)
"""
def
render_data
(
self
,
data
):
serializer_class
=
serializers
.
CourseEnrollmentByBirthYearSerializer
return
{
model
=
models
.
CourseEnrollmentByBirthYear
'birth_years'
:
dict
(
data
.
values_list
(
'birth_year'
,
'count'
))
}
class
CourseEnrollmentByEducationView
(
Abstract
CourseEnrollmentView
):
class
CourseEnrollmentByEducationView
(
Base
CourseEnrollmentView
):
"""
"""
Course enrollment broken down by user level of education
Course enrollment broken down by user level of education
Returns the enrollment of a course with users binned by their education levels.
Returns the enrollment of a course with users binned by their education levels.
"""
model
=
CourseEnrollmentByEducation
def
render_data
(
self
,
data
):
If no start or end dates are passed, the data for the latest date is returned. All dates should are in the UTC zone.
return
{
'education_levels'
:
dict
(
data
.
values_list
(
'education_level__short_name'
,
'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
.
CourseEnrollmentByEducationSerializer
model
=
models
.
CourseEnrollmentByEducation
class
CourseEnrollmentByGenderView
(
Abstract
CourseEnrollmentView
):
class
CourseEnrollmentByGenderView
(
Base
CourseEnrollmentView
):
"""
"""
Course enrollment broken down by user gender
Course enrollment broken down by user gender
...
@@ -117,66 +128,56 @@ class CourseEnrollmentByGenderView(AbstractCourseEnrollmentView):
...
@@ -117,66 +128,56 @@ class CourseEnrollmentByGenderView(AbstractCourseEnrollmentView):
m - male
m - male
f - female
f - female
o - other
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
):
If no start or end dates are passed, the data for the latest date is returned. All dates should are in the UTC zone.
""" Returns the latest enrollment count for the specified course. """
model
=
CourseEnrollmentDaily
serializer_class
=
CourseEnrollmentDailySerializer
def
get_object
(
self
,
queryset
=
None
):
Date format: YYYY-mm-dd (e.g. 2014-01-31)
try
:
course_id
=
self
.
kwargs
[
'course_id'
]
start_date -- Date after which all data should be returned (inclusive)
return
CourseEnrollmentDaily
.
objects
.
filter
(
course__course_id
=
course_id
)
.
order_by
(
'-date'
)[
0
]
end_date -- Date before which all data should be returned (exclusive)
except
IndexError
:
"""
raise
Http404
serializer_class
=
serializers
.
CourseEnrollmentDailySerializer
model
=
models
.
CourseEnrollmentDaily
# pylint: disable=line-too-long
# pylint: disable=line-too-long
class
CourseEnrollmentByLocationView
(
generics
.
ListAPI
View
):
class
CourseEnrollmentByLocationView
(
BaseCourseEnrollment
View
):
"""
"""
Course enrollment broken down by user location
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
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>.
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)
Date format: YYYY-mm-dd (e.g. 2014-01-31)
start_date -- Date after which all data should be returned (inclusive)
start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive)
end_date -- Date before which all data should be returned (exclusive)
"""
"""
serializer_class
=
CourseEnrollmentByCountrySerializer
serializer_class
=
serializers
.
CourseEnrollmentByCountrySerializer
def
get_queryset
(
self
):
def
get_queryset
(
self
):
course
=
get_object_or_404
(
Course
,
course_id
=
self
.
kwargs
.
get
(
'course_id'
))
course
=
self
.
get_course_or_404
()
queryset
=
CourseEnrollmentByCountry
.
objects
.
filter
(
course
=
course
)
queryset
=
models
.
CourseEnrollmentByCountry
.
objects
.
filter
(
course
=
course
)
queryset
=
self
.
apply_date_filtering
(
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
)
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
queryset
analyticsdataserver/settings/local.py
View file @
acefe9fc
...
@@ -83,3 +83,7 @@ ENABLE_ADMIN_SITE = True
...
@@ -83,3 +83,7 @@ ENABLE_ADMIN_SITE = True
########## END ANALYTICS DATA API CONFIGURATION
########## END ANALYTICS DATA API CONFIGURATION
TEST_RUNNER
=
'django_nose.NoseTestSuiteRunner'
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