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
818b9230
Commit
818b9230
authored
Dec 22, 2015
by
Dennis Jen
Committed by
Daniel Friedman
Apr 11, 2016
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added last_updated field to learner endpoints.
parent
2e3a154d
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
113 additions
and
40 deletions
+113
-40
analytics_data_api/v0/models.py
+14
-1
analytics_data_api/v0/serializers.py
+13
-19
analytics_data_api/v0/tests/views/test_learners.py
+40
-14
analytics_data_api/v0/views/learners.py
+44
-6
analyticsdataserver/settings/base.py
+1
-0
analyticsdataserver/settings/test.py
+1
-0
No files found.
analytics_data_api/v0/models.py
View file @
818b9230
...
@@ -6,7 +6,6 @@ from django.db.models import Sum
...
@@ -6,7 +6,6 @@ from django.db.models import Sum
# some fields (e.g. Float, Integer) are dynamic and your IDE may highlight them as unavailable
# some fields (e.g. Float, Integer) are dynamic and your IDE may highlight them as unavailable
from
elasticsearch_dsl
import
Date
,
DocType
,
Float
,
Integer
,
Q
,
String
from
elasticsearch_dsl
import
Date
,
DocType
,
Float
,
Integer
,
Q
,
String
from
analytics_data_api.constants
import
country
,
engagement_entity_types
,
genders
,
learner
from
analytics_data_api.constants
import
country
,
engagement_entity_types
,
genders
,
learner
...
@@ -215,6 +214,20 @@ class Video(BaseVideo):
...
@@ -215,6 +214,20 @@ class Video(BaseVideo):
db_table
=
'video'
db_table
=
'video'
class
RosterUpdate
(
DocType
):
date
=
Date
()
# pylint: disable=old-style-class
class
Meta
:
index
=
settings
.
ELASTICSEARCH_LEARNERS_UPDATE_INDEX
doc_type
=
'marker'
@classmethod
def
get_last_updated
(
cls
):
return
cls
.
search
()
.
query
(
'term'
,
target_index
=
settings
.
ELASTICSEARCH_LEARNERS_INDEX
)
.
execute
()
class
RosterEntry
(
DocType
):
class
RosterEntry
(
DocType
):
course_id
=
String
()
course_id
=
String
()
...
...
analytics_data_api/v0/serializers.py
View file @
818b9230
...
@@ -317,17 +317,20 @@ class VideoTimelineSerializer(ModelSerializerWithCreatedField):
...
@@ -317,17 +317,20 @@ class VideoTimelineSerializer(ModelSerializerWithCreatedField):
)
)
class
LastUpdatedSerializer
(
serializers
.
Serializer
):
last_updated
=
serializers
.
DateField
(
source
=
'date'
,
format
=
settings
.
DATE_FORMAT
)
class
LearnerSerializer
(
serializers
.
Serializer
,
DefaultIfNoneMixin
):
class
LearnerSerializer
(
serializers
.
Serializer
,
DefaultIfNoneMixin
):
username
=
serializers
.
CharField
()
username
=
serializers
.
CharField
(
source
=
'username'
)
enrollment_mode
=
serializers
.
CharField
()
enrollment_mode
=
serializers
.
CharField
(
source
=
'enrollment_mode'
)
name
=
serializers
.
CharField
()
name
=
serializers
.
CharField
(
source
=
'name'
)
account_url
=
serializers
.
SerializerMethodField
(
'get_account_url'
)
account_url
=
serializers
.
SerializerMethodField
(
'get_account_url'
)
email
=
serializers
.
CharField
()
email
=
serializers
.
CharField
(
source
=
'email'
)
segments
=
serializers
.
Field
(
source
=
'segments'
)
segments
=
serializers
.
Field
(
source
=
'segments'
)
engagements
=
serializers
.
SerializerMethodField
(
'get_engagements'
)
engagements
=
serializers
.
SerializerMethodField
(
'get_engagements'
)
enrollment_date
=
serializers
.
DateField
(
format
=
settings
.
DATE_FORMAT
)
enrollment_date
=
serializers
.
DateField
(
source
=
'enrollment_date'
,
format
=
settings
.
DATE_FORMAT
)
last_updated
=
serializers
.
DateField
(
format
=
settings
.
DATE_FORMAT
)
cohort
=
serializers
.
CharField
(
source
=
'cohort'
)
cohort
=
serializers
.
CharField
()
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
:
...
@@ -426,20 +429,11 @@ class EnagementRangeMetricSerializer(serializers.Serializer):
...
@@ -426,20 +429,11 @@ class EnagementRangeMetricSerializer(serializers.Serializer):
class
CourseLearnerMetadataSerializer
(
serializers
.
Serializer
):
class
CourseLearnerMetadataSerializer
(
serializers
.
Serializer
):
enrollment_modes
=
serializers
.
SerializerMethodField
(
'get_
enrollment_modes'
)
enrollment_modes
=
serializers
.
Field
(
source
=
'es_data.
enrollment_modes'
)
segments
=
serializers
.
SerializerMethodField
(
'get_
segments'
)
segments
=
serializers
.
Field
(
source
=
'es_data.
segments'
)
cohorts
=
serializers
.
SerializerMethodField
(
'get_
cohorts'
)
cohorts
=
serializers
.
Field
(
source
=
'es_data.
cohorts'
)
engagement_ranges
=
serializers
.
SerializerMethodField
(
'get_engagement_ranges'
)
engagement_ranges
=
serializers
.
SerializerMethodField
(
'get_engagement_ranges'
)
def
get_enrollment_modes
(
self
,
obj
):
return
obj
[
'es_data'
][
'enrollment_modes'
]
def
get_segments
(
self
,
obj
):
return
obj
[
'es_data'
][
'segments'
]
def
get_cohorts
(
self
,
obj
):
return
obj
[
'es_data'
][
'cohorts'
]
def
get_engagement_ranges
(
self
,
obj
):
def
get_engagement_ranges
(
self
,
obj
):
query_set
=
obj
[
'engagement_ranges'
]
query_set
=
obj
[
'engagement_ranges'
]
engagement_ranges
=
{
engagement_ranges
=
{
...
...
analytics_data_api/v0/tests/views/test_learners.py
View file @
818b9230
...
@@ -25,10 +25,15 @@ class LearnerAPITestMixin(object):
...
@@ -25,10 +25,15 @@ class LearnerAPITestMixin(object):
"""Creates the index and defines a mapping."""
"""Creates the index and defines a mapping."""
super
(
LearnerAPITestMixin
,
self
)
.
setUp
()
super
(
LearnerAPITestMixin
,
self
)
.
setUp
()
self
.
_es
=
Elasticsearch
([
settings
.
ELASTICSEARCH_LEARNERS_HOST
])
self
.
_es
=
Elasticsearch
([
settings
.
ELASTICSEARCH_LEARNERS_HOST
])
# delete index if for some reason the index wasn't deleted in tearDown
if
self
.
_es
.
indices
.
exists
(
index
=
settings
.
ELASTICSEARCH_LEARNERS_INDEX
):
for
index
in
[
settings
.
ELASTICSEARCH_LEARNERS_INDEX
,
settings
.
ELASTICSEARCH_LEARNERS_UPDATE_INDEX
]:
self
.
_es
.
indices
.
delete
(
index
=
settings
.
ELASTICSEARCH_LEARNERS_INDEX
)
# ensure the test index is deleted
self
.
_es
.
indices
.
create
(
index
=
settings
.
ELASTICSEARCH_LEARNERS_INDEX
)
def
delete_index
(
to_delete
):
if
self
.
_es
.
indices
.
exists
(
index
=
to_delete
):
self
.
_es
.
indices
.
delete
(
index
=
to_delete
)
self
.
addCleanup
(
delete_index
,
index
)
self
.
_es
.
indices
.
create
(
index
=
index
)
self
.
_es
.
indices
.
put_mapping
(
self
.
_es
.
indices
.
put_mapping
(
index
=
settings
.
ELASTICSEARCH_LEARNERS_INDEX
,
index
=
settings
.
ELASTICSEARCH_LEARNERS_INDEX
,
doc_type
=
'roster_entry'
,
doc_type
=
'roster_entry'
,
...
@@ -76,18 +81,25 @@ class LearnerAPITestMixin(object):
...
@@ -76,18 +81,25 @@ class LearnerAPITestMixin(object):
'enrollment_date'
:
{
'enrollment_date'
:
{
'type'
:
'date'
,
'doc_values'
:
True
'type'
:
'date'
,
'doc_values'
:
True
},
},
'last_updated'
:
{
}
}
)
self
.
_es
.
indices
.
put_mapping
(
index
=
settings
.
ELASTICSEARCH_LEARNERS_UPDATE_INDEX
,
doc_type
=
'marker'
,
body
=
{
'properties'
:
{
'date'
:
{
'type'
:
'date'
,
'doc_values'
:
True
'type'
:
'date'
,
'doc_values'
:
True
},
},
'target_index'
:
{
'type'
:
'string'
},
}
}
}
}
)
)
def
tearDown
(
self
):
"""Remove the index after every test."""
super
(
LearnerAPITestMixin
,
self
)
.
tearDown
()
self
.
_es
.
indices
.
delete
(
index
=
settings
.
ELASTICSEARCH_LEARNERS_INDEX
)
def
_create_learner
(
def
_create_learner
(
self
,
self
,
username
,
username
,
...
@@ -104,7 +116,6 @@ class LearnerAPITestMixin(object):
...
@@ -104,7 +116,6 @@ class LearnerAPITestMixin(object):
attempt_ratio_order
=
0
,
attempt_ratio_order
=
0
,
videos_viewed
=
0
,
videos_viewed
=
0
,
enrollment_date
=
'2015-01-28'
,
enrollment_date
=
'2015-01-28'
,
last_updated
=
'2015-01-28'
):
):
"""Create a single learner roster entry in the elasticsearch index."""
"""Create a single learner roster entry in the elasticsearch index."""
self
.
_es
.
create
(
self
.
_es
.
create
(
...
@@ -125,7 +136,6 @@ class LearnerAPITestMixin(object):
...
@@ -125,7 +136,6 @@ class LearnerAPITestMixin(object):
'attempt_ratio_order'
:
attempt_ratio_order
,
'attempt_ratio_order'
:
attempt_ratio_order
,
'videos_viewed'
:
videos_viewed
,
'videos_viewed'
:
videos_viewed
,
'enrollment_date'
:
enrollment_date
,
'enrollment_date'
:
enrollment_date
,
'last_updated'
:
last_updated
}
}
)
)
...
@@ -141,6 +151,20 @@ class LearnerAPITestMixin(object):
...
@@ -141,6 +151,20 @@ class LearnerAPITestMixin(object):
self
.
_create_learner
(
**
learner
)
self
.
_create_learner
(
**
learner
)
self
.
_es
.
indices
.
refresh
(
index
=
settings
.
ELASTICSEARCH_LEARNERS_INDEX
)
self
.
_es
.
indices
.
refresh
(
index
=
settings
.
ELASTICSEARCH_LEARNERS_INDEX
)
def
create_update_index
(
self
,
date
=
None
):
"""
Created an index with the date of when the learner index was updated.
"""
self
.
_es
.
create
(
index
=
settings
.
ELASTICSEARCH_LEARNERS_UPDATE_INDEX
,
doc_type
=
'marker'
,
body
=
{
'date'
:
date
,
'target_index'
:
settings
.
ELASTICSEARCH_LEARNERS_INDEX
,
}
)
self
.
_es
.
indices
.
refresh
(
index
=
settings
.
ELASTICSEARCH_LEARNERS_UPDATE_INDEX
)
@ddt.ddt
@ddt.ddt
class
LearnerTests
(
VerifyCourseIdMixin
,
LearnerAPITestMixin
,
TestCaseWithAuthentication
):
class
LearnerTests
(
VerifyCourseIdMixin
,
LearnerAPITestMixin
,
TestCaseWithAuthentication
):
...
@@ -172,8 +196,8 @@ class LearnerTests(VerifyCourseIdMixin, LearnerAPITestMixin, TestCaseWithAuthent
...
@@ -172,8 +196,8 @@ class LearnerTests(VerifyCourseIdMixin, LearnerAPITestMixin, TestCaseWithAuthent
"problem_attempts_per_completed"
:
problem_attempts_per_completed
,
"problem_attempts_per_completed"
:
problem_attempts_per_completed
,
"attempt_ratio_order"
:
attempt_ratio_order
,
"attempt_ratio_order"
:
attempt_ratio_order
,
"enrollment_date"
:
enrollment_date
,
"enrollment_date"
:
enrollment_date
,
"last_updated"
:
last_updated
,
}])
}])
self
.
create_update_index
(
last_updated
)
response
=
self
.
authenticated_get
(
self
.
path_template
.
format
(
username
,
course_id
))
response
=
self
.
authenticated_get
(
self
.
path_template
.
format
(
username
,
course_id
))
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertEquals
(
response
.
status_code
,
200
)
...
@@ -227,6 +251,7 @@ class LearnerListTests(LearnerAPITestMixin, VerifyCourseIdMixin, TestCaseWithAut
...
@@ -227,6 +251,7 @@ class LearnerListTests(LearnerAPITestMixin, VerifyCourseIdMixin, TestCaseWithAut
def
setUp
(
self
):
def
setUp
(
self
):
super
(
LearnerListTests
,
self
)
.
setUp
()
super
(
LearnerListTests
,
self
)
.
setUp
()
self
.
course_id
=
'edX/DemoX/Demo_Course'
self
.
course_id
=
'edX/DemoX/Demo_Course'
self
.
create_update_index
(
'2015-09-28'
)
def
_get
(
self
,
course_id
,
**
query_params
):
def
_get
(
self
,
course_id
,
**
query_params
):
"""Helper to send a GET request to the API."""
"""Helper to send a GET request to the API."""
...
@@ -291,7 +316,8 @@ class LearnerListTests(LearnerAPITestMixin, VerifyCourseIdMixin, TestCaseWithAut
...
@@ -291,7 +316,8 @@ class LearnerListTests(LearnerAPITestMixin, VerifyCourseIdMixin, TestCaseWithAut
"videos_viewed"
:
6
,
"videos_viewed"
:
6
,
"discussions_contributed"
:
0
,
"discussions_contributed"
:
0
,
"problem_attempts_per_completed"
:
23.14
,
"problem_attempts_per_completed"
:
23.14
,
}
},
'last_updated'
:
'2015-09-28'
,
}])
}])
@ddt.data
(
@ddt.data
(
...
...
analytics_data_api/v0/views/learners.py
View file @
818b9230
"""
"""
API methods for module level data.
API methods for module level data.
"""
"""
import
logging
from
rest_framework
import
generics
,
status
from
rest_framework
import
generics
,
status
from
analytics_data_api.constants
import
(
from
analytics_data_api.constants
import
(
...
@@ -14,19 +16,38 @@ from analytics_data_api.v0.exceptions import (
...
@@ -14,19 +16,38 @@ from analytics_data_api.v0.exceptions import (
from
analytics_data_api.v0.models
import
(
from
analytics_data_api.v0.models
import
(
ModuleEngagement
,
ModuleEngagement
,
ModuleEngagementMetricRanges
,
ModuleEngagementMetricRanges
,
RosterEntry
RosterEntry
,
RosterUpdate
,
)
)
from
analytics_data_api.v0.serializers
import
(
from
analytics_data_api.v0.serializers
import
(
CourseLearnerMetadataSerializer
,
CourseLearnerMetadataSerializer
,
ElasticsearchDSLSearchSerializer
,
ElasticsearchDSLSearchSerializer
,
EngagementDaySerializer
,
EngagementDaySerializer
,
LastUpdatedSerializer
,
LearnerSerializer
,
LearnerSerializer
,
)
)
from
analytics_data_api.v0.views
import
CourseViewMixin
from
analytics_data_api.v0.views
import
CourseViewMixin
from
analytics_data_api.v0.views.utils
import
split_query_argument
from
analytics_data_api.v0.views.utils
import
split_query_argument
class
LearnerView
(
CourseViewMixin
,
generics
.
RetrieveAPIView
):
logger
=
logging
.
getLogger
(
__name__
)
class
LastUpdateMixin
(
object
):
@classmethod
def
get_last_updated
(
cls
):
""" Returns the serialized RosterUpdate last_updated field. """
roster_update
=
RosterUpdate
.
get_last_updated
()
last_updated
=
{
'date'
:
None
}
if
len
(
roster_update
)
==
1
:
last_updated
=
roster_update
[
0
]
else
:
logger
.
warn
(
'RosterUpdate not found.'
)
return
LastUpdatedSerializer
(
last_updated
)
.
data
class
LearnerView
(
LastUpdateMixin
,
CourseViewMixin
,
generics
.
RetrieveAPIView
):
"""
"""
Get data for a particular learner in a particular course.
Get data for a particular learner in a particular course.
...
@@ -73,6 +94,14 @@ class LearnerView(CourseViewMixin, generics.RetrieveAPIView):
...
@@ -73,6 +94,14 @@ class LearnerView(CourseViewMixin, generics.RetrieveAPIView):
self
.
username
=
self
.
kwargs
.
get
(
'username'
)
self
.
username
=
self
.
kwargs
.
get
(
'username'
)
return
super
(
LearnerView
,
self
)
.
get
(
request
,
*
args
,
**
kwargs
)
return
super
(
LearnerView
,
self
)
.
get
(
request
,
*
args
,
**
kwargs
)
def
retrieve
(
self
,
request
,
*
args
,
**
kwargs
):
"""
Adds the last_updated field to the result.
"""
response
=
super
(
LearnerView
,
self
)
.
retrieve
(
request
,
args
,
kwargs
)
response
.
data
.
update
(
self
.
get_last_updated
())
return
response
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
)
...
@@ -83,7 +112,7 @@ class LearnerView(CourseViewMixin, generics.RetrieveAPIView):
...
@@ -83,7 +112,7 @@ class LearnerView(CourseViewMixin, generics.RetrieveAPIView):
raise
LearnerNotFoundError
(
username
=
self
.
username
,
course_id
=
self
.
course_id
)
raise
LearnerNotFoundError
(
username
=
self
.
username
,
course_id
=
self
.
course_id
)
class
LearnerListView
(
CourseViewMixin
,
generics
.
ListAPIView
):
class
LearnerListView
(
LastUpdateMixin
,
CourseViewMixin
,
generics
.
ListAPIView
):
"""
"""
Get a paginated list of data for all learners in a course.
Get a paginated list of data for all learners in a course.
...
@@ -186,11 +215,20 @@ class LearnerListView(CourseViewMixin, generics.ListAPIView):
...
@@ -186,11 +215,20 @@ class LearnerListView(CourseViewMixin, generics.ListAPIView):
if
page_size
>
self
.
max_paginate_by
or
page_size
<
1
:
if
page_size
>
self
.
max_paginate_by
or
page_size
<
1
:
raise
ParameterValueError
(
'Page size must be in the range [1, {}]'
.
format
(
self
.
max_paginate_by
))
raise
ParameterValueError
(
'Page size must be in the range [1, {}]'
.
format
(
self
.
max_paginate_by
))
def
list
(
self
,
request
,
*
args
,
**
kwargs
):
"""
Adds the last_updated field to the results.
"""
response
=
super
(
LearnerListView
,
self
)
.
list
(
request
,
args
,
kwargs
)
last_updated
=
self
.
get_last_updated
()
for
result
in
response
.
data
[
'results'
]:
result
.
update
(
last_updated
)
return
response
def
get_queryset
(
self
):
def
get_queryset
(
self
):
"""
"""
Fetches the user list from elasticsearch. Note that an
Fetches the user list and last updated from elasticsearch returned returned
elasticsearch_dsl `Search` object is returned, not an actual
as a an array of dicts with fields "learner" and "last_updated".
queryset.
"""
"""
self
.
_validate_query_params
()
self
.
_validate_query_params
()
query_params
=
self
.
request
.
QUERY_PARAMS
query_params
=
self
.
request
.
QUERY_PARAMS
...
...
analyticsdataserver/settings/base.py
View file @
818b9230
...
@@ -54,6 +54,7 @@ DATABASES = {
...
@@ -54,6 +54,7 @@ DATABASES = {
########## ELASTICSEARCH CONFIGURATION
########## ELASTICSEARCH CONFIGURATION
ELASTICSEARCH_LEARNERS_HOST
=
environ
.
get
(
'ELASTICSEARCH_LEARNERS_HOST'
,
None
)
ELASTICSEARCH_LEARNERS_HOST
=
environ
.
get
(
'ELASTICSEARCH_LEARNERS_HOST'
,
None
)
ELASTICSEARCH_LEARNERS_INDEX
=
environ
.
get
(
'ELASTICSEARCH_LEARNERS_INDEX'
,
None
)
ELASTICSEARCH_LEARNERS_INDEX
=
environ
.
get
(
'ELASTICSEARCH_LEARNERS_INDEX'
,
None
)
ELASTICSEARCH_LEARNERS_UPDATE_INDEX
=
environ
.
get
(
'ELASTICSEARCH_LEARNERS_UPDATE_INDEX'
,
None
)
########## END ELASTICSEARCH CONFIGURATION
########## END ELASTICSEARCH CONFIGURATION
########## GENERAL CONFIGURATION
########## GENERAL CONFIGURATION
...
...
analyticsdataserver/settings/test.py
View file @
818b9230
...
@@ -25,3 +25,4 @@ TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
...
@@ -25,3 +25,4 @@ TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
# Default elasticsearch port when running locally
# Default elasticsearch port when running locally
ELASTICSEARCH_LEARNERS_HOST
=
'http://localhost:9200/'
ELASTICSEARCH_LEARNERS_HOST
=
'http://localhost:9200/'
ELASTICSEARCH_LEARNERS_INDEX
=
'roster_test'
ELASTICSEARCH_LEARNERS_INDEX
=
'roster_test'
ELASTICSEARCH_LEARNERS_UPDATE_INDEX
=
'index_update_test'
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