Merge pull request #626 from MITx/feature/victor/answer-export
Feature/victor/answer export
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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('<','<')
|
||||
@@ -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')), \
|
||||
|
||||
Reference in New Issue
Block a user