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
6596d90f
Commit
6596d90f
authored
Nov 22, 2016
by
Dennis Jen
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added course meta summary enrollment
parent
d2fcb7b3
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
198 additions
and
39 deletions
+198
-39
analytics_data_api/v0/models.py
+41
-30
analytics_data_api/v0/serializers.py
+19
-0
analytics_data_api/v0/tests/views/test_utils.py
+51
-0
analytics_data_api/v0/urls/__init__.py
+1
-0
analytics_data_api/v0/urls/course_summaries.py
+9
-0
analytics_data_api/v0/views/__init__.py
+3
-8
analytics_data_api/v0/views/course_summaries.py
+60
-0
analytics_data_api/v0/views/utils.py
+14
-1
No files found.
analytics_data_api/v0/models.py
View file @
6596d90f
...
...
@@ -12,21 +12,27 @@ from analytics_data_api.constants.engagement_types import EngagementType
from
analytics_data_api.utils
import
date_range
class
CourseActivityWeekly
(
models
.
Model
):
"""A count of unique users who performed a particular action during a week."""
class
BaseCourseModel
(
models
.
Model
):
course_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
created
=
models
.
DateTimeField
(
auto_now_add
=
True
)
class
Meta
(
object
):
abstract
=
True
class
CourseActivityWeekly
(
BaseCourseModel
):
"""A count of unique users who performed a particular action during a week."""
class
Meta
(
BaseCourseModel
.
Meta
):
db_table
=
'course_activity'
index_together
=
[[
'course_id'
,
'activity_type'
]]
ordering
=
(
'interval_end'
,
'interval_start'
,
'course_id'
)
get_latest_by
=
'interval_end'
course_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
interval_start
=
models
.
DateTimeField
()
interval_end
=
models
.
DateTimeField
(
db_index
=
True
)
activity_type
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
,
db_column
=
'label'
)
count
=
models
.
IntegerField
()
created
=
models
.
DateTimeField
(
auto_now_add
=
True
)
@classmethod
def
get_most_recent
(
cls
,
course_id
,
activity_type
):
...
...
@@ -34,13 +40,11 @@ class CourseActivityWeekly(models.Model):
return
cls
.
objects
.
filter
(
course_id
=
course_id
,
activity_type
=
activity_type
)
.
latest
(
'interval_end'
)
class
BaseCourseEnrollment
(
models
.
Model
):
course_id
=
models
.
CharField
(
max_length
=
255
)
class
BaseCourseEnrollment
(
BaseCourseModel
):
date
=
models
.
DateField
(
null
=
False
,
db_index
=
True
)
count
=
models
.
IntegerField
(
null
=
False
)
created
=
models
.
DateTimeField
(
auto_now_add
=
True
)
class
Meta
(
object
):
class
Meta
(
BaseCourseModel
.
Meta
):
abstract
=
True
get_latest_by
=
'date'
index_together
=
[(
'course_id'
,
'date'
,)]
...
...
@@ -63,6 +67,23 @@ class CourseEnrollmentModeDaily(BaseCourseEnrollment):
unique_together
=
[(
'course_id'
,
'date'
,
'mode'
)]
class
CourseMetaSummaryEnrollment
(
BaseCourseModel
):
catalog_course_title
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
catalog_course
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
start_date
=
models
.
DateTimeField
()
end_date
=
models
.
DateTimeField
()
pacing_type
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
availability
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
mode
=
models
.
CharField
(
max_length
=
255
)
cumulative_count
=
models
.
IntegerField
(
null
=
False
)
count_change_7_days
=
models
.
IntegerField
(
default
=
0
)
class
Meta
(
BaseCourseModel
.
Meta
):
db_table
=
'course_meta_summary_enrollment'
ordering
=
(
'course_id'
,)
unique_together
=
[(
'course_id'
,
'mode'
,)]
class
CourseEnrollmentByBirthYear
(
BaseCourseEnrollment
):
birth_year
=
models
.
IntegerField
(
null
=
False
)
...
...
@@ -103,14 +124,13 @@ class CourseEnrollmentByGender(BaseCourseEnrollment):
unique_together
=
[(
'course_id'
,
'date'
,
'gender'
)]
class
BaseProblemResponseAnswerDistribution
(
models
.
Model
):
class
BaseProblemResponseAnswerDistribution
(
BaseCourse
Model
):
""" Base model for the answer_distribution table. """
class
Meta
(
object
):
class
Meta
(
BaseCourseModel
.
Meta
):
db_table
=
'answer_distribution'
abstract
=
True
course_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
module_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
part_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
correct
=
models
.
NullBooleanField
()
...
...
@@ -119,7 +139,6 @@ class BaseProblemResponseAnswerDistribution(models.Model):
variant
=
models
.
IntegerField
(
null
=
True
)
problem_display_name
=
models
.
TextField
(
null
=
True
)
question_text
=
models
.
TextField
(
null
=
True
)
created
=
models
.
DateTimeField
(
auto_now_add
=
True
)
class
ProblemResponseAnswerDistribution
(
BaseProblemResponseAnswerDistribution
):
...
...
@@ -131,19 +150,17 @@ class ProblemResponseAnswerDistribution(BaseProblemResponseAnswerDistribution):
count
=
models
.
IntegerField
()
class
ProblemsAndTags
(
models
.
Model
):
class
ProblemsAndTags
(
BaseCourse
Model
):
""" Model for the tags_distribution table """
class
Meta
(
object
):
class
Meta
(
BaseCourseModel
.
Meta
):
db_table
=
'tags_distribution'
course_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
module_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
tag_name
=
models
.
CharField
(
max_length
=
255
)
tag_value
=
models
.
CharField
(
max_length
=
255
)
total_submissions
=
models
.
IntegerField
(
default
=
0
)
correct_submissions
=
models
.
IntegerField
(
default
=
0
)
created
=
models
.
DateTimeField
(
auto_now_add
=
True
)
class
ProblemFirstLastResponseAnswerDistribution
(
BaseProblemResponseAnswerDistribution
):
...
...
@@ -172,30 +189,26 @@ class CourseEnrollmentByCountry(BaseCourseEnrollment):
unique_together
=
[(
'course_id'
,
'date'
,
'country_code'
)]
class
GradeDistribution
(
models
.
Model
):
class
GradeDistribution
(
BaseCourse
Model
):
""" Each row stores the count of a particular grade on a module for a given course. """
class
Meta
(
object
):
class
Meta
(
BaseCourseModel
.
Meta
):
db_table
=
'grade_distribution'
module_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
course_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
grade
=
models
.
IntegerField
()
max_grade
=
models
.
IntegerField
()
count
=
models
.
IntegerField
()
created
=
models
.
DateTimeField
(
auto_now_add
=
True
)
class
SequentialOpenDistribution
(
models
.
Model
):
class
SequentialOpenDistribution
(
BaseCourse
Model
):
""" Each row stores the count of views a particular module has had in a given course. """
class
Meta
(
object
):
class
Meta
(
BaseCourseModel
.
Meta
):
db_table
=
'sequential_open_distribution'
module_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
course_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
count
=
models
.
IntegerField
()
created
=
models
.
DateTimeField
(
auto_now_add
=
True
)
class
BaseVideo
(
models
.
Model
):
...
...
@@ -465,10 +478,9 @@ class ModuleEngagementTimelineManager(models.Manager):
return
full_timeline
class
ModuleEngagement
(
models
.
Model
):
class
ModuleEngagement
(
BaseCourse
Model
):
"""User interactions with entities within the courseware."""
course_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
username
=
models
.
CharField
(
max_length
=
255
)
date
=
models
.
DateField
()
# This will be one of "problem", "video" or "discussion"
...
...
@@ -483,18 +495,17 @@ class ModuleEngagement(models.Model):
objects
=
ModuleEngagementTimelineManager
()
class
Meta
(
object
):
class
Meta
(
BaseCourseModel
.
Meta
):
db_table
=
'module_engagement'
class
ModuleEngagementMetricRanges
(
models
.
Model
):
class
ModuleEngagementMetricRanges
(
BaseCourse
Model
):
"""
Represents the low and high values for a module engagement entity and event
pair, known as the metric. The range_type will either be low, normal, or
high, bounded by low_value and high_value.
"""
course_id
=
models
.
CharField
(
db_index
=
True
,
max_length
=
255
)
start_date
=
models
.
DateField
()
# This is a left-closed interval. No data from the end_date is included in the analysis.
end_date
=
models
.
DateField
()
...
...
@@ -505,5 +516,5 @@ class ModuleEngagementMetricRanges(models.Model):
high_value
=
models
.
FloatField
()
low_value
=
models
.
FloatField
()
class
Meta
(
object
):
class
Meta
(
BaseCourseModel
.
Meta
):
db_table
=
'module_engagement_metric_ranges'
analytics_data_api/v0/serializers.py
View file @
6596d90f
...
...
@@ -507,3 +507,22 @@ class CourseLearnerMetadataSerializer(serializers.Serializer):
})
return
engagement_ranges
class
CourseMetaSummaryEnrollmentSerializer
(
ModelSerializerWithCreatedField
):
"""
Serializer for problems.
"""
course_id
=
serializers
.
CharField
()
catalog_course_title
=
serializers
.
CharField
()
catalog_course
=
serializers
.
CharField
()
start_date
=
serializers
.
DateTimeField
()
end_date
=
serializers
.
DateTimeField
()
pacing_type
=
serializers
.
CharField
()
availability
=
serializers
.
CharField
()
mode
=
serializers
.
CharField
()
cumulative_count
=
serializers
.
IntegerField
(
default
=
0
)
count_change_7_days
=
serializers
.
IntegerField
(
default
=
0
)
# TODO: 0 as default?
class
Meta
(
object
):
model
=
models
.
CourseMetaSummaryEnrollment
analytics_data_api/v0/tests/views/test_utils.py
0 → 100644
View file @
6596d90f
import
ddt
from
mock
import
Mock
from
django.http
import
Http404
from
django.test
import
TestCase
from
analytics_data_api.v0.exceptions
import
CourseKeyMalformedError
import
analytics_data_api.v0.views.utils
as
utils
@ddt.ddt
class
UtilsTest
(
TestCase
):
@ddt.data
(
None
,
'not-a-key'
,
)
def
test_invalid_course_id
(
self
,
course_id
):
with
self
.
assertRaises
(
CourseKeyMalformedError
):
utils
.
validate_course_id
(
course_id
)
# TODO: DDT w/ the refactored CourseSamples once https://github.com/edx/edx-analytics-data-api/pull/143 merges
@ddt.data
(
'edX/DemoX/Demo_Course'
,
'course-v1:edX+DemoX+Demo_2014'
,
)
def
test_valid_course_id
(
self
,
course_id
):
try
:
utils
.
validate_course_id
(
course_id
)
except
CourseKeyMalformedError
:
self
.
fail
(
'Unexpected CourseKeyMalformedError!'
)
def
test_split_query_argument_none
(
self
):
self
.
assertIsNone
(
utils
.
split_query_argument
(
None
))
@ddt.data
(
(
'one'
,
[
'one'
]),
(
'one,two'
,
[
'one'
,
'two'
]),
)
@ddt.unpack
def
test_split_query_argument
(
self
,
input
,
expected
):
self
.
assertListEqual
(
utils
.
split_query_argument
(
input
),
expected
)
def
test_raise_404_if_none_raises_error
(
self
):
decorated_func
=
utils
.
raise_404_if_none
(
Mock
(
return_value
=
None
))
with
self
.
assertRaises
(
Http404
):
decorated_func
(
self
)
def
test_raise_404_if_none_passes_through
(
self
):
decorated_func
=
utils
.
raise_404_if_none
(
Mock
(
return_value
=
'Not a 404'
))
self
.
assertEqual
(
decorated_func
(
self
),
'Not a 404'
)
analytics_data_api/v0/urls/__init__.py
View file @
6596d90f
...
...
@@ -9,6 +9,7 @@ urlpatterns = [
url
(
r'^problems/'
,
include
(
'analytics_data_api.v0.urls.problems'
,
'problems'
)),
url
(
r'^videos/'
,
include
(
'analytics_data_api.v0.urls.videos'
,
'videos'
)),
url
(
'^'
,
include
(
'analytics_data_api.v0.urls.learners'
,
'learners'
)),
url
(
'^'
,
include
(
'analytics_data_api.v0.urls.course_summaries'
,
'course_summaries'
)),
# pylint: disable=no-value-for-parameter
url
(
r'^authenticated/$'
,
RedirectView
.
as_view
(
url
=
reverse_lazy
(
'authenticated'
)),
name
=
'authenticated'
),
...
...
analytics_data_api/v0/urls/course_summaries.py
0 → 100644
View file @
6596d90f
from
django.conf.urls
import
url
from
analytics_data_api.v0.views
import
course_summaries
as
views
USERNAME_PATTERN
=
r'(?P<username>[\w.+-]+)'
urlpatterns
=
[
url
(
r'^course_summaries/$'
,
views
.
CourseSummariesView
.
as_view
(),
name
=
'course_summaries'
),
]
analytics_data_api/v0/views/__init__.py
View file @
6596d90f
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.keys
import
CourseKey
from
django.utils
import
timezone
from
rest_framework.response
import
Response
from
analytics_data_api.v0.exceptions
import
(
CourseNotSpecifiedError
,
CourseKeyMalformedError
)
from
analytics_data_api.v0.exceptions
import
CourseNotSpecifiedError
import
analytics_data_api.utils
as
utils
class
CourseViewMixin
(
object
):
"""
...
...
@@ -18,10 +16,7 @@ class CourseViewMixin(object):
if
not
self
.
course_id
:
raise
CourseNotSpecifiedError
()
try
:
CourseKey
.
from_string
(
self
.
course_id
)
except
InvalidKeyError
:
raise
CourseKeyMalformedError
(
course_id
=
self
.
course_id
)
utils
.
validate_course_id
(
self
.
course_id
)
return
super
(
CourseViewMixin
,
self
)
.
get
(
request
,
*
args
,
**
kwargs
)
...
...
analytics_data_api/v0/views/course_summaries.py
0 → 100644
View file @
6596d90f
"""
TODO: TBD
"""
from
django.db.models
import
Q
from
rest_framework
import
generics
from
analytics_data_api.v0
import
models
,
serializers
from
analytics_data_api.v0.views.utils
import
(
raise_404_if_none
,
split_query_argument
,
validate_course_id
,
)
class
CourseSummariesView
(
generics
.
ListAPIView
):
"""
TBD.
**Example Request**
GET /api/v0/course_summaries/?course_ids={course_id},{course_id}
**Response Values**
TBD
**Parameters**
You can specify the course IDs for which you want data.
course_ids -- The comma-separated course identifiers for which user data is requested.
For example, edX/DemoX/Demo_Course,course-v1:edX+DemoX+Demo_2016
"""
course_ids
=
None
fields
=
None
serializer_class
=
serializers
.
CourseMetaSummaryEnrollmentSerializer
model
=
models
.
CourseMetaSummaryEnrollment
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
query_params
=
self
.
request
.
query_params
self
.
fields
=
split_query_argument
(
query_params
.
get
(
'fields'
))
self
.
course_ids
=
split_query_argument
(
query_params
.
get
(
'course_ids'
))
if
self
.
course_ids
is
not
None
:
for
course_id
in
self
.
course_ids
:
validate_course_id
(
course_id
)
return
super
(
CourseSummariesView
,
self
)
.
get
(
request
,
*
args
,
**
kwargs
)
@raise_404_if_none
def
get_queryset
(
self
):
if
self
.
course_ids
:
# create an OR query for course IDs that match
query
=
reduce
(
lambda
q
,
course_id
:
q
|
Q
(
course_id
=
course_id
),
self
.
course_ids
,
Q
())
queryset
=
self
.
model
.
objects
.
filter
(
query
)
else
:
queryset
=
self
.
model
.
objects
.
all
()
return
queryset
analytics_data_api/v0/views/utils.py
View file @
6596d90f
"""Utilities for view-level API logic."""
from
django.http
import
Http404
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.keys
import
CourseKey
from
analytics_data_api.v0.exceptions
import
CourseKeyMalformedError
def
split_query_argument
(
argument
):
"""
...
...
@@ -15,7 +20,7 @@ def split_query_argument(argument):
def
raise_404_if_none
(
func
):
"""
Decorator for rais
eing Http404 if function evaul
ation is falsey (e.g. empty queryset).
Decorator for rais
ing Http404 if function evalu
ation is falsey (e.g. empty queryset).
"""
def
func_wrapper
(
self
):
queryset
=
func
(
self
)
...
...
@@ -24,3 +29,11 @@ def raise_404_if_none(func):
else
:
raise
Http404
return
func_wrapper
def
validate_course_id
(
course_id
):
"""Raises CourseKeyMalformedError if course ID is invalid."""
try
:
CourseKey
.
from_string
(
course_id
)
except
InvalidKeyError
:
raise
CourseKeyMalformedError
(
course_id
=
course_id
)
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