Commit a5be8559 by Dennis Jen

Merge pull request #71 from edx/dsjen/video

Added video and video timeline endpoints.
parents 51d9a5a5 9ff20e08
......@@ -78,6 +78,7 @@ class Command(BaseCommand):
# Delete existing data
for model in [models.CourseEnrollmentDaily,
models.CourseEnrollmentModeDaily,
models.CourseEnrollmentByGender,
models.CourseEnrollmentByEducation,
models.CourseEnrollmentByBirthYear,
......@@ -155,8 +156,34 @@ class Command(BaseCommand):
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):
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)
num_weeks = options['num_weeks']
......@@ -168,3 +195,5 @@ class Command(BaseCommand):
logger.info("Generating data for %s...", course_id)
self.generate_weekly_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)
......@@ -152,3 +152,37 @@ class SequentialOpenDistribution(models.Model):
course_id = models.CharField(db_index=True, max_length=255)
count = models.IntegerField()
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'
......@@ -241,3 +241,28 @@ class CourseActivityWeeklySerializer(serializers.ModelSerializer):
model = models.CourseActivityWeekly
# TODO: Add 'posted_forum' here to restore forum data
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'
)
......@@ -641,3 +641,61 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
response = self._get_data('foo/bar/course')
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)
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)
......@@ -6,6 +6,7 @@ urlpatterns = patterns(
'',
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'^videos/', include('analytics_data_api.v0.urls.videos', namespace='videos')),
# pylint: disable=no-value-for-parameter
url(r'^authenticated/$', RedirectView.as_view(url=reverse_lazy('authenticated')), name='authenticated'),
......
......@@ -12,7 +12,8 @@ COURSE_URLS = [
('enrollment/education', views.CourseEnrollmentByEducationView, 'enrollment_by_education'),
('enrollment/gender', views.CourseEnrollmentByGenderView, 'enrollment_by_gender'),
('enrollment/location', views.CourseEnrollmentByLocationView, 'enrollment_by_location'),
('problems', views.ProblemsListView, 'problems')
('problems', views.ProblemsListView, 'problems'),
('videos', views.VideosListView, 'videos')
]
urlpatterns = []
......
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))
......@@ -664,3 +664,33 @@ GROUP BY module_id;
row['created'] = datetime.datetime.strptime(created, '%Y-%m-%d %H:%M:%S')
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
"""
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)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment