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
d5bc70ac
Commit
d5bc70ac
authored
Jan 05, 2015
by
Clinton Blackburn
Committed by
Clinton Blackburn
Jan 06, 2015
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added Endpoint for Course Problems
This replaces the submission counts endpoint.
parent
addc2a1f
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
118 additions
and
132 deletions
+118
-132
analytics_data_api/v0/serializers.py
+6
-5
analytics_data_api/v0/tests/views/test_courses.py
+57
-0
analytics_data_api/v0/tests/views/test_problems.py
+0
-66
analytics_data_api/v0/urls/courses.py
+1
-0
analytics_data_api/v0/urls/problems.py
+0
-1
analytics_data_api/v0/views/courses.py
+53
-1
analytics_data_api/v0/views/problems.py
+1
-59
No files found.
analytics_data_api/v0/serializers.py
View file @
d5bc70ac
...
...
@@ -39,14 +39,15 @@ class ModelSerializerWithCreatedField(serializers.ModelSerializer):
created
=
serializers
.
DateTimeField
(
format
=
settings
.
DATETIME_FORMAT
)
class
ProblemS
ubmissionCountS
erializer
(
serializers
.
Serializer
):
class
ProblemSerializer
(
serializers
.
Serializer
):
"""
Serializer for problem
submission count
s.
Serializer for problems.
"""
module_id
=
serializers
.
CharField
()
total
=
serializers
.
IntegerField
(
default
=
0
)
correct
=
serializers
.
IntegerField
(
default
=
0
)
module_id
=
serializers
.
CharField
(
required
=
True
)
total_submissions
=
serializers
.
IntegerField
(
default
=
0
)
correct_submissions
=
serializers
.
IntegerField
(
default
=
0
)
part_ids
=
serializers
.
CharField
()
class
ProblemResponseAnswerDistributionSerializer
(
ModelSerializerWithCreatedField
):
...
...
analytics_data_api/v0/tests/views/test_courses.py
View file @
d5bc70ac
...
...
@@ -580,3 +580,60 @@ class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthent
expected
=
self
.
format_as_response
(
*
self
.
model
.
objects
.
all
())
self
.
assertEqual
(
len
(
expected
),
2
)
self
.
assertIntervalFilteringWorks
(
expected
,
self
.
interval_start
,
interval_end
+
datetime
.
timedelta
(
days
=
1
))
class
CourseProblemsListViewTests
(
DemoCourseMixin
,
TestCaseWithAuthentication
):
def
_get_data
(
self
,
course_id
=
None
):
"""
Retrieve data for the specified course.
"""
course_id
=
course_id
or
self
.
course_id
url
=
'/api/v0/courses/{}/problems/'
.
format
(
course_id
)
return
self
.
authenticated_get
(
url
)
def
test_get
(
self
):
"""
The view should return data when data exists for the course.
"""
# This data should never be returned by the tests below because the course_id doesn't match.
G
(
models
.
ProblemResponseAnswerDistribution
)
# This test assumes the view is using Python's groupby for grouping. 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'
o1
=
G
(
models
.
ProblemResponseAnswerDistribution
,
course_id
=
self
.
course_id
,
module_id
=
module_id
,
correct
=
True
,
count
=
100
)
o2
=
G
(
models
.
ProblemResponseAnswerDistribution
,
course_id
=
self
.
course_id
,
module_id
=
alt_module_id
,
correct
=
True
,
count
=
100
)
o3
=
G
(
models
.
ProblemResponseAnswerDistribution
,
course_id
=
self
.
course_id
,
module_id
=
module_id
,
correct
=
False
,
count
=
200
)
expected
=
[
{
'module_id'
:
module_id
,
'total_submissions'
:
300
,
'correct_submissions'
:
100
,
'part_ids'
:
[
o1
.
part_id
,
o3
.
part_id
]
},
{
'module_id'
:
alt_module_id
,
'total_submissions'
:
100
,
'correct_submissions'
:
100
,
'part_ids'
:
[
o2
.
part_id
]
}
]
response
=
self
.
_get_data
(
self
.
course_id
)
self
.
assertEquals
(
response
.
status_code
,
200
)
self
.
assertListEqual
(
response
.
data
,
expected
)
def
test_get_404
(
self
):
"""
The view should return 404 if no data exists for the course.
"""
response
=
self
.
_get_data
(
'foo/bar/course'
)
self
.
assertEquals
(
response
.
status_code
,
404
)
analytics_data_api/v0/tests/views/test_problems.py
View file @
d5bc70ac
...
...
@@ -10,7 +10,6 @@ from django_dynamic_fixture import G
from
analytics_data_api.v0
import
models
from
analytics_data_api.v0.serializers
import
ProblemResponseAnswerDistributionSerializer
,
\
GradeDistributionSerializer
,
SequentialOpenDistributionSerializer
from
analytics_data_api.v0.tests.views
import
DemoCourseMixin
from
analyticsdataserver.tests
import
TestCaseWithAuthentication
...
...
@@ -98,68 +97,3 @@ class SequentialOpenDistributionTests(TestCaseWithAuthentication):
def
test_get_404
(
self
):
response
=
self
.
authenticated_get
(
'/api/v0/problems/
%
s
%
s'
%
(
"DOES-NOT-EXIST"
,
self
.
path
))
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/courses.py
View file @
d5bc70ac
...
...
@@ -12,6 +12,7 @@ COURSE_URLS = [
(
'enrollment/education'
,
views
.
CourseEnrollmentByEducationView
,
'enrollment_by_education'
),
(
'enrollment/gender'
,
views
.
CourseEnrollmentByGenderView
,
'enrollment_by_gender'
),
(
'enrollment/location'
,
views
.
CourseEnrollmentByLocationView
,
'enrollment_by_location'
),
(
'problems'
,
views
.
ProblemsListView
,
'problems'
)
]
urlpatterns
=
[]
...
...
analytics_data_api/v0/urls/problems.py
View file @
d5bc70ac
...
...
@@ -11,7 +11,6 @@ PROBLEM_URLS = [
urlpatterns
=
patterns
(
''
,
url
(
r'^submission_counts/$'
,
views
.
SubmissionCountsListView
.
as_view
(),
name
=
'submission_counts'
),
url
(
r'^(?P<module_id>.+)/sequential_open_distribution/$'
,
views
.
SequentialOpenDistributionView
.
as_view
(),
name
=
'sequential_open_distribution'
),
)
...
...
analytics_data_api/v0/views/courses.py
View file @
d5bc70ac
...
...
@@ -9,8 +9,8 @@ from django.http import Http404
from
django.utils.timezone
import
make_aware
,
utc
from
rest_framework
import
generics
from
opaque_keys.edx.keys
import
CourseKey
from
analytics_data_api.constants
import
enrollment_modes
from
analytics_data_api.constants
import
enrollment_modes
from
analytics_data_api.v0
import
models
,
serializers
...
...
@@ -608,3 +608,55 @@ class CourseEnrollmentByLocationView(BaseCourseEnrollmentView):
# acceptable since the consuming code simply expects the returned
# value to be iterable, not necessarily a queryset.
return
returned_items
class
ProblemsListView
(
BaseCourseView
):
"""
Get the problems.
**Example request**
GET /api/v0/courses/{course_id}/problems/
**Response Values**
Returns a collection of submission counts and part IDs for each problem. Each collection contains:
* module_id: The ID of the problem.
* total_submissions: Total number of submissions
* correct_submissions: Total number of *correct* submissions.
* part_ids: List of problem part IDs
"""
model
=
models
.
ProblemResponseAnswerDistribution
serializer_class
=
serializers
.
ProblemSerializer
def
apply_date_filtering
(
self
,
queryset
):
# Date filtering is not possible for this data.
return
queryset
def
get_queryset
(
self
):
queryset
=
super
(
ProblemsListView
,
self
)
.
get_queryset
()
queryset
=
queryset
.
order_by
(
'module_id'
,
'part_id'
)
data
=
[]
for
problem_id
,
distribution
in
groupby
(
queryset
,
lambda
x
:
x
.
module_id
):
total
=
0
correct
=
0
part_ids
=
set
()
# Use a set to remove duplicate values.
for
answer
in
distribution
:
part_ids
.
add
(
answer
.
part_id
)
count
=
answer
.
count
total
+=
count
if
answer
.
correct
:
correct
+=
count
data
.
append
({
'module_id'
:
problem_id
,
'total_submissions'
:
total
,
'correct_submissions'
:
correct
,
'part_ids'
:
sorted
(
part_ids
)
})
return
data
analytics_data_api/v0/views/problems.py
View file @
d5bc70ac
from
itertools
import
groupby
from
rest_framework
import
generics
from
rest_framework.exceptions
import
NotAcceptable
from
analytics_data_api.v0.models
import
ProblemResponseAnswerDistribution
from
analytics_data_api.v0.serializers
import
ProblemResponseAnswerDistributionSerializer
,
\
ProblemSubmissionCountSerializer
from
analytics_data_api.v0.serializers
import
ProblemResponseAnswerDistributionSerializer
from
analytics_data_api.v0.models
import
GradeDistribution
from
analytics_data_api.v0.serializers
import
GradeDistributionSerializer
from
analytics_data_api.v0.models
import
SequentialOpenDistribution
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
):
"""
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