Commit 579544ec by Calen Pennington

Merge pull request #626 from MITx/feature/victor/answer-export

Feature/victor/answer export
parents 77029076 a8cd4633
...@@ -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,71 @@ def yield_module_descendents(module): ...@@ -24,6 +27,71 @@ 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):
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)
...@@ -368,5 +370,3 @@ def progress(request, course_id, student_id=None): ...@@ -368,5 +370,3 @@ def progress(request, course_id, student_id=None):
return render_to_response('courseware/progress.html', context) return render_to_response('courseware/progress.html', context)
...@@ -48,7 +48,7 @@ def instructor_dashboard(request, course_id): ...@@ -48,7 +48,7 @@ def instructor_dashboard(request, course_id):
"""Display the instructor dashboard for a course.""" """Display the instructor dashboard for a course."""
course = get_course_with_access(request.user, course_id, 'staff') course = get_course_with_access(request.user, course_id, 'staff')
instructor_access = has_access(request.user, course, 'instructor') # an instructor can manage staff lists instructor_access = has_access(request.user, course, 'instructor') # an instructor can manage staff lists
msg = '' msg = ''
# msg += ('POST=%s' % dict(request.POST)).replace('<','&lt;') # msg += ('POST=%s' % dict(request.POST)).replace('<','&lt;')
...@@ -99,7 +99,7 @@ def instructor_dashboard(request, course_id): ...@@ -99,7 +99,7 @@ def instructor_dashboard(request, course_id):
msg += "git pull on %s:<p>" % data_dir msg += "git pull on %s:<p>" % data_dir
msg += "<pre>%s</pre></p>" % escape(os.popen(cmd).read()) msg += "<pre>%s</pre></p>" % escape(os.popen(cmd).read())
track.views.server_track(request, 'git pull %s' % data_dir, {}, page='idashboard') track.views.server_track(request, 'git pull %s' % data_dir, {}, page='idashboard')
if 'Reload course' in action: if 'Reload course' in action:
log.debug('reloading %s (%s)' % (course_id, course)) log.debug('reloading %s (%s)' % (course_id, course))
try: try:
...@@ -144,6 +144,10 @@ def instructor_dashboard(request, course_id): ...@@ -144,6 +144,10 @@ def instructor_dashboard(request, course_id):
return return_csv('grades_%s_raw.csv' % course_id, return return_csv('grades_%s_raw.csv' % course_id,
get_student_grade_summary_data(request, course, course_id, get_raw_scores=True)) get_student_grade_summary_data(request, course, course_id, get_raw_scores=True))
elif 'Download CSV of answer distributions' in action:
track.views.server_track(request, 'dump-answer-dist-csv', {}, page='idashboard')
return return_csv('answer_dist_%s.csv' % course_id, get_answers_distribution(request, course_id))
elif 'List course staff' in action: elif 'List course staff' in action:
group = get_staff_group(course) group = get_staff_group(course)
msg += 'Staff group = %s' % group.name msg += 'Staff group = %s' % group.name
...@@ -290,7 +294,7 @@ def grade_summary(request, course_id): ...@@ -290,7 +294,7 @@ def grade_summary(request, course_id):
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def enroll_students(request, course_id): def enroll_students(request, course_id):
''' Allows a staff member to enroll students in a course. """Allows a staff member to enroll students in a course.
This is a short-term hack for Berkeley courses launching fall This is a short-term hack for Berkeley courses launching fall
2012. In the long term, we would like functionality like this, but 2012. In the long term, we would like functionality like this, but
...@@ -300,7 +304,7 @@ def enroll_students(request, course_id): ...@@ -300,7 +304,7 @@ def enroll_students(request, course_id):
It is poorly written and poorly tested, but it's designed to be It is poorly written and poorly tested, but it's designed to be
stripped out. stripped out.
''' """
course = get_course_with_access(request.user, course_id, 'staff') course = get_course_with_access(request.user, course_id, 'staff')
existing_students = [ce.user.email for ce in CourseEnrollment.objects.filter(course_id=course_id)] existing_students = [ce.user.email for ce in CourseEnrollment.objects.filter(course_id=course_id)]
...@@ -328,6 +332,28 @@ def enroll_students(request, course_id): ...@@ -328,6 +332,28 @@ def enroll_students(request, course_id):
'rejected_students': rejected_students, 'rejected_students': rejected_students,
'debug': new_students}) 'debug': new_students})
def get_answers_distribution(request, course_id):
"""
Get the distribution of answers for all graded problems in the course.
Return a dict with two keys:
'header': a header row
'data': a list of rows
"""
course = get_course_with_access(request.user, course_id, 'staff')
dist = grades.answer_distributions(request, course)
d = {}
d['header'] = ['url_name', 'display name', 'answer id', 'answer', 'count']
d['data'] = [[url_name, display_name, answer_id, a, answers[a]]
for (url_name, display_name, answer_id), answers in dist.items()
for a in answers]
return d
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
......
...@@ -58,6 +58,9 @@ table.stat_table td { ...@@ -58,6 +58,9 @@ table.stat_table td {
<input type="submit" name="action" value="Dump all RAW grades for all students in this course"> <input type="submit" name="action" value="Dump all RAW grades for all students in this course">
<input type="submit" name="action" value="Download CSV of all RAW grades"> <input type="submit" name="action" value="Download CSV of all RAW grades">
<p>
<input type="submit" name="action" value="Download CSV of answer distributions">
%if instructor_access: %if instructor_access:
<hr width="40%" style="align:left"> <hr width="40%" style="align:left">
<p> <p>
......
...@@ -197,8 +197,8 @@ if settings.WIKI_ENABLED: ...@@ -197,8 +197,8 @@ if settings.WIKI_ENABLED:
) )
if settings.QUICKEDIT: if settings.QUICKEDIT:
urlpatterns += (url(r'^quickedit/(?P<id>[^/]*)$', 'dogfood.views.quickedit'),) urlpatterns += (url(r'^quickedit/(?P<id>[^/]*)$', 'dogfood.views.quickedit'),)
urlpatterns += (url(r'^dogfood/(?P<id>[^/]*)$', 'dogfood.views.df_capa_problem'),) urlpatterns += (url(r'^dogfood/(?P<id>[^/]*)$', 'dogfood.views.df_capa_problem'),)
if settings.ASKBOT_ENABLED: if settings.ASKBOT_ENABLED:
urlpatterns += (url(r'^%s' % settings.ASKBOT_URL, include('askbot.urls')), \ urlpatterns += (url(r'^%s' % settings.ASKBOT_URL, include('askbot.urls')), \
......
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