Finished prototype of hint moderation view. Began re-writing tests of the crowdsource hinter module. (Old tests no longer cover all the code, now that moderation has been added.)
This commit is contained in:
committed by
Carlos Andrés Rocha
parent
0c0de20a2f
commit
aba99084f2
296
common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py
Normal file
296
common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py
Normal file
@@ -0,0 +1,296 @@
|
||||
from mock import Mock, patch
|
||||
import unittest
|
||||
|
||||
import xmodule
|
||||
from xmodule.crowdsource_hinter import CrowdsourceHinterModule
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from django.http import QueryDict
|
||||
|
||||
from . import test_system
|
||||
|
||||
import json
|
||||
|
||||
class CHModuleFactory(object):
|
||||
'''
|
||||
Helps us make a CrowdsourceHinterModule with the specified internal
|
||||
state.
|
||||
'''
|
||||
|
||||
sample_problem_xml = '''
|
||||
<?xml version="1.0"?>
|
||||
<crowdsource_hinter>
|
||||
<problem display_name="Numerical Input" markdown="A numerical input problem accepts a line of text input from the student, and evaluates the input for correctness based on its numerical value. The answer is correct if it is within a specified numerical tolerance of the expected answer. Enter the number of fingers on a human hand: = 5 [explanation] If you look at your hand, you can count that you have five fingers. [explanation] " rerandomize="never" showanswer="finished">
|
||||
<p>A numerical input problem accepts a line of text input from the student, and evaluates the input for correctness based on its numerical value.</p>
|
||||
<p>The answer is correct if it is within a specified numerical tolerance of the expected answer.</p>
|
||||
<p>Enter the number of fingers on a human hand:</p>
|
||||
<numericalresponse answer="5">
|
||||
<textline/>
|
||||
</numericalresponse>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>If you look at your hand, you can count that you have five fingers. </p>
|
||||
</div>
|
||||
</solution>
|
||||
</problem>
|
||||
</crowdsource_hinter>
|
||||
'''
|
||||
|
||||
num = 0
|
||||
|
||||
@staticmethod
|
||||
def next_num():
|
||||
CHModuleFactory.num += 1
|
||||
return CHModuleFactory.num
|
||||
|
||||
@staticmethod
|
||||
def create(hints=None,
|
||||
previous_answers=None,
|
||||
user_voted=None,
|
||||
moderate=None,
|
||||
mod_queue=None):
|
||||
|
||||
location = Location(["i4x", "edX", "capa_test", "problem",
|
||||
"SampleProblem{0}".format(CHModuleFactory.next_num())])
|
||||
model_data = {'data': CHModuleFactory.sample_problem_xml}
|
||||
|
||||
if hints != None:
|
||||
model_data['hints'] = hints
|
||||
else:
|
||||
model_data['hints'] = {
|
||||
'24.0': {'0': ['Best hint', 40],
|
||||
'3': ['Another hint', 30],
|
||||
'4': ['A third hint', 20],
|
||||
'6': ['A less popular hint', 3]},
|
||||
'25.0': {'1': ['Really popular hint', 100]}
|
||||
}
|
||||
|
||||
if mod_queue != None:
|
||||
model_data['mod_queue'] = mod_queue
|
||||
else:
|
||||
model_data['mod_queue'] = {
|
||||
'24.0': {'2': ['A non-approved hint']},
|
||||
'26.0': {'5': ['Another non-approved hint']}
|
||||
}
|
||||
|
||||
if previous_answers != None:
|
||||
model_data['previous_answers'] = previous_answers
|
||||
else:
|
||||
model_data['previous_answers'] = [
|
||||
['24.0', [0, 3, 4]],
|
||||
['29.0', [None, None, None]]
|
||||
]
|
||||
|
||||
if user_voted != None:
|
||||
model_data['user_voted'] = user_voted
|
||||
|
||||
if moderate != None:
|
||||
model_data['moderate'] = moderate
|
||||
|
||||
descriptor = Mock(weight="1")
|
||||
system = test_system()
|
||||
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
||||
module = CrowdsourceHinterModule(system, descriptor, model_data)
|
||||
|
||||
return module
|
||||
|
||||
class CrowdsourceHinterTest(unittest.TestCase):
|
||||
'''
|
||||
In the below tests, '24.0' represents a wrong answer, and '42.5' represents
|
||||
a correct answer.
|
||||
'''
|
||||
|
||||
def test_gethint_0hint(self):
|
||||
'''
|
||||
Someone asks for a hint, when there's no hint to give.
|
||||
- Output should be blank.
|
||||
- New entry should be added to previous_answers
|
||||
'''
|
||||
m = CHModuleFactory.create()
|
||||
json_in = {'problem_name': '26.0'}
|
||||
json_out = json.loads(m.get_hint(json_in))['contents']
|
||||
self.assertTrue(json_out == ' ')
|
||||
self.assertTrue(['26.0', [None, None, None]] in m.previous_answers)
|
||||
|
||||
def test_gethint_1hint(self):
|
||||
'''
|
||||
Someone asks for a hint, with exactly one hint in the database.
|
||||
Output should contain that hint.
|
||||
'''
|
||||
m = CHModuleFactory.create()
|
||||
json_in = {'problem_name': '25.0'}
|
||||
json_out = json.loads(m.get_hint(json_in))['contents']
|
||||
self.assertTrue('Really popular hint' in json_out)
|
||||
|
||||
|
||||
def test_gethint_manyhints(self):
|
||||
'''
|
||||
Someone asks for a hint, with many matching hints in the database.
|
||||
- The top-rated hint should be returned.
|
||||
- Two other random hints should be returned.
|
||||
Currently, the best hint could be returned twice - need to fix this
|
||||
in implementation.
|
||||
'''
|
||||
m = CHModuleFactory.create()
|
||||
json_in = {'problem_name': '24.0'}
|
||||
json_out = json.loads(m.get_hint(json_in))['contents']
|
||||
self.assertTrue('Best hint' in json_out)
|
||||
self.assertTrue(json_out.count('hint') == 3)
|
||||
|
||||
|
||||
def test_getfeedback_0wronganswers(self):
|
||||
'''
|
||||
Someone has gotten the problem correct on the first try.
|
||||
Output should be empty.
|
||||
'''
|
||||
m = CHModuleFactory.create(previous_answers=[])
|
||||
json_in = {'problem_name': '42.5'}
|
||||
json_out = json.loads(m.get_feedback(json_in))['contents']
|
||||
self.assertTrue(json_out == ' ')
|
||||
|
||||
def test_getfeedback_1wronganswer_nohints(self):
|
||||
'''
|
||||
Someone has gotten the problem correct, with one previous wrong
|
||||
answer. However, we don't actually have hints for this problem.
|
||||
There should be a dialog to submit a new hint.
|
||||
'''
|
||||
m = CHModuleFactory.create(previous_answers=[['26.0',[None, None, None]]])
|
||||
json_in = {'problem_name': '42.5'}
|
||||
json_out = json.loads(m.get_feedback(json_in))['contents']
|
||||
self.assertTrue('textarea' in json_out)
|
||||
self.assertTrue('Vote' not in json_out)
|
||||
|
||||
|
||||
def test_getfeedback_1wronganswer_withhints(self):
|
||||
'''
|
||||
Same as above, except the user did see hints. There should be
|
||||
a voting dialog, with the correct choices, plus a hint submission
|
||||
dialog.
|
||||
'''
|
||||
m = CHModuleFactory.create(hints={
|
||||
'24.0': {'0': ['a hint', 42],
|
||||
'1': ['another hint', 35],
|
||||
'2': ['irrelevent hint', 25.0]}
|
||||
},
|
||||
previous_answers=[
|
||||
['24.0', [0, 1, None]]],
|
||||
)
|
||||
json_in = {'problem_name': '42.5'}
|
||||
json_out = json.loads(m.get_feedback(json_in))['contents']
|
||||
self.assertTrue('a hint' in json_out)
|
||||
self.assertTrue('another hint' in json_out)
|
||||
self.assertTrue('irrelevent hint' not in json_out)
|
||||
self.assertTrue('textarea' in json_out)
|
||||
|
||||
|
||||
def test_vote_nopermission(self):
|
||||
'''
|
||||
A user tries to vote for a hint, but he has already voted!
|
||||
Should not change any vote tallies.
|
||||
'''
|
||||
m = CHModuleFactory.create(hints={
|
||||
'24.0': {'0': ['a hint', 42],
|
||||
'1': ['another hint', 35],
|
||||
'2': ['irrelevent hint', 25.0]}
|
||||
},
|
||||
previous_answers=[
|
||||
['24.0', [0, 1, None]]],
|
||||
user_voted=True
|
||||
)
|
||||
json_in = {'answer': 0, 'hint': 1}
|
||||
json_out = json.loads(m.tally_vote(json_in))['contents']
|
||||
self.assertTrue(m.hints['24.0']['0'][1] == 42)
|
||||
self.assertTrue(m.hints['24.0']['1'][1] == 35)
|
||||
self.assertTrue(m.hints['24.0']['2'][1] == 25.0)
|
||||
|
||||
|
||||
def test_vote_withpermission(self):
|
||||
'''
|
||||
A user votes for a hint.
|
||||
'''
|
||||
m = CHModuleFactory.create(hints={
|
||||
'24.0': {'0': ['a hint', 42],
|
||||
'1': ['another hint', 35],
|
||||
'2': ['irrelevent hint', 25.0]}
|
||||
},
|
||||
previous_answers=[
|
||||
['24.0', [0, 1, None]]],
|
||||
)
|
||||
json_in = {'answer': 0, 'hint': 1}
|
||||
json_out = json.loads(m.tally_vote(json_in))['contents']
|
||||
self.assertTrue(m.hints['24.0']['0'][1] == 42)
|
||||
self.assertTrue(m.hints['24.0']['1'][1] == 36)
|
||||
self.assertTrue(m.hints['24.0']['2'][1] == 25.0)
|
||||
|
||||
|
||||
def test_submithint_nopermission(self):
|
||||
'''
|
||||
A user tries to submit a hint, but he has already voted.
|
||||
'''
|
||||
m = CHModuleFactory.create(previous_answers=[
|
||||
['24.0', [None, None, None]]],
|
||||
user_voted=True)
|
||||
json_in = {'answer': 0, 'hint': 'This is a new hint.'}
|
||||
m.submit_hint(json_in)
|
||||
self.assertTrue('24.0' not in m.hints)
|
||||
|
||||
|
||||
def test_submithint_withpermission_new(self):
|
||||
'''
|
||||
A user submits a hint to an answer for which no hints
|
||||
exist yet.
|
||||
'''
|
||||
m = CHModuleFactory.create(previous_answers=[
|
||||
['24.0', [None, None, None]]],
|
||||
)
|
||||
json_in = {'answer': 0, 'hint': 'This is a new hint.'}
|
||||
m.submit_hint(json_in)
|
||||
# Make a hint request.
|
||||
json_in = {'problem name': '24.0'}
|
||||
json_out = json.loads(m.get_hint(json_in))['contents']
|
||||
self.assertTrue('This is a new hint.' in json_out)
|
||||
|
||||
|
||||
def test_submithint_withpermission_existing(self):
|
||||
'''
|
||||
A user submits a hint to an answer that has other hints
|
||||
already.
|
||||
'''
|
||||
m = CHModuleFactory.create(previous_answers=[
|
||||
['24.0', [0, None, None]]],
|
||||
hints={'24.0': {'0': ['Existing hint.', 1]}}
|
||||
)
|
||||
json_in = {'answer': 0, 'hint': 'This is a new hint.'}
|
||||
m.submit_hint(json_in)
|
||||
# Make a hint request.
|
||||
json_in = {'problem name': '24.0'}
|
||||
json_out = json.loads(m.get_hint(json_in))['contents']
|
||||
self.assertTrue('This is a new hint.' in json_out)
|
||||
|
||||
def test_deletehint(self):
|
||||
'''
|
||||
An admin / instructor deletes a hint.
|
||||
'''
|
||||
m = CHModuleFactory.create(hints={
|
||||
'24.0': {'0': ['Deleted hint', 5],
|
||||
'1': ['Safe hint', 4]}
|
||||
})
|
||||
m.delete_hint('24.0', '0')
|
||||
json_in = {'problem name': '24.0'}
|
||||
json_out = json.loads(m.get_hint(json_in))['contents']
|
||||
self.assertTrue('Deleted hint' not in json_out)
|
||||
self.assertTrue('Safe hint' in json_out)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ from django.core.urlresolvers import reverse
|
||||
|
||||
from courseware.courses import get_course_with_access
|
||||
from courseware.models import XModuleContentField
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -48,6 +50,10 @@ def hint_manager(request, course_id):
|
||||
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))
|
||||
return HttpResponse(json.dumps({'success': True, 'contents': rendered_html}))
|
||||
|
||||
@@ -59,7 +65,6 @@ def get_hints(request, course_id, field):
|
||||
# DON'T TRUST field attributes that come from ajax. Use an if statement
|
||||
# to make sure the field is valid before plugging into functions.
|
||||
|
||||
out = ''
|
||||
if field == 'mod_queue':
|
||||
other_field = 'hints'
|
||||
field_label = 'Hints Awaiting Moderation'
|
||||
@@ -71,32 +76,40 @@ def get_hints(request, course_id, field):
|
||||
chopped_id = '/'.join(course_id.split('/')[:-1])
|
||||
chopped_id = re.escape(chopped_id)
|
||||
all_hints = XModuleContentField.objects.filter(field_name=field, definition_id__regex=chopped_id)
|
||||
big_out_dict = {}
|
||||
name_dict = {}
|
||||
for problem in all_hints:
|
||||
out += '<h2> Problem: ' + problem.definition_id + '</h2>'
|
||||
for answer, hint_dict in json.loads(problem.value).items():
|
||||
out += '<h4> Answer: ' + answer + '</h4>'
|
||||
for pk, hint in hint_dict.items():
|
||||
out += '<p data-problem="'\
|
||||
+ problem.definition_id + '" data-pk="' + str(pk) + '" data-answer="'\
|
||||
+ answer + '">'
|
||||
out += '<input class="hint-select" type="checkbox"/>' + hint[0] + \
|
||||
'<br /> Votes: <input type="text" class="votes" value="' + str(hint[1]) + '"></input>'
|
||||
out += '</p>'
|
||||
out += '''<h4> Add a hint to this problem </h4>
|
||||
Answer (exact formatting):
|
||||
<input type="text" id="new-hint-answer-''' + problem.definition_id \
|
||||
+ '"/> <br /> Hint: <br /><textarea cols="50" style="height:200px" id="new-hint-' + problem.definition_id \
|
||||
+ '"></textarea> <br /> <button class="submit-new-hint" data-problem="' + problem.definition_id \
|
||||
+ '"> Submit </button><br />'
|
||||
loc = Location(problem.definition_id)
|
||||
try:
|
||||
descriptor = modulestore().get_items(loc)[0]
|
||||
except IndexError:
|
||||
# Sometimes, the problem is no longer in the course. Just
|
||||
# don't include said problem.
|
||||
continue
|
||||
name_dict[problem.definition_id] = descriptor.get_children()[0].display_name
|
||||
# Answer list contains (answer, dict_of_hints) tuples.
|
||||
|
||||
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')
|
||||
|
||||
out += '<button id="hint-delete"> Delete selected </button> <button id="update-votes"> Update votes </button>'
|
||||
render_dict = {'out': out,
|
||||
'field': field,
|
||||
answer_list = sorted(json.loads(problem.value).items(), key=answer_sorter)
|
||||
big_out_dict[problem.definition_id] = answer_list
|
||||
|
||||
render_dict = {'field': field,
|
||||
'other_field': other_field,
|
||||
'field_label': field_label,
|
||||
'other_field_label': other_field_label,
|
||||
'all_hints': all_hints}
|
||||
'all_hints': big_out_dict,
|
||||
'id_to_name': name_dict}
|
||||
return render_dict
|
||||
|
||||
def delete_hints(request, course_id, field):
|
||||
@@ -131,6 +144,80 @@ def change_votes(request, course_id, field):
|
||||
problem_dict[answer][pk][1] = new_votes
|
||||
this_problem.value = json.dumps(problem_dict)
|
||||
this_problem.save()
|
||||
|
||||
def add_hint(request, course_id, field):
|
||||
'''
|
||||
Add a new hint. 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']
|
||||
answer = request.POST['answer']
|
||||
hint_text = request.POST['hint']
|
||||
this_problem = XModuleContentField.objects.get(field_name=field, definition_id=problem_id)
|
||||
|
||||
hint_pk_entry = XModuleContentField.objects.get(field_name='hint_pk', definition_id=problem_id)
|
||||
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):
|
||||
'''
|
||||
Approve a list of hints, moving them from the mod_queue to the real
|
||||
hint list. POST:
|
||||
op, field
|
||||
(some number) -> [problem, answer, pk]
|
||||
'''
|
||||
for key in request.POST:
|
||||
if key == 'op' or key == 'field':
|
||||
continue
|
||||
problem_id, answer, 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]
|
||||
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
|
||||
problem_in_hints.value = json.dumps(problem_dict)
|
||||
problem_in_hints.save()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
data_dict[i] = [$(this).parent().attr("data-problem"),
|
||||
$(this).parent().attr("data-answer"),
|
||||
$(this).parent().attr("data-pk")];
|
||||
i += 1
|
||||
}
|
||||
});
|
||||
$.ajax(window.location.pathname, {
|
||||
@@ -64,6 +65,40 @@
|
||||
});
|
||||
});
|
||||
|
||||
$(".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);
|
||||
|
||||
@@ -1,39 +1,45 @@
|
||||
<%block name="main">
|
||||
<div id="field-label" style="display:none"> ${field} </div>
|
||||
<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>
|
||||
Switch to <a id="switch-fields" other-field="${other_field}">${other_field_label}</a>
|
||||
|
||||
|
||||
% for problem in all_hints:
|
||||
<h2> Problem: ${problem.definition_id} </h2>
|
||||
<%
|
||||
import json
|
||||
loaded_json = json.loads(problem.value).items()
|
||||
%>
|
||||
% for answer, hint_dict in loaded_json:
|
||||
<h4> Answer: ${answer} </h4>
|
||||
% 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="${problem.definition_id}" data-pk="${pk}" data-answer="${answer}">
|
||||
<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])}"></input>
|
||||
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>
|
||||
Answer (exact formatting):
|
||||
<input type="text" id="new-hint-answer-${problem.definition_id}"/>
|
||||
<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" id="new-hint-${problem.definition_id}"></textarea>
|
||||
<textarea cols="50" style="height:200px" class="submit-hint-text" data-problem="${definition_id}"></textarea>
|
||||
<br />
|
||||
<button class="submit-new-hint" data-problem="${problem.definition_id}"> Submit </button>
|
||||
<button class="submit-new-hint" data-problem="${definition_id}"> Submit </button>
|
||||
<br />
|
||||
% endfor
|
||||
|
||||
<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>
|
||||
Reference in New Issue
Block a user