Commit c89031b0 by Victor Shnayder

Export of answer distibutions to csv

- go through all students, all of their problems, save count for each answer
- return csv
- url exists, but no links to it yet
- Will need to integrate with Ike's new dashboard code
parent 77029076
...@@ -4,11 +4,14 @@ from __future__ import division ...@@ -4,11 +4,14 @@ from __future__ import division
import random import random
import logging import logging
from collections import defaultdict
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User
from models import StudentModuleCache from models import StudentModuleCache
from module_render import get_module, get_instance_module from module_render import get_module, get_instance_module
from xmodule import graders from xmodule import graders
from xmodule.capa_module import CapaModule
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.graders import Score from xmodule.graders import Score
from models import StudentModule from models import StudentModule
...@@ -24,6 +27,73 @@ def yield_module_descendents(module): ...@@ -24,6 +27,73 @@ def yield_module_descendents(module):
stack.extend( next_module.get_display_items() ) stack.extend( next_module.get_display_items() )
yield next_module yield next_module
def yield_problems(request, course, student):
"""
Return an iterator over capa_modules that this student has
potentially answered. (all that student has answered will definitely be in
the list, but there may be others as well).
"""
grading_context = course.grading_context
student_module_cache = StudentModuleCache(course.id, student, grading_context['all_descriptors'])
for section_format, sections in grading_context['graded_sections'].iteritems():
for section in sections:
section_descriptor = section['section_descriptor']
# If the student hasn't seen a single problem in the section, skip it.
skip = True
for moduledescriptor in section['xmoduledescriptors']:
if student_module_cache.lookup(
course.id, moduledescriptor.category, moduledescriptor.location.url()):
skip = False
break
if skip:
continue
section_module = get_module(student, request,
section_descriptor.location, student_module_cache,
course.id)
if section_module is None:
# student doesn't have access to this module, or something else
# went wrong.
log.debug("couldn't get module for student {0} for section location {1}"
.format(student.username, section_descriptor.location))
continue
for problem in yield_module_descendents(section_module):
if isinstance(problem, CapaModule):
yield problem
def answer_distributions(request, course):
"""
Given a course_descriptor, compute frequencies of answers for each problem:
Format is:
dict: (problem url_name, problem display_name, problem_id) -> (dict : answer -> count)
TODO (vshnayder): this is currently doing a full linear pass through all
students and all problems. This will be just a little slow.
"""
counts = defaultdict(lambda: defaultdict(int))
enrolled_students = User.objects.filter(courseenrollment__course_id=course.id)
for student in enrolled_students:
for capa_module in yield_problems(request, course, student):
log.debug("looking at problem {0} for {1}. answers {2}".format(
capa_module.display_name, student.username, capa_module.lcp.student_answers))
for problem_id in capa_module.lcp.student_answers:
answer = capa_module.lcp.student_answers[problem_id]
key = (capa_module.url_name, capa_module.display_name, problem_id)
counts[key][answer] += 1
return counts
def grade(student, request, course, student_module_cache=None, keep_raw_scores=False): def grade(student, request, course, student_module_cache=None, keep_raw_scores=False):
""" """
This grades a student as quickly as possible. It retuns the This grades a student as quickly as possible. It retuns the
......
import csv
import json import json
import logging import logging
import urllib import urllib
import itertools import itertools
import StringIO
from functools import partial from functools import partial
...@@ -219,9 +221,9 @@ def jump_to(request, course_id, location): ...@@ -219,9 +221,9 @@ def jump_to(request, course_id, location):
# Rely on index to do all error handling and access control. # Rely on index to do all error handling and access control.
return redirect('courseware_position', return redirect('courseware_position',
course_id=course_id, course_id=course_id,
chapter=chapter, chapter=chapter,
section=section, section=section,
position=position) position=position)
@ensure_csrf_cookie @ensure_csrf_cookie
def course_info(request, course_id): def course_info(request, course_id):
...@@ -342,7 +344,7 @@ def progress(request, course_id, student_id=None): ...@@ -342,7 +344,7 @@ def progress(request, course_id, student_id=None):
# NOTE: To make sure impersonation by instructor works, use # NOTE: To make sure impersonation by instructor works, use
# student instead of request.user in the rest of the function. # student instead of request.user in the rest of the function.
# The pre-fetching of groups is done to make auth checks not require an # The pre-fetching of groups is done to make auth checks not require an
# additional DB lookup (this kills the Progress page in particular). # additional DB lookup (this kills the Progress page in particular).
student = User.objects.prefetch_related("groups").get(id=student.id) student = User.objects.prefetch_related("groups").get(id=student.id)
...@@ -370,3 +372,24 @@ def progress(request, course_id, student_id=None): ...@@ -370,3 +372,24 @@ def progress(request, course_id, student_id=None):
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def answers_export(request, course_id):
"""
Export the distribution of student answers to all problems as a csv file.
- only displayed to course staff
"""
course = get_course_with_access(request.user, course_id, 'staff')
dist = grades.answer_distributions(request, course)
response = HttpResponse(mimetype='text/csv')
response['Content-Disposition'] = 'attachment; filename=%s' % "answer_distribution.csv"
writer = csv.writer(response)
for (url_name, display_name, answer_id), answers in dist.items():
# HEADER?
for a in answers:
writer.writerow([url_name, display_name, answer_id, a, answers[a]])
return response
...@@ -161,6 +161,9 @@ if settings.COURSEWARE_ENABLED: ...@@ -161,6 +161,9 @@ if settings.COURSEWARE_ENABLED:
'instructor.views.grade_summary', name='grade_summary'), 'instructor.views.grade_summary', name='grade_summary'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/enroll_students$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/enroll_students$',
'instructor.views.enroll_students', name='enroll_students'), 'instructor.views.enroll_students', name='enroll_students'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/answers_export$',
'courseware.views.answers_export', name='answers_export'),
) )
# discussion forums live within courseware, so courseware must be enabled first # discussion forums live within courseware, so courseware must be enabled first
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment