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
25ef8334
Commit
25ef8334
authored
Sep 03, 2014
by
Clinton Blackburn
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #21 from edx/activity-update
Added Activity Resource
parents
0674b790
0c6b3341
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
404 additions
and
180 deletions
+404
-180
Makefile
+1
-1
analytics_data_api/management/commands/generate_fake_course_data.py
+3
-3
analytics_data_api/v0/models.py
+4
-2
analytics_data_api/v0/serializers.py
+16
-1
analytics_data_api/v0/tests/test_views.py
+234
-145
analytics_data_api/v0/urls/courses.py
+1
-0
analytics_data_api/v0/views/courses.py
+140
-25
analyticsdataserver/settings/base.py
+4
-3
requirements/test.txt
+1
-0
No files found.
Makefile
View file @
25ef8334
...
...
@@ -20,7 +20,7 @@ clean:
coverage erase
test
:
clean
.
./.test_env
&&
./manage.py
test
--settings
=
analyticsdataserver.settings.test
\
.
./.test_env
&&
./manage.py
test
--settings
=
analyticsdataserver.settings.test
--with-ignore-docstrings
\
--exclude-dir
=
analyticsdataserver/settings
--with-coverage
--cover-inclusive
--cover-branches
\
--cover-html
--cover-html-dir
=
$(COVERAGE)
/html/
\
--cover-xml
--cover-xml-file
=
$(COVERAGE)
/coverage.xml
\
...
...
analytics_data_api/management/commands/generate_fake_course_data.py
View file @
25ef8334
...
...
@@ -109,7 +109,7 @@ class Command(BaseCommand):
activity_types
=
[
'PLAYED_VIDEO'
,
'ATTEMPTED_PROBLEM'
,
'POSTED_FORUM'
]
start
=
start_date
models
.
CourseActivity
ByWeek
.
objects
.
all
()
.
delete
()
models
.
CourseActivity
Weekly
.
objects
.
all
()
.
delete
()
logger
.
info
(
"Deleted all weekly course activity."
)
logger
.
info
(
"Generating new weekly course activity data..."
)
...
...
@@ -121,10 +121,10 @@ class Command(BaseCommand):
counts
=
constrained_sum_sample_pos
(
len
(
activity_types
),
active_students
)
for
activity_type
,
count
in
zip
(
activity_types
,
counts
):
models
.
CourseActivity
ByWeek
.
objects
.
create
(
course_id
=
course_id
,
activity_type
=
activity_type
,
models
.
CourseActivity
Weekly
.
objects
.
create
(
course_id
=
course_id
,
activity_type
=
activity_type
,
count
=
count
,
interval_start
=
start
,
interval_end
=
end
)
models
.
CourseActivity
ByWeek
.
objects
.
create
(
course_id
=
course_id
,
activity_type
=
'ACTIVE'
,
count
=
active_students
,
models
.
CourseActivity
Weekly
.
objects
.
create
(
course_id
=
course_id
,
activity_type
=
'ACTIVE'
,
count
=
active_students
,
interval_start
=
start
,
interval_end
=
end
)
start
=
end
...
...
analytics_data_api/v0/models.py
View file @
25ef8334
...
...
@@ -2,16 +2,18 @@ from django.db import models
from
iso3166
import
countries
class
CourseActivity
ByWeek
(
models
.
Model
):
class
CourseActivity
Weekly
(
models
.
Model
):
"""A count of unique users who performed a particular action during a week."""
class
Meta
(
object
):
db_table
=
'course_activity'
index_together
=
[[
'course_id'
,
'activity_type'
]]
ordering
=
(
'interval_end'
,
'interval_start'
,
'course_id'
)
get_latest_by
=
'interval_end'
course_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
interval_start
=
models
.
DateTimeField
()
interval_end
=
models
.
DateTimeField
()
interval_end
=
models
.
DateTimeField
(
db_index
=
True
)
activity_type
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
,
db_column
=
'label'
)
count
=
models
.
IntegerField
()
...
...
analytics_data_api/v0/serializers.py
View file @
25ef8334
from
django.conf
import
settings
from
rest_framework
import
serializers
from
analytics_data_api.v0
import
models
from
analytics_data_api.v0.models
import
CourseActivityWeekly
class
CourseActivityByWeekSerializer
(
serializers
.
ModelSerializer
):
...
...
@@ -25,7 +26,7 @@ class CourseActivityByWeekSerializer(serializers.ModelSerializer):
return
activity_type
class
Meta
(
object
):
model
=
models
.
CourseActivity
ByWeek
model
=
models
.
CourseActivity
Weekly
fields
=
(
'interval_start'
,
'interval_end'
,
'activity_type'
,
'count'
,
'course_id'
)
...
...
@@ -112,3 +113,17 @@ class CourseEnrollmentByBirthYearSerializer(BaseCourseEnrollmentModelSerializer)
class
Meta
(
object
):
model
=
models
.
CourseEnrollmentByBirthYear
fields
=
(
'course_id'
,
'date'
,
'birth_year'
,
'count'
)
class
CourseActivityWeeklySerializer
(
serializers
.
ModelSerializer
):
interval_start
=
serializers
.
DateTimeField
(
format
=
settings
.
DATETIME_FORMAT
)
interval_end
=
serializers
.
DateTimeField
(
format
=
settings
.
DATETIME_FORMAT
)
any
=
serializers
.
IntegerField
(
required
=
False
)
attempted_problem
=
serializers
.
IntegerField
(
required
=
False
)
played_video
=
serializers
.
IntegerField
(
required
=
False
)
posted_forum
=
serializers
.
IntegerField
(
required
=
False
)
class
Meta
(
object
):
model
=
CourseActivityWeekly
fields
=
(
'interval_start'
,
'interval_end'
,
'course_id'
,
'any'
,
'attempted_problem'
,
'played_video'
,
'posted_forum'
)
analytics_data_api/v0/tests/test_views.py
View file @
25ef8334
...
...
@@ -4,6 +4,7 @@
import
StringIO
import
csv
import
datetime
from
itertools
import
groupby
from
django.conf
import
settings
from
django_dynamic_fixture
import
G
...
...
@@ -11,28 +12,138 @@ from iso3166 import countries
import
pytz
from
analytics_data_api.v0
import
models
from
analytics_data_api.v0.models
import
CourseActivityWeekly
from
analytics_data_api.v0.serializers
import
ProblemResponseAnswerDistributionSerializer
from
analytics_data_api.v0.tests.utils
import
flatten
from
analyticsdataserver.tests
import
TestCaseWithAuthentication
# pylint: disable=no-member
class
CourseViewTestCaseMixin
(
object
):
model
=
None
api_root_path
=
'/api/v0/'
path
=
None
order_by
=
[]
def
format_as_response
(
self
,
*
args
):
"""
Format given data as a response that would be issued by the endpoint.
Arguments
args -- Iterable list of objects
"""
raise
NotImplementedError
def
get_latest_data
(
self
):
"""
Return the latest row/rows that would be returned if a user made a call
to the endpoint with no date filtering.
Return value must be an iterable.
"""
raise
NotImplementedError
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
))
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
))
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
)
csv_content_type
=
'text/csv'
response
=
self
.
authenticated_get
(
path
,
HTTP_ACCEPT
=
csv_content_type
)
# Validate the basic response status and content code
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
[
'Content-Type'
]
.
split
(
';'
)[
0
],
csv_content_type
)
# Validate the actual data
data
=
self
.
format_as_response
(
*
self
.
get_latest_data
())
data
=
map
(
flatten
,
data
)
# The CSV renderer sorts the headers alphabetically
fieldnames
=
sorted
(
data
[
0
]
.
keys
())
# Generate the expected CSV output
expected
=
StringIO
.
StringIO
()
writer
=
csv
.
DictWriter
(
expected
,
fieldnames
)
writer
.
writeheader
()
writer
.
writerows
(
data
)
self
.
assertEqual
(
response
.
content
,
expected
.
getvalue
())
def
test_get_with_intervals
(
self
):
""" Verify the endpoint returns multiple data points when supplied with an interval of dates. """
raise
NotImplementedError
def
assertIntervalFilteringWorks
(
self
,
expected_response
,
start_date
,
end_date
):
# 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
(
'
%
scourses/
%
s
%
s?start_date=
%
s'
%
(
self
.
api_root_path
,
self
.
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
(
'
%
scourses/
%
s
%
s?end_date=
%
s'
%
(
self
.
api_root_path
,
self
.
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
(
'
%
scourses/
%
s
%
s?start_date=
%
s&end_date=
%
s'
%
(
self
.
api_root_path
,
self
.
course_id
,
self
.
path
,
start_date
,
end_date
))
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertListEqual
(
response
.
data
,
expected_response
)
# pylint: disable=abstract-method
class
CourseEnrollmentViewTestCaseMixin
(
CourseViewTestCaseMixin
):
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
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'
interval_start
=
'2014-05-24T00:00:00Z'
interval_end
=
'2014-06-01T00:00:00Z'
G
(
models
.
CourseActivity
ByWeek
,
course_id
=
self
.
course_id
,
interval_start
=
interval_start
,
interval_start
=
datetime
.
datetime
(
2014
,
1
,
1
,
tzinfo
=
pytz
.
utc
)
interval_end
=
interval_start
+
datetime
.
timedelta
(
weeks
=
1
)
G
(
models
.
CourseActivity
Weekly
,
course_id
=
self
.
course_id
,
interval_start
=
interval_start
,
interval_end
=
interval_end
,
activity_type
=
'POSTED_FORUM'
,
count
=
100
)
G
(
models
.
CourseActivity
ByWeek
,
course_id
=
self
.
course_id
,
interval_start
=
interval_start
,
G
(
models
.
CourseActivity
Weekly
,
course_id
=
self
.
course_id
,
interval_start
=
interval_start
,
interval_end
=
interval_end
,
activity_type
=
'ATTEMPTED_PROBLEM'
,
count
=
200
)
G
(
models
.
CourseActivity
ByWeek
,
course_id
=
self
.
course_id
,
interval_start
=
interval_start
,
G
(
models
.
CourseActivity
Weekly
,
course_id
=
self
.
course_id
,
interval_start
=
interval_start
,
interval_end
=
interval_end
,
activity_type
=
'ACTIVE'
,
count
=
300
)
G
(
models
.
CourseActivity
ByWeek
,
course_id
=
self
.
course_id
,
interval_start
=
interval_start
,
G
(
models
.
CourseActivity
Weekly
,
course_id
=
self
.
course_id
,
interval_start
=
interval_start
,
interval_end
=
interval_end
,
activity_type
=
'PLAYED_VIDEO'
,
count
=
400
)
...
...
@@ -51,8 +162,8 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
def
get_activity_record
(
**
kwargs
):
default
=
{
'course_id'
:
'edX/DemoX/Demo_Course'
,
'interval_start'
:
datetime
.
datetime
(
2014
,
5
,
24
,
0
,
0
,
tzinfo
=
pytz
.
utc
),
'interval_end'
:
datetime
.
datetime
(
2014
,
6
,
1
,
0
,
0
,
tzinfo
=
pytz
.
utc
),
'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'
,
'count'
:
300
,
}
...
...
@@ -98,96 +209,19 @@ class CourseActivityLastWeekTest(TestCaseWithAuthentication):
self
.
assertEquals
(
response
.
data
,
self
.
get_activity_record
(
activity_type
=
activity_type
,
count
=
400
))
# pylint: disable=no-member
class
CourseEnrollmentViewTestCase
(
object
):
model
=
None
path
=
None
order_by
=
[]
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
=
'edX/DemoX/Non_Existent_Course'
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s'
%
(
course_id
,
self
.
path
))
self
.
assertEquals
(
response
.
status_code
,
404
)
def
test_get
(
self
):
# Validate the basic response status
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s'
%
(
self
.
course_id
,
self
.
path
,))
self
.
assertEquals
(
response
.
status_code
,
200
)
# Validate the data is correct and sorted chronologically
expected
=
self
.
get_expected_response
(
*
self
.
model
.
objects
.
filter
(
date
=
self
.
date
)
.
order_by
(
'date'
,
*
self
.
order_by
))
# pylint: disable=line-too-long
self
.
assertEquals
(
response
.
data
,
expected
)
def
test_get_csv
(
self
):
path
=
'/api/v0/courses/
%
s
%
s'
%
(
self
.
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 code
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
[
'Content-Type'
]
.
split
(
';'
)[
0
],
csv_content_type
)
# Validate the actual data
data
=
self
.
get_expected_response
(
*
self
.
model
.
objects
.
filter
(
date
=
self
.
date
))
data
=
map
(
flatten
,
data
)
# The CSV renderer sorts the headers alphabetically
fieldnames
=
sorted
(
data
[
0
]
.
keys
())
# Generate the expected CSV output
expected
=
StringIO
.
StringIO
()
writer
=
csv
.
DictWriter
(
expected
,
fieldnames
)
writer
.
writeheader
()
writer
.
writerows
(
data
)
self
.
assertEqual
(
response
.
content
,
expected
.
getvalue
())
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
):
# 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'
%
(
self
.
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'
%
(
self
.
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'
%
(
self
.
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
(
CourseEnrollmentViewTestCaseMixin
,
TestCaseWithAuthentication
):
path
=
'/enrollment/birth_year'
model
=
models
.
CourseEnrollmentByBirthYear
order_by
=
[
'birth_year'
]
@classmethod
def
setUpClass
(
cls
):
cls
.
course_id
=
'edX/DemoX/Demo_Course'
cls
.
date
=
datetime
.
date
(
2014
,
1
,
1
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
date
=
cls
.
date
,
birth_year
=
1956
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
date
=
cls
.
date
,
birth_year
=
1986
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
date
=
cls
.
date
-
datetime
.
timedelta
(
days
=
10
),
birth_year
=
1956
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
date
=
cls
.
date
-
datetime
.
timedelta
(
days
=
10
),
birth_year
=
1986
)
def
get_expected_response
(
self
,
*
args
):
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
)
def
format_as_response
(
self
,
*
args
):
return
[
{
'course_id'
:
str
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
'birth_year'
:
ce
.
birth_year
}
for
ce
in
args
]
...
...
@@ -196,51 +230,43 @@ class CourseEnrollmentByBirthYearViewTests(TestCaseWithAuthentication, CourseEnr
response
=
self
.
authenticated_get
(
'/api/v0/courses/
%
s
%
s'
%
(
self
.
course_id
,
self
.
path
,))
self
.
assertEquals
(
response
.
status_code
,
200
)
expected
=
self
.
get_expected
_response
(
*
self
.
model
.
objects
.
filter
(
date
=
self
.
date
))
expected
=
self
.
format_as
_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
))
class
CourseEnrollmentByEducationViewTests
(
TestCaseWithAuthentication
,
CourseEnrollmentViewTestCase
):
class
CourseEnrollmentByEducationViewTests
(
CourseEnrollmentViewTestCaseMixin
,
TestCaseWithAuthentication
):
path
=
'/enrollment/education/'
model
=
models
.
CourseEnrollmentByEducation
order_by
=
[
'education_level'
]
@classmethod
def
setUpClass
(
cls
):
cls
.
el1
=
G
(
models
.
EducationLevel
,
name
=
'Doctorate'
,
short_name
=
'doctorate'
)
cls
.
el2
=
G
(
models
.
EducationLevel
,
name
=
'Top Secret'
,
short_name
=
'top_secret'
)
cls
.
course_id
=
'edX/DemoX/Demo_Course'
cls
.
date
=
datetime
.
date
(
2014
,
1
,
1
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
date
=
cls
.
date
,
education_level
=
cls
.
el1
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
date
=
cls
.
date
,
education_level
=
cls
.
el2
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
date
=
cls
.
date
-
datetime
.
timedelta
(
days
=
2
),
education_level
=
cls
.
el2
)
def
get_expected_response
(
self
,
*
args
):
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
)
def
format_as_response
(
self
,
*
args
):
return
[
{
'course_id'
:
str
(
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
}}
for
ce
in
args
]
class
CourseEnrollmentByGenderViewTests
(
TestCaseWithAuthentication
,
CourseEnrollmentViewTestCase
):
class
CourseEnrollmentByGenderViewTests
(
CourseEnrollmentViewTestCaseMixin
,
TestCaseWithAuthentication
):
path
=
'/enrollment/gender/'
model
=
models
.
CourseEnrollmentByGender
order_by
=
[
'gender'
]
@classmethod
def
setUpClass
(
cls
):
cls
.
course_id
=
'edX/DemoX/Demo_Course'
cls
.
date
=
datetime
.
date
(
2014
,
1
,
1
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
gender
=
'm'
,
date
=
cls
.
date
,
count
=
34
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
gender
=
'f'
,
date
=
cls
.
date
,
count
=
45
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
gender
=
'f'
,
date
=
cls
.
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
)
def
get_expected
_response
(
self
,
*
args
):
def
format_as
_response
(
self
,
*
args
):
return
[
{
'course_id'
:
str
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
),
'gender'
:
ce
.
gender
}
for
ce
in
args
]
...
...
@@ -277,28 +303,26 @@ class AnswerDistributionTests(TestCaseWithAuthentication):
self
.
assertEquals
(
response
.
status_code
,
404
)
class
CourseEnrollmentViewTests
(
TestCaseWithAuthentication
,
CourseEnrollmentViewTestCase
):
class
CourseEnrollmentViewTests
(
CourseEnrollmentViewTestCaseMixin
,
TestCaseWithAuthentication
):
model
=
models
.
CourseEnrollmentDaily
path
=
'/enrollment'
@classmethod
def
setUpClass
(
cls
):
cls
.
course_id
=
'edX/DemoX/Demo_Course'
cls
.
date
=
datetime
.
date
(
2014
,
1
,
1
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
date
=
cls
.
date
,
count
=
203
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
date
=
cls
.
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
)
def
get_expected
_response
(
self
,
*
args
):
def
format_as
_response
(
self
,
*
args
):
return
[
{
'course_id'
:
str
(
ce
.
course_id
),
'count'
:
ce
.
count
,
'date'
:
ce
.
date
.
strftime
(
settings
.
DATE_FORMAT
)}
for
ce
in
args
]
class
CourseEnrollmentByLocationViewTests
(
TestCaseWithAuthentication
,
CourseEnrollmentViewTestCase
):
class
CourseEnrollmentByLocationViewTests
(
CourseEnrollmentViewTestCaseMixin
,
TestCaseWithAuthentication
):
path
=
'/enrollment/location/'
model
=
models
.
CourseEnrollmentByCountry
def
get_expected
_response
(
self
,
*
args
):
def
format_as
_response
(
self
,
*
args
):
args
=
[
arg
for
arg
in
args
if
arg
.
country_code
not
in
[
''
,
'A1'
,
'A2'
,
'AP'
,
'EU'
,
'O1'
,
'UNKNOWN'
]]
args
=
sorted
(
args
,
key
=
lambda
item
:
(
item
.
date
,
item
.
course_id
,
item
.
country
.
alpha3
))
return
[
...
...
@@ -306,17 +330,82 @@ class CourseEnrollmentByLocationViewTests(TestCaseWithAuthentication, CourseEnro
'country'
:
{
'alpha2'
:
ce
.
country
.
alpha2
,
'alpha3'
:
ce
.
country
.
alpha3
,
'name'
:
ce
.
country
.
name
}}
for
ce
in
args
]
@classmethod
def
setUpClass
(
cls
):
cls
.
course_id
=
'edX/DemoX/Demo_Course'
cls
.
date
=
datetime
.
date
(
2014
,
1
,
1
)
cls
.
country
=
countries
.
get
(
'US'
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
country_code
=
'US'
,
count
=
455
,
date
=
cls
.
date
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
country_code
=
'CA'
,
count
=
356
,
date
=
cls
.
date
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
country_code
=
'IN'
,
count
=
12
,
date
=
cls
.
date
-
datetime
.
timedelta
(
days
=
29
))
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
country_code
=
''
,
count
=
356
,
date
=
cls
.
date
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
country_code
=
'A1'
,
count
=
1
,
date
=
cls
.
date
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
country_code
=
'A2'
,
count
=
2
,
date
=
cls
.
date
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
country_code
=
'AP'
,
count
=
1
,
date
=
cls
.
date
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
country_code
=
'EU'
,
count
=
4
,
date
=
cls
.
date
)
G
(
cls
.
model
,
course_id
=
cls
.
course_id
,
country_code
=
'O1'
,
count
=
7
,
date
=
cls
.
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
)
class
CourseActivityWeeklyViewTests
(
CourseViewTestCaseMixin
,
TestCaseWithAuthentication
):
path
=
'/activity/'
default_order_by
=
'interval_end'
model
=
CourseActivityWeekly
activity_types
=
[
'ACTIVE'
,
'ATTEMPTED_PROBLEM'
,
'PLAYED_VIDEO'
,
'POSTED_FORUM'
]
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
)
for
activity_type
in
self
.
activity_types
:
G
(
CourseActivityWeekly
,
course_id
=
self
.
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
format_as_response
(
self
,
*
args
):
response
=
[]
# Group by date
for
_key
,
group
in
groupby
(
args
,
lambda
x
:
x
.
interval_end
):
# Iterate over groups and create a single item with all activity types
item
=
{}
for
activity
in
group
:
activity_type
=
activity
.
activity_type
.
lower
()
if
activity_type
==
'active'
:
activity_type
=
'any'
item
.
update
({
u'course_id'
:
activity
.
course_id
,
u'interval_start'
:
activity
.
interval_start
.
strftime
(
settings
.
DATETIME_FORMAT
),
u'interval_end'
:
activity
.
interval_end
.
strftime
(
settings
.
DATETIME_FORMAT
),
activity_type
:
activity
.
count
})
response
.
append
(
item
)
return
response
def
test_get_with_intervals
(
self
):
""" Verify the endpoint returns multiple data points when supplied with an interval of dates. """
# Create additional data
interval_start
=
self
.
interval_start
+
datetime
.
timedelta
(
weeks
=
1
)
interval_end
=
self
.
interval_end
+
datetime
.
timedelta
(
weeks
=
1
)
for
activity_type
in
self
.
activity_types
:
G
(
CourseActivityWeekly
,
course_id
=
self
.
course_id
,
interval_start
=
interval_start
,
interval_end
=
interval_end
,
activity_type
=
activity_type
,
count
=
200
)
expected
=
self
.
format_as_response
(
*
self
.
model
.
objects
.
all
())
self
.
assertEqual
(
len
(
expected
),
2
)
self
.
assertIntervalFilteringWorks
(
expected
,
self
.
interval_start
,
interval_end
+
datetime
.
timedelta
(
days
=
1
))
analytics_data_api/v0/urls/courses.py
View file @
25ef8334
...
...
@@ -6,6 +6,7 @@ from analytics_data_api.v0.views import courses as views
COURSE_URLS
=
[
(
'activity'
,
views
.
CourseActivityWeeklyView
,
'activity'
),
(
'recent_activity'
,
views
.
CourseActivityMostRecentWeekView
,
'recent_activity'
),
(
'enrollment'
,
views
.
CourseEnrollmentView
,
'enrollment_latest'
),
(
'enrollment/birth_year'
,
views
.
CourseEnrollmentByBirthYearView
,
'enrollment_by_birth_year'
),
...
...
analytics_data_api/v0/views/courses.py
View file @
25ef8334
import
datetime
from
itertools
import
groupby
import
warnings
from
django.conf
import
settings
from
django.core.exceptions
import
ObjectDoesNotExist
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
analytics_data_api.v0
import
models
,
serializers
class
BaseCourseView
(
generics
.
ListAPIView
):
start_date
=
None
end_date
=
None
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
start_date
=
request
.
QUERY_PARAMS
.
get
(
'start_date'
)
end_date
=
request
.
QUERY_PARAMS
.
get
(
'end_date'
)
timezone
=
utc
if
start_date
:
start_date
=
datetime
.
datetime
.
strptime
(
start_date
,
settings
.
DATE_FORMAT
)
start_date
=
make_aware
(
start_date
,
timezone
)
if
end_date
:
end_date
=
datetime
.
datetime
.
strptime
(
end_date
,
settings
.
DATE_FORMAT
)
end_date
=
make_aware
(
end_date
,
timezone
)
self
.
start_date
=
start_date
self
.
end_date
=
end_date
return
super
(
BaseCourseView
,
self
)
.
get
(
request
,
*
args
,
**
kwargs
)
def
verify_course_exists_or_404
(
self
,
course_id
):
if
self
.
model
.
objects
.
filter
(
course_id
=
course_id
)
.
exists
():
return
True
raise
Http404
def
apply_date_filtering
(
self
,
queryset
):
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
)
queryset
=
self
.
apply_date_filtering
(
queryset
)
return
queryset
# pylint: disable=line-too-long
class
CourseActivityWeeklyView
(
BaseCourseView
):
"""
Weekly course activity
Returns the course activity. Each row/item will contain all activity types for the course-week.
<strong>Activity Types</strong>
<dl>
<dt>ANY</dt>
<dd>The number of unique users who performed any action within the course, including actions not enumerated below.</dd>
<dt>ATTEMPTED_PROBLEM</dt>
<dd>The number of unique users who answered any loncapa based question in the course.</dd>
<dt>PLAYED_VIDEO</dt>
<dd>The number of unique users who started watching any video in the course.</dd>
<dt>POSTED_FORUM</dt>
<dd>The number of unique users who created a new post, responded to a post, or submitted a comment on any forum in the course.</dd>
</dl>
If no start or end dates are passed, the data for the latest date is returned. All dates should are in the UTC zone.
Data is sorted chronologically (earliest to latest).
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
=
models
.
CourseActivityWeekly
serializer_class
=
serializers
.
CourseActivityWeeklySerializer
def
apply_date_filtering
(
self
,
queryset
):
if
self
.
start_date
or
self
.
end_date
:
# Filter by start/end date
if
self
.
start_date
:
queryset
=
queryset
.
filter
(
interval_start__gte
=
self
.
start_date
)
if
self
.
end_date
:
queryset
=
queryset
.
filter
(
interval_end__lt
=
self
.
end_date
)
else
:
# No date filter supplied, so only return data for the latest date
latest_date
=
queryset
.
aggregate
(
Max
(
'interval_end'
))
if
latest_date
:
latest_date
=
latest_date
[
'interval_end__max'
]
queryset
=
queryset
.
filter
(
interval_end
=
latest_date
)
return
queryset
def
get_queryset
(
self
):
queryset
=
super
(
CourseActivityWeeklyView
,
self
)
.
get_queryset
()
queryset
=
self
.
format_data
(
queryset
)
return
queryset
def
_format_activity_type
(
self
,
activity_type
):
activity_type
=
activity_type
.
lower
()
# The data pipeline stores "any" as "active"; however, the API should display "any".
if
activity_type
==
'active'
:
activity_type
=
'any'
return
activity_type
def
format_data
(
self
,
data
):
"""
Group the data by date and combine multiple activity rows into a single row/element.
Arguments
data (iterable) -- Data to be formatted.
"""
formatted_data
=
[]
for
key
,
group
in
groupby
(
data
,
lambda
x
:
(
x
.
course_id
,
x
.
interval_start
,
x
.
interval_end
)):
# Iterate over groups and create a single item with all activity types
item
=
{
u'course_id'
:
key
[
0
],
u'interval_start'
:
key
[
1
],
u'interval_end'
:
key
[
2
],
}
for
activity
in
group
:
activity_type
=
self
.
_format_activity_type
(
activity
.
activity_type
)
item
[
activity_type
]
=
activity
.
count
formatted_data
.
append
(
item
)
return
formatted_data
class
CourseActivityMostRecentWeekView
(
generics
.
RetrieveAPIView
):
"""
Counts of users who performed various actions at least once during the most recently computed week.
...
...
@@ -67,34 +197,26 @@ class CourseActivityMostRecentWeekView(generics.RetrieveAPIView):
def
get_object
(
self
,
queryset
=
None
):
"""Select the activity report for the given course and activity type."""
warnings
.
warn
(
'CourseActivityMostRecentWeekView has been deprecated! Use CourseActivityWeeklyView instead.'
,
DeprecationWarning
)
course_id
=
self
.
kwargs
.
get
(
'course_id'
)
activity_type
=
self
.
_get_activity_type
()
try
:
return
models
.
CourseActivity
ByWeek
.
get_most_recent
(
course_id
,
activity_type
)
return
models
.
CourseActivity
Weekly
.
get_most_recent
(
course_id
,
activity_type
)
except
ObjectDoesNotExist
:
raise
Http404
class
BaseCourseEnrollmentView
(
generics
.
ListAPIView
):
def
verify_course_exists_or_404
(
self
,
course_id
):
if
self
.
model
.
objects
.
filter
(
course_id
=
course_id
)
.
exists
():
return
True
raise
Http404
class
BaseCourseEnrollmentView
(
BaseCourseView
):
def
apply_date_filtering
(
self
,
queryset
):
if
'start_date'
in
self
.
request
.
QUERY_PARAMS
or
'end_date'
in
self
.
request
.
QUERY_PARAMS
:
if
self
.
start_date
or
self
.
end_date
:
# 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
)
if
self
.
start_date
:
queryset
=
queryset
.
filter
(
date__gte
=
self
.
start_date
)
if
self
.
end_date
:
queryset
=
queryset
.
filter
(
date__lt
=
self
.
end_date
)
else
:
# No date filter supplied, so only return data for the latest date
latest_date
=
queryset
.
aggregate
(
Max
(
'date'
))
...
...
@@ -103,13 +225,6 @@ class BaseCourseEnrollmentView(generics.ListAPIView):
queryset
=
queryset
.
filter
(
date
=
latest_date
)
return
queryset
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
)
queryset
=
self
.
apply_date_filtering
(
queryset
)
return
queryset
class
CourseEnrollmentByBirthYearView
(
BaseCourseEnrollmentView
):
"""
...
...
analyticsdataserver/settings/base.py
View file @
25ef8334
...
...
@@ -53,7 +53,7 @@ DATABASES = {
########## GENERAL CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#time-zone
TIME_ZONE
=
'
America/New_York
'
TIME_ZONE
=
'
UTC
'
# See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code
LANGUAGE_CODE
=
'en-us'
...
...
@@ -62,10 +62,10 @@ LANGUAGE_CODE = 'en-us'
SITE_ID
=
1
# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n
USE_I18N
=
Tru
e
USE_I18N
=
Fals
e
# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n
USE_L10N
=
Tru
e
USE_L10N
=
Fals
e
# See: https://docs.djangoproject.com/en/dev/ref/settings/#use-tz
USE_TZ
=
True
...
...
@@ -268,3 +268,4 @@ ENABLE_ADMIN_SITE = False
########## END ANALYTICS DATA API CONFIGURATION
DATE_FORMAT
=
'
%
Y-
%
m-
%
d'
DATETIME_FORMAT
=
'
%
Y-
%
m-
%
dT
%
H
%
M
%
S'
requirements/test.txt
View file @
25ef8334
...
...
@@ -11,3 +11,4 @@ pep257==0.3.2
pep8==1.5.7
pylint==1.2.1
pytz==2012h
nose-ignore-docstring==0.2
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