Began work on instructor view to hinting system.
Added moderation feature - you can now choose to hold all hints for moderator approval before showing.
This commit is contained in:
@@ -42,6 +42,14 @@ class CrowdsourceHinterFields(object):
|
||||
user_voted = Boolean(help='Specifies if the user has voted on this problem or not.',
|
||||
scope=Scope.user_state, default=False)
|
||||
|
||||
moderate = String(help='''If 'True', then all hints must be approved by staff before
|
||||
becoming visible.
|
||||
This field is automatically populated from the xml metadata.''', scope=Scope.settings,
|
||||
default='False')
|
||||
|
||||
mod_queue = Dict(help='''Contains hints that have not been approved by the staff yet. Structured
|
||||
identically to the hints dictionary.''', scope=Scope.content, default={})
|
||||
|
||||
|
||||
class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
|
||||
''' An Xmodule that makes crowdsourced hints.
|
||||
@@ -115,7 +123,11 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
|
||||
print self.hints
|
||||
answer = self.ans_to_text(get)
|
||||
# Look for a hint to give.
|
||||
<<<<<<< HEAD
|
||||
if answer not in self.hints:
|
||||
=======
|
||||
if (answer not in self.hints) or (len(self.hints[answer]) == 0):
|
||||
>>>>>>> Began work on instructor view to hinting system.
|
||||
# No hints to give. Return.
|
||||
self.previous_answers += [(answer, (None, None, None))]
|
||||
return json.dumps({'contents': ' '})
|
||||
@@ -126,12 +138,23 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
|
||||
if len(self.hints[answer]) == 1:
|
||||
rand_hint_1 = ''
|
||||
rand_hint_2 = ''
|
||||
<<<<<<< HEAD
|
||||
self.previous_answers += [(answer, (0, None, None))]
|
||||
elif len(self.hints[answer]) == 2:
|
||||
best_hint = self.hints[answer][0][0]
|
||||
rand_hint_1 = self.hints[answer][1][0]
|
||||
rand_hint_2 = ''
|
||||
self.previous_answers += [(answer, (0, 1, None))]
|
||||
=======
|
||||
self.previous_answers += [[answer, [best_hint_index, None, None]]]
|
||||
elif n_hints == 2:
|
||||
best_hint = self.hints[answer].values()[0][0]
|
||||
best_hint_index = self.hints[answer].keys()[0]
|
||||
rand_hint_1 = self.hints[answer].values()[1][0]
|
||||
hint_index_1 = self.hints[answer].keys()[1]
|
||||
rand_hint_2 = ''
|
||||
self.previous_answers += [[answer, [best_hint_index, hint_index_1, None]]]
|
||||
>>>>>>> Began work on instructor view to hinting system.
|
||||
else:
|
||||
hint_index_1, hint_index_2 = random.sample(xrange(len(self.hints[answer])), 2)
|
||||
rand_hint_1 = self.hints[answer][hint_index_1][0]
|
||||
@@ -163,10 +186,20 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
|
||||
'" style="display:none"> Which hint was most helpful when you got the wrong answer of '\
|
||||
+ answer + '?'
|
||||
# Add each hint to the html string, with a vote button.
|
||||
for j, hint_id in enumerate(hints_offered):
|
||||
for hint_id in hints_offered:
|
||||
if hint_id != None:
|
||||
<<<<<<< HEAD
|
||||
out += '<br /><input class="vote" data-answer="'+str(i)+'" data-hintno="'+str(j)+\
|
||||
'" type="button" value="Vote"> ' + self.hints[answer][hint_id][0]
|
||||
=======
|
||||
hint_id = str(hint_id)
|
||||
try:
|
||||
out += '<br /><input class="vote" data-answer="'+str(i)+'" data-hintno="'+hint_id+\
|
||||
'" type="button" value="Vote"> ' + self.hints[answer][hint_id][0]
|
||||
except KeyError:
|
||||
# Sometimes, the hint that a user saw will have been deleted by the instructor.
|
||||
continue
|
||||
>>>>>>> Began work on instructor view to hinting system.
|
||||
|
||||
|
||||
# Or, let the student create his own hint
|
||||
@@ -227,15 +260,45 @@ What would you say to help someone who got this wrong answer?
|
||||
answer = self.previous_answers[int(get['answer'])][0]
|
||||
# Add the new hint to self.hints. (Awkward because a direct write
|
||||
# is necessary.)
|
||||
<<<<<<< HEAD
|
||||
temp_dict = self.hints
|
||||
temp_dict[answer].append([hint, 1]) # With one vote (the user himself).
|
||||
self.hints = temp_dict
|
||||
=======
|
||||
if self.moderate:
|
||||
temp_dict = self.mod_queue
|
||||
else:
|
||||
temp_dict = self.hints
|
||||
if answer in temp_dict:
|
||||
temp_dict[answer][self.hint_pk] = [hint, 1] # With one vote (the user himself).
|
||||
else:
|
||||
temp_dict[answer] = {self.hint_pk: [hint, 1]}
|
||||
self.hint_pk += 1
|
||||
if self.moderate:
|
||||
self.mod_queue = temp_dict
|
||||
else:
|
||||
self.hints = temp_dict
|
||||
>>>>>>> Began work on instructor view to hinting system.
|
||||
# Mark the user has having voted; reset previous_answers
|
||||
self.user_voted = True
|
||||
self.previous_answers = []
|
||||
return json.dumps({'contents': 'Thank you for your hint!'})
|
||||
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
def delete_hint(self, answer, hint_id):
|
||||
'''
|
||||
From the answer, delete the hint with hint_id.
|
||||
Not designed to be accessed via POST request, for now.
|
||||
-LIKELY DEPRECATED.
|
||||
'''
|
||||
temp_hints = self.hints
|
||||
del temp_hints[answer][str(hint_id)]
|
||||
self.hints = temp_hints
|
||||
|
||||
|
||||
>>>>>>> Began work on instructor view to hinting system.
|
||||
class CrowdsourceHinterDescriptor(CrowdsourceHinterFields, XmlDescriptor):
|
||||
module_class = CrowdsourceHinterModule
|
||||
stores_state = True
|
||||
|
||||
138
lms/djangoapps/instructor/hint_manager.py
Normal file
138
lms/djangoapps/instructor/hint_manager.py
Normal file
@@ -0,0 +1,138 @@
|
||||
'''
|
||||
Views for hint management.
|
||||
'''
|
||||
|
||||
from collections import defaultdict
|
||||
import csv
|
||||
import json
|
||||
import logging
|
||||
from markupsafe import escape
|
||||
import os
|
||||
import re
|
||||
import requests
|
||||
from requests.status_codes import codes
|
||||
import urllib
|
||||
from collections import OrderedDict
|
||||
|
||||
from StringIO import StringIO
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.http import HttpResponse, Http404
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.cache import cache_control
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from courseware.courses import get_course_with_access
|
||||
from courseware.models import XModuleContentField
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def hint_manager(request, course_id):
|
||||
try:
|
||||
course = 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
|
||||
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)
|
||||
return
|
||||
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)
|
||||
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):
|
||||
# field indicates the database entry that we are modifying.
|
||||
# Right now, the options are 'hints' or 'mod_queue'.
|
||||
# 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'
|
||||
other_field_label = 'Approved Hints'
|
||||
elif field == 'hints':
|
||||
other_field = 'mod_queue'
|
||||
field_label = 'Approved Hints'
|
||||
other_field_label = 'Hints Awaiting Moderation'
|
||||
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)
|
||||
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 />'
|
||||
|
||||
|
||||
out += '<button id="hint-delete"> Delete selected </button> <button id="update-votes"> Update votes </button>'
|
||||
render_dict = {'out': out,
|
||||
'field': field,
|
||||
'other_field': other_field,
|
||||
'field_label': field_label,
|
||||
'other_field_label': other_field_label,
|
||||
'all_hints': all_hints}
|
||||
return render_dict
|
||||
|
||||
def delete_hints(request, course_id, field):
|
||||
'''
|
||||
Deletes the hints specified by the [problem_defn_id, answer, pk] tuples in the numbered
|
||||
fields of request.POST.
|
||||
'''
|
||||
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][1] = new_votes
|
||||
this_problem.value = json.dumps(problem_dict)
|
||||
this_problem.save()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
89
lms/templates/courseware/hint_manager.html
Normal file
89
lms/templates/courseware/hint_manager.html
Normal file
@@ -0,0 +1,89 @@
|
||||
<%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").live('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")];
|
||||
}
|
||||
});
|
||||
$.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
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
$(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>
|
||||
39
lms/templates/courseware/hint_manager_inner.html
Normal file
39
lms/templates/courseware/hint_manager_inner.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<%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 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 pk, hint in hint_dict.items():
|
||||
<p data-problem="${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>
|
||||
</p>
|
||||
% endfor
|
||||
% endfor
|
||||
|
||||
<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 />
|
||||
% endfor
|
||||
|
||||
<button id="hint-delete"> Delete selected </button> <button id="update-votes"> Update votes </button>
|
||||
|
||||
|
||||
</%block>
|
||||
@@ -264,6 +264,9 @@ if settings.COURSEWARE_ENABLED:
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor$',
|
||||
'instructor.views.instructor_dashboard', name="instructor_dashboard"),
|
||||
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/hint_manager$',
|
||||
'instructor.hint_manager.hint_manager', name="hint_manager"),
|
||||
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/gradebook$',
|
||||
'instructor.views.gradebook', name='gradebook'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/grade_summary$',
|
||||
|
||||
Reference in New Issue
Block a user