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
# 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)
......
......@@ -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': '<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):
"""
......@@ -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
......
......@@ -4,7 +4,7 @@
<%def name="get_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>
<li> ${best_hint} </li>
% endif
......
'''
"""
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
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