diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index bdc6e33fd9..73b4d4e168 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -35,7 +35,6 @@ XMODULES = [ "word_cloud = xmodule.word_cloud_module:WordCloudDescriptor", "hidden = xmodule.hidden_module:HiddenDescriptor", "raw = xmodule.raw_module:RawDescriptor", - "crowdsource_hinter = xmodule.crowdsource_hinter:CrowdsourceHinterDescriptor", "lti = xmodule.lti_module:LTIDescriptor", ] XBLOCKS = [ diff --git a/common/lib/xmodule/xmodule/crowdsource_hinter.py b/common/lib/xmodule/xmodule/crowdsource_hinter.py deleted file mode 100644 index 1d0d0c56a5..0000000000 --- a/common/lib/xmodule/xmodule/crowdsource_hinter.py +++ /dev/null @@ -1,404 +0,0 @@ -""" -Adds crowdsourced hinting functionality to lon-capa numerical response problems. - -Currently experimental - not for instructor use, yet. -""" - -import logging -import json -import random -import copy - -from pkg_resources import resource_string - -from lxml import etree - -from xmodule.x_module import XModule, STUDENT_VIEW -from xmodule.raw_module import RawDescriptor -from xblock.fields import Scope, String, Integer, Boolean, Dict, List - -from capa.responsetypes import FormulaResponse - -from django.utils.html import escape - -log = logging.getLogger(__name__) - - -class CrowdsourceHinterFields(object): - """Defines fields for the crowdsource hinter module.""" - has_children = True - - 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') - # Usage: hints[answer] = {str(pk): [hint_text, #votes]} - # hints is a dictionary that takes answer keys. - # Each value is itself a dictionary, accepting hint_pk strings as keys, - # and returning [hint text, #votes] pairs as values - 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 hints that a student viewed. - # Of the form [answer, [hint_pk_1, ...]] for each problem. - # Sorry about the variable name - I know it's confusing. - previous_answers = List(help='A list of hints viewed.', scope=Scope.user_state, default=[]) - - # user_submissions actually contains a list of previous answers submitted. - # (Originally, preivous_answers did this job, hence the name confusion.) - user_submissions = 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. - 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' - css = {'scss': [resource_string(__name__, 'css/crowdsource_hinter/display.scss')]} - js = {'coffee': [resource_string(__name__, 'js/src/crowdsource_hinter/display.coffee')], - 'js': []} - js_module_name = "Hinter" - - def __init__(self, *args, **kwargs): - super(CrowdsourceHinterModule, self).__init__(*args, **kwargs) - # We need to know whether we are working with a FormulaResponse problem. - try: - responder = self.get_display_items()[0].lcp.responders.values()[0] - except (IndexError, AttributeError): - log.exception('Unable to find a capa problem child.') - return - - self.is_formula = isinstance(self, FormulaResponse) - if self.is_formula: - self.answer_to_str = self.formula_answer_to_str - else: - self.answer_to_str = self.numerical_answer_to_str - # compare_answer is expected to return whether its two inputs are close enough - # to be equal, or raise a StudentInputError if one of the inputs is malformatted. - if hasattr(responder, 'compare_answer') and hasattr(responder, 'validate_answer'): - self.compare_answer = responder.compare_answer - self.validate_answer = responder.validate_answer - else: - # This response type is not supported! - log.exception('Response type not supported for hinting: ' + str(responder)) - - def get_html(self): - """ - Puts a wrapper around the problem html. This wrapper includes ajax urls of the - hinter and of the problem. - - Dependent on lon-capa problem. - """ - 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 = {} - - try: - child = self.get_display_items()[0] - out = child.render(STUDENT_VIEW).content - # The event listener uses the ajax url to find the child. - child_id = child.id - except IndexError: - out = u"Error in loading crowdsourced hinter - can't find child problem." - child_id = '' - - # Wrap the module in a
. This lets us pass data attributes to the javascript. - out += u'
'.format( - ajax_url=self.runtime.ajax_url, - child_id=child_id - ) - - return out - - def numerical_answer_to_str(self, answer): - """ - Converts capa numerical answer format to a string representation - of the answer. - -Lon-capa dependent. - -Assumes that the problem only has one part. - """ - return str(answer.values()[0]) - - def formula_answer_to_str(self, answer): - """ - Converts capa formula answer into a string. - -Lon-capa dependent. - -Assumes that the problem only has one part. - """ - return str(answer.values()[0]) - - def get_matching_answers(self, answer): - """ - Look in self.hints, and find all answer keys that are "equal with tolerance" - to the input answer. - """ - return [key for key in self.hints if self.compare_answer(key, answer)] - - def handle_ajax(self, dispatch, data): - """ - This is the landing method for AJAX calls. - """ - if dispatch == 'get_hint': - out = self.get_hint(data) - elif dispatch == 'get_feedback': - out = self.get_feedback(data) - elif dispatch == 'vote': - out = self.tally_vote(data) - elif dispatch == 'submit_hint': - out = self.submit_hint(data) - else: - return json.dumps({'contents': 'Error - invalid operation.'}) - - if out is None: - out = {'op': 'empty'} - elif 'error' in out: - # Error in processing. - out.update({'op': 'error'}) - else: - out.update({'op': dispatch}) - return json.dumps({'contents': self.runtime.render_template('hinter_display.html', out)}) - - def get_hint(self, data): - """ - The student got the incorrect answer found in data. Give him a hint. - - Called by hinter javascript after a problem is graded as incorrect. - Args: - `data` -- must be interpretable by answer_to_str. - Output keys: - - 'hints' is a list of hint strings to show to the user. - - 'answer' is the parsed answer that was submitted. - Will record the user's wrong answer in user_submissions, and the hints shown - in previous_answers. - """ - # First, validate our inputs. - try: - answer = self.answer_to_str(data) - except (ValueError, AttributeError): - # Sometimes, we get an answer that's just not parsable. Do nothing. - log.exception('Answer not parsable: ' + str(data)) - return - if not self.validate_answer(answer): - # Answer is not in the right form. - log.exception('Answer not valid: ' + str(answer)) - return - if answer not in self.user_submissions: - self.user_submissions += [answer] - - # For all answers similar enough to our own, accumulate all hints together. - # Also track the original answer of each hint. - matching_answers = self.get_matching_answers(answer) - matching_hints = {} - for matching_answer in matching_answers: - temp_dict = copy.deepcopy(self.hints[matching_answer]) - for key, value in temp_dict.items(): - # Each value now has hint, votes, matching_answer. - temp_dict[key] = value + [matching_answer] - matching_hints.update(temp_dict) - # matching_hints now maps pk's to lists of [hint, votes, matching_answer] - - # Finally, randomly choose a subset of matching_hints to actually show. - if not matching_hints: - # No hints to give. Return. - return - # Get the top hint, plus two random hints. - n_hints = len(matching_hints) - hints = [] - # max(dict) returns the maximum key in dict. - # The key function takes each pk, and returns the number of votes for the - # hint with that pk. - best_hint_index = max(matching_hints, key=lambda pk: matching_hints[pk][1]) - hints.append(matching_hints[best_hint_index][0]) - best_hint_answer = matching_hints[best_hint_index][2] - # The brackets surrounding the index are for backwards compatability purposes. - # (It used to be that each answer was paired with multiple hints in a list.) - self.previous_answers += [[best_hint_answer, [best_hint_index]]] - for _ in xrange(min(2, n_hints - 1)): - # Keep making random hints until we hit a target, or run out. - while True: - # random.choice randomly chooses an element from its input list. - # (We then unpack the item, in this case data for a hint.) - (hint_index, (rand_hint, _, hint_answer)) =\ - random.choice(matching_hints.items()) - if rand_hint not in hints: - break - hints.append(rand_hint) - self.previous_answers += [[hint_answer, [hint_index]]] - return {'hints': hints, - 'answer': answer} - - def get_feedback(self, data): - """ - The student got it correct. Ask him to vote on hints, or submit a hint. - - Args: - `data` -- not actually used. (It is assumed that the answer is correct.) - Output keys: - - 'answer_to_hints': a nested dictionary. - answer_to_hints[answer][hint_pk] returns the text of the hint. - - 'user_submissions': the same thing as self.user_submissions. A list of - the answers that the user previously submitted. - """ - # The student got it right. - # Did he submit at least one wrong answer? - if len(self.user_submissions) == 0: - # No. Nothing to do here. - return - # Make a hint-voting interface for each wrong answer. The student will only - # be allowed to make one vote / submission, but he can choose which wrong answer - # he wants to look at. - answer_to_hints = {} # answer_to_hints[answer text][hint pk] -> hint text - - # 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] - if answer not in answer_to_hints: - answer_to_hints[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 is not None) and (hint_id not in answer_to_hints[answer]): - try: - answer_to_hints[answer][hint_id] = self.hints[answer][str(hint_id)][0] - except KeyError: - # Sometimes, the hint that a user saw will have been deleted by the instructor. - continue - return {'answer_to_hints': answer_to_hints, - 'user_submissions': self.user_submissions} - - def tally_vote(self, data): - """ - Tally a user's vote on his favorite hint. - - Args: - `data` -- expected to have the following keys: - 'answer': text of answer we're voting on - 'hint': hint_pk - 'pk_list': A list of [answer, pk] pairs, each of which representing a hint. - We will return a list of how many votes each hint in the list has so far. - It's up to the browser to specify which hints to return vote counts for. - - Returns key 'hint_and_votes', a list of (hint_text, #votes) pairs. - """ - if self.user_voted: - return {'error': 'Sorry, but you have already voted!'} - ans = data['answer'] - if not self.validate_answer(ans): - # Uh oh. Invalid answer. - log.exception('Failure in hinter tally_vote: Unable to parse answer: {ans}'.format(ans=ans)) - return {'error': 'Failure in voting!'} - hint_pk = str(data['hint']) - # We use temp_dict because we need to do a direct write for the database to update. - temp_dict = self.hints - try: - temp_dict[ans][hint_pk][1] += 1 - except KeyError: - log.exception('''Failure in hinter tally_vote: User voted for non-existant hint: - Answer={ans} pk={hint_pk}'''.format(ans=ans, hint_pk=hint_pk)) - return {'error': 'Failure in voting!'} - self.hints = temp_dict - # Don't let the user vote again! - self.user_voted = True - - # Return a list of how many votes each hint got. - pk_list = json.loads(data['pk_list']) - hint_and_votes = [] - for answer, vote_pk in pk_list: - if not self.validate_answer(answer): - log.exception('In hinter tally_vote, couldn\'t parse {ans}'.format(ans=answer)) - continue - try: - hint_and_votes.append(temp_dict[answer][str(vote_pk)]) - except KeyError: - log.exception('In hinter tally_vote, couldn\'t find: {ans}, {vote_pk}'.format( - ans=answer, vote_pk=str(vote_pk))) - - hint_and_votes.sort(key=lambda pair: pair[1], reverse=True) - # Reset self.previous_answers and user_submissions. - self.previous_answers = [] - self.user_submissions = [] - return {'hint_and_votes': hint_and_votes} - - def submit_hint(self, data): - """ - Take a hint submission and add it to the database. - - Args: - `data` -- expected to have the following keys: - 'answer': text of answer - '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(data['hint']) - answer = data['answer'] - if not self.validate_answer(answer): - log.exception('Failure in hinter submit_hint: Unable to parse answer: {ans}'.format( - ans=answer)) - return {'error': 'Could not submit answer'} - # 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 or self.mod_queue. (Awkward because a direct write - # is necessary.) - if self.moderate == 'True': - temp_dict = self.mod_queue - else: - temp_dict = self.hints - if answer in temp_dict: - temp_dict[answer][str(self.hint_pk)] = [hint, 1] # With one vote (the user himself). - else: - temp_dict[answer] = {str(self.hint_pk): [hint, 1]} - self.hint_pk += 1 - if self.moderate == 'True': - self.mod_queue = temp_dict - else: - self.hints = temp_dict - # Mark the user has having voted; reset previous_answers - self.user_voted = True - self.previous_answers = [] - self.user_submissions = [] - return {'message': 'Thank you for your hint!'} - - -class CrowdsourceHinterDescriptor(CrowdsourceHinterFields, RawDescriptor): - module_class = CrowdsourceHinterModule - stores_state = True - - @classmethod - def definition_from_xml(cls, xml_object, system): - children = [] - for child in xml_object: - try: - child_block = system.process_xml(etree.tostring(child, encoding='unicode')) - children.append(child_block.scope_ids.usage_id) - except Exception as e: - log.exception("Unable to load child when parsing CrowdsourceHinter. Continuing...") - if system.error_tracker is not None: - system.error_tracker(u"ERROR: {0}".format(e)) - continue - return {}, children - - def definition_to_xml(self, resource_fs): - xml_object = etree.Element('crowdsource_hinter') - for child in self.get_children(): - self.runtime.add_block_as_child_node(child, xml_object) - return xml_object diff --git a/common/lib/xmodule/xmodule/css/crowdsource_hinter/display.scss b/common/lib/xmodule/xmodule/css/crowdsource_hinter/display.scss deleted file mode 100644 index beb6a70fec..0000000000 --- a/common/lib/xmodule/xmodule/css/crowdsource_hinter/display.scss +++ /dev/null @@ -1,40 +0,0 @@ -.crowdsource-wrapper { - box-shadow: inset 0 1px 2px 1px rgba(0,0,0,0.1); - border-radius: 2px; - display: none; - margin-top: $baseline; - padding: ($baseline*0.75); - background: rgb(253, 248, 235); -} - -.hint-inner-container { - padding-left: 15px; - padding-right: 15px; - font-size: 16px; -} - -.vote { - padding-top: 0 !important; - padding-bottom: 0 !important; -} - -.wizard-view { - float: left; - width: 790px; - margin-right: ($baseline/2); -} - -.wizard-container { - width: 3000px; - - -webkit-transition:all $tmg-s1 ease-in-out; - -moz-transition:all $tmg-s1 ease-in-out; - -o-transition:all $tmg-s1 ease-in-out; - transition:all $tmg-s1 ease-in-out; -} - -.wizard-viewbox { - width: 800px; - overflow: hidden; - position: relative; -} diff --git a/common/lib/xmodule/xmodule/js/fixtures/crowdsource_hinter.html b/common/lib/xmodule/xmodule/js/fixtures/crowdsource_hinter.html deleted file mode 100644 index 09644c1369..0000000000 --- a/common/lib/xmodule/xmodule/js/fixtures/crowdsource_hinter.html +++ /dev/null @@ -1,52 +0,0 @@ -
  • - - -
    - - -
    -
    - - -

    - Numerical Input -

    - -
    (1/1 point)
    - -
    -

    The answer is 2*x^2*y + 5 -


    Answer = -
    -
    - - -
    - - - -
    - -
    - - - - -
    -
    -
    - -
    - - - -
    - - - - -
    - - - -
  • diff --git a/common/lib/xmodule/xmodule/js/spec/crowdsource_hinter/display_spec.coffee b/common/lib/xmodule/xmodule/js/spec/crowdsource_hinter/display_spec.coffee deleted file mode 100644 index 80910ddc95..0000000000 --- a/common/lib/xmodule/xmodule/js/spec/crowdsource_hinter/display_spec.coffee +++ /dev/null @@ -1,55 +0,0 @@ -describe 'Crowdsourced hinter', -> - beforeEach -> - window.update_schematics = -> - jasmine.stubRequests() - # note that the fixturesPath is set in spec/helper.coffee - loadFixtures 'crowdsource_hinter.html' - @hinter = new Hinter($('#hinter-root')) - - describe 'high-level integration tests', -> - # High-level, happy-path tests for integration with capa problems. - beforeEach -> - # Make a more thorough $.postWithPrefix mock. - spyOn($, 'postWithPrefix').andCallFake( -> - last_argument = arguments[arguments.length - 1] - if typeof last_argument == 'function' - response = - success: 'incorrect' - contents: 'mock grader response' - last_argument(response) - promise = - always: (callable) -> callable() - done: (callable) -> callable() - ) - @problem = new Problem($('#problem')) - @problem.bind() - - it 'knows when a capa problem is graded, using check.', -> - @problem.answers = 'test answer' - @problem.check() - expect($.postWithPrefix).toHaveBeenCalledWith("#{@hinter.url}/get_hint", 'test answer', jasmine.any(Function)) - - it 'knows when a capa problem is graded usig check_fd.', -> - spyOn($, 'ajaxWithPrefix').andCallFake((url, settings) -> - response = - success: 'incorrect' - contents: 'mock grader response' - settings.success(response) if settings - ) - @problem.answers = 'test answer' - @problem.check_fd() - expect($.postWithPrefix).toHaveBeenCalledWith("#{@hinter.url}/get_hint", 'test answer', jasmine.any(Function)) - - describe 'capture_problem', -> - beforeEach -> - spyOn($, 'postWithPrefix').andReturn(null) - - it 'gets hints for an incorrect answer', -> - data = ['some answers', ''] - @hinter.capture_problem('problem_graded', data, 'fake element') - expect($.postWithPrefix).toHaveBeenCalledWith("#{@hinter.url}/get_hint", 'some answers', jasmine.any(Function)) - - it 'gets feedback for a correct answer', -> - data = ['some answers', ''] - @hinter.capture_problem('problem_graded', data, 'fake element') - expect($.postWithPrefix).toHaveBeenCalledWith("#{@hinter.url}/get_feedback", 'some answers', jasmine.any(Function)) diff --git a/common/lib/xmodule/xmodule/js/src/crowdsource_hinter/display.coffee b/common/lib/xmodule/xmodule/js/src/crowdsource_hinter/display.coffee deleted file mode 100644 index 8c22477034..0000000000 --- a/common/lib/xmodule/xmodule/js/src/crowdsource_hinter/display.coffee +++ /dev/null @@ -1,141 +0,0 @@ -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-id'), @capture_problem) - @render() - - capture_problem: (event_type, data, element) => - # After a problem gets graded, we get the info here. - # We want to send this info to the server in another AJAX - # request. - answers = data[0] - response = data[1] - if response.search(/class="correct/) == -1 - # Incorrect. Get hints. - $.postWithPrefix "#{@url}/get_hint", answers, (response) => - @render(response.contents) - else - # Correct. Get feedback from students. - $.postWithPrefix "#{@url}/get_feedback", answers, (response) => - @render(response.contents) - - $: (selector) -> - $(selector, @el) - - bind: => - @$('input.vote').click @vote - @$('input.submit-hint').click @submit_hint - @$('.custom-hint').click @clear_default_text - @$('.expand').click @expand - @$('.wizard-link').click @wizard_link_handle - @$('.answer-choice').click @answer_choice_handle - - expand: (eventObj) => - # Expand a hidden div. - target = @$('#' + @$(eventObj.currentTarget).data('target')) - if @$(target).css('display') == 'none' - @$(target).css('display', 'block') - else - @$(target).css('display', 'none') - # Fix positioning errors with the bottom class. - @set_bottom_links() - - vote: (eventObj) => - # Make an ajax request with the user's vote. - target = @$(eventObj.currentTarget) - all_pks = @$('#pk-list').attr('data-pk-list') - post_json = {'answer': target.attr('data-answer'), 'hint': target.data('hintno'), 'pk_list': all_pks} - $.postWithPrefix "#{@url}/vote", post_json, (response) => - @render(response.contents) - - submit_hint: (eventObj) => - # Make an ajax request with the user's new hint. - textarea = $('.custom-hint') - if @answer == '' - # The user didn't choose an answer, somehow. Do nothing. - return - post_json = {'answer': @answer, 'hint': textarea.val()} - $.postWithPrefix "#{@url}/submit_hint",post_json, (response) => - @render(response.contents) - - clear_default_text: (eventObj) => - # Remove placeholder text in the hint submission textbox. - target = @$(eventObj.currentTarget) - if target.data('cleared') == undefined - target.val('') - target.data('cleared', true) - - wizard_link_handle: (eventObj) => - # Move to another wizard view, based on the link that the user clicked. - target = @$(eventObj.currentTarget) - @go_to(target.attr('dest')) - - answer_choice_handle: (eventObj) => - # A special case of wizard_link_handle - we need to track a state variable, - # the answer that the user chose. - @answer = @$(eventObj.target).attr('value') - @$('#blank-answer').html(@answer) - @go_to('p3') - - set_bottom_links: => - # Makes each .bottom class stick to the bottom of .wizard-viewbox - @$('.bottom').css('margin-top', '0px') - viewbox_height = parseInt(@$('.wizard-viewbox').css('height'), 10) - @$('.bottom').each((index, obj) -> - view_height = parseInt($(obj).parent().css('height'), 10) - $(obj).css('margin-top', (viewbox_height - view_height) + 'px') - ) - - render: (content) -> - if content - # Trim leading and trailing whitespace - content = content.trim() - - if content - @el.html(content) - @el.show() - JavascriptLoader.executeModuleScripts @el, () => - @bind() - @$('#previous-answer-0').css('display', 'inline') - else - @el.hide() - # Initialize the answer choice - remembers which answer the user picked on - # p2 when he submits a hint on p3. - @answer = '' - # Determine whether the browser supports CSS3 transforms. - styles = document.body.style - if styles.WebkitTransform == '' or styles.transform == '' - @go_to = @transform_go_to - else - @go_to = @legacy_go_to - - # Make the correct wizard view show up. - hints_exist = @$('#hints-exist').html() == 'True' - if hints_exist - @go_to('p1') - else - @go_to('p2') - - transform_go_to: (view_id) -> - # Switch wizard views using sliding transitions. - id_to_index = { - 'p1': 0, - 'p2': 1, - 'p3': 2, - } - translate_string = 'translateX(' +id_to_index[view_id] * -1 * parseInt($('#' + view_id).css('width'), 10) + 'px)' - @$('.wizard-container').css('transform', translate_string) - @$('.wizard-container').css('-webkit-transform', translate_string) - @set_bottom_links() - - legacy_go_to: (view_id) -> - # For older browsers - switch wizard views by changing the screen. - @$('.wizard-view').css('display', 'none') - @$('#' + view_id).css('display', 'block') - @set_bottom_links() \ No newline at end of file diff --git a/common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py b/common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py deleted file mode 100644 index 02c670595d..0000000000 --- a/common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py +++ /dev/null @@ -1,583 +0,0 @@ -""" -Tests the crowdsourced hinter xmodule. -""" - -from mock import Mock, MagicMock -import unittest -import copy - -from xmodule.crowdsource_hinter import CrowdsourceHinterModule -from xmodule.vertical_block import VerticalBlock -from xmodule.x_module import STUDENT_VIEW -from xblock.field_data import DictFieldData -from xblock.fragment import Fragment -from xblock.core import XBlock -from xblock.fields import ScopeIds - -from . import get_test_system - -import json - - -class CHModuleFactory(object): - """ - Helps us make a CrowdsourceHinterModule with the specified internal - state. - """ - - sample_problem_xml = """ - - - -

    A numerical input problem accepts a line of text input from the student, and evaluates the input for correctness based on its numerical value.

    -

    The answer is correct if it is within a specified numerical tolerance of the expected answer.

    -

    Enter the number of fingers on a human hand:

    - - - - -
    -

    Explanation

    -

    If you look at your hand, you can count that you have five fingers.

    -
    -
    -
    -
    - """ - - num = 0 - - @staticmethod - def next_num(): - """ - Helps make unique names for our mock CrowdsourceHinterModule's - """ - CHModuleFactory.num += 1 - return CHModuleFactory.num - - @staticmethod - def create(hints=None, - previous_answers=None, - user_submissions=None, - user_voted=None, - moderate=None, - mod_queue=None): - """ - A factory method for making CHM's - """ - # Should have a single child, but it doesn't matter what that child is - field_data = {'data': CHModuleFactory.sample_problem_xml, 'children': [None]} - - if hints is not None: - field_data['hints'] = hints - else: - field_data['hints'] = { - '24.0': {'0': ['Best hint', 40], - '3': ['Another hint', 30], - '4': ['A third hint', 20], - '6': ['A less popular hint', 3]}, - '25.0': {'1': ['Really popular hint', 100]} - } - - if mod_queue is not None: - field_data['mod_queue'] = mod_queue - else: - field_data['mod_queue'] = { - '24.0': {'2': ['A non-approved hint']}, - '26.0': {'5': ['Another non-approved hint']} - } - - if previous_answers is not None: - field_data['previous_answers'] = previous_answers - else: - field_data['previous_answers'] = [ - ['24.0', [0, 3, 4]], - ['29.0', []] - ] - - if user_submissions is not None: - field_data['user_submissions'] = user_submissions - else: - field_data['user_submissions'] = ['24.0', '29.0'] - - if user_voted is not None: - field_data['user_voted'] = user_voted - - if moderate is not None: - field_data['moderate'] = moderate - - descriptor = Mock(weight='1') - # Make the descriptor have a capa problem child. - capa_descriptor = MagicMock() - capa_descriptor.name = 'capa' - capa_descriptor.displayable_items.return_value = [capa_descriptor] - descriptor.get_children.return_value = [capa_descriptor] - - # Make a fake capa module. - capa_module = MagicMock() - capa_module.lcp = MagicMock() - responder = MagicMock() - - def validate_answer(answer): - """ A mock answer validator - simulates a numerical response""" - try: - float(answer) - return True - except ValueError: - return False - responder.validate_answer = validate_answer - - def compare_answer(ans1, ans2): - """ A fake answer comparer """ - return ans1 == ans2 - responder.compare_answer = compare_answer - - capa_module.lcp.responders = {'responder0': responder} - capa_module.displayable_items.return_value = [capa_module] - - system = get_test_system() - # Make the system have a marginally-functional get_module - - def fake_get_module(descriptor): - """ - A fake module-maker. - """ - return capa_module - system.get_module = fake_get_module - module = CrowdsourceHinterModule(descriptor, system, DictFieldData(field_data), Mock()) - - 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 = """ - - - -

    Test numerical problem.

    - - - - -
    -

    Explanation

    -

    If you look at your hand, you can count that you have five fingers.

    -
    -
    -
    -
    - - - -

    Another test numerical problem.

    - - - - -
    -

    Explanation

    -

    If you look at your hand, you can count that you have five fingers.

    -
    -
    -
    -
    -
    - """ - - num = 0 - - @staticmethod - def next_num(): - """Increments a global counter for naming.""" - CHModuleFactory.num += 1 - return CHModuleFactory.num - - @staticmethod - def create(): - """Make a vertical.""" - field_data = {'data': VerticalWithModulesFactory.sample_problem_xml} - system = get_test_system() - descriptor = VerticalBlock.parse_xml(VerticalWithModulesFactory.sample_problem_xml, system) - module = VerticalBlock(system, descriptor, field_data) - - return module - - -class FakeChild(XBlock): - """ - A fake Xmodule. - """ - def __init__(self): - self.runtime = get_test_system() - self.student_view = Mock(return_value=Fragment(self.get_html())) - self.save = Mock() - self.id = 'i4x://this/is/a/fake/id' - self.scope_ids = ScopeIds('fake_user_id', 'fake_block_type', 'fake_definition_id', 'fake_usage_id') - - def get_html(self): - """ - Return a fake html string. - """ - return u'This is supposed to be test html.' - - -class CrowdsourceHinterTest(unittest.TestCase): - """ - In the below tests, '24.0' represents a wrong answer, and '42.5' represents - a correct answer. - """ - - def test_gethtml(self): - """ - A simple test of get_html - make sure it returns the html of the inner - problem. - """ - mock_module = CHModuleFactory.create() - - def fake_get_display_items(): - """ - A mock of get_display_items - """ - return [FakeChild()] - mock_module.get_display_items = fake_get_display_items - out_html = mock_module.render(STUDENT_VIEW).content - self.assertTrue('This is supposed to be test html.' in out_html) - self.assertTrue('i4x://this/is/a/fake/id' in out_html) - - def test_gethtml_nochild(self): - """ - get_html, except the module has no child :( Should return a polite - error message. - """ - mock_module = CHModuleFactory.create() - - def fake_get_display_items(): - """ - Returns no children. - """ - return [] - mock_module.get_display_items = fake_get_display_items - out_html = mock_module.render(STUDENT_VIEW).content - self.assertTrue('Error in loading crowdsourced hinter' in out_html) - - @unittest.skip("Needs to be finished.") - def test_gethtml_multiple(self): - """ - Makes sure that multiple crowdsourced hinters play nice, when get_html - is called. - NOT WORKING RIGHT NOW - """ - mock_module = VerticalWithModulesFactory.create() - out_html = mock_module.render(STUDENT_VIEW).content - self.assertTrue('Test numerical problem.' in out_html) - self.assertTrue('Another test numerical problem.' in out_html) - - def test_numerical_answer_to_str(self): - """ - Tests the get request to string converter for numerical responses. - """ - mock_module = CHModuleFactory.create() - get = {'response1': '4'} - parsed = mock_module.numerical_answer_to_str(get) - self.assertTrue(parsed == '4') - - def test_formula_answer_to_str(self): - """ - Tests the get request to string converter for formula responses. - """ - mock_module = CHModuleFactory.create() - get = {'response1': 'x*y^2'} - parsed = mock_module.formula_answer_to_str(get) - self.assertTrue(parsed == 'x*y^2') - - def test_gethint_0hint(self): - """ - Someone asks for a hint, when there's no hint to give. - - Output should be blank. - - New entry should be added to previous_answers - """ - mock_module = CHModuleFactory.create() - json_in = {'problem_name': '26.0'} - out = mock_module.get_hint(json_in) - print mock_module.previous_answers - self.assertTrue(out is None) - self.assertTrue('26.0' in mock_module.user_submissions) - - def test_gethint_unparsable(self): - """ - Someone submits an answer that is in the wrong format. - - The answer should not be added to previous_answers. - """ - mock_module = CHModuleFactory.create() - old_answers = copy.deepcopy(mock_module.previous_answers) - json_in = 'blah' - out = mock_module.get_hint(json_in) - self.assertTrue(out is None) - self.assertTrue(mock_module.previous_answers == old_answers) - - def test_gethint_signature_error(self): - """ - Someone submits an answer that cannot be calculated as a float. - Nothing should change. - """ - mock_module = CHModuleFactory.create() - old_answers = copy.deepcopy(mock_module.previous_answers) - old_user_submissions = copy.deepcopy(mock_module.user_submissions) - json_in = {'problem1': 'fish'} - out = mock_module.get_hint(json_in) - self.assertTrue(out is None) - self.assertTrue(mock_module.previous_answers == old_answers) - self.assertTrue(mock_module.user_submissions == old_user_submissions) - - def test_gethint_1hint(self): - """ - Someone asks for a hint, with exactly one hint in the database. - Output should contain that hint. - """ - mock_module = CHModuleFactory.create() - json_in = {'problem_name': '25.0'} - out = mock_module.get_hint(json_in) - self.assertTrue('Really popular hint' in out['hints']) - # Also make sure that the input gets added to user_submissions, - # and that the hint is logged in previous_answers. - self.assertTrue('25.0' in mock_module.user_submissions) - self.assertTrue(['25.0', ['1']] in mock_module.previous_answers) - - def test_gethint_manyhints(self): - """ - Someone asks for a hint, with many matching hints in the database. - - The top-rated hint should be returned. - - Two other random hints should be returned. - Currently, the best hint could be returned twice - need to fix this - in implementation. - """ - mock_module = CHModuleFactory.create() - json_in = {'problem_name': '24.0'} - out = mock_module.get_hint(json_in) - self.assertTrue('Best hint' in out['hints']) - self.assertTrue(len(out['hints']) == 3) - - def test_getfeedback_0wronganswers(self): - """ - Someone has gotten the problem correct on the first try. - Output should be empty. - """ - mock_module = CHModuleFactory.create(previous_answers=[], user_submissions=[]) - json_in = {'problem_name': '42.5'} - out = mock_module.get_feedback(json_in) - print out - self.assertTrue(out is None) - - def test_getfeedback_1wronganswer_nohints(self): - """ - Someone has gotten the problem correct, with one previous wrong - answer. However, we don't actually have hints for this problem. - There should be a dialog to submit a new hint. - """ - mock_module = CHModuleFactory.create(previous_answers=[['26.0', [None, None, None]]]) - json_in = {'problem_name': '42.5'} - out = mock_module.get_feedback(json_in) - self.assertTrue(out['answer_to_hints'] == {'26.0': {}}) - - def test_getfeedback_1wronganswer_withhints(self): - """ - Same as above, except the user did see hints. There should be - a voting dialog, with the correct choices, plus a hint submission - dialog. - """ - mock_module = CHModuleFactory.create(previous_answers=[['24.0', [0, 3, None]]]) - json_in = {'problem_name': '42.5'} - out = mock_module.get_feedback(json_in) - self.assertTrue(len(out['answer_to_hints']['24.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. - """ - mock_module = CHModuleFactory.create( - previous_answers=[['24.0', [0, 100, None]]]) - json_in = {'problem_name': '42.5'} - out = mock_module.get_feedback(json_in) - self.assertTrue(len(out['answer_to_hints']['24.0']) == 1) - - def test_vote_nopermission(self): - """ - A user tries to vote for a hint, but he has already voted! - Should not change any vote tallies. - """ - mock_module = CHModuleFactory.create(user_voted=True) - json_in = {'answer': '24.0', 'hint': 1, 'pk_list': json.dumps([['24.0', 1], ['24.0', 3]])} - old_hints = copy.deepcopy(mock_module.hints) - mock_module.tally_vote(json_in) - self.assertTrue(mock_module.hints == old_hints) - - def test_vote_withpermission(self): - """ - A user votes for a hint. - Also tests vote result rendering. - """ - mock_module = CHModuleFactory.create( - previous_answers=[['24.0', [0, 3, None]]]) - json_in = {'answer': '24.0', 'hint': 3, 'pk_list': json.dumps([['24.0', 0], ['24.0', 3]])} - dict_out = mock_module.tally_vote(json_in) - self.assertTrue(mock_module.hints['24.0']['0'][1] == 40) - self.assertTrue(mock_module.hints['24.0']['3'][1] == 31) - self.assertTrue(['Best hint', 40] in dict_out['hint_and_votes']) - self.assertTrue(['Another hint', 31] in dict_out['hint_and_votes']) - - def test_vote_unparsable(self): - """ - A user somehow votes for an unparsable answer. - Should return a friendly error. - (This is an unusual exception path - I don't know how it occurs, - except if you manually make a post request. But, it seems to happen - occasionally.) - """ - mock_module = CHModuleFactory.create() - # None means that the answer couldn't be parsed. - mock_module.answer_signature = lambda text: None - json_in = {'answer': 'fish', 'hint': 3, 'pk_list': '[]'} - dict_out = mock_module.tally_vote(json_in) - print dict_out - self.assertTrue(dict_out == {'error': 'Failure in voting!'}) - - def test_vote_nohint(self): - """ - A user somehow votes for a hint that doesn't exist. - Should return a friendly error. - """ - mock_module = CHModuleFactory.create() - json_in = {'answer': '24.0', 'hint': '25', 'pk_list': '[]'} - dict_out = mock_module.tally_vote(json_in) - self.assertTrue(dict_out == {'error': 'Failure in voting!'}) - - def test_vote_badpklist(self): - """ - Some of the pk's specified in pk_list are invalid. - Should just skip those. - """ - mock_module = CHModuleFactory.create() - json_in = {'answer': '24.0', 'hint': '0', 'pk_list': json.dumps([['24.0', 0], ['24.0', 12]])} - hint_and_votes = mock_module.tally_vote(json_in)['hint_and_votes'] - self.assertTrue(['Best hint', 41] in hint_and_votes) - self.assertTrue(len(hint_and_votes) == 1) - - def test_submithint_nopermission(self): - """ - A user tries to submit a hint, but he has already voted. - """ - mock_module = CHModuleFactory.create(user_voted=True) - json_in = {'answer': '29.0', 'hint': 'This is a new hint.'} - print mock_module.user_voted - mock_module.submit_hint(json_in) - print mock_module.hints - self.assertTrue('29.0' not in mock_module.hints) - - def test_submithint_withpermission_new(self): - """ - A user submits a hint to an answer for which no hints - exist yet. - """ - mock_module = CHModuleFactory.create() - json_in = {'answer': '29.0', 'hint': 'This is a new hint.'} - mock_module.submit_hint(json_in) - self.assertTrue('29.0' in mock_module.hints) - - def test_submithint_withpermission_existing(self): - """ - A user submits a hint to an answer that has other hints - already. - """ - mock_module = CHModuleFactory.create(previous_answers=[['25.0', [1, None, None]]]) - json_in = {'answer': '25.0', 'hint': 'This is a new hint.'} - mock_module.submit_hint(json_in) - # Make a hint request. - json_in = {'problem name': '25.0'} - out = mock_module.get_hint(json_in) - self.assertTrue('This is a new hint.' in out['hints']) - - def test_submithint_moderate(self): - """ - A user submits a hint, but moderation is on. The hint should - show up in the mod_queue, not the public-facing hints - dict. - """ - mock_module = CHModuleFactory.create(moderate='True') - json_in = {'answer': '29.0', 'hint': 'This is a new hint.'} - mock_module.submit_hint(json_in) - self.assertTrue('29.0' not in mock_module.hints) - self.assertTrue('29.0' in mock_module.mod_queue) - - def test_submithint_escape(self): - """ - Make sure that hints are being html-escaped. - """ - mock_module = CHModuleFactory.create() - json_in = {'answer': '29.0', 'hint': ''} - mock_module.submit_hint(json_in) - self.assertTrue(mock_module.hints['29.0']['0'][0] == u'<script> alert("Trololo"); </script>') - - def test_submithint_unparsable(self): - mock_module = CHModuleFactory.create() - mock_module.answer_signature = lambda text: None - json_in = {'answer': 'fish', 'hint': 'A hint'} - dict_out = mock_module.submit_hint(json_in) - print dict_out - print mock_module.hints - self.assertTrue('error' in dict_out) - self.assertTrue(None not in mock_module.hints) - self.assertTrue('fish' not in mock_module.hints) - - def test_template_gethint(self): - """ - Test the templates for get_hint. - """ - mock_module = CHModuleFactory.create() - - def fake_get_hint(_): - """ - Creates a rendering dictionary, with which we can test - the templates. - """ - return {'best_hint': 'This is the best hint.', - 'rand_hint_1': 'A random hint', - 'rand_hint_2': 'Another random hint', - 'answer': '42.5'} - - mock_module.get_hint = fake_get_hint - json_in = {'problem_name': '42.5'} - out = json.loads(mock_module.handle_ajax('get_hint', json_in))['contents'] - self.assertIn('This is the best hint.', out) - self.assertIn('A random hint', out) - self.assertIn('Another random hint', out) - - def test_template_feedback(self): - """ - Test the templates for get_feedback. - NOT FINISHED - - from lxml import etree - mock_module = CHModuleFactory.create() - - def fake_get_feedback(get): - index_to_answer = {'0': '42.0', '1': '9000.01'} - index_to_hints = {'0': [('A hint for 42', 12), - ('Another hint for 42', 14)], - '1': [('A hint for 9000.01', 32)]} - return {'index_to_hints': index_to_hints, 'index_to_answer': index_to_answer} - - mock_module.get_feedback = fake_get_feedback - json_in = {'problem_name': '42.5'} - out = json.loads(mock_module.handle_ajax('get_feedback', json_in))['contents'] - html_tree = etree.XML(out) - # To be continued... - - """ - pass diff --git a/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py b/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py index e66b6ae5a2..7dc06e011d 100644 --- a/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py +++ b/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py @@ -35,7 +35,6 @@ from xmodule.discussion_module import DiscussionDescriptor from xmodule.html_module import HtmlDescriptor from xmodule.poll_module import PollDescriptor from xmodule.word_cloud_module import WordCloudDescriptor -from xmodule.crowdsource_hinter import CrowdsourceHinterDescriptor #from xmodule.video_module import VideoDescriptor from xmodule.seq_module import SequenceDescriptor from xmodule.conditional_module import ConditionalDescriptor @@ -66,7 +65,6 @@ LEAF_XMODULES = { CONTAINER_XMODULES = { ConditionalDescriptor: [{}], CourseDescriptor: [{}], - CrowdsourceHinterDescriptor: [{}], RandomizeDescriptor: [{}], SequenceDescriptor: [{}], VerticalBlock: [{}], @@ -75,8 +73,7 @@ CONTAINER_XMODULES = { # These modules are not editable in studio yet NOT_STUDIO_EDITABLE = ( - CrowdsourceHinterDescriptor, - PollDescriptor + PollDescriptor, ) diff --git a/common/templates/hinter_display.html b/common/templates/hinter_display.html deleted file mode 100644 index 12f6b9f2f4..0000000000 --- a/common/templates/hinter_display.html +++ /dev/null @@ -1,177 +0,0 @@ -## The hinter module passes in a field called ${op}, which determines which -## sub-function to render. - - -<%def name="get_hint()"> - % if len(hints) > 0: -

    Hints from students who made similar mistakes:

    -
      - % for hint in hints: -
    • ${hint}
    • - % endfor -
    - % endif - - -<%def name="get_feedback()"> - <% - def unspace(in_str): - """ - HTML id's can't have spaces in them. This little function - removes spaces. - """ - return ''.join(in_str.split()) - - # Make a list of all hints shown. (This is fed back to the site as pk_list.) - # At the same time, determine whether any hints were shown at all. - # If the user never saw hints, don't ask him to vote. - import json - hints_exist = False - pk_list = [] - for answer, pk_dict in answer_to_hints.items(): - if len(pk_dict) > 0: - hints_exist = True - for pk, hint_text in pk_dict.items(): - pk_list.append([answer, pk]) - json_pk_list = json.dumps(pk_list) - %> - - - -
    -
    -

    - Optional. Help us improve our hints! Which hint was most helpful to you? -

    - - - - % for answer, pk_dict in answer_to_hints.items(): - % for hint_pk, hint_text in pk_dict.items(): -

    - - ${hint_text} -

    - % endfor - % endfor - -

    - Don't like any of the hints above? - - Write your own! -

    -
    - -
    - % if hints_exist: -

    - Choose the incorrect answer for which you want to write a hint: -

    - % else: -

    - Optional. Help other students by submitting a hint! Pick one of your previous - answers for which you would like to write a hint: -

    - % endif - % for answer in user_submissions: - ${answer}
    - % endfor - % if hints_exist: -

    - Back -

    - % endif - -
    - -
    - -

    - Write a hint for other students who get the wrong answer of . -

    -

    Read about what makes a good hint.

    - - -

    - - -

    - Back -

    -
    - -
    - - - - -<%def name="show_votes()"> - % if hint_and_votes is UNDEFINED: - Sorry, but you've already voted! - % else: - Thank you for voting! -
    - % for hint, votes in hint_and_votes: - ${votes} votes. - ${hint} -
    - % endfor - % endif - - -<%def name="simple_message()"> - ${message} - - -% if op == "get_hint": - ${get_hint()} -% endif - -% if op == "get_feedback": - ${get_feedback()} -% endif - -% if op == "submit_hint": - ${simple_message()} -% endif - -% if op == "error": - ${error} -% endif - -% if op == "vote": - ${show_votes()} -% endif - diff --git a/lms/djangoapps/instructor/hint_manager.py b/lms/djangoapps/instructor/hint_manager.py deleted file mode 100644 index e42d02f6bc..0000000000 --- a/lms/djangoapps/instructor/hint_manager.py +++ /dev/null @@ -1,287 +0,0 @@ -""" -Views for hint management. - -Get to these views through courseurl/hint_manager. -For example: https://courses.edx.org/courses/MITx/2.01x/2013_Spring/hint_manager - -These views will only be visible if FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True -""" - -import json -import re - -from django.http import HttpResponse, Http404 -from django.views.decorators.csrf import ensure_csrf_cookie - -from edxmako.shortcuts import render_to_response, render_to_string - -from courseware.courses import get_course_with_access -from courseware.models import XModuleUserStateSummaryField -import courseware.module_render as module_render -import courseware.model_data as model_data -from xmodule.modulestore.django import modulestore -from opaque_keys.edx.locations import SlashSeparatedCourseKey -from xmodule.modulestore.exceptions import ItemNotFoundError - - -@ensure_csrf_cookie -def hint_manager(request, course_id): - """ - The URL landing function for all calls to the hint manager, both POST and GET. - """ - course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) - try: - course = get_course_with_access(request.user, 'staff', course_key, depth=None) - except Http404: - out = 'Sorry, but students are not allowed to access the hint manager!' - return HttpResponse(out) - if request.method == 'GET': - out = get_hints(request, course_key, 'mod_queue') - out.update({'error': ''}) - return render_to_response('instructor/hint_manager.html', out) - 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) - out = 'Error in hint manager - an invalid field was accessed.' - return HttpResponse(out) - - switch_dict = { - 'delete hints': delete_hints, - 'switch fields': lambda *args: None, # Takes any number of arguments, returns None. - 'change votes': change_votes, - 'add hint': add_hint, - 'approve': approve, - } - - # Do the operation requested, and collect any error messages. - error_text = switch_dict[request.POST['op']](request, course_key, field) - if error_text is None: - error_text = '' - render_dict = get_hints(request, course_key, field, course=course) - render_dict.update({'error': error_text}) - rendered_html = render_to_string('instructor/hint_manager_inner.html', render_dict) - return HttpResponse(json.dumps({'success': True, 'contents': rendered_html})) - - -def get_hints(request, course_id, field, course=None): # pylint: disable=unused-argument - """ - 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' - field_label = 'Hints Awaiting Moderation' - other_field_label = 'Approved Hints' - elif field == 'hints': - other_field = 'mod_queue' - field_label = 'Approved Hints' - other_field_label = 'Hints Awaiting Moderation' - # We want to use the course_id to find all matching usage_id's. - # To do this, just take the school/number part - leave off the classname. - # FIXME: we need to figure out how to do this with opaque keys - all_hints = XModuleUserStateSummaryField.objects.filter( - field_name=field, - usage_id__regex=re.escape(u'{0.org}/{0.course}'.format(course_id)), - ) - # big_out_dict[problem id] = [[answer, {pk: [hint, votes]}], sorted by answer] - # big_out_dict maps a problem id to a list of [answer, hints] pairs, sorted in order of answer. - big_out_dict = {} - # id_to name maps a problem id to the name of the problem. - # id_to_name[problem id] = Display name of problem - id_to_name = {} - - for hints_by_problem in all_hints: - hints_by_problem.usage_id = hints_by_problem.usage_id.map_into_course(course_id) - name = location_to_problem_name(course_id, hints_by_problem.usage_id) - if name is None: - continue - id_to_name[hints_by_problem.usage_id] = name - - 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 - 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 contains [answer, dict_of_hints] pairs. - answer_list = sorted(json.loads(hints_by_problem.value).items(), key=answer_sorter) - big_out_dict[hints_by_problem.usage_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': id_to_name} - return render_dict - - -def location_to_problem_name(course_id, 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_item(loc) - return descriptor.get_children()[0].display_name - except ItemNotFoundError: - # Sometimes, the problem is no longer in the course. Just - # don't include said problem. - return None - - -def delete_hints(request, course_id, field, course=None): # pylint: disable=unused-argument - """ - 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 - problem_id, answer, pk = request.POST.getlist(key) - problem_key = course_id.make_usage_key_from_deprecated_string(problem_id) - # Can be optimized - sort the delete list by problem_id, and load each problem - # from the database only once. - this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key) - problem_dict = json.loads(this_problem.value) - del problem_dict[answer][pk] - this_problem.value = json.dumps(problem_dict) - this_problem.save() - - -def change_votes(request, course_id, field, course=None): # pylint: disable=unused-argument - """ - Updates the number of votes. - - The numbered fields of `request.POST` contain [problem_id, answer, pk, new_votes] tuples. - See `delete_hints`. - - Example `request.POST`: - {'op': 'delete_hints', - 'field': 'mod_queue', - 1: ['problem_whatever', '42.0', '3', 42], - 2: ['problem_whatever', '32.5', '12', 9001]} - """ - - for key in request.POST: - if key == 'op' or key == 'field': - continue - problem_id, answer, pk, new_votes = request.POST.getlist(key) - problem_key = course_id.make_usage_key_from_deprecated_string(problem_id) - this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key) - problem_dict = json.loads(this_problem.value) - # problem_dict[answer][pk] points to a [hint_text, #votes] pair. - problem_dict[answer][pk][1] = int(new_votes) - this_problem.value = json.dumps(problem_dict) - this_problem.save() - - -def add_hint(request, course_id, field, course=None): - """ - 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'] - problem_key = course_id.make_usage_key_from_deprecated_string(problem_id) - answer = request.POST['answer'] - hint_text = request.POST['hint'] - - # Validate the answer. This requires initializing the xmodules, which - # is annoying. - try: - descriptor = modulestore().get_item(problem_key) - descriptors = [descriptor] - except ItemNotFoundError: - descriptors = [] - field_data_cache = model_data.FieldDataCache(descriptors, course_id, request.user) - hinter_module = module_render.get_module( - request.user, - request, - problem_key, - field_data_cache, - course_id, - course=course - ) - if not hinter_module.validate_answer(answer): - # Invalid answer. Don't add it to the database, or else the - # hinter will crash when we encounter it. - return 'Error - the answer you specified is not properly formatted: ' + str(answer) - - this_problem = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key) - - hint_pk_entry = XModuleUserStateSummaryField.objects.get(field_name='hint_pk', usage_id=problem_key) - this_pk = int(hint_pk_entry.value) - hint_pk_entry.value = this_pk + 1 - hint_pk_entry.save() - - problem_dict = json.loads(this_problem.value) - if answer not in problem_dict: - problem_dict[answer] = {} - problem_dict[answer][this_pk] = [hint_text, 1] - this_problem.value = json.dumps(problem_dict) - this_problem.save() - - -def approve(request, course_id, field, course=None): # pylint: disable=unused-argument - """ - Approve a list of hints, moving them from the mod_queue to the real - hint list. POST: - op, field - (some number) -> [problem, answer, pk] - - The numbered fields are analogous to those in `delete_hints` and `change_votes`. - """ - - for key in request.POST: - if key == 'op' or key == 'field': - continue - problem_id, answer, pk = request.POST.getlist(key) - problem_key = course_id.make_usage_key_from_deprecated_string(problem_id) - # Can be optimized - sort the delete list by problem_id, and load each problem - # from the database only once. - problem_in_mod = XModuleUserStateSummaryField.objects.get(field_name=field, usage_id=problem_key) - problem_dict = json.loads(problem_in_mod.value) - hint_to_move = problem_dict[answer][pk] - del problem_dict[answer][pk] - problem_in_mod.value = json.dumps(problem_dict) - problem_in_mod.save() - - problem_in_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_id=problem_key) - problem_dict = json.loads(problem_in_hints.value) - if answer not in problem_dict: - problem_dict[answer] = {} - problem_dict[answer][pk] = hint_to_move - problem_in_hints.value = json.dumps(problem_dict) - problem_in_hints.save() diff --git a/lms/djangoapps/instructor/tests/test_hint_manager.py b/lms/djangoapps/instructor/tests/test_hint_manager.py deleted file mode 100644 index 39e9dbc06c..0000000000 --- a/lms/djangoapps/instructor/tests/test_hint_manager.py +++ /dev/null @@ -1,206 +0,0 @@ -import json - -from django.test.client import Client, RequestFactory -from mock import patch, MagicMock -from nose.plugins.attrib import attr - -from courseware.models import XModuleUserStateSummaryField -from courseware.tests.factories import UserStateSummaryFactory -import instructor.hint_manager as view -from student.tests.factories import UserFactory -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory - -# pylint: disable=missing-docstring - - -@attr('shard_1') -class HintManagerTest(SharedModuleStoreTestCase): - @classmethod - def setUpClass(cls): - super(HintManagerTest, cls).setUpClass() - cls.course = CourseFactory.create(org='Me', number='19.002', display_name='test_course') - cls.url = '/courses/Me/19.002/test_course/hint_manager' - cls.course_id = cls.course.id - cls.problem_id = cls.course_id.make_usage_key('crowdsource_hinter', 'crowdsource_hinter_001') - - 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. - """ - super(HintManagerTest, self).setUp() - - 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') - UserStateSummaryFactory.create( - field_name='hints', - usage_id=self.problem_id, - value=json.dumps({ - '1.0': {'1': ['Hint 1', 2], '3': ['Hint 3', 12]}, - '2.0': {'4': ['Hint 4', 3]} - }) - ) - UserStateSummaryFactory.create( - field_name='mod_queue', - usage_id=self.problem_id, - value=json.dumps({'2.0': {'2': ['Hint 2', 1]}}) - ) - - UserStateSummaryFactory.create( - field_name='hint_pk', - usage_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 course_id, loc: "Test problem" - - def test_student_block(self): - """ - Makes sure that students cannot see the hint management view. - """ - c = Client() - UserFactory.create(username='student', email='student@edx.org', password='test') - c.login(username='student', password='test') - out = c.get(self.url) - 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. - """ - out = self.c.get(self.url) - print out - 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'}) - print out - self.assertTrue('an invalid field was accessed' in out.content) - - def test_switchfields(self): - """ - Checks that the op: 'switch fields' POST request works. - """ - out = self.c.post(self.url, {'op': 'switch fields', 'field': 'mod_queue'}) - print out - self.assertTrue('Hint 2' 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_gethints_other(self): - """ - Same as above, with hints instead of mod_queue - """ - request = RequestFactory() - post = request.post(self.url, {'field': 'hints'}) - out = view.get_hints(post, self.course_id, 'hints') - print out - self.assertTrue(out['other_field'] == 'mod_queue') - expected = {self.problem_id: [('1.0', {'1': ['Hint 1', 2], - '3': ['Hint 3', 12]}), - ('2.0', {'4': ['Hint 4', 3]}) - ]} - 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.to_deprecated_string(), '1.0', '1']}) - view.delete_hints(post, self.course_id, 'hints') - problem_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_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.to_deprecated_string(), '1.0', '1', 5]}) - view.change_votes(post, self.course_id, 'hints') - problem_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_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) - - def test_addhint(self): - """ - Check that instructors can add new hints. - """ - # Because add_hint accesses the xmodule, this test requires a bunch - # of monkey patching. - hinter = MagicMock() - hinter.validate_answer = lambda string: True - - request = RequestFactory() - post = request.post(self.url, {'field': 'mod_queue', - 'op': 'add hint', - 'problem': self.problem_id.to_deprecated_string(), - 'answer': '3.14', - 'hint': 'This is a new hint.'}) - post.user = 'fake user' - with patch('courseware.module_render.get_module', MagicMock(return_value=hinter)): - with patch('courseware.model_data.FieldDataCache', MagicMock(return_value=None)): - view.add_hint(post, self.course_id, 'mod_queue') - problem_hints = XModuleUserStateSummaryField.objects.get(field_name='mod_queue', usage_id=self.problem_id).value - self.assertTrue('3.14' in json.loads(problem_hints)) - - def test_addbadhint(self): - """ - Check that instructors cannot add hints with unparsable answers. - """ - # Patching. - hinter = MagicMock() - hinter.validate_answer = lambda string: False - - request = RequestFactory() - post = request.post(self.url, {'field': 'mod_queue', - 'op': 'add hint', - 'problem': self.problem_id.to_deprecated_string(), - 'answer': 'fish', - 'hint': 'This is a new hint.'}) - post.user = 'fake user' - with patch('courseware.module_render.get_module', MagicMock(return_value=hinter)): - with patch('courseware.model_data.FieldDataCache', MagicMock(return_value=None)): - view.add_hint(post, self.course_id, 'mod_queue') - problem_hints = XModuleUserStateSummaryField.objects.get(field_name='mod_queue', usage_id=self.problem_id).value - self.assertTrue('fish' not in json.loads(problem_hints)) - - def test_approve(self): - """ - Check that instructors can approve hints. (Move them - from the mod_queue to the hints.) - """ - request = RequestFactory() - post = request.post(self.url, {'field': 'mod_queue', - 'op': 'approve', - 1: [self.problem_id.to_deprecated_string(), '2.0', '2']}) - view.approve(post, self.course_id, 'mod_queue') - problem_hints = XModuleUserStateSummaryField.objects.get(field_name='mod_queue', usage_id=self.problem_id).value - self.assertTrue('2.0' not in json.loads(problem_hints) or len(json.loads(problem_hints)['2.0']) == 0) - problem_hints = XModuleUserStateSummaryField.objects.get(field_name='hints', usage_id=self.problem_id).value - self.assertTrue(json.loads(problem_hints)['2.0']['2'] == ['Hint 2', 1]) - self.assertTrue(len(json.loads(problem_hints)['2.0']) == 2) diff --git a/lms/envs/common.py b/lms/envs/common.py index c73eaae451..97e2d5aec3 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -184,9 +184,6 @@ FEATURES = { # Toggle to enable certificates of courses on dashboard 'ENABLE_VERIFIED_CERTIFICATES': False, - # Allow use of the hint managment instructor view. - 'ENABLE_HINTER_INSTRUCTOR_VIEW': False, - # for load testing 'AUTOMATIC_AUTH_FOR_TESTING': False, diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 024bb630ea..09560ba7de 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -24,7 +24,6 @@ FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True FEATURES['ENABLE_SERVICE_STATUS'] = True FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = True # Enable email for all Studio courses FEATURES['REQUIRE_COURSE_EMAIL_AUTH'] = False # Give all courses email (don't require django-admin perms) -FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True FEATURES['ENABLE_SHOPPING_CART'] = True FEATURES['AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'] = True FEATURES['ENABLE_S3_GRADE_DOWNLOADS'] = True diff --git a/lms/envs/test.py b/lms/envs/test.py index 83f55c337c..330034d39d 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -62,8 +62,6 @@ FEATURES['ENABLE_DISCUSSION_SERVICE'] = False FEATURES['ENABLE_SERVICE_STATUS'] = True -FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True - FEATURES['ENABLE_SHOPPING_CART'] = True FEATURES['ENABLE_VERIFIED_CERTIFICATES'] = True diff --git a/lms/templates/instructor/hint_manager.html b/lms/templates/instructor/hint_manager.html deleted file mode 100644 index bedfd4add6..0000000000 --- a/lms/templates/instructor/hint_manager.html +++ /dev/null @@ -1,126 +0,0 @@ -<%inherit file="/main.html" /> -<%namespace name='static' file='/static_content.html'/> -<%namespace name="content" file="/instructor/hint_manager_inner.html"/> - - -<%block name="headextra"> - <%static:css group='style-course-vendor'/> - <%static:css group='style-course'/> - - - - - - - - - - - -
    -
    - -
    - ${content.main()} -
    - -
    -
    diff --git a/lms/templates/instructor/hint_manager_inner.html b/lms/templates/instructor/hint_manager_inner.html deleted file mode 100644 index 45101be2f6..0000000000 --- a/lms/templates/instructor/hint_manager_inner.html +++ /dev/null @@ -1,47 +0,0 @@ -<%block name="main"> - - -

    ${field_label}

    -Switch to ${other_field_label} - -

    ${error}

    - -% for definition_id in all_hints: -

    Problem: ${id_to_name[definition_id]}

    - % for answer, hint_dict in all_hints[definition_id]: - % if len(hint_dict) > 0: -

    Answer: ${answer}

    - % endif - % for pk, hint in hint_dict.items(): -

    - ${hint[0]} -
    - Votes: -

    -

    - % endfor - % if len(hint_dict) > 0: -

    - % endif - % endfor - -

    Add a hint to this problem

    -

    Answer:

    - - (Be sure to format your answer in the same way as the other answers you see here.) -
    - Hint:
    - -
    - -
    -% endfor - -

    ${error}

    - - -% if field == 'mod_queue': - -% endif - - \ No newline at end of file diff --git a/lms/urls.py b/lms/urls.py index 078797bf3a..74f9eead03 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -870,13 +870,6 @@ urlpatterns += ( url(r'^debug/show_parameters$', 'debug.views.show_parameters'), ) -# Crowdsourced hinting instructor manager. -if settings.FEATURES.get('ENABLE_HINTER_INSTRUCTOR_VIEW'): - urlpatterns += ( - url(r'^courses/{}/hint_manager$'.format(settings.COURSE_ID_PATTERN), - 'instructor.hint_manager.hint_manager', name="hint_manager"), - ) - # enable automatic login if settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING'): urlpatterns += (