Merge pull request #9095 from solashirai/sola/feature_csh
(WIP) Crowdsource Hinter Prototype
This commit is contained in:
1
AUTHORS
1
AUTHORS
@@ -232,6 +232,7 @@ Daniel Naranjo <daniel.naranjo@edunext.co>
|
||||
Vedran Karačić <vedran@edx.org>
|
||||
William Ono <william.ono@ubc.ca>
|
||||
Dongwook Yoon <dy252@cornell.edu>
|
||||
Sola Shirai <sola@edx.org>
|
||||
Awais Qureshi <awais.qureshi@arbisoft.com>
|
||||
Eric Fischer <efischer@edx.org>
|
||||
Brian Beggs <macdiesel@gmail.com>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
PageObject for Crowdsourcehinter
|
||||
"""
|
||||
from bok_choy.page_object import PageObject
|
||||
|
||||
|
||||
class CrowdsourcehinterProblemPage(PageObject):
|
||||
"""
|
||||
A PageObject representing the Crowdsourcehinter xblock.
|
||||
"""
|
||||
|
||||
url = None
|
||||
|
||||
def __init__(self, browser):
|
||||
"""
|
||||
Args:
|
||||
browser (selenium.webdriver): The Selenium-controlled browser that this page is loaded in.
|
||||
"""
|
||||
super(CrowdsourcehinterProblemPage, self).__init__(browser)
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return len(self.browser.find_elements_by_class_name('crowdsourcehinter_block')) > 0
|
||||
|
||||
def submit_text_answer(self, text):
|
||||
"""
|
||||
Submit an answer to the problem block
|
||||
"""
|
||||
self.q(css='input[type="text"]').fill(text)
|
||||
self.q(css='.action [data-value="Check"]').click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
def get_hint_text(self):
|
||||
"""
|
||||
Return the hint shown to the student
|
||||
"""
|
||||
return self.q(css='div.csh_hint_text').text
|
||||
|
||||
def get_student_answer_text(self):
|
||||
"""
|
||||
Check the student answer is set correctly
|
||||
"""
|
||||
return self.q(css='div.csh_hint_text').attrs('student_answer')
|
||||
|
||||
def rate_hint(self):
|
||||
"""
|
||||
Click the rate_hint button
|
||||
"""
|
||||
self.q(css='div.csh_rate_hint').click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
def submit_new_hint(self, text):
|
||||
"""
|
||||
Fill in the textbox and submit a new hint
|
||||
"""
|
||||
self.q(css='.csh_student_hint_creation input[type="button"]').click()
|
||||
self.wait_for_ajax()
|
||||
self.q(css='.csh_student_text_input input[type="text"]').fill(text)
|
||||
self.q(css='.csh_submit_new input[type="button"]').click()
|
||||
self.wait_for_ajax()
|
||||
0
common/test/acceptance/tests/xblock/__init__.py
Normal file
0
common/test/acceptance/tests/xblock/__init__.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Javascript tests for the crowdsourcehinter xblock
|
||||
"""
|
||||
from textwrap import dedent
|
||||
from common.test.acceptance.fixtures.course import CourseFixture, XBlockFixtureDesc
|
||||
from common.test.acceptance.pages.lms.courseware import CoursewarePage
|
||||
from common.test.acceptance.pages.xblock.crowdsourcehinter_problem import CrowdsourcehinterProblemPage
|
||||
from common.test.acceptance.pages.studio.auto_auth import AutoAuthPage
|
||||
from common.test.acceptance.tests.helpers import UniqueCourseTest
|
||||
|
||||
|
||||
class CrowdsourcehinterProblemTest(UniqueCourseTest):
|
||||
"""
|
||||
Test scenario for the hinter.
|
||||
"""
|
||||
USERNAME = "STAFF_TESTER"
|
||||
EMAIL = "johndoe@example.com"
|
||||
|
||||
def setUp(self):
|
||||
super(CrowdsourcehinterProblemTest, self).setUp()
|
||||
|
||||
self.courseware_page = CoursewarePage(self.browser, self.course_id)
|
||||
|
||||
# Install a course with sections/problems, tabs, updates, and handouts
|
||||
course_fix = CourseFixture(
|
||||
self.course_info['org'], self.course_info['number'],
|
||||
self.course_info['run'], self.course_info['display_name']
|
||||
)
|
||||
problem_data = dedent('''
|
||||
<problem>
|
||||
<p>A text input problem accepts a line of text from the student, and evaluates the input for correctness based on an expected answer.</p>
|
||||
<p>The answer is correct if it matches every character of the expected answer. This can be a problem with international spelling, dates, or anything where the format of the answer is not clear.</p>
|
||||
<p>Which US state has Lansing as its capital?</p>
|
||||
<stringresponse answer="Michigan" type="ci" >
|
||||
<textline label="Which US state has Lansing as its capital?" size="20"/>
|
||||
</stringresponse>
|
||||
<solution>
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>Lansing is the capital of Michigan, although it is not Michigan's largest city, or even the seat of the county in which it resides.</p>
|
||||
</div>
|
||||
</solution>
|
||||
</problem>
|
||||
''')
|
||||
|
||||
children = XBlockFixtureDesc('chapter', 'Test Section').add_children(
|
||||
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
|
||||
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
|
||||
XBlockFixtureDesc('problem', 'text input problem', data=problem_data),
|
||||
XBlockFixtureDesc('crowdsourcehinter', 'test crowdsourcehinter')
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
course_fix.add_children(children).install()
|
||||
|
||||
# Auto-auth register for the course.
|
||||
AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL,
|
||||
course_id=self.course_id, staff=False).visit()
|
||||
|
||||
def _goto_csh_problem_page(self):
|
||||
"""
|
||||
Visit the page courseware page containing the hinter
|
||||
"""
|
||||
self.courseware_page.visit()
|
||||
csh_problem_page = CrowdsourcehinterProblemPage(self.browser)
|
||||
self.assertGreater(len(self.browser.find_elements_by_class_name('crowdsourcehinter_block')), 0)
|
||||
return csh_problem_page
|
||||
|
||||
def test_student_hint_workflow(self):
|
||||
"""
|
||||
Test the basic workflow of a student recieving hints. The student should submit an incorrect answer and
|
||||
receive a hint (in this case no hint since none are set), be able to rate that hint, see a different UX
|
||||
after submitting a correct answer, and be capable of contributing a new hint to the system.
|
||||
"""
|
||||
csh_problem_page = self._goto_csh_problem_page()
|
||||
|
||||
csh_problem_page.submit_text_answer("michigann")
|
||||
csh_problem_page.wait_for_ajax()
|
||||
self.assertEqual(csh_problem_page.get_hint_text()[0], u"Hint: Sorry, there are no hints for this answer.")
|
||||
|
||||
self.assertGreater(len(self.browser.find_elements_by_class_name('csh_rate_hint')), 0)
|
||||
csh_problem_page.rate_hint()
|
||||
csh_problem_page.wait_for_ajax()
|
||||
|
||||
csh_problem_page.submit_text_answer("michigan")
|
||||
csh_problem_page.wait_for_ajax()
|
||||
self.assertGreater(len(self.browser.find_elements_by_id('show_hint_rating_ux')), 0)
|
||||
|
||||
csh_problem_page.submit_new_hint("new hint text")
|
||||
0
openedx/tests/xblock_integration/pages/__init__.py
Normal file
0
openedx/tests/xblock_integration/pages/__init__.py
Normal file
292
openedx/tests/xblock_integration/test_crowdsource_hinter.py
Normal file
292
openedx/tests/xblock_integration/test_crowdsource_hinter.py
Normal file
@@ -0,0 +1,292 @@
|
||||
"""
|
||||
Test scenarios for the crowdsource hinter xblock.
|
||||
"""
|
||||
import json
|
||||
import unittest
|
||||
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory
|
||||
from lms.djangoapps.lms_xblock.runtime import quote_slashes
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class TestCrowdsourceHinter(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
Create the test environment with the crowdsourcehinter xblock.
|
||||
"""
|
||||
STUDENTS = [
|
||||
{'email': 'view@test.com', 'password': 'foo'},
|
||||
{'email': 'view2@test.com', 'password': 'foo'}
|
||||
]
|
||||
XBLOCK_NAMES = ['crowdsourcehinter']
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# Nose runs setUpClass methods even if a class decorator says to skip
|
||||
# the class: https://github.com/nose-devs/nose/issues/946
|
||||
# So, skip the test class here if we are not in the LMS.
|
||||
if settings.ROOT_URLCONF != 'lms.urls':
|
||||
raise unittest.SkipTest('Test only valid in lms')
|
||||
|
||||
super(TestCrowdsourceHinter, cls).setUpClass()
|
||||
cls.course = CourseFactory.create(
|
||||
display_name='CrowdsourceHinter_Test_Course'
|
||||
)
|
||||
with cls.store.bulk_operations(cls.course.id, emit_signals=False):
|
||||
cls.chapter = ItemFactory.create(
|
||||
parent=cls.course, display_name='Overview'
|
||||
)
|
||||
cls.section = ItemFactory.create(
|
||||
parent=cls.chapter, display_name='Welcome'
|
||||
)
|
||||
cls.unit = ItemFactory.create(
|
||||
parent=cls.section, display_name='New Unit'
|
||||
)
|
||||
cls.xblock = ItemFactory.create(
|
||||
parent=cls.unit,
|
||||
category='crowdsourcehinter',
|
||||
display_name='crowdsourcehinter'
|
||||
)
|
||||
|
||||
cls.course_url = reverse(
|
||||
'courseware_section',
|
||||
kwargs={
|
||||
'course_id': cls.course.id.to_deprecated_string(),
|
||||
'chapter': 'Overview',
|
||||
'section': 'Welcome',
|
||||
}
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super(TestCrowdsourceHinter, self).setUp()
|
||||
for idx, student in enumerate(self.STUDENTS):
|
||||
username = "u{}".format(idx)
|
||||
self.create_account(username, student['email'], student['password'])
|
||||
self.activate_user(student['email'])
|
||||
|
||||
self.staff_user = GlobalStaffFactory()
|
||||
|
||||
def get_handler_url(self, handler, xblock_name=None):
|
||||
"""
|
||||
Get url for the specified xblock handler
|
||||
"""
|
||||
if xblock_name is None:
|
||||
xblock_name = TestCrowdsourceHinter.XBLOCK_NAMES[0]
|
||||
return reverse('xblock_handler', kwargs={
|
||||
'course_id': self.course.id.to_deprecated_string(),
|
||||
'usage_id': quote_slashes(self.course.id.make_usage_key('crowdsourcehinter', xblock_name).
|
||||
to_deprecated_string()),
|
||||
'handler': handler,
|
||||
'suffix': ''
|
||||
})
|
||||
|
||||
def enroll_student(self, email, password):
|
||||
"""
|
||||
Student login and enroll for the course
|
||||
"""
|
||||
self.login(email, password)
|
||||
self.enroll(self.course, verify=True)
|
||||
|
||||
def enroll_staff(self, staff):
|
||||
"""
|
||||
Staff login and enroll for the course
|
||||
"""
|
||||
email = staff.email
|
||||
password = 'test'
|
||||
self.login(email, password)
|
||||
self.enroll(self.course, verify=True)
|
||||
|
||||
def initialize_database_by_id(self, handler, resource_id, times, xblock_name=None):
|
||||
"""
|
||||
Call a ajax event (vote, delete, endorse) on a resource by its id
|
||||
several times
|
||||
"""
|
||||
if xblock_name is None:
|
||||
xblock_name = TestCrowdsourceHinter.XBLOCK_NAMES[0]
|
||||
url = self.get_handler_url(handler, xblock_name)
|
||||
for _ in range(times):
|
||||
self.client.post(url, json.dumps({'id': resource_id}), '')
|
||||
|
||||
def call_event(self, handler, resource, xblock_name=None):
|
||||
"""
|
||||
Call a ajax event (add, edit, flag, etc.) by specifying the resource
|
||||
it takes
|
||||
"""
|
||||
if xblock_name is None:
|
||||
xblock_name = TestCrowdsourceHinter.XBLOCK_NAMES[0]
|
||||
url = self.get_handler_url(handler, xblock_name)
|
||||
return self.client.post(url, json.dumps(resource), '')
|
||||
|
||||
def check_event_response_by_element(self, handler, resource, resp_key, resp_val, xblock_name=None):
|
||||
"""
|
||||
Call the event specified by the handler with the resource, and check
|
||||
whether the element (resp_key) in response is as expected (resp_val)
|
||||
"""
|
||||
if xblock_name is None:
|
||||
xblock_name = TestCrowdsourceHinter.XBLOCK_NAMES[0]
|
||||
resp = self.call_event(handler, resource, xblock_name)
|
||||
self.assertEqual(resp[resp_key], resp_val)
|
||||
self.assert_request_status_code(200, self.course_url)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class TestHinterFunctions(TestCrowdsourceHinter):
|
||||
"""
|
||||
Check that the essential functions of the hinter work as expected.
|
||||
Tests cover the basic process of receiving a hint, adding a new hint,
|
||||
and rating/reporting hints.
|
||||
"""
|
||||
def test_get_hint_with_no_hints(self):
|
||||
"""
|
||||
Check that a generic statement is returned when no default/specific hints exist
|
||||
"""
|
||||
result = self.call_event('get_hint', {'submittedanswer': 'ans=incorrect+answer+1'}, 'crowdsourcehinter')
|
||||
expected = {'BestHint': 'Sorry, there are no hints for this answer.', 'StudentAnswer': 'incorrect answer 1',
|
||||
'HintCategory': False}
|
||||
self.assertEqual(json.loads(result.content), expected)
|
||||
|
||||
def test_add_new_hint(self):
|
||||
"""
|
||||
Test the ability to add a new specific hint
|
||||
"""
|
||||
self.enroll_student(self.STUDENTS[0]['email'], self.STUDENTS[0]['password'])
|
||||
data = {'new_hint_submission': 'new hint for answer 1', 'answer': 'incorrect answer 1'}
|
||||
self.call_event('get_hint', {'submittedanswer': 'ans=incorrect+answer+1'})
|
||||
result = self.call_event('add_new_hint', data)
|
||||
expected = {'success': True,
|
||||
'result': 'Hint added'}
|
||||
self.assertEqual(json.loads(result.content), expected)
|
||||
|
||||
def test_get_hint(self):
|
||||
"""
|
||||
Check that specific hints are returned
|
||||
"""
|
||||
self.call_event('get_hint', {'submittedanswer': 'ans=incorrect+answer+1'})
|
||||
submission = {'new_hint_submission': 'new hint for answer 1',
|
||||
'answer': 'incorrect answer 1'}
|
||||
self.call_event('add_new_hint', submission)
|
||||
result = self.call_event('get_hint', {'submittedanswer': 'ans=incorrect+answer+1'})
|
||||
expected = {'BestHint': 'new hint for answer 1', 'StudentAnswer': 'incorrect answer 1',
|
||||
'HintCategory': 'ErrorResponse'}
|
||||
self.assertEqual(json.loads(result.content), expected)
|
||||
|
||||
def test_rate_hint_upvote(self):
|
||||
"""
|
||||
Test hint upvoting
|
||||
"""
|
||||
self.call_event('get_hint', {'submittedanswer': 'ans=incorrect+answer+1'})
|
||||
submission = {'new_hint_submission': 'new hint for answer 1',
|
||||
'answer': 'incorrect answer 1'}
|
||||
self.call_event('add_new_hint', submission)
|
||||
data = {
|
||||
'student_answer': 'incorrect answer 1',
|
||||
'hint': 'new hint for answer 1',
|
||||
'student_rating': 'upvote'
|
||||
}
|
||||
expected = {'success': True}
|
||||
result = self.call_event('rate_hint', data)
|
||||
self.assertEqual(json.loads(result.content), expected)
|
||||
|
||||
def test_rate_hint_downvote(self):
|
||||
"""
|
||||
Test hint downvoting
|
||||
"""
|
||||
self.call_event('get_hint', {'submittedanswer': 'ans=incorrect+answer+1'})
|
||||
submission = {'new_hint_submission': 'new hint for answer 1',
|
||||
'answer': 'incorrect answer 1'}
|
||||
self.call_event('add_new_hint', submission)
|
||||
data = {
|
||||
'student_answer': 'incorrect answer 1',
|
||||
'hint': 'new hint for answer 1',
|
||||
'student_rating': 'downvote'
|
||||
}
|
||||
expected = {'success': True}
|
||||
result = self.call_event('rate_hint', data)
|
||||
self.assertEqual(json.loads(result.content), expected)
|
||||
|
||||
def test_report_hint(self):
|
||||
"""
|
||||
Test hint reporting
|
||||
"""
|
||||
self.call_event('get_hint', {'submittedanswer': 'ans=incorrect+answer+1'})
|
||||
submission = {'new_hint_submission': 'new hint for answer 1',
|
||||
'answer': 'incorrect answer 1'}
|
||||
self.call_event('add_new_hint', submission)
|
||||
data = {
|
||||
'student_answer': 'incorrect answer 1',
|
||||
'hint': 'new hint for answer 1',
|
||||
'student_rating': 'report'
|
||||
}
|
||||
expected = {'rating': 'reported', 'hint': 'new hint for answer 1'}
|
||||
result = self.call_event('rate_hint', data)
|
||||
self.assertEqual(json.loads(result.content), expected)
|
||||
|
||||
def test_dont_show_reported_hint(self):
|
||||
"""
|
||||
Check that reported hints are returned
|
||||
"""
|
||||
self.call_event('get_hint', {'submittedanswer': 'ans=incorrect+answer+1'})
|
||||
submission = {'new_hint_submission': 'new hint for answer 1',
|
||||
'answer': 'incorrect answer 1'}
|
||||
self.call_event('add_new_hint', submission)
|
||||
data = {
|
||||
'student_answer': 'incorrect answer 1',
|
||||
'hint': 'new hint for answer 1',
|
||||
'student_rating': 'report'
|
||||
}
|
||||
self.call_event('rate_hint', data)
|
||||
result = self.call_event('get_hint', {'submittedanswer': 'ans=incorrect+answer+1'})
|
||||
expected = {'BestHint': 'Sorry, there are no hints for this answer.', 'StudentAnswer': 'incorrect answer 1',
|
||||
'HintCategory': False}
|
||||
self.assertEqual(json.loads(result.content), expected)
|
||||
|
||||
def test_get_used_hint_answer_data(self):
|
||||
"""
|
||||
Check that hint/answer information from previous submissions are returned upon correctly
|
||||
answering the problem
|
||||
"""
|
||||
self.call_event('get_hint', {'submittedanswer': 'ans=incorrect+answer+1'})
|
||||
self.call_event('get_used_hint_answer_data', "")
|
||||
submission = {'new_hint_submission': 'new hint for answer 1',
|
||||
'answer': 'incorrect answer 1'}
|
||||
self.call_event('add_new_hint', submission)
|
||||
self.call_event('get_hint', {'submittedanswer': 'ans=incorrect+answer+1'})
|
||||
result = self.call_event('get_used_hint_answer_data', "")
|
||||
expected = {'new hint for answer 1': 'incorrect answer 1'}
|
||||
self.assertEqual(json.loads(result.content), expected)
|
||||
|
||||
def test_show_best_hint(self):
|
||||
"""
|
||||
Check that the most upvoted hint is shown
|
||||
"""
|
||||
self.call_event('get_hint', {'submittedanswer': 'ans=incorrect+answer+1'})
|
||||
submission1 = {'new_hint_submission': 'new hint for answer 1',
|
||||
'answer': 'incorrect answer 1'}
|
||||
submission2 = {'new_hint_submission': 'new hint for answer 1 to report',
|
||||
'answer': 'incorrect answer 1'}
|
||||
self.call_event('add_new_hint', submission1)
|
||||
self.call_event('add_new_hint', submission2)
|
||||
data_upvote = {
|
||||
'student_answer': 'incorrect answer 1',
|
||||
'hint': 'new hint for answer 1 to report',
|
||||
'student_rating': 'upvote'
|
||||
}
|
||||
self.call_event('rate_hint', data_upvote)
|
||||
data_downvote = {
|
||||
'student_answer': 'incorrect answer 1',
|
||||
'hint': 'new hint for answer 1 to report',
|
||||
'student_rating': 'report'
|
||||
}
|
||||
self.call_event('rate_hint', data_downvote)
|
||||
result = self.call_event('get_hint', {'submittedanswer': 'ans=incorrect+answer+1'})
|
||||
expected = {'BestHint': 'new hint for answer 1', 'StudentAnswer': 'incorrect answer 1',
|
||||
'HintCategory': 'ErrorResponse'}
|
||||
self.assertEqual(json.loads(result.content), expected)
|
||||
@@ -84,6 +84,7 @@ git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3
|
||||
git+https://github.com/edx/i18n-tools.git@v0.2#egg=i18n-tools==v0.2
|
||||
git+https://github.com/edx/edx-val.git@0.0.9#egg=edxval==0.0.9
|
||||
-e git+https://github.com/pmitros/RecommenderXBlock.git@518234bc354edbfc2651b9e534ddb54f96080779#egg=recommender-xblock
|
||||
git+https://github.com/solashirai/crowdsourcehinter.git@518605f0a95190949fe77bd39158450639e2e1dc#egg=crowdsourcehinter-xblock==0.1
|
||||
-e git+https://github.com/pmitros/RateXBlock.git@367e19c0f6eac8a5f002fd0f1559555f8e74bfff#egg=rate-xblock
|
||||
-e git+https://github.com/pmitros/DoneXBlock.git@857bf365f19c904d7e48364428f6b93ff153fabd#egg=done-xblock
|
||||
git+https://github.com/edx/edx-milestones.git@v0.1.8#egg=edx-milestones==0.1.8
|
||||
|
||||
Reference in New Issue
Block a user