Skip to content
Snippets Groups Projects
Commit ba06c899 authored by cahrens's avatar cahrens
Browse files

Delete crowdsource_hinter prototype xmodule.

TNL-4195
parent b7856906
No related merge requests found
Showing
with 1 addition and 2136 deletions
......@@ -35,7 +35,6 @@ XMODULES = [
"word_cloud = xmodule.word_cloud_module:WordCloudDescriptor",
"hidden = xmodule.hidden_module:HiddenDescriptor",
"raw = xmodule.raw_module:RawDescriptor",
"crowdsource_hinter = xmodule.crowdsource_hinter:CrowdsourceHinterDescriptor",
"lti = xmodule.lti_module:LTIDescriptor",
]
XBLOCKS = [
......
"""
Adds crowdsourced hinting functionality to lon-capa numerical response problems.
Currently experimental - not for instructor use, yet.
"""
import logging
import json
import random
import copy
from pkg_resources import resource_string
from lxml import etree
from xmodule.x_module import XModule, STUDENT_VIEW
from xmodule.raw_module import RawDescriptor
from xblock.fields import Scope, String, Integer, Boolean, Dict, List
from capa.responsetypes import FormulaResponse
from django.utils.html import escape
log = logging.getLogger(__name__)
class CrowdsourceHinterFields(object):
"""Defines fields for the crowdsource hinter module."""
has_children = True
moderate = String(help='String "True"/"False" - activates moderation', scope=Scope.content,
default='False')
debug = String(help='String "True"/"False" - allows multiple voting', scope=Scope.content,
default='False')
# Usage: hints[answer] = {str(pk): [hint_text, #votes]}
# hints is a dictionary that takes answer keys.
# Each value is itself a dictionary, accepting hint_pk strings as keys,
# and returning [hint text, #votes] pairs as values
hints = Dict(help='A dictionary containing all the active hints.', scope=Scope.content, default={})
mod_queue = Dict(help='A dictionary containing hints still awaiting approval', scope=Scope.content,
default={})
hint_pk = Integer(help='Used to index hints.', scope=Scope.content, default=0)
# A list of previous hints that a student viewed.
# Of the form [answer, [hint_pk_1, ...]] for each problem.
# Sorry about the variable name - I know it's confusing.
previous_answers = List(help='A list of hints viewed.', scope=Scope.user_state, default=[])
# user_submissions actually contains a list of previous answers submitted.
# (Originally, preivous_answers did this job, hence the name confusion.)
user_submissions = List(help='A list of previous submissions', scope=Scope.user_state, default=[])
user_voted = Boolean(help='Specifies if the user has voted on this problem or not.',
scope=Scope.user_state, default=False)
class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
"""
An Xmodule that makes crowdsourced hints.
Currently, only works on capa problems with exactly one numerical response,
and no other parts.
Example usage:
<crowdsource_hinter>
<problem blah blah />
</crowdsource_hinter>
XML attributes:
-moderate="True" will not display hints until staff approve them in the hint manager.
-debug="True" will let users vote as often as they want.
"""
icon_class = 'crowdsource_hinter'
css = {'scss': [resource_string(__name__, 'css/crowdsource_hinter/display.scss')]}
js = {'coffee': [resource_string(__name__, 'js/src/crowdsource_hinter/display.coffee')],
'js': []}
js_module_name = "Hinter"
def __init__(self, *args, **kwargs):
super(CrowdsourceHinterModule, self).__init__(*args, **kwargs)
# We need to know whether we are working with a FormulaResponse problem.
try:
responder = self.get_display_items()[0].lcp.responders.values()[0]
except (IndexError, AttributeError):
log.exception('Unable to find a capa problem child.')
return
self.is_formula = isinstance(self, FormulaResponse)
if self.is_formula:
self.answer_to_str = self.formula_answer_to_str
else:
self.answer_to_str = self.numerical_answer_to_str
# compare_answer is expected to return whether its two inputs are close enough
# to be equal, or raise a StudentInputError if one of the inputs is malformatted.
if hasattr(responder, 'compare_answer') and hasattr(responder, 'validate_answer'):
self.compare_answer = responder.compare_answer
self.validate_answer = responder.validate_answer
else:
# This response type is not supported!
log.exception('Response type not supported for hinting: ' + str(responder))
def get_html(self):
"""
Puts a wrapper around the problem html. This wrapper includes ajax urls of the
hinter and of the problem.
- Dependent on lon-capa problem.
"""
if self.debug == 'True':
# Reset the user vote, for debugging only!
self.user_voted = False
if self.hints == {}:
# Force self.hints to be written into the database. (When an xmodule is initialized,
# fields are not added to the db until explicitly changed at least once.)
self.hints = {}
try:
child = self.get_display_items()[0]
out = child.render(STUDENT_VIEW).content
# The event listener uses the ajax url to find the child.
child_id = child.id
except IndexError:
out = u"Error in loading crowdsourced hinter - can't find child problem."
child_id = ''
# Wrap the module in a <section>. This lets us pass data attributes to the javascript.
out += u'<section class="crowdsource-wrapper" data-url="{ajax_url}" data-child-id="{child_id}"> </section>'.format(
ajax_url=self.runtime.ajax_url,
child_id=child_id
)
return out
def numerical_answer_to_str(self, answer):
"""
Converts capa numerical answer format to a string representation
of the answer.
-Lon-capa dependent.
-Assumes that the problem only has one part.
"""
return str(answer.values()[0])
def formula_answer_to_str(self, answer):
"""
Converts capa formula answer into a string.
-Lon-capa dependent.
-Assumes that the problem only has one part.
"""
return str(answer.values()[0])
def get_matching_answers(self, answer):
"""
Look in self.hints, and find all answer keys that are "equal with tolerance"
to the input answer.
"""
return [key for key in self.hints if self.compare_answer(key, answer)]
def handle_ajax(self, dispatch, data):
"""
This is the landing method for AJAX calls.
"""
if dispatch == 'get_hint':
out = self.get_hint(data)
elif dispatch == 'get_feedback':
out = self.get_feedback(data)
elif dispatch == 'vote':
out = self.tally_vote(data)
elif dispatch == 'submit_hint':
out = self.submit_hint(data)
else:
return json.dumps({'contents': 'Error - invalid operation.'})
if out is None:
out = {'op': 'empty'}
elif 'error' in out:
# Error in processing.
out.update({'op': 'error'})
else:
out.update({'op': dispatch})
return json.dumps({'contents': self.runtime.render_template('hinter_display.html', out)})
def get_hint(self, data):
"""
The student got the incorrect answer found in data. Give him a hint.
Called by hinter javascript after a problem is graded as incorrect.
Args:
`data` -- must be interpretable by answer_to_str.
Output keys:
- 'hints' is a list of hint strings to show to the user.
- 'answer' is the parsed answer that was submitted.
Will record the user's wrong answer in user_submissions, and the hints shown
in previous_answers.
"""
# First, validate our inputs.
try:
answer = self.answer_to_str(data)
except (ValueError, AttributeError):
# Sometimes, we get an answer that's just not parsable. Do nothing.
log.exception('Answer not parsable: ' + str(data))
return
if not self.validate_answer(answer):
# Answer is not in the right form.
log.exception('Answer not valid: ' + str(answer))
return
if answer not in self.user_submissions:
self.user_submissions += [answer]
# For all answers similar enough to our own, accumulate all hints together.
# Also track the original answer of each hint.
matching_answers = self.get_matching_answers(answer)
matching_hints = {}
for matching_answer in matching_answers:
temp_dict = copy.deepcopy(self.hints[matching_answer])
for key, value in temp_dict.items():
# Each value now has hint, votes, matching_answer.
temp_dict[key] = value + [matching_answer]
matching_hints.update(temp_dict)
# matching_hints now maps pk's to lists of [hint, votes, matching_answer]
# Finally, randomly choose a subset of matching_hints to actually show.
if not matching_hints:
# No hints to give. Return.
return
# Get the top hint, plus two random hints.
n_hints = len(matching_hints)
hints = []
# max(dict) returns the maximum key in dict.
# The key function takes each pk, and returns the number of votes for the
# hint with that pk.
best_hint_index = max(matching_hints, key=lambda pk: matching_hints[pk][1])
hints.append(matching_hints[best_hint_index][0])
best_hint_answer = matching_hints[best_hint_index][2]
# The brackets surrounding the index are for backwards compatability purposes.
# (It used to be that each answer was paired with multiple hints in a list.)
self.previous_answers += [[best_hint_answer, [best_hint_index]]]
for _ in xrange(min(2, n_hints - 1)):
# Keep making random hints until we hit a target, or run out.
while True:
# random.choice randomly chooses an element from its input list.
# (We then unpack the item, in this case data for a hint.)
(hint_index, (rand_hint, _, hint_answer)) =\
random.choice(matching_hints.items())
if rand_hint not in hints:
break
hints.append(rand_hint)
self.previous_answers += [[hint_answer, [hint_index]]]
return {'hints': hints,
'answer': answer}
def get_feedback(self, data):
"""
The student got it correct. Ask him to vote on hints, or submit a hint.
Args:
`data` -- not actually used. (It is assumed that the answer is correct.)
Output keys:
- 'answer_to_hints': a nested dictionary.
answer_to_hints[answer][hint_pk] returns the text of the hint.
- 'user_submissions': the same thing as self.user_submissions. A list of
the answers that the user previously submitted.
"""
# The student got it right.
# Did he submit at least one wrong answer?
if len(self.user_submissions) == 0:
# No. Nothing to do here.
return
# Make a hint-voting interface for each wrong answer. The student will only
# be allowed to make one vote / submission, but he can choose which wrong answer
# he wants to look at.
answer_to_hints = {} # answer_to_hints[answer text][hint pk] -> hint text
# Go through each previous answer, and populate index_to_hints and index_to_answer.
for i in xrange(len(self.previous_answers)):
answer, hints_offered = self.previous_answers[i]
if answer not in answer_to_hints:
answer_to_hints[answer] = {}
if answer 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[answer]):
try:
answer_to_hints[answer][hint_id] = self.hints[answer][str(hint_id)][0]
except KeyError:
# Sometimes, the hint that a user saw will have been deleted by the instructor.
continue
return {'answer_to_hints': answer_to_hints,
'user_submissions': self.user_submissions}
def tally_vote(self, data):
"""
Tally a user's vote on his favorite hint.
Args:
`data` -- expected to have the following keys:
'answer': text of answer we're voting on
'hint': hint_pk
'pk_list': A list of [answer, pk] pairs, each of which representing a hint.
We will return a list of how many votes each hint in the list has so far.
It's up to the browser to specify which hints to return vote counts for.
Returns key 'hint_and_votes', a list of (hint_text, #votes) pairs.
"""
if self.user_voted:
return {'error': 'Sorry, but you have already voted!'}
ans = data['answer']
if not self.validate_answer(ans):
# Uh oh. Invalid answer.
log.exception('Failure in hinter tally_vote: Unable to parse answer: {ans}'.format(ans=ans))
return {'error': 'Failure in voting!'}
hint_pk = str(data['hint'])
# We use temp_dict because we need to do a direct write for the database to update.
temp_dict = self.hints
try:
temp_dict[ans][hint_pk][1] += 1
except KeyError:
log.exception('''Failure in hinter tally_vote: User voted for non-existant hint:
Answer={ans} pk={hint_pk}'''.format(ans=ans, hint_pk=hint_pk))
return {'error': 'Failure in voting!'}
self.hints = temp_dict
# Don't let the user vote again!
self.user_voted = True
# Return a list of how many votes each hint got.
pk_list = json.loads(data['pk_list'])
hint_and_votes = []
for answer, vote_pk in pk_list:
if not self.validate_answer(answer):
log.exception('In hinter tally_vote, couldn\'t parse {ans}'.format(ans=answer))
continue
try:
hint_and_votes.append(temp_dict[answer][str(vote_pk)])
except KeyError:
log.exception('In hinter tally_vote, couldn\'t find: {ans}, {vote_pk}'.format(
ans=answer, vote_pk=str(vote_pk)))
hint_and_votes.sort(key=lambda pair: pair[1], reverse=True)
# Reset self.previous_answers and user_submissions.
self.previous_answers = []
self.user_submissions = []
return {'hint_and_votes': hint_and_votes}
def submit_hint(self, data):
"""
Take a hint submission and add it to the database.
Args:
`data` -- expected to have the following keys:
'answer': text of answer
'hint': text of the new hint that the user is adding
Returns a thank-you message.
"""
# Do html escaping. Perhaps in the future do profanity filtering, etc. as well.
hint = escape(data['hint'])
answer = data['answer']
if not self.validate_answer(answer):
log.exception('Failure in hinter submit_hint: Unable to parse answer: {ans}'.format(
ans=answer))
return {'error': 'Could not submit answer'}
# Only allow a student to vote or submit a hint once.
if self.user_voted:
return {'message': 'Sorry, but you have already voted!'}
# Add the new hint to self.hints or self.mod_queue. (Awkward because a direct write
# is necessary.)
if self.moderate == 'True':
temp_dict = self.mod_queue
else:
temp_dict = self.hints
if answer in temp_dict:
temp_dict[answer][str(self.hint_pk)] = [hint, 1] # With one vote (the user himself).
else:
temp_dict[answer] = {str(self.hint_pk): [hint, 1]}
self.hint_pk += 1
if self.moderate == 'True':
self.mod_queue = temp_dict
else:
self.hints = temp_dict
# Mark the user has having voted; reset previous_answers
self.user_voted = True
self.previous_answers = []
self.user_submissions = []
return {'message': 'Thank you for your hint!'}
class CrowdsourceHinterDescriptor(CrowdsourceHinterFields, RawDescriptor):
module_class = CrowdsourceHinterModule
stores_state = True
@classmethod
def definition_from_xml(cls, xml_object, system):
children = []
for child in xml_object:
try:
child_block = system.process_xml(etree.tostring(child, encoding='unicode'))
children.append(child_block.scope_ids.usage_id)
except Exception as e:
log.exception("Unable to load child when parsing CrowdsourceHinter. Continuing...")
if system.error_tracker is not None:
system.error_tracker(u"ERROR: {0}".format(e))
continue
return {}, children
def definition_to_xml(self, resource_fs):
xml_object = etree.Element('crowdsource_hinter')
for child in self.get_children():
self.runtime.add_block_as_child_node(child, xml_object)
return xml_object
.crowdsource-wrapper {
box-shadow: inset 0 1px 2px 1px rgba(0,0,0,0.1);
border-radius: 2px;
display: none;
margin-top: $baseline;
padding: ($baseline*0.75);
background: rgb(253, 248, 235);
}
.hint-inner-container {
padding-left: 15px;
padding-right: 15px;
font-size: 16px;
}
.vote {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.wizard-view {
float: left;
width: 790px;
margin-right: ($baseline/2);
}
.wizard-container {
width: 3000px;
-webkit-transition:all $tmg-s1 ease-in-out;
-moz-transition:all $tmg-s1 ease-in-out;
-o-transition:all $tmg-s1 ease-in-out;
transition:all $tmg-s1 ease-in-out;
}
.wizard-viewbox {
width: 800px;
overflow: hidden;
position: relative;
}
<li id="vert-0" data-id="i4x://Me/19.002/crowdsource_hinter/crowdsource_hinter_def7a1142dd0">
<section class="xblock xblock-student_view xmodule_display xmodule_CrowdsourceHinterModule" data-type="Hinter" id="hinter-root">
<section class="xblock xblock-student_view xmodule_display xmodule_CapaModule" data-type="Problem" id="problem">
<section id="problem_i4x-Me-19_002-problem-Numerical_Input" class="problems-wrapper" data-problem-id="i4x://Me/19.002/problem/Numerical_Input" data-url="/courses/Me/19.002/Test/modx/i4x://Me/19.002/problem/Numerical_Input" data-progress_status="done" data-progress_detail="1/1">
<h3 class="hd hd-2 problem-header">
Numerical Input
</h3>
<section class="problem-progress">(1/1 point)</section>
<section class="problem">
<div><p>The answer is 2*x^2*y + 5
</p><span><br><span> Answer =</span>
<section id="inputtype_i4x-Me-19_002-problem-Numerical_Input_2_1" class="text-input-dynamath capa_inputtype ">
<div class="correct " id="status_i4x-Me-19_002-problem-Numerical_Input_2_1">
<input type="text" name="input_i4x-Me-19_002-problem-Numerical_Input_2_1" id="input_i4x-Me-19_002-problem-Numerical_Input_2_1" aria-describedby="answer_i4x-Me-19_002-problem-Numerical_Input_2_1" value="2*x^2*y +5" class="math" size="40">
</div></section></span>
<input type="file" />
<section class="solution-span"><span id="solution_i4x-Me-19_002-problem-Numerical_Input_solution_1"></span></section></div>
<section class="action">
<input type="hidden" name="problem_id" value="Numerical Input">
<input class="check Check" type="button" value="Check">
<button class="show"><span class="show-label">Show Answer(s)</span> <span class="sr">(for question(s) above - adjacent to each field)</span></button>
</section>
</section>
</section>
</section>
<div id="i4x_Me_19_002_problem_Numerical_Input_setup"></div>
<section class="crowdsource-wrapper" data-url="/courses/Me/19.002/Test/modx/i4x://Me/19.002/crowdsource_hinter/crowdsource_hinter_def7a1142dd0" data-child-id="i4x://Me/19.002/problem/Numerical_Input" style="display: none;"> </section>
</section>
</li>
describe 'Crowdsourced hinter', ->
beforeEach ->
window.update_schematics = ->
jasmine.stubRequests()
# note that the fixturesPath is set in spec/helper.coffee
loadFixtures 'crowdsource_hinter.html'
@hinter = new Hinter($('#hinter-root'))
describe 'high-level integration tests', ->
# High-level, happy-path tests for integration with capa problems.
beforeEach ->
# Make a more thorough $.postWithPrefix mock.
spyOn($, 'postWithPrefix').andCallFake( ->
last_argument = arguments[arguments.length - 1]
if typeof last_argument == 'function'
response =
success: 'incorrect'
contents: 'mock grader response'
last_argument(response)
promise =
always: (callable) -> callable()
done: (callable) -> callable()
)
@problem = new Problem($('#problem'))
@problem.bind()
it 'knows when a capa problem is graded, using check.', ->
@problem.answers = 'test answer'
@problem.check()
expect($.postWithPrefix).toHaveBeenCalledWith("#{@hinter.url}/get_hint", 'test answer', jasmine.any(Function))
it 'knows when a capa problem is graded usig check_fd.', ->
spyOn($, 'ajaxWithPrefix').andCallFake((url, settings) ->
response =
success: 'incorrect'
contents: 'mock grader response'
settings.success(response) if settings
)
@problem.answers = 'test answer'
@problem.check_fd()
expect($.postWithPrefix).toHaveBeenCalledWith("#{@hinter.url}/get_hint", 'test answer', jasmine.any(Function))
describe 'capture_problem', ->
beforeEach ->
spyOn($, 'postWithPrefix').andReturn(null)
it 'gets hints for an incorrect answer', ->
data = ['some answers', '<thing class="incorrect">']
@hinter.capture_problem('problem_graded', data, 'fake element')
expect($.postWithPrefix).toHaveBeenCalledWith("#{@hinter.url}/get_hint", 'some answers', jasmine.any(Function))
it 'gets feedback for a correct answer', ->
data = ['some answers', '<thing class="correct">']
@hinter.capture_problem('problem_graded', data, 'fake element')
expect($.postWithPrefix).toHaveBeenCalledWith("#{@hinter.url}/get_feedback", 'some answers', jasmine.any(Function))
class @Hinter
# The client side code for the crowdsource_hinter.
# Contains code for capturing problem checks and making ajax calls to
# the server component. Also contains styling code to clear default
# text on a textarea.
constructor: (element) ->
@el = $(element).find('.crowdsource-wrapper')
@url = @el.data('url')
Logger.listen('problem_graded', @el.data('child-id'), @capture_problem)
@render()
capture_problem: (event_type, data, element) =>
# After a problem gets graded, we get the info here.
# We want to send this info to the server in another AJAX
# request.
answers = data[0]
response = data[1]
if response.search(/class="correct/) == -1
# Incorrect. Get hints.
$.postWithPrefix "#{@url}/get_hint", answers, (response) =>
@render(response.contents)
else
# Correct. Get feedback from students.
$.postWithPrefix "#{@url}/get_feedback", answers, (response) =>
@render(response.contents)
$: (selector) ->
$(selector, @el)
bind: =>
@$('input.vote').click @vote
@$('input.submit-hint').click @submit_hint
@$('.custom-hint').click @clear_default_text
@$('.expand').click @expand
@$('.wizard-link').click @wizard_link_handle
@$('.answer-choice').click @answer_choice_handle
expand: (eventObj) =>
# Expand a hidden div.
target = @$('#' + @$(eventObj.currentTarget).data('target'))
if @$(target).css('display') == 'none'
@$(target).css('display', 'block')
else
@$(target).css('display', 'none')
# Fix positioning errors with the bottom class.
@set_bottom_links()
vote: (eventObj) =>
# Make an ajax request with the user's vote.
target = @$(eventObj.currentTarget)
all_pks = @$('#pk-list').attr('data-pk-list')
post_json = {'answer': target.attr('data-answer'), 'hint': target.data('hintno'), 'pk_list': all_pks}
$.postWithPrefix "#{@url}/vote", post_json, (response) =>
@render(response.contents)
submit_hint: (eventObj) =>
# Make an ajax request with the user's new hint.
textarea = $('.custom-hint')
if @answer == ''
# The user didn't choose an answer, somehow. Do nothing.
return
post_json = {'answer': @answer, 'hint': textarea.val()}
$.postWithPrefix "#{@url}/submit_hint",post_json, (response) =>
@render(response.contents)
clear_default_text: (eventObj) =>
# Remove placeholder text in the hint submission textbox.
target = @$(eventObj.currentTarget)
if target.data('cleared') == undefined
target.val('')
target.data('cleared', true)
wizard_link_handle: (eventObj) =>
# Move to another wizard view, based on the link that the user clicked.
target = @$(eventObj.currentTarget)
@go_to(target.attr('dest'))
answer_choice_handle: (eventObj) =>
# A special case of wizard_link_handle - we need to track a state variable,
# the answer that the user chose.
@answer = @$(eventObj.target).attr('value')
@$('#blank-answer').html(@answer)
@go_to('p3')
set_bottom_links: =>
# Makes each .bottom class stick to the bottom of .wizard-viewbox
@$('.bottom').css('margin-top', '0px')
viewbox_height = parseInt(@$('.wizard-viewbox').css('height'), 10)
@$('.bottom').each((index, obj) ->
view_height = parseInt($(obj).parent().css('height'), 10)
$(obj).css('margin-top', (viewbox_height - view_height) + 'px')
)
render: (content) ->
if content
# Trim leading and trailing whitespace
content = content.trim()
if content
@el.html(content)
@el.show()
JavascriptLoader.executeModuleScripts @el, () =>
@bind()
@$('#previous-answer-0').css('display', 'inline')
else
@el.hide()
# Initialize the answer choice - remembers which answer the user picked on
# p2 when he submits a hint on p3.
@answer = ''
# Determine whether the browser supports CSS3 transforms.
styles = document.body.style
if styles.WebkitTransform == '' or styles.transform == ''
@go_to = @transform_go_to
else
@go_to = @legacy_go_to
# Make the correct wizard view show up.
hints_exist = @$('#hints-exist').html() == 'True'
if hints_exist
@go_to('p1')
else
@go_to('p2')
transform_go_to: (view_id) ->
# Switch wizard views using sliding transitions.
id_to_index = {
'p1': 0,
'p2': 1,
'p3': 2,
}
translate_string = 'translateX(' +id_to_index[view_id] * -1 * parseInt($('#' + view_id).css('width'), 10) + 'px)'
@$('.wizard-container').css('transform', translate_string)
@$('.wizard-container').css('-webkit-transform', translate_string)
@set_bottom_links()
legacy_go_to: (view_id) ->
# For older browsers - switch wizard views by changing the screen.
@$('.wizard-view').css('display', 'none')
@$('#' + view_id).css('display', 'block')
@set_bottom_links()
\ No newline at end of file
This diff is collapsed.
......@@ -35,7 +35,6 @@ from xmodule.discussion_module import DiscussionDescriptor
from xmodule.html_module import HtmlDescriptor
from xmodule.poll_module import PollDescriptor
from xmodule.word_cloud_module import WordCloudDescriptor
from xmodule.crowdsource_hinter import CrowdsourceHinterDescriptor
#from xmodule.video_module import VideoDescriptor
from xmodule.seq_module import SequenceDescriptor
from xmodule.conditional_module import ConditionalDescriptor
......@@ -66,7 +65,6 @@ LEAF_XMODULES = {
CONTAINER_XMODULES = {
ConditionalDescriptor: [{}],
CourseDescriptor: [{}],
CrowdsourceHinterDescriptor: [{}],
RandomizeDescriptor: [{}],
SequenceDescriptor: [{}],
VerticalBlock: [{}],
......@@ -75,8 +73,7 @@ CONTAINER_XMODULES = {
# These modules are not editable in studio yet
NOT_STUDIO_EDITABLE = (
CrowdsourceHinterDescriptor,
PollDescriptor
PollDescriptor,
)
......
## The hinter module passes in a field called ${op}, which determines which
## sub-function to render.
<%def name="get_hint()">
% if len(hints) > 0:
<h4> Hints from students who made similar mistakes: </h4>
<ul>
% for hint in hints:
<li> ${hint} </li>
% endfor
</ul>
% endif
</%def>
<%def name="get_feedback()">
<%
def unspace(in_str):
"""
HTML id's can't have spaces in them. This little function
removes spaces.
"""
return ''.join(in_str.split())
# Make a list of all hints shown. (This is fed back to the site as pk_list.)
# At the same time, determine whether any hints were shown at all.
# If the user never saw hints, don't ask him to vote.
import json
hints_exist = False
pk_list = []
for answer, pk_dict in answer_to_hints.items():
if len(pk_dict) > 0:
hints_exist = True
for pk, hint_text in pk_dict.items():
pk_list.append([answer, pk])
json_pk_list = json.dumps(pk_list)
%>
<!-- Tells coffeescript whether there are hints to show. -->
<span id="hints-exist" style="display:none">${hints_exist}</span>
<div class="wizard-viewbox"><div class="wizard-container">
<div class="wizard-view" id="p1">
<p>
<em> Optional. </em> Help us improve our hints! Which hint was most helpful to you?
</p>
<div id="pk-list" data-pk-list='${json_pk_list}' style="display:none"> </div>
% for answer, pk_dict in answer_to_hints.items():
% for hint_pk, hint_text in pk_dict.items():
<p>
<input class="vote" data-answer="${answer}" data-hintno="${hint_pk}" type="button" value="Vote">
${hint_text}
</p>
% endfor
% endfor
<p>
Don't like any of the hints above?
<a class="wizard-link" dest="p2" href="javascript: void(0);">
Write your own!
</a></p>
</div>
<div class="wizard-view" id="p2">
% if hints_exist:
<p>
Choose the incorrect answer for which you want to write a hint:
</p>
% else:
<p>
<em>Optional.</em> Help other students by submitting a hint! Pick one of your previous
answers for which you would like to write a hint:
</p>
% endif
% for answer in user_submissions:
<a class="answer-choice" href="javascript: void(0)" value="${answer}">${answer}</a><br />
% endfor
% if hints_exist:
<p class="bottom">
<a href="javascript: void(0);" class="wizard-link" dest="p1"> Back </a>
</p>
% endif
</div>
<div class="wizard-view" id="p3">
<p>
Write a hint for other students who get the wrong answer of <span id="blank-answer"></span>.
</p>
<p>Read about <a class="expand" data-target="goodhint" href="javascript:void(0);">what makes a good hint</a>.</p>
<textarea cols="50" class="custom-hint" data-answer="${answer}" style="height: 200px">
Write your hint here. Please don't give away the correct answer.
</textarea>
<br /><br />
<input class="submit-hint" data-answer="${answer}" type="button" value="Submit">
<div id="goodhint" style="display:none">
<h4>What makes a good hint?</h4>
<p>It depends on the type of problem you ran into. For stupid errors --
an arithmetic error or similar -- simply letting the student you'll be
helping to check their signs is sufficient.</p>
<p>For deeper errors of understanding, the best hints allow students to
discover a contradiction in how they are thinking about the
problem. An example that clearly demonstrates inconsistency or
<a href="http://en.wikipedia.org/wiki/Cognitive_dissonance" target="_blank"> cognitive dissonace </a>
is ideal, although in most cases, not possible.</p>
<p>
Good hints either:
<ul>
<li> Point out the specific misunderstanding your classmate might have </li>
<li> Point to concepts or theories where your classmates might have a
misunderstanding </li>
<li> Show simpler, analogous examples. </li>
<li> Provide references to relevant parts of the text </li>
</ul>
</p>
<p>Still, remember even a crude hint -- virtually anything short of
giving away the answer -- is better than no hint.</p>
<p>
<a href="http://www.apa.org/education/k12/misconceptions.aspx?item=2" target="_blank">Learn even more</a>
</p>
</div>
<p class="bottom">
<a href="javascript: void(0);" class="wizard-link" dest="p2"> Back </a>
</p>
</div>
<!-- Close wizard contaner and wizard viewbox. -->
</div></div>
</%def>
<%def name="show_votes()">
% if hint_and_votes is UNDEFINED:
Sorry, but you've already voted!
% else:
Thank you for voting!
<br />
% for hint, votes in hint_and_votes:
<span style="color:green"> ${votes} votes. </span>
${hint}
<br />
% endfor
% endif
</%def>
<%def name="simple_message()">
${message}
</%def>
% if op == "get_hint":
${get_hint()}
% endif
% if op == "get_feedback":
${get_feedback()}
% endif
% if op == "submit_hint":
${simple_message()}
% endif
% if op == "error":
${error}
% endif
% if op == "vote":
${show_votes()}
% endif
"""
Views for hint management.
Get to these views through courseurl/hint_manager.
For example: https://courses.edx.org/courses/MITx/2.01x/2013_Spring/hint_manager
These views will only be visible if FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True
"""
import json
import re
from django.http import HttpResponse, Http404
from django.views.decorators.csrf import ensure_csrf_cookie
from edxmako.shortcuts import render_to_response, render_to_string
from courseware.courses import get_course_with_access
from courseware.models import XModuleUserStateSummaryField
import courseware.module_render as module_render
import courseware.model_data as model_data
from xmodule.modulestore.django import modulestore
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.modulestore.exceptions import ItemNotFoundError
@ensure_csrf_cookie
def hint_manager(request, course_id):
"""
The URL landing function for all calls to the hint manager, both POST and GET.
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
try:
course = get_course_with_access(request.user, 'staff', course_key, depth=None)
except Http404:
out = 'Sorry, but students are not allowed to access the hint manager!'
return HttpResponse(out)
if request.method == 'GET':
out = get_hints(request, course_key, 'mod_queue')
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)
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_key, field)
if error_text is None:
error_text = ''
render_dict = get_hints(request, course_key, field, course=course)
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}))
def get_hints(request, course_id, field, course=None): # pylint: disable=unused-argument
"""
Load all of the hints submitted to the course.
Args:
`request` -- Django request object.
`course_id` -- The course id, like 'Me/19.002/test_course'
`field` -- Either 'hints' or 'mod_queue'; specifies which set of hints to load.
Keys in returned dict:
- 'field': Same as input
- 'other_field': 'mod_queue' if `field` == 'hints'; and vice-versa.
- 'field_label', 'other_field_label': English name for the above.
- 'all_hints': A list of [answer, pk dict] pairs, representing all hints.
Sorted by answer.
- 'id_to_name': A dictionary mapping problem id to problem name.
"""
if field == 'mod_queue':
other_field = 'hints'
field_label = 'Hints Awaiting Moderation'
other_field_label = 'Approved Hints'
elif field == 'hints':
other_field = 'mod_queue'
field_label = 'Approved Hints'
other_field_label = 'Hints Awaiting Moderation'
# We want to use the course_id to find all matching usage_id's.
# To do this, just take the school/number part - leave off the classname.
# FIXME: we need to figure out how to do this with opaque keys
all_hints = XModuleUserStateSummaryField.objects.filter(
field_name=field,
usage_id__regex=re.escape(u'{0.org}/{0.course}'.format(course_id)),
)
# big_out_dict[problem id] = [[answer, {pk: [hint, votes]}], sorted by answer]
# big_out_dict maps a problem id to a list of [answer, hints] pairs, sorted in order of answer.
big_out_dict = {}
# id_to name maps a problem id to the name of the problem.
# id_to_name[problem id] = Display name of problem
id_to_name = {}
for hints_by_problem in all_hints:
hints_by_problem.usage_id = hints_by_problem.usage_id.map_into_course(course_id)
name = location_to_problem_name(course_id, hints_by_problem.usage_id)
if name is None:
continue
id_to_name[hints_by_problem.usage_id] = name
def answer_sorter(thing):
"""
`thing` is a tuple, where `thing[0]` contains an answer, and `thing[1]` contains
a dict of hints. This function returns an index based on `thing[0]`, which
is used as a key to sort the list of things.
"""
try:
return float(thing[0])
except ValueError:
# 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)
big_out_dict[hints_by_problem.usage_id] = answer_list
render_dict = {'field': field,
'other_field': other_field,
'field_label': field_label,
'other_field_label': other_field_label,
'all_hints': big_out_dict,
'id_to_name': id_to_name}
return render_dict
def location_to_problem_name(course_id, loc):
"""
Given the location of a crowdsource_hinter module, try to return the name of the
problem it wraps around. Return None if the hinter no longer exists.
"""
try:
descriptor = modulestore().get_item(loc)
return descriptor.get_children()[0].display_name
except ItemNotFoundError:
# Sometimes, the problem is no longer in the course. Just
# don't include said problem.
return None
def delete_hints(request, course_id, field, course=None): # pylint: disable=unused-argument
"""
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.
Example `request.POST`:
{'op': 'delete_hints',
'field': 'mod_queue',
1: ['problem_whatever', '42.0', '3'],
2: ['problem_whatever', '32.5', '12']}
"""
for key in request.POST:
if key == 'op' or key == 'field':
continue
problem_id, answer, pk = request.POST.getlist(key)
problem_key = course_id.make_usage_key_from_deprecated_string(problem_id)
# Can be optimized - sort the delete list by problem_id, and load each problem
# from the database only once.
this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key)
problem_dict = json.loads(this_problem.value)
del problem_dict[answer][pk]
this_problem.value = json.dumps(problem_dict)
this_problem.save()
def change_votes(request, course_id, field, course=None): # pylint: disable=unused-argument
"""
Updates the number of votes.
The numbered fields of `request.POST` contain [problem_id, answer, pk, new_votes] tuples.
See `delete_hints`.
Example `request.POST`:
{'op': 'delete_hints',
'field': 'mod_queue',
1: ['problem_whatever', '42.0', '3', 42],
2: ['problem_whatever', '32.5', '12', 9001]}
"""
for key in request.POST:
if key == 'op' or key == 'field':
continue
problem_id, answer, pk, new_votes = request.POST.getlist(key)
problem_key = course_id.make_usage_key_from_deprecated_string(problem_id)
this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key)
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)
this_problem.value = json.dumps(problem_dict)
this_problem.save()
def add_hint(request, course_id, field, course=None):
"""
Add a new hint. `request.POST`:
op
field
problem - The problem id
answer - The answer to which a hint will be added
hint - The text of the hint
"""
problem_id = request.POST['problem']
problem_key = course_id.make_usage_key_from_deprecated_string(problem_id)
answer = request.POST['answer']
hint_text = request.POST['hint']
# Validate the answer. This requires initializing the xmodules, which
# is annoying.
try:
descriptor = modulestore().get_item(problem_key)
descriptors = [descriptor]
except ItemNotFoundError:
descriptors = []
field_data_cache = model_data.FieldDataCache(descriptors, course_id, request.user)
hinter_module = module_render.get_module(
request.user,
request,
problem_key,
field_data_cache,
course_id,
course=course
)
if not hinter_module.validate_answer(answer):
# Invalid answer. Don't add it to the database, or else the
# hinter will crash when we encounter it.
return 'Error - the answer you specified is not properly formatted: ' + str(answer)
this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key)
hint_pk_entry = XModuleUserStateSummaryField.objects.get(field_name='hint_pk', usage_id=problem_key)
this_pk = int(hint_pk_entry.value)
hint_pk_entry.value = this_pk + 1
hint_pk_entry.save()
problem_dict = json.loads(this_problem.value)
if answer not in problem_dict:
problem_dict[answer] = {}
problem_dict[answer][this_pk] = [hint_text, 1]
this_problem.value = json.dumps(problem_dict)
this_problem.save()
def approve(request, course_id, field, course=None): # pylint: disable=unused-argument
"""
Approve a list of hints, moving them from the mod_queue to the real
hint list. POST:
op, field
(some number) -> [problem, answer, pk]
The numbered fields are analogous to those in `delete_hints` and `change_votes`.
"""
for key in request.POST:
if key == 'op' or key == 'field':
continue
problem_id, answer, pk = request.POST.getlist(key)
problem_key = course_id.make_usage_key_from_deprecated_string(problem_id)
# Can be optimized - sort the delete list by problem_id, and load each problem
# from the database only once.
problem_in_mod = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key)
problem_dict = json.loads(problem_in_mod.value)
hint_to_move = problem_dict[answer][pk]
del problem_dict[answer][pk]
problem_in_mod.value = json.dumps(problem_dict)
problem_in_mod.save()
problem_in_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_id=problem_key)
problem_dict = json.loads(problem_in_hints.value)
if answer not in problem_dict:
problem_dict[answer] = {}
problem_dict[answer][pk] = hint_to_move
problem_in_hints.value = json.dumps(problem_dict)
problem_in_hints.save()
import json
from django.test.client import Client, RequestFactory
from mock import patch, MagicMock
from nose.plugins.attrib import attr
from courseware.models import XModuleUserStateSummaryField
from courseware.tests.factories import UserStateSummaryFactory
import instructor.hint_manager as view
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
# pylint: disable=missing-docstring
@attr('shard_1')
class HintManagerTest(SharedModuleStoreTestCase):
@classmethod
def setUpClass(cls):
super(HintManagerTest, cls).setUpClass()
cls.course = CourseFactory.create(org='Me', number='19.002', display_name='test_course')
cls.url = '/courses/Me/19.002/test_course/hint_manager'
cls.course_id = cls.course.id
cls.problem_id = cls.course_id.make_usage_key('crowdsource_hinter', 'crowdsource_hinter_001')
def setUp(self):
"""
Makes a course, which will be the same for all tests.
Set up mako middleware, which is necessary for template rendering to happen.
"""
super(HintManagerTest, self).setUp()
self.user = UserFactory.create(username='robot', email='robot@edx.org', password='test', is_staff=True)
self.c = Client()
self.c.login(username='robot', password='test')
UserStateSummaryFactory.create(
field_name='hints',
usage_id=self.problem_id,
value=json.dumps({
'1.0': {'1': ['Hint 1', 2], '3': ['Hint 3', 12]},
'2.0': {'4': ['Hint 4', 3]}
})
)
UserStateSummaryFactory.create(
field_name='mod_queue',
usage_id=self.problem_id,
value=json.dumps({'2.0': {'2': ['Hint 2', 1]}})
)
UserStateSummaryFactory.create(
field_name='hint_pk',
usage_id=self.problem_id,
value=5
)
# Mock out location_to_problem_name, which ordinarily accesses the modulestore.
# (I can't figure out how to get fake structures into the modulestore.)
view.location_to_problem_name = lambda course_id, loc: "Test problem"
def test_student_block(self):
"""
Makes sure that students cannot see the hint management view.
"""
c = Client()
UserFactory.create(username='student', email='student@edx.org', password='test')
c.login(username='student', password='test')
out = c.get(self.url)
print out
self.assertTrue('Sorry, but students are not allowed to access the hint manager!' in out.content)
def test_staff_access(self):
"""
Makes sure that staff can access the hint management view.
"""
out = self.c.get(self.url)
print out
self.assertTrue('Hints Awaiting Moderation' in out.content)
def test_invalid_field_access(self):
"""
Makes sure that field names other than 'mod_queue' and 'hints' are
rejected.
"""
out = self.c.post(self.url, {'op': 'delete hints', 'field': 'all your private data'})
print out
self.assertTrue('an invalid field was accessed' in out.content)
def test_switchfields(self):
"""
Checks that the op: 'switch fields' POST request works.
"""
out = self.c.post(self.url, {'op': 'switch fields', 'field': 'mod_queue'})
print out
self.assertTrue('Hint 2' in out.content)
def test_gethints(self):
"""
Checks that gethints returns the right data.
"""
request = RequestFactory()
post = request.post(self.url, {'field': 'mod_queue'})
out = view.get_hints(post, self.course_id, 'mod_queue')
print out
self.assertTrue(out['other_field'] == 'hints')
expected = {self.problem_id: [(u'2.0', {u'2': [u'Hint 2', 1]})]}
self.assertTrue(out['all_hints'] == expected)
def test_gethints_other(self):
"""
Same as above, with hints instead of mod_queue
"""
request = RequestFactory()
post = request.post(self.url, {'field': 'hints'})
out = view.get_hints(post, self.course_id, 'hints')
print out
self.assertTrue(out['other_field'] == 'mod_queue')
expected = {self.problem_id: [('1.0', {'1': ['Hint 1', 2],
'3': ['Hint 3', 12]}),
('2.0', {'4': ['Hint 4', 3]})
]}
self.assertTrue(out['all_hints'] == expected)
def test_deletehints(self):
"""
Checks that delete_hints deletes the right stuff.
"""
request = RequestFactory()
post = request.post(self.url, {'field': 'hints',
'op': 'delete hints',
1: [self.problem_id.to_deprecated_string(), '1.0', '1']})
view.delete_hints(post, self.course_id, 'hints')
problem_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_id=self.problem_id).value
self.assertTrue('1' not in json.loads(problem_hints)['1.0'])
def test_changevotes(self):
"""
Checks that vote changing works.
"""
request = RequestFactory()
post = request.post(self.url, {'field': 'hints',
'op': 'change votes',
1: [self.problem_id.to_deprecated_string(), '1.0', '1', 5]})
view.change_votes(post, self.course_id, 'hints')
problem_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_id=self.problem_id).value
# hints[answer][hint_pk (string)] = [hint text, vote count]
print json.loads(problem_hints)['1.0']['1']
self.assertTrue(json.loads(problem_hints)['1.0']['1'][1] == 5)
def test_addhint(self):
"""
Check that instructors can add new hints.
"""
# Because add_hint accesses the xmodule, this test requires a bunch
# of monkey patching.
hinter = MagicMock()
hinter.validate_answer = lambda string: True
request = RequestFactory()
post = request.post(self.url, {'field': 'mod_queue',
'op': 'add hint',
'problem': self.problem_id.to_deprecated_string(),
'answer': '3.14',
'hint': 'This is a new hint.'})
post.user = 'fake user'
with patch('courseware.module_render.get_module', MagicMock(return_value=hinter)):
with patch('courseware.model_data.FieldDataCache', MagicMock(return_value=None)):
view.add_hint(post, self.course_id, 'mod_queue')
problem_hints = XModuleUserStateSummaryField.objects.get(field_name='mod_queue', usage_id=self.problem_id).value
self.assertTrue('3.14' in json.loads(problem_hints))
def test_addbadhint(self):
"""
Check that instructors cannot add hints with unparsable answers.
"""
# Patching.
hinter = MagicMock()
hinter.validate_answer = lambda string: False
request = RequestFactory()
post = request.post(self.url, {'field': 'mod_queue',
'op': 'add hint',
'problem': self.problem_id.to_deprecated_string(),
'answer': 'fish',
'hint': 'This is a new hint.'})
post.user = 'fake user'
with patch('courseware.module_render.get_module', MagicMock(return_value=hinter)):
with patch('courseware.model_data.FieldDataCache', MagicMock(return_value=None)):
view.add_hint(post, self.course_id, 'mod_queue')
problem_hints = XModuleUserStateSummaryField.objects.get(field_name='mod_queue', usage_id=self.problem_id).value
self.assertTrue('fish' not in json.loads(problem_hints))
def test_approve(self):
"""
Check that instructors can approve hints. (Move them
from the mod_queue to the hints.)
"""
request = RequestFactory()
post = request.post(self.url, {'field': 'mod_queue',
'op': 'approve',
1: [self.problem_id.to_deprecated_string(), '2.0', '2']})
view.approve(post, self.course_id, 'mod_queue')
problem_hints = XModuleUserStateSummaryField.objects.get(field_name='mod_queue', usage_id=self.problem_id).value
self.assertTrue('2.0' not in json.loads(problem_hints) or len(json.loads(problem_hints)['2.0']) == 0)
problem_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_id=self.problem_id).value
self.assertTrue(json.loads(problem_hints)['2.0']['2'] == ['Hint 2', 1])
self.assertTrue(len(json.loads(problem_hints)['2.0']) == 2)
......@@ -184,9 +184,6 @@ FEATURES = {
# Toggle to enable certificates of courses on dashboard
'ENABLE_VERIFIED_CERTIFICATES': False,
# Allow use of the hint managment instructor view.
'ENABLE_HINTER_INSTRUCTOR_VIEW': False,
# for load testing
'AUTOMATIC_AUTH_FOR_TESTING': False,
......
......@@ -24,7 +24,6 @@ FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True
FEATURES['ENABLE_SERVICE_STATUS'] = True
FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = True # Enable email for all Studio courses
FEATURES['REQUIRE_COURSE_EMAIL_AUTH'] = False # Give all courses email (don't require django-admin perms)
FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True
FEATURES['ENABLE_SHOPPING_CART'] = True
FEATURES['AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'] = True
FEATURES['ENABLE_S3_GRADE_DOWNLOADS'] = True
......
......@@ -62,8 +62,6 @@ FEATURES['ENABLE_DISCUSSION_SERVICE'] = False
FEATURES['ENABLE_SERVICE_STATUS'] = True
FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True
FEATURES['ENABLE_SHOPPING_CART'] = True
FEATURES['ENABLE_VERIFIED_CERTIFICATES'] = True
......
<%inherit file="/main.html" />
<%namespace name='static' file='/static_content.html'/>
<%namespace name="content" file="/instructor/hint_manager_inner.html"/>
<%block name="headextra">
<%static:css group='style-course-vendor'/>
<%static:css group='style-course'/>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.axislabels.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-world-mill-en.js')}"></script>
<script type="text/javascript" src="${static.url('js/course_groups/cohorts.js')}"></script>
<script>
function setup() {
field = $("#field-label").html()
changed_votes = []
$(".votes").on('input', function() {
changed_votes.push($(this))
});
$("#hint-delete").click(function(){
var data_dict = {'op': 'delete hints',
'field': field}
var i = 1
$(".hint-select").each(function(){
if ($(this).is(":checked")) {
data_dict[i] = [$(this).parent().attr("data-problem"),
$(this).parent().attr("data-answer"),
$(this).parent().attr("data-pk")];
i += 1
}
});
$.ajax(window.location.pathname, {
type: "POST",
data: data_dict,
success: update_contents
});
});
$("#update-votes").click(function(){
var data_dict = {'op': 'change votes',
'field': field}
for (var i=0; i<changed_votes.length; i++) {
data_dict[i] = [$(changed_votes[i]).parent().attr("data-problem"),
$(changed_votes[i]).parent().attr("data-answer"),
$(changed_votes[i]).parent().attr("data-pk"),
$(changed_votes[i]).val()];
}
$.ajax(window.location.pathname, {
type: "POST",
data: data_dict,
success: update_contents
});
});
$("#switch-fields").click(function(){
out_dict = {'op': 'switch fields',
'field': $(this).attr("other-field")};
$.ajax(window.location.pathname, {
type: "POST",
data: out_dict,
success: update_contents
});
});
$(".submit-new-hint").click(function(){
problem_name = $(this).data("problem");
hint_text = $(".submit-hint-text").filter('*[data-problem="'+problem_name+'"]').val();
hint_answer = $(".submit-hint-answer").filter('*[data-problem="'+problem_name+'"]').val();
data_dict = {'op': 'add hint',
'field': field,
'problem': problem_name,
'answer': hint_answer,
'hint': hint_text};
$.ajax(window.location.pathname, {
type: "POST",
data: data_dict,
success: update_contents
});
});
$("#approve").click(function(){
var data_dict = {'op': 'approve',
'field': field}
var i = 1
$(".hint-select").each(function(){
if ($(this).is(":checked")) {
data_dict[i] = [$(this).parent().attr("data-problem"),
$(this).parent().attr("data-answer"),
$(this).parent().attr("data-pk")];
i += 1
}
});
$.ajax(window.location.pathname, {
type: "POST",
data: data_dict,
success: update_contents
});
});
}
$(document).ready(setup);
function update_contents(data, status, jqXHR) {
$('.instructor-dashboard-content').html(data.contents);
setup();
}
</script>
</%block>
<section class="container">
<div class="instructor-dashboard-wrapper">
<section class="instructor-dashboard-content">
${content.main()}
</section>
</div>
</section>
<%block name="main">
<div id="field-label" style="display:none">${field}</div>
<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]:
% 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}">
<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>
<br /><br />
</p>
% endfor
% if len(hint_dict) > 0:
</div><br />
% endif
% endfor
<h4> Add a hint to this problem </h4>
<h4> Answer: </h4>
<input type="text" class="submit-hint-answer" data-problem="${definition_id}"/>
(Be sure to format your answer in the same way as the other answers you see here.)
<br />
Hint: <br />
<textarea cols="50" style="height:200px" class="submit-hint-text" data-problem="${definition_id}"></textarea>
<br />
<button class="submit-new-hint" data-problem="${definition_id}"> Submit </button>
<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':
<button id="approve"> Approve selected </button>
% endif
</%block>
\ No newline at end of file
......@@ -870,13 +870,6 @@ urlpatterns += (
url(r'^debug/show_parameters$', 'debug.views.show_parameters'),
)
# Crowdsourced hinting instructor manager.
if settings.FEATURES.get('ENABLE_HINTER_INSTRUCTOR_VIEW'):
urlpatterns += (
url(r'^courses/{}/hint_manager$'.format(settings.COURSE_ID_PATTERN),
'instructor.hint_manager.hint_manager', name="hint_manager"),
)
# enable automatic login
if settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING'):
urlpatterns += (
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment