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
f0ca9978
Commit
f0ca9978
authored
Jan 14, 2015
by
Clinton Blackburn
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #51 from edx/course-problems
Added Endpoint for Course Problems
parents
addc2a1f
d5bc70ac
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 @
f0ca9978
...
...
@@ -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 @
f0ca9978
...
...
@@ -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 @
f0ca9978
...
...
@@ -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 @
f0ca9978
...
...
@@ -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 @
f0ca9978
...
...
@@ -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 @
f0ca9978
...
...
@@ -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 @
f0ca9978
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