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
e95f2178
Commit
e95f2178
authored
Aug 29, 2016
by
Dennis Jen
Committed by
GitHub
Aug 29, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #135 from edx/dsjen/updgrade-drf
Update DRF to 3.4.6
parents
1e8aa546
87c4a49c
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
184 additions
and
135 deletions
+184
-135
analytics_data_api/v0/models.py
+4
-4
analytics_data_api/v0/serializers.py
+109
-90
analytics_data_api/v0/tests/views/test_courses.py
+22
-18
analytics_data_api/v0/tests/views/test_learners.py
+1
-2
analytics_data_api/v0/views/__init__.py
+1
-1
analytics_data_api/v0/views/courses.py
+15
-6
analytics_data_api/v0/views/learners.py
+9
-13
analytics_data_api/v0/views/problems.py
+5
-0
analytics_data_api/v0/views/utils.py
+14
-0
analytics_data_api/v0/views/videos.py
+3
-0
requirements/base.txt
+1
-1
No files found.
analytics_data_api/v0/models.py
View file @
e95f2178
...
@@ -234,7 +234,7 @@ class Video(BaseVideo):
...
@@ -234,7 +234,7 @@ class Video(BaseVideo):
class
RosterUpdate
(
DocType
):
class
RosterUpdate
(
DocType
):
date
=
Date
()
date
=
Date
(
format
=
settings
.
DATE_FORMAT
)
# pylint: disable=old-style-class
# pylint: disable=old-style-class
class
Meta
:
class
Meta
:
...
@@ -265,8 +265,8 @@ class RosterEntry(DocType):
...
@@ -265,8 +265,8 @@ class RosterEntry(DocType):
attempt_ratio_order
=
Integer
()
attempt_ratio_order
=
Integer
()
discussion_contributions
=
Integer
()
discussion_contributions
=
Integer
()
videos_watched
=
Integer
()
videos_watched
=
Integer
()
enrollment_date
=
Date
()
enrollment_date
=
Date
(
format
=
settings
.
DATE_FORMAT
)
last_updated
=
Date
()
last_updated
=
Date
(
format
=
settings
.
DATE_FORMAT
)
# pylint: disable=old-style-class
# pylint: disable=old-style-class
class
Meta
:
class
Meta
:
...
@@ -460,7 +460,7 @@ class ModuleEngagement(models.Model):
...
@@ -460,7 +460,7 @@ class ModuleEngagement(models.Model):
course_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
course_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
username
=
models
.
CharField
(
max_length
=
255
)
username
=
models
.
CharField
(
max_length
=
255
)
date
=
models
.
Date
Time
Field
()
date
=
models
.
DateField
()
# This will be one of "problem", "video" or "discussion"
# This will be one of "problem", "video" or "discussion"
entity_type
=
models
.
CharField
(
max_length
=
255
)
entity_type
=
models
.
CharField
(
max_length
=
255
)
# For problems this will be the usage key, for videos it will be the html encoded module ID,
# For problems this will be the usage key, for videos it will be the html encoded module ID,
...
...
analytics_data_api/v0/serializers.py
View file @
e95f2178
from
collections
import
OrderedDict
from
urlparse
import
urljoin
from
urlparse
import
urljoin
from
django.conf
import
settings
from
django.conf
import
settings
from
rest_framework
import
pagination
,
serializers
from
rest_framework
import
pagination
,
serializers
from
rest_framework.response
import
Response
from
analytics_data_api.constants
import
(
from
analytics_data_api.constants
import
(
engagement_events
,
engagement_events
,
enrollment_modes
,
enrollment_modes
,
genders
,
learner
,
)
)
from
analytics_data_api.v0
import
models
from
analytics_data_api.v0
import
models
...
@@ -23,7 +25,7 @@ class CourseActivityByWeekSerializer(serializers.ModelSerializer):
...
@@ -23,7 +25,7 @@ class CourseActivityByWeekSerializer(serializers.ModelSerializer):
particular record is likely to change unexpectedly so we avoid exposing it.
particular record is likely to change unexpectedly so we avoid exposing it.
"""
"""
activity_type
=
serializers
.
SerializerMethodField
(
'get_activity_type'
)
activity_type
=
serializers
.
SerializerMethodField
()
def
get_activity_type
(
self
,
obj
):
def
get_activity_type
(
self
,
obj
):
"""
"""
...
@@ -45,6 +47,7 @@ class ModelSerializerWithCreatedField(serializers.ModelSerializer):
...
@@ -45,6 +47,7 @@ class ModelSerializerWithCreatedField(serializers.ModelSerializer):
created
=
serializers
.
DateTimeField
(
format
=
settings
.
DATETIME_FORMAT
)
created
=
serializers
.
DateTimeField
(
format
=
settings
.
DATETIME_FORMAT
)
# pylint: disable=abstract-method
class
ProblemSerializer
(
serializers
.
Serializer
):
class
ProblemSerializer
(
serializers
.
Serializer
):
"""
"""
Serializer for problems.
Serializer for problems.
...
@@ -57,6 +60,7 @@ class ProblemSerializer(serializers.Serializer):
...
@@ -57,6 +60,7 @@ class ProblemSerializer(serializers.Serializer):
created
=
serializers
.
DateTimeField
(
format
=
settings
.
DATETIME_FORMAT
)
created
=
serializers
.
DateTimeField
(
format
=
settings
.
DATETIME_FORMAT
)
# pylint: disable=abstract-method
class
ProblemsAndTagsSerializer
(
serializers
.
Serializer
):
class
ProblemsAndTagsSerializer
(
serializers
.
Serializer
):
"""
"""
Serializer for problems and tags.
Serializer for problems and tags.
...
@@ -187,13 +191,7 @@ class SequentialOpenDistributionSerializer(ModelSerializerWithCreatedField):
...
@@ -187,13 +191,7 @@ class SequentialOpenDistributionSerializer(ModelSerializerWithCreatedField):
)
)
class
DefaultIfNoneMixin
(
object
):
class
BaseCourseEnrollmentModelSerializer
(
ModelSerializerWithCreatedField
):
def
default_if_none
(
self
,
value
,
default
=
0
):
return
value
if
value
is
not
None
else
default
class
BaseCourseEnrollmentModelSerializer
(
DefaultIfNoneMixin
,
ModelSerializerWithCreatedField
):
date
=
serializers
.
DateField
(
format
=
settings
.
DATE_FORMAT
)
date
=
serializers
.
DateField
(
format
=
settings
.
DATE_FORMAT
)
...
@@ -207,32 +205,35 @@ class CourseEnrollmentDailySerializer(BaseCourseEnrollmentModelSerializer):
...
@@ -207,32 +205,35 @@ class CourseEnrollmentDailySerializer(BaseCourseEnrollmentModelSerializer):
class
CourseEnrollmentModeDailySerializer
(
BaseCourseEnrollmentModelSerializer
):
class
CourseEnrollmentModeDailySerializer
(
BaseCourseEnrollmentModelSerializer
):
""" Representation of course enrollment, broken down by mode, for a single day and course. """
""" Representation of course enrollment, broken down by mode, for a single day and course. """
audit
=
serializers
.
SerializerMethodField
()
credit
=
serializers
.
SerializerMethodField
()
honor
=
serializers
.
SerializerMethodField
()
professional
=
serializers
.
SerializerMethodField
()
verified
=
serializers
.
SerializerMethodField
()
def
get_default_fields
(
self
):
def
get_audit
(
self
,
obj
):
# pylint: disable=super-on-old-class
return
obj
.
get
(
'audit'
,
0
)
fields
=
super
(
CourseEnrollmentModeDailySerializer
,
self
)
.
get_default_fields
()
# Create a field for each enrollment mode
def
get_honor
(
self
,
obj
):
for
mode
in
ENROLLMENT_MODES
:
return
obj
.
get
(
'honor'
,
0
)
fields
[
mode
]
=
serializers
.
IntegerField
(
required
=
True
,
default
=
0
)
# Create a transform method for each field
def
get_credit
(
self
,
obj
):
setattr
(
self
,
'transform_
%
s'
%
mode
,
self
.
_transform_mode
)
return
obj
.
get
(
'credit'
,
0
)
fields
[
'cumulative_count'
]
=
serializers
.
IntegerField
(
required
=
True
,
default
=
0
)
def
get_professional
(
self
,
obj
):
return
obj
.
get
(
'professional'
,
0
)
return
fields
def
get_verified
(
self
,
obj
):
return
obj
.
get
(
'verified'
,
0
)
def
_transform_mode
(
self
,
_obj
,
value
):
return
self
.
default_if_none
(
value
,
0
)
class
Meta
(
object
):
class
Meta
(
object
):
model
=
models
.
CourseEnrollmentDaily
model
=
models
.
CourseEnrollment
Mode
Daily
# Declare the dynamically-created fields here as well so that they will be picked up by Swagger.
# Declare the dynamically-created fields here as well so that they will be picked up by Swagger.
fields
=
[
'course_id'
,
'date'
,
'count'
,
'cumulative_count'
,
'created'
]
+
ENROLLMENT_MODES
fields
=
[
'course_id'
,
'date'
,
'count'
,
'cumulative_count'
,
'created'
]
+
ENROLLMENT_MODES
# pylint: disable=abstract-method
class
CountrySerializer
(
serializers
.
Serializer
):
class
CountrySerializer
(
serializers
.
Serializer
):
"""
"""
Serialize country to an object with fields for the complete country name
Serialize country to an object with fields for the complete country name
...
@@ -256,21 +257,23 @@ class CourseEnrollmentByCountrySerializer(BaseCourseEnrollmentModelSerializer):
...
@@ -256,21 +257,23 @@ class CourseEnrollmentByCountrySerializer(BaseCourseEnrollmentModelSerializer):
class
CourseEnrollmentByGenderSerializer
(
BaseCourseEnrollmentModelSerializer
):
class
CourseEnrollmentByGenderSerializer
(
BaseCourseEnrollmentModelSerializer
):
def
get_default_fields
(
self
):
# pylint: disable=super-on-old-class
fields
=
super
(
CourseEnrollmentByGenderSerializer
,
self
)
.
get_default_fields
()
# Create a field for each gender
female
=
serializers
.
ReadOnlyField
()
for
gender
in
genders
.
ALL
:
male
=
serializers
.
ReadOnlyField
()
fields
[
gender
]
=
serializers
.
IntegerField
(
required
=
True
,
default
=
0
)
other
=
serializers
.
ReadOnlyField
()
unknown
=
serializers
.
ReadOnlyField
()
def
get_female
(
self
,
obj
):
return
obj
.
get
(
'female'
,
None
)
# Create a transform method for each field
def
get_male
(
self
,
obj
):
setattr
(
self
,
'transform_
%
s'
%
gender
,
self
.
_transform_gender
)
return
obj
.
get
(
'male'
,
None
)
return
fields
def
get_other
(
self
,
obj
):
return
obj
.
get
(
'other'
,
None
)
def
_transform_gender
(
self
,
_obj
,
value
):
def
get_unknown
(
self
,
obj
):
return
self
.
default_if_none
(
value
,
0
)
return
obj
.
get
(
'unknown'
,
None
)
class
Meta
(
object
):
class
Meta
(
object
):
model
=
models
.
CourseEnrollmentByGender
model
=
models
.
CourseEnrollmentByGender
...
@@ -329,28 +332,37 @@ class VideoTimelineSerializer(ModelSerializerWithCreatedField):
...
@@ -329,28 +332,37 @@ class VideoTimelineSerializer(ModelSerializerWithCreatedField):
)
)
# pylint: disable=abstract-method
class
LastUpdatedSerializer
(
serializers
.
Serializer
):
class
LastUpdatedSerializer
(
serializers
.
Serializer
):
last_updated
=
serializers
.
DateField
(
source
=
'date'
,
format
=
settings
.
DATE_FORMAT
)
last_updated
=
serializers
.
Date
Time
Field
(
source
=
'date'
,
format
=
settings
.
DATE_FORMAT
)
class
LearnerSerializer
(
serializers
.
Serializer
,
DefaultIfNoneMixin
):
# pylint: disable=abstract-method
username
=
serializers
.
CharField
(
source
=
'username'
)
class
LearnerSerializer
(
serializers
.
Serializer
):
enrollment_mode
=
serializers
.
CharField
(
source
=
'enrollment_mode'
)
username
=
serializers
.
CharField
()
name
=
serializers
.
CharField
(
source
=
'name'
)
enrollment_mode
=
serializers
.
CharField
()
account_url
=
serializers
.
SerializerMethodField
(
'get_account_url'
)
name
=
serializers
.
CharField
()
email
=
serializers
.
CharField
(
source
=
'email'
)
account_url
=
serializers
.
SerializerMethodField
()
segments
=
serializers
.
Field
(
source
=
'segments'
)
email
=
serializers
.
CharField
()
engagements
=
serializers
.
SerializerMethodField
(
'get_engagements'
)
segments
=
serializers
.
SerializerMethodField
()
enrollment_date
=
serializers
.
DateField
(
source
=
'enrollment_date'
,
format
=
settings
.
DATE_FORMAT
)
engagements
=
serializers
.
SerializerMethodField
()
cohort
=
serializers
.
CharField
(
source
=
'cohort'
)
enrollment_date
=
serializers
.
DateTimeField
(
format
=
settings
.
DATE_FORMAT
)
cohort
=
serializers
.
SerializerMethodField
()
def
transform_segments
(
self
,
_obj
,
value
):
# returns null instead of empty strings
def
get_segments
(
self
,
obj
):
return
value
or
[]
# using hasattr() instead because DocType.get() is overloaded and makes a request
if
hasattr
(
obj
,
'segments'
):
# json parsing will fail unless in unicode
return
[
unicode
(
segment
)
for
segment
in
obj
.
segments
]
else
:
return
[]
def
transform_cohort
(
self
,
_obj
,
value
):
def
get_cohort
(
self
,
obj
):
# returns null instead of empty strings
# using hasattr() instead because DocType.get() is overloaded and makes a request
return
value
or
None
if
hasattr
(
obj
,
'cohort'
)
and
len
(
obj
.
cohort
)
>
0
:
return
obj
.
cohort
else
:
return
None
def
get_account_url
(
self
,
obj
):
def
get_account_url
(
self
,
obj
):
if
settings
.
LMS_USER_ACCOUNT_BASE_URL
:
if
settings
.
LMS_USER_ACCOUNT_BASE_URL
:
...
@@ -358,6 +370,9 @@ class LearnerSerializer(serializers.Serializer, DefaultIfNoneMixin):
...
@@ -358,6 +370,9 @@ class LearnerSerializer(serializers.Serializer, DefaultIfNoneMixin):
else
:
else
:
return
None
return
None
def
default_if_none
(
self
,
value
,
default
=
0
):
return
value
if
value
is
not
None
else
default
def
get_engagements
(
self
,
obj
):
def
get_engagements
(
self
,
obj
):
"""
"""
Add the engagement totals.
Add the engagement totals.
...
@@ -376,52 +391,55 @@ class LearnerSerializer(serializers.Serializer, DefaultIfNoneMixin):
...
@@ -376,52 +391,55 @@ class LearnerSerializer(serializers.Serializer, DefaultIfNoneMixin):
return
engagements
return
engagements
class
EdxPaginationSerializer
(
pagination
.
Pag
inationSerializer
):
class
EdxPaginationSerializer
(
pagination
.
Pag
eNumberPagination
):
"""
"""
Adds values to the response according to edX REST API Conventions.
Adds values to the response according to edX REST API Conventions.
"""
"""
count
=
serializers
.
Field
(
source
=
'paginator.count'
)
page_size_query_param
=
'page_size'
num_pages
=
serializers
.
Field
(
source
=
'paginator.num_pages'
)
page_size
=
learner
.
LEARNER_API_DEFAULT_LIST_PAGE_SIZE
max_page_size
=
100
# TODO -- tweak during load testing
class
ElasticsearchDSLSearchSerializer
(
EdxPaginationSerializer
):
def
get_paginated_response
(
self
,
data
):
def
__init__
(
self
,
*
args
,
**
kwargs
):
# The output is more readable with num_pages included not at the end, but
"""Make sure that the elasticsearch query is executed."""
# inefficient to insert into an OrderedDict, so the response is copied from
# Because the elasticsearch-dsl search object has a different
# rest_framework.pagination with the addition of "num_pages".
# API from the queryset object that's expected by the django
return
Response
(
OrderedDict
([
# Paginator object, we have to manually execute the query.
(
'count'
,
self
.
page
.
paginator
.
count
),
# Note that the `kwargs['instance']` is the Page object, and
(
'num_pages'
,
self
.
page
.
paginator
.
num_pages
),
# `kwargs['instance'].object_list` is actually an
(
'next'
,
self
.
get_next_link
()),
# elasticsearch-dsl search object.
(
'previous'
,
self
.
get_previous_link
()),
kwargs
[
'instance'
]
.
object_list
=
kwargs
[
'instance'
]
.
object_list
.
execute
()
(
'results'
,
data
)
super
(
ElasticsearchDSLSearchSerializer
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
]))
class
EngagementDaySerializer
(
DefaultIfNoneMixin
,
serializers
.
Serializer
):
# pylint: disable=abstract-method
class
EngagementDaySerializer
(
serializers
.
Serializer
):
date
=
serializers
.
DateField
(
format
=
settings
.
DATE_FORMAT
)
date
=
serializers
.
DateField
(
format
=
settings
.
DATE_FORMAT
)
problems_attempted
=
serializers
.
IntegerField
(
required
=
True
,
default
=
0
)
problems_attempted
=
serializers
.
SerializerMethodField
(
)
problems_completed
=
serializers
.
IntegerField
(
required
=
True
,
default
=
0
)
problems_completed
=
serializers
.
SerializerMethodField
(
)
discussion_contributions
=
serializers
.
IntegerField
(
required
=
True
,
default
=
0
)
discussion_contributions
=
serializers
.
SerializerMethodField
(
)
videos_viewed
=
serializers
.
IntegerField
(
required
=
True
,
default
=
0
)
videos_viewed
=
serializers
.
SerializerMethodField
(
)
def
transform_problems_attempted
(
self
,
_obj
,
value
):
def
get_problems_attempted
(
self
,
obj
):
return
self
.
default_if_none
(
value
,
0
)
return
obj
.
get
(
'problems_attempted'
,
0
)
def
transform_problems_completed
(
self
,
_obj
,
value
):
def
get_problems_completed
(
self
,
obj
):
return
self
.
default_if_none
(
value
,
0
)
return
obj
.
get
(
'problems_completed'
,
0
)
def
transform_discussion_contributions
(
self
,
_obj
,
value
):
def
get_discussion_contributions
(
self
,
obj
):
return
self
.
default_if_none
(
value
,
0
)
return
obj
.
get
(
'discussion_contributions'
,
0
)
def
transform_videos_viewed
(
self
,
_obj
,
value
):
def
get_videos_viewed
(
self
,
obj
):
return
self
.
default_if_none
(
value
,
0
)
return
obj
.
get
(
'videos_viewed'
,
0
)
# pylint: disable=abstract-method
class
DateRangeSerializer
(
serializers
.
Serializer
):
class
DateRangeSerializer
(
serializers
.
Serializer
):
start
=
serializers
.
DateTimeField
(
source
=
'start_date'
,
format
=
settings
.
DATE_FORMAT
)
start
=
serializers
.
DateTimeField
(
source
=
'start_date'
,
format
=
settings
.
DATE_FORMAT
)
end
=
serializers
.
DateTimeField
(
source
=
'end_date'
,
format
=
settings
.
DATE_FORMAT
)
end
=
serializers
.
DateTimeField
(
source
=
'end_date'
,
format
=
settings
.
DATE_FORMAT
)
# pylint: disable=abstract-method
class
EnagementRangeMetricSerializer
(
serializers
.
Serializer
):
class
EnagementRangeMetricSerializer
(
serializers
.
Serializer
):
"""
"""
Serializes ModuleEngagementMetricRanges ('bottom', 'average', and 'top') into
Serializes ModuleEngagementMetricRanges ('bottom', 'average', and 'top') into
...
@@ -429,9 +447,9 @@ class EnagementRangeMetricSerializer(serializers.Serializer):
...
@@ -429,9 +447,9 @@ class EnagementRangeMetricSerializer(serializers.Serializer):
represented as arrays. If any one of the ranges is not defined, it is not
represented as arrays. If any one of the ranges is not defined, it is not
included in the serialized output.
included in the serialized output.
"""
"""
class_rank_bottom
=
serializers
.
SerializerMethodField
(
'get_class_rank_bottom'
)
class_rank_bottom
=
serializers
.
SerializerMethodField
()
class_rank_average
=
serializers
.
SerializerMethodField
(
'get_class_rank_average'
)
class_rank_average
=
serializers
.
SerializerMethodField
()
class_rank_top
=
serializers
.
SerializerMethodField
(
'get_class_rank_top'
)
class_rank_top
=
serializers
.
SerializerMethodField
()
def
get_class_rank_average
(
self
,
obj
):
def
get_class_rank_average
(
self
,
obj
):
return
self
.
_transform_range
(
obj
[
'average'
])
return
self
.
_transform_range
(
obj
[
'average'
])
...
@@ -446,11 +464,12 @@ class EnagementRangeMetricSerializer(serializers.Serializer):
...
@@ -446,11 +464,12 @@ class EnagementRangeMetricSerializer(serializers.Serializer):
return
[
metric_range
.
low_value
,
metric_range
.
high_value
]
if
metric_range
else
None
return
[
metric_range
.
low_value
,
metric_range
.
high_value
]
if
metric_range
else
None
# pylint: disable=abstract-method
class
CourseLearnerMetadataSerializer
(
serializers
.
Serializer
):
class
CourseLearnerMetadataSerializer
(
serializers
.
Serializer
):
enrollment_modes
=
serializers
.
Field
(
source
=
'es_data.enrollment_modes'
)
enrollment_modes
=
serializers
.
ReadOnly
Field
(
source
=
'es_data.enrollment_modes'
)
segments
=
serializers
.
Field
(
source
=
'es_data.segments'
)
segments
=
serializers
.
ReadOnly
Field
(
source
=
'es_data.segments'
)
cohorts
=
serializers
.
Field
(
source
=
'es_data.cohorts'
)
cohorts
=
serializers
.
ReadOnly
Field
(
source
=
'es_data.cohorts'
)
engagement_ranges
=
serializers
.
SerializerMethodField
(
'get_engagement_ranges'
)
engagement_ranges
=
serializers
.
SerializerMethodField
()
def
get_engagement_ranges
(
self
,
obj
):
def
get_engagement_ranges
(
self
,
obj
):
query_set
=
obj
[
'engagement_ranges'
]
query_set
=
obj
[
'engagement_ranges'
]
...
...
analytics_data_api/v0/tests/views/test_courses.py
View file @
e95f2178
...
@@ -210,10 +210,11 @@ class CourseActivityLastWeekTest(DemoCourseMixin, TestCaseWithAuthentication):
...
@@ -210,10 +210,11 @@ class CourseActivityLastWeekTest(DemoCourseMixin, TestCaseWithAuthentication):
@staticmethod
@staticmethod
def
get_activity_record
(
**
kwargs
):
def
get_activity_record
(
**
kwargs
):
datetime_format
=
"
%
Y-
%
m-
%
dT
%
H:
%
M:
%
SZ"
default
=
{
default
=
{
'course_id'
:
DEMO_COURSE_ID
,
'course_id'
:
DEMO_COURSE_ID
,
'interval_start'
:
datetime
.
datetime
(
2014
,
1
,
1
,
0
,
0
,
tzinfo
=
pytz
.
utc
),
'interval_start'
:
datetime
.
datetime
(
2014
,
1
,
1
,
0
,
0
,
tzinfo
=
pytz
.
utc
)
.
strftime
(
datetime_format
)
,
'interval_end'
:
datetime
.
datetime
(
2014
,
1
,
8
,
0
,
0
,
tzinfo
=
pytz
.
utc
),
'interval_end'
:
datetime
.
datetime
(
2014
,
1
,
8
,
0
,
0
,
tzinfo
=
pytz
.
utc
)
.
strftime
(
datetime_format
)
,
'activity_type'
:
'any'
,
'activity_type'
:
'any'
,
'count'
:
300
,
'count'
:
300
,
}
}
...
@@ -339,6 +340,9 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, Defau
...
@@ -339,6 +340,9 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, Defau
super
(
CourseEnrollmentByGenderViewTests
,
self
)
.
setUp
()
super
(
CourseEnrollmentByGenderViewTests
,
self
)
.
setUp
()
self
.
generate_data
()
self
.
generate_data
()
def
tearDown
(
self
):
self
.
destroy_data
()
def
serialize_enrollment
(
self
,
enrollment
):
def
serialize_enrollment
(
self
,
enrollment
):
return
{
return
{
'created'
:
enrollment
.
created
.
strftime
(
settings
.
DATETIME_FORMAT
),
'created'
:
enrollment
.
created
.
strftime
(
settings
.
DATETIME_FORMAT
),
...
@@ -606,8 +610,8 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
...
@@ -606,8 +610,8 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
# Create multiple objects here to test the grouping. Add a model with a different module_id to break up the
# Create multiple objects here to test the grouping. Add a model with a different module_id to break up the
# natural order and ensure the view properly sorts the objects before grouping.
# natural order and ensure the view properly sorts the objects before grouping.
module_id
=
'i4x://test/problem/1'
module_id
=
u
'i4x://test/problem/1'
alt_module_id
=
'i4x://test/problem/2'
alt_module_id
=
u
'i4x://test/problem/2'
created
=
datetime
.
datetime
.
utcnow
()
created
=
datetime
.
datetime
.
utcnow
()
alt_created
=
created
+
datetime
.
timedelta
(
seconds
=
2
)
alt_created
=
created
+
datetime
.
timedelta
(
seconds
=
2
)
date_time_format
=
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
date_time_format
=
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
...
@@ -624,21 +628,21 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
...
@@ -624,21 +628,21 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
'module_id'
:
module_id
,
'module_id'
:
module_id
,
'total_submissions'
:
150
,
'total_submissions'
:
150
,
'correct_submissions'
:
50
,
'correct_submissions'
:
50
,
'part_ids'
:
[
o1
.
part_id
,
o3
.
part_id
]
,
'part_ids'
:
unicode
([
o1
.
part_id
,
o3
.
part_id
])
,
'created'
:
alt_created
.
strftime
(
settings
.
DATETIME_FORMAT
)
'created'
:
alt_created
.
strftime
(
settings
.
DATETIME_FORMAT
)
},
},
{
{
'module_id'
:
alt_module_id
,
'module_id'
:
alt_module_id
,
'total_submissions'
:
100
,
'total_submissions'
:
100
,
'correct_submissions'
:
100
,
'correct_submissions'
:
100
,
'part_ids'
:
[
o2
.
part_id
]
,
'part_ids'
:
unicode
([
o2
.
part_id
])
,
'created'
:
created
.
strftime
(
settings
.
DATETIME_FORMAT
)
'created'
:
unicode
(
created
.
strftime
(
settings
.
DATETIME_FORMAT
)
)
}
}
]
]
response
=
self
.
_get_data
(
self
.
course_id
)
response
=
self
.
_get_data
(
self
.
course_id
)
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertListEqual
(
response
.
data
,
expected
)
self
.
assertListEqual
(
[
dict
(
d
)
for
d
in
response
.
data
]
,
expected
)
def
test_get_404
(
self
):
def
test_get_404
(
self
):
"""
"""
...
@@ -669,8 +673,8 @@ class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentica
...
@@ -669,8 +673,8 @@ class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentica
# Create multiple objects here to test the grouping. Add a model with a different module_id to break up the
# Create multiple objects here to test the grouping. Add a model with a different module_id to break up the
# natural order and ensure the view properly sorts the objects before grouping.
# natural order and ensure the view properly sorts the objects before grouping.
module_id
=
'i4x://test/problem/1'
module_id
=
u
'i4x://test/problem/1'
alt_module_id
=
'i4x://test/problem/2'
alt_module_id
=
u
'i4x://test/problem/2'
tags
=
{
tags
=
{
'difficulty'
:
[
'Easy'
,
'Medium'
,
'Hard'
],
'difficulty'
:
[
'Easy'
,
'Medium'
,
'Hard'
],
...
@@ -695,26 +699,26 @@ class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentica
...
@@ -695,26 +699,26 @@ class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentica
'module_id'
:
module_id
,
'module_id'
:
module_id
,
'total_submissions'
:
11
,
'total_submissions'
:
11
,
'correct_submissions'
:
4
,
'correct_submissions'
:
4
,
'tags'
:
{
'tags'
:
unicode
(
{
'difficulty'
:
'Easy'
,
u'difficulty'
:
u
'Easy'
,
'learning_outcome'
:
'Learned a few things'
,
u'learning_outcome'
:
u
'Learned a few things'
,
},
}
)
,
'created'
:
alt_created
.
strftime
(
settings
.
DATETIME_FORMAT
)
'created'
:
alt_created
.
strftime
(
settings
.
DATETIME_FORMAT
)
},
},
{
{
'module_id'
:
alt_module_id
,
'module_id'
:
alt_module_id
,
'total_submissions'
:
4
,
'total_submissions'
:
4
,
'correct_submissions'
:
0
,
'correct_submissions'
:
0
,
'tags'
:
{
'tags'
:
unicode
(
{
'learning_outcome'
:
'Learned everything'
,
u'learning_outcome'
:
u
'Learned everything'
,
},
}
)
,
'created'
:
created
.
strftime
(
settings
.
DATETIME_FORMAT
)
'created'
:
created
.
strftime
(
settings
.
DATETIME_FORMAT
)
}
}
]
]
response
=
self
.
_get_data
(
self
.
course_id
)
response
=
self
.
_get_data
(
self
.
course_id
)
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertListEqual
(
sorted
(
response
.
data
),
sorted
(
expected
))
self
.
assertListEqual
(
sorted
(
[
dict
(
d
)
for
d
in
response
.
data
]
),
sorted
(
expected
))
def
test_get_404
(
self
):
def
test_get_404
(
self
):
"""
"""
...
...
analytics_data_api/v0/tests/views/test_learners.py
View file @
e95f2178
...
@@ -205,8 +205,7 @@ class LearnerListTests(LearnerAPITestMixin, VerifyCourseIdMixin, TestCaseWithAut
...
@@ -205,8 +205,7 @@ class LearnerListTests(LearnerAPITestMixin, VerifyCourseIdMixin, TestCaseWithAut
returned.
returned.
"""
"""
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
payload
=
json
.
loads
(
response
.
content
)
returned_learners
=
json
.
loads
(
response
.
content
)[
'results'
]
returned_learners
=
payload
[
'results'
]
if
expected_learners
is
None
:
if
expected_learners
is
None
:
self
.
assertEqual
(
returned_learners
,
list
())
self
.
assertEqual
(
returned_learners
,
list
())
else
:
else
:
...
...
analytics_data_api/v0/views/__init__.py
View file @
e95f2178
...
@@ -12,7 +12,7 @@ class CourseViewMixin(object):
...
@@ -12,7 +12,7 @@ class CourseViewMixin(object):
course_id
=
None
course_id
=
None
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
self
.
course_id
=
self
.
kwargs
.
get
(
'course_id'
,
request
.
QUERY_PARAMS
.
get
(
'course_id'
,
None
))
self
.
course_id
=
self
.
kwargs
.
get
(
'course_id'
,
request
.
query_params
.
get
(
'course_id'
,
None
))
if
not
self
.
course_id
:
if
not
self
.
course_id
:
raise
CourseNotSpecifiedError
()
raise
CourseNotSpecifiedError
()
...
...
analytics_data_api/v0/views/courses.py
View file @
e95f2178
...
@@ -15,6 +15,8 @@ from analytics_data_api.constants import enrollment_modes
...
@@ -15,6 +15,8 @@ from analytics_data_api.constants import enrollment_modes
from
analytics_data_api.utils
import
dictfetchall
from
analytics_data_api.utils
import
dictfetchall
from
analytics_data_api.v0
import
models
,
serializers
from
analytics_data_api.v0
import
models
,
serializers
from
analytics_data_api.v0.views.utils
import
raise_404_if_none
class
BaseCourseView
(
generics
.
ListAPIView
):
class
BaseCourseView
(
generics
.
ListAPIView
):
start_date
=
None
start_date
=
None
...
@@ -25,8 +27,8 @@ class BaseCourseView(generics.ListAPIView):
...
@@ -25,8 +27,8 @@ class BaseCourseView(generics.ListAPIView):
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
self
.
course_id
=
self
.
kwargs
.
get
(
'course_id'
)
self
.
course_id
=
self
.
kwargs
.
get
(
'course_id'
)
start_date
=
request
.
QUERY_PARAMS
.
get
(
'start_date'
)
start_date
=
request
.
query_params
.
get
(
'start_date'
)
end_date
=
request
.
QUERY_PARAMS
.
get
(
'end_date'
)
end_date
=
request
.
query_params
.
get
(
'end_date'
)
timezone
=
utc
timezone
=
utc
self
.
start_date
=
self
.
parse_date
(
start_date
,
timezone
)
self
.
start_date
=
self
.
parse_date
(
start_date
,
timezone
)
...
@@ -46,6 +48,7 @@ class BaseCourseView(generics.ListAPIView):
...
@@ -46,6 +48,7 @@ class BaseCourseView(generics.ListAPIView):
def
apply_date_filtering
(
self
,
queryset
):
def
apply_date_filtering
(
self
,
queryset
):
raise
NotImplementedError
raise
NotImplementedError
@raise_404_if_none
def
get_queryset
(
self
):
def
get_queryset
(
self
):
queryset
=
self
.
model
.
objects
.
filter
(
course_id
=
self
.
course_id
)
queryset
=
self
.
model
.
objects
.
filter
(
course_id
=
self
.
course_id
)
queryset
=
self
.
apply_date_filtering
(
queryset
)
queryset
=
self
.
apply_date_filtering
(
queryset
)
...
@@ -232,14 +235,14 @@ class CourseActivityMostRecentWeekView(generics.RetrieveAPIView):
...
@@ -232,14 +235,14 @@ class CourseActivityMostRecentWeekView(generics.RetrieveAPIView):
""" Retrieve the activity type from the query string. """
""" Retrieve the activity type from the query string. """
# Support the old label param
# Support the old label param
activity_type
=
self
.
request
.
QUERY_PARAMS
.
get
(
'label'
,
None
)
activity_type
=
self
.
request
.
query_params
.
get
(
'label'
,
None
)
activity_type
=
activity_type
or
self
.
request
.
QUERY_PARAMS
.
get
(
'activity_type'
,
self
.
DEFAULT_ACTIVITY_TYPE
)
activity_type
=
activity_type
or
self
.
request
.
query_params
.
get
(
'activity_type'
,
self
.
DEFAULT_ACTIVITY_TYPE
)
activity_type
=
self
.
_format_activity_type
(
activity_type
)
activity_type
=
self
.
_format_activity_type
(
activity_type
)
return
activity_type
return
activity_type
def
get_object
(
self
,
queryset
=
None
):
def
get_object
(
self
):
"""Select the activity report for the given course and activity type."""
"""Select the activity report for the given course and activity type."""
warnings
.
warn
(
'CourseActivityMostRecentWeekView has been deprecated! Use CourseActivityWeeklyView instead.'
,
warnings
.
warn
(
'CourseActivityMostRecentWeekView has been deprecated! Use CourseActivityWeeklyView instead.'
,
...
@@ -400,7 +403,11 @@ class CourseEnrollmentByGenderView(BaseCourseEnrollmentView):
...
@@ -400,7 +403,11 @@ class CourseEnrollmentByGenderView(BaseCourseEnrollmentView):
item
=
{
item
=
{
u'course_id'
:
key
[
0
],
u'course_id'
:
key
[
0
],
u'date'
:
key
[
1
],
u'date'
:
key
[
1
],
u'created'
:
None
u'created'
:
None
,
u'male'
:
0
,
u'female'
:
0
,
u'other'
:
0
,
u'unknown'
:
0
}
}
for
enrollment
in
group
:
for
enrollment
in
group
:
...
@@ -633,6 +640,7 @@ class ProblemsListView(BaseCourseView):
...
@@ -633,6 +640,7 @@ class ProblemsListView(BaseCourseView):
serializer_class
=
serializers
.
ProblemSerializer
serializer_class
=
serializers
.
ProblemSerializer
allow_empty
=
False
allow_empty
=
False
@raise_404_if_none
def
get_queryset
(
self
):
def
get_queryset
(
self
):
# last_response_count is the number of submissions for the problem part and must
# last_response_count is the number of submissions for the problem part and must
# be divided by the number of problem parts to get the problem submission rather
# be divided by the number of problem parts to get the problem submission rather
...
@@ -709,6 +717,7 @@ class ProblemsAndTagsListView(BaseCourseView):
...
@@ -709,6 +717,7 @@ class ProblemsAndTagsListView(BaseCourseView):
allow_empty
=
False
allow_empty
=
False
model
=
models
.
ProblemsAndTags
model
=
models
.
ProblemsAndTags
@raise_404_if_none
def
get_queryset
(
self
):
def
get_queryset
(
self
):
queryset
=
self
.
model
.
objects
.
filter
(
course_id
=
self
.
course_id
)
queryset
=
self
.
model
.
objects
.
filter
(
course_id
=
self
.
course_id
)
items
=
queryset
.
all
()
items
=
queryset
.
all
()
...
...
analytics_data_api/v0/views/learners.py
View file @
e95f2178
...
@@ -5,9 +5,6 @@ import logging
...
@@ -5,9 +5,6 @@ import logging
from
rest_framework
import
generics
,
status
from
rest_framework
import
generics
,
status
from
analytics_data_api.constants
import
(
learner
)
from
analytics_data_api.v0.exceptions
import
(
from
analytics_data_api.v0.exceptions
import
(
LearnerEngagementTimelineNotFoundError
,
LearnerEngagementTimelineNotFoundError
,
LearnerNotFoundError
,
LearnerNotFoundError
,
...
@@ -21,7 +18,7 @@ from analytics_data_api.v0.models import (
...
@@ -21,7 +18,7 @@ from analytics_data_api.v0.models import (
)
)
from
analytics_data_api.v0.serializers
import
(
from
analytics_data_api.v0.serializers
import
(
CourseLearnerMetadataSerializer
,
CourseLearnerMetadataSerializer
,
E
lasticsearchDSLSearch
Serializer
,
E
dxPagination
Serializer
,
EngagementDaySerializer
,
EngagementDaySerializer
,
LastUpdatedSerializer
,
LastUpdatedSerializer
,
LearnerSerializer
,
LearnerSerializer
,
...
@@ -106,7 +103,7 @@ class LearnerView(LastUpdateMixin, CourseViewMixin, generics.RetrieveAPIView):
...
@@ -106,7 +103,7 @@ class LearnerView(LastUpdateMixin, CourseViewMixin, generics.RetrieveAPIView):
def
get_queryset
(
self
):
def
get_queryset
(
self
):
return
RosterEntry
.
get_course_user
(
self
.
course_id
,
self
.
username
)
return
RosterEntry
.
get_course_user
(
self
.
course_id
,
self
.
username
)
def
get_object
(
self
,
queryset
=
None
):
def
get_object
(
self
):
queryset
=
self
.
get_queryset
()
queryset
=
self
.
get_queryset
()
if
len
(
queryset
)
==
1
:
if
len
(
queryset
)
==
1
:
return
queryset
[
0
]
return
queryset
[
0
]
...
@@ -187,14 +184,12 @@ class LearnerListView(LastUpdateMixin, CourseViewMixin, generics.ListAPIView):
...
@@ -187,14 +184,12 @@ class LearnerListView(LastUpdateMixin, CourseViewMixin, generics.ListAPIView):
"""
"""
serializer_class
=
LearnerSerializer
serializer_class
=
LearnerSerializer
pagination_serializer_class
=
ElasticsearchDSLSearchSerializer
pagination_class
=
EdxPaginationSerializer
paginate_by_param
=
'page_size'
paginate_by
=
learner
.
LEARNER_API_DEFAULT_LIST_PAGE_SIZE
max_paginate_by
=
100
# TODO -- tweak during load testing
max_paginate_by
=
100
# TODO -- tweak during load testing
def
_validate_query_params
(
self
):
def
_validate_query_params
(
self
):
"""Validates various querystring parameters."""
"""Validates various querystring parameters."""
query_params
=
self
.
request
.
QUERY_PARAMS
query_params
=
self
.
request
.
query_params
page
=
query_params
.
get
(
'page'
)
page
=
query_params
.
get
(
'page'
)
if
page
:
if
page
:
try
:
try
:
...
@@ -222,8 +217,9 @@ class LearnerListView(LastUpdateMixin, CourseViewMixin, generics.ListAPIView):
...
@@ -222,8 +217,9 @@ class LearnerListView(LastUpdateMixin, CourseViewMixin, generics.ListAPIView):
"""
"""
response
=
super
(
LearnerListView
,
self
)
.
list
(
request
,
args
,
kwargs
)
response
=
super
(
LearnerListView
,
self
)
.
list
(
request
,
args
,
kwargs
)
last_updated
=
self
.
get_last_updated
()
last_updated
=
self
.
get_last_updated
()
for
result
in
response
.
data
[
'results'
]:
if
response
.
data
[
'results'
]
is
not
None
:
result
.
update
(
last_updated
)
for
result
in
response
.
data
[
'results'
]:
result
.
update
(
last_updated
)
return
response
return
response
def
get_queryset
(
self
):
def
get_queryset
(
self
):
...
@@ -232,7 +228,7 @@ class LearnerListView(LastUpdateMixin, CourseViewMixin, generics.ListAPIView):
...
@@ -232,7 +228,7 @@ class LearnerListView(LastUpdateMixin, CourseViewMixin, generics.ListAPIView):
as a an array of dicts with fields "learner" and "last_updated".
as a an array of dicts with fields "learner" and "last_updated".
"""
"""
self
.
_validate_query_params
()
self
.
_validate_query_params
()
query_params
=
self
.
request
.
QUERY_PARAMS
query_params
=
self
.
request
.
query_params
order_by
=
query_params
.
get
(
'order_by'
)
order_by
=
query_params
.
get
(
'order_by'
)
sort_order
=
query_params
.
get
(
'sort_order'
)
sort_order
=
query_params
.
get
(
'sort_order'
)
...
@@ -366,7 +362,7 @@ class CourseLearnerMetadata(CourseViewMixin, generics.RetrieveAPIView):
...
@@ -366,7 +362,7 @@ class CourseLearnerMetadata(CourseViewMixin, generics.RetrieveAPIView):
"""
"""
serializer_class
=
CourseLearnerMetadataSerializer
serializer_class
=
CourseLearnerMetadataSerializer
def
get_object
(
self
,
queryset
=
None
):
def
get_object
(
self
):
# Because we're serializing data from both Elasticsearch and MySQL into
# Because we're serializing data from both Elasticsearch and MySQL into
# the same JSON object, we have to pass both sources of data in a dict
# the same JSON object, we have to pass both sources of data in a dict
# to our custom course metadata serializer.
# to our custom course metadata serializer.
...
...
analytics_data_api/v0/views/problems.py
View file @
e95f2178
...
@@ -22,6 +22,8 @@ from analytics_data_api.v0.serializers import (
...
@@ -22,6 +22,8 @@ from analytics_data_api.v0.serializers import (
)
)
from
analytics_data_api.utils
import
matching_tuple
from
analytics_data_api.utils
import
matching_tuple
from
analytics_data_api.v0.views.utils
import
raise_404_if_none
class
ProblemResponseAnswerDistributionView
(
generics
.
ListAPIView
):
class
ProblemResponseAnswerDistributionView
(
generics
.
ListAPIView
):
"""
"""
...
@@ -98,6 +100,7 @@ class ProblemResponseAnswerDistributionView(generics.ListAPIView):
...
@@ -98,6 +100,7 @@ class ProblemResponseAnswerDistributionView(generics.ListAPIView):
return
consolidated_answers
return
consolidated_answers
@raise_404_if_none
def
get_queryset
(
self
):
def
get_queryset
(
self
):
"""Select all the answer distribution response having to do with this usage of the problem."""
"""Select all the answer distribution response having to do with this usage of the problem."""
problem_id
=
self
.
kwargs
.
get
(
'problem_id'
)
problem_id
=
self
.
kwargs
.
get
(
'problem_id'
)
...
@@ -142,6 +145,7 @@ class GradeDistributionView(generics.ListAPIView):
...
@@ -142,6 +145,7 @@ class GradeDistributionView(generics.ListAPIView):
serializer_class
=
GradeDistributionSerializer
serializer_class
=
GradeDistributionSerializer
allow_empty
=
False
allow_empty
=
False
@raise_404_if_none
def
get_queryset
(
self
):
def
get_queryset
(
self
):
"""Select all grade distributions for a particular module"""
"""Select all grade distributions for a particular module"""
problem_id
=
self
.
kwargs
.
get
(
'problem_id'
)
problem_id
=
self
.
kwargs
.
get
(
'problem_id'
)
...
@@ -170,6 +174,7 @@ class SequentialOpenDistributionView(generics.ListAPIView):
...
@@ -170,6 +174,7 @@ class SequentialOpenDistributionView(generics.ListAPIView):
serializer_class
=
SequentialOpenDistributionSerializer
serializer_class
=
SequentialOpenDistributionSerializer
allow_empty
=
False
allow_empty
=
False
@raise_404_if_none
def
get_queryset
(
self
):
def
get_queryset
(
self
):
"""Select the view count for a specific module"""
"""Select the view count for a specific module"""
module_id
=
self
.
kwargs
.
get
(
'module_id'
)
module_id
=
self
.
kwargs
.
get
(
'module_id'
)
...
...
analytics_data_api/v0/views/utils.py
View file @
e95f2178
"""Utilities for view-level API logic."""
"""Utilities for view-level API logic."""
from
django.http
import
Http404
def
split_query_argument
(
argument
):
def
split_query_argument
(
argument
):
...
@@ -10,3 +11,16 @@ def split_query_argument(argument):
...
@@ -10,3 +11,16 @@ def split_query_argument(argument):
return
argument
.
split
(
','
)
return
argument
.
split
(
','
)
else
:
else
:
return
None
return
None
def
raise_404_if_none
(
func
):
"""
Decorator for raiseing Http404 if function evaulation is falsey (e.g. empty queryset).
"""
def
func_wrapper
(
self
):
queryset
=
func
(
self
)
if
queryset
:
return
queryset
else
:
raise
Http404
return
func_wrapper
analytics_data_api/v0/views/videos.py
View file @
e95f2178
...
@@ -7,6 +7,8 @@ from rest_framework import generics
...
@@ -7,6 +7,8 @@ from rest_framework import generics
from
analytics_data_api.v0.models
import
VideoTimeline
from
analytics_data_api.v0.models
import
VideoTimeline
from
analytics_data_api.v0.serializers
import
VideoTimelineSerializer
from
analytics_data_api.v0.serializers
import
VideoTimelineSerializer
from
analytics_data_api.v0.views.utils
import
raise_404_if_none
class
VideoTimelineView
(
generics
.
ListAPIView
):
class
VideoTimelineView
(
generics
.
ListAPIView
):
"""
"""
...
@@ -30,6 +32,7 @@ class VideoTimelineView(generics.ListAPIView):
...
@@ -30,6 +32,7 @@ class VideoTimelineView(generics.ListAPIView):
serializer_class
=
VideoTimelineSerializer
serializer_class
=
VideoTimelineSerializer
allow_empty
=
False
allow_empty
=
False
@raise_404_if_none
def
get_queryset
(
self
):
def
get_queryset
(
self
):
"""Select the view count for a specific module"""
"""Select the view count for a specific module"""
video_id
=
self
.
kwargs
.
get
(
'video_id'
)
video_id
=
self
.
kwargs
.
get
(
'video_id'
)
...
...
requirements/base.txt
View file @
e95f2178
boto==2.22.1 # MIT
boto==2.22.1 # MIT
Django==1.8.14 # BSD License
Django==1.8.14 # BSD License
django-model-utils==2.2 # BSD
django-model-utils==2.2 # BSD
djangorestframework==
2.4.4
# BSD
djangorestframework==
3.4.6
# BSD
django-rest-swagger==0.2.8 # BSD
django-rest-swagger==0.2.8 # BSD
djangorestframework-csv==1.3.3 # BSD
djangorestframework-csv==1.3.3 # BSD
django-countries==3.2 # MIT
django-countries==3.2 # MIT
...
...
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