Crowdsourced hinter now supports formula responses. Tests still broken.
This commit is contained in:
@@ -13,7 +13,7 @@ from pkg_resources import resource_string
|
||||
from lxml import etree
|
||||
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xblock.core import Scope, String, Integer, Boolean, Dict, List
|
||||
|
||||
from capa.responsetypes import FormulaResponse, StudentInputError
|
||||
@@ -150,10 +150,10 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
|
||||
# errors don't make a difference.
|
||||
out = str(responder.hash_answers(answer, self.formula_test_values))
|
||||
except StudentInputError:
|
||||
# I'm not sure what's the best thing to do here.
|
||||
# I'll return the empty string, for now.
|
||||
# That way, all invalid hints are clustered together.
|
||||
return ''
|
||||
# I'm not sure what's the best thing to do here. I'm returning
|
||||
# None, for now, so that the calling function has a chance to catch
|
||||
# the error without having to import StudentInputError.
|
||||
return None
|
||||
return out
|
||||
|
||||
def handle_ajax(self, dispatch, data):
|
||||
@@ -197,6 +197,10 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
|
||||
return
|
||||
# Make a signature of the answer, for formula responses.
|
||||
signature = self.answer_signature(answer)
|
||||
if signature == None:
|
||||
# Sometimes, signature conversion may fail.
|
||||
log.exception('Signature conversion failed: ' + str(answer))
|
||||
return
|
||||
# Look for a hint to give.
|
||||
# Make a local copy of self.hints - this means we only need to do one json unpacking.
|
||||
# (This is because xblocks storage makes the following command a deep copy.)
|
||||
@@ -261,7 +265,7 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
|
||||
if signature in self.hints:
|
||||
# Go through each hint, and add to index_to_hints
|
||||
for hint_id in hints_offered:
|
||||
if (hint_id is not None) and (hint_id not in answer_to_hints[signature]):
|
||||
if (hint_id is not None) and (hint_id not in answer_to_hints[answer]):
|
||||
try:
|
||||
answer_to_hints[answer][hint_id] = self.hints[signature][str(hint_id)][0]
|
||||
except KeyError:
|
||||
@@ -335,10 +339,7 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
|
||||
temp_dict[signature] = {str(self.hint_pk): [hint, 1]}
|
||||
# Add the signature to signature_to_ans, if it's not there yet.
|
||||
# This allows instructors to see a human-readable answer that corresponds to each signature.
|
||||
if answer not in self.signature_to_ans:
|
||||
local_sta = self.signature_to_ans
|
||||
local_sta[signature] = answer
|
||||
self.signature_to_ans = local_sta
|
||||
self.add_signature(signature, answer)
|
||||
self.hint_pk += 1
|
||||
if self.moderate == 'True':
|
||||
self.mod_queue = temp_dict
|
||||
@@ -349,8 +350,18 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
|
||||
self.previous_answers = []
|
||||
return {'message': 'Thank you for your hint!'}
|
||||
|
||||
def add_signature(self, signature, answer):
|
||||
"""
|
||||
Add a signature to self.signature_to_ans. If the signature already
|
||||
exists, do nothing.
|
||||
"""
|
||||
if signature not in self.signature_to_ans:
|
||||
local_sta = self.signature_to_ans
|
||||
local_sta[signature] = answer
|
||||
self.signature_to_ans = local_sta
|
||||
|
||||
class CrowdsourceHinterDescriptor(CrowdsourceHinterFields, XmlDescriptor):
|
||||
|
||||
class CrowdsourceHinterDescriptor(CrowdsourceHinterFields, RawDescriptor):
|
||||
module_class = CrowdsourceHinterModule
|
||||
stores_state = True
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ class @Hinter
|
||||
|
||||
vote: (eventObj) =>
|
||||
target = @$(eventObj.currentTarget)
|
||||
parent_div_selector = '#previous-answer-' + @jq_escape(target.attr('data-answer'))
|
||||
all_pks = @$(parent_div_selector).attr('data-all-pks')
|
||||
parent_div = $('.previous-answer[data-answer="'+target.attr('data-answer')+'"]')
|
||||
all_pks = parent_div.attr('data-all-pks')
|
||||
console.debug(all_pks)
|
||||
post_json = {'answer': target.attr('data-answer'), 'hint': target.data('hintno'), 'pk_list': all_pks}
|
||||
$.postWithPrefix "#{@url}/vote", post_json, (response) =>
|
||||
@@ -57,8 +57,9 @@ class @Hinter
|
||||
|
||||
submit_hint: (eventObj) =>
|
||||
target = @$(eventObj.currentTarget)
|
||||
textarea_id = '#custom-hint-' + @jq_escape(target.attr('data-answer'))
|
||||
post_json = {'answer': target.attr('data-answer'), 'hint': @$(textarea_id).val()}
|
||||
textarea = $('.custom-hint[data-answer="'+target.attr('data-answer')+'"]')
|
||||
console.debug(textarea)
|
||||
post_json = {'answer': target.attr('data-answer'), 'hint': @$(textarea).val()}
|
||||
$.postWithPrefix "#{@url}/submit_hint",post_json, (response) =>
|
||||
@render(response.contents)
|
||||
|
||||
|
||||
@@ -23,10 +23,19 @@
|
||||
Help your classmates by writing hints for this problem. Start by picking one of your previous incorrect answers from below:
|
||||
</p>
|
||||
|
||||
<%
|
||||
def unspace(string):
|
||||
"""
|
||||
HTML id's can't have spaces in them. This little function
|
||||
removes spaces.
|
||||
"""
|
||||
return ''.join(string.split())
|
||||
%>
|
||||
|
||||
<div id="answer-tabs">
|
||||
<ul>
|
||||
% for answer in answer_to_hints:
|
||||
<li><a href="#previous-answer-${answer}"> ${answer} </a></li>
|
||||
<li><a href="#previous-answer-${unspace(answer)}"> ${answer} </a></li>
|
||||
% endfor
|
||||
</ul>
|
||||
|
||||
@@ -35,7 +44,7 @@
|
||||
import json
|
||||
all_pks = json.dumps(pk_dict.keys())
|
||||
%>
|
||||
<div class = "previous-answer" id="previous-answer-${answer}" data-all-pks='${all_pks}'>
|
||||
<div class = "previous-answer" id="previous-answer-${unspace(answer)}" data-answer="${answer}" data-all-pks='${all_pks}'>
|
||||
<div class = "hint-inner-container">
|
||||
% if len(pk_dict) > 0:
|
||||
<p>
|
||||
@@ -54,7 +63,7 @@
|
||||
<p>
|
||||
What hint would you give a student who made the same mistake you did? Please don't give away the answer.
|
||||
</p>
|
||||
<textarea cols="50" class="custom-hint" id="custom-hint-${answer}">
|
||||
<textarea cols="50" class="custom-hint" data-answer="${answer}">
|
||||
What would you say to help someone who got this wrong answer?
|
||||
(Don't give away the answer, please.)
|
||||
</textarea>
|
||||
|
||||
@@ -10,11 +10,14 @@ import re
|
||||
|
||||
from django.http import HttpResponse, Http404
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
|
||||
from courseware.courses import get_course_with_access
|
||||
from courseware.models import XModuleContentField
|
||||
from courseware.module_render import get_module
|
||||
from courseware.model_data import ModelDataCache
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
@@ -28,24 +31,29 @@ def hint_manager(request, course_id):
|
||||
return HttpResponse(out)
|
||||
if request.method == 'GET':
|
||||
out = get_hints(request, course_id, 'mod_queue')
|
||||
return render_to_response('courseware/hint_manager.html', out)
|
||||
out.update({'error': ''})
|
||||
return render_to_response('instructor/hint_manager.html', out)
|
||||
field = request.POST['field']
|
||||
if not (field == 'mod_queue' or field == 'hints'):
|
||||
# Invalid field. (Don't let users continue - they may overwrite other db's)
|
||||
out = 'Error in hint manager - an invalid field was accessed.'
|
||||
return HttpResponse(out)
|
||||
|
||||
if request.POST['op'] == 'delete hints':
|
||||
delete_hints(request, course_id, field)
|
||||
if request.POST['op'] == 'switch fields':
|
||||
pass
|
||||
if request.POST['op'] == 'change votes':
|
||||
change_votes(request, course_id, field)
|
||||
if request.POST['op'] == 'add hint':
|
||||
add_hint(request, course_id, field)
|
||||
if request.POST['op'] == 'approve':
|
||||
approve(request, course_id, field)
|
||||
rendered_html = render_to_string('courseware/hint_manager_inner.html', get_hints(request, course_id, field))
|
||||
switch_dict = {
|
||||
'delete hints': delete_hints,
|
||||
'switch fields': lambda *args: None, # Takes any number of arguments, returns None.
|
||||
'change votes': change_votes,
|
||||
'add hint': add_hint,
|
||||
'approve': approve,
|
||||
}
|
||||
|
||||
# Do the operation requested, and collect any error messages.
|
||||
error_text = switch_dict[request.POST['op']](request, course_id, field)
|
||||
if error_text is None:
|
||||
error_text = ''
|
||||
render_dict = get_hints(request, course_id, field)
|
||||
render_dict.update({'error': error_text})
|
||||
rendered_html = render_to_string('instructor/hint_manager_inner.html', render_dict)
|
||||
return HttpResponse(json.dumps({'success': True, 'contents': rendered_html}))
|
||||
|
||||
|
||||
@@ -106,8 +114,27 @@ def get_hints(request, course_id, field):
|
||||
# Put all non-numerical answers first.
|
||||
return float('-inf')
|
||||
|
||||
# Answer list contains [answer, dict_of_hints] pairs.
|
||||
answer_list = sorted(json.loads(hints_by_problem.value).items(), key=answer_sorter)
|
||||
# Find the signature to answer converter for this problem. Sometimes,
|
||||
# it doesn't exist; just assume that the signatures are the answers.
|
||||
try:
|
||||
signature_to_ans = XModuleContentField.objects.get(
|
||||
field_name='signature_to_ans',
|
||||
definition_id__regex=chopped_id
|
||||
)
|
||||
signature_to_ans = json.loads(signature_to_ans.value)
|
||||
except ObjectDoesNotExist:
|
||||
signature_to_ans = {}
|
||||
|
||||
signatures_dict = json.loads(hints_by_problem.value)
|
||||
unsorted = []
|
||||
for signature, dict_of_hints in signatures_dict.items():
|
||||
if signature in signature_to_ans:
|
||||
ans_txt = signature_to_ans[signature]
|
||||
else:
|
||||
ans_txt = signature
|
||||
unsorted.append([signature, ans_txt, dict_of_hints])
|
||||
# Answer list contains [signature, answer, dict_of_hints] sub-lists.
|
||||
answer_list = sorted(unsorted, key=answer_sorter)
|
||||
big_out_dict[hints_by_problem.definition_id] = answer_list
|
||||
|
||||
render_dict = {'field': field,
|
||||
@@ -138,7 +165,7 @@ def delete_hints(request, course_id, field):
|
||||
Deletes the hints specified.
|
||||
|
||||
`request.POST` contains some fields keyed by integers. Each such field contains a
|
||||
[problem_defn_id, answer, pk] tuple. These tuples specify the hints to be deleted.
|
||||
[problem_defn_id, signature, pk] tuple. These tuples specify the hints to be deleted.
|
||||
|
||||
Example `request.POST`:
|
||||
{'op': 'delete_hints',
|
||||
@@ -150,12 +177,12 @@ def delete_hints(request, course_id, field):
|
||||
for key in request.POST:
|
||||
if key == 'op' or key == 'field':
|
||||
continue
|
||||
problem_id, answer, pk = request.POST.getlist(key)
|
||||
problem_id, signature, pk = request.POST.getlist(key)
|
||||
# Can be optimized - sort the delete list by problem_id, and load each problem
|
||||
# from the database only once.
|
||||
this_problem = XModuleContentField.objects.get(field_name=field, definition_id=problem_id)
|
||||
problem_dict = json.loads(this_problem.value)
|
||||
del problem_dict[answer][pk]
|
||||
del problem_dict[signature][pk]
|
||||
this_problem.value = json.dumps(problem_dict)
|
||||
this_problem.save()
|
||||
|
||||
@@ -164,18 +191,18 @@ def change_votes(request, course_id, field):
|
||||
"""
|
||||
Updates the number of votes.
|
||||
|
||||
The numbered fields of `request.POST` contain [problem_id, answer, pk, new_votes] tuples.
|
||||
The numbered fields of `request.POST` contain [problem_id, signature, pk, new_votes] tuples.
|
||||
- Very similar to `delete_hints`. Is there a way to merge them? Nah, too complicated.
|
||||
"""
|
||||
|
||||
for key in request.POST:
|
||||
if key == 'op' or key == 'field':
|
||||
continue
|
||||
problem_id, answer, pk, new_votes = request.POST.getlist(key)
|
||||
problem_id, signature, pk, new_votes = request.POST.getlist(key)
|
||||
this_problem = XModuleContentField.objects.get(field_name=field, definition_id=problem_id)
|
||||
problem_dict = json.loads(this_problem.value)
|
||||
# problem_dict[answer][pk] points to a [hint_text, #votes] pair.
|
||||
problem_dict[answer][pk][1] = int(new_votes)
|
||||
# problem_dict[signature][pk] points to a [hint_text, #votes] pair.
|
||||
problem_dict[signature][pk][1] = int(new_votes)
|
||||
this_problem.value = json.dumps(problem_dict)
|
||||
this_problem.save()
|
||||
|
||||
@@ -187,6 +214,7 @@ def add_hint(request, course_id, field):
|
||||
field
|
||||
problem - The problem id
|
||||
answer - The answer to which a hint will be added
|
||||
- Needs to be converted into signature first.
|
||||
hint - The text of the hint
|
||||
"""
|
||||
|
||||
@@ -200,10 +228,23 @@ def add_hint(request, course_id, field):
|
||||
hint_pk_entry.value = this_pk + 1
|
||||
hint_pk_entry.save()
|
||||
|
||||
# Make signature. This is really annoying, but I don't see
|
||||
# any alternative :(
|
||||
loc = Location(problem_id)
|
||||
descriptors = modulestore().get_items(loc)
|
||||
m_d_c = ModelDataCache(descriptors, course_id, request.user)
|
||||
hinter_module = get_module(request.user, request, loc, m_d_c, course_id)
|
||||
signature = hinter_module.answer_signature(answer)
|
||||
if signature is None:
|
||||
# Signature generation failed.
|
||||
# We should probably return an error message, too... working on that.
|
||||
return 'Error - your answer could not be parsed as a formula expression.'
|
||||
hinter_module.add_signature(signature, answer)
|
||||
|
||||
problem_dict = json.loads(this_problem.value)
|
||||
if answer not in problem_dict:
|
||||
problem_dict[answer] = {}
|
||||
problem_dict[answer][this_pk] = [hint_text, 1]
|
||||
if signature not in problem_dict:
|
||||
problem_dict[signature] = {}
|
||||
problem_dict[signature][this_pk] = [hint_text, 1]
|
||||
this_problem.value = json.dumps(problem_dict)
|
||||
this_problem.save()
|
||||
|
||||
@@ -213,26 +254,26 @@ def approve(request, course_id, field):
|
||||
Approve a list of hints, moving them from the mod_queue to the real
|
||||
hint list. POST:
|
||||
op, field
|
||||
(some number) -> [problem, answer, pk]
|
||||
(some number) -> [problem, signature, pk]
|
||||
"""
|
||||
|
||||
for key in request.POST:
|
||||
if key == 'op' or key == 'field':
|
||||
continue
|
||||
problem_id, answer, pk = request.POST.getlist(key)
|
||||
problem_id, signature, pk = request.POST.getlist(key)
|
||||
# Can be optimized - sort the delete list by problem_id, and load each problem
|
||||
# from the database only once.
|
||||
problem_in_mod = XModuleContentField.objects.get(field_name=field, definition_id=problem_id)
|
||||
problem_dict = json.loads(problem_in_mod.value)
|
||||
hint_to_move = problem_dict[answer][pk]
|
||||
del problem_dict[answer][pk]
|
||||
hint_to_move = problem_dict[signature][pk]
|
||||
del problem_dict[signature][pk]
|
||||
problem_in_mod.value = json.dumps(problem_dict)
|
||||
problem_in_mod.save()
|
||||
|
||||
problem_in_hints = XModuleContentField.objects.get(field_name='hints', definition_id=problem_id)
|
||||
problem_dict = json.loads(problem_in_hints.value)
|
||||
if answer not in problem_dict:
|
||||
problem_dict[answer] = {}
|
||||
problem_dict[answer][pk] = hint_to_move
|
||||
if signature not in problem_dict:
|
||||
problem_dict[signature] = {}
|
||||
problem_dict[signature][pk] = hint_to_move
|
||||
problem_in_hints.value = json.dumps(problem_dict)
|
||||
problem_in_hints.save()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%inherit file="/main.html" />
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
<%namespace name="content" file="/courseware/hint_manager_inner.html"/>
|
||||
<%namespace name="content" file="/instructor/hint_manager_inner.html"/>
|
||||
|
||||
|
||||
<%block name="headextra">
|
||||
@@ -4,15 +4,16 @@
|
||||
<h1> ${field_label} </h1>
|
||||
Switch to <a id="switch-fields" other-field="${other_field}">${other_field_label}</a>
|
||||
|
||||
<p style="color:red"> ${error} </p>
|
||||
|
||||
% for definition_id in all_hints:
|
||||
<h2> Problem: ${id_to_name[definition_id]} </h2>
|
||||
% for answer, hint_dict in all_hints[definition_id]:
|
||||
% for signature, answer, hint_dict in all_hints[definition_id]:
|
||||
% if len(hint_dict) > 0:
|
||||
<h4> Answer: ${answer} </h4><div style="background-color:#EEEEEE">
|
||||
% endif
|
||||
% for pk, hint in hint_dict.items():
|
||||
<p data-problem="${definition_id}" data-pk="${pk}" data-answer="${answer}">
|
||||
<p data-problem="${definition_id}" data-pk="${pk}" data-answer="${signature}">
|
||||
<input class="hint-select" type="checkbox"/> ${hint[0]}
|
||||
<br />
|
||||
Votes: <input type="text" class="votes" value="${str(hint[1])}" style="font-size:12px; height:20px; width:50px"></input>
|
||||
@@ -36,6 +37,7 @@ Switch to <a id="switch-fields" other-field="${other_field}">${other_field_label
|
||||
<br />
|
||||
% endfor
|
||||
|
||||
<p style="color:red"> ${error} </p>
|
||||
<button id="hint-delete"> Delete selected </button> <button id="update-votes"> Update votes </button>
|
||||
|
||||
% if field == 'mod_queue':
|
||||
Reference in New Issue
Block a user