Commit b6760ceb by Felix Sun

Fixed a small, but dangerous, string-to-integer casting bug in hint_manager.

Expanded tests of hint_manager.

Enabled the hint_manager by default in development environments.
parent 6890563d
...@@ -5,6 +5,7 @@ import random ...@@ -5,6 +5,7 @@ import random
import xmodule import xmodule
from xmodule.crowdsource_hinter import CrowdsourceHinterModule from xmodule.crowdsource_hinter import CrowdsourceHinterModule
from xmodule.vertical_module import VerticalModule, VerticalDescriptor
from xmodule.modulestore import Location from xmodule.modulestore import Location
from django.http import QueryDict from django.http import QueryDict
...@@ -96,6 +97,65 @@ class CHModuleFactory(object): ...@@ -96,6 +97,65 @@ class CHModuleFactory(object):
return module return module
class VerticalWithModulesFactory(object):
"""
Makes a vertical with several crowdsourced hinter modules inside.
Used to make sure that several crowdsourced hinter modules can co-exist
on one vertical.
"""
sample_problem_xml = """<?xml version="1.0"?>
<vertical display_name="Test vertical">
<crowdsource_hinter>
<problem display_name="Numerical Input" markdown=" " rerandomize="never" showanswer="finished">
<p>Test numerical problem.</p>
<numericalresponse answer="5">
<textline/>
</numericalresponse>
<solution>
<div class="detailed-solution">
<p>Explanation</p>
<p>If you look at your hand, you can count that you have five fingers. </p>
</div>
</solution>
</problem>
</crowdsource_hinter>
<crowdsource_hinter>
<problem display_name="Numerical Input" markdown=" " rerandomize="never" showanswer="finished">
<p>Another test numerical problem.</p>
<numericalresponse answer="5">
<textline/>
</numericalresponse>
<solution>
<div class="detailed-solution">
<p>Explanation</p>
<p>If you look at your hand, you can count that you have five fingers. </p>
</div>
</solution>
</problem>
</crowdsource_hinter>
</vertical>
"""
num = 0
@staticmethod
def next_num():
CHModuleFactory.num += 1
return CHModuleFactory.num
@staticmethod
def create():
location = Location(["i4x", "edX", "capa_test", "vertical",
"SampleVertical{0}".format(CHModuleFactory.next_num())])
model_data = {'data': VerticalWithModulesFactory.sample_problem_xml}
system = get_test_system()
descriptor = VerticalDescriptor.from_xml(VerticalWithModulesFactory.sample_problem_xml, system)
module = VerticalModule(system, descriptor, model_data)
return module
class FakeChild(object): class FakeChild(object):
""" """
...@@ -132,6 +192,19 @@ class CrowdsourceHinterTest(unittest.TestCase): ...@@ -132,6 +192,19 @@ class CrowdsourceHinterTest(unittest.TestCase):
self.assertTrue('This is supposed to be test html.' in out_html) self.assertTrue('This is supposed to be test html.' in out_html)
self.assertTrue('this/is/a/fake/ajax/url' in out_html) self.assertTrue('this/is/a/fake/ajax/url' in out_html)
def test_gethtml_multiple(self):
"""
Makes sure that multiple crowdsourced hinters play nice, when get_html
is called.
NOT WORKING RIGHT NOW
"""
return
m = VerticalWithModulesFactory.create()
out_html = m.get_html()
print out_html
self.assertTrue('Test numerical problem.' in out_html)
self.assertTrue('Another test numerical problem.' 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.
......
...@@ -66,7 +66,6 @@ def get_hints(request, course_id, field): ...@@ -66,7 +66,6 @@ def get_hints(request, course_id, field):
Sorted by answer. Sorted by answer.
- id_to_name: A dictionary mapping problem id to problem name. - 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'
field_label = 'Hints Awaiting Moderation' field_label = 'Hints Awaiting Moderation'
...@@ -85,13 +84,10 @@ def get_hints(request, course_id, field): ...@@ -85,13 +84,10 @@ def get_hints(request, course_id, field):
for hints_by_problem in all_hints: for hints_by_problem in all_hints:
loc = Location(hints_by_problem.definition_id) loc = Location(hints_by_problem.definition_id)
try: name = location_to_problem_name(loc)
descriptor = modulestore().get_items(loc)[0] if name is None:
except IndexError:
# Sometimes, the problem is no longer in the course. Just
# don't include said problem.
continue continue
id_to_name[hints_by_problem.definition_id] = descriptor.get_children()[0].display_name id_to_name[hints_by_problem.definition_id] = 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):
...@@ -117,6 +113,19 @@ def get_hints(request, course_id, field): ...@@ -117,6 +113,19 @@ def get_hints(request, course_id, field):
'id_to_name': id_to_name} 'id_to_name': id_to_name}
return render_dict 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): def delete_hints(request, course_id, field):
""" """
...@@ -128,8 +137,8 @@ def delete_hints(request, course_id, field): ...@@ -128,8 +137,8 @@ def delete_hints(request, course_id, field):
Example request.POST: Example request.POST:
{'op': 'delete_hints', {'op': 'delete_hints',
'field': 'mod_queue', 'field': 'mod_queue',
1: ['problem_whatever', '42.0', 3], 1: ['problem_whatever', '42.0', '3'],
2: ['problem_whatever', '32.5', 12]} 2: ['problem_whatever', '32.5', '12']}
""" """
for key in request.POST: for key in request.POST:
...@@ -160,7 +169,7 @@ def change_votes(request, course_id, field): ...@@ -160,7 +169,7 @@ def change_votes(request, course_id, field):
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] points to a [hint_text, #votes] pair.
problem_dict[answer][pk][1] = new_votes problem_dict[answer][pk][1] = int(new_votes)
this_problem.value = json.dumps(problem_dict) this_problem.value = json.dumps(problem_dict)
this_problem.save() this_problem.save()
......
from factory import DjangoModelFactory
import unittest import unittest
import nose.tools import nose.tools
import json import json
from django.http import Http404 from django.http import Http404
from django.test.client import Client from django.test.client import Client, RequestFactory
from django.test.utils import override_settings from django.test.utils import override_settings
import mitxmako.middleware import mitxmako.middleware
from courseware.models import XModuleContentField 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 import instructor.hint_manager as view
from student.tests.factories import UserFactory, AdminFactory 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 from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
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) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class HintManagerTest(ModuleStoreTestCase): class HintManagerTest(ModuleStoreTestCase):
...@@ -49,18 +24,39 @@ class HintManagerTest(ModuleStoreTestCase): ...@@ -49,18 +24,39 @@ class HintManagerTest(ModuleStoreTestCase):
Makes a course, which will be the same for all tests. Makes a course, which will be the same for all tests.
Set up mako middleware, which is necessary for template rendering to happen. Set up mako middleware, which is necessary for template rendering to happen.
""" """
course = CourseFactory.create(org='Me', number='19.002', display_name='test_course') 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): def test_student_block(self):
""" """
Makes sure that students cannot see the hint management view. Makes sure that students cannot see the hint management view.
""" """
nose.tools.set_trace()
c = Client() c = Client()
user = UserFactory.create(username='robot', email='robot@edx.org', password='test') user = UserFactory.create(username='student', email='student@edx.org', password='test')
c.login(username='robot', password='test') c.login(username='student', password='test')
out = c.get('/courses/Me/19.002/test_course/hint_manager') out = c.get(self.url)
print out print out
self.assertTrue('Sorry, but students are not allowed to access the hint manager!' in out.content) self.assertTrue('Sorry, but students are not allowed to access the hint manager!' in out.content)
...@@ -68,12 +64,62 @@ class HintManagerTest(ModuleStoreTestCase): ...@@ -68,12 +64,62 @@ class HintManagerTest(ModuleStoreTestCase):
""" """
Makes sure that staff can access the hint management view. Makes sure that staff can access the hint management view.
""" """
c = Client() out = self.c.get('/courses/Me/19.002/test_course/hint_manager')
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 print out
self.assertTrue('Hints Awaiting Moderation' in out.content) 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'})
# Keep this around for reference - might be useful later.
# request = RequestFactory()
# post = request.post(self.url, {'op': 'delete hints', 'field': 'all your private data'})
# out = view.hint_manager(post, 'Me/19.002/test_course')
print out
self.assertTrue('an invalid field was accessed' 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_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)
...@@ -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
......
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