diff --git a/common/lib/xmodule/xmodule/crowdsource_hinter.py b/common/lib/xmodule/xmodule/crowdsource_hinter.py
index c5d5dd7f80..664cf85f1a 100644
--- a/common/lib/xmodule/xmodule/crowdsource_hinter.py
+++ b/common/lib/xmodule/xmodule/crowdsource_hinter.py
@@ -1,20 +1,20 @@
+"""
+Adds crowdsourced hinting functionality to lon-capa numerical response problems.
+
+Currently experimental - not for instructor use, yet.
+"""
+
import logging
-import copy
import json
-import os
-import re
-import string
import random
-from pkg_resources import resource_listdir, resource_string, resource_isdir
+from pkg_resources import resource_string
from lxml import etree
-from xmodule.modulestore import Location
-from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor
-from xblock.core import XBlock, Scope, String, Integer, Float, Boolean, Dict, List
+from xblock.core import Scope, String, Integer, Boolean, Dict, List
from django.utils.html import escape
@@ -22,112 +22,123 @@ log = logging.getLogger(__name__)
class CrowdsourceHinterFields(object):
+ """Defines fields for the crowdsource hinter module."""
has_children = True
- hints = Dict(help="""A dictionary mapping answers to lists of [hint, number_of_votes] pairs.
- """, scope=Scope.content, default= {})
-
- previous_answers = List(help="""A list of previous answers this student made to this problem.
- Of the form (answer, (hint_id_1, hint_id_2, hint_id_3)) for each problem. hint_id's are
- None if the hint was not given.""",
- 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)
-
- 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.content,
- 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={})
+ 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')
+ # hints[answer] = {str(pk): [hint_text, #votes]}
+ 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 answers this student made to this problem.
+ # Of the form (answer, (hint_pk_1, hint_pk_2, hint_pk_3)) for each problem. hint_pk's are
+ # None if the hint was not given.
+ previous_answers = 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.
+ """
+ An Xmodule that makes crowdsourced hints.
+ Currently, only works on capa problems with exactly one numerical response,
+ and no other parts.
+
+ Example usage:
+
+
+
+
+ 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'
-
- js = {'coffee': [resource_string(__name__, 'js/src/crowdsource_hinter/display.coffee'),
- ],
- 'js': []}
+ js = {'coffee': [resource_string(__name__, 'js/src/crowdsource_hinter/display.coffee')],
+ 'js': []}
js_module_name = "Hinter"
-
def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs)
-
def get_html(self):
"""
- Does a regular expression find and replace to change the AJAX url.
+ Puts a wrapper around the problem html. This wrapper includes ajax urls of the
+ hinter and of the problem.
- Dependent on lon-capa problem.
"""
- # Reset the user vote, for debugging only! Remove for prod.
- self.user_voted = False
- # You are invited to guess what the lines below do :)
+ 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 = {}
- for child in self.get_display_items():
+ try:
+ child = self.get_display_items()[0]
out = child.get_html()
# The event listener uses the ajax url to find the child.
child_url = child.system.ajax_url
- break
+ except IndexError:
+ out = 'Error in loading crowdsourced hinter - can\'t find child problem.'
+ child_url = ''
+
# Wrap the module in a . This lets us pass data attributes to the javascript.
out += ''
return out
- def capa_make_answer_hashable(self, answer):
- """
- Capa answer format: dict[problem name] -> [list of answers]
- Output format: ((problem name, (answers)))
- """
- out = []
- for problem, a in answer.items():
- out.append((problem, tuple(a)))
- return str(tuple(sorted(out)))
-
-
- def ans_to_text(self, answer):
+ def capa_answer_to_str(self, answer):
"""
Converts capa answer format to a string representation
of the answer.
-Lon-capa dependent.
+ -Assumes that the problem only has one part.
"""
return str(float(answer.values()[0]))
-
def handle_ajax(self, dispatch, get):
"""
This is the landing method for AJAX calls.
"""
if dispatch == 'get_hint':
out = self.get_hint(get)
- if dispatch == 'get_feedback':
+ elif dispatch == 'get_feedback':
out = self.get_feedback(get)
- if dispatch == 'vote':
+ elif dispatch == 'vote':
out = self.tally_vote(get)
- if dispatch == 'submit_hint':
+ elif dispatch == 'submit_hint':
out = self.submit_hint(get)
+ else:
+ return json.dumps({'contents': 'Error - invalid operation.'})
- if out == None:
+ if out is None:
out = {'op': 'empty'}
else:
out.update({'op': dispatch})
return json.dumps({'contents': self.system.render_template('hinter_display.html', out)})
-
def get_hint(self, get):
"""
The student got the incorrect answer found in get. Give him a hint.
+
+ Called by hinter javascript after a problem is graded as incorrect.
+ Args:
+ get -- must be interpretable by capa_answer_to_str.
+ Output keys:
+ - best_hint is the hint text with the most votes.
+ - rand_hint_1 and rand_hint_2 are two random hints to the answer in get.
+ - answer is the parsed answer that was submitted.
"""
- answer = self.ans_to_text(get)
+ answer = self.capa_answer_to_str(get)
# Look for a hint to give.
# Make a local copy of self.hints - this means we only need to do one json unpacking.
+ # (This is because xblocks storage makes the following command a deep copy.)
local_hints = self.hints
if (answer not in local_hints) or (len(local_hints[answer]) == 0):
# No hints to give. Return.
@@ -156,13 +167,19 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
self.previous_answers += [(answer, (best_hint_index, hint_index_1, hint_index_2))]
return {'best_hint': best_hint,
- 'rand_hint_1': rand_hint_1,
- 'rand_hint_2': rand_hint_2,
+ 'rand_hint_1': rand_hint_1,
+ 'rand_hint_2': rand_hint_2,
'answer': answer}
def get_feedback(self, get):
"""
The student got it correct. Ask him to vote on hints, or submit a hint.
+
+ Args:
+ get -- not actually used. (It is assumed that the answer is correct.)
+ Output keys:
+ - index_to_hints maps previous answer indices to hints that the user saw earlier.
+ - index_to_answer maps previous answer indices to the actual answer submitted.
"""
# The student got it right.
# Did he submit at least one wrong answer?
@@ -178,13 +195,15 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
# index_to_answer[previous answer #] = answer text
index_to_answer = {}
+ # 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]
index_to_hints[i] = []
index_to_answer[i] = 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 != None:
+ if hint_id is None:
try:
index_to_hints[i].append((self.hints[answer][str(hint_id)][0], hint_id))
except KeyError:
@@ -193,22 +212,24 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
return {'index_to_hints': index_to_hints, 'index_to_answer': index_to_answer}
-
def tally_vote(self, get):
"""
Tally a user's vote on his favorite hint.
- get:
+
+ Args:
+ get -- expected to have the following keys:
'answer': ans_no (index in previous_answers)
- 'hint': hint_no
+ 'hint': hint_pk
+ Returns key hint_and_votes, a list of (hint_text, #votes) pairs.
"""
if self.user_voted:
- return json.dumps({'contents': 'Sorry, but you have already voted!'})
- ans_no = int(get['answer'])
+ return json.dumps({'contents': 'Sorry, but you have already voted!'})
+ ans_no = int(get['answer'])
hint_no = str(get['hint'])
answer = self.previous_answers[ans_no][0]
+ # We use temp_dict because we need to do a direct write for the database to update.
temp_dict = self.hints
temp_dict[answer][hint_no][1] += 1
- # Awkward, but you need to do a direct write for the database to update.
self.hints = temp_dict
# Don't let the user vote again!
self.user_voted = True
@@ -216,7 +237,7 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
# Return a list of how many votes each hint got.
hint_and_votes = []
for hint_no in self.previous_answers[ans_no][1]:
- if hint_no == None:
+ if hint_no is None:
continue
hint_and_votes.append(temp_dict[answer][str(hint_no)])
@@ -227,16 +248,20 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
def submit_hint(self, get):
"""
Take a hint submission and add it to the database.
- get:
+
+ Args:
+ get -- expected to have the following keys:
'answer': answer index in previous_answers
'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(get['hint'])
answer = self.previous_answers[int(get['answer'])][0]
+ # 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. (Awkward because a direct write
+ 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
@@ -257,17 +282,6 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
return {'message': 'Thank you for your hint!'}
- 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
-
-
class CrowdsourceHinterDescriptor(CrowdsourceHinterFields, XmlDescriptor):
module_class = CrowdsourceHinterModule
stores_state = True
diff --git a/common/lib/xmodule/xmodule/js/src/crowdsource_hinter/display.coffee b/common/lib/xmodule/xmodule/js/src/crowdsource_hinter/display.coffee
index 8eeab4cb02..ea42601622 100644
--- a/common/lib/xmodule/xmodule/js/src/crowdsource_hinter/display.coffee
+++ b/common/lib/xmodule/xmodule/js/src/crowdsource_hinter/display.coffee
@@ -1,10 +1,13 @@
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)
- # The line below will eventually be generated by Python.
@render()
capture_problem: (event_type, data, element) =>
@@ -32,7 +35,6 @@ class @Hinter
@$('.custom-hint').click @clear_default_text
@$('#answer-tabs').tabs({active: 0})
-
vote: (eventObj) =>
target = @$(eventObj.currentTarget)
post_json = {'answer': target.data('answer'), 'hint': target.data('hintno')}
@@ -42,7 +44,6 @@ class @Hinter
submit_hint: (eventObj) =>
target = @$(eventObj.currentTarget)
textarea_id = '#custom-hint-' + target.data('answer')
- console.debug(textarea_id)
post_json = {'answer': target.data('answer'), 'hint': @$(textarea_id).val()}
$.postWithPrefix "#{@url}/submit_hint",post_json, (response) =>
@render(response.contents)
@@ -53,7 +54,6 @@ class @Hinter
target.val('')
target.data('cleared', true)
-
feedback_ui_change: =>
# Make all of the previous-answer divs hidden.
@$('.previous-answer').css('display', 'none')
@@ -61,7 +61,6 @@ class @Hinter
selector = '#previous-answer-' + @$('#feedback-select option:selected').attr('value')
@$(selector).css('display', 'inline')
-
render: (content) ->
if content
@el.html(content)
diff --git a/common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py b/common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py
index 350abe9c8f..31614c4849 100644
--- a/common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py
+++ b/common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py
@@ -97,6 +97,17 @@ class CHModuleFactory(object):
return module
+class FakeChild(object):
+ """
+ A fake Xmodule.
+ """
+ def __init__(self):
+ self.system = Mock()
+ self.system.ajax_url = 'this/is/a/fake/ajax/url'
+
+ def get_html(self):
+ return 'This is supposed to be test html.'
+
class CrowdsourceHinterTest(unittest.TestCase):
@@ -105,6 +116,22 @@ class CrowdsourceHinterTest(unittest.TestCase):
a correct answer.
"""
+ def test_gethtml(self):
+ """
+ A simple test of get_html - make sure it returns the html of the inner
+ problem.
+ """
+ m = CHModuleFactory.create()
+ def fake_get_display_items():
+ """
+ A mock of get_display_items
+ """
+ return [FakeChild()]
+ m.get_display_items = fake_get_display_items
+ out_html = m.get_html()
+ self.assertTrue('This is supposed to be test html.' in out_html)
+ self.assertTrue('this/is/a/fake/ajax/url' in out_html)
+
def test_gethint_0hint(self):
"""
Someone asks for a hint, when there's no hint to give.
@@ -182,6 +209,18 @@ class CrowdsourceHinterTest(unittest.TestCase):
out = m.get_feedback(json_in)
self.assertTrue(len(out['index_to_hints'][0])==2)
+
+ def test_getfeedback_missingkey(self):
+ """
+ Someone gets a problem correct, but one of the hints that he saw
+ earlier (pk=100) has been deleted. Should just skip that hint.
+ """
+ m = CHModuleFactory.create(
+ previous_answers=[['24.0', [0, 100, None]]])
+ json_in = {'problem_name': '42.5'}
+ out = m.get_feedback(json_in)
+ self.assertTrue(len(out['index_to_hints'][0])==1)
+
def test_vote_nopermission(self):
"""
A user tries to vote for a hint, but he has already voted!
@@ -197,13 +236,16 @@ class CrowdsourceHinterTest(unittest.TestCase):
def test_vote_withpermission(self):
"""
A user votes for a hint.
+ Also tests vote result rendering.
"""
- m = CHModuleFactory.create()
+ m = CHModuleFactory.create(
+ previous_answers=[['24.0', [0, 3, None]]])
json_in = {'answer': 0, 'hint': 3}
- m.tally_vote(json_in)
+ dict_out = m.tally_vote(json_in)
self.assertTrue(m.hints['24.0']['0'][1] == 40)
self.assertTrue(m.hints['24.0']['3'][1] == 31)
- self.assertTrue(m.hints['24.0']['4'][1] == 20)
+ self.assertTrue(['Best hint', 40] in dict_out['hint_and_votes'])
+ self.assertTrue(['Another hint', 31] in dict_out['hint_and_votes'])
def test_submithint_nopermission(self):
@@ -256,6 +298,16 @@ class CrowdsourceHinterTest(unittest.TestCase):
self.assertTrue('29.0' not in m.hints)
self.assertTrue('29.0' in m.mod_queue)
+ def test_submithint_escape(self):
+ """
+ Make sure that hints are being html-escaped.
+ """
+ m = CHModuleFactory.create()
+ json_in = {'answer': 1, 'hint': ''}
+ m.submit_hint(json_in)
+ print m.hints
+ self.assertTrue(m.hints['29.0'][0][0] == u'<script> alert("Trololo"); </script>')
+
def test_template_gethint(self):
"""
@@ -284,7 +336,9 @@ class CrowdsourceHinterTest(unittest.TestCase):
def test_template_feedback(self):
"""
Test the templates for get_feedback.
- """
+ NOT FINISHED
+
+ from lxml import etree
m = CHModuleFactory.create()
def fake_get_feedback(get):
@@ -297,9 +351,11 @@ class CrowdsourceHinterTest(unittest.TestCase):
m.get_feedback = fake_get_feedback
json_in = {'problem_name': '42.5'}
out = json.loads(m.handle_ajax('get_feedback', json_in))['contents']
-
-
-
+ html_tree = etree.XML(out)
+ # To be continued...
+
+ """
+ pass
diff --git a/common/templates/hinter_display.html b/common/templates/hinter_display.html
index a253f9f639..f05bb34c40 100644
--- a/common/templates/hinter_display.html
+++ b/common/templates/hinter_display.html
@@ -4,7 +4,7 @@
<%def name="get_hint()">
% if best_hint != '':
- Other students who arrvied at the wrong answer of ${answer} recommend the following hints:
+ Other students who arrived at the wrong answer of ${answer} recommend the following hints:
- ${best_hint}
% endif
diff --git a/lms/djangoapps/instructor/hint_manager.py b/lms/djangoapps/instructor/hint_manager.py
index 520255a8fc..96ea91eabc 100644
--- a/lms/djangoapps/instructor/hint_manager.py
+++ b/lms/djangoapps/instructor/hint_manager.py
@@ -1,28 +1,17 @@
-'''
+"""
Views for hint management.
-'''
-from collections import defaultdict
-import csv
+Along with the crowdsource_hinter xmodule, this code is still
+experimental, and should not be used in new courses, yet.
+"""
+
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
@@ -43,7 +32,9 @@ def hint_manager(request, course_id):
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
+ 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':
@@ -58,12 +49,23 @@ def hint_manager(request, course_id):
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.
+ """
+ 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'
@@ -76,47 +78,60 @@ 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[problem id] = [[answer, {pk: [hint, votes]}], sorted by answer]
big_out_dict = {}
- name_dict = {}
- for problem in all_hints:
- loc = Location(problem.definition_id)
+ # name_dict[problem id] = Display name of problem
+ id_to_name = {}
+
+ for hints_by_problem in all_hints:
+ loc = Location(hints_by_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
+ id_to_name[hints_by_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
+ 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 = sorted(json.loads(problem.value).items(), key=answer_sorter)
- big_out_dict[problem.definition_id] = answer_list
+ 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': name_dict}
+ 'id_to_name': id_to_name}
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.
- '''
+ """
+ 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
@@ -129,31 +144,37 @@ def delete_hints(request, course_id, field):
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.
+ """
+ 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] = new_votes
this_problem.value = json.dumps(problem_dict)
this_problem.save()
+
def add_hint(request, course_id, field):
- '''
- Add a new hint. POST:
+ """
+ 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']
@@ -171,13 +192,15 @@ def add_hint(request, course_id, field):
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
@@ -197,29 +220,4 @@ def approve(request, course_id, field):
problem_dict[answer] = {}
problem_dict[answer][pk] = hint_to_move
problem_in_hints.value = json.dumps(problem_dict)
- problem_in_hints.save()
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ problem_in_hints.save()
\ No newline at end of file
diff --git a/lms/djangoapps/instructor/tests/test_hint_manager.py b/lms/djangoapps/instructor/tests/test_hint_manager.py
new file mode 100644
index 0000000000..44e676dd83
--- /dev/null
+++ b/lms/djangoapps/instructor/tests/test_hint_manager.py
@@ -0,0 +1,79 @@
+from factory import DjangoModelFactory
+import unittest
+import nose.tools
+import json
+
+from django.http import Http404
+from django.test.client import Client
+from django.test.utils import override_settings
+import mitxmako.middleware
+
+from courseware.models import XModuleContentField
+import instructor.hint_manager as view
+from student.tests.factories import UserFactory, AdminFactory
+from xmodule.modulestore.tests.factories import CourseFactory
+from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
+from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
+
+
+class HintsFactory(DjangoModelFactory):
+ FACTORY_FOR = XModuleContentField
+ definition_id = 'i4x://Me/19.002/crowdsource_hinter/crowdsource_hinter_001'
+ field_name = 'hints'
+ value = json.dumps({'1.0':
+ {'1': ['Hint 1', 2],
+ '3': ['Hint 3', 12]},
+ '2.0':
+ {'4': ['Hint 4', 3]}
+ })
+
+class ModQueueFactory(DjangoModelFactory):
+ FACTORY_FOR = XModuleContentField
+ definition_id = 'i4x://Me/19.002/crowdsource_hinter/crowdsource_hinter_001'
+ field_name = 'mod_queue'
+ value = json.dumps({'2.0':
+ {'2': ['Hint 2', 1]}
+ })
+
+class PKFactory(DjangoModelFactory):
+ FACTORY_FOR = XModuleContentField
+ definition_id = 'i4x://Me/19.002/crowdsource_hinter/crowdsource_hinter_001'
+ field_name = 'hint_pk'
+ value = 5
+
+@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.
+ """
+ course = CourseFactory.create(org='Me', number='19.002', display_name='test_course')
+ # mitxmako.middleware.MakoMiddleware()
+
+
+ def test_student_block(self):
+ """
+ Makes sure that students cannot see the hint management view.
+ """
+ c = Client()
+ user = UserFactory.create(username='robot', email='robot@edx.org', password='test')
+ c.login(username='robot', password='test')
+ out = c.get('/courses/Me/19.002/test_course/hint_manager')
+ 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.
+ """
+ c = Client()
+ user = UserFactory.create(username='robot', email='robot@edx.org', password='test', is_staff=True)
+ c.login(username='robot', password='test')
+ out = c.get('/courses/Me/19.002/test_course/hint_manager')
+ print out
+ self.assertTrue('Hints Awaiting Moderation' in out.content)
+
+
+