Commit b015a226 by Felix Sun

Merge pull request #303 from edx/felix/hinter2

Felix/hinter2
parents eedeaf28 0c4d6ba6
...@@ -55,6 +55,7 @@ setup( ...@@ -55,6 +55,7 @@ setup(
"word_cloud = xmodule.word_cloud_module:WordCloudDescriptor", "word_cloud = xmodule.word_cloud_module:WordCloudDescriptor",
"hidden = xmodule.hidden_module:HiddenDescriptor", "hidden = xmodule.hidden_module:HiddenDescriptor",
"raw = xmodule.raw_module:RawDescriptor", "raw = xmodule.raw_module:RawDescriptor",
"crowdsource_hinter = xmodule.crowdsource_hinter:CrowdsourceHinterDescriptor",
], ],
'console_scripts': [ 'console_scripts': [
'xmodule_assets = xmodule.static_content:main', 'xmodule_assets = xmodule.static_content:main',
......
.crowdsource-wrapper {
@include box-shadow(inset 0 1px 2px 1px rgba(0,0,0,0.1));
@include border-radius(2px);
display: none;
margin-top: 20px;
padding: (15px);
background: rgb(253, 248, 235);
}
#answer-tabs {
background: #FFFFFF;
border: none;
margin-bottom: 20px;
padding-bottom: 20px;
}
#answer-tabs .ui-widget-header {
border-bottom: 1px solid #DCDCDC;
background: #F3F3F3;
}
#answer-tabs .ui-tabs-nav .ui-state-default {
border: 1px solid #DCDCDC;
background: #F8F8F8;
margin-bottom: 0px;
}
#answer-tabs .ui-tabs-nav .ui-state-default:hover {
background: #FFFFFF;
}
#answer-tabs .ui-tabs-nav .ui-state-active:hover {
background: #FFFFFF;
}
#answer-tabs .ui-tabs-nav .ui-state-active {
border: 1px solid #DCDCDC;
background: #FFFFFF;
}
#answer-tabs .ui-tabs-nav .ui-state-active a {
color: #222222;
background: #FFFFFF;
}
#answer-tabs .ui-tabs-nav .ui-state-default a:hover {
color: #222222;
background: #FFFFFF;
}
#answer-tabs .custom-hint {
height: 100px;
width: 100%;
}
.hint-inner-container {
padding-left: 15px;
padding-right: 15px;
font-size: 16px;
}
.vote {
padding-top: 0px !important;
padding-bottom: 0px !important;
}
...@@ -223,6 +223,7 @@ class @Problem ...@@ -223,6 +223,7 @@ class @Problem
@el.removeClass 'showed' @el.removeClass 'showed'
else else
@gentle_alert response.success @gentle_alert response.success
Logger.log 'problem_graded', [@answers, response.contents], @url
reset: => reset: =>
Logger.log 'problem_reset', @answers Logger.log 'problem_reset', @answers
......
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-url'), @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: =>
window.update_schematics()
@$('input.vote').click @vote
@$('input.submit-hint').click @submit_hint
@$('.custom-hint').click @clear_default_text
@$('#answer-tabs').tabs({active: 0})
@$('.expand-goodhint').click @expand_goodhint
expand_goodhint: =>
if @$('.goodhint').css('display') == 'none'
@$('.goodhint').css('display', 'block')
else
@$('.goodhint').css('display', 'none')
vote: (eventObj) =>
target = @$(eventObj.currentTarget)
post_json = {'answer': target.data('answer'), 'hint': target.data('hintno')}
$.postWithPrefix "#{@url}/vote", post_json, (response) =>
@render(response.contents)
submit_hint: (eventObj) =>
target = @$(eventObj.currentTarget)
textarea_id = '#custom-hint-' + target.data('answer')
post_json = {'answer': target.data('answer'), 'hint': @$(textarea_id).val()}
$.postWithPrefix "#{@url}/submit_hint",post_json, (response) =>
@render(response.contents)
clear_default_text: (eventObj) =>
target = @$(eventObj.currentTarget)
if target.data('cleared') == undefined
target.val('')
target.data('cleared', true)
render: (content) ->
if content
# Trim leading and trailing whitespace
content = content.replace /^\s+|\s+$/g, ""
if content
@el.html(content)
@el.show()
JavascriptLoader.executeModuleScripts @el, () =>
@bind()
@$('#previous-answer-0').css('display', 'inline')
class @Logger class @Logger
# events we want sent to Segment.io for tracking # events we want sent to Segment.io for tracking
SEGMENT_IO_WHITELIST = ["seq_goto", "seq_next", "seq_prev", "problem_check", "problem_reset", "problem_show", "problem_save"] SEGMENT_IO_WHITELIST = ["seq_goto", "seq_next", "seq_prev", "problem_check", "problem_reset", "problem_show", "problem_save"]
@log: (event_type, data) -> # listeners[event_type][element] -> list of callbacks
listeners = {}
@log: (event_type, data, element = null) ->
# Segment.io event tracking # Segment.io event tracking
if event_type in SEGMENT_IO_WHITELIST if event_type in SEGMENT_IO_WHITELIST
# to avoid changing the format of data sent to our servers, we only massage it here # to avoid changing the format of data sent to our servers, we only massage it here
...@@ -11,11 +14,36 @@ class @Logger ...@@ -11,11 +14,36 @@ class @Logger
else else
analytics.track event_type, data analytics.track event_type, data
# Check to see if we're listening for the event type.
if event_type of listeners
# Cool. Do the elements also match?
# null element in the listener dictionary means any element will do.
# null element in the @log call means we don't know the element name.
if null of listeners[event_type]
# Make the callbacks.
for callback in listeners[event_type][null]
callback(event_type, data, element)
else if element of listeners[event_type]
for callback in listeners[event_type][element]
callback(event_type, data, element)
# Regardless of whether any callbacks were made, log this event.
$.getWithPrefix '/event', $.getWithPrefix '/event',
event_type: event_type event_type: event_type
event: JSON.stringify(data) event: JSON.stringify(data)
page: window.location.href page: window.location.href
@listen: (event_type, element, callback) ->
# Add a listener. If you want any element to trigger this listener,
# do element = null
if event_type not of listeners
listeners[event_type] = {}
if element not of listeners[event_type]
listeners[event_type][element] = [callback]
else
listeners[event_type][element].push callback
@bind: -> @bind: ->
window.onunload = -> window.onunload = ->
$.ajaxWithPrefix $.ajaxWithPrefix
......
## The hinter module passes in a field called ${op}, which determines which
## sub-function to render.
<%def name="get_hint()">
% if best_hint != '':
<h4> Hints from students who made similar mistakes: </h4>
<ul>
<li> ${best_hint} </li>
% endif
% if rand_hint_1 != '':
<li> ${rand_hint_1} </li>
% endif
% if rand_hint_2 != '':
<li> ${rand_hint_2} </li>
% endif
</ul>
</%def>
<%def name="get_feedback()">
<p><em> Participation in the hinting system is strictly optional, and will not influence your grade. </em></p>
<p>
Help your classmates by writing hints for this problem. Start by picking one of your previous incorrect answers from below:
</p>
<div id="answer-tabs">
<ul>
% for index, answer in index_to_answer.items():
<li><a href="#previous-answer-${index}"> ${answer} </a></li>
% endfor
</ul>
% for index, answer in index_to_answer.items():
<div class = "previous-answer" id="previous-answer-${index}">
<div class = "hint-inner-container">
% if index in index_to_hints and len(index_to_hints[index]) > 0:
<p>
Which hint would be most effective to show a student who also got ${answer}?
</p>
% for hint_text, hint_pk in index_to_hints[index]:
<p>
<input class="vote" data-answer="${index}" data-hintno="${hint_pk}" type="button" value="Vote"/>
${hint_text}
</p>
% endfor
<p>
Don't like any of the hints above? You can also submit your own.
</p>
% endif
<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-${index}">
What would you say to help someone who got this wrong answer?
(Don't give away the answer, please.)
</textarea>
<br/><br/>
<input class="submit-hint" data-answer="${index}" type="button" value="submit">
</div></div>
% endfor
</div>
<p>Read about <a class="expand-goodhint" href="javascript:void(0);">what makes a good hint</a>.</p>
<div class="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>
</%def>
<%def name="show_votes()">
Thank you for voting!
<br />
% for hint, votes in hint_and_votes:
<span style="color:green"> ${votes} votes. </span>
${hint}
<br />
% endfor
</%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 == "vote":
${show_votes()}
% endif
"""
Views for hint management.
Along with the crowdsource_hinter xmodule, this code is still
experimental, and should not be used in new courses, yet.
"""
import json
import re
from django.http import HttpResponse, Http404
from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response, render_to_string
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
def hint_manager(request, course_id):
try:
get_course_with_access(request.user, course_id, 'staff', 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_id, 'mod_queue')
return render_to_response('courseware/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))
return HttpResponse(json.dumps({'success': True, 'contents': rendered_html}))
def get_hints(request, course_id, field):
"""
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'
# The course_id is of the form school/number/classname.
# We want to use the course_id to find all matching definition_id's.
# To do this, just take the school/number part - leave off the classname.
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[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:
loc = Location(hints_by_problem.definition_id)
name = location_to_problem_name(loc)
if name is None:
continue
id_to_name[hints_by_problem.definition_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.definition_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(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_items(loc)[0]
return descriptor.get_children()[0].display_name
except IndexError:
# Sometimes, the problem is no longer in the course. Just
# don't include said problem.
return None
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.
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)
# 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]
this_problem.value = json.dumps(problem_dict)
this_problem.save()
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.
- 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)
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)
this_problem.value = json.dumps(problem_dict)
this_problem.save()
def add_hint(request, course_id, field):
"""
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']
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()
import json
from django.test.client import Client, RequestFactory
from django.test.utils import override_settings
from courseware.models import XModuleContentField
from courseware.tests.factories import ContentFactory
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
import instructor.hint_manager as view
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class HintManagerTest(ModuleStoreTestCase):
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.
"""
self.course = CourseFactory.create(org='Me', number='19.002', display_name='test_course')
self.url = '/courses/Me/19.002/test_course/hint_manager'
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')
self.problem_id = 'i4x://Me/19.002/crowdsource_hinter/crowdsource_hinter_001'
self.course_id = 'Me/19.002/test_course'
ContentFactory.create(field_name='hints',
definition_id=self.problem_id,
value=json.dumps({'1.0': {'1': ['Hint 1', 2],
'3': ['Hint 3', 12]},
'2.0': {'4': ['Hint 4', 3]}
}))
ContentFactory.create(field_name='mod_queue',
definition_id=self.problem_id,
value=json.dumps({'2.0': {'2': ['Hint 2', 1]}}))
ContentFactory.create(field_name='hint_pk',
definition_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 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('/courses/Me/19.002/test_course/hint_manager')
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, '1.0', '1']})
view.delete_hints(post, self.course_id, 'hints')
problem_hints = XModuleContentField.objects.get(field_name='hints', definition_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, '1.0', '1', 5]})
view.change_votes(post, self.course_id, 'hints')
problem_hints = XModuleContentField.objects.get(field_name='hints', definition_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.
"""
request = RequestFactory()
post = request.post(self.url, {'field': 'mod_queue',
'op': 'add hint',
'problem': self.problem_id,
'answer': '3.14',
'hint': 'This is a new hint.'})
view.add_hint(post, self.course_id, 'mod_queue')
problem_hints = XModuleContentField.objects.get(field_name='mod_queue', definition_id=self.problem_id).value
self.assertTrue('3.14' 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, '2.0', '2']})
view.approve(post, self.course_id, 'mod_queue')
problem_hints = XModuleContentField.objects.get(field_name='mod_queue', definition_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 = XModuleContentField.objects.get(field_name='hints', definition_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)
...@@ -141,6 +141,9 @@ MITX_FEATURES = { ...@@ -141,6 +141,9 @@ MITX_FEATURES = {
# Enable instructor dash to submit background tasks # Enable instructor dash to submit background tasks
'ENABLE_INSTRUCTOR_BACKGROUND_TASKS': True, 'ENABLE_INSTRUCTOR_BACKGROUND_TASKS': True,
# Allow use of the hint managment instructor view.
'ENABLE_HINTER_INSTRUCTOR_VIEW': False,
} }
# Used for A/B testing # Used for A/B testing
......
...@@ -28,6 +28,7 @@ MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True ...@@ -28,6 +28,7 @@ MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True
MITX_FEATURES['ENABLE_PSYCHOMETRICS'] = False # real-time psychometrics (eg item response theory analysis in instructor dashboard) MITX_FEATURES['ENABLE_PSYCHOMETRICS'] = False # real-time psychometrics (eg item response theory analysis in instructor dashboard)
MITX_FEATURES['ENABLE_INSTRUCTOR_ANALYTICS'] = True MITX_FEATURES['ENABLE_INSTRUCTOR_ANALYTICS'] = True
MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True
MITX_FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True
WIKI_ENABLED = True WIKI_ENABLED = True
......
...@@ -27,6 +27,8 @@ MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = False ...@@ -27,6 +27,8 @@ MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = False
MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True
MITX_FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True
# Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it. # Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it.
WIKI_ENABLED = True WIKI_ENABLED = True
......
<%inherit file="/main.html" />
<%namespace name='static' file='/static_content.html'/>
<%namespace name="content" file="/courseware/hint_manager_inner.html"/>
<%block name="headextra">
<%static:css group='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>
% 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
<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
...@@ -430,6 +430,13 @@ if settings.MITX_FEATURES.get('ENABLE_DEBUG_RUN_PYTHON'): ...@@ -430,6 +430,13 @@ if settings.MITX_FEATURES.get('ENABLE_DEBUG_RUN_PYTHON'):
url(r'^debug/run_python', 'debug.views.run_python'), url(r'^debug/run_python', 'debug.views.run_python'),
) )
# Crowdsourced hinting instructor manager.
if settings.MITX_FEATURES.get('ENABLE_HINTER_INSTRUCTOR_VIEW'):
urlpatterns += (
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/hint_manager$',
'instructor.hint_manager.hint_manager', name="hint_manager"),
)
urlpatterns = patterns(*urlpatterns) urlpatterns = patterns(*urlpatterns)
if settings.DEBUG: if settings.DEBUG:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment