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
import random
import logging
from collections import defaultdict
from django.conf import settings
from django.contrib.auth.models import User
from models import StudentModuleCache
from module_render import get_module, get_instance_module
from xmodule import graders
from xmodule.capa_module import CapaModule
from xmodule.course_module import CourseDescriptor
from xmodule.graders import Score
from models import StudentModule
......@@ -24,6 +27,71 @@ def yield_module_descendents(module):
stack.extend( next_module.get_display_items() )
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):
"""
This grades a student as quickly as possible. It retuns the
......
import csv
import json
import logging
import urllib
import itertools
import StringIO
from functools import partial
......@@ -219,9 +221,9 @@ def jump_to(request, course_id, location):
# Rely on index to do all error handling and access control.
return redirect('courseware_position',
course_id=course_id,
chapter=chapter,
section=section,
course_id=course_id,
chapter=chapter,
section=section,
position=position)
@ensure_csrf_cookie
def course_info(request, course_id):
......@@ -342,7 +344,7 @@ def progress(request, course_id, student_id=None):
# NOTE: To make sure impersonation by instructor works, use
# 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).
student = User.objects.prefetch_related("groups").get(id=student.id)
......@@ -368,5 +370,3 @@ def progress(request, course_id, student_id=None):
return render_to_response('courseware/progress.html', context)
......@@ -48,7 +48,7 @@ def instructor_dashboard(request, course_id):
"""Display the instructor dashboard for a course."""
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 += ('POST=%s' % dict(request.POST)).replace('<','&lt;')
......@@ -99,7 +99,7 @@ def instructor_dashboard(request, course_id):
msg += "git pull on %s:<p>" % data_dir
msg += "<pre>%s</pre></p>" % escape(os.popen(cmd).read())
track.views.server_track(request, 'git pull %s' % data_dir, {}, page='idashboard')
if 'Reload course' in action:
log.debug('reloading %s (%s)' % (course_id, course))
try:
......@@ -144,6 +144,10 @@ def instructor_dashboard(request, course_id):
return return_csv('grades_%s_raw.csv' % course_id,
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:
group = get_staff_group(course)
msg += 'Staff group = %s' % group.name
......@@ -290,7 +294,7 @@ def grade_summary(request, course_id):
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
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
2012. In the long term, we would like functionality like this, but
......@@ -300,7 +304,7 @@ def enroll_students(request, course_id):
It is poorly written and poorly tested, but it's designed to be
stripped out.
'''
"""
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)]
......@@ -328,6 +332,28 @@ def enroll_students(request, course_id):
'rejected_students': rejected_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 {
<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">
<p>
<input type="submit" name="action" value="Download CSV of answer distributions">
%if instructor_access:
<hr width="40%" style="align:left">
<p>
......
......@@ -197,8 +197,8 @@ if settings.WIKI_ENABLED:
)
if settings.QUICKEDIT:
urlpatterns += (url(r'^quickedit/(?P<id>[^/]*)$', 'dogfood.views.quickedit'),)
urlpatterns += (url(r'^dogfood/(?P<id>[^/]*)$', 'dogfood.views.df_capa_problem'),)
urlpatterns += (url(r'^quickedit/(?P<id>[^/]*)$', 'dogfood.views.quickedit'),)
urlpatterns += (url(r'^dogfood/(?P<id>[^/]*)$', 'dogfood.views.df_capa_problem'),)
if settings.ASKBOT_ENABLED:
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