Commit ab303e75 by Felix Sun

Fixed numerous code-formatting issues and pep8 violations.

Began enforcing one-vote-per-person.  This can be disabled with debug="True" in the <crowdsource_hinter> tag.

Started tests of the hint manager.
parent 242d0c28
class @Hinter 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) -> constructor: (element) ->
@el = $(element).find('.crowdsource-wrapper') @el = $(element).find('.crowdsource-wrapper')
@url = @el.data('url') @url = @el.data('url')
Logger.listen('problem_graded', @el.data('child-url'), @capture_problem) Logger.listen('problem_graded', @el.data('child-url'), @capture_problem)
# The line below will eventually be generated by Python.
@render() @render()
capture_problem: (event_type, data, element) => capture_problem: (event_type, data, element) =>
...@@ -32,7 +35,6 @@ class @Hinter ...@@ -32,7 +35,6 @@ class @Hinter
@$('.custom-hint').click @clear_default_text @$('.custom-hint').click @clear_default_text
@$('#answer-tabs').tabs({active: 0}) @$('#answer-tabs').tabs({active: 0})
vote: (eventObj) => vote: (eventObj) =>
target = @$(eventObj.currentTarget) target = @$(eventObj.currentTarget)
post_json = {'answer': target.data('answer'), 'hint': target.data('hintno')} post_json = {'answer': target.data('answer'), 'hint': target.data('hintno')}
...@@ -42,7 +44,6 @@ class @Hinter ...@@ -42,7 +44,6 @@ class @Hinter
submit_hint: (eventObj) => submit_hint: (eventObj) =>
target = @$(eventObj.currentTarget) target = @$(eventObj.currentTarget)
textarea_id = '#custom-hint-' + target.data('answer') textarea_id = '#custom-hint-' + target.data('answer')
console.debug(textarea_id)
post_json = {'answer': target.data('answer'), 'hint': @$(textarea_id).val()} post_json = {'answer': target.data('answer'), 'hint': @$(textarea_id).val()}
$.postWithPrefix "#{@url}/submit_hint",post_json, (response) => $.postWithPrefix "#{@url}/submit_hint",post_json, (response) =>
@render(response.contents) @render(response.contents)
...@@ -53,7 +54,6 @@ class @Hinter ...@@ -53,7 +54,6 @@ class @Hinter
target.val('') target.val('')
target.data('cleared', true) target.data('cleared', true)
feedback_ui_change: => feedback_ui_change: =>
# Make all of the previous-answer divs hidden. # Make all of the previous-answer divs hidden.
@$('.previous-answer').css('display', 'none') @$('.previous-answer').css('display', 'none')
...@@ -61,7 +61,6 @@ class @Hinter ...@@ -61,7 +61,6 @@ class @Hinter
selector = '#previous-answer-' + @$('#feedback-select option:selected').attr('value') selector = '#previous-answer-' + @$('#feedback-select option:selected').attr('value')
@$(selector).css('display', 'inline') @$(selector).css('display', 'inline')
render: (content) -> render: (content) ->
if content if content
@el.html(content) @el.html(content)
......
...@@ -97,6 +97,17 @@ class CHModuleFactory(object): ...@@ -97,6 +97,17 @@ class CHModuleFactory(object):
return module 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): class CrowdsourceHinterTest(unittest.TestCase):
...@@ -105,6 +116,22 @@ class CrowdsourceHinterTest(unittest.TestCase): ...@@ -105,6 +116,22 @@ class CrowdsourceHinterTest(unittest.TestCase):
a correct answer. 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): def test_gethint_0hint(self):
""" """
Someone asks for a hint, when there's no hint to give. Someone asks for a hint, when there's no hint to give.
...@@ -182,6 +209,18 @@ class CrowdsourceHinterTest(unittest.TestCase): ...@@ -182,6 +209,18 @@ class CrowdsourceHinterTest(unittest.TestCase):
out = m.get_feedback(json_in) out = m.get_feedback(json_in)
self.assertTrue(len(out['index_to_hints'][0])==2) 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): def test_vote_nopermission(self):
""" """
A user tries to vote for a hint, but he has already voted! A user tries to vote for a hint, but he has already voted!
...@@ -197,13 +236,16 @@ class CrowdsourceHinterTest(unittest.TestCase): ...@@ -197,13 +236,16 @@ class CrowdsourceHinterTest(unittest.TestCase):
def test_vote_withpermission(self): def test_vote_withpermission(self):
""" """
A user votes for a hint. 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} 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']['0'][1] == 40)
self.assertTrue(m.hints['24.0']['3'][1] == 31) 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): def test_submithint_nopermission(self):
...@@ -256,6 +298,16 @@ class CrowdsourceHinterTest(unittest.TestCase): ...@@ -256,6 +298,16 @@ class CrowdsourceHinterTest(unittest.TestCase):
self.assertTrue('29.0' not in m.hints) self.assertTrue('29.0' not in m.hints)
self.assertTrue('29.0' in m.mod_queue) 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': '<script> alert("Trololo"); </script>'}
m.submit_hint(json_in)
print m.hints
self.assertTrue(m.hints['29.0'][0][0] == u'&lt;script&gt; alert(&quot;Trololo&quot;); &lt;/script&gt;')
def test_template_gethint(self): def test_template_gethint(self):
""" """
...@@ -284,7 +336,9 @@ class CrowdsourceHinterTest(unittest.TestCase): ...@@ -284,7 +336,9 @@ class CrowdsourceHinterTest(unittest.TestCase):
def test_template_feedback(self): def test_template_feedback(self):
""" """
Test the templates for get_feedback. Test the templates for get_feedback.
""" NOT FINISHED
from lxml import etree
m = CHModuleFactory.create() m = CHModuleFactory.create()
def fake_get_feedback(get): def fake_get_feedback(get):
...@@ -297,9 +351,11 @@ class CrowdsourceHinterTest(unittest.TestCase): ...@@ -297,9 +351,11 @@ class CrowdsourceHinterTest(unittest.TestCase):
m.get_feedback = fake_get_feedback m.get_feedback = fake_get_feedback
json_in = {'problem_name': '42.5'} json_in = {'problem_name': '42.5'}
out = json.loads(m.handle_ajax('get_feedback', json_in))['contents'] out = json.loads(m.handle_ajax('get_feedback', json_in))['contents']
html_tree = etree.XML(out)
# To be continued...
"""
pass
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<%def name="get_hint()"> <%def name="get_hint()">
% if best_hint != '': % if best_hint != '':
<h4> Other students who arrvied at the wrong answer of ${answer} recommend the following hints: </h4> <h4> Other students who arrived at the wrong answer of ${answer} recommend the following hints: </h4>
<ul> <ul>
<li> ${best_hint} </li> <li> ${best_hint} </li>
% endif % endif
......
''' """
Views for hint management. Views for hint management.
'''
from collections import defaultdict Along with the crowdsource_hinter xmodule, this code is still
import csv experimental, and should not be used in new courses, yet.
"""
import json import json
import logging
from markupsafe import escape
import os
import re 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.http import HttpResponse, Http404
from django_future.csrf import ensure_csrf_cookie 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 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.courses import get_course_with_access
from courseware.models import XModuleContentField from courseware.models import XModuleContentField
...@@ -43,7 +32,9 @@ def hint_manager(request, course_id): ...@@ -43,7 +32,9 @@ def hint_manager(request, course_id):
field = request.POST['field'] field = request.POST['field']
if not (field == 'mod_queue' or field == 'hints'): if not (field == 'mod_queue' or field == 'hints'):
# Invalid field. (Don't let users continue - they may overwrite other db's) # 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': if request.POST['op'] == 'delete hints':
delete_hints(request, course_id, field) delete_hints(request, course_id, field)
if request.POST['op'] == 'switch fields': if request.POST['op'] == 'switch fields':
...@@ -58,12 +49,23 @@ def hint_manager(request, course_id): ...@@ -58,12 +49,23 @@ def hint_manager(request, course_id):
return HttpResponse(json.dumps({'success': True, 'contents': rendered_html})) return HttpResponse(json.dumps({'success': True, 'contents': rendered_html}))
def get_hints(request, course_id, field): 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'. Load all of the hints submitted to the course.
# DON'T TRUST field attributes that come from ajax. Use an if statement
# to make sure the field is valid before plugging into functions. 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': if field == 'mod_queue':
other_field = 'hints' other_field = 'hints'
...@@ -76,47 +78,60 @@ def get_hints(request, course_id, field): ...@@ -76,47 +78,60 @@ def get_hints(request, course_id, field):
chopped_id = '/'.join(course_id.split('/')[:-1]) chopped_id = '/'.join(course_id.split('/')[:-1])
chopped_id = re.escape(chopped_id) chopped_id = re.escape(chopped_id)
all_hints = XModuleContentField.objects.filter(field_name=field, definition_id__regex=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 = {} big_out_dict = {}
name_dict = {} # name_dict[problem id] = Display name of problem
for problem in all_hints: id_to_name = {}
loc = Location(problem.definition_id)
for hints_by_problem in all_hints:
loc = Location(hints_by_problem.definition_id)
try: try:
descriptor = modulestore().get_items(loc)[0] descriptor = modulestore().get_items(loc)[0]
except IndexError: except IndexError:
# Sometimes, the problem is no longer in the course. Just # Sometimes, the problem is no longer in the course. Just
# don't include said problem. # don't include said problem.
continue 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. # Answer list contains (answer, dict_of_hints) tuples.
def answer_sorter(thing): def answer_sorter(thing):
''' """
thing is a tuple, where thing[0] contains an answer, and thing[1] contains 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. is used as a key to sort the list of things.
''' """
try: try:
return float(thing[0]) return float(thing[0])
except ValueError: except ValueError:
# Put all non-numerical answers first. # Put all non-numerical answers first.
return float('-inf') return float('-inf')
answer_list = sorted(json.loads(problem.value).items(), key=answer_sorter) answer_list = sorted(json.loads(hints_by_problem.value).items(), key=answer_sorter)
big_out_dict[problem.definition_id] = answer_list big_out_dict[hints_by_problem.definition_id] = answer_list
render_dict = {'field': field, render_dict = {'field': field,
'other_field': other_field, 'other_field': other_field,
'field_label': field_label, 'field_label': field_label,
'other_field_label': other_field_label, 'other_field_label': other_field_label,
'all_hints': big_out_dict, 'all_hints': big_out_dict,
'id_to_name': name_dict} 'id_to_name': id_to_name}
return render_dict return render_dict
def delete_hints(request, course_id, field): def delete_hints(request, course_id, field):
''' """
Deletes the hints specified by the [problem_defn_id, answer, pk] tuples in the numbered Deletes the hints specified.
fields of request.POST.
''' 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: for key in request.POST:
if key == 'op' or key == 'field': if key == 'op' or key == 'field':
continue continue
...@@ -129,31 +144,37 @@ def delete_hints(request, course_id, field): ...@@ -129,31 +144,37 @@ def delete_hints(request, course_id, field):
this_problem.value = json.dumps(problem_dict) this_problem.value = json.dumps(problem_dict)
this_problem.save() this_problem.save()
def change_votes(request, course_id, field): def change_votes(request, course_id, field):
''' """
Updates the number of votes. The numbered fields of request.POST contain Updates the number of votes.
[problem_id, answer, pk, new_votes] tuples.
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. - Very similar to delete_hints. Is there a way to merge them? Nah, too complicated.
''' """
for key in request.POST: for key in request.POST:
if key == 'op' or key == 'field': if key == 'op' or key == 'field':
continue continue
problem_id, answer, pk, new_votes = request.POST.getlist(key) problem_id, answer, pk, new_votes = request.POST.getlist(key)
this_problem = XModuleContentField.objects.get(field_name=field, definition_id=problem_id) this_problem = XModuleContentField.objects.get(field_name=field, definition_id=problem_id)
problem_dict = json.loads(this_problem.value) 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 problem_dict[answer][pk][1] = new_votes
this_problem.value = json.dumps(problem_dict) this_problem.value = json.dumps(problem_dict)
this_problem.save() this_problem.save()
def add_hint(request, course_id, field): def add_hint(request, course_id, field):
''' """
Add a new hint. POST: Add a new hint. request.POST:
op op
field field
problem - The problem id problem - The problem id
answer - The answer to which a hint will be added answer - The answer to which a hint will be added
hint - The text of the hint hint - The text of the hint
''' """
problem_id = request.POST['problem'] problem_id = request.POST['problem']
answer = request.POST['answer'] answer = request.POST['answer']
hint_text = request.POST['hint'] hint_text = request.POST['hint']
...@@ -171,13 +192,15 @@ def add_hint(request, course_id, field): ...@@ -171,13 +192,15 @@ def add_hint(request, course_id, field):
this_problem.value = json.dumps(problem_dict) this_problem.value = json.dumps(problem_dict)
this_problem.save() this_problem.save()
def approve(request, course_id, field): def approve(request, course_id, field):
''' """
Approve a list of hints, moving them from the mod_queue to the real Approve a list of hints, moving them from the mod_queue to the real
hint list. POST: hint list. POST:
op, field op, field
(some number) -> [problem, answer, pk] (some number) -> [problem, answer, pk]
''' """
for key in request.POST: for key in request.POST:
if key == 'op' or key == 'field': if key == 'op' or key == 'field':
continue continue
...@@ -197,29 +220,4 @@ def approve(request, course_id, field): ...@@ -197,29 +220,4 @@ def approve(request, course_id, field):
problem_dict[answer] = {} problem_dict[answer] = {}
problem_dict[answer][pk] = hint_to_move problem_dict[answer][pk] = hint_to_move
problem_in_hints.value = json.dumps(problem_dict) problem_in_hints.value = json.dumps(problem_dict)
problem_in_hints.save() problem_in_hints.save()
\ No newline at end of file
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)
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