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
abf9e77b
Commit
abf9e77b
authored
Dec 17, 2014
by
Clinton Blackburn
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added Endpoint to Get Submission Counts for Multiple Problems
parent
5971ac6a
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
158 additions
and
23 deletions
+158
-23
analytics_data_api/v0/serializers.py
+13
-0
analytics_data_api/v0/tests/views/__init__.py
+14
-0
analytics_data_api/v0/tests/views/test_courses.py
+1
-14
analytics_data_api/v0/tests/views/test_problems.py
+66
-0
analytics_data_api/v0/urls/problems.py
+5
-6
analytics_data_api/v0/views/problems.py
+59
-3
No files found.
analytics_data_api/v0/serializers.py
View file @
abf9e77b
from
django.conf
import
settings
from
django.conf
import
settings
from
rest_framework
import
serializers
from
rest_framework
import
serializers
from
analytics_data_api.constants
import
enrollment_modes
,
genders
from
analytics_data_api.constants
import
enrollment_modes
,
genders
from
analytics_data_api.v0
import
models
from
analytics_data_api.v0
import
models
# Below are the enrollment modes supported by this API. The audit and honor enrollment modes are merged into honor.
# Below are the enrollment modes supported by this API. The audit and honor enrollment modes are merged into honor.
ENROLLMENT_MODES
=
[
enrollment_modes
.
HONOR
,
enrollment_modes
.
PROFESSIONAL
,
enrollment_modes
.
VERIFIED
]
ENROLLMENT_MODES
=
[
enrollment_modes
.
HONOR
,
enrollment_modes
.
PROFESSIONAL
,
enrollment_modes
.
VERIFIED
]
...
@@ -37,6 +39,16 @@ class ModelSerializerWithCreatedField(serializers.ModelSerializer):
...
@@ -37,6 +39,16 @@ class ModelSerializerWithCreatedField(serializers.ModelSerializer):
created
=
serializers
.
DateTimeField
(
format
=
settings
.
DATETIME_FORMAT
)
created
=
serializers
.
DateTimeField
(
format
=
settings
.
DATETIME_FORMAT
)
class
ProblemSubmissionCountSerializer
(
serializers
.
Serializer
):
"""
Serializer for problem submission counts.
"""
module_id
=
serializers
.
CharField
()
total
=
serializers
.
IntegerField
(
default
=
0
)
correct
=
serializers
.
IntegerField
(
default
=
0
)
class
ProblemResponseAnswerDistributionSerializer
(
ModelSerializerWithCreatedField
):
class
ProblemResponseAnswerDistributionSerializer
(
ModelSerializerWithCreatedField
):
"""
"""
Representation of the Answer Distribution table, without id.
Representation of the Answer Distribution table, without id.
...
@@ -67,6 +79,7 @@ class GradeDistributionSerializer(ModelSerializerWithCreatedField):
...
@@ -67,6 +79,7 @@ class GradeDistributionSerializer(ModelSerializerWithCreatedField):
"""
"""
Representation of the grade_distribution table without id
Representation of the grade_distribution table without id
"""
"""
class
Meta
(
object
):
class
Meta
(
object
):
model
=
models
.
GradeDistribution
model
=
models
.
GradeDistribution
fields
=
(
fields
=
(
...
...
analytics_data_api/v0/tests/views/__init__.py
View file @
abf9e77b
from
opaque_keys.edx.keys
import
CourseKey
DEMO_COURSE_ID
=
u'course-v1:edX+DemoX+Demo_2014'
class
DemoCourseMixin
(
object
):
course_key
=
None
course_id
=
None
@classmethod
def
setUpClass
(
cls
):
cls
.
course_id
=
DEMO_COURSE_ID
cls
.
course_key
=
CourseKey
.
from_string
(
cls
.
course_id
)
super
(
DemoCourseMixin
,
cls
)
.
setUpClass
()
analytics_data_api/v0/tests/views/test_courses.py
View file @
abf9e77b
...
@@ -12,29 +12,16 @@ import urllib
...
@@ -12,29 +12,16 @@ import urllib
from
django.conf
import
settings
from
django.conf
import
settings
from
django_dynamic_fixture
import
G
from
django_dynamic_fixture
import
G
import
pytz
import
pytz
from
opaque_keys.edx.keys
import
CourseKey
from
analytics_data_api.constants.country
import
get_country
from
analytics_data_api.constants.country
import
get_country
from
analytics_data_api.v0
import
models
from
analytics_data_api.v0
import
models
from
analytics_data_api.constants
import
country
,
enrollment_modes
,
genders
from
analytics_data_api.constants
import
country
,
enrollment_modes
,
genders
from
analytics_data_api.v0.models
import
CourseActivityWeekly
from
analytics_data_api.v0.models
import
CourseActivityWeekly
from
analytics_data_api.v0.tests.utils
import
flatten
from
analytics_data_api.v0.tests.utils
import
flatten
from
analytics_data_api.v0.tests.views
import
DemoCourseMixin
,
DEMO_COURSE_ID
from
analyticsdataserver.tests
import
TestCaseWithAuthentication
from
analyticsdataserver.tests
import
TestCaseWithAuthentication
DEMO_COURSE_ID
=
u'course-v1:edX+DemoX+Demo_2014'
class
DemoCourseMixin
(
object
):
course_key
=
None
course_id
=
None
def
setUp
(
self
):
self
.
course_id
=
DEMO_COURSE_ID
self
.
course_key
=
CourseKey
.
from_string
(
self
.
course_id
)
super
(
DemoCourseMixin
,
self
)
.
setUp
()
class
DefaultFillTestMixin
(
object
):
class
DefaultFillTestMixin
(
object
):
"""
"""
Test that the view fills in missing data with a default value.
Test that the view fills in missing data with a default value.
...
...
analytics_data_api/v0/tests/views/test_problems.py
View file @
abf9e77b
...
@@ -10,6 +10,7 @@ from django_dynamic_fixture import G
...
@@ -10,6 +10,7 @@ from django_dynamic_fixture import G
from
analytics_data_api.v0
import
models
from
analytics_data_api.v0
import
models
from
analytics_data_api.v0.serializers
import
ProblemResponseAnswerDistributionSerializer
,
\
from
analytics_data_api.v0.serializers
import
ProblemResponseAnswerDistributionSerializer
,
\
GradeDistributionSerializer
,
SequentialOpenDistributionSerializer
GradeDistributionSerializer
,
SequentialOpenDistributionSerializer
from
analytics_data_api.v0.tests.views
import
DemoCourseMixin
from
analyticsdataserver.tests
import
TestCaseWithAuthentication
from
analyticsdataserver.tests
import
TestCaseWithAuthentication
...
@@ -97,3 +98,68 @@ class SequentialOpenDistributionTests(TestCaseWithAuthentication):
...
@@ -97,3 +98,68 @@ class SequentialOpenDistributionTests(TestCaseWithAuthentication):
def
test_get_404
(
self
):
def
test_get_404
(
self
):
response
=
self
.
authenticated_get
(
'/api/v0/problems/
%
s
%
s'
%
(
"DOES-NOT-EXIST"
,
self
.
path
))
response
=
self
.
authenticated_get
(
'/api/v0/problems/
%
s
%
s'
%
(
"DOES-NOT-EXIST"
,
self
.
path
))
self
.
assertEquals
(
response
.
status_code
,
404
)
self
.
assertEquals
(
response
.
status_code
,
404
)
class
SubmissionCountsListViewTests
(
DemoCourseMixin
,
TestCaseWithAuthentication
):
path
=
'/api/v0/problems/submission_counts/'
@classmethod
def
setUpClass
(
cls
):
super
(
SubmissionCountsListViewTests
,
cls
)
.
setUpClass
()
cls
.
ad_1
=
G
(
models
.
ProblemResponseAnswerDistribution
)
cls
.
ad_2
=
G
(
models
.
ProblemResponseAnswerDistribution
)
def
_get_data
(
self
,
problem_ids
=
None
):
"""
Retrieve data for the specified problems from the server.
"""
url
=
self
.
path
if
problem_ids
:
problem_ids
=
','
.
join
(
problem_ids
)
url
=
'{}?problem_ids={}'
.
format
(
url
,
problem_ids
)
return
self
.
authenticated_get
(
url
)
def
assertValidResponse
(
self
,
*
problem_ids
):
expected_data
=
[]
for
problem_id
in
problem_ids
:
_models
=
models
.
ProblemResponseAnswerDistribution
.
objects
.
filter
(
module_id
=
problem_id
)
serialized
=
[{
'module_id'
:
model
.
module_id
,
'total'
:
model
.
count
,
'correct'
:
model
.
correct
or
0
}
for
model
in
_models
]
expected_data
+=
serialized
response
=
self
.
_get_data
(
problem_ids
)
self
.
assertEquals
(
response
.
status_code
,
200
)
actual
=
response
.
data
self
.
assertListEqual
(
actual
,
expected_data
)
def
test_get
(
self
):
"""
The view should return data when data exists for at least one of the problems.
"""
problem_id_1
=
self
.
ad_1
.
module_id
problem_id_2
=
self
.
ad_2
.
module_id
self
.
assertValidResponse
(
problem_id_1
)
self
.
assertValidResponse
(
problem_id_1
,
problem_id_2
)
self
.
assertValidResponse
(
problem_id_1
,
problem_id_2
,
'DOES-NOT-EXIST'
)
def
test_get_404
(
self
):
"""
The view should return 404 if data does not exist for at least one of the provided problems.
"""
problem_ids
=
[
'DOES-NOT-EXIST'
]
response
=
self
.
_get_data
(
problem_ids
)
self
.
assertEquals
(
response
.
status_code
,
404
)
def
test_get_406
(
self
):
"""
The view should return a 406 if no problem ID values are supplied.
"""
response
=
self
.
_get_data
()
self
.
assertEquals
(
response
.
status_code
,
406
)
analytics_data_api/v0/urls/problems.py
View file @
abf9e77b
...
@@ -2,19 +2,18 @@ import re
...
@@ -2,19 +2,18 @@ import re
from
django.conf.urls
import
patterns
,
url
from
django.conf.urls
import
patterns
,
url
from
analytics_data_api.v0.views.problems
import
ProblemResponseAnswerDistributionView
from
analytics_data_api.v0.views
import
problems
as
views
from
analytics_data_api.v0.views.problems
import
GradeDistributionView
from
analytics_data_api.v0.views.problems
import
SequentialOpenDistributionView
PROBLEM_URLS
=
[
PROBLEM_URLS
=
[
(
'answer_distribution'
,
ProblemResponseAnswerDistributionView
,
'answer_distribution'
),
(
'answer_distribution'
,
views
.
ProblemResponseAnswerDistributionView
,
'answer_distribution'
),
(
'grade_distribution'
,
GradeDistributionView
,
'grade_distribution'
),
(
'grade_distribution'
,
views
.
GradeDistributionView
,
'grade_distribution'
),
]
]
urlpatterns
=
patterns
(
urlpatterns
=
patterns
(
''
,
''
,
url
(
r'^submission_counts/$'
,
views
.
SubmissionCountsListView
.
as_view
(),
name
=
'submission_counts'
),
url
(
r'^(?P<module_id>.+)/sequential_open_distribution/$'
,
url
(
r'^(?P<module_id>.+)/sequential_open_distribution/$'
,
SequentialOpenDistributionView
.
as_view
(),
name
=
'sequential_open_distribution'
),
views
.
SequentialOpenDistributionView
.
as_view
(),
name
=
'sequential_open_distribution'
),
)
)
for
path
,
view
,
name
in
PROBLEM_URLS
:
for
path
,
view
,
name
in
PROBLEM_URLS
:
...
...
analytics_data_api/v0/views/problems.py
View file @
abf9e77b
from
itertools
import
groupby
from
rest_framework
import
generics
from
rest_framework
import
generics
from
rest_framework.exceptions
import
NotAcceptable
from
analytics_data_api.v0.models
import
ProblemResponseAnswerDistribution
from
analytics_data_api.v0.models
import
ProblemResponseAnswerDistribution
from
analytics_data_api.v0.serializers
import
ProblemResponseAnswerDistributionSerializer
from
analytics_data_api.v0.serializers
import
ProblemResponseAnswerDistributionSerializer
,
\
ProblemSubmissionCountSerializer
from
analytics_data_api.v0.models
import
GradeDistribution
from
analytics_data_api.v0.models
import
GradeDistribution
from
analytics_data_api.v0.serializers
import
GradeDistributionSerializer
from
analytics_data_api.v0.serializers
import
GradeDistributionSerializer
from
analytics_data_api.v0.models
import
SequentialOpenDistribution
from
analytics_data_api.v0.models
import
SequentialOpenDistribution
from
analytics_data_api.v0.serializers
import
SequentialOpenDistributionSerializer
from
analytics_data_api.v0.serializers
import
SequentialOpenDistributionSerializer
class
SubmissionCountsListView
(
generics
.
ListAPIView
):
"""
Get the number of submissions to one, or more, problems.
**Example request**
GET /api/v0/problems/submission_counts/?problem_ids={problem_id},{problem_id}
**Response Values**
Returns a collection of counts of total and correct solutions to the specified
problems. Each collection contains:
* module_id: The ID of the problem.
* total: Total number of submissions
* correct: Total number of *correct* submissions.
**Parameters**
problem_ids -- Comma-separated list of problem IDs representing the problems whose data should be returned.
"""
serializer_class
=
ProblemSubmissionCountSerializer
allow_empty
=
False
def
get_queryset
(
self
):
problem_ids
=
self
.
request
.
QUERY_PARAMS
.
get
(
'problem_ids'
,
''
)
if
not
problem_ids
:
raise
NotAcceptable
problem_ids
=
problem_ids
.
split
(
','
)
queryset
=
ProblemResponseAnswerDistribution
.
objects
.
filter
(
module_id__in
=
problem_ids
)
.
order_by
(
'module_id'
)
data
=
[]
for
problem_id
,
distribution
in
groupby
(
queryset
,
lambda
x
:
x
.
module_id
):
total
=
0
correct
=
0
for
answer
in
distribution
:
count
=
answer
.
count
total
+=
count
if
answer
.
correct
:
correct
+=
count
data
.
append
({
'module_id'
:
problem_id
,
'total'
:
total
,
'correct'
:
correct
})
return
data
class
ProblemResponseAnswerDistributionView
(
generics
.
ListAPIView
):
class
ProblemResponseAnswerDistributionView
(
generics
.
ListAPIView
):
"""
"""
Get the distribution of student answers to a specific problem.
Get the distribution of student answers to a specific problem.
...
...
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