def grade(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')

    # Get weight
    weight = pl.get_integer_attrib(element, 'weight', 1)

    # Get true answer (if it does not exist, create no grade - leave it
    # up to the question code)
    a_tru = pl.from_json(data['correct_answers'].get(name, None))
    if a_tru is None:
        return
    # Wrap true answer in ndarray (if it already is one, this does nothing)
    a_tru = np.array(a_tru)
    # Throw an error if true answer is not a 2D numpy array
    if a_tru.ndim != 2:
        raise ValueError('true answer must be a 2D array')

    # Get submitted answer (if it does not exist, score is zero)
    a_sub = data['submitted_answers'].get(name, None)
    if a_sub is None:
        data['partial_scores'][name] = {'score': 0, 'weight': weight}
        return
    # If submitted answer is in a format generated by pl.to_json, convert it
    # back to a standard type (otherwise, do nothing)
    a_sub = pl.from_json(a_sub)
    # Wrap submitted answer in an ndarray (if it's already one, this does nothing)
    a_sub = np.array(a_sub)

    # If true and submitted answers have different shapes, score is zero
    if not (a_sub.shape == a_tru.shape):
        data['partial_scores'][name] = {'score': 0, 'weight': weight}
        return

    # Get method of comparison, with relabs as default
    comparison = pl.get_string_attrib(element, 'comparison', 'relabs')

    # Compare submitted answer with true answer
    if comparison == 'relabs':
        rtol = pl.get_float_attrib(element, 'rtol', 1e-2)
        atol = pl.get_float_attrib(element, 'atol', 1e-8)
        correct = pl.is_correct_ndarray2D_ra(a_sub, a_tru, rtol, atol)
    elif comparison == 'sigfig':
        digits = pl.get_integer_attrib(element, 'digits', 2)
        correct = pl.is_correct_ndarray2D_sf(a_sub, a_tru, digits)
    elif comparison == 'decdig':
        digits = pl.get_integer_attrib(element, 'digits', 2)
        correct = pl.is_correct_ndarray2D_dd(a_sub, a_tru, digits)
    else:
        raise ValueError('method of comparison "%s" is not valid' % comparison)

    if correct:
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    else:
        data['partial_scores'][name] = {'score': 0, 'weight': weight}
def test(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    weight = pl.get_integer_attrib(element, 'weight', 1)

    correct_key = data['correct_answers'].get(name, {'key': None}).get('key', None)
    if correct_key is None:
        raise Exception('could not determine correct_key')
    number_answers = len(data['params'][name])
    all_keys = [chr(ord('a') + i) for i in range(number_answers)]
    incorrect_keys = list(set(all_keys) - set([correct_key]))

    result = random.choices(['correct', 'incorrect', 'invalid'], [5, 5, 1])[0]
    if result == 'correct':
        data['raw_submitted_answers'][name] = data['correct_answers'][name]['key']
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    elif result == 'incorrect':
        if len(incorrect_keys) > 0:
            data['raw_submitted_answers'][name] = random.choice(incorrect_keys)
            data['partial_scores'][name] = {'score': 0, 'weight': weight}
        else:
            # actually an invalid submission
            data['raw_submitted_answers'][name] = '0'
            data['format_errors'][name] = 'INVALID choice'
    elif result == 'invalid':
        data['raw_submitted_answers'][name] = '0'
        data['format_errors'][name] = 'INVALID choice'

        # FIXME: add more invalid choices
    else:
        raise Exception('invalid result: %s' % result)
def grade(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')

    # Get weight
    weight = pl.get_integer_attrib(element, 'weight', 1)

    # Get true answer (if it does not exist, create no grade - leave it
    # up to the question code)
    a_tru = pl.from_json(data['correct_answers'].get(name, None))
    if a_tru is None:
        return

    # Get submitted answer (if it does not exist, score is zero)
    a_sub = data['submitted_answers'].get(name, None)
    if a_sub is None:
        data['partial_scores'][name] = {'score': 0, 'weight': weight}
        return
    # If submitted answer is in a format generated by pl.to_json, convert it
    # back to a standard type (otherwise, do nothing)
    a_sub = pl.from_json(a_sub)

    # Cast both submitted and true answers as integers.
    a_tru = int(a_tru)
    a_sub = int(a_sub)

    if a_tru == a_sub:
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    else:
        data['partial_scores'][name] = {'score': 0, 'weight': weight}
def test(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    weight = pl.get_integer_attrib(element, 'weight', 1)

    # Get correct answer
    a_tru = data['correct_answers'][name]

    # If correct answer is in a format generated by pl.to_json, convert it
    # back to a standard type (otherwise, do nothing)
    a_tru = pl.from_json(a_tru)

    result = random.choices(['correct', 'incorrect', 'invalid'], [5, 5, 1])[0]
    if result == 'correct':
        data['raw_submitted_answers'][name] = str(a_tru)
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    elif result == 'incorrect':
        data['raw_submitted_answers'][name] = str(a_tru + (random.randint(1, 11) * random.choice([-1, 1])))
        data['partial_scores'][name] = {'score': 0, 'weight': weight}
    elif result == 'invalid':
        # FIXME: add more invalid expressions, make text of format_errors
        # correct, and randomize
        if random.choice([True, False]):
            data['raw_submitted_answers'][name] = '1 + 2'
        else:
            data['raw_submitted_answers'][name] = '3.4'
        data['format_errors'][name] = 'invalid'
    else:
        raise Exception('invalid result: %s' % result)
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)

    # Get the number of digits to output
    digits = pl.get_integer_attrib(element, 'digits', 2)
    # Get the presentation type
    presentation_type = pl.get_string_attrib(element, 'presentation-type', 'f')

    var_name = pl.get_string_attrib(element, 'params-name')
    # Get value of variable, raising exception if variable does not exist
    var_data = data['params'].get(var_name, None)

    if var_data is None:
        raise Exception('No value in data["params"] for variable %s in pl-matrix-latex element' % var_name)

    # If the variable is in a format generated by pl.to_json, convert it
    # back to a standard type (otherwise, do nothing)
    var_data = pl.from_json(var_data)

    if not np.isscalar(var_data):
        var_data = np.array(var_data)
        # Check if numpy array type is numeric (integer, float or complex)
        if np.issubdtype(var_data.dtype, np.number):
            # Check shape of variable
            if var_data.ndim != 2:
                raise Exception('Value in data["params"] for variable %s in pl-matrix-latex element must be 2D array or scalar' % var_name)
        else:
            raise Exception('Value in data["params"] for variable %s in pl-matrix-latex element must be numeric' % var_name)

    # Create string for latex matrix format
    html = pl.latex_from_2darray(var_data, presentation_type=presentation_type, digits=digits)

    return html
def test(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    weight = pl.get_integer_attrib(element, 'weight', 1)
    allow_blank = pl.get_string_attrib(element, 'allow-blank', False)

    # Get correct answer
    a_tru = data['correct_answers'][name]

    # If correct answer is in a format generated by pl.to_json, convert it
    # back to a standard type (otherwise, do nothing)
    a_tru = pl.from_json(a_tru)

    if allow_blank:
        # no invalid answer implemented when allow-blank="true"
        result = random.choices(['correct', 'incorrect'], [5, 5])[0]
    else:
        result = random.choices(['correct', 'incorrect', 'invalid'], [5, 5, 1])[0]

    if result == 'correct':
        data['raw_submitted_answers'][name] = a_tru
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    elif result == 'incorrect':
        data['raw_submitted_answers'][name] = a_tru + str((random.randint(1, 11) * random.choice([-1, 1])))
        data['partial_scores'][name] = {'score': 0, 'weight': weight}
    elif result == 'invalid':
        data['raw_submitted_answers'][name] = ''
        data['format_errors'][name] = 'invalid'
    else:
        raise Exception('invalid result: %s' % result)
def test(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    weight = pl.get_integer_attrib(element, 'weight', 1)

    result = random.choices(['correct', 'incorrect', 'invalid'], [5, 5, 1])[0]
    if result == 'correct':
        data['raw_submitted_answers'][name] = str(pl.from_json(data['correct_answers'][name]))
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    elif result == 'incorrect':
        data['raw_submitted_answers'][name] = str(pl.from_json(data['correct_answers'][name])) + ' + {:d}'.format(random.randint(1, 100))
        data['partial_scores'][name] = {'score': 0, 'weight': weight}
    elif result == 'invalid':
        invalid_type = random.choice(['float', 'complex', 'expression', 'function', 'variable', 'syntax', 'escape', 'comment'])
        if invalid_type == 'float':
            data['raw_submitted_answers'][name] = 'x + 1.234'
            s = 'Your answer contains the floating-point number ' + str(1.234) + '. '
            s += 'All numbers must be expressed as integers (or ratios of integers). '
            s += '<br><br><pre>' + phs.point_to_error('x + 1.234', 4) + '</pre>'
            data['format_errors'][name] = s
        elif invalid_type == 'complex':
            data['raw_submitted_answers'][name] = 'x + (1+2j)'
            s = 'Your answer contains the complex number ' + str(2j) + '. '
            s += 'All numbers must be expressed as integers (or ratios of integers). '
            s += '<br><br><pre>' + phs.point_to_error('x + (1+2j)', 7) + '</pre>'
            data['format_errors'][name] = s
        elif invalid_type == 'expression':
            data['raw_submitted_answers'][name] = '1 and 0'
            s = 'Your answer has an invalid expression. '
            s += '<br><br><pre>' + phs.point_to_error('1 and 0', 0) + '</pre>'
            data['format_errors'][name] = s
        elif invalid_type == 'function':
            data['raw_submitted_answers'][name] = 'atan(x)'
            s = 'Your answer calls an invalid function "' + 'atan' + '". '
            s += '<br><br><pre>' + phs.point_to_error('atan(x)', 0) + '</pre>'
            data['format_errors'][name] = s
        elif invalid_type == 'variable':
            data['raw_submitted_answers'][name] = 'x + y'
            s = 'Your answer refers to an invalid variable "' + 'y' + '". '
            s += '<br><br><pre>' + phs.point_to_error('x + y', 4) + '</pre>'
            data['format_errors'][name] = s
        elif invalid_type == 'syntax':
            data['raw_submitted_answers'][name] = 'x +* 1'
            s = 'Your answer has a syntax error. '
            s += '<br><br><pre>' + phs.point_to_error('x +* 1', 4) + '</pre>'
            data['format_errors'][name] = s
        elif invalid_type == 'escape':
            data['raw_submitted_answers'][name] = 'x + 1\\n'
            s = 'Your answer must not contain the character "\\". '
            s += '<br><br><pre>' + phs.point_to_error('x + 1\\n', 5) + '</pre>'
            data['format_errors'][name] = s
        elif invalid_type == 'comment':
            data['raw_submitted_answers'][name] = 'x # some text'
            s = 'Your answer must not contain the character "#". '
            s += '<br><br><pre>' + phs.point_to_error('x # some text', 2) + '</pre>'
            data['format_errors'][name] = s
        else:
            raise Exception('invalid invalid_type: %s' % invalid_type)
    else:
        raise Exception('invalid result: %s' % result)
Beispiel #8
0
def grade(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    answer_name = pl.get_string_attrib(element, 'answer-name')

    # Check if this element is intended to produce a grade
    will_be_graded = pl.get_boolean_attrib(element, 'grade', True)
    if not will_be_graded:
        return

    # Get weight
    weight = pl.get_integer_attrib(element, 'weight', 1)

    # Get submitted answer (the "state")
    state = data['submitted_answers'].get(answer_name, None)
    if state is None:
        # This might happen. It means that, somehow, the hidden input element
        # did not get populated with the PLThreeJS state. The student is not at
        # fault, so we'll return nothing - don't grade.
        return

    # Get correct answer (if none, don't grade)
    a = data['correct_answers'].get(answer_name, None)
    if a is None:
        return

    # Get submitted position (as np.array([x, y, z]))
    p_sub = np.array(state['body_position'])

    # Get submitted orientation (as Quaternion - first, roll [x,y,z,w] to [w,x,y,z])
    q_sub = pyquaternion.Quaternion(np.roll(state['body_quaternion'], 1))

    # Get format of correct answer
    f = pl.get_string_attrib(element, 'answer-pose-format', 'rpy')

    # Get correct position (as np.array([x, y, z])) and orientation (as Quaternion)
    p_tru, q_tru = parse_correct_answer(f, a)

    # Find distance between submitted position and correct position
    error_in_translation = np.linalg.norm(p_sub - p_tru)

    # Find smallest angle of rotation between submitted orientation and correct orientation
    error_in_rotation = np.abs((q_tru.inverse * q_sub).degrees)

    # Get tolerances
    tol_translation = pl.get_float_attrib(element, 'tol-translation', 0.5)
    tol_rotation = pl.get_float_attrib(element, 'tol-rotation', 5)
    if (tol_translation <= 0):
        raise Exception('tol_translation must be a positive real number: {:g}'.format(tol_translation))
    if (tol_rotation <= 0):
        raise Exception('tol_rotation must be a positive real number (angle in degrees): {:g}'.format(tol_rotation))

    # Check if angle is no greater than tolerance
    if ((error_in_rotation <= tol_rotation) and (error_in_translation <= tol_translation)):
        data['partial_scores'][answer_name] = {'score': 1, 'weight': weight, 'feedback': {'error_in_rotation': error_in_rotation, 'error_in_translation': error_in_translation}}
    else:
        data['partial_scores'][answer_name] = {'score': 0, 'weight': weight, 'feedback': {'error_in_rotation': error_in_rotation, 'error_in_translation': error_in_translation}}
def test(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    weight = pl.get_integer_attrib(element, 'weight', 1)
    allow_partial_credit = pl.get_boolean_attrib(element, 'allow-partial-credit', False)

    # Get correct answer
    a_tru = data['correct_answers'][name]
    # If correct answer is in a format generated by pl.to_json, convert it
    # back to a standard type (otherwise, do nothing)
    a_tru = pl.from_json(a_tru)
    # Wrap true answer in ndarray (if it already is one, this does nothing)
    a_tru = np.array(a_tru)
    # Throw an error if true answer is not a 2D numpy array
    if a_tru.ndim != 2:
        raise ValueError('true answer must be a 2D array')
    else:
        m, n = np.shape(a_tru)

    result = random.choices(['correct', 'incorrect', 'incorrect'], [5, 5, 1])[0]

    number_of_correct = 0
    feedback = {}
    for i in range(m):
        for j in range(n):
            each_entry_name = name + str(n * i + j + 1)

            if result == 'correct':
                data['raw_submitted_answers'][each_entry_name] = str(a_tru[i, j])
                number_of_correct += 1
                feedback.update({each_entry_name: 'correct'})
            elif result == 'incorrect':
                data['raw_submitted_answers'][each_entry_name] = str(a_tru[i, j] + (random.uniform(1, 10) * random.choice([-1, 1])))
                feedback.update({each_entry_name: 'incorrect'})
            elif result == 'invalid':
                if random.choice([True, False]):
                    data['raw_submitted_answers'][each_entry_name] = '1,2'
                    data['format_errors'][each_entry_name] = '(Invalid format)'
                else:
                    data['raw_submitted_answers'][name] = ''
                    data['format_errors'][each_entry_name] = '(Invalid blank entry)'
            else:
                raise Exception('invalid result: %s' % result)

    if result == 'invalid':
        data['format_errors'][name] = 'At least one of the entries has invalid format (empty entries or not a double precision floating point number)'

    if number_of_correct == m * n:
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    else:
        if not allow_partial_credit:
            score_value = 0
        else:
            score_value = number_of_correct / (m * n)
        data['partial_scores'][name] = {'score': score_value, 'weight': weight, 'feedback': feedback}
def prepare(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    required_attribs = ['answers-name']
    optional_attribs = ['weight', 'correct-answer', 'label', 'suffix', 'display']
    pl.check_attribs(element, required_attribs, optional_attribs)
    name = pl.get_string_attrib(element, 'answers-name')

    correct_answer = pl.get_integer_attrib(element, 'correct-answer', None)
    if correct_answer is not None:
        if name in data['correct_answers']:
            raise Exception('duplicate correct_answers variable name: %s' % name)
        data['correct_answers'][name] = correct_answer
def grade(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    weight = pl.get_integer_attrib(element, 'weight', 1)

    submitted_key = data['submitted_answers'].get(name, None)
    correct_key = data['correct_answers'].get(name, {'key': None}).get('key', None)

    score = 0
    if (submitted_key is not None and submitted_key == correct_key):
        score = 1

    data['partial_scores'][name] = {'score': score, 'weight': weight}
def test(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    weight = pl.get_integer_attrib(element, 'weight', 1)

    # Get correct answer
    a_tru = data['correct_answers'][name]

    # If correct answer is in a format generated by pl.to_json, convert it
    # back to a standard type (otherwise, do nothing)
    a_tru = pl.from_json(a_tru)

    # Wrap true answer in ndarray (if it already is one, this does nothing)
    a_tru = np.array(a_tru)

    result = random.choices(['correct', 'incorrect', 'invalid'], [5, 5, 1])[0]
    if random.choice([True, False]):
        # matlab
        if result == 'correct':
            data['raw_submitted_answers'][name] = pl.numpy_to_matlab(a_tru, ndigits=12, wtype='g')
            data['partial_scores'][name] = {'score': 1, 'weight': weight}
        elif result == 'incorrect':
            data['raw_submitted_answers'][name] = pl.numpy_to_matlab(a_tru + (random.uniform(1, 10) * random.choice([-1, 1])), ndigits=12, wtype='g')
            data['partial_scores'][name] = {'score': 0, 'weight': weight}
        elif result == 'invalid':
            # FIXME: add more invalid expressions, make text of format_errors
            # correct, and randomize
            data['raw_submitted_answers'][name] = '[1, 2, 3]'
            data['format_errors'][name] = 'invalid'
        else:
            raise Exception('invalid result: %s' % result)
    else:
        # python
        if result == 'correct':
            data['raw_submitted_answers'][name] = str(np.array(a_tru).tolist())
            data['partial_scores'][name] = {'score': 1, 'weight': weight}
        elif result == 'incorrect':
            data['raw_submitted_answers'][name] = str((a_tru + (random.uniform(1, 10) * random.choice([-1, 1]))).tolist())
            data['partial_scores'][name] = {'score': 0, 'weight': weight}
        elif result == 'invalid':
            # FIXME: add more invalid expressions, make text of format_errors
            # correct, and randomize
            data['raw_submitted_answers'][name] = '[[1, 2, 3], [4, 5]]'
            data['format_errors'][name] = 'invalid'
        else:
            raise Exception('invalid result: %s' % result)
def grade(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')

    # Get weight
    weight = pl.get_integer_attrib(element, 'weight', 1)

    # Get remove-spaces option
    remove_spaces = pl.get_string_attrib(element, 'remove-spaces', False)

    # Get remove-leading-trailing option
    remove_leading_trailing = pl.get_string_attrib(element, 'remove-leading-trailing', False)

    # Get true answer (if it does not exist, create no grade - leave it
    # up to the question code)
    a_tru = pl.from_json(data['correct_answers'].get(name, None))
    if a_tru is None:
        return

    # Get submitted answer (if it does not exist, score is zero)
    a_sub = data['submitted_answers'].get(name, None)
    if a_sub is None:
        data['partial_scores'][name] = {'score': 0, 'weight': weight}
        return
    # If submitted answer is in a format generated by pl.to_json, convert it
    # back to a standard type (otherwise, do nothing)
    a_sub = pl.from_json(a_sub)

    # Remove the leading and trailing characters
    if (remove_leading_trailing):
        a_sub = a_sub.strip()

    # Remove the blank spaces between characters
    if (remove_spaces):
        a_sub = a_sub.replace(' ', '')

    if a_tru == a_sub:
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    else:
        data['partial_scores'][name] = {'score': 0, 'weight': weight}
def grade(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    variables = get_variables_list(pl.get_string_attrib(element, 'variables', None))
    allow_complex = pl.get_boolean_attrib(element, 'allow-complex', False)
    weight = pl.get_integer_attrib(element, 'weight', 1)

    # Get true answer (if it does not exist, create no grade - leave it
    # up to the question code)
    a_tru = data['correct_answers'].get(name, None)
    if a_tru is None:
        return

    # Get submitted answer (if it does not exist, score is zero)
    a_sub = data['submitted_answers'].get(name, None)
    if a_sub is None:
        data['partial_scores'][name] = {'score': 0, 'weight': weight}
        return

    # Parse true answer
    if isinstance(a_tru, str):
        # this is so instructors can specify the true answer simply as a string
        a_tru = phs.convert_string_to_sympy(a_tru, variables, allow_complex=allow_complex)
    else:
        a_tru = phs.json_to_sympy(a_tru, allow_complex=allow_complex)

    # Parse submitted answer
    if isinstance(a_sub, str):
        # this is for backward-compatibility
        a_sub = phs.convert_string_to_sympy(a_sub, variables, allow_complex=allow_complex)
    else:
        a_sub = phs.json_to_sympy(a_sub, allow_complex=allow_complex)

    # Check equality
    correct = a_tru.equals(a_sub)

    if correct:
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    else:
        data['partial_scores'][name] = {'score': 0, 'weight': weight}
Beispiel #15
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    label = pl.get_string_attrib(element, 'label', LABEL_DEFAULT)
    variables_string = pl.get_string_attrib(element, 'variables',
                                            VARIABLES_DEFAULT)
    variables = get_variables_list(variables_string)
    display = pl.get_string_attrib(element, 'display', DISPLAY_DEFAULT)
    allow_complex = pl.get_boolean_attrib(element, 'allow-complex',
                                          ALLOW_COMPLEX_DEFAULT)
    imaginary_unit = pl.get_string_attrib(element,
                                          'imaginary-unit-for-display',
                                          IMAGINARY_UNIT_FOR_DISPLAY_DEFAULT)
    size = pl.get_integer_attrib(element, 'size', SIZE_DEFAULT)

    operators = [
        'cos', 'sin', 'tan', 'exp', 'log', 'sqrt', '( )', '+', '-', '*', '/',
        '^', '**'
    ]
    constants = ['pi', 'e']

    if data['panel'] == 'question':
        editable = data['editable']
        raw_submitted_answer = data['raw_submitted_answers'].get(name, None)

        info_params = {
            'format': True,
            'variables': variables,
            'operators': operators,
            'constants': constants,
            'allow_complex': allow_complex,
        }
        with open('pl-symbolic-input.mustache', 'r', encoding='utf-8') as f:
            info = chevron.render(f, info_params).strip()
        with open('pl-symbolic-input.mustache', 'r', encoding='utf-8') as f:
            info_params.pop('format', None)
            info_params['shortformat'] = True
            shortinfo = chevron.render(f, info_params).strip()

        html_params = {
            'question':
            True,
            'name':
            name,
            'label':
            label,
            'editable':
            editable,
            'info':
            info,
            'shortinfo':
            shortinfo,
            'size':
            size,
            'show_info':
            pl.get_boolean_attrib(element, 'show-help-text',
                                  SHOW_HELP_TEXT_DEFAULT),
            'uuid':
            pl.get_uuid(),
            'allow_complex':
            allow_complex,
            'show_placeholder':
            size >= PLACEHOLDER_TEXT_THRESHOLD
        }

        partial_score = data['partial_scores'].get(name, {'score': None})
        score = partial_score.get('score', None)
        if score is not None:
            try:
                score = float(score)
                if score >= 1:
                    html_params['correct'] = True
                elif score > 0:
                    html_params['partial'] = math.floor(score * 100)
                else:
                    html_params['incorrect'] = True
            except Exception:
                raise ValueError('invalid score' + score)

        if display == 'inline':
            html_params['inline'] = True
        elif display == 'block':
            html_params['block'] = True
        else:
            raise ValueError(
                'method of display "%s" is not valid (must be "inline" or "block")'
                % display)
        if raw_submitted_answer is not None:
            html_params['raw_submitted_answer'] = escape(raw_submitted_answer)
        with open('pl-symbolic-input.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()

    elif data['panel'] == 'submission':
        parse_error = data['format_errors'].get(name, None)

        html_params = {
            'submission': True,
            'label': label,
            'parse_error': parse_error,
            'uuid': pl.get_uuid()
        }
        if parse_error is None and name in data['submitted_answers']:
            a_sub = data['submitted_answers'][name]
            if isinstance(a_sub, str):
                # this is for backward-compatibility
                a_sub = phs.convert_string_to_sympy(
                    a_sub, variables, allow_complex=allow_complex)
            else:
                a_sub = phs.json_to_sympy(a_sub, allow_complex=allow_complex)
            a_sub = a_sub.subs(sympy.I, sympy.Symbol(imaginary_unit))
            html_params['a_sub'] = sympy.latex(a_sub)
        elif name not in data['submitted_answers']:
            html_params['missing_input'] = True
            html_params['parse_error'] = None
        else:
            # Use the existing format text in the invalid popup.
            info_params = {
                'format': True,
                'variables': variables,
                'operators': operators,
                'constants': constants,
                'allow_complex': allow_complex,
            }
            with open('pl-symbolic-input.mustache', 'r',
                      encoding='utf-8') as f:
                info = chevron.render(f, info_params).strip()

            # Render invalid popup
            raw_submitted_answer = data['raw_submitted_answers'].get(
                name, None)
            with open('pl-symbolic-input.mustache', 'r',
                      encoding='utf-8') as f:
                parse_error += chevron.render(f, {
                    'format_error': True,
                    'format_string': info
                }).strip()

            html_params['parse_error'] = parse_error
            if raw_submitted_answer is not None:
                html_params['raw_submitted_answer'] = pl.escape_unicode_string(
                    raw_submitted_answer)

        partial_score = data['partial_scores'].get(name, {'score': None})
        score = partial_score.get('score', None)
        if score is not None:
            try:
                score = float(score)
                if score >= 1:
                    html_params['correct'] = True
                elif score > 0:
                    html_params['partial'] = math.floor(score * 100)
                else:
                    html_params['incorrect'] = True
            except Exception:
                raise ValueError('invalid score' + score)

        if display == 'inline':
            html_params['inline'] = True
        elif display == 'block':
            html_params['block'] = True
        else:
            raise ValueError(
                'method of display "%s" is not valid (must be "inline" or "block")'
                % display)

        html_params['error'] = html_params['parse_error'] or html_params.get(
            'missing_input', False)

        with open('pl-symbolic-input.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()

    elif data['panel'] == 'answer':
        a_tru = data['correct_answers'].get(name, None)
        if a_tru is not None:
            if isinstance(a_tru, str):
                # this is so instructors can specify the true answer simply as a string
                a_tru = phs.convert_string_to_sympy(
                    a_tru, variables, allow_complex=allow_complex)
            else:
                a_tru = phs.json_to_sympy(a_tru, allow_complex=allow_complex)
            a_tru = a_tru.subs(sympy.I, sympy.Symbol(imaginary_unit))
            html_params = {
                'answer': True,
                'label': label,
                'a_tru': sympy.latex(a_tru)
            }
            with open('pl-symbolic-input.mustache', 'r',
                      encoding='utf-8') as f:
                html = chevron.render(f, html_params).strip()
        else:
            html = ''

    else:
        raise Exception('Invalid panel type: %s' % data['panel'])

    return html
def prepare(element_html, element_index, data):
    element = lxml.html.fragment_fromstring(element_html)
    required_attribs = ['answers_name']
    optional_attribs = ['weight', 'number_answers', 'fixed_order', 'inline']
    pl.check_attribs(element, required_attribs, optional_attribs)
    name = element.get('answers_name')

    correct_answers = []
    incorrect_answers = []
    index = 0
    for child in element:
        if child.tag == 'pl_answer':
            pl.check_attribs(child, required_attribs=[], optional_attribs=['correct'])
            correct = pl.get_boolean_attrib(child, 'correct', False)
            child_html = pl.inner_html(child)
            answer_tuple = (index, correct, child_html)
            if correct:
                correct_answers.append(answer_tuple)
            else:
                incorrect_answers.append(answer_tuple)
            index += 1

    len_correct = len(correct_answers)
    len_incorrect = len(incorrect_answers)
    len_total = len_correct + len_incorrect

    if len_correct < 1:
        raise Exception('pl_multiple_choice element must have at least one correct answer')

    number_answers = pl.get_integer_attrib(element, 'number_answers', len_total)

    number_answers = max(1, min(1 + len_incorrect, number_answers))
    number_correct = 1
    number_incorrect = number_answers - number_correct
    if not (0 <= number_incorrect <= len_incorrect):
        raise Exception('INTERNAL ERROR: number_incorrect: (%d, %d, %d)' % (number_incorrect, len_incorrect, number_answers))

    sampled_correct = random.sample(correct_answers, number_correct)
    sampled_incorrect = random.sample(incorrect_answers, number_incorrect)

    sampled_answers = sampled_correct + sampled_incorrect
    random.shuffle(sampled_answers)

    fixed_order = pl.get_boolean_attrib(element, 'fixed_order', False)
    if fixed_order:
        # we can't simply skip the shuffle because we already broke the original
        # order by separating into correct/incorrect lists
        sampled_answers.sort(key=lambda a: a[0])  # sort by stored original index

    display_answers = []
    correct_answer = None
    for (i, (index, correct, html)) in enumerate(sampled_answers):
        keyed_answer = {'key': chr(ord('a') + i), 'html': html}
        display_answers.append(keyed_answer)
        if correct:
            correct_answer = keyed_answer

    if name in data['params']:
        raise Exception('duplicate params variable name: %s' % name)
    if name in data['correct_answers']:
        raise Exception('duplicate correct_answers variable name: %s' % name)
    data['params'][name] = display_answers
    data['correct_answers'][name] = correct_answer
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    label = pl.get_string_attrib(element, 'label', None)

    if '_pl_matrix_input_format' in data['submitted_answers']:
        format_type = data['submitted_answers']['_pl_matrix_input_format'].get(name, 'matlab')
    else:
        format_type = 'matlab'

    if data['panel'] == 'question':
        editable = data['editable']
        raw_submitted_answer = data['raw_submitted_answers'].get(name, None)

        # Get comparison parameters and info strings
        comparison = pl.get_string_attrib(element, 'comparison', 'relabs')
        if comparison == 'relabs':
            rtol = pl.get_float_attrib(element, 'rtol', 1e-2)
            atol = pl.get_float_attrib(element, 'atol', 1e-8)
            if (rtol < 0):
                raise ValueError('Attribute rtol = {:g} must be non-negative'.format(rtol))
            if (atol < 0):
                raise ValueError('Attribute atol = {:g} must be non-negative'.format(atol))
            info_params = {'format': True, 'relabs': True, 'rtol': '{:g}'.format(rtol), 'atol': '{:g}'.format(atol)}
        elif comparison == 'sigfig':
            digits = pl.get_integer_attrib(element, 'digits', 2)
            if (digits < 0):
                raise ValueError('Attribute digits = {:d} must be non-negative'.format(digits))
            info_params = {'format': True, 'sigfig': True, 'digits': '{:d}'.format(digits), 'comparison_eps': 0.51 * (10**-(digits - 1))}
        elif comparison == 'decdig':
            digits = pl.get_integer_attrib(element, 'digits', 2)
            if (digits < 0):
                raise ValueError('Attribute digits = {:d} must be non-negative'.format(digits))
            info_params = {'format': True, 'decdig': True, 'digits': '{:d}'.format(digits), 'comparison_eps': 0.51 * (10**-(digits - 0))}
        else:
            raise ValueError('method of comparison "%s" is not valid (must be "relabs", "sigfig", or "decdig")' % comparison)
        info_params['allow_complex'] = pl.get_boolean_attrib(element, 'allow-complex', False)
        with open('pl-matrix-input.mustache', 'r', encoding='utf-8') as f:
            info = chevron.render(f, info_params).strip()
        with open('pl-matrix-input.mustache', 'r', encoding='utf-8') as f:
            info_params.pop('format', None)
            info_params['shortformat'] = True
            shortinfo = chevron.render(f, info_params).strip()

        html_params = {
            'question': True,
            'name': name,
            'label': label,
            'editable': editable,
            'info': info,
            'shortinfo': shortinfo,
            'uuid': pl.get_uuid()
        }

        partial_score = data['partial_scores'].get(name, {'score': None})
        score = partial_score.get('score', None)
        if score is not None:
            try:
                score = float(score)
                if score >= 1:
                    html_params['correct'] = True
                elif score > 0:
                    html_params['partial'] = math.floor(score * 100)
                else:
                    html_params['incorrect'] = True
            except Exception:
                raise ValueError('invalid score' + score)

        if raw_submitted_answer is not None:
            html_params['raw_submitted_answer'] = escape(raw_submitted_answer)
        with open('pl-matrix-input.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()

    elif data['panel'] == 'submission':
        parse_error = data['format_errors'].get(name, None)
        html_params = {
            'submission': True,
            'label': label,
            'parse_error': parse_error,
            'uuid': pl.get_uuid()
        }
        if parse_error is None:
            # Get submitted answer, raising an exception if it does not exist
            a_sub = data['submitted_answers'].get(name, None)
            if a_sub is None:
                raise Exception('submitted answer is None')

            # If answer is in a format generated by pl.to_json, convert it
            # back to a standard type (otherwise, do nothing)
            a_sub = pl.from_json(a_sub)

            # Wrap answer in an ndarray (if it's already one, this does nothing)
            a_sub = np.array(a_sub)

            # Format answer as a string
            html_params['a_sub'] = pl.string_from_2darray(a_sub, language=format_type, digits=12, presentation_type='g')
        else:
            raw_submitted_answer = data['raw_submitted_answers'].get(name, None)
            if raw_submitted_answer is not None:
                html_params['raw_submitted_answer'] = escape(raw_submitted_answer)

        partial_score = data['partial_scores'].get(name, {'score': None})
        score = partial_score.get('score', None)
        if score is not None:
            try:
                score = float(score)
                if score >= 1:
                    html_params['correct'] = True
                elif score > 0:
                    html_params['partial'] = math.floor(score * 100)
                else:
                    html_params['incorrect'] = True
            except Exception:
                raise ValueError('invalid score' + score)

        with open('pl-matrix-input.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()

    elif data['panel'] == 'answer':
        # Get true answer - do nothing if it does not exist
        a_tru = pl.from_json(data['correct_answers'].get(name, None))
        if a_tru is not None:
            a_tru = np.array(a_tru)

            # Get comparison parameters
            comparison = pl.get_string_attrib(element, 'comparison', 'relabs')
            if comparison == 'relabs':
                rtol = pl.get_float_attrib(element, 'rtol', 1e-2)
                atol = pl.get_float_attrib(element, 'atol', 1e-8)
                # FIXME: render correctly with respect to rtol and atol
                matlab_data = pl.string_from_2darray(a_tru, language='matlab', digits=12, presentation_type='g')
                python_data = pl.string_from_2darray(a_tru, language='python', digits=12, presentation_type='g')
            elif comparison == 'sigfig':
                digits = pl.get_integer_attrib(element, 'digits', 2)
                matlab_data = pl.string_from_2darray(a_tru, language='matlab', digits=digits, presentation_type='sigfig')
                python_data = pl.string_from_2darray(a_tru, language='python', digits=digits, presentation_type='sigfig')
            elif comparison == 'decdig':
                digits = pl.get_integer_attrib(element, 'digits', 2)
                matlab_data = pl.string_from_2darray(a_tru, language='matlab', digits=digits, presentation_type='f')
                python_data = pl.string_from_2darray(a_tru, language='python', digits=digits, presentation_type='f')
            else:
                raise ValueError('method of comparison "%s" is not valid (must be "relabs", "sigfig", or "decdig")' % comparison)

            html_params = {
                'answer': True,
                'label': label,
                'matlab_data': matlab_data,
                'python_data': python_data,
                'uuid': pl.get_uuid()
            }

            if format_type == 'matlab':
                html_params['default_is_matlab'] = True
            else:
                html_params['default_is_python'] = True
            with open('pl-matrix-input.mustache', 'r', encoding='utf-8') as f:
                html = chevron.render(f, html_params).strip()
        else:
            html = ''

    else:
        raise Exception('Invalid panel type: %s' % data['panel'])

    return html
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    digits = pl.get_integer_attrib(element, 'digits', DIGITS_DEFAULT)
    show_matlab = pl.get_boolean_attrib(element, 'show-matlab', SHOW_MATLAB_DEFAULT)
    show_mathematica = pl.get_boolean_attrib(element, 'show-mathematica', SHOW_MATHEMATICA_DEFAULT)
    show_python = pl.get_boolean_attrib(element, 'show-python', SHOW_PYTHON_DEFAULT)
    show_r = pl.get_boolean_attrib(element, 'show-r', SHOW_R_DEFAULT)
    default_tab = pl.get_string_attrib(element, 'default-tab', DEFAULT_TAB_DEFAULT)

    tab_list = ['matlab', 'mathematica', 'python', 'r']
    if default_tab not in tab_list:
        raise Exception(f'invalid default-tab: {default_tab}')

    # Setting the default tab
    displayed_tab = [show_matlab, show_mathematica, show_python, show_r]
    if not any(displayed_tab):
        raise Exception('All tabs have been hidden from display. At least one tab must be shown.')

    default_tab_index = tab_list.index(default_tab)
    # If not displayed, make first visible tab the default
    if not displayed_tab[default_tab_index]:
        first_display = displayed_tab.index(True)
        default_tab = tab_list[first_display]
    default_tab_index = tab_list.index(default_tab)

    # Active tab should be the default tab
    default_tab_list = [False, False, False, False]
    default_tab_list[default_tab_index] = True
    [active_tab_matlab, active_tab_mathematica, active_tab_python, active_tab_r] = default_tab_list

    # Process parameter data
    matlab_data = ''
    mathematica_data = ''
    python_data = 'import numpy as np\n\n'
    r_data = ''
    for child in element:
        if child.tag == 'variable':
            # Raise exception if variable does not have a name
            pl.check_attribs(child, required_attribs=['params-name'], optional_attribs=['comment', 'digits'])

            # Get name of variable
            var_name = pl.get_string_attrib(child, 'params-name')

            # Get value of variable, raising exception if variable does not exist
            var_data = data['params'].get(var_name, None)
            if var_data is None:
                raise Exception('No value in data["params"] for variable %s in pl-variable-output element' % var_name)

            # If the variable is in a format generated by pl.to_json, convert it
            # back to a standard type (otherwise, do nothing)
            var_data = pl.from_json(var_data)

            # Get comment, if it exists
            var_matlab_comment = ''
            var_mathematica_comment = ''
            var_python_comment = ''
            var_r_comment = ''
            if pl.has_attrib(child, 'comment'):
                var_comment = pl.get_string_attrib(child, 'comment')
                var_matlab_comment = f' % {var_comment}'
                var_mathematica_comment = f' (* {var_comment} *)'
                var_python_comment = f' # {var_comment}'
                var_r_comment = f' # {var_comment}'

            # Get digit for child, if it exists
            if not pl.has_attrib(child, 'digits'):
                var_digits = digits
            else:
                var_digits = pl.get_string_attrib(child, 'digits')

            # Assembling Python array formatting
            if np.isscalar(var_data):
                prefix = ''
                suffix = ''
            else:
                # Wrap the variable in an ndarray (if it's already one, this does nothing)
                var_data = np.array(var_data)
                # Check shape of variable
                if var_data.ndim > 2:
                    raise Exception('Value in data["params"] for variable %s in pl-variable-output element must be a scalar, a vector, or a 2D array' % var_name)
                # Create prefix/suffix so python string is np.array( ... )
                prefix = 'np.array('
                suffix = ')'

            # Mathematica reserved letters: C D E I K N O
            mathematica_reserved = ['C', 'D', 'E', 'I', 'K', 'N', 'O']
            if pl.inner_html(child) in mathematica_reserved:
                mathematica_suffix = 'm'
            else:
                mathematica_suffix = ''

            # Create string for matlab and python format
            var_name_disp = pl.inner_html(child)
            var_matlab_data = pl.string_from_numpy(var_data, language='matlab', digits=var_digits)
            var_mathematica = pl.string_from_numpy(var_data, language='mathematica', digits=var_digits)
            var_python_data = pl.string_from_numpy(var_data, language='python', digits=var_digits)
            var_r_data = pl.string_from_numpy(var_data, language='r', digits=var_digits)

            matlab_data += f'{var_name_disp} = {var_matlab_data};{var_matlab_comment}\n'
            mathematica_data += f'{var_name_disp}{mathematica_suffix} = {var_mathematica};{var_mathematica_comment}\n'
            python_data += f'{var_name_disp} = {prefix}{var_python_data}{suffix}{var_python_comment}\n'
            r_data += f'{var_name_disp} = {var_r_data}{var_r_comment}\n'

    html_params = {
        'active_tab_matlab': active_tab_matlab,
        'active_tab_mathematica': active_tab_mathematica,
        'active_tab_python': active_tab_python,
        'active_tab_r': active_tab_r,
        'show_matlab': show_matlab,
        'show_mathematica': show_mathematica,
        'show_python': show_python,
        'show_r': show_r,
        'matlab_data': matlab_data,
        'mathematica_data': mathematica_data,
        'python_data': python_data,
        'r_data': r_data,
        'uuid': pl.get_uuid()
    }

    with open('pl-variable-output.mustache', 'r', encoding='utf-8') as f:
        html = chevron.render(f, html_params).strip()

    return html
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    # get the name of the element, in this case, the name of the array
    name = pl.get_string_attrib(element, 'answers-name')
    label = pl.get_string_attrib(element, 'label', None)
    allow_partial_credit = pl.get_boolean_attrib(element, 'allow-partial-credit', False)
    allow_feedback = pl.get_boolean_attrib(element, 'allow-feedback', allow_partial_credit)

    if data['panel'] == 'question':
        editable = data['editable']

        # Get true answer
        a_tru = pl.from_json(data['correct_answers'].get(name, None))
        if a_tru is None:
            raise Exception('No value in data["correct_answers"] for variable %s in pl-matrix-component-input element' % name)
        else:
            if np.isscalar(a_tru):
                raise Exception('Value in data["correct_answers"] for variable %s in pl-matrix-component-input element cannot be a scalar.' % name)
            else:
                a_tru = np.array(a_tru)

        if a_tru.ndim != 2:
            raise Exception('Value in data["correct_answers"] for variable %s in pl-matrix-component-input element must be a 2D array.' % name)
        else:
            m, n = np.shape(a_tru)

        input_array = createTableForHTMLDisplay(m, n, name, label, data, 'input')

        # Get comparison parameters and info strings
        comparison = pl.get_string_attrib(element, 'comparison', 'relabs')
        if comparison == 'relabs':
            rtol = pl.get_float_attrib(element, 'rtol', 1e-2)
            atol = pl.get_float_attrib(element, 'atol', 1e-8)
            if (rtol < 0):
                raise ValueError('Attribute rtol = {:g} must be non-negative'.format(rtol))
            if (atol < 0):
                raise ValueError('Attribute atol = {:g} must be non-negative'.format(atol))
            info_params = {'format': True, 'relabs': True, 'rtol': '{:g}'.format(rtol), 'atol': '{:g}'.format(atol)}
        elif comparison == 'sigfig':
            digits = pl.get_integer_attrib(element, 'digits', 2)
            if (digits < 0):
                raise ValueError('Attribute digits = {:d} must be non-negative'.format(digits))
            info_params = {'format': True, 'sigfig': True, 'digits': '{:d}'.format(digits), 'comparison_eps': 0.51 * (10**-(digits - 1))}
        elif comparison == 'decdig':
            digits = pl.get_integer_attrib(element, 'digits', 2)
            if (digits < 0):
                raise ValueError('Attribute digits = {:d} must be non-negative'.format(digits))
            info_params = {'format': True, 'decdig': True, 'digits': '{:d}'.format(digits), 'comparison_eps': 0.51 * (10**-(digits - 0))}
        else:
            raise ValueError('method of comparison "%s" is not valid (must be "relabs", "sigfig", or "decdig")' % comparison)

        with open('pl-matrix-component-input.mustache', 'r', encoding='utf-8') as f:
            info = chevron.render(f, info_params).strip()
        with open('pl-matrix-component-input.mustache', 'r', encoding='utf-8') as f:
            info_params.pop('format', None)
            info_params['shortformat'] = True
            shortinfo = chevron.render(f, info_params).strip()

        html_params = {
            'question': True,
            'name': name,
            'label': label,
            'editable': editable,
            'info': info,
            'shortinfo': shortinfo,
            'input_array': input_array,
            'inline': True,
            'uuid': pl.get_uuid()
        }

        partial_score = data['partial_scores'].get(name, {'score': None})
        score = partial_score.get('score', None)
        if score is not None:
            try:
                score = float(score)
                if score >= 1:
                    html_params['correct'] = True
                elif score > 0:
                    html_params['partial'] = math.floor(score * 100)
                else:
                    html_params['incorrect'] = True
            except Exception:
                raise ValueError('invalid score' + score)

        with open('pl-matrix-component-input.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()

    elif data['panel'] == 'submission':

        parse_error = data['format_errors'].get(name, None)
        html_params = {
            'submission': True,
            'label': label,
            'parse_error': parse_error,
            'uuid': pl.get_uuid()
        }

        a_tru = pl.from_json(data['correct_answers'].get(name, None))
        m, n = np.shape(a_tru)

        partial_score = data['partial_scores'].get(name, {'score': None})
        score = partial_score.get('score', None)
        if score is not None:
            try:
                score = float(score)
                if score >= 1:
                    html_params['correct'] = True
                elif score > 0:
                    html_params['partial'] = math.floor(score * 100)
                else:
                    html_params['incorrect'] = True
            except Exception:
                raise ValueError('invalid score' + score)

        if parse_error is None:
            # Get submitted answer, raising an exception if it does not exist
            a_sub = data['submitted_answers'].get(name, None)
            if a_sub is None:
                raise Exception('submitted answer is None')
            # If answer is in a format generated by pl.to_json, convert it back to a standard type (otherwise, do nothing)
            a_sub = pl.from_json(a_sub)
            # Wrap answer in an ndarray (if it's already one, this does nothing)
            a_sub = np.array(a_sub)
            # Format submitted answer as a latex string
            sub_latex = '$' + pl.latex_from_2darray(a_sub, presentation_type='g', digits=12) + '$'
            # When allowing feedback, display submitted answers using html table
            sub_html_table = createTableForHTMLDisplay(m, n, name, label, data, 'output-feedback')
            if allow_feedback and score is not None:
                if score < 1:
                    html_params['a_sub_feedback'] = sub_html_table
                else:
                    html_params['a_sub'] = sub_latex
            else:
                html_params['a_sub'] = sub_latex
        else:
            # create html table to show submitted answer when there is an invalid format
            html_params['raw_submitted_answer'] = createTableForHTMLDisplay(m, n, name, label, data, 'output-invalid')

        with open('pl-matrix-component-input.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()

    elif data['panel'] == 'answer':

        # Get true answer - do nothing if it does not exist
        a_tru = pl.from_json(data['correct_answers'].get(name, None))
        if a_tru is not None:
            a_tru = np.array(a_tru)

            # Get comparison parameters and create the display data
            comparison = pl.get_string_attrib(element, 'comparison', 'relabs')
            if comparison == 'relabs':
                rtol = pl.get_float_attrib(element, 'rtol', 1e-2)
                atol = pl.get_float_attrib(element, 'atol', 1e-8)
                # FIXME: render correctly with respect to rtol and atol
                latex_data = '$' + pl.latex_from_2darray(a_tru, presentation_type='g', digits=12) + '$'
            elif comparison == 'sigfig':
                digits = pl.get_integer_attrib(element, 'digits', 2)
                latex_data = '$' + pl.latex_from_2darray(a_tru, presentation_type='sigfig', digits=digits) + '$'
            elif comparison == 'decdig':
                digits = pl.get_integer_attrib(element, 'digits', 2)
                latex_data = '$' + pl.latex_from_2darray(a_tru, presentation_type='f', digits=digits) + '$'
            else:
                raise ValueError('method of comparison "%s" is not valid (must be "relabs", "sigfig", or "decdig")' % comparison)

            html_params = {
                'answer': True,
                'label': label,
                'latex_data': latex_data,
                'uuid': pl.get_uuid()
            }

            with open('pl-matrix-component-input.mustache', 'r', encoding='utf-8') as f:
                html = chevron.render(f, html_params).strip()
        else:
            html = ''

    else:
        raise Exception('Invalid panel type: %s' % data['panel'])

    return html
def grade(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    allow_partial_credit = pl.get_boolean_attrib(element, 'allow-partial-credit', False)

    # Get weight
    weight = pl.get_integer_attrib(element, 'weight', 1)

    # Get method of comparison, with relabs as default
    comparison = pl.get_string_attrib(element, 'comparison', 'relabs')
    if comparison == 'relabs':
        rtol = pl.get_float_attrib(element, 'rtol', 1e-2)
        atol = pl.get_float_attrib(element, 'atol', 1e-8)
    elif comparison == 'sigfig':
        digits = pl.get_integer_attrib(element, 'digits', 2)
    elif comparison == 'decdig':
        digits = pl.get_integer_attrib(element, 'digits', 2)
    else:
        raise ValueError('method of comparison "%s" is not valid' % comparison)

    # Get true answer (if it does not exist, create no grade - leave it
    # up to the question code)
    a_tru = pl.from_json(data['correct_answers'].get(name, None))
    if a_tru is None:
        return
    # Wrap true answer in ndarray (if it already is one, this does nothing)
    a_tru = np.array(a_tru)
    # Throw an error if true answer is not a 2D numpy array
    if a_tru.ndim != 2:
        raise ValueError('true answer must be a 2D array')
    else:
        m, n = np.shape(a_tru)

    number_of_correct = 0
    feedback = {}
    for i in range(m):
        for j in range(n):

            each_entry_name = name + str(n * i + j + 1)
            a_sub = data['submitted_answers'].get(each_entry_name, None)
            # Get submitted answer (if it does not exist, score is zero)
            if a_sub is None:
                data['partial_scores'][name] = {'score': 0, 'weight': weight}
                return
            # If submitted answer is in a format generated by pl.to_json, convert it
            # back to a standard type (otherwise, do nothing)
            a_sub = pl.from_json(a_sub)

            # Compare submitted answer with true answer
            if comparison == 'relabs':
                correct = pl.is_correct_scalar_ra(a_sub, a_tru[i, j], rtol, atol)
            elif comparison == 'sigfig':
                correct = pl.is_correct_scalar_sf(a_sub, a_tru[i, j], digits)
            elif comparison == 'decdig':
                correct = pl.is_correct_scalar_dd(a_sub, a_tru[i, j], digits)

            if correct:
                number_of_correct += 1
                feedback.update({each_entry_name: 'correct'})
            else:
                feedback.update({each_entry_name: 'incorrect'})

    if number_of_correct == m * n:
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    else:
        if not allow_partial_credit:
            score_value = 0
        else:
            score_value = number_of_correct / (m * n)
        data['partial_scores'][name] = {'score': score_value, 'weight': weight, 'feedback': feedback}
Beispiel #21
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    partial_credit = pl.get_boolean_attrib(element, 'partial-credit',
                                           PARTIAL_CREDIT_DEFAULT)
    partial_credit_method = pl.get_string_attrib(
        element, 'partial-credit-method', PARTIAL_CREDIT_METHOD_DEFAULT)

    editable = data['editable']
    # answer feedback is not displayed when partial credit is True
    # (unless the question is disabled)
    show_answer_feedback = True
    if partial_credit and editable:
        show_answer_feedback = False

    display_answers = data['params'].get(name, [])
    inline = pl.get_boolean_attrib(element, 'inline', INLINE_DEFAULT)
    submitted_keys = data['submitted_answers'].get(name, [])

    # if there is only one key then it is passed as a string,
    # not as a length-one list, so we fix that next
    if isinstance(submitted_keys, str):
        submitted_keys = [submitted_keys]

    correct_answer_list = data['correct_answers'].get(name, [])
    correct_keys = [answer['key'] for answer in correct_answer_list]

    if data['panel'] == 'question':
        partial_score = data['partial_scores'].get(name, {'score': None})
        score = partial_score.get('score', None)

        answerset = []
        for answer in display_answers:
            answer_html = {
                'key':
                answer['key'],
                'checked': (answer['key'] in submitted_keys),
                'html':
                answer['html'].strip(),
                'display_score_badge':
                score is not None and show_answer_feedback
                and answer['key'] in submitted_keys
            }
            if answer_html['display_score_badge']:
                answer_html['correct'] = (answer['key'] in correct_keys)
                answer_html['incorrect'] = (answer['key'] not in correct_keys)
            answerset.append(answer_html)

        info_params = {'format': True}
        # Adds decorative help text per bootstrap formatting guidelines:
        # http://getbootstrap.com/docs/4.0/components/forms/#help-text
        # Determine whether we should add a choice selection requirement
        hide_help_text = pl.get_boolean_attrib(element, 'hide-help-text',
                                               HIDE_HELP_TEXT_DEFAULT)
        if not hide_help_text:
            # Should we reveal the depth of the choice?
            detailed_help_text = pl.get_boolean_attrib(
                element, 'detailed-help-text', DETAILED_HELP_TEXT_DEFAULT)
            min_correct = pl.get_integer_attrib(element, 'min-correct', 1)
            max_correct = pl.get_integer_attrib(element, 'max-correct',
                                                len(correct_answer_list))
            if detailed_help_text:
                if min_correct != max_correct:
                    insert_text = ' between <b>%d</b> and <b>%d</b> options.' % (
                        min_correct, max_correct)
                    helptext = '<small class="form-text text-muted">Select ' + insert_text + '</small>'
                else:
                    insert_text = ' exactly <b>%d</b> options.' % (max_correct)
                    helptext = '<small class="form-text text-muted">Select' + insert_text + '</small>'
            else:
                insert_text = ' at least one option.'
                helptext = '<small class="form-text text-muted">Select all possible options that apply.</small>'

            if partial_credit:
                if partial_credit_method == 'PC':
                    gradingtext = 'You must select ' + insert_text + ' You will receive a score of <code>100% * (t - f) / n</code>, ' \
                        + 'where <code>t</code> is the number of true options that you select, <code>f</code> ' \
                        + 'is the number of false options that you select, and <code>n</code> is the total number of true options. ' \
                        + 'At minimum, you will receive a score of 0%.'
                else:
                    gradingtext = 'You must select ' + insert_text + ' You will receive a score of <code>100% * (t + f) / ' + str(len(display_answers)) + '</code>, ' \
                        + 'where <code>t</code> is the number of true options that you select and <code>f</code> ' \
                        + 'is the number of false options that you do not select.'
            else:
                gradingtext = 'You must select' + insert_text + ' You will receive a score of 100% ' \
                    + 'if you select all options that are true and no options that are false. ' \
                    + 'Otherwise, you will receive a score of 0%.'

            info_params.update({'gradingtext': gradingtext})

        with open('pl-checkbox.mustache', 'r', encoding='utf-8') as f:
            info = chevron.render(f, info_params).strip()

        html_params = {
            'question': True,
            'name': name,
            'editable': editable,
            'uuid': pl.get_uuid(),
            'info': info,
            'answers': answerset,
            'inline': inline
        }

        if not hide_help_text:
            html_params['helptext'] = helptext

        if score is not None:
            try:
                score = float(score)
                if score >= 1:
                    html_params['correct'] = True
                elif score > 0:
                    html_params['partial'] = math.floor(score * 100)
                else:
                    html_params['incorrect'] = True
            except Exception:
                raise ValueError('invalid score' + score)

        with open('pl-checkbox.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()

    elif data['panel'] == 'submission':
        parse_error = data['format_errors'].get(name, None)
        if parse_error is None:
            partial_score = data['partial_scores'].get(name, {'score': None})
            score = partial_score.get('score', None)

            answers = []
            for submitted_key in submitted_keys:
                submitted_answer = next(
                    filter(lambda a: a['key'] == submitted_key,
                           display_answers), None)
                answer_item = {
                    'key':
                    submitted_key,
                    'html':
                    submitted_answer['html'],
                    'display_score_badge':
                    score is not None and show_answer_feedback
                }
                if answer_item['display_score_badge']:
                    answer_item['correct'] = (submitted_key in correct_keys)
                    answer_item['incorrect'] = (submitted_key
                                                not in correct_keys)
                answers.append(answer_item)

            html_params = {
                'submission': True,
                'display_score_badge': (score is not None),
                'answers': answers,
                'inline': inline
            }

            if html_params['display_score_badge']:
                try:
                    score = float(score)
                    if score >= 1:
                        html_params['correct'] = True
                    elif score > 0:
                        html_params['partial'] = math.floor(score * 100)
                    else:
                        html_params['incorrect'] = True
                except Exception:
                    raise ValueError('invalid score' + score)

            with open('pl-checkbox.mustache', 'r', encoding='utf-8') as f:
                html = chevron.render(f, html_params).strip()
        else:
            html_params = {
                'submission': True,
                'uuid': pl.get_uuid(),
                'parse_error': parse_error,
                'inline': inline,
            }
            with open('pl-checkbox.mustache', 'r', encoding='utf-8') as f:
                html = chevron.render(f, html_params).strip()

    elif data['panel'] == 'answer':

        if not pl.get_boolean_attrib(element, 'hide-answer-panel',
                                     HIDE_ANSWER_PANEL_DEFAULT):
            correct_answer_list = data['correct_answers'].get(name, [])
            if len(correct_answer_list) == 0:
                raise ValueError('At least one option must be true.')
            else:
                html_params = {
                    'answer': True,
                    'inline': inline,
                    'answers': correct_answer_list
                }
                with open('pl-checkbox.mustache', 'r', encoding='utf-8') as f:
                    html = chevron.render(f, html_params).strip()
        else:
            html = ''

    else:
        raise ValueError('Invalid panel type: %s' % data['panel'])

    return html
Beispiel #22
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    digits = pl.get_integer_attrib(element, 'digits', 2)

    matlab_data = ''
    python_data = 'import numpy as np\n\n'
    for child in element:
        if child.tag == 'variable':
            # Raise exception of variable does not have a name
            pl.check_attribs(child,
                             required_attribs=['params-name'],
                             optional_attribs=[])

            # Get name of variable
            var_name = pl.get_string_attrib(child, 'params-name')

            # Get value of variable, raising exception if variable does not exist
            var_data = data['params'].get(var_name, None)
            if var_data is None:
                raise Exception(
                    'No value in data["params"] for variable %s in pl-matrix-output element'
                    % var_name)

            # If the variable is in a format generated by pl.to_json, convert it
            # back to a standard type (otherwise, do nothing)
            var_data = pl.from_json(var_data)

            if np.isscalar(var_data):
                prefix = ''
                suffix = ''
            else:
                # Wrap the variable in an ndarray (if it's already one, this does nothing)
                var_data = np.array(var_data)
                # Check shape of variable
                if var_data.ndim != 2:
                    raise Exception(
                        'Value in data["params"] for variable %s in pl-matrix-output element must be a scalar or a 2D array'
                        % var_name)
                # Create prefix/suffix so python string is np.array( ... )
                prefix = 'np.array('
                suffix = ')'

            # Create string for matlab and python format
            matlab_data += pl.inner_html(
                child) + ' = ' + pl.string_from_2darray(
                    var_data, language='matlab', digits=digits) + ';\n'
            python_data += pl.inner_html(
                child) + ' = ' + prefix + pl.string_from_2darray(
                    var_data, language='python', digits=digits) + suffix + '\n'

    html_params = {
        'default_is_matlab': True,
        'matlab_data': matlab_data,
        'python_data': python_data,
        'uuid': pl.get_uuid()
    }

    with open('pl-matrix-output.mustache', 'r', encoding='utf-8') as f:
        html = chevron.render(f, html_params).strip()

    return html
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    digits = pl.get_integer_attrib(element, 'digits', 2)
    show_matlab = pl.get_boolean_attrib(element, 'show-matlab', True)
    show_mathematica = pl.get_boolean_attrib(element, 'show-mathematica', True)
    show_python = pl.get_boolean_attrib(element, 'show-python', True)
    default_tab = pl.get_string_attrib(element, 'default-tab', 'matlab')

    tab_list = ['matlab', 'mathematica', 'python']
    if default_tab not in tab_list:
        raise Exception(f'invalid default-tab: {default_tab}')

    # Setting the default tab
    displayed_tab = [show_matlab, show_mathematica, show_python]
    if not any(displayed_tab):
        raise Exception('All tabs have been hidden from display. At least one tab must be shown.')

    default_tab_index = tab_list.index(default_tab)
    # If not displayed, make first visible tab the default
    if not displayed_tab[default_tab_index]:
        first_display = displayed_tab.index(True)
        default_tab = tab_list[first_display]
    default_tab_index = tab_list.index(default_tab)

    # Active tab should be the default tab
    default_tab_list = [False, False, False]
    default_tab_list[default_tab_index] = True
    [active_tab_matlab, active_tab_mathematica, active_tab_python] = default_tab_list

    # Process parameter data
    matlab_data = ''
    mathematica_data = ''
    python_data = 'import numpy as np\n\n'
    for child in element:
        if child.tag == 'variable':
            # Raise exception if variable does not have a name
            pl.check_attribs(child, required_attribs=['params-name'], optional_attribs=['comment', 'digits'])

            # Get name of variable
            var_name = pl.get_string_attrib(child, 'params-name')

            # Get value of variable, raising exception if variable does not exist
            var_data = data['params'].get(var_name, None)
            if var_data is None:
                raise Exception('No value in data["params"] for variable %s in pl-variable-output element' % var_name)

            # If the variable is in a format generated by pl.to_json, convert it
            # back to a standard type (otherwise, do nothing)
            var_data = pl.from_json(var_data)

            # Get comment, if it exists
            var_matlab_comment = ''
            var_mathematica_comment = ''
            var_python_comment = ''
            if pl.has_attrib(child, 'comment'):
                var_comment = pl.get_string_attrib(child, 'comment')
                var_matlab_comment = f' % {var_comment}'
                var_mathematica_comment = f' (* {var_comment} *)'
                var_python_comment = f' # {var_comment}'

            # Get digit for child, if it exists
            if not pl.has_attrib(child, 'digits'):
                var_digits = digits
            else:
                var_digits = pl.get_string_attrib(child, 'digits')

            # Assembling Python array formatting
            if np.isscalar(var_data):
                prefix = ''
                suffix = ''
            else:
                # Wrap the variable in an ndarray (if it's already one, this does nothing)
                var_data = np.array(var_data)
                # Check shape of variable
                if var_data.ndim > 2:
                    raise Exception('Value in data["params"] for variable %s in pl-variable-output element must be a scalar, a vector, or a 2D array' % var_name)
                # Create prefix/suffix so python string is np.array( ... )
                prefix = 'np.array('
                suffix = ')'

            # Mathematica reserved letters: C D E I K N O
            mathematica_reserved = ['C', 'D', 'E', 'I', 'K', 'N', 'O']
            if pl.inner_html(child) in mathematica_reserved:
                mathematica_suffix = 'm'
            else:
                mathematica_suffix = ''

            # Create string for matlab and python format
            var_name_disp = pl.inner_html(child)
            var_matlab_data = pl.string_from_numpy(var_data, language='matlab', digits=var_digits)
            var_mathematica = pl.string_from_numpy(var_data, language='mathematica', digits=var_digits)
            var_python_data = pl.string_from_numpy(var_data, language='python', digits=var_digits)

            matlab_data += f'{var_name_disp} = {var_matlab_data};{var_matlab_comment}\n'
            mathematica_data += f'{var_name_disp}{mathematica_suffix} = {var_mathematica};{var_mathematica_comment}\n'
            python_data += f'{var_name_disp} = {prefix}{var_python_data}{suffix}{var_python_comment}\n'

    html_params = {
        'active_tab_matlab': active_tab_matlab,
        'active_tab_mathematica': active_tab_mathematica,
        'active_tab_python': active_tab_python,
        'show_matlab': show_matlab,
        'show_mathematica': show_mathematica,
        'show_python': show_python,
        'matlab_data': matlab_data,
        'mathematica_data': mathematica_data,
        'python_data': python_data,
        'uuid': pl.get_uuid()
    }

    with open('pl-variable-output.mustache', 'r', encoding='utf-8') as f:
        html = chevron.render(f, html_params).strip()

    return html
def prepare(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    required_attribs = ['answers-name']
    optional_attribs = ['weight', 'number-answers', 'fixed-order', 'inline']
    pl.check_attribs(element, required_attribs, optional_attribs)
    name = pl.get_string_attrib(element, 'answers-name')

    correct_answers = []
    incorrect_answers = []
    index = 0
    for child in element:
        if child.tag in ['pl-answer', 'pl_answer']:
            pl.check_attribs(child, required_attribs=[], optional_attribs=['correct'])
            correct = pl.get_boolean_attrib(child, 'correct', False)
            child_html = pl.inner_html(child)
            answer_tuple = (index, correct, child_html)
            if correct:
                correct_answers.append(answer_tuple)
            else:
                incorrect_answers.append(answer_tuple)
            index += 1

    len_correct = len(correct_answers)
    len_incorrect = len(incorrect_answers)
    len_total = len_correct + len_incorrect

    if len_correct < 1:
        raise Exception('pl-multiple-choice element must have at least one correct answer')

    number_answers = pl.get_integer_attrib(element, 'number-answers', len_total)

    number_answers = max(1, min(1 + len_incorrect, number_answers))
    number_correct = 1
    number_incorrect = number_answers - number_correct
    if not (0 <= number_incorrect <= len_incorrect):
        raise Exception('INTERNAL ERROR: number_incorrect: (%d, %d, %d)' % (number_incorrect, len_incorrect, number_answers))

    sampled_correct = random.sample(correct_answers, number_correct)
    sampled_incorrect = random.sample(incorrect_answers, number_incorrect)

    sampled_answers = sampled_correct + sampled_incorrect
    random.shuffle(sampled_answers)

    fixed_order = pl.get_boolean_attrib(element, 'fixed-order', False)
    if fixed_order:
        # we can't simply skip the shuffle because we already broke the original
        # order by separating into correct/incorrect lists
        sampled_answers.sort(key=lambda a: a[0])  # sort by stored original index

    display_answers = []
    correct_answer = None
    for (i, (index, correct, html)) in enumerate(sampled_answers):
        keyed_answer = {'key': chr(ord('a') + i), 'html': html}
        display_answers.append(keyed_answer)
        if correct:
            correct_answer = keyed_answer

    if name in data['params']:
        raise Exception('duplicate params variable name: %s' % name)
    if name in data['correct_answers']:
        raise Exception('duplicate correct_answers variable name: %s' % name)
    data['params'][name] = display_answers
    data['correct_answers'][name] = correct_answer
def test(element_html, element_index, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers_name')
    weight = pl.get_integer_attrib(element, 'weight', 1)

    result = random.choices(['correct', 'incorrect', 'invalid'], [5, 5, 1])[0]
    if result == 'correct':
        data['raw_submitted_answers'][name] = str(pl.from_json(data['correct_answers'][name]))
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    elif result == 'incorrect':
        data['raw_submitted_answers'][name] = str(pl.from_json(data['correct_answers'][name])) + ' + {:d}'.format(random.randint(1, 100))
        data['partial_scores'][name] = {'score': 0, 'weight': weight}
    elif result == 'invalid':
        invalid_type = random.choice(['float', 'complex', 'expression', 'function', 'variable', 'syntax', 'escape', 'comment'])
        if invalid_type == 'float':
            data['raw_submitted_answers'][name] = 'x + 1.234'
            s = 'Your answer contains the floating-point number ' + str(1.234) + '. '
            s += 'All numbers must be expressed as integers (or ratios of integers). '
            s += '<br><br><pre>' + phs.point_to_error('x + 1.234', 4) + '</pre>'
            data['format_errors'][name] = s
        elif invalid_type == 'complex':
            data['raw_submitted_answers'][name] = 'x + (1+2j)'
            s = 'Your answer contains the complex number ' + str(2j) + '. '
            s += 'All numbers must be expressed as integers (or ratios of integers). '
            s += '<br><br><pre>' + phs.point_to_error('x + (1+2j)', 7) + '</pre>'
            data['format_errors'][name] = s
        elif invalid_type == 'expression':
            data['raw_submitted_answers'][name] = '1 and 0'
            s = 'Your answer has an invalid expression. '
            s += '<br><br><pre>' + phs.point_to_error('1 and 0', 0) + '</pre>'
            data['format_errors'][name] = s
        elif invalid_type == 'function':
            data['raw_submitted_answers'][name] = 'atan(x)'
            s = 'Your answer calls an invalid function "' + 'atan' + '". '
            s += '<br><br><pre>' + phs.point_to_error('atan(x)', 0) + '</pre>'
            data['format_errors'][name] = s
        elif invalid_type == 'variable':
            data['raw_submitted_answers'][name] = 'x + y'
            s = 'Your answer refers to an invalid variable "' + 'y' + '". '
            s += '<br><br><pre>' + phs.point_to_error('x + y', 4) + '</pre>'
            data['format_errors'][name] = s
        elif invalid_type == 'syntax':
            data['raw_submitted_answers'][name] = 'x +* 1'
            s = 'Your answer has a syntax error. '
            s += '<br><br><pre>' + phs.point_to_error('x +* 1', 4) + '</pre>'
            data['format_errors'][name] = s
        elif invalid_type == 'escape':
            data['raw_submitted_answers'][name] = 'x + 1\\n'
            s = 'Your answer must not contain the character "\\". '
            s += '<br><br><pre>' + phs.point_to_error('x + 1\\n', 5) + '</pre>'
            data['format_errors'][name] = s
        elif invalid_type == 'comment':
            data['raw_submitted_answers'][name] = 'x # some text'
            s = 'Your answer must not contain the character "#". '
            s += '<br><br><pre>' + phs.point_to_error('x # some text', 2) + '</pre>'
            data['format_errors'][name] = s
        else:
            raise Exception('invalid invalid_type: %s' % invalid_type)
    else:
        raise Exception('invalid result: %s' % result)

    return data
Beispiel #26
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    answer_name = pl.get_string_attrib(element, 'answers-name')

    if data['panel'] == 'question':
        mcq_options = []
        student_previous_submission = []
        submission_indent = []
        student_submission_dict_list = []

        answer_name = pl.get_string_attrib(element, 'answers-name')
        source_header = pl.get_string_attrib(element, 'source-header', SOURCE_HEADER_DEFAULT)
        solution_header = pl.get_string_attrib(element, 'solution-header', SOLUTION_HEADER_DEFAULT)
        grading_method = pl.get_string_attrib(element, 'grading-method', GRADING_METHOD_DEFAULT)

        mcq_options = data['params'][answer_name]
        mcq_options = filter_multiple_from_array(mcq_options, ['inner_html', 'uuid'])

        if answer_name in data['submitted_answers']:
            student_previous_submission = filter_multiple_from_array(data['submitted_answers'][answer_name], ['inner_html', 'uuid', 'indent'])
            mcq_options = [opt for opt in mcq_options if opt not in filter_multiple_from_array(student_previous_submission, ['inner_html', 'uuid'])]

        for index, option in enumerate(student_previous_submission):
            submission_indent = option.get('indent', None)
            if submission_indent is not None:
                submission_indent = (int(submission_indent) * TAB_SIZE_PX) + INDENT_OFFSET
            temp = {'inner_html': option['inner_html'], 'indent': submission_indent, 'uuid': option['uuid']}
            student_submission_dict_list.append(dict(temp))

        dropzone_layout = pl.get_string_attrib(element, 'solution-placement', SOLUTION_PLACEMENT_DEFAULT)
        check_indentation = pl.get_boolean_attrib(element, 'indentation', INDENTION_DEFAULT)
        max_indent = pl.get_integer_attrib(element, 'max-indent', MAX_INDENTION_DEFAULT)
        inline_layout = pl.get_boolean_attrib(element, 'inline', INLINE_DEFAULT)

        help_text = 'Drag answer tiles into the answer area to the ' + dropzone_layout + '. '

        if grading_method == 'unordered':
            help_text += '<br>Your answer ordering does not matter. '
        elif grading_method != 'external':
            help_text += '<br>The ordering of your answer matters and is graded.'
        else:
            help_text += '<br>Your answer will be autograded; be sure to indent and order your answer properly.'

        if check_indentation:
            help_text += '<br><b>Your answer should be indented. </b> Indent your tiles by dragging them horizontally in the answer area.'

        uuid = pl.get_uuid()
        html_params = {
            'question': True,
            'answer_name': answer_name,
            'options': mcq_options,
            'source-header': source_header,
            'solution-header': solution_header,
            'submission_dict': student_submission_dict_list,
            'dropzone_layout': 'pl-order-blocks-bottom' if dropzone_layout == 'bottom' else 'pl-order-blocks-right',
            'check_indentation': 'true' if check_indentation else 'false',
            'help_text': help_text,
            'inline': 'inline' if inline_layout is True else None,
            'max_indent': max_indent,
            'uuid': uuid
        }

        with open('pl-order-blocks.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params)
        return html

    elif data['panel'] == 'submission':
        if pl.get_string_attrib(element, 'grading-method', 'ordered') == 'external':
            return ''  # external grader is responsible for displaying results screen

        student_submission = ''
        score = None
        feedback = None
        if answer_name in data['submitted_answers']:
            student_submission = [{
                'inner_html': attempt['inner_html'],
                'indent': ((attempt['indent'] or 0) * TAB_SIZE_PX) + INDENT_OFFSET
            } for attempt in data['submitted_answers'][answer_name]]

        if answer_name in data['partial_scores']:
            score = data['partial_scores'][answer_name]['score']
            feedback = data['partial_scores'][answer_name]['feedback']

        html_params = {
            'submission': True,
            'parse-error': data['format_errors'].get(answer_name, None),
            'student_submission': student_submission,
            'feedback': feedback
        }

        if score is not None:
            try:
                score = float(score * 100)
                if score >= 100:
                    html_params['correct'] = True
                elif score > 0:
                    html_params['partially_correct'] = math.floor(score)
                else:
                    html_params['incorrect'] = True
            except Exception:
                raise ValueError('invalid score: ' + data['partial_scores'][answer_name]['score'])

        with open('pl-order-blocks.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params)
        return html

    elif data['panel'] == 'answer':
        if pl.get_string_attrib(element, 'grading-method', 'ordered') == 'external':
            try:
                base_path = data['options']['question_path']
                file_lead_path = os.path.join(base_path, 'tests/ans.py')
                with open(file_lead_path, 'r') as file:
                    solution_file = file.read()
                return f'<pl-code language="python">{solution_file}</pl-code>'
            except FileNotFoundError:
                return 'The reference solution is not provided for this question.'

        grading_mode = pl.get_string_attrib(element, 'grading-method', 'ordered')
        if grading_mode == 'unordered':
            grading_mode = 'in any order'
        elif grading_mode == 'dag' or grading_mode == 'ranking':
            grading_mode = 'one possible correct order'
        else:
            grading_mode = 'in the specified order'
        check_indentation = pl.get_boolean_attrib(element, 'indentation', INDENTION_DEFAULT)
        indentation_message = ', with correct indentation' if check_indentation is True else None

        if answer_name in data['correct_answers']:
            question_solution = [{
                'inner_html': solution['inner_html'],
                'indent': ((solution['indent'] or 0) * TAB_SIZE_PX) + INDENT_OFFSET
            } for solution in data['correct_answers'][answer_name]]

            html_params = {
                'true_answer': True,
                'question_solution': question_solution,
                'grading_mode': grading_mode,
                'indentation_message': indentation_message
            }
            with open('pl-order-blocks.mustache', 'r', encoding='utf-8') as f:
                html = chevron.render(f, html_params)
            return html
        else:
            return ''
    else:
        raise Exception('Invalid panel type')
Beispiel #27
0
def grade(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    answer_name = pl.get_string_attrib(element, 'answers-name')

    student_answer = data['submitted_answers'][answer_name]
    grading_mode = pl.get_string_attrib(element, 'grading-method', GRADING_METHOD_DEFAULT)
    check_indentation = pl.get_boolean_attrib(element, 'indentation', INDENTION_DEFAULT)
    feedback_type = pl.get_string_attrib(element, 'feedback', FEEDBACK_DEFAULT)
    answer_weight = pl.get_integer_attrib(element, 'weight', WEIGHT_DEFAULT)
    partial_credit_type = pl.get_string_attrib(element, 'partial-credit', 'lcs')

    true_answer_list = data['correct_answers'][answer_name]

    final_score = 0
    feedback = ''
    first_wrong = -1

    if len(student_answer) == 0:
        data['format_errors'][answer_name] = 'Your submitted answer was empty.'
        return

    if check_indentation:
        indentations = {ans['uuid']: ans['indent'] for ans in true_answer_list}
        for ans in student_answer:
            if ans['indent'] != indentations.get(ans['uuid']):
                if 'tag' in ans:
                    ans['tag'] = None
                else:
                    ans['inner_html'] = None

    if grading_mode == 'unordered':
        true_answer_list = filter_multiple_from_array(true_answer_list, ['uuid', 'indent', 'inner_html'])
        correct_selections = [opt for opt in student_answer if opt in true_answer_list]
        incorrect_selections = [opt for opt in student_answer if opt not in true_answer_list]
        final_score = float((len(correct_selections) - len(incorrect_selections)) / len(true_answer_list))
        final_score = max(0.0, final_score)  # scores cannot be below 0
    elif grading_mode == 'ordered':
        student_answer = [ans['inner_html'] for ans in student_answer]
        true_answer = [ans['inner_html'] for ans in true_answer_list]
        final_score = 1 if student_answer == true_answer else 0

    elif grading_mode in ['ranking', 'dag']:
        submission = [ans['tag'] for ans in student_answer]
        depends_graph = {}
        group_belonging = {}

        if grading_mode == 'ranking':
            true_answer_list = sorted(true_answer_list, key=lambda x: int(x['ranking']))
            true_answer = [answer['tag'] for answer in true_answer_list]
            tag_to_rank = {answer['tag']: answer['ranking'] for answer in true_answer_list}
            lines_of_rank = {rank: [tag for tag in tag_to_rank if tag_to_rank[tag] == rank] for rank in set(tag_to_rank.values())}

            cur_rank_depends = []
            prev_rank = None
            for tag in true_answer:
                ranking = tag_to_rank[tag]
                if prev_rank is not None and ranking != prev_rank:
                    cur_rank_depends = lines_of_rank[prev_rank]
                depends_graph[tag] = cur_rank_depends
                prev_rank = ranking

        elif grading_mode == 'dag':
            depends_graph = {ans['tag']: ans['depends'] for ans in true_answer_list}
            group_belonging = {ans['tag']: ans['group'] for ans in true_answer_list}

        num_initial_correct = grade_dag(submission, depends_graph, group_belonging)
        first_wrong = -1 if num_initial_correct == len(submission) else num_initial_correct

        true_answer_length = len(depends_graph.keys())
        if partial_credit_type == 'none':
            if num_initial_correct == true_answer_length:
                final_score = 1
            elif num_initial_correct < true_answer_length:
                final_score = 0
        elif partial_credit_type == 'lcs':
            edit_distance = lcs_partial_credit(submission, depends_graph, group_belonging)
            final_score = max(0, float(true_answer_length - edit_distance) / true_answer_length)

        if final_score < 1:
            if feedback_type == 'none':
                feedback = ''
            elif feedback_type == 'first-wrong':
                if first_wrong == -1:
                    feedback = FIRST_WRONG_FEEDBACK['incomplete']
                else:
                    feedback = FIRST_WRONG_FEEDBACK['wrong-at-block'].format(str(first_wrong + 1))
                    has_block_groups = group_belonging != {} and set(group_belonging.values()) != {None}
                    if check_indentation:
                        feedback += FIRST_WRONG_FEEDBACK['indentation']
                    if has_block_groups:
                        feedback += FIRST_WRONG_FEEDBACK['block-group']
                    feedback += '</ul>'

    data['partial_scores'][answer_name] = {'score': round(final_score, 2), 'feedback': feedback, 'weight': answer_weight, 'first_wrong': first_wrong}
def prepare(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    required_attribs = ['answers-name']
    optional_attribs = ['weight', 'number-answers', 'fixed-order', 'inline', 'none-of-the-above', 'all-of-the-above', 'hide-letter-keys']
    pl.check_attribs(element, required_attribs, optional_attribs)
    name = pl.get_string_attrib(element, 'answers-name')

    correct_answers, incorrect_answers = categorize_options(element)

    len_correct = len(correct_answers)
    len_incorrect = len(incorrect_answers)
    len_total = len_correct + len_incorrect

    enable_nota = pl.get_boolean_attrib(element, 'none-of-the-above', NONE_OF_THE_ABOVE_DEFAULT)
    enable_aota = pl.get_boolean_attrib(element, 'all-of-the-above', ALL_OF_THE_ABOVE_DEFAULT)

    nota_correct = False
    aota_correct = False
    if enable_nota or enable_aota:
        prob_space = len_correct + enable_nota + enable_aota
        rand_int = random.randint(1, prob_space)
        # Either 'None of the above' or 'All of the above' is correct
        # with probability 1/(number_correct + enable-nota + enable-aota).
        # However, if len_correct is 0, nota_correct is guaranteed to be True.
        # Thus, if no correct option is provided, 'None of the above' will always
        # be correct, and 'All of the above' always incorrect
        nota_correct = enable_nota and (rand_int == 1 or len_correct == 0)
        # 'All of the above' will always be correct when no incorrect option is
        # provided, while still never both True
        aota_correct = enable_aota and (rand_int == 2 or len_incorrect == 0) and not nota_correct

    if len_correct < 1 and not enable_nota:
        # This means the code needs to handle the special case when len_correct == 0
        raise Exception('pl-multiple-choice element must have at least 1 correct answer or set none-of-the-above')

    if enable_aota and len_correct < 2:
        # To prevent confusion on the client side
        raise Exception('pl-multiple-choice element must have at least 2 correct answers when all-of-the-above is set')

    # 1. Pick the choice(s) to display
    number_answers = pl.get_integer_attrib(element, 'number-answers', None)
    # determine if user provides number-answers
    set_num_answers = True
    if number_answers is None:
        set_num_answers = False
        number_answers = len_total + enable_nota + enable_aota
    # figure out how many choice(s) to choose from the *provided* choices,
    # excluding 'none-of-the-above' and 'all-of-the-above'
    number_answers -= (enable_nota + enable_aota)

    expected_num_answers = number_answers

    if enable_aota:
        # min number if 'All of the above' is correct
        number_answers = min(len_correct, number_answers)
        # raise exception when the *provided* number-answers can't be satisfied
        if set_num_answers and number_answers < expected_num_answers:
            raise Exception(f'Not enough correct choices for all-of-the-above. Need {expected_num_answers - number_answers} more')
    if enable_nota:
        # if nota correct
        number_answers = min(len_incorrect, number_answers)
        # raise exception when the *provided* number-answers can't be satisfied
        if set_num_answers and number_answers < expected_num_answers:
            raise Exception(f'Not enough incorrect choices for none-of-the-above. Need {expected_num_answers - number_answers} more')
    # this is the case for
    # - 'All of the above' is incorrect
    # - 'None of the above' is incorrect
    # - nota and aota disabled
    number_answers = min(min(1, len_correct) + len_incorrect, number_answers)

    if aota_correct:
        # when 'All of the above' is correct, we choose all from correct
        # and none from incorrect
        number_correct = number_answers
        number_incorrect = 0
    elif nota_correct:
        # when 'None of the above' is correct, we choose all from incorrect
        # and none from correct
        number_correct = 0
        number_incorrect = number_answers
    else:
        # PROOF: by the above probability, if len_correct == 0, then nota_correct
        # conversely; if not nota_correct, then len_correct != 0. Since len_correct
        # is none negative, this means len_correct >= 1.
        number_correct = 1
        number_incorrect = max(0, number_answers - number_correct)

    if not (0 <= number_incorrect <= len_incorrect):
        raise Exception('INTERNAL ERROR: number_incorrect: (%d, %d, %d)' % (number_incorrect, len_incorrect, number_answers))

    # 2. Sample correct and incorrect choices
    sampled_correct = random.sample(correct_answers, number_correct)
    sampled_incorrect = random.sample(incorrect_answers, number_incorrect)

    sampled_answers = sampled_correct + sampled_incorrect
    random.shuffle(sampled_answers)

    # 3. Modify sampled choices
    fixed_order = pl.get_boolean_attrib(element, 'fixed-order', FIXED_ORDER_DEFAULT)
    if fixed_order:
        # we can't simply skip the shuffle because we already broke the original
        # order by separating into correct/incorrect lists
        sampled_answers.sort(key=lambda a: a[0])  # sort by stored original index

    inline = pl.get_boolean_attrib(element, 'inline', INLINE_DEFAULT)
    if enable_aota:
        if inline:
            aota_text = 'All of these'
        else:
            aota_text = 'All of the above'
        # Add 'All of the above' option after shuffling
        sampled_answers.append((len_total, aota_correct, aota_text))

    if enable_nota:
        if inline:
            nota_text = 'None of these'
        else:
            nota_text = 'None of the above'
        # Add 'None of the above' option after shuffling
        sampled_answers.append((len_total + 1, nota_correct, nota_text))

    # 4. Write to data
    # Because 'All of the above' is below all the correct choice(s) when it's
    # true, the variable correct_answer will save it as correct, and
    # overwriting previous choice(s)
    display_answers = []
    correct_answer = None
    for (i, (index, correct, html)) in enumerate(sampled_answers):
        keyed_answer = {'key': chr(ord('a') + i), 'html': html}
        display_answers.append(keyed_answer)
        if correct:
            correct_answer = keyed_answer

    if name in data['params']:
        raise Exception('duplicate params variable name: %s' % name)
    if name in data['correct_answers']:
        raise Exception('duplicate correct_answers variable name: %s' % name)
    data['params'][name] = display_answers
    data['correct_answers'][name] = correct_answer
Beispiel #29
0
def prepare(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)

    required_attribs = ['answers-name']
    optional_attribs = [
        'weight', 'number-answers', 'min-correct', 'max-correct',
        'fixed-order', 'inline', 'hide-answer-panel', 'hide-help-text',
        'detailed-help-text', 'partial-credit', 'partial-credit-method'
    ]

    pl.check_attribs(element, required_attribs, optional_attribs)
    name = pl.get_string_attrib(element, 'answers-name')

    partial_credit = pl.get_boolean_attrib(element, 'partial-credit',
                                           PARTIAL_CREDIT_DEFAULT)
    partial_credit_method = pl.get_string_attrib(element,
                                                 'partial-credit-method', None)
    if not partial_credit and partial_credit_method is not None:
        raise Exception(
            'Cannot specify partial-credit-method if partial-credit is not enabled'
        )

    correct_answers = []
    incorrect_answers = []
    index = 0
    for child in element:
        if child.tag in ['pl-answer', 'pl_answer']:
            pl.check_attribs(child,
                             required_attribs=[],
                             optional_attribs=['correct'])
            correct = pl.get_boolean_attrib(child, 'correct', False)
            child_html = pl.inner_html(child)
            answer_tuple = (index, correct, child_html)
            if correct:
                correct_answers.append(answer_tuple)
            else:
                incorrect_answers.append(answer_tuple)
            index += 1

    len_correct = len(correct_answers)
    len_incorrect = len(incorrect_answers)
    len_total = len_correct + len_incorrect

    if len_correct == 0:
        raise ValueError('At least one option must be true.')

    number_answers = pl.get_integer_attrib(element, 'number-answers',
                                           len_total)
    min_correct = pl.get_integer_attrib(element, 'min-correct', 1)
    max_correct = pl.get_integer_attrib(element, 'max-correct',
                                        len(correct_answers))

    if min_correct < 1:
        raise ValueError(
            'The attribute min-correct is {:d} but must be at least 1'.format(
                min_correct))

    # FIXME: why enforce a maximum number of options?
    max_answers = 26  # will not display more than 26 checkbox answers

    number_answers = max(0, min(len_total, min(max_answers, number_answers)))
    min_correct = min(
        len_correct,
        min(number_answers,
            max(0, max(number_answers - len_incorrect, min_correct))))
    max_correct = min(len_correct,
                      min(number_answers, max(min_correct, max_correct)))
    if not (0 <= min_correct <= max_correct <= len_correct):
        raise ValueError(
            'INTERNAL ERROR: correct number: (%d, %d, %d, %d)' %
            (min_correct, max_correct, len_correct, len_incorrect))
    min_incorrect = number_answers - max_correct
    max_incorrect = number_answers - min_correct
    if not (0 <= min_incorrect <= max_incorrect <= len_incorrect):
        raise ValueError(
            'INTERNAL ERROR: incorrect number: (%d, %d, %d, %d)' %
            (min_incorrect, max_incorrect, len_incorrect, len_correct))

    number_correct = random.randint(min_correct, max_correct)
    number_incorrect = number_answers - number_correct

    sampled_correct = random.sample(correct_answers, number_correct)
    sampled_incorrect = random.sample(incorrect_answers, number_incorrect)

    sampled_answers = sampled_correct + sampled_incorrect
    random.shuffle(sampled_answers)

    fixed_order = pl.get_boolean_attrib(element, 'fixed-order',
                                        FIXED_ORDER_DEFAULT)
    if fixed_order:
        # we can't simply skip the shuffle because we already broke the original
        # order by separating into correct/incorrect lists
        sampled_answers.sort(
            key=lambda a: a[0])  # sort by stored original index

    display_answers = []
    correct_answer_list = []
    for (i, (index, correct, html)) in enumerate(sampled_answers):
        keyed_answer = {'key': chr(ord('a') + i), 'html': html}
        display_answers.append(keyed_answer)
        if correct:
            correct_answer_list.append(keyed_answer)

    if name in data['params']:
        raise Exception('duplicate params variable name: %s' % name)
    if name in data['correct_answers']:
        raise Exception('duplicate correct_answers variable name: %s' % name)
    data['params'][name] = display_answers
    data['correct_answers'][name] = correct_answer_list
Beispiel #30
0
def prepare(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    answer_name = pl.get_string_attrib(element, 'answers-name')

    required_attribs = ['answers-name']
    optional_attribs = ['source-blocks-order', 'grading-method',
                        'indentation', 'source-header',
                        'solution-header', 'file-name',
                        'solution-placement', 'max-incorrect',
                        'min-incorrect', 'weight',
                        'inline', 'max-indent',
                        'feedback', 'partial-credit']

    pl.check_attribs(element, required_attribs=required_attribs, optional_attribs=optional_attribs)

    check_indentation = pl.get_boolean_attrib(element, 'indentation', INDENTION_DEFAULT)
    grading_method = pl.get_string_attrib(element, 'grading-method', GRADING_METHOD_DEFAULT)
    feedback_type = pl.get_string_attrib(element, 'feedback', FEEDBACK_DEFAULT)

    if grading_method in ['dag', 'ranking']:
        partial_credit_type = pl.get_string_attrib(element, 'partial-credit', 'lcs')
        if partial_credit_type not in ['none', 'lcs']:
            raise Exception('partial credit type "' + partial_credit_type + '" is not available with the "' + grading_method + '" grading-method.')
    elif pl.get_string_attrib(element, 'partial-credit', None) is not None:
        raise Exception('You may only specify partial credit options in the DAG and ranking grading modes.')

    accepted_grading_method = ['ordered', 'unordered', 'ranking', 'dag', 'external']
    if grading_method not in accepted_grading_method:
        raise Exception('The grading-method attribute must be one of the following: ' + ', '.join(accepted_grading_method))

    if (grading_method not in ['dag', 'ranking'] and feedback_type != 'none') or \
       (grading_method in ['dag', 'ranking'] and feedback_type not in ['none', 'first-wrong']):
        raise Exception('feedback type "' + feedback_type + '" is not available with the "' + grading_method + '" grading-method.')

    correct_answers = []
    incorrect_answers = []

    def prepare_tag(html_tags, index, group=None):
        if html_tags.tag != 'pl-answer':
            raise Exception('Any html tags nested inside <pl-order-blocks> must be <pl-answer> or <pl-block-group>. \
                Any html tags nested inside <pl-block-group> must be <pl-answer>')

        if grading_method == 'external':
            pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct'])
        elif grading_method == 'unordered':
            pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct', 'indent'])
        elif grading_method in ['ranking', 'ordered']:
            pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct', 'ranking', 'indent'])
        elif grading_method == 'dag':
            pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct', 'tag', 'depends', 'comment', 'indent'])

        is_correct = pl.get_boolean_attrib(html_tags, 'correct', PL_ANSWER_CORRECT_DEFAULT)
        answer_indent = pl.get_integer_attrib(html_tags, 'indent', None)
        inner_html = pl.inner_html(html_tags)
        ranking = pl.get_integer_attrib(html_tags, 'ranking', -1)

        tag = pl.get_string_attrib(html_tags, 'tag', None)
        if grading_method == 'ranking':
            tag = str(index)
        depends = pl.get_string_attrib(html_tags, 'depends', '')
        depends = depends.strip().split(',') if depends else []

        if check_indentation is False and answer_indent is not None:
            raise Exception('<pl-answer> should not specify indentation if indentation is disabled.')

        answer_data_dict = {'inner_html': inner_html,
                            'indent': answer_indent,
                            'ranking': ranking,
                            'index': index,
                            'tag': tag,          # set by HTML with DAG grader, set internally for ranking grader
                            'depends': depends,  # only used with DAG grader
                            'group': group       # only used with DAG grader
                            }
        if is_correct:
            correct_answers.append(answer_data_dict)
        else:
            incorrect_answers.append(answer_data_dict)

    index = 0
    group_counter = 0
    for html_tags in element:  # iterate through the html tags inside pl-order-blocks
        if html_tags.tag is etree.Comment:
            continue
        elif html_tags.tag == 'pl-block-group':
            if grading_method != 'dag':
                raise Exception('Block groups only supported in the "dag" grading mode.')

            group_counter += 1
            for grouped_tag in html_tags:
                if html_tags.tag is etree.Comment:
                    continue
                else:
                    prepare_tag(grouped_tag, index, group_counter)
                    index += 1
        else:
            prepare_tag(html_tags, index)
            index += 1

    if pl.get_string_attrib(element, 'grading-method', GRADING_METHOD_DEFAULT) != 'external' and len(correct_answers) == 0:
        raise Exception('There are no correct answers specified for this question.')

    all_incorrect_answers = len(incorrect_answers)
    max_incorrect = pl.get_integer_attrib(element, 'max-incorrect', all_incorrect_answers)
    min_incorrect = pl.get_integer_attrib(element, 'min-incorrect', all_incorrect_answers)

    if min_incorrect > len(incorrect_answers) or max_incorrect > len(incorrect_answers):
        raise Exception('The min-incorrect or max-incorrect attribute may not exceed the number of incorrect <pl-answers>.')
    if min_incorrect > max_incorrect:
        raise Exception('The attribute min-incorrect must be smaller than max-incorrect.')

    incorrect_answers_count = random.randint(min_incorrect, max_incorrect)

    sampled_correct_answers = correct_answers
    sampled_incorrect_answers = random.sample(incorrect_answers, incorrect_answers_count)

    mcq_options = sampled_correct_answers + sampled_incorrect_answers

    source_blocks_order = pl.get_string_attrib(element, 'source-blocks-order', SOURCE_BLOCKS_ORDER_DEFAULT)
    if source_blocks_order == 'random':
        random.shuffle(mcq_options)
    elif source_blocks_order == 'ordered':
        mcq_options.sort(key=lambda a: a['index'])
    else:
        raise Exception('The specified option for the "source-blocks-order" attribute is invalid.')

    # data['params'][answer_name] = filter_keys_from_array(mcq_options, 'inner_html')
    for option in mcq_options:
        option['uuid'] = pl.get_uuid()

    data['params'][answer_name] = mcq_options
    data['correct_answers'][answer_name] = correct_answers