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
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
75 additions
and
45 deletions
+75
-45
analytics_data_api/v0/models.py
+4
-4
analytics_data_api/v0/serializers.py
+0
-0
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):
class
RosterUpdate
(
DocType
):
date
=
Date
()
date
=
Date
(
format
=
settings
.
DATE_FORMAT
)
# pylint: disable=old-style-class
class
Meta
:
...
...
@@ -265,8 +265,8 @@ class RosterEntry(DocType):
attempt_ratio_order
=
Integer
()
discussion_contributions
=
Integer
()
videos_watched
=
Integer
()
enrollment_date
=
Date
()
last_updated
=
Date
()
enrollment_date
=
Date
(
format
=
settings
.
DATE_FORMAT
)
last_updated
=
Date
(
format
=
settings
.
DATE_FORMAT
)
# pylint: disable=old-style-class
class
Meta
:
...
...
@@ -460,7 +460,7 @@ class ModuleEngagement(models.Model):
course_id
=
models
.
CharField
(
db_index
=
True
,
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"
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,
...
...
analytics_data_api/v0/serializers.py
View file @
e95f2178
This diff is collapsed.
Click to expand it.
analytics_data_api/v0/tests/views/test_courses.py
View file @
e95f2178
...
...
@@ -210,10 +210,11 @@ class CourseActivityLastWeekTest(DemoCourseMixin, TestCaseWithAuthentication):
@staticmethod
def
get_activity_record
(
**
kwargs
):
datetime_format
=
"
%
Y-
%
m-
%
dT
%
H:
%
M:
%
SZ"
default
=
{
'course_id'
:
DEMO_COURSE_ID
,
'interval_start'
:
datetime
.
datetime
(
2014
,
1
,
1
,
0
,
0
,
tzinfo
=
pytz
.
utc
),
'interval_end'
:
datetime
.
datetime
(
2014
,
1
,
8
,
0
,
0
,
tzinfo
=
pytz
.
utc
),
'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
)
.
strftime
(
datetime_format
)
,
'activity_type'
:
'any'
,
'count'
:
300
,
}
...
...
@@ -339,6 +340,9 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, Defau
super
(
CourseEnrollmentByGenderViewTests
,
self
)
.
setUp
()
self
.
generate_data
()
def
tearDown
(
self
):
self
.
destroy_data
()
def
serialize_enrollment
(
self
,
enrollment
):
return
{
'created'
:
enrollment
.
created
.
strftime
(
settings
.
DATETIME_FORMAT
),
...
...
@@ -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
# natural order and ensure the view properly sorts the objects before grouping.
module_id
=
'i4x://test/problem/1'
alt_module_id
=
'i4x://test/problem/2'
module_id
=
u
'i4x://test/problem/1'
alt_module_id
=
u
'i4x://test/problem/2'
created
=
datetime
.
datetime
.
utcnow
()
alt_created
=
created
+
datetime
.
timedelta
(
seconds
=
2
)
date_time_format
=
'
%
Y-
%
m-
%
d
%
H:
%
M:
%
S'
...
...
@@ -624,21 +628,21 @@ class CourseProblemsListViewTests(DemoCourseMixin, TestCaseWithAuthentication):
'module_id'
:
module_id
,
'total_submissions'
:
150
,
'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
)
},
{
'module_id'
:
alt_module_id
,
'total_submissions'
:
100
,
'correct_submissions'
:
100
,
'part_ids'
:
[
o2
.
part_id
]
,
'created'
:
created
.
strftime
(
settings
.
DATETIME_FORMAT
)
'part_ids'
:
unicode
([
o2
.
part_id
])
,
'created'
:
unicode
(
created
.
strftime
(
settings
.
DATETIME_FORMAT
)
)
}
]
response
=
self
.
_get_data
(
self
.
course_id
)
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
):
"""
...
...
@@ -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
# natural order and ensure the view properly sorts the objects before grouping.
module_id
=
'i4x://test/problem/1'
alt_module_id
=
'i4x://test/problem/2'
module_id
=
u
'i4x://test/problem/1'
alt_module_id
=
u
'i4x://test/problem/2'
tags
=
{
'difficulty'
:
[
'Easy'
,
'Medium'
,
'Hard'
],
...
...
@@ -695,26 +699,26 @@ class CourseProblemsAndTagsListViewTests(DemoCourseMixin, TestCaseWithAuthentica
'module_id'
:
module_id
,
'total_submissions'
:
11
,
'correct_submissions'
:
4
,
'tags'
:
{
'difficulty'
:
'Easy'
,
'learning_outcome'
:
'Learned a few things'
,
},
'tags'
:
unicode
(
{
u'difficulty'
:
u
'Easy'
,
u'learning_outcome'
:
u
'Learned a few things'
,
}
)
,
'created'
:
alt_created
.
strftime
(
settings
.
DATETIME_FORMAT
)
},
{
'module_id'
:
alt_module_id
,
'total_submissions'
:
4
,
'correct_submissions'
:
0
,
'tags'
:
{
'learning_outcome'
:
'Learned everything'
,
},
'tags'
:
unicode
(
{
u'learning_outcome'
:
u
'Learned everything'
,
}
)
,
'created'
:
created
.
strftime
(
settings
.
DATETIME_FORMAT
)
}
]
response
=
self
.
_get_data
(
self
.
course_id
)
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
):
"""
...
...
analytics_data_api/v0/tests/views/test_learners.py
View file @
e95f2178
...
...
@@ -205,8 +205,7 @@ class LearnerListTests(LearnerAPITestMixin, VerifyCourseIdMixin, TestCaseWithAut
returned.
"""
self
.
assertEqual
(
response
.
status_code
,
200
)
payload
=
json
.
loads
(
response
.
content
)
returned_learners
=
payload
[
'results'
]
returned_learners
=
json
.
loads
(
response
.
content
)[
'results'
]
if
expected_learners
is
None
:
self
.
assertEqual
(
returned_learners
,
list
())
else
:
...
...
analytics_data_api/v0/views/__init__.py
View file @
e95f2178
...
...
@@ -12,7 +12,7 @@ class CourseViewMixin(object):
course_id
=
None
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
:
raise
CourseNotSpecifiedError
()
...
...
analytics_data_api/v0/views/courses.py
View file @
e95f2178
...
...
@@ -15,6 +15,8 @@ from analytics_data_api.constants import enrollment_modes
from
analytics_data_api.utils
import
dictfetchall
from
analytics_data_api.v0
import
models
,
serializers
from
analytics_data_api.v0.views.utils
import
raise_404_if_none
class
BaseCourseView
(
generics
.
ListAPIView
):
start_date
=
None
...
...
@@ -25,8 +27,8 @@ class BaseCourseView(generics.ListAPIView):
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
self
.
course_id
=
self
.
kwargs
.
get
(
'course_id'
)
start_date
=
request
.
QUERY_PARAMS
.
get
(
'start_date'
)
end_date
=
request
.
QUERY_PARAMS
.
get
(
'end_date'
)
start_date
=
request
.
query_params
.
get
(
'start_date'
)
end_date
=
request
.
query_params
.
get
(
'end_date'
)
timezone
=
utc
self
.
start_date
=
self
.
parse_date
(
start_date
,
timezone
)
...
...
@@ -46,6 +48,7 @@ class BaseCourseView(generics.ListAPIView):
def
apply_date_filtering
(
self
,
queryset
):
raise
NotImplementedError
@raise_404_if_none
def
get_queryset
(
self
):
queryset
=
self
.
model
.
objects
.
filter
(
course_id
=
self
.
course_id
)
queryset
=
self
.
apply_date_filtering
(
queryset
)
...
...
@@ -232,14 +235,14 @@ class CourseActivityMostRecentWeekView(generics.RetrieveAPIView):
""" Retrieve the activity type from the query string. """
# 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
)
return
activity_type
def
get_object
(
self
,
queryset
=
None
):
def
get_object
(
self
):
"""Select the activity report for the given course and activity type."""
warnings
.
warn
(
'CourseActivityMostRecentWeekView has been deprecated! Use CourseActivityWeeklyView instead.'
,
...
...
@@ -400,7 +403,11 @@ class CourseEnrollmentByGenderView(BaseCourseEnrollmentView):
item
=
{
u'course_id'
:
key
[
0
],
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
:
...
...
@@ -633,6 +640,7 @@ class ProblemsListView(BaseCourseView):
serializer_class
=
serializers
.
ProblemSerializer
allow_empty
=
False
@raise_404_if_none
def
get_queryset
(
self
):
# 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
...
...
@@ -709,6 +717,7 @@ class ProblemsAndTagsListView(BaseCourseView):
allow_empty
=
False
model
=
models
.
ProblemsAndTags
@raise_404_if_none
def
get_queryset
(
self
):
queryset
=
self
.
model
.
objects
.
filter
(
course_id
=
self
.
course_id
)
items
=
queryset
.
all
()
...
...
analytics_data_api/v0/views/learners.py
View file @
e95f2178
...
...
@@ -5,9 +5,6 @@ import logging
from
rest_framework
import
generics
,
status
from
analytics_data_api.constants
import
(
learner
)
from
analytics_data_api.v0.exceptions
import
(
LearnerEngagementTimelineNotFoundError
,
LearnerNotFoundError
,
...
...
@@ -21,7 +18,7 @@ from analytics_data_api.v0.models import (
)
from
analytics_data_api.v0.serializers
import
(
CourseLearnerMetadataSerializer
,
E
lasticsearchDSLSearch
Serializer
,
E
dxPagination
Serializer
,
EngagementDaySerializer
,
LastUpdatedSerializer
,
LearnerSerializer
,
...
...
@@ -106,7 +103,7 @@ class LearnerView(LastUpdateMixin, CourseViewMixin, generics.RetrieveAPIView):
def
get_queryset
(
self
):
return
RosterEntry
.
get_course_user
(
self
.
course_id
,
self
.
username
)
def
get_object
(
self
,
queryset
=
None
):
def
get_object
(
self
):
queryset
=
self
.
get_queryset
()
if
len
(
queryset
)
==
1
:
return
queryset
[
0
]
...
...
@@ -187,14 +184,12 @@ class LearnerListView(LastUpdateMixin, CourseViewMixin, generics.ListAPIView):
"""
serializer_class
=
LearnerSerializer
pagination_serializer_class
=
ElasticsearchDSLSearchSerializer
paginate_by_param
=
'page_size'
paginate_by
=
learner
.
LEARNER_API_DEFAULT_LIST_PAGE_SIZE
pagination_class
=
EdxPaginationSerializer
max_paginate_by
=
100
# TODO -- tweak during load testing
def
_validate_query_params
(
self
):
"""Validates various querystring parameters."""
query_params
=
self
.
request
.
QUERY_PARAMS
query_params
=
self
.
request
.
query_params
page
=
query_params
.
get
(
'page'
)
if
page
:
try
:
...
...
@@ -222,8 +217,9 @@ class LearnerListView(LastUpdateMixin, CourseViewMixin, generics.ListAPIView):
"""
response
=
super
(
LearnerListView
,
self
)
.
list
(
request
,
args
,
kwargs
)
last_updated
=
self
.
get_last_updated
()
for
result
in
response
.
data
[
'results'
]:
result
.
update
(
last_updated
)
if
response
.
data
[
'results'
]
is
not
None
:
for
result
in
response
.
data
[
'results'
]:
result
.
update
(
last_updated
)
return
response
def
get_queryset
(
self
):
...
...
@@ -232,7 +228,7 @@ class LearnerListView(LastUpdateMixin, CourseViewMixin, generics.ListAPIView):
as a an array of dicts with fields "learner" and "last_updated".
"""
self
.
_validate_query_params
()
query_params
=
self
.
request
.
QUERY_PARAMS
query_params
=
self
.
request
.
query_params
order_by
=
query_params
.
get
(
'order_by'
)
sort_order
=
query_params
.
get
(
'sort_order'
)
...
...
@@ -366,7 +362,7 @@ class CourseLearnerMetadata(CourseViewMixin, generics.RetrieveAPIView):
"""
serializer_class
=
CourseLearnerMetadataSerializer
def
get_object
(
self
,
queryset
=
None
):
def
get_object
(
self
):
# 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
# 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 (
)
from
analytics_data_api.utils
import
matching_tuple
from
analytics_data_api.v0.views.utils
import
raise_404_if_none
class
ProblemResponseAnswerDistributionView
(
generics
.
ListAPIView
):
"""
...
...
@@ -98,6 +100,7 @@ class ProblemResponseAnswerDistributionView(generics.ListAPIView):
return
consolidated_answers
@raise_404_if_none
def
get_queryset
(
self
):
"""Select all the answer distribution response having to do with this usage of the problem."""
problem_id
=
self
.
kwargs
.
get
(
'problem_id'
)
...
...
@@ -142,6 +145,7 @@ class GradeDistributionView(generics.ListAPIView):
serializer_class
=
GradeDistributionSerializer
allow_empty
=
False
@raise_404_if_none
def
get_queryset
(
self
):
"""Select all grade distributions for a particular module"""
problem_id
=
self
.
kwargs
.
get
(
'problem_id'
)
...
...
@@ -170,6 +174,7 @@ class SequentialOpenDistributionView(generics.ListAPIView):
serializer_class
=
SequentialOpenDistributionSerializer
allow_empty
=
False
@raise_404_if_none
def
get_queryset
(
self
):
"""Select the view count for a specific module"""
module_id
=
self
.
kwargs
.
get
(
'module_id'
)
...
...
analytics_data_api/v0/views/utils.py
View file @
e95f2178
"""Utilities for view-level API logic."""
from
django.http
import
Http404
def
split_query_argument
(
argument
):
...
...
@@ -10,3 +11,16 @@ def split_query_argument(argument):
return
argument
.
split
(
','
)
else
:
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
from
analytics_data_api.v0.models
import
VideoTimeline
from
analytics_data_api.v0.serializers
import
VideoTimelineSerializer
from
analytics_data_api.v0.views.utils
import
raise_404_if_none
class
VideoTimelineView
(
generics
.
ListAPIView
):
"""
...
...
@@ -30,6 +32,7 @@ class VideoTimelineView(generics.ListAPIView):
serializer_class
=
VideoTimelineSerializer
allow_empty
=
False
@raise_404_if_none
def
get_queryset
(
self
):
"""Select the view count for a specific module"""
video_id
=
self
.
kwargs
.
get
(
'video_id'
)
...
...
requirements/base.txt
View file @
e95f2178
boto==2.22.1 # MIT
Django==1.8.14 # BSD License
django-model-utils==2.2 # BSD
djangorestframework==
2.4.4
# BSD
djangorestframework==
3.4.6
# BSD
django-rest-swagger==0.2.8 # BSD
djangorestframework-csv==1.3.3 # BSD
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