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
e9c0c069
Commit
e9c0c069
authored
Dec 14, 2015
by
Dennis Jen
Committed by
Daniel Friedman
Apr 11, 2016
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added fields for learner endpoints.
parent
9fb50c73
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
177 additions
and
78 deletions
+177
-78
analytics_data_api/v0/models.py
+31
-10
analytics_data_api/v0/serializers.py
+18
-15
analytics_data_api/v0/tests/views/test_learners.py
+117
-50
analytics_data_api/v0/views/learners.py
+11
-3
No files found.
analytics_data_api/v0/models.py
View file @
e9c0c069
...
...
@@ -3,7 +3,9 @@ from itertools import groupby
from
django.conf
import
settings
from
django.db
import
models
from
django.db.models
import
Sum
from
elasticsearch_dsl
import
DocType
,
Q
# 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
analytics_data_api.constants
import
country
,
engagement_entity_types
,
genders
,
learner
...
...
@@ -214,6 +216,27 @@ class Video(BaseVideo):
class
RosterEntry
(
DocType
):
course_id
=
String
()
username
=
String
()
name
=
String
()
email
=
String
()
enrollment_mode
=
String
()
cohort
=
String
()
segments
=
String
(
fields
=
{
'raw'
:
String
()})
problems_attempted
=
Integer
()
problems_completed
=
Integer
()
problem_attempts_per_completed
=
Float
()
# Useful for ordering problem_attempts_per_completed (because results can include null, which is
# different from zero). attempt_ratio_order is equal to the number of problem attempts if
# problem_attempts_per_completed is > 1 and set to -problem_attempts if
# problem_attempts_per_completed = 1.
attempt_ratio_order
=
Integer
()
discussions_contributed
=
Integer
()
videos_watched
=
Integer
()
enrollment_date
=
Date
()
last_updated
=
Date
()
# pylint: disable=old-style-class
class
Meta
:
index
=
settings
.
ELASTICSEARCH_LEARNERS_INDEX
...
...
@@ -230,8 +253,7 @@ class RosterEntry(DocType):
course_id
,
segments
=
None
,
ignore_segments
=
None
,
# TODO: enable during https://openedx.atlassian.net/browse/AN-6319
# cohort=None,
cohort
=
None
,
enrollment_mode
=
None
,
text_search
=
None
,
order_by
=
'username'
,
...
...
@@ -251,13 +273,14 @@ class RosterEntry(DocType):
segment
=
segment
,
segments
=
', '
.
join
(
learner
.
SEGMENTS
)
))
order_by_options
=
(
'username'
,
'email'
,
'discussions_contributed'
,
'problems_attempted'
,
'problems_completed'
,
'videos_viewed'
'username'
,
'email'
,
'discussions_contributed'
,
'problems_attempted'
,
'problems_completed'
,
'attempt_ratio_order'
,
'videos_viewed'
)
sort_order_options
=
(
'asc'
,
'desc'
)
if
order_by
not
in
order_by_options
:
raise
ValueError
(
"order_by value '{order_by}' must be one of: ({order_by_options})"
.
format
(
order_by
=
order_by
,
order_by_options
=
', '
.
join
(
order_by_options
)
))
sort_order_options
=
(
'asc'
,
'desc'
)
if
sort_order
not
in
sort_order_options
:
raise
ValueError
(
"sort_order value '{sort_order}' must be one of: ({sort_order_options})"
.
format
(
sort_order
=
sort_order
,
sort_order_options
=
', '
.
join
(
sort_order_options
)
...
...
@@ -272,9 +295,8 @@ class RosterEntry(DocType):
elif
ignore_segments
:
for
segment
in
ignore_segments
:
search
=
search
.
query
(
~
Q
(
'term'
,
segments
=
segment
))
# TODO: enable during https://openedx.atlassian.net/browse/AN-6319
# if cohort:
# search = search.query('term', cohort=cohort)
if
cohort
:
search
=
search
.
query
(
'term'
,
cohort
=
cohort
)
if
enrollment_mode
:
search
=
search
.
query
(
'term'
,
enrollment_mode
=
enrollment_mode
)
if
text_search
:
...
...
@@ -309,8 +331,7 @@ class RosterEntry(DocType):
search
.
query
=
Q
(
'bool'
,
must
=
[
Q
(
'term'
,
course_id
=
course_id
)])
search
.
aggs
.
bucket
(
'enrollment_modes'
,
'terms'
,
field
=
'enrollment_mode'
)
search
.
aggs
.
bucket
(
'segments'
,
'terms'
,
field
=
'segments'
)
# TODO: enable during https://openedx.atlassian.net/browse/AN-6319
# search.aggs.bucket('group_by_cohorts', 'terms', field='cohort')
search
.
aggs
.
bucket
(
'cohorts'
,
'terms'
,
field
=
'cohort'
)
response
=
search
.
execute
()
# Build up the map of aggregation name to count
aggregations
=
{
...
...
analytics_data_api/v0/serializers.py
View file @
e9c0c069
...
...
@@ -317,7 +317,7 @@ class VideoTimelineSerializer(ModelSerializerWithCreatedField):
)
class
LearnerSerializer
(
serializers
.
Serializer
):
class
LearnerSerializer
(
serializers
.
Serializer
,
DefaultIfNoneMixin
):
username
=
serializers
.
CharField
()
enrollment_mode
=
serializers
.
CharField
()
name
=
serializers
.
CharField
()
...
...
@@ -325,11 +325,9 @@ class LearnerSerializer(serializers.Serializer):
email
=
serializers
.
CharField
()
segments
=
serializers
.
Field
(
source
=
'segments'
)
engagements
=
serializers
.
SerializerMethodField
(
'get_engagements'
)
# TODO: add these back in when the index returns them
# enrollment_date = serializers.DateField(format=settings.DATE_FORMAT, allow_empty=True)
# last_updated = serializers.DateField(format=settings.DATE_FORMAT)
# cohort = serializers.CharField(allow_none=True)
enrollment_date
=
serializers
.
DateField
(
format
=
settings
.
DATE_FORMAT
)
last_updated
=
serializers
.
DateField
(
format
=
settings
.
DATE_FORMAT
)
cohort
=
serializers
.
CharField
()
def
get_account_url
(
self
,
obj
):
if
settings
.
LMS_USER_ACCOUNT_BASE_URL
:
...
...
@@ -342,10 +340,17 @@ class LearnerSerializer(serializers.Serializer):
Add the engagement totals.
"""
engagements
=
{}
for
entity_type
in
engagement_entity_types
.
AGGREGATE_TYPES
:
for
event
in
engagement_events
.
EVENTS
[
entity_type
]:
metric
=
'{0}_{1}'
.
format
(
entity_type
,
event
)
engagements
[
metric
]
=
getattr
(
obj
,
metric
,
0
)
# fill in these fields will 0 if values not returned/found
default_if_none_fields
=
[
'discussions_contributed'
,
'problems_attempted'
,
'problems_completed'
,
'videos_viewed'
]
for
field
in
default_if_none_fields
:
engagements
[
field
]
=
self
.
default_if_none
(
getattr
(
obj
,
field
,
None
),
0
)
# preserve null values for problem attempts per completed
no_default_field
=
'problem_attempts_per_completed'
engagements
[
no_default_field
]
=
getattr
(
obj
,
no_default_field
,
None
)
return
engagements
...
...
@@ -424,8 +429,7 @@ class EnagementRangeMetricSerializer(serializers.Serializer):
class
CourseLearnerMetadataSerializer
(
serializers
.
Serializer
):
enrollment_modes
=
serializers
.
SerializerMethodField
(
'get_enrollment_modes'
)
segments
=
serializers
.
SerializerMethodField
(
'get_segments'
)
# TODO: enable during https://openedx.atlassian.net/browse/AN-6319
# cohorts = serializers.SerializerMethodField('get_cohorts')
cohorts
=
serializers
.
SerializerMethodField
(
'get_cohorts'
)
engagement_ranges
=
serializers
.
SerializerMethodField
(
'get_engagement_ranges'
)
def
get_enrollment_modes
(
self
,
obj
):
...
...
@@ -434,9 +438,8 @@ class CourseLearnerMetadataSerializer(serializers.Serializer):
def
get_segments
(
self
,
obj
):
return
obj
[
'es_data'
][
'segments'
]
# TODO: enable during https://openedx.atlassian.net/browse/AN-6319
# def get_cohorts(self, obj):
# return obj['es_data']['cohorts']
def
get_cohorts
(
self
,
obj
):
return
obj
[
'es_data'
][
'cohorts'
]
def
get_engagement_ranges
(
self
,
obj
):
query_set
=
obj
[
'engagement_ranges'
]
...
...
analytics_data_api/v0/tests/views/test_learners.py
View file @
e9c0c069
...
...
@@ -25,6 +25,9 @@ class LearnerAPITestMixin(object):
"""Creates the index and defines a mapping."""
super
(
LearnerAPITestMixin
,
self
)
.
setUp
()
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
):
self
.
_es
.
indices
.
delete
(
index
=
settings
.
ELASTICSEARCH_LEARNERS_INDEX
)
self
.
_es
.
indices
.
create
(
index
=
settings
.
ELASTICSEARCH_LEARNERS_INDEX
)
self
.
_es
.
indices
.
put_mapping
(
index
=
settings
.
ELASTICSEARCH_LEARNERS_INDEX
,
...
...
@@ -61,12 +64,21 @@ class LearnerAPITestMixin(object):
'problems_completed'
:
{
'type'
:
'integer'
,
'doc_values'
:
True
},
'
attempts_per_problem
_completed'
:
{
'
problem_attempts_per
_completed'
:
{
'type'
:
'float'
,
'doc_values'
:
True
},
'attempt_ratio_order'
:
{
'type'
:
'integer'
,
'doc_values'
:
True
},
'videos_viewed'
:
{
'type'
:
'integer'
,
'doc_values'
:
True
}
},
'enrollment_date'
:
{
'type'
:
'date'
,
'doc_values'
:
True
},
'last_updated'
:
{
'type'
:
'date'
,
'doc_values'
:
True
},
}
}
)
...
...
@@ -84,12 +96,15 @@ class LearnerAPITestMixin(object):
email
=
None
,
enrollment_mode
=
'honor'
,
segments
=
None
,
cohort
=
''
,
cohort
=
'
Team edX
'
,
discussions_contributed
=
0
,
problems_attempted
=
0
,
problems_completed
=
0
,
attempts_per_problem_completed
=
0
,
videos_viewed
=
0
problem_attempts_per_completed
=
None
,
attempt_ratio_order
=
0
,
videos_viewed
=
0
,
enrollment_date
=
'2015-01-28'
,
last_updated
=
'2015-01-28'
):
"""Create a single learner roster entry in the elasticsearch index."""
self
.
_es
.
create
(
...
...
@@ -106,8 +121,11 @@ class LearnerAPITestMixin(object):
'discussions_contributed'
:
discussions_contributed
,
'problems_attempted'
:
problems_attempted
,
'problems_completed'
:
problems_completed
,
'attempts_per_problem_completed'
:
attempts_per_problem_completed
,
'videos_viewed'
:
videos_viewed
'problem_attempts_per_completed'
:
problem_attempts_per_completed
,
'attempt_ratio_order'
:
attempt_ratio_order
,
'videos_viewed'
:
videos_viewed
,
'enrollment_date'
:
enrollment_date
,
'last_updated'
:
last_updated
}
)
...
...
@@ -124,42 +142,59 @@ class LearnerAPITestMixin(object):
self
.
_es
.
indices
.
refresh
(
index
=
settings
.
ELASTICSEARCH_LEARNERS_INDEX
)
@ddt.ddt
class
LearnerTests
(
VerifyCourseIdMixin
,
LearnerAPITestMixin
,
TestCaseWithAuthentication
):
"""Tests for the single learner endpoint."""
path_template
=
'/api/v0/learners/{}/?course_id={}'
def
setUp
(
self
):
super
(
LearnerTests
,
self
)
.
setUp
()
@ddt.data
(
(
'ed_xavier'
,
'Edward Xavier'
,
'edX/DemoX/Demo_Course'
,
'honor'
,
[
'has_potential'
],
'Team edX'
,
43
,
3
,
6
,
0
,
8.4
,
2
,
'2015-04-24'
,
'2015-08-05'
),
(
'ed_xavier'
,
'Edward Xavier'
,
'edX/DemoX/Demo_Course'
,
'verified'
,
None
,
None
,
None
,
None
,
None
,
None
,
None
,
None
,
None
,
None
),
)
@ddt.unpack
def
test_get_user
(
self
,
username
,
name
,
course_id
,
enrollment_mode
,
segments
,
cohort
,
problems_attempted
,
problems_completed
,
videos_viewed
,
discussions_contributed
,
problem_attempts_per_completed
,
attempt_ratio_order
,
enrollment_date
,
last_updated
):
self
.
create_learners
([{
"username"
:
"ed_xavier"
,
"name"
:
"Edward Xavier"
,
"course_id"
:
"edX/DemoX/Demo_Course"
,
"segments"
:
[
"has_potential"
],
"problems_attempted"
:
43
,
"problems_completed"
:
3
,
"videos_viewed"
:
6
,
"discussions_contributed"
:
0
"username"
:
username
,
"name"
:
name
,
"course_id"
:
course_id
,
"enrollment_mode"
:
enrollment_mode
,
"segments"
:
segments
,
"cohort"
:
cohort
,
"problems_attempted"
:
problems_attempted
,
"problems_completed"
:
problems_completed
,
"videos_viewed"
:
videos_viewed
,
"discussions_contributed"
:
discussions_contributed
,
"problem_attempts_per_completed"
:
problem_attempts_per_completed
,
"attempt_ratio_order"
:
attempt_ratio_order
,
"enrollment_date"
:
enrollment_date
,
"last_updated"
:
last_updated
,
}])
def
test_get_user
(
self
):
user_name
=
'ed_xavier'
course_id
=
'edX/DemoX/Demo_Course'
response
=
self
.
authenticated_get
(
self
.
path_template
.
format
(
user_name
,
course_id
))
response
=
self
.
authenticated_get
(
self
.
path_template
.
format
(
username
,
course_id
))
self
.
assertEquals
(
response
.
status_code
,
200
)
expected
=
{
"username"
:
"ed_xavier"
,
"enrollment_mode"
:
"honor"
,
"name"
:
"Edward Xavier"
,
"email"
:
"ed_xavier@example.com"
,
"account_url"
:
"http://lms-host/ed_xavier"
,
"segments"
:
[
"has_potential"
],
"username"
:
username
,
"enrollment_mode"
:
enrollment_mode
,
"name"
:
name
,
"email"
:
"{}@example.com"
.
format
(
username
),
"account_url"
:
"http://lms-host/{}"
.
format
(
username
),
"segments"
:
segments
or
[],
"cohort"
:
cohort
,
"engagements"
:
{
"problems_attempted"
:
43
,
"problems_completed"
:
3
,
"videos_viewed"
:
6
,
"discussions_contributed"
:
0
"problems_attempted"
:
problems_attempted
or
0
,
"problems_completed"
:
problems_completed
or
0
,
"videos_viewed"
:
videos_viewed
or
0
,
"discussions_contributed"
:
discussions_contributed
or
0
,
"problem_attempts_per_completed"
:
problem_attempts_per_completed
,
},
"enrollment_date"
:
enrollment_date
,
"last_updated"
:
last_updated
,
}
self
.
assertDictEqual
(
expected
,
response
.
data
)
...
...
@@ -237,25 +272,25 @@ class LearnerListTests(LearnerAPITestMixin, VerifyCourseIdMixin, TestCaseWithAut
'course_id'
:
self
.
course_id
,
'enrollment_mode'
:
'honor'
,
'segments'
:
[
'a'
,
'b'
],
# TODO: enable during https://openedx.atlassian.net/browse/AN-6319
# 'cohort': 'alpha',
'cohort'
:
'alpha'
,
"problems_attempted"
:
43
,
"problems_completed"
:
3
,
"videos_viewed"
:
6
,
"discussions_contributed"
:
0
"discussions_contributed"
:
0
,
"problem_attempts_per_completed"
:
23.14
,
}])
response
=
self
.
_get
(
self
.
course_id
)
self
.
assert_learners_returned
(
response
,
[{
'username'
:
'user_1'
,
'enrollment_mode'
:
'honor'
,
'segments'
:
[
'a'
,
'b'
],
# TODO: enable during https://openedx.atlassian.net/browse/AN-6319
# 'cohort': 'alpha',
'cohort'
:
'alpha'
,
"engagements"
:
{
"problems_attempted"
:
43
,
"problems_completed"
:
3
,
"videos_viewed"
:
6
,
"discussions_contributed"
:
0
"discussions_contributed"
:
0
,
"problem_attempts_per_completed"
:
23.14
,
}
}])
...
...
@@ -272,10 +307,9 @@ class LearnerListTests(LearnerAPITestMixin, VerifyCourseIdMixin, TestCaseWithAut
(
'segments'
,
[
'highly_engaged'
,
'struggling'
],
'ignore_segments'
,
'highly_engaged,struggling'
,
False
),
(
'segments'
,
[
'highly_engaged'
,
'struggling'
],
'ignore_segments'
,
''
,
True
),
(
'segments'
,
[
'highly_engaged'
,
'struggling'
],
'ignore_segments'
,
'disengaging'
,
True
),
# TODO: enable during https://openedx.atlassian.net/browse/AN-6319
# ('cohort', 'a', 'cohort', 'a', True),
# ('cohort', 'a', 'cohort', '', True),
# ('cohort', 'a', 'cohort', 'b', False),
(
'cohort'
,
'a'
,
'cohort'
,
'a'
,
True
),
(
'cohort'
,
'a'
,
'cohort'
,
''
,
True
),
(
'cohort'
,
'a'
,
'cohort'
,
'b'
,
False
),
(
'enrollment_mode'
,
'a'
,
'enrollment_mode'
,
'a'
,
True
),
(
'enrollment_mode'
,
'a'
,
'enrollment_mode'
,
''
,
True
),
(
'enrollment_mode'
,
'a'
,
'enrollment_mode'
,
'b'
,
False
),
...
...
@@ -347,6 +381,18 @@ class LearnerListTests(LearnerAPITestMixin, VerifyCourseIdMixin, TestCaseWithAut
[{
'username'
:
'a'
,
'videos_viewed'
:
0
},
{
'username'
:
'b'
,
'videos_viewed'
:
1
}],
'videos_viewed'
,
'desc'
,
[{
'username'
:
'b'
},
{
'username'
:
'a'
}]
),
(
[{
'username'
:
'a'
,
'problem_attempts_per_completed'
:
None
,
'attempt_ratio_order'
:
-
1
},
{
'username'
:
'b'
,
'problem_attempts_per_completed'
:
None
,
'attempt_ratio_order'
:
0
},
{
'username'
:
'c'
,
'problem_attempts_per_completed'
:
None
,
'attempt_ratio_order'
:
1
}],
'problem_attempts_per_completed'
,
'asc'
,
[{
'username'
:
'a'
},
{
'username'
:
'b'
},
{
'username'
:
'c'
}]
),
(
[{
'username'
:
'a'
,
'problem_attempts_per_completed'
:
None
,
'attempt_ratio_order'
:
-
1
},
{
'username'
:
'b'
,
'problem_attempts_per_completed'
:
0.0
,
'attempt_ratio_order'
:
0
},
{
'username'
:
'c'
,
'problem_attempts_per_completed'
:
123.64
,
'attempt_ratio_order'
:
1
}],
'problem_attempts_per_completed'
,
'desc'
,
[{
'username'
:
'c'
},
{
'username'
:
'b'
},
{
'username'
:
'a'
}]
),
)
@ddt.unpack
def
test_sort
(
self
,
learners
,
order_by
,
sort_order
,
expected_users
):
...
...
@@ -398,7 +444,6 @@ class LearnerListTests(LearnerAPITestMixin, VerifyCourseIdMixin, TestCaseWithAut
self
.
assert_learners_returned
(
response
,
[{
'username'
:
'e'
}])
# Error cases
@ddt.data
(
({},
'course_not_specified'
),
({
'course_id'
:
''
},
'course_not_specified'
),
...
...
@@ -433,10 +478,11 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
"""Helper to send a GET request to the API."""
return
self
.
authenticated_get
(
'/api/v0/course_learner_metadata/{}/'
.
format
(
course_id
))
def
get_expected_json
(
self
,
segments
,
enrollment_modes
):
def
get_expected_json
(
self
,
segments
,
enrollment_modes
,
cohorts
):
expected_json
=
self
.
_get_full_engagement_ranges
()
expected_json
[
'segments'
]
=
segments
expected_json
[
'enrollment_modes'
]
=
enrollment_modes
expected_json
[
'cohorts'
]
=
cohorts
return
expected_json
def
assert_response_matches
(
self
,
response
,
expected_status_code
,
expected_data
):
...
...
@@ -471,7 +517,9 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
expected_segments
=
{
"highly_engaged"
:
0
,
"disengaging"
:
0
,
"struggling"
:
0
,
"inactive"
:
0
,
"unenrolled"
:
0
}
expected_segments
.
update
(
segments
)
expected
=
self
.
get_expected_json
(
segments
=
expected_segments
,
enrollment_modes
=
{
'honor'
:
len
(
learners
)}
if
learners
else
{}
segments
=
expected_segments
,
enrollment_modes
=
{
'honor'
:
len
(
learners
)}
if
learners
else
{},
cohorts
=
{
'Team edX'
:
len
(
learners
)}
if
learners
else
{},
)
self
.
assert_response_matches
(
self
.
_get
(
self
.
course_id
),
200
,
expected
)
...
...
@@ -485,7 +533,8 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
])
expected
=
self
.
get_expected_json
(
segments
=
{
'disengaging'
:
2
,
'struggling'
:
1
,
'highly_engaged'
:
0
,
'inactive'
:
0
,
'unenrolled'
:
0
},
enrollment_modes
=
{
'honor'
:
2
}
enrollment_modes
=
{
'honor'
:
2
},
cohorts
=
{
'Team edX'
:
2
},
)
self
.
assert_response_matches
(
self
.
_get
(
self
.
course_id
),
200
,
expected
)
...
...
@@ -510,7 +559,29 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
expected_enrollment_modes
[
enrollment_mode
]
=
count
expected
=
self
.
get_expected_json
(
segments
=
{
'disengaging'
:
0
,
'struggling'
:
0
,
'highly_engaged'
:
0
,
'inactive'
:
0
,
'unenrolled'
:
0
},
enrollment_modes
=
expected_enrollment_modes
enrollment_modes
=
expected_enrollment_modes
,
cohorts
=
{
'Team edX'
:
len
(
enrollment_modes
)}
if
enrollment_modes
else
{},
)
self
.
assert_response_matches
(
self
.
_get
(
self
.
course_id
),
200
,
expected
)
@ddt.data
(
[],
[
'Yellow'
],
[
'Blue'
],
[
'Red'
,
'Red'
,
'yellow team'
,
'yellow team'
,
'green'
],
)
def
test_cohorts
(
self
,
cohorts
):
self
.
create_learners
([
{
'username'
:
'user_{}'
.
format
(
i
),
'course_id'
:
self
.
course_id
,
'cohort'
:
cohort
}
for
i
,
cohort
in
enumerate
(
cohorts
)
])
expected_cohorts
=
{
cohort
:
len
([
mode
for
mode
in
group
])
for
cohort
,
group
in
groupby
(
cohorts
)
}
expected
=
self
.
get_expected_json
(
segments
=
{
'disengaging'
:
0
,
'struggling'
:
0
,
'highly_engaged'
:
0
,
'inactive'
:
0
,
'unenrolled'
:
0
},
enrollment_modes
=
{
'honor'
:
len
(
cohorts
)}
if
cohorts
else
{},
cohorts
=
expected_cohorts
,
)
self
.
assert_response_matches
(
self
.
_get
(
self
.
course_id
),
200
,
expected
)
...
...
@@ -541,10 +612,6 @@ class CourseLearnerMetadataTests(DemoCourseMixin, VerifyCourseIdMixin,
metrics
.
append
(
'{0}_{1}'
.
format
(
entity_type
,
event
))
return
metrics
# TODO: enable during https://openedx.atlassian.net/browse/AN-6319
# def test_cohorts(self):
# pass
def
test_no_engagement_ranges
(
self
):
response
=
self
.
_get
(
self
.
course_id
)
self
.
assertEqual
(
response
.
status_code
,
200
)
...
...
analytics_data_api/v0/views/learners.py
View file @
e9c0c069
...
...
@@ -174,14 +174,21 @@ class LearnerListView(CourseViewMixin, generics.ListAPIView):
"""
self
.
_validate_query_params
()
query_params
=
self
.
request
.
QUERY_PARAMS
# Ordering by problem_attempts_per_completed can be ambiguous because
# it's a ratio values could be infinite (e.g. divide by zero) if no problems
# were completed. Instead, sorting by attempt_ratio_order will produce
# a sensible ordering
order_by
=
query_params
.
get
(
'order_by'
)
order_by
=
'attempt_ratio_order'
if
order_by
==
'problem_attempts_per_completed'
else
order_by
params
=
{
'segments'
:
split_query_argument
(
query_params
.
get
(
'segments'
)),
'ignore_segments'
:
split_query_argument
(
query_params
.
get
(
'ignore_segments'
)),
# TODO: enable during https://openedx.atlassian.net/browse/AN-6319
# 'cohort': query_params.get('cohort'),
'cohort'
:
query_params
.
get
(
'cohort'
),
'enrollment_mode'
:
query_params
.
get
(
'enrollment_mode'
),
'text_search'
:
query_params
.
get
(
'text_search'
),
'order_by'
:
query_params
.
get
(
'order_by'
)
,
'order_by'
:
order_by
,
'sort_order'
:
query_params
.
get
(
'sort_order'
)
}
# Remove None values from `params` so that we don't overwrite default
...
...
@@ -211,6 +218,7 @@ class EngagementTimelineView(CourseViewMixin, generics.ListAPIView):
* problems_completed: Unique number of problems completed.
* discussions_contributed: Number of discussions participated in (e.g. forum posts)
* videos_viewed: Number of videos watched.
* problem_attempts_per_completed: TBD
**Parameters**
...
...
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