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
646ad4a1
Commit
646ad4a1
authored
Oct 15, 2014
by
Clinton Blackburn
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #34 from edx/opaque-keys
Updated Course ID Regex and CSV filename
parents
29437878
66914fa0
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
165 additions
and
79 deletions
+165
-79
analytics_data_api/v0/tests/test_views.py
+139
-70
analytics_data_api/v0/urls/courses.py
+3
-4
analytics_data_api/v0/views/courses.py
+22
-5
requirements/base.txt
+1
-0
No files found.
analytics_data_api/v0/tests/test_views.py
View file @
646ad4a1
# coding=utf-8
# 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.
...
...
@@ -5,11 +6,13 @@ import StringIO
import
csv
import
datetime
from
itertools
import
groupby
import
urllib
from
django.conf
import
settings
from
django_dynamic_fixture
import
G
from
iso3166
import
countries
import
pytz
from
opaque_keys.edx.keys
import
CourseKey
from
analytics_data_api.v0
import
models
from
analytics_data_api.v0.constants
import
UNKNOWN_COUNTRY
,
UNKNOWN_COUNTRY_CODE
...
...
@@ -19,12 +22,29 @@ from analytics_data_api.v0.tests.utils import flatten
from
analyticsdataserver.tests
import
TestCaseWithAuthentication
DEMO_COURSE_ID
=
u'course-v1:edX+DemoX+Demo_2014'
class
DemoCourseMixin
(
object
):
course_key
=
None
course_id
=
None
def
setUp
(
self
):
self
.
course_id
=
DEMO_COURSE_ID
self
.
course_key
=
CourseKey
.
from_string
(
self
.
course_id
)
super
(
DemoCourseMixin
,
self
)
.
setUp
()
# pylint: disable=no-member
class
CourseViewTestCaseMixin
(
object
):
class
CourseViewTestCaseMixin
(
DemoCourseMixin
):
model
=
None
api_root_path
=
'/api/v0/'
path
=
None
order_by
=
[]
csv_filename_slug
=
None
def
generate_data
(
self
,
course_id
=
None
):
raise
NotImplementedError
def
format_as_response
(
self
,
*
args
):
"""
...
...
@@ -35,7 +55,7 @@ class CourseViewTestCaseMixin(object):
"""
raise
NotImplementedError
def
get_latest_data
(
self
):
def
get_latest_data
(
self
,
course_id
=
None
):
"""
Return the latest row/rows that would be returned if a user made a call
to the endpoint with no date filtering.
...
...
@@ -44,34 +64,37 @@ class CourseViewTestCaseMixin(object):
"""
raise
NotImplementedError
def
get_csv_filename
(
self
):
return
u'edX-DemoX-Demo_2014--{0}.csv'
.
format
(
self
.
csv_filename_slug
)
def
test_get_not_found
(
self
):
""" Requests made against non-existent courses should return a 404 """
course_id
=
'edX/DemoX/Non_Existent_Course'
response
=
self
.
authenticated_get
(
'
%
scourses/
%
s
%
s'
%
(
self
.
api_root_path
,
course_id
,
self
.
path
))
course_id
=
u
'edX/DemoX/Non_Existent_Course'
response
=
self
.
authenticated_get
(
u
'
%
scourses/
%
s
%
s'
%
(
self
.
api_root_path
,
course_id
,
self
.
path
))
self
.
assertEquals
(
response
.
status_code
,
404
)
def
test_get
(
self
):
""" Verify the endpoint returns an HTTP 200 status and the correct data. """
# Validate the basic response status
response
=
self
.
authenticated_get
(
'
%
scourses/
%
s
%
s'
%
(
self
.
api_root_path
,
self
.
course_id
,
self
.
path
))
response
=
self
.
authenticated_get
(
u
'
%
scourses/
%
s
%
s'
%
(
self
.
api_root_path
,
self
.
course_id
,
self
.
path
))
self
.
assertEquals
(
response
.
status_code
,
200
)
# Validate the data is correct and sorted chronologically
expected
=
self
.
format_as_response
(
*
self
.
get_latest_data
())
self
.
assertEquals
(
response
.
data
,
expected
)
def
test_get_csv
(
self
):
""" Verify the endpoint returns data that has been properly converted to CSV. """
path
=
'
%
scourses/
%
s
%
s'
%
(
self
.
api_root_path
,
self
.
course_id
,
self
.
path
)
def
assertCSVIsValid
(
self
,
course_id
,
filename
):
path
=
u'{0}courses/{1}{2}'
.
format
(
self
.
api_root_path
,
course_id
,
self
.
path
)
csv_content_type
=
'text/csv'
response
=
self
.
authenticated_get
(
path
,
HTTP_ACCEPT
=
csv_content_type
)
# Validate the basic response status
and content cod
e
# Validate the basic response status
, content type, and filenam
e
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
[
'Content-Type'
]
.
split
(
';'
)[
0
],
csv_content_type
)
self
.
assertEquals
(
response
[
'Content-Disposition'
],
u'attachment; filename={}'
.
format
(
filename
))
# Validate the actual data
data
=
self
.
format_as_response
(
*
self
.
get_latest_data
())
data
=
self
.
format_as_response
(
*
self
.
get_latest_data
(
course_id
=
course_id
))
data
=
map
(
flatten
,
data
)
# The CSV renderer sorts the headers alphabetically
...
...
@@ -82,9 +105,21 @@ class CourseViewTestCaseMixin(object):
writer
=
csv
.
DictWriter
(
expected
,
fieldnames
)
writer
.
writeheader
()
writer
.
writerows
(
data
)
self
.
assertEqual
(
response
.
content
,
expected
.
getvalue
())
def
test_get_csv
(
self
):
""" Verify the endpoint returns data that has been properly converted to CSV. """
self
.
assertCSVIsValid
(
self
.
course_id
,
self
.
get_csv_filename
())
def
test_get_csv_with_deprecated_key
(
self
):
"""
Verify the endpoint returns data that has been properly converted to CSV even if the course ID is deprecated.
"""
course_id
=
u'edX/DemoX/Demo_Course'
self
.
generate_data
(
course_id
)
filename
=
u'{0}--{1}.csv'
.
format
(
u'edX-DemoX-Demo_Course'
,
self
.
csv_filename_slug
)
self
.
assertCSVIsValid
(
course_id
,
filename
)
def
test_get_with_intervals
(
self
):
""" Verify the endpoint returns multiple data points when supplied with an interval of dates. """
raise
NotImplementedError
...
...
@@ -115,46 +150,50 @@ class CourseViewTestCaseMixin(object):
# pylint: disable=abstract-method
class
CourseEnrollmentViewTestCaseMixin
(
CourseViewTestCaseMixin
):
date
=
None
def
setUp
(
self
):
super
(
CourseEnrollmentViewTestCaseMixin
,
self
)
.
setUp
()
self
.
course_id
=
'edX/DemoX/Demo_Course'
self
.
date
=
datetime
.
date
(
2014
,
1
,
1
)
def
get_latest_data
(
self
):
return
self
.
model
.
objects
.
filter
(
date
=
self
.
date
)
.
order_by
(
'date'
,
*
self
.
order_by
)
def
get_latest_data
(
self
,
course_id
=
None
):
course_id
=
course_id
or
self
.
course_id
return
self
.
model
.
objects
.
filter
(
course_id
=
course_id
,
date
=
self
.
date
)
.
order_by
(
'date'
,
*
self
.
order_by
)
def
test_get_with_intervals
(
self
):
expected
=
self
.
format_as_response
(
*
self
.
model
.
objects
.
filter
(
date
=
self
.
date
))
self
.
assertIntervalFilteringWorks
(
expected
,
self
.
date
,
self
.
date
+
datetime
.
timedelta
(
days
=
1
))
class
CourseActivityLastWeekTest
(
TestCaseWithAuthentication
):
# pylint: disable=line-too-long
def
setUp
(
self
):
super
(
CourseActivityLastWeekTest
,
self
)
.
setUp
()
self
.
course_id
=
'edX/DemoX/Demo_Course'
class
CourseActivityLastWeekTest
(
DemoCourseMixin
,
TestCaseWithAuthentication
):
def
generate_data
(
self
,
course_id
=
None
):
course_id
=
course_id
or
self
.
course_id
interval_start
=
datetime
.
datetime
(
2014
,
1
,
1
,
tzinfo
=
pytz
.
utc
)
interval_end
=
interval_start
+
datetime
.
timedelta
(
weeks
=
1
)
# G(models.CourseActivityWeekly, course_id=
self.
course_id, interval_start=interval_start,
#
interval_end=interval_end,
#
activity_type='POSTED_FORUM', count=100)
G
(
models
.
CourseActivityWeekly
,
course_id
=
self
.
course_id
,
interval_start
=
interval_start
,
# G(models.CourseActivityWeekly, course_id=course_id, interval_start=interval_start,
# interval_end=interval_end,
# activity_type='POSTED_FORUM', count=100)
G
(
models
.
CourseActivityWeekly
,
course_id
=
course_id
,
interval_start
=
interval_start
,
interval_end
=
interval_end
,
activity_type
=
'ATTEMPTED_PROBLEM'
,
count
=
200
)
G
(
models
.
CourseActivityWeekly
,
course_id
=
self
.
course_id
,
interval_start
=
interval_start
,
G
(
models
.
CourseActivityWeekly
,
course_id
=
course_id
,
interval_start
=
interval_start
,
interval_end
=
interval_end
,
activity_type
=
'ACTIVE'
,
count
=
300
)
G
(
models
.
CourseActivityWeekly
,
course_id
=
self
.
course_id
,
interval_start
=
interval_start
,
G
(
models
.
CourseActivityWeekly
,
course_id
=
course_id
,
interval_start
=
interval_start
,
interval_end
=
interval_end
,
activity_type
=
'PLAYED_VIDEO'
,
count
=
400
)
def
setUp
(
self
):
super
(
CourseActivityLastWeekTest
,
self
)
.
setUp
()
self
.
generate_data
()
def
test_activity
(
self
):
response
=
self
.
authenticated_get
(
'/api/v0/courses/{0}/recent_activity'
.
format
(
self
.
course_id
))
response
=
self
.
authenticated_get
(
u
'/api/v0/courses/{0}/recent_activity'
.
format
(
self
.
course_id
))
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
data
,
self
.
get_activity_record
())
def
assertValidActivityResponse
(
self
,
activity_type
,
count
):
response
=
self
.
authenticated_get
(
'/api/v0/courses/{0}/recent_activity?activity_type={1}'
.
format
(
response
=
self
.
authenticated_get
(
u
'/api/v0/courses/{0}/recent_activity?activity_type={1}'
.
format
(
self
.
course_id
,
activity_type
))
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
data
,
self
.
get_activity_record
(
activity_type
=
activity_type
,
count
=
count
))
...
...
@@ -162,7 +201,7 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
@staticmethod
def
get_activity_record
(
**
kwargs
):
default
=
{
'course_id'
:
'edX/DemoX/Demo_Course'
,
'course_id'
:
DEMO_COURSE_ID
,
'interval_start'
:
datetime
.
datetime
(
2014
,
1
,
1
,
0
,
0
,
tzinfo
=
pytz
.
utc
),
'interval_end'
:
datetime
.
datetime
(
2014
,
1
,
8
,
0
,
0
,
tzinfo
=
pytz
.
utc
),
'activity_type'
:
'any'
,
...
...
@@ -173,11 +212,12 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
return
default
def
test_activity_auth
(
self
):
response
=
self
.
client
.
get
(
'/api/v0/courses/{0}/recent_activity'
.
format
(
self
.
course_id
),
follow
=
True
)
response
=
self
.
client
.
get
(
u
'/api/v0/courses/{0}/recent_activity'
.
format
(
self
.
course_id
),
follow
=
True
)
self
.
assertEquals
(
response
.
status_code
,
401
)
def
test_url_encoded_course_id
(
self
):
response
=
self
.
authenticated_get
(
'/api/v0/courses/edX
%2
FDemoX
%2
FDemo_Course/recent_activity'
)
url_encoded_course_id
=
urllib
.
quote_plus
(
self
.
course_id
)
response
=
self
.
authenticated_get
(
u'/api/v0/courses/{}/recent_activity'
.
format
(
url_encoded_course_id
))
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
data
,
self
.
get_activity_record
())
...
...
@@ -190,21 +230,21 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
def
test_unknown_activity
(
self
):
activity_type
=
'missing_activity_type'
response
=
self
.
authenticated_get
(
'/api/v0/courses/{0}/recent_activity?activity_type={1}'
.
format
(
response
=
self
.
authenticated_get
(
u
'/api/v0/courses/{0}/recent_activity?activity_type={1}'
.
format
(
self
.
course_id
,
activity_type
))
self
.
assertEquals
(
response
.
status_code
,
404
)
def
test_unknown_course_id
(
self
):
response
=
self
.
authenticated_get
(
'/api/v0/courses/{0}/recent_activity'
.
format
(
'foo'
))
response
=
self
.
authenticated_get
(
u
'/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'
)
response
=
self
.
authenticated_get
(
u
'/api/v0/courses/recent_activity'
)
self
.
assertEquals
(
response
.
status_code
,
404
)
def
test_label_parameter
(
self
):
activity_type
=
'played_video'
response
=
self
.
authenticated_get
(
'/api/v0/courses/{0}/recent_activity?label={1}'
.
format
(
response
=
self
.
authenticated_get
(
u
'/api/v0/courses/{0}/recent_activity?label={1}'
.
format
(
self
.
course_id
,
activity_type
))
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
data
,
self
.
get_activity_record
(
activity_type
=
activity_type
,
count
=
400
))
...
...
@@ -214,17 +254,22 @@ class CourseEnrollmentByBirthYearViewTests(CourseEnrollmentViewTestCaseMixin, Te
path
=
'/enrollment/birth_year'
model
=
models
.
CourseEnrollmentByBirthYear
order_by
=
[
'birth_year'
]
csv_filename_slug
=
u'enrollment-age'
def
generate_data
(
self
,
course_id
=
None
):
course_id
=
course_id
or
self
.
course_id
G
(
self
.
model
,
course_id
=
course_id
,
date
=
self
.
date
,
birth_year
=
1956
)
G
(
self
.
model
,
course_id
=
course_id
,
date
=
self
.
date
,
birth_year
=
1986
)
G
(
self
.
model
,
course_id
=
course_id
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
10
),
birth_year
=
1956
)
G
(
self
.
model
,
course_id
=
course_id
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
10
),
birth_year
=
1986
)
def
setUp
(
self
):
super
(
CourseEnrollmentByBirthYearViewTests
,
self
)
.
setUp
()
G
(
self
.
model
,
course_id
=
self
.
course_id
,
date
=
self
.
date
,
birth_year
=
1956
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
date
=
self
.
date
,
birth_year
=
1986
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
10
),
birth_year
=
1956
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
10
),
birth_year
=
1986
)
self
.
generate_data
()
def
format_as_response
(
self
,
*
args
):
return
[
{
'course_id'
:
str
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
{
'course_id'
:
unicode
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
'birth_year'
:
ce
.
birth_year
,
'created'
:
ce
.
created
.
strftime
(
settings
.
DATETIME_FORMAT
)}
for
ce
in
args
]
def
test_get
(
self
):
...
...
@@ -239,19 +284,23 @@ class CourseEnrollmentByEducationViewTests(CourseEnrollmentViewTestCaseMixin, Te
path
=
'/enrollment/education/'
model
=
models
.
CourseEnrollmentByEducation
order_by
=
[
'education_level'
]
csv_filename_slug
=
u'enrollment-education'
def
generate_data
(
self
,
course_id
=
None
):
course_id
=
course_id
or
self
.
course_id
G
(
self
.
model
,
course_id
=
course_id
,
date
=
self
.
date
,
education_level
=
self
.
el1
)
G
(
self
.
model
,
course_id
=
course_id
,
date
=
self
.
date
,
education_level
=
self
.
el2
)
G
(
self
.
model
,
course_id
=
course_id
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
2
),
education_level
=
self
.
el2
)
def
setUp
(
self
):
super
(
CourseEnrollmentByEducationViewTests
,
self
)
.
setUp
()
self
.
el1
=
G
(
models
.
EducationLevel
,
name
=
'Doctorate'
,
short_name
=
'doctorate'
)
self
.
el2
=
G
(
models
.
EducationLevel
,
name
=
'Top Secret'
,
short_name
=
'top_secret'
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
date
=
self
.
date
,
education_level
=
self
.
el1
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
date
=
self
.
date
,
education_level
=
self
.
el2
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
2
),
education_level
=
self
.
el2
)
self
.
generate_data
()
def
format_as_response
(
self
,
*
args
):
return
[
{
'course_id'
:
str
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
{
'course_id'
:
unicode
(
ce
.
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
},
'created'
:
ce
.
created
.
strftime
(
settings
.
DATETIME_FORMAT
)}
for
ce
in
args
]
...
...
@@ -261,16 +310,21 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, TestC
path
=
'/enrollment/gender/'
model
=
models
.
CourseEnrollmentByGender
order_by
=
[
'gender'
]
csv_filename_slug
=
u'enrollment-gender'
def
generate_data
(
self
,
course_id
=
None
):
course_id
=
course_id
or
self
.
course_id
G
(
self
.
model
,
course_id
=
course_id
,
gender
=
'm'
,
date
=
self
.
date
,
count
=
34
)
G
(
self
.
model
,
course_id
=
course_id
,
gender
=
'f'
,
date
=
self
.
date
,
count
=
45
)
G
(
self
.
model
,
course_id
=
course_id
,
gender
=
'f'
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
2
),
count
=
45
)
def
setUp
(
self
):
super
(
CourseEnrollmentByGenderViewTests
,
self
)
.
setUp
()
G
(
self
.
model
,
course_id
=
self
.
course_id
,
gender
=
'm'
,
date
=
self
.
date
,
count
=
34
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
gender
=
'f'
,
date
=
self
.
date
,
count
=
45
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
gender
=
'f'
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
2
),
count
=
45
)
self
.
generate_data
()
def
format_as_response
(
self
,
*
args
):
return
[
{
'course_id'
:
str
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
{
'course_id'
:
unicode
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
'gender'
:
ce
.
gender
,
'created'
:
ce
.
created
.
strftime
(
settings
.
DATETIME_FORMAT
)}
for
ce
in
args
]
...
...
@@ -308,15 +362,20 @@ class AnswerDistributionTests(TestCaseWithAuthentication):
class
CourseEnrollmentViewTests
(
CourseEnrollmentViewTestCaseMixin
,
TestCaseWithAuthentication
):
model
=
models
.
CourseEnrollmentDaily
path
=
'/enrollment'
csv_filename_slug
=
u'enrollment'
def
generate_data
(
self
,
course_id
=
None
):
course_id
=
course_id
or
self
.
course_id
G
(
self
.
model
,
course_id
=
course_id
,
date
=
self
.
date
,
count
=
203
)
G
(
self
.
model
,
course_id
=
course_id
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
5
),
count
=
203
)
def
setUp
(
self
):
super
(
CourseEnrollmentViewTests
,
self
)
.
setUp
()
G
(
self
.
model
,
course_id
=
self
.
course_id
,
date
=
self
.
date
,
count
=
203
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
5
),
count
=
203
)
self
.
generate_data
()
def
format_as_response
(
self
,
*
args
):
return
[
{
'course_id'
:
str
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
{
'course_id'
:
unicode
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
'created'
:
ce
.
created
.
strftime
(
settings
.
DATETIME_FORMAT
)}
for
ce
in
args
]
...
...
@@ -324,6 +383,7 @@ class CourseEnrollmentViewTests(CourseEnrollmentViewTestCaseMixin, TestCaseWithA
class
CourseEnrollmentByLocationViewTests
(
CourseEnrollmentViewTestCaseMixin
,
TestCaseWithAuthentication
):
path
=
'/enrollment/location/'
model
=
models
.
CourseEnrollmentByCountry
csv_filename_slug
=
u'enrollment-location'
def
format_as_response
(
self
,
*
args
):
unknown
=
{
'course_id'
:
None
,
'count'
:
0
,
'date'
:
None
,
...
...
@@ -341,26 +401,29 @@ class CourseEnrollmentByLocationViewTests(CourseEnrollmentViewTestCaseMixin, Tes
response
=
[
unknown
]
response
+=
[
{
'course_id'
:
str
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
{
'course_id'
:
unicode
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
'country'
:
{
'alpha2'
:
ce
.
country
.
alpha2
,
'alpha3'
:
ce
.
country
.
alpha3
,
'name'
:
ce
.
country
.
name
},
'created'
:
ce
.
created
.
strftime
(
settings
.
DATETIME_FORMAT
)}
for
ce
in
args
]
return
response
def
generate_data
(
self
,
course_id
=
None
):
course_id
=
course_id
or
self
.
course_id
G
(
self
.
model
,
course_id
=
course_id
,
country_code
=
'US'
,
count
=
455
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
course_id
,
country_code
=
'CA'
,
count
=
356
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
course_id
,
country_code
=
'IN'
,
count
=
12
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
29
))
G
(
self
.
model
,
course_id
=
course_id
,
country_code
=
''
,
count
=
356
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
course_id
,
country_code
=
'A1'
,
count
=
1
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
course_id
,
country_code
=
'A2'
,
count
=
2
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
course_id
,
country_code
=
'AP'
,
count
=
1
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
course_id
,
country_code
=
'EU'
,
count
=
4
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
course_id
,
country_code
=
'O1'
,
count
=
7
,
date
=
self
.
date
)
def
setUp
(
self
):
super
(
CourseEnrollmentByLocationViewTests
,
self
)
.
setUp
()
self
.
country
=
countries
.
get
(
'US'
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
country_code
=
'US'
,
count
=
455
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
country_code
=
'CA'
,
count
=
356
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
country_code
=
'IN'
,
count
=
12
,
date
=
self
.
date
-
datetime
.
timedelta
(
days
=
29
))
G
(
self
.
model
,
course_id
=
self
.
course_id
,
country_code
=
''
,
count
=
356
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
country_code
=
'A1'
,
count
=
1
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
country_code
=
'A2'
,
count
=
2
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
country_code
=
'AP'
,
count
=
1
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
country_code
=
'EU'
,
count
=
4
,
date
=
self
.
date
)
G
(
self
.
model
,
course_id
=
self
.
course_id
,
country_code
=
'O1'
,
count
=
7
,
date
=
self
.
date
)
self
.
generate_data
()
class
CourseActivityWeeklyViewTests
(
CourseViewTestCaseMixin
,
TestCaseWithAuthentication
):
...
...
@@ -369,23 +432,29 @@ class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthent
model
=
CourseActivityWeekly
# activity_types = ['ACTIVE', 'ATTEMPTED_PROBLEM', 'PLAYED_VIDEO', 'POSTED_FORUM']
activity_types
=
[
'ACTIVE'
,
'ATTEMPTED_PROBLEM'
,
'PLAYED_VIDEO'
]
csv_filename_slug
=
u'engagement-activity'
def
setUp
(
self
):
super
(
CourseActivityWeeklyViewTests
,
self
)
.
setUp
()
self
.
course_id
=
'edX/DemoX/Demo_Course'
self
.
interval_start
=
datetime
.
datetime
(
2014
,
1
,
1
,
tzinfo
=
pytz
.
utc
)
self
.
interval_end
=
self
.
interval_start
+
datetime
.
timedelta
(
weeks
=
1
)
def
generate_data
(
self
,
course_id
=
None
):
course_id
=
course_id
or
self
.
course_id
for
activity_type
in
self
.
activity_types
:
G
(
CourseActivityWeekly
,
course_id
=
self
.
course_id
,
course_id
=
course_id
,
interval_start
=
self
.
interval_start
,
interval_end
=
self
.
interval_end
,
activity_type
=
activity_type
,
count
=
100
)
def
get_latest_data
(
self
):
return
self
.
model
.
objects
.
filter
(
course_id
=
self
.
course_id
,
interval_end
=
self
.
interval_end
)
def
setUp
(
self
):
super
(
CourseActivityWeeklyViewTests
,
self
)
.
setUp
()
self
.
interval_start
=
datetime
.
datetime
(
2014
,
1
,
1
,
tzinfo
=
pytz
.
utc
)
self
.
interval_end
=
self
.
interval_start
+
datetime
.
timedelta
(
weeks
=
1
)
self
.
generate_data
()
def
get_latest_data
(
self
,
course_id
=
None
):
course_id
=
course_id
or
self
.
course_id
return
self
.
model
.
objects
.
filter
(
course_id
=
course_id
,
interval_end
=
self
.
interval_end
)
def
format_as_response
(
self
,
*
args
):
response
=
[]
...
...
analytics_data_api/v0/urls/courses.py
View file @
646ad4a1
import
re
from
django.conf.urls
import
patterns
,
url
from
analytics_data_api.v0.views
import
courses
as
views
COURSE_ID_PATTERN
=
r'(?P<course_id>[^/+]+[/+][^/+]+[/+][^/]+)'
COURSE_URLS
=
[
(
'activity'
,
views
.
CourseActivityWeeklyView
,
'activity'
),
(
'recent_activity'
,
views
.
CourseActivityMostRecentWeekView
,
'recent_activity'
),
...
...
@@ -18,4 +16,5 @@ COURSE_URLS = [
urlpatterns
=
[]
for
path
,
view
,
name
in
COURSE_URLS
:
urlpatterns
+=
patterns
(
''
,
url
(
r'^(?P<course_id>.+)/'
+
re
.
escape
(
path
)
+
r'/$'
,
view
.
as_view
(),
name
=
name
))
regex
=
r'^{0}/{1}/$'
.
format
(
COURSE_ID_PATTERN
,
path
)
urlpatterns
+=
patterns
(
''
,
url
(
regex
,
view
.
as_view
(),
name
=
name
))
analytics_data_api/v0/views/courses.py
View file @
646ad4a1
...
...
@@ -8,6 +8,7 @@ from django.db.models import Max
from
django.http
import
Http404
from
django.utils.timezone
import
make_aware
,
utc
from
rest_framework
import
generics
from
opaque_keys.edx.keys
import
CourseKey
from
analytics_data_api.v0
import
models
,
serializers
...
...
@@ -15,8 +16,11 @@ from analytics_data_api.v0 import models, serializers
class
BaseCourseView
(
generics
.
ListAPIView
):
start_date
=
None
end_date
=
None
course_id
=
None
slug
=
None
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
self
.
course_id
=
self
.
kwargs
.
get
(
'course_id'
)
start_date
=
request
.
QUERY_PARAMS
.
get
(
'start_date'
)
end_date
=
request
.
QUERY_PARAMS
.
get
(
'end_date'
)
timezone
=
utc
...
...
@@ -44,12 +48,21 @@ class BaseCourseView(generics.ListAPIView):
raise
NotImplementedError
def
get_queryset
(
self
):
course_id
=
self
.
kwargs
.
get
(
'course_id'
)
self
.
verify_course_exists_or_404
(
course_id
)
queryset
=
self
.
model
.
objects
.
filter
(
course_id
=
course_id
)
self
.
verify_course_exists_or_404
(
self
.
course_id
)
queryset
=
self
.
model
.
objects
.
filter
(
course_id
=
self
.
course_id
)
queryset
=
self
.
apply_date_filtering
(
queryset
)
return
queryset
def
get_csv_filename
(
self
):
course_key
=
CourseKey
.
from_string
(
self
.
course_id
)
course_id
=
u'-'
.
join
([
course_key
.
org
,
course_key
.
course
,
course_key
.
run
])
return
u'{0}--{1}.csv'
.
format
(
course_id
,
self
.
slug
)
def
finalize_response
(
self
,
request
,
response
,
*
args
,
**
kwargs
):
if
request
.
META
.
get
(
'HTTP_ACCEPT'
)
==
u'text/csv'
:
response
[
'Content-Disposition'
]
=
u'attachment; filename={}'
.
format
(
self
.
get_csv_filename
())
return
super
(
BaseCourseView
,
self
)
.
finalize_response
(
request
,
response
,
*
args
,
**
kwargs
)
# pylint: disable=line-too-long
class
CourseActivityWeeklyView
(
BaseCourseView
):
...
...
@@ -80,6 +93,7 @@ class CourseActivityWeeklyView(BaseCourseView):
end_date -- Date before which all data should be returned (exclusive)
"""
slug
=
u'engagement-activity'
model
=
models
.
CourseActivityWeekly
serializer_class
=
serializers
.
CourseActivityWeeklySerializer
...
...
@@ -244,6 +258,7 @@ class CourseEnrollmentByBirthYearView(BaseCourseEnrollmentView):
end_date -- Date before which all data should be returned (exclusive)
"""
slug
=
u'enrollment-age'
serializer_class
=
serializers
.
CourseEnrollmentByBirthYearSerializer
model
=
models
.
CourseEnrollmentByBirthYear
...
...
@@ -263,6 +278,7 @@ class CourseEnrollmentByEducationView(BaseCourseEnrollmentView):
start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive)
"""
slug
=
u'enrollment-education'
serializer_class
=
serializers
.
CourseEnrollmentByEducationSerializer
model
=
models
.
CourseEnrollmentByEducation
...
...
@@ -287,6 +303,7 @@ class CourseEnrollmentByGenderView(BaseCourseEnrollmentView):
start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive)
"""
slug
=
u'enrollment-gender'
serializer_class
=
serializers
.
CourseEnrollmentByGenderSerializer
model
=
models
.
CourseEnrollmentByGender
...
...
@@ -304,7 +321,7 @@ class CourseEnrollmentView(BaseCourseEnrollmentView):
start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive)
"""
slug
=
u'enrollment'
serializer_class
=
serializers
.
CourseEnrollmentDailySerializer
model
=
models
.
CourseEnrollmentDaily
...
...
@@ -329,7 +346,7 @@ class CourseEnrollmentByLocationView(BaseCourseEnrollmentView):
start_date -- Date after which all data should be returned (inclusive)
end_date -- Date before which all data should be returned (exclusive)
"""
slug
=
u'enrollment-location'
serializer_class
=
serializers
.
CourseEnrollmentByCountrySerializer
model
=
models
.
CourseEnrollmentByCountry
...
...
requirements/base.txt
View file @
646ad4a1
...
...
@@ -7,3 +7,4 @@ ipython==2.1.0 # BSD
django-rest-swagger==0.1.14 # BSD
djangorestframework-csv==1.3.3 # BSD
iso3166==0.1 # MIT
-e git+https://github.com/edx/opaque-keys.git@d45d0bd8d64c69531be69178b9505b5d38806ce0#egg=opaque-keys
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