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
9ff20e08
Commit
9ff20e08
authored
Apr 14, 2015
by
Dennis Jen
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added video and video timeline endpoints.
parent
51d9a5a5
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
292 additions
and
1 deletions
+292
-1
analytics_data_api/management/commands/generate_fake_course_data.py
+29
-0
analytics_data_api/v0/models.py
+34
-0
analytics_data_api/v0/serializers.py
+25
-0
analytics_data_api/v0/tests/views/test_courses.py
+58
-0
analytics_data_api/v0/tests/views/test_videos.py
+64
-0
analytics_data_api/v0/urls/__init__.py
+1
-0
analytics_data_api/v0/urls/courses.py
+2
-1
analytics_data_api/v0/urls/videos.py
+14
-0
analytics_data_api/v0/views/courses.py
+30
-0
analytics_data_api/v0/views/videos.py
+35
-0
No files found.
analytics_data_api/management/commands/generate_fake_course_data.py
View file @
9ff20e08
...
@@ -78,6 +78,7 @@ class Command(BaseCommand):
...
@@ -78,6 +78,7 @@ class Command(BaseCommand):
# Delete existing data
# Delete existing data
for
model
in
[
models
.
CourseEnrollmentDaily
,
for
model
in
[
models
.
CourseEnrollmentDaily
,
models
.
CourseEnrollmentModeDaily
,
models
.
CourseEnrollmentByGender
,
models
.
CourseEnrollmentByGender
,
models
.
CourseEnrollmentByEducation
,
models
.
CourseEnrollmentByEducation
,
models
.
CourseEnrollmentByBirthYear
,
models
.
CourseEnrollmentByBirthYear
,
...
@@ -155,8 +156,34 @@ class Command(BaseCommand):
...
@@ -155,8 +156,34 @@ class Command(BaseCommand):
logger
.
info
(
"Done!"
)
logger
.
info
(
"Done!"
)
def
generate_video_timeline_data
(
self
,
video_id
):
logger
.
info
(
"Deleting video timeline data..."
)
models
.
VideoTimeline
.
objects
.
all
()
.
delete
()
logger
.
info
(
"Generating new video timeline..."
)
for
segment
in
range
(
100
):
active_students
=
random
.
randint
(
100
,
4000
)
counts
=
constrained_sum_sample_pos
(
2
,
active_students
)
models
.
VideoTimeline
.
objects
.
create
(
pipeline_video_id
=
video_id
,
segment
=
segment
,
num_users
=
counts
[
0
],
num_views
=
counts
[
1
])
logger
.
info
(
"Done!"
)
def
generate_video_data
(
self
,
course_id
,
video_id
,
module_id
):
logger
.
info
(
"Deleting course video data..."
)
models
.
Video
.
objects
.
all
()
.
delete
()
logger
.
info
(
"Generating new course videos..."
)
start_views
=
1234
models
.
Video
.
objects
.
create
(
course_id
=
course_id
,
pipeline_video_id
=
video_id
,
encoded_module_id
=
module_id
,
duration
=
500
,
segment_length
=
5
,
start_views
=
start_views
,
end_views
=
random
.
randint
(
100
,
start_views
))
def
handle
(
self
,
*
args
,
**
options
):
def
handle
(
self
,
*
args
,
**
options
):
course_id
=
'edX/DemoX/Demo_Course'
course_id
=
'edX/DemoX/Demo_Course'
video_id
=
'0fac49ba'
video_module_id
=
'i4x-edX-DemoX-video-5c90cffecd9b48b188cbfea176bf7fe9'
start_date
=
datetime
.
datetime
(
year
=
2014
,
month
=
1
,
day
=
1
,
tzinfo
=
timezone
.
utc
)
start_date
=
datetime
.
datetime
(
year
=
2014
,
month
=
1
,
day
=
1
,
tzinfo
=
timezone
.
utc
)
num_weeks
=
options
[
'num_weeks'
]
num_weeks
=
options
[
'num_weeks'
]
...
@@ -168,3 +195,5 @@ class Command(BaseCommand):
...
@@ -168,3 +195,5 @@ class Command(BaseCommand):
logger
.
info
(
"Generating data for
%
s..."
,
course_id
)
logger
.
info
(
"Generating data for
%
s..."
,
course_id
)
self
.
generate_weekly_data
(
course_id
,
start_date
,
end_date
)
self
.
generate_weekly_data
(
course_id
,
start_date
,
end_date
)
self
.
generate_daily_data
(
course_id
,
start_date
,
end_date
)
self
.
generate_daily_data
(
course_id
,
start_date
,
end_date
)
self
.
generate_video_data
(
course_id
,
video_id
,
video_module_id
)
self
.
generate_video_timeline_data
(
video_id
)
analytics_data_api/v0/models.py
View file @
9ff20e08
...
@@ -152,3 +152,37 @@ class SequentialOpenDistribution(models.Model):
...
@@ -152,3 +152,37 @@ class SequentialOpenDistribution(models.Model):
course_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
course_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
count
=
models
.
IntegerField
()
count
=
models
.
IntegerField
()
created
=
models
.
DateTimeField
(
auto_now_add
=
True
)
created
=
models
.
DateTimeField
(
auto_now_add
=
True
)
class
BaseVideo
(
models
.
Model
):
""" Base video model. """
pipeline_video_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
created
=
models
.
DateTimeField
(
auto_now_add
=
True
)
class
Meta
(
object
):
abstract
=
True
class
VideoTimeline
(
BaseVideo
):
""" Timeline of video segments. """
segment
=
models
.
IntegerField
()
num_users
=
models
.
IntegerField
()
num_views
=
models
.
IntegerField
()
class
Meta
(
BaseVideo
.
Meta
):
db_table
=
'video_timeline'
class
Video
(
BaseVideo
):
""" Videos associated with a particular course. """
course_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
encoded_module_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
duration
=
models
.
IntegerField
()
segment_length
=
models
.
IntegerField
()
start_views
=
models
.
IntegerField
()
end_views
=
models
.
IntegerField
()
class
Meta
(
BaseVideo
.
Meta
):
db_table
=
'video'
analytics_data_api/v0/serializers.py
View file @
9ff20e08
...
@@ -241,3 +241,28 @@ class CourseActivityWeeklySerializer(serializers.ModelSerializer):
...
@@ -241,3 +241,28 @@ class CourseActivityWeeklySerializer(serializers.ModelSerializer):
model
=
models
.
CourseActivityWeekly
model
=
models
.
CourseActivityWeekly
# TODO: Add 'posted_forum' here to restore forum data
# TODO: Add 'posted_forum' here to restore forum data
fields
=
(
'interval_start'
,
'interval_end'
,
'course_id'
,
'any'
,
'attempted_problem'
,
'played_video'
,
'created'
)
fields
=
(
'interval_start'
,
'interval_end'
,
'course_id'
,
'any'
,
'attempted_problem'
,
'played_video'
,
'created'
)
class
VideoSerializer
(
ModelSerializerWithCreatedField
):
class
Meta
(
object
):
model
=
models
.
Video
fields
=
(
'pipeline_video_id'
,
'encoded_module_id'
,
'duration'
,
'segment_length'
,
'start_views'
,
'end_views'
,
'created'
)
class
VideoTimelineSerializer
(
ModelSerializerWithCreatedField
):
class
Meta
(
object
):
model
=
models
.
VideoTimeline
fields
=
(
'segment'
,
'num_users'
,
'num_views'
,
'created'
)
analytics_data_api/v0/tests/views/test_courses.py
View file @
9ff20e08
...
@@ -641,3 +641,61 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
...
@@ -641,3 +641,61 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
response
=
self
.
_get_data
(
'foo/bar/course'
)
response
=
self
.
_get_data
(
'foo/bar/course'
)
self
.
assertEquals
(
response
.
status_code
,
404
)
self
.
assertEquals
(
response
.
status_code
,
404
)
class
CourseVideosListViewTests
(
DemoCourseMixin
,
TestCaseWithAuthentication
):
def
_get_data
(
self
,
course_id
=
None
):
"""
Retrieve videos for a specified course.
"""
course_id
=
course_id
or
self
.
course_id
url
=
'/api/v0/courses/{}/videos/'
.
format
(
course_id
)
return
self
.
authenticated_get
(
url
)
def
test_get
(
self
):
# add a blank row, which shouldn't be included in results
G
(
models
.
Video
)
module_id
=
'i4x-test-video-1'
video_id
=
'v1d30'
created
=
datetime
.
datetime
.
utcnow
()
date_time_format
=
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
G
(
models
.
Video
,
course_id
=
self
.
course_id
,
encoded_module_id
=
module_id
,
pipeline_video_id
=
video_id
,
duration
=
100
,
segment_length
=
1
,
start_views
=
50
,
end_views
=
10
,
created
=
created
.
strftime
(
date_time_format
))
alt_module_id
=
'i4x-test-video-2'
alt_video_id
=
'a1d30'
alt_created
=
created
+
datetime
.
timedelta
(
seconds
=
10
)
G
(
models
.
Video
,
course_id
=
self
.
course_id
,
encoded_module_id
=
alt_module_id
,
pipeline_video_id
=
alt_video_id
,
duration
=
200
,
segment_length
=
5
,
start_views
=
1050
,
end_views
=
50
,
created
=
alt_created
.
strftime
(
date_time_format
))
expected
=
[
{
'duration'
:
100
,
'encoded_module_id'
:
module_id
,
'pipeline_video_id'
:
video_id
,
'segment_length'
:
1
,
'start_views'
:
50
,
'end_views'
:
10
,
'created'
:
created
.
strftime
(
settings
.
DATETIME_FORMAT
)
},
{
'duration'
:
200
,
'encoded_module_id'
:
alt_module_id
,
'pipeline_video_id'
:
alt_video_id
,
'segment_length'
:
5
,
'start_views'
:
1050
,
'end_views'
:
50
,
'created'
:
alt_created
.
strftime
(
settings
.
DATETIME_FORMAT
)
}
]
response
=
self
.
_get_data
(
self
.
course_id
)
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertListEqual
(
response
.
data
,
expected
)
def
test_get_404
(
self
):
response
=
self
.
_get_data
(
'foo/bar/course'
)
self
.
assertEquals
(
response
.
status_code
,
404
)
analytics_data_api/v0/tests/views/test_videos.py
0 → 100644
View file @
9ff20e08
import
datetime
from
django.conf
import
settings
from
django_dynamic_fixture
import
G
from
analytics_data_api.v0
import
models
from
analyticsdataserver.tests
import
TestCaseWithAuthentication
class
VideoTimelineTests
(
TestCaseWithAuthentication
):
def
_get_data
(
self
,
video_id
=
None
):
return
self
.
authenticated_get
(
'/api/v0/videos/{}/timeline'
.
format
(
video_id
))
def
test_get
(
self
):
# add a blank row, which shouldn't be included in results
G
(
models
.
VideoTimeline
)
video_id
=
'v1d30'
created
=
datetime
.
datetime
.
utcnow
()
date_time_format
=
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
G
(
models
.
VideoTimeline
,
pipeline_video_id
=
video_id
,
segment
=
0
,
num_users
=
10
,
num_views
=
50
,
created
=
created
.
strftime
(
date_time_format
))
G
(
models
.
VideoTimeline
,
pipeline_video_id
=
video_id
,
segment
=
1
,
num_users
=
1
,
num_views
=
1234
,
created
=
created
.
strftime
(
date_time_format
))
alt_video_id
=
'altv1d30'
alt_created
=
created
+
datetime
.
timedelta
(
seconds
=
17
)
G
(
models
.
VideoTimeline
,
pipeline_video_id
=
alt_video_id
,
segment
=
0
,
num_users
=
10231
,
num_views
=
834828
,
created
=
alt_created
.
strftime
(
date_time_format
))
expected
=
[
{
'segment'
:
0
,
'num_users'
:
10
,
'num_views'
:
50
,
'created'
:
created
.
strftime
(
settings
.
DATETIME_FORMAT
)
},
{
'segment'
:
1
,
'num_users'
:
1
,
'num_views'
:
1234
,
'created'
:
created
.
strftime
(
settings
.
DATETIME_FORMAT
)
}
]
response
=
self
.
_get_data
(
video_id
)
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertListEqual
(
response
.
data
,
expected
)
expected
=
[
{
'segment'
:
0
,
'num_users'
:
10231
,
'num_views'
:
834828
,
'created'
:
alt_created
.
strftime
(
settings
.
DATETIME_FORMAT
)
}
]
response
=
self
.
_get_data
(
alt_video_id
)
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertListEqual
(
response
.
data
,
expected
)
def
test_get_404
(
self
):
response
=
self
.
_get_data
(
'no_id'
)
self
.
assertEquals
(
response
.
status_code
,
404
)
analytics_data_api/v0/urls/__init__.py
View file @
9ff20e08
...
@@ -6,6 +6,7 @@ urlpatterns = patterns(
...
@@ -6,6 +6,7 @@ urlpatterns = patterns(
''
,
''
,
url
(
r'^courses/'
,
include
(
'analytics_data_api.v0.urls.courses'
,
namespace
=
'courses'
)),
url
(
r'^courses/'
,
include
(
'analytics_data_api.v0.urls.courses'
,
namespace
=
'courses'
)),
url
(
r'^problems/'
,
include
(
'analytics_data_api.v0.urls.problems'
,
namespace
=
'problems'
)),
url
(
r'^problems/'
,
include
(
'analytics_data_api.v0.urls.problems'
,
namespace
=
'problems'
)),
url
(
r'^videos/'
,
include
(
'analytics_data_api.v0.urls.videos'
,
namespace
=
'videos'
)),
# pylint: disable=no-value-for-parameter
# pylint: disable=no-value-for-parameter
url
(
r'^authenticated/$'
,
RedirectView
.
as_view
(
url
=
reverse_lazy
(
'authenticated'
)),
name
=
'authenticated'
),
url
(
r'^authenticated/$'
,
RedirectView
.
as_view
(
url
=
reverse_lazy
(
'authenticated'
)),
name
=
'authenticated'
),
...
...
analytics_data_api/v0/urls/courses.py
View file @
9ff20e08
...
@@ -12,7 +12,8 @@ COURSE_URLS = [
...
@@ -12,7 +12,8 @@ COURSE_URLS = [
(
'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'
),
(
'enrollment/location'
,
views
.
CourseEnrollmentByLocationView
,
'enrollment_by_location'
),
(
'enrollment/location'
,
views
.
CourseEnrollmentByLocationView
,
'enrollment_by_location'
),
(
'problems'
,
views
.
ProblemsListView
,
'problems'
)
(
'problems'
,
views
.
ProblemsListView
,
'problems'
),
(
'videos'
,
views
.
VideosListView
,
'videos'
)
]
]
urlpatterns
=
[]
urlpatterns
=
[]
...
...
analytics_data_api/v0/urls/videos.py
0 → 100644
View file @
9ff20e08
import
re
from
django.conf.urls
import
patterns
,
url
from
analytics_data_api.v0.views
import
videos
as
views
VIDEO_URLS
=
[
(
'timeline'
,
views
.
VideoTimelineView
,
'timeline'
),
]
urlpatterns
=
[]
for
path
,
view
,
name
in
VIDEO_URLS
:
urlpatterns
+=
patterns
(
''
,
url
(
r'^(?P<video_id>.+)/'
+
re
.
escape
(
path
)
+
r'/$'
,
view
.
as_view
(),
name
=
name
))
analytics_data_api/v0/views/courses.py
View file @
9ff20e08
...
@@ -664,3 +664,33 @@ GROUP BY module_id;
...
@@ -664,3 +664,33 @@ GROUP BY module_id;
row
[
'created'
]
=
datetime
.
datetime
.
strptime
(
created
,
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
)
row
[
'created'
]
=
datetime
.
datetime
.
strptime
(
created
,
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
)
return
rows
return
rows
class
VideosListView
(
BaseCourseView
):
"""
Get videos for a course.
**Example request**
GET /api/v0/courses/{course_id}/videos/
**Response Values**
Returns a collection of video views and metadata for each video. Each collection contains:
* video_id: The ID of the video.
* encoded_module_id: The encoded module ID.
* duration: Length of the video in seconds.
* segment_length: Length of each segment of the video in seconds.
* start_views: Number of views at the start of the video.
* end_views: Number of views at the end of the video.
* created: The date the video data was updated.
"""
serializer_class
=
serializers
.
VideoSerializer
allow_empty
=
False
model
=
models
.
Video
def
apply_date_filtering
(
self
,
queryset
):
# no date filtering for videos -- just return the queryset
return
queryset
analytics_data_api/v0/views/videos.py
0 → 100644
View file @
9ff20e08
"""
API methods for module level data.
"""
from
rest_framework
import
generics
from
analytics_data_api.v0.models
import
VideoTimeline
from
analytics_data_api.v0.serializers
import
VideoTimelineSerializer
class
VideoTimelineView
(
generics
.
ListAPIView
):
"""
Get the timeline for a video.
**Example request**
GET /api/v0/videos/{video_id}/timeline/
**Response Values**
Returns viewing data for segments of a video. Each collection contains:
* segment: Order of the segment in the timeline.
* num_users: Number of unique users that have viewed this segment.
* num_views: Number of total views for this segment.
* created: The date the segment data was computed.
"""
serializer_class
=
VideoTimelineSerializer
allow_empty
=
False
def
get_queryset
(
self
):
"""Select the view count for a specific module"""
video_id
=
self
.
kwargs
.
get
(
'video_id'
)
return
VideoTimeline
.
objects
.
filter
(
pipeline_video_id
=
video_id
)
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