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
addc2a1f
Commit
addc2a1f
authored
Dec 23, 2014
by
Clinton Blackburn
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #49 from edx/answer_distribution_list
Added Endpoint to Get Submission Counts for Multiple Problems
parents
4773c13f
abf9e77b
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
259 additions
and
116 deletions
+259
-116
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
+3
-107
analytics_data_api/v0/tests/views/test_problems.py
+165
-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 @
addc2a1f
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
0 → 100644
View file @
addc2a1f
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/
test_view
s.py
→
analytics_data_api/v0/tests/
views/test_course
s.py
View file @
addc2a1f
...
@@ -2,6 +2,7 @@
...
@@ -2,6 +2,7 @@
# NOTE: Full URLs are used throughout these tests to ensure that the API contract is fulfilled. The URLs should *not*
# NOTE: Full URLs are used throughout these tests to ensure that the API contract is fulfilled. The URLs should *not*
# change for versions greater than 1.0.0. Tests target a specific version of the API, additional tests should be added
# change for versions greater than 1.0.0. Tests target a specific version of the API, additional tests should be added
# for subsequent versions if there are breaking changes introduced in those versions.
# for subsequent versions if there are breaking changes introduced in those versions.
import
StringIO
import
StringIO
import
csv
import
csv
import
datetime
import
datetime
...
@@ -11,32 +12,16 @@ import urllib
...
@@ -11,32 +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.serializers
import
ProblemResponseAnswerDistributionSerializer
from
analytics_data_api.v0.serializers
import
GradeDistributionSerializer
from
analytics_data_api.v0.serializers
import
SequentialOpenDistributionSerializer
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.
...
@@ -390,37 +375,6 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, Defau
...
@@ -390,37 +375,6 @@ class CourseEnrollmentByGenderViewTests(CourseEnrollmentViewTestCaseMixin, Defau
self
.
assertViewReturnsExpectedData
(
expected
)
self
.
assertViewReturnsExpectedData
(
expected
)
# pylint: disable=no-member,no-value-for-parameter
class
AnswerDistributionTests
(
TestCaseWithAuthentication
):
path
=
'/answer_distribution/'
maxDiff
=
None
@classmethod
def
setUpClass
(
cls
):
cls
.
course_id
=
"org/num/run"
cls
.
module_id
=
"i4x://org/num/run/problem/RANDOMNUMBER"
cls
.
part_id1
=
"i4x-org-num-run-problem-RANDOMNUMBER_2_1"
cls
.
ad1
=
G
(
models
.
ProblemResponseAnswerDistribution
,
course_id
=
cls
.
course_id
,
module_id
=
cls
.
module_id
,
part_id
=
cls
.
part_id1
)
def
test_get
(
self
):
response
=
self
.
authenticated_get
(
'/api/v0/problems/
%
s
%
s'
%
(
self
.
module_id
,
self
.
path
))
self
.
assertEquals
(
response
.
status_code
,
200
)
expected_dict
=
ProblemResponseAnswerDistributionSerializer
(
self
.
ad1
)
.
data
actual_list
=
response
.
data
self
.
assertEquals
(
len
(
actual_list
),
1
)
self
.
assertDictEqual
(
actual_list
[
0
],
expected_dict
)
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
CourseEnrollmentViewTests
(
CourseEnrollmentViewTestCaseMixin
,
TestCaseWithAuthentication
):
class
CourseEnrollmentViewTests
(
CourseEnrollmentViewTestCaseMixin
,
TestCaseWithAuthentication
):
model
=
models
.
CourseEnrollmentDaily
model
=
models
.
CourseEnrollmentDaily
path
=
'/enrollment'
path
=
'/enrollment'
...
@@ -626,61 +580,3 @@ class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthent
...
@@ -626,61 +580,3 @@ class CourseActivityWeeklyViewTests(CourseViewTestCaseMixin, TestCaseWithAuthent
expected
=
self
.
format_as_response
(
*
self
.
model
.
objects
.
all
())
expected
=
self
.
format_as_response
(
*
self
.
model
.
objects
.
all
())
self
.
assertEqual
(
len
(
expected
),
2
)
self
.
assertEqual
(
len
(
expected
),
2
)
self
.
assertIntervalFilteringWorks
(
expected
,
self
.
interval_start
,
interval_end
+
datetime
.
timedelta
(
days
=
1
))
self
.
assertIntervalFilteringWorks
(
expected
,
self
.
interval_start
,
interval_end
+
datetime
.
timedelta
(
days
=
1
))
# pylint: disable=no-member,no-value-for-parameter
class
GradeDistributionTests
(
TestCaseWithAuthentication
):
path
=
'/grade_distribution/'
maxDiff
=
None
@classmethod
def
setUpClass
(
cls
):
cls
.
course_id
=
"org/class/test"
cls
.
module_id
=
"i4x://org/class/test/problem/RANDOM_NUMBER"
cls
.
ad1
=
G
(
models
.
GradeDistribution
,
course_id
=
cls
.
course_id
,
module_id
=
cls
.
module_id
,
)
def
test_get
(
self
):
response
=
self
.
authenticated_get
(
'/api/v0/problems/
%
s
%
s'
%
(
self
.
module_id
,
self
.
path
))
self
.
assertEquals
(
response
.
status_code
,
200
)
expected_dict
=
GradeDistributionSerializer
(
self
.
ad1
)
.
data
actual_list
=
response
.
data
self
.
assertEquals
(
len
(
actual_list
),
1
)
self
.
assertDictEqual
(
actual_list
[
0
],
expected_dict
)
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
)
# pylint: disable=no-member,no-value-for-parameter
class
SequentialOpenDistributionTests
(
TestCaseWithAuthentication
):
path
=
'/sequential_open_distribution/'
maxDiff
=
None
@classmethod
def
setUpClass
(
cls
):
cls
.
course_id
=
"org/class/test"
cls
.
module_id
=
"i4x://org/class/test/problem/RANDOM_NUMBER"
cls
.
ad1
=
G
(
models
.
SequentialOpenDistribution
,
course_id
=
cls
.
course_id
,
module_id
=
cls
.
module_id
,
)
def
test_get
(
self
):
response
=
self
.
authenticated_get
(
'/api/v0/problems/
%
s
%
s'
%
(
self
.
module_id
,
self
.
path
))
self
.
assertEquals
(
response
.
status_code
,
200
)
expected_dict
=
SequentialOpenDistributionSerializer
(
self
.
ad1
)
.
data
actual_list
=
response
.
data
self
.
assertEquals
(
len
(
actual_list
),
1
)
self
.
assertDictEqual
(
actual_list
[
0
],
expected_dict
)
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
)
analytics_data_api/v0/tests/views/test_problems.py
0 → 100644
View file @
addc2a1f
# coding=utf-8
# NOTE: Full URLs are used throughout these tests to ensure that the API contract is fulfilled. The URLs should *not*
# change for versions greater than 1.0.0. Tests target a specific version of the API, additional tests should be added
# for subsequent versions if there are breaking changes introduced in those versions.
# pylint: disable=no-member,no-value-for-parameter
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
class
AnswerDistributionTests
(
TestCaseWithAuthentication
):
path
=
'/answer_distribution/'
maxDiff
=
None
@classmethod
def
setUpClass
(
cls
):
cls
.
course_id
=
"org/num/run"
cls
.
module_id
=
"i4x://org/num/run/problem/RANDOMNUMBER"
cls
.
part_id1
=
"i4x-org-num-run-problem-RANDOMNUMBER_2_1"
cls
.
ad1
=
G
(
models
.
ProblemResponseAnswerDistribution
,
course_id
=
cls
.
course_id
,
module_id
=
cls
.
module_id
,
part_id
=
cls
.
part_id1
)
def
test_get
(
self
):
response
=
self
.
authenticated_get
(
'/api/v0/problems/
%
s
%
s'
%
(
self
.
module_id
,
self
.
path
))
self
.
assertEquals
(
response
.
status_code
,
200
)
expected_dict
=
ProblemResponseAnswerDistributionSerializer
(
self
.
ad1
)
.
data
actual_list
=
response
.
data
self
.
assertEquals
(
len
(
actual_list
),
1
)
self
.
assertDictEqual
(
actual_list
[
0
],
expected_dict
)
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
GradeDistributionTests
(
TestCaseWithAuthentication
):
path
=
'/grade_distribution/'
maxDiff
=
None
@classmethod
def
setUpClass
(
cls
):
cls
.
course_id
=
"org/class/test"
cls
.
module_id
=
"i4x://org/class/test/problem/RANDOM_NUMBER"
cls
.
ad1
=
G
(
models
.
GradeDistribution
,
course_id
=
cls
.
course_id
,
module_id
=
cls
.
module_id
,
)
def
test_get
(
self
):
response
=
self
.
authenticated_get
(
'/api/v0/problems/
%
s
%
s'
%
(
self
.
module_id
,
self
.
path
))
self
.
assertEquals
(
response
.
status_code
,
200
)
expected_dict
=
GradeDistributionSerializer
(
self
.
ad1
)
.
data
actual_list
=
response
.
data
self
.
assertEquals
(
len
(
actual_list
),
1
)
self
.
assertDictEqual
(
actual_list
[
0
],
expected_dict
)
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
SequentialOpenDistributionTests
(
TestCaseWithAuthentication
):
path
=
'/sequential_open_distribution/'
maxDiff
=
None
@classmethod
def
setUpClass
(
cls
):
cls
.
course_id
=
"org/class/test"
cls
.
module_id
=
"i4x://org/class/test/problem/RANDOM_NUMBER"
cls
.
ad1
=
G
(
models
.
SequentialOpenDistribution
,
course_id
=
cls
.
course_id
,
module_id
=
cls
.
module_id
,
)
def
test_get
(
self
):
response
=
self
.
authenticated_get
(
'/api/v0/problems/
%
s
%
s'
%
(
self
.
module_id
,
self
.
path
))
self
.
assertEquals
(
response
.
status_code
,
200
)
expected_dict
=
SequentialOpenDistributionSerializer
(
self
.
ad1
)
.
data
actual_list
=
response
.
data
self
.
assertEquals
(
len
(
actual_list
),
1
)
self
.
assertDictEqual
(
actual_list
[
0
],
expected_dict
)
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/problems.py
View file @
addc2a1f
...
@@ -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 @
addc2a1f
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