Esempio n. 1
0
 def generate(el, data):
     return {
         'left': pl.get_float_attrib(el, 'x', 20),
         'top': pl.get_float_attrib(el, 'y', 20),
         'angle': pl.get_float_attrib(el, 'angle', 0),
         'image_url': path.join(data['clientFilesUrl'], 'logo.png')
     }
Esempio n. 2
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}}
Esempio n. 3
0
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', WEIGHT_DEFAULT)

    # 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',
                                      COMPARISON_DEFAULT)

    # Compare submitted answer with true answer
    if comparison == 'relabs':
        rtol = pl.get_float_attrib(element, 'rtol', RTOL_DEFAULT)
        atol = pl.get_float_attrib(element, 'atol', ATOL_DEFAULT)
        correct = pl.is_correct_ndarray2D_ra(a_sub, a_tru, rtol, atol)
    elif comparison == 'sigfig':
        digits = pl.get_integer_attrib(element, 'digits', DIGITS_DEFAULT)
        correct = pl.is_correct_ndarray2D_sf(a_sub, a_tru, digits)
    elif comparison == 'decdig':
        digits = pl.get_integer_attrib(element, 'digits', DIGITS_DEFAULT)
        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}
Esempio n. 4
0
def grade(element_html, element_index, 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 = data['correct_answers'].get(name, None)
    if a_tru is None:
        return data
    # Convert true answer to numpy
    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 data
    # Convert submitted answer to numpy
    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 data

    # 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-5)
        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)
        eps_digits = pl.get_integer_attrib(element, 'eps_digits', 3)
        correct = pl.is_correct_ndarray2D_sf(a_sub, a_tru, digits, eps_digits)
    elif comparison == 'decdig':
        digits = pl.get_integer_attrib(element, 'digits', 2)
        eps_digits = pl.get_integer_attrib(element, 'eps_digits', 3)
        correct = pl.is_correct_ndarray2D_dd(a_sub, a_tru, digits, eps_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}

    return data
Esempio n. 5
0
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', WEIGHT_DEFAULT)

    # 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['partial_scores'][name] = {'score': 0, 'weight': weight}
        # Get method of comparison, with relabs as default
        comparison = pl.get_string_attrib(element, 'comparison',
                                          COMPARISON_DEFAULT)
        if comparison == 'relabs':
            rtol = pl.get_float_attrib(element, 'rtol', RTOL_DEFAULT)
            atol = pl.get_float_attrib(element, 'atol', ATOL_DEFAULT)
            # Get max error according to numpy.allclose()
            eps = np.absolute(a_tru) * rtol + atol
            eps += random.uniform(1, 10)
            answer = a_tru + eps * random.choice([-1, 1])
        elif comparison == 'sigfig':
            digits = pl.get_integer_attrib(element, 'digits', DIGITS_DEFAULT)
            # Get max error according to pl.is_correct_scalar_sf()
            if (a_tru == 0):
                n = digits - 1
            else:
                n = -int(np.floor(np.log10(np.abs(a_tru)))) + (digits - 1)
            eps = 0.51 * (10**-n)
            eps += random.uniform(1, 10)
            answer = a_tru + eps * random.choice([-1, 1])
        elif comparison == 'decdig':
            digits = pl.get_integer_attrib(element, 'digits', DIGITS_DEFAULT)
            # Get max error according to pl.is_correct_scalar_dd()
            eps = 0.51 * (10**-digits)
            eps += random.uniform(1, 10)
            answer = a_tru + eps * random.choice([-1, 1])
        else:
            raise ValueError('method of comparison "%s" is not valid' %
                             comparison)
        data['raw_submitted_answers'][name] = str(answer)
    elif result == 'invalid':
        # FIXME: add more invalid expressions, make text of format_errors
        # correct, and randomize
        data['raw_submitted_answers'][name] = '1 + 2'
        data['format_errors'][name] = 'invalid'
    else:
        raise Exception('invalid result: %s' % result)
Esempio n. 7
0
def grade(element_html, data):
    # Get the name of the element and the weight for this answer
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    weight = pl.get_float_attrib(element, 'weight', 1.0)

    # Get the number of submitted clicks and the correct number of clicks
    submitted_answer = data["submitted_answers"][name]
    correct_answer = data["correct_answers"][name]
    score = 0.0
    feedback = None

    # Grade the actual number of clicks
    if submitted_answer == correct_answer:
        score = 1.0
    elif submitted_answer == correct_answer - 1:
        score = 0.75
        feedback = 'Your number was one too small.'
    elif submitted_answer == correct_answer + 1:
        score = 0.5
        feedback = 'Your number was one too large.'
    else:
        score = 0
        feedback = "You didn't click on the image the correct number of times"

    # Put the score, weight, and feedback into the data object
    data['partial_scores'][name] = {
        'score': score,
        'weight': weight,
        'feedback': feedback
    }
def grade(element_html, element_index, 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 = data['correct_answers'].get(name, None)
    if a_tru is None:
        return data

    # 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 data

    # 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-5)
        atol = pl.get_float_attrib(element, 'atol', 1e-8)
        correct = pl.is_correct_scalar_ra(a_sub, a_tru, rtol, atol)
    elif comparison == 'sigfig':
        digits = pl.get_integer_attrib(element, 'digits', 2)
        eps_digits = pl.get_integer_attrib(element, 'eps_digits', 3)
        correct = pl.is_correct_scalar_sf(a_sub, a_tru, digits, eps_digits)
    elif comparison == 'decdig':
        digits = pl.get_integer_attrib(element, 'digits', 2)
        eps_digits = pl.get_integer_attrib(element, 'eps_digits', 3)
        correct = pl.is_correct_scalar_dd(a_sub, a_tru, digits, eps_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}

    return data
Esempio n. 9
0
def prepare(element_html, element_index, data):
    element = lxml.html.fragment_fromstring(element_html)
    required_attribs = ['answers-name']
    optional_attribs = ['weight', 'correct-answer', 'label', 'suffix', 'display', 'comparison', 'rtol', 'atol', 'digits', 'allow-complex']
    pl.check_attribs(element, required_attribs, optional_attribs)
    name = pl.get_string_attrib(element, 'answers-name')

    correct_answer = pl.get_float_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
Esempio n. 10
0
def prepare(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    required_attribs = ['answers-name']
    optional_attribs = ['weight', 'correct-answer', 'label', 'suffix', 'display', 'comparison', 'rtol', 'atol', 'digits', 'allow-complex', 'show-help-text', 'size', 'show-correct-answer', 'show-placeholder', 'allow-fractions', 'allow-blank', 'blank-value', 'custom-format']
    pl.check_attribs(element, required_attribs, optional_attribs)
    name = pl.get_string_attrib(element, 'answers-name')

    correct_answer = pl.get_float_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

    custom_format = pl.get_string_attrib(element, 'custom-format', None)
    if custom_format is not None:
        try:
            _ = ('{:' + custom_format + '}').format(0)
        except ValueError:
            raise Exception('invalid custom format: %s' % custom_format) from None
Esempio n. 11
0
def render(element_html, element_index, 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 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-5)
            atol = pl.get_float_attrib(element, 'atol', 1e-8)
            info_params = {
                'format': True,
                'relabs': True,
                'rtol': rtol,
                'atol': atol
            }
        elif comparison == 'sigfig':
            digits = pl.get_integer_attrib(element, 'digits', 2)
            info_params = {'format': True, 'sigfig': True, 'digits': digits}
        elif comparison == 'decdig':
            digits = pl.get_integer_attrib(element, 'digits', 2)
            info_params = {'format': True, 'decdig': True, 'digits': digits}
        else:
            raise ValueError(
                'method of comparison "%s" is not valid (must be "relabs", "sigfig", or "decdig")'
                % comparison)
        with open('pl_matrix_input.mustache', 'r') as f:
            info = chevron.render(f, info_params).strip()
        with open('pl_matrix_input.mustache', 'r') 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
        }
        if raw_submitted_answer is not None:
            html_params['raw_submitted_answer'] = escape(raw_submitted_answer)
        with open('pl_matrix_input.mustache', 'r') 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
        }
        if parse_error is None:
            a_sub = np.array(data['submitted_answers'][name])
            html_params['a_sub'] = pl.numpy_to_matlab(a_sub,
                                                      ndigits=12,
                                                      wtype='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)
        with open('pl_matrix_input.mustache', 'r') 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 = 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-5)
                atol = pl.get_float_attrib(element, 'atol', 1e-8)
                # FIXME: render correctly with respect to rtol and atol
                a_tru = pl.numpy_to_matlab(a_tru, ndigits=12, wtype='g')
            elif comparison == 'sigfig':
                digits = pl.get_integer_attrib(element, 'digits', 2)
                a_tru = pl.numpy_to_matlab_sf(a_tru, ndigits=digits)
            elif comparison == 'decdig':
                digits = pl.get_integer_attrib(element, 'digits', 2)
                a_tru = pl.numpy_to_matlab(a_tru, ndigits=digits, wtype='f')
            else:
                raise ValueError(
                    'method of comparison "%s" is not valid (must be "relabs", "sigfig", or "decdig")'
                    % comparison)

            # FIXME: render correctly with respect to method of comparison
            html_params = {'answer': True, 'label': label, 'a_tru': a_tru}
            with open('pl_matrix_input.mustache', 'r') as f:
                html = chevron.render(f, html_params).strip()
        else:
            html = ''

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

    return html
Esempio n. 12
0
def categorize_options(element, data):
    """Get provided correct and incorrect answers"""
    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=['score', 'correct', 'feedback'])
            correct = pl.get_boolean_attrib(child, 'correct', False)
            child_html = pl.inner_html(child)
            child_feedback = pl.get_string_attrib(child, 'feedback',
                                                  FEEDBACK_DEFAULT)

            default_score = SCORE_CORRECT_DEFAULT if correct else SCORE_INCORRECT_DEFAULT
            score = pl.get_float_attrib(child, 'score', default_score)

            if not (SCORE_INCORRECT_DEFAULT <= score <= SCORE_CORRECT_DEFAULT):
                raise Exception(
                    f'Score {score} is invalid, must be in the range [0.0, 1.0].'
                )

            if correct and score != SCORE_CORRECT_DEFAULT:
                raise Exception('Correct answers must give full credit.')

            answer_tuple = (index, correct, child_html, child_feedback, score)
            if correct:
                correct_answers.append(answer_tuple)
            else:
                incorrect_answers.append(answer_tuple)
            index += 1

    file_path = pl.get_string_attrib(element, 'external-json',
                                     EXTERNAL_JSON_DEFAULT)
    if file_path is not EXTERNAL_JSON_DEFAULT:
        correct_attrib = pl.get_string_attrib(
            element, 'external-json-correct-key',
            EXTERNAL_JSON_CORRECT_KEY_DEFAULT)
        incorrect_attrib = pl.get_string_attrib(
            element, 'external-json-incorrect-key',
            EXTERNAL_JSON_INCORRECT_KEY_DEFAULT)
        if pathlib.PurePath(file_path).is_absolute():
            json_file = file_path
        else:
            json_file = pathlib.PurePath(
                data['options']['question_path']).joinpath(file_path)
        try:
            with open(json_file, mode='r', encoding='utf-8') as f:
                obj = json.load(f)
                for text in obj.get(correct_attrib, []):
                    correct_answers.append(
                        (index, True, text, None, SCORE_CORRECT_DEFAULT))
                    index += 1
                for text in obj.get(incorrect_attrib, []):
                    incorrect_answers.append(
                        (index, False, text, None, SCORE_INCORRECT_DEFAULT))
                    index += 1
        except FileNotFoundError:
            raise Exception(
                f'JSON answer file: "{json_file}" could not be found')
    return correct_answers, incorrect_answers
Esempio n. 13
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', 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
Esempio n. 14
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name', '')
    preview_mode = not pl.get_boolean_attrib(
        element, 'gradable', defaults.element_defaults['gradable'])
    with open('pl-drawing.mustache') as f:
        template = f.read()

    btn_markup = ''
    init = []

    load_extensions(data)

    for el in element:
        if el.tag is lxml.etree.Comment:
            continue
        elif el.tag == 'pl-controls' and not preview_mode:
            btn_markup = render_controls(template, el)
        elif el.tag == 'pl-drawing-initial':
            init, _ = render_drawing_items(el)
            draw_error_box = pl.get_boolean_attrib(
                el, 'draw-error-box',
                defaults.element_defaults['draw-error-box'])

    for obj in init:
        obj['graded'] = False
        obj['drawErrorBox'] = draw_error_box
        if 'objectDrawErrorBox' in obj:
            if obj['objectDrawErrorBox'] is not None:
                obj['drawErrorBox'] = obj['objectDrawErrorBox']

    grid_size = pl.get_integer_attrib(element, 'grid-size',
                                      defaults.element_defaults['grid-size'])
    tol = pl.get_float_attrib(element, 'tol', grid_size / 2)
    angle_tol = pl.get_float_attrib(element, 'angle-tol',
                                    defaults.element_defaults['angle-tol'])
    tol_percent = round(tol / grid_size, 2) if grid_size != 0 else 1

    js_options = {
        'snap_to_grid':
        pl.get_boolean_attrib(element, 'snap-to-grid',
                              defaults.element_defaults['snap-to-grid']),
        'grid_size':
        grid_size,
        'editable': (data['panel'] == 'question' and not preview_mode),
        'base_url':
        data['options']['base_url'],
        'client_files':
        '/pl/static/elements/pl-drawing/clientFilesElement/',
        'element_client_files':
        data['options']['client_files_extensions_url'],
        'render_scale':
        pl.get_float_attrib(element, 'render-scale',
                            defaults.element_defaults['render-scale']),
        'width':
        pl.get_string_attrib(element, 'width',
                             defaults.element_defaults['width']),
        'height':
        pl.get_string_attrib(element, 'height',
                             defaults.element_defaults['height'])
    }

    show_btn = data['panel'] == 'question' and not preview_mode

    if tol == grid_size / 2:
        message_default = 'The expected tolerance is 1/2 square grid for position and ' + str(
            angle_tol) + ' degrees for angle.'
    else:
        message_default = 'The expected tolerance is ' + str(
            tol_percent) + ' square grid for position and ' + str(
                angle_tol) + ' degrees for angle.'

    html_params = {
        'uuid':
        pl.get_uuid(),
        'width':
        pl.get_string_attrib(element, 'width',
                             defaults.element_defaults['width']),
        'height':
        pl.get_string_attrib(element, 'height',
                             defaults.element_defaults['height']),
        'options_json':
        json.dumps(js_options),
        'show_buttons':
        show_btn,
        'name':
        name,
        'render_element':
        True,
        'btn_markup':
        btn_markup,
        'show_tolerance':
        show_btn and pl.get_boolean_attrib(
            element, 'show-tolerance-hint',
            defaults.element_defaults['show-tolerance-hint']),
        'tolerance':
        pl.get_string_attrib(element, 'tolerance-hint', message_default),
    }

    if data['panel'] == 'answer' and pl.get_boolean_attrib(
            element, 'hide-answer-panel', True):
        return ''

    if preview_mode:
        html_params['input_answer'] = json.dumps(init)
    else:
        if data['panel'] == 'answer' and name in data['correct_answers']:
            html_params['input_answer'] = json.dumps(
                data['correct_answers'][name])
        else:
            sub = []
            if name in data['submitted_answers']:
                sub = data['submitted_answers'][name]
            items = union_drawing_items(init, sub)
            html_params['input_answer'] = json.dumps(items)

    # Grading feedback
    if data['panel'] == 'submission':
        parse_error = data['format_errors'].get(name, None)
        html_params['parse_error'] = parse_error

    return chevron.render(template, html_params).strip()
Esempio n. 15
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    answer_name = pl.get_string_attrib(element, 'answer-name')

    uuid = pl.get_uuid()

    body_position = get_position(element,
                                 'body-position',
                                 default=BODY_POSITION_DEFAULT)
    body_orientation = get_orientation(element, 'body-orientation',
                                       BODY_ORIENTATION_DEFAULT)
    camera_position = get_position(element,
                                   'camera-position',
                                   default=CAMERA_POSITION_DEFAULT,
                                   must_be_nonzero=True)
    body_cantranslate = pl.get_boolean_attrib(element, 'body-cantranslate',
                                              BODY_CANTRANSLATE_DEFAULT)
    body_canrotate = pl.get_boolean_attrib(element, 'body-canrotate',
                                           BODY_CANROTATE_DEFAULT)
    camera_canmove = pl.get_boolean_attrib(element, 'camera-canmove',
                                           CAMERA_CANMOVE_DEFAULT)
    text_pose_format = pl.get_string_attrib(element, 'text-pose-format',
                                            TEXT_POSE_FORMAT_DEFAULT)
    if text_pose_format not in ['matrix', 'quaternion', 'homogeneous']:
        raise Exception(
            'attribute "text-pose-format" must be either "matrix", "quaternion", or homogeneous'
        )
    objects = get_objects(element, data)

    if data['panel'] == 'question':
        will_be_graded = pl.get_boolean_attrib(element, 'grade', GRADE_DEFAULT)
        show_pose = pl.get_boolean_attrib(element, 'show-pose-in-question',
                                          SHOW_POSE_IN_QUESTION_DEFAULT)

        # Restore pose of body and camera, if available - otherwise use values
        # from attributes (note that restored pose will also have camera_orientation,
        # which we currently ignore because the camera is always z up and looking
        # at the origin of the space frame).
        #
        # Be careful. It's possible that data['submitted_answers'][answer_name]
        # exists but is None (due to some other error). So we need to use None
        # as the default and to check if the result - either from the existing
        # value or the default value - is None.
        pose_default = {
            'body_quaternion': body_orientation,
            'body_position': body_position,
            'camera_position': camera_position
        }
        pose = data['submitted_answers'].get(answer_name, None)
        if pose is None:
            pose = pose_default

        # These are passed as arguments to PLThreeJS constructor in client code
        options = {
            'uuid': uuid,
            'pose': dict_to_b64(pose),
            'pose_default': dict_to_b64(pose_default),
            'body_cantranslate': body_cantranslate,
            'body_canrotate': body_canrotate,
            'camera_canmove': camera_canmove,
            'text_pose_format': text_pose_format,
            'objects': objects
        }

        # These are used for templating
        html_params = {
            'question':
            True,
            'uuid':
            uuid,
            'answer_name':
            answer_name,
            'show_bodybuttons':
            body_cantranslate or body_canrotate,
            'show_toggle':
            body_cantranslate and body_canrotate,
            'show_reset':
            body_cantranslate or body_canrotate or camera_canmove,
            'show_pose':
            show_pose,
            'show_instructions':
            will_be_graded,
            'tol_translation':
            '{:.2f}'.format(
                pl.get_float_attrib(element, 'tol-translation', 0.5)),
            'tol_rotation':
            '{:.1f}'.format(pl.get_float_attrib(element, 'tol-rotation', 5)),
            'default_is_python':
            True,
            'options':
            json.dumps(options, allow_nan=False)
        }

        with open('pl-threejs.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()
    elif data['panel'] == 'submission':
        will_be_graded = pl.get_boolean_attrib(element, 'grade', GRADE_DEFAULT)
        if not will_be_graded:
            return ''

        show_pose = pl.get_boolean_attrib(
            element, 'show-pose-in-submitted-answer',
            SHOW_POSE_IN_SUBMITTED_ANSWER_DEFAULT)

        # Get submitted answer
        pose = data['submitted_answers'].get(answer_name)

        # These are passed as arguments to PLThreeJS constructor in client code
        options = {
            'uuid': uuid,
            'pose': dict_to_b64(pose),
            'body_cantranslate': False,
            'body_canrotate': False,
            'camera_canmove': False,
            'text_pose_format': text_pose_format,
            'objects': objects
        }

        # These are used for templating
        html_params = {
            'submission': True,
            'uuid': uuid,
            'answer_name': answer_name,
            'show_bodybuttons': False,
            'show_toggle': False,
            'show_pose': show_pose,
            'default_is_python': True,
            'options': json.dumps(options, allow_nan=False)
        }

        partial_score = data['partial_scores'].get(answer_name, None)
        if partial_score is not None:
            html_params['error_in_translation'] = str(
                np.abs(
                    np.round(partial_score['feedback']['error_in_translation'],
                             2)))
            html_params['error_in_rotation'] = str(
                np.abs(
                    np.round(partial_score['feedback']['error_in_rotation'],
                             1)))
            html_params['show_feedback'] = True
            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-threejs.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()
    elif data['panel'] == 'answer':
        will_be_graded = pl.get_boolean_attrib(element, 'grade', GRADE_DEFAULT)
        if not will_be_graded:
            return ''

        show_pose = pl.get_boolean_attrib(element,
                                          'show-pose-in-correct-answer',
                                          SHOW_POSE_IN_CORRECT_ANSWER_DEFAULT)

        # Get submitted answer
        pose = data['submitted_answers'].get(answer_name, None)
        if pose is None:
            # If we are here, an error has occurred. Replace pose with its default.
            # (Only pose['camera_position'] is actually used.)
            pose = {
                'body_quaternion': body_orientation,
                'body_position': body_position,
                'camera_position': camera_position
            }

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

        # Convert correct answer to Quaternion, then to [x, y, z, w]
        f = pl.get_string_attrib(element, 'answer-pose-format',
                                 ANSWER_POSE_FORMAT_DEFAULT)
        p, q = parse_correct_answer(f, a)
        p = p.tolist()
        q = np.roll(q.elements, -1).tolist()

        # Replace body pose with correct answer
        pose['body_position'] = p
        pose['body_quaternion'] = q

        # These are passed as arguments to PLThreeJS constructor in client code
        options = {
            'uuid': uuid,
            'pose': dict_to_b64(pose),
            'body_cantranslate': False,
            'body_canrotate': False,
            'camera_canmove': False,
            'text_pose_format': text_pose_format,
            'objects': objects
        }

        # These are used for templating
        html_params = {
            'answer': True,
            'uuid': uuid,
            'answer_name': answer_name,
            'show_bodybuttons': False,
            'show_toggle': False,
            'show_pose': show_pose,
            'default_is_python': True,
            'options': json.dumps(options, allow_nan=False)
        }

        with open('pl-threejs.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()
    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}
Esempio n. 17
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    width = pl.get_float_attrib(element, 'width', None)
    height = pl.get_float_attrib(element, 'height', None)
    background = None

    # Assign layer index in order children are defined
    # Later defined elements will be placed on top of earlier ones
    locations = []
    z_index = 0
    for child in element:
        # Ignore comments
        if isinstance(child, lxml.html.HtmlComment):
            continue

        # Don't do any special processing for backgrounds
        if child.tag == 'pl-background':
            background = pl.inner_html(child)
            continue

        # Otherwise, continue as normal
        valign = pl.get_string_attrib(child, 'valign', VALIGN_DEFAULT)
        halign = pl.get_string_attrib(child, 'halign', HALIGN_DEFAULT)

        left = pl.get_float_attrib(child, 'left', None)
        right = pl.get_float_attrib(child, 'right', None)
        top = pl.get_float_attrib(child, 'top', None)
        bottom = pl.get_float_attrib(child, 'bottom', None)

        # We allow both left/right and top/bottom but only set top and left
        # so we don't have to worry about all the alignment possibilities
        if left is not None:
            x = left
        elif right is not None:
            x = width - right

        if top is not None:
            y = top
        elif bottom is not None:
            y = height - bottom

        hoff = ALIGNMENT_TO_PERC[halign]
        voff = ALIGNMENT_TO_PERC[valign]
        transform = f'translate({hoff}, {voff})'

        style = f'top: {y}px; left: {x}px; transform: {transform}; z-index: {z_index}'
        obj = {
            'html': pl.inner_html(child),
            'outer_style': style,
        }
        locations.append(obj)
        z_index += 1

    html_params = {
        'width': width,
        'height': height,
        'locations': locations,
        'background': background,
        'clip': pl.get_boolean_attrib(element, 'clip', CLIP_DEFAULT)
    }
    with open('pl-overlay.mustache', 'r', encoding='utf-8') as f:
        html = chevron.render(f, html_params).strip()
    return html
Esempio n. 18
0
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
        }
Esempio n. 19
0
def render(element_html, element_index, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers_name')
    label = pl.get_string_attrib(element, 'label', None)
    suffix = pl.get_string_attrib(element, 'suffix', None)
    display = pl.get_string_attrib(element, 'display', 'inline')

    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-5)
            atol = pl.get_float_attrib(element, 'atol', 1e-8)
            info_params = {
                'format': True,
                'relabs': True,
                'rtol': rtol,
                'atol': atol
            }
        elif comparison == 'sigfig':
            digits = pl.get_integer_attrib(element, 'digits', 2)
            info_params = {'format': True, 'sigfig': True, 'digits': digits}
        elif comparison == 'decdig':
            digits = pl.get_integer_attrib(element, 'digits', 2)
            info_params = {'format': True, 'decdig': True, 'digits': digits}
        else:
            raise ValueError(
                'method of comparison "%s" is not valid (must be "relabs", "sigfig", or "decdig")'
                % comparison)
        with open('pl_number_input.mustache', 'r') as f:
            info = chevron.render(f, info_params).strip()
        with open('pl_number_input.mustache', 'r') 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,
            'suffix': suffix,
            'editable': editable,
            'info': info,
            'shortinfo': shortinfo
        }

        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:
                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", "block", or "display")'
                % display)
        if raw_submitted_answer is not None:
            html_params['raw_submitted_answer'] = escape(raw_submitted_answer)
        with open('pl_number_input.mustache', 'r') 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
        }
        if parse_error is None:
            a_sub = data['submitted_answers'][name]
            html_params['suffix'] = suffix
            html_params['a_sub'] = '{:.12g}'.format(a_sub)
        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:
                raise ValueError('invalid score' + score)

        with open('pl_number_input.mustache', 'r') 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:

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

            # FIXME: render correctly with respect to method of comparison
            html_params = {
                'answer': True,
                'label': label,
                'a_tru': a_tru,
                'suffix': suffix
            }
            with open('pl_number_input.mustache', 'r') as f:
                html = chevron.render(f, html_params).strip()
        else:
            html = ''
    else:
        raise Exception('Invalid panel type: %s' % data['panel'])

    return html
Esempio n. 20
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    answer_name = pl.get_string_attrib(element, 'answer-name')

    uuid = pl.get_uuid()

    body_position = get_position(element, 'body-position', default=[0, 0, 0])
    body_orientation = get_orientation(element, 'body-orientation', 'body-pose-format')
    camera_position = get_position(element, 'camera-position', default=[5, 2, 2], must_be_nonzero=True)
    body_cantranslate = pl.get_boolean_attrib(element, 'body-cantranslate', True)
    body_canrotate = pl.get_boolean_attrib(element, 'body-canrotate', True)
    camera_canmove = pl.get_boolean_attrib(element, 'camera-canmove', True)
    text_pose_format = pl.get_string_attrib(element, 'text-pose-format', 'matrix')
    if text_pose_format not in ['matrix', 'quaternion', 'homogeneous']:
        raise Exception('attribute "text-pose-format" must be either "matrix", "quaternion", or homogeneous')
    objects = get_objects(element, data)

    if data['panel'] == 'question':
        will_be_graded = pl.get_boolean_attrib(element, 'grade', True)
        show_pose = pl.get_boolean_attrib(element, 'show-pose-in-question', True)

        # Restore pose of body and camera, if available - otherwise use values
        # from attributes (note that restored pose will also have camera_orientation,
        # which we currently ignore because the camera is always z up and looking
        # at the origin of the space frame).
        #
        # Be careful. It's possible that data['submitted_answers'][answer_name]
        # exists but is None (due to some other error). So we need to use None
        # as the default and to check if the result - either from the existing
        # value or the default value - is None.
        pose_default = {
            'body_quaternion': body_orientation,
            'body_position': body_position,
            'camera_position': camera_position
        }
        pose = data['submitted_answers'].get(answer_name, None)
        if pose is None:
            pose = pose_default

        # These are passed as arguments to PLThreeJS constructor in client code
        options = {
            'uuid': uuid,
            'pose': dict_to_b64(pose),
            'pose_default': dict_to_b64(pose_default),
            'body_cantranslate': body_cantranslate,
            'body_canrotate': body_canrotate,
            'camera_canmove': camera_canmove,
            'text_pose_format': text_pose_format,
            'objects': objects
        }

        # These are used for templating
        html_params = {
            'question': True,
            'uuid': uuid,
            'answer_name': answer_name,
            'show_bodybuttons': body_cantranslate or body_canrotate,
            'show_toggle': body_cantranslate and body_canrotate,
            'show_reset': body_cantranslate or body_canrotate or camera_canmove,
            'show_pose': show_pose,
            'show_instructions': will_be_graded,
            'tol_translation': '{:.2f}'.format(pl.get_float_attrib(element, 'tol-translation', 0.5)),
            'tol_rotation': '{:.1f}'.format(pl.get_float_attrib(element, 'tol-rotation', 5)),
            'default_is_python': True,
            'options': json.dumps(options, allow_nan=False)
        }

        with open('pl-threejs.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()
    elif data['panel'] == 'submission':
        will_be_graded = pl.get_boolean_attrib(element, 'grade', True)
        if not will_be_graded:
            return ''

        show_pose = pl.get_boolean_attrib(element, 'show-pose-in-submitted-answer', True)

        # Get submitted answer
        pose = data['submitted_answers'].get(answer_name)

        # These are passed as arguments to PLThreeJS constructor in client code
        options = {
            'uuid': uuid,
            'pose': dict_to_b64(pose),
            'body_cantranslate': False,
            'body_canrotate': False,
            'camera_canmove': False,
            'text_pose_format': text_pose_format,
            'objects': objects
        }

        # These are used for templating
        html_params = {
            'submission': True,
            'uuid': uuid,
            'answer_name': answer_name,
            'show_bodybuttons': False,
            'show_toggle': False,
            'show_pose': show_pose,
            'default_is_python': True,
            'options': json.dumps(options, allow_nan=False)
        }

        partial_score = data['partial_scores'].get(answer_name, None)
        if partial_score is not None:
            html_params['error_in_translation'] = str(np.abs(np.round(partial_score['feedback']['error_in_translation'], 2)))
            html_params['error_in_rotation'] = str(np.abs(np.round(partial_score['feedback']['error_in_rotation'], 1)))
            html_params['show_feedback'] = True
            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-threejs.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()
    elif data['panel'] == 'answer':
        will_be_graded = pl.get_boolean_attrib(element, 'grade', True)
        if not will_be_graded:
            return ''

        show_pose = pl.get_boolean_attrib(element, 'show-pose-in-correct-answer', True)

        # Get submitted answer
        pose = data['submitted_answers'].get(answer_name, None)
        if pose is None:
            # If we are here, an error has occurred. Replace pose with its default.
            # (Only pose['camera_position'] is actually used.)
            pose = {
                'body_quaternion': body_orientation,
                'body_position': body_position,
                'camera_position': camera_position
            }

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

        # Convert correct answer to Quaternion, then to [x, y, z, w]
        f = pl.get_string_attrib(element, 'answer-pose-format', 'rpy')
        p, q = parse_correct_answer(f, a)
        p = p.tolist()
        q = np.roll(q.elements, -1).tolist()

        # Replace body pose with correct answer
        pose['body_position'] = p
        pose['body_quaternion'] = q

        # These are passed as arguments to PLThreeJS constructor in client code
        options = {
            'uuid': uuid,
            'pose': dict_to_b64(pose),
            'body_cantranslate': False,
            'body_canrotate': False,
            'camera_canmove': False,
            'text_pose_format': text_pose_format,
            'objects': objects
        }

        # These are used for templating
        html_params = {
            'answer': True,
            'uuid': uuid,
            'answer_name': answer_name,
            'show_bodybuttons': False,
            'show_toggle': False,
            'show_pose': show_pose,
            'default_is_python': True,
            'options': json.dumps(options, allow_nan=False)
        }

        with open('pl-threejs.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()
    else:
        raise Exception('Invalid panel type: %s' % data['panel'])

    return html
Esempio n. 21
0
def grade(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    prev = not pl.get_boolean_attrib(element, 'gradable',
                                     defaults.element_defaults['gradable'])
    if prev:
        return

    grid_size = pl.get_integer_attrib(element, 'grid-size',
                                      defaults.element_defaults['grid-size'])
    tol = pl.get_float_attrib(element, 'tol', grid_size / 2)
    angtol = pl.get_float_attrib(element, 'angle-tol',
                                 defaults.element_defaults['angle-tol'])
    disregard_extra_elements = pl.get_boolean_attrib(
        element, 'disregard-extra-elements',
        defaults.element_defaults['disregard-extra-elements'])

    name = pl.get_string_attrib(element, 'answers-name',
                                defaults.element_defaults['answers-name'])
    student = data['submitted_answers'][name]
    reference = data['correct_answers'][name]

    if not isinstance(student, list) or len(student) == 0:
        data['format_errors'][name] = 'No submitted answer.'
        return data

    matches = {}  # If a reference object is matched to a student object
    num_correct = 0  # number correct
    num_total_ref = 0
    num_total_st = 0
    num_optional = 0

    # Ensure that each reference object matches one and only one
    # student object.  Ungraded elements are skipped.
    # num_total_ref is the total number of objects that are expected to be graded
    # this disregards optional objects and objects that don't have a grading function
    for ref_element in reference:
        if elements.is_gradable(
                ref_element['gradingName']) and ref_element['graded']:
            matches[ref_element['id']] = False
            if 'optional_grading' in ref_element and ref_element[
                    'optional_grading']:
                continue
            num_total_ref += 1

    # Loop through and check everything
    for element in student:
        if 'gradingName' not in element or not elements.is_gradable(
                element['gradingName']
        ) or 'graded' not in element or not element['graded']:
            continue
        # total number of objects inserted by students (using buttons)
        # this will disregard the initial objects placed by question authors
        num_total_st += 1

        for ref_element in reference:
            if not elements.is_gradable(
                    ref_element['gradingName']
            ) or not ref_element['graded'] or element[
                    'gradingName'] != ref_element['gradingName']:
                # Skip if the reference element is not gradable
                continue

            if not disregard_extra_elements and matches[ref_element['id']]:
                # Skip if this object has already been matched
                continue

            if elements.grade(ref_element, element, element['gradingName'],
                              tol, angtol):
                if ('optional_grading' in ref_element
                        and ref_element['optional_grading']) or (
                            disregard_extra_elements
                            and matches[ref_element['id']]):
                    # It's optional but correct, so the score should not be affected
                    # Or, it's a duplicate and we're okay with that.
                    num_optional += 1
                else:
                    # if correct but optional, it does not add on number of correct answers,
                    # but the object will not be considered as extra
                    num_correct += 1

                matches[ref_element['id']] = True
                break

    extra_not_optional = num_total_st - (num_optional + num_correct)

    if num_total_ref == 0:
        score = 1
    else:
        percent_correct = num_correct / num_total_ref
        if percent_correct == 1:
            # if all the expected objects are matched
            # penalize extra objects
            score = max(0,
                        percent_correct - extra_not_optional / num_total_ref)
        else:
            score = percent_correct

    data['partial_scores'][name] = {
        'score': score,
        'weight': 1,
        'feedback': {
            'correct': (score == 1),
            'missing': {},
            'matches': matches
        }
    }

    return data
Esempio n. 22
0
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
Esempio n. 23
0
def get_objects(element, data):
    obj_list = []

    for child in element:
        is_stl = (child.tag in ['pl-threejs-stl', 'pl_threejs_stl'])
        is_txt = (child.tag in ['pl-threejs-txt', 'pl_threejs_txt'])
        if not (is_stl or is_txt):
            continue

        # Type-specific check and get (stl)
        if is_stl:
            # Attributes
            pl.check_attribs(child, required_attribs=['file-name'], optional_attribs=['file-directory', 'frame', 'color', 'position', 'orientation', 'format', 'scale', 'opacity'])
            # - file-name (and file-directory)
            file_url = get_file_url(child, data)
            # - type
            object_type = 'stl'
            # - object
            specific = {
                'type': object_type,
                'file_url': file_url
            }

        # Type-specific check and get (txt)
        if is_txt:
            # Attributes
            pl.check_attribs(child, required_attribs=[], optional_attribs=['frame', 'color', 'position', 'orientation', 'format', 'scale', 'opacity'])
            # - text
            text = pl.inner_html(child)
            # - type
            object_type = 'txt'
            # - object
            specific = {
                'type': object_type,
                'text': text
            }

        # Common
        # - frame
        frame = pl.get_string_attrib(child, 'frame', 'body')
        if frame not in ['body', 'space']:
            raise Exception('"frame" must be either "body" or "space": {:s}'.format(frame))
        if frame == 'body':
            default_color = '#e84a27'
            default_opacity = 0.7
        else:
            default_color = '#13294b'
            default_opacity = 0.4
        # - color
        color = pl.get_color_attrib(child, 'color', default_color)
        # - opacity
        opacity = pl.get_float_attrib(child, 'opacity', default_opacity)
        # - position
        p = pl.get_string_attrib(child, 'position', '[0, 0, 0]')
        try:
            position = np.array(json.loads(p), dtype=np.float64)
            if position.shape == (3,):
                position = position.tolist()
            else:
                raise ValueError()
        except Exception:
            raise Exception('attribute "position" must have format [x, y, z]: {:s}'.format(p))
        # - orientation (and format)
        orientation = get_orientation(child, 'orientation', 'format')
        # - scale
        scale = pl.get_float_attrib(child, 'scale', 1.0)

        common = {
            'frame': frame,
            'color': color,
            'opacity': opacity,
            'position': position,
            'quaternion': orientation,
            'scale': scale
        }

        obj = {**specific, **common}
        obj_list.append(obj)

    return obj_list
Esempio n. 24
0
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', WEIGHT_DEFAULT)

    # 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 np.float64, because...
    #
    #   If the method of comparison is relabs (i.e., using relative and
    #   absolute tolerance) then np.allclose is applied to check if the
    #   submitted and true answers are the same. If either answer is an
    #   integer outside the range of int64...
    #
    #       https://docs.scipy.org/doc/numpy-1.13.0/user/basics.types.html
    #
    #   ...then numpy throws this error:
    #
    #       TypeError: ufunc 'isfinite' not supported for the input types, and
    #       the inputs could not be safely coerced to any supported types
    #       according to the casting rule ''safe''
    #
    #   Casting as np.float64 avoids this error. This is reasonable in any case,
    #   because <pl-number-input> accepts double-precision floats, not ints.
    #
    if np.iscomplexobj(a_sub) or np.iscomplexobj(a_tru):
        a_sub = np.complex128(a_sub)
        a_tru = np.complex128(a_tru)
    else:
        a_sub = np.float64(a_sub)
        a_tru = np.float64(a_tru)

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

    # Compare submitted answer with true answer
    if comparison == 'relabs':
        rtol = pl.get_float_attrib(element, 'rtol', RTOL_DEFAULT)
        atol = pl.get_float_attrib(element, 'atol', ATOL_DEFAULT)
        correct = pl.is_correct_scalar_ra(a_sub, a_tru, rtol, atol)
    elif comparison == 'sigfig':
        digits = pl.get_integer_attrib(element, 'digits', DIGITS_DEFAULT)
        correct = pl.is_correct_scalar_sf(a_sub, a_tru, digits)
    elif comparison == 'decdig':
        digits = pl.get_integer_attrib(element, 'digits', DIGITS_DEFAULT)
        correct = pl.is_correct_scalar_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}
Esempio n. 25
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)

    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',
                                          COMPARISON_DEFAULT)
        if comparison == 'relabs':
            rtol = pl.get_float_attrib(element, 'rtol', RTOL_DEFAULT)
            atol = pl.get_float_attrib(element, 'atol', ATOL_DEFAULT)
            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', DIGITS_DEFAULT)
            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', DIGITS_DEFAULT)
            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', ALLOW_COMPLEX_DEFAULT)
        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,
            'show_info':
            pl.get_boolean_attrib(element, 'show-help-text',
                                  SHOW_HELP_TEXT_DEFAULT),
            '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'] = pl.escape_unicode_string(
                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 and name in data['submitted_answers']:
            # 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')
        elif name not in data['submitted_answers']:
            html_params['missing_input'] = True
            html_params['parse_error'] = None
        else:
            raw_submitted_answer = data['raw_submitted_answers'].get(
                name, None)
            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)

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

        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',
                                              COMPARISON_DEFAULT)
            if comparison == 'relabs':
                rtol = pl.get_float_attrib(element, 'rtol', RTOL_DEFAULT)
                atol = pl.get_float_attrib(element, 'atol', ATOL_DEFAULT)
                # 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',
                                               DIGITS_DEFAULT)
                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',
                                               DIGITS_DEFAULT)
                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
Esempio n. 26
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', None)
    suffix = pl.get_string_attrib(element, 'suffix', None)
    display = pl.get_string_attrib(element, 'display', DISPLAY_DEFAULT)

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

        html_params = {
            'question': True,
            'name': name,
            'label': label,
            'suffix': suffix,
            'editable': editable,
            'size': pl.get_integer_attrib(element, 'size', SIZE_DEFAULT),
            '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)

        # Get comparison parameters and info strings
        comparison = pl.get_string_attrib(element, 'comparison',
                                          COMPARISON_DEFAULT)
        if comparison == 'relabs':
            rtol = pl.get_float_attrib(element, 'rtol', RTOL_DEFAULT)
            atol = pl.get_float_attrib(element, 'atol', ATOL_DEFAULT)
            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', DIGITS_DEFAULT)
            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', DIGITS_DEFAULT)
            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)

        # Update parameters for the info popup
        show_correct = 'correct' in html_params and pl.get_boolean_attrib(
            element, 'show-correct-answer', SHOW_CORRECT_ANSWER_DEFAULT)
        info_params['allow_complex'] = pl.get_boolean_attrib(
            element, 'allow-complex', ALLOW_COMPLEX_DEFAULT)
        info_params['show_info'] = pl.get_boolean_attrib(
            element, 'show-help-text', SHOW_HELP_TEXT_DEFAULT)
        info_params['show_correct'] = show_correct

        # Find the true answer to be able to display it in the info popup
        ans_true = None
        if pl.get_boolean_attrib(element, 'show-correct-answer',
                                 SHOW_CORRECT_ANSWER_DEFAULT):
            ans_true = format_true_ans(element, data, name)
        if ans_true is not None:
            info_params['a_tru'] = ans_true

        with open('pl-number-input.mustache', 'r', encoding='utf-8') as f:
            info = chevron.render(f, info_params).strip()
        with open('pl-number-input.mustache', 'r', encoding='utf-8') as f:
            info_params.pop('format', None)
            # Within mustache, the shortformat generates the shortinfo that is used as a placeholder inside of the numeric entry.
            # Here we opt to not generate the value, hence the placeholder is empty.
            info_params['shortformat'] = pl.get_boolean_attrib(
                element, 'show-placeholder', SHOW_PLACEHOLDER_DEFAULT)
            shortinfo = chevron.render(f, info_params).strip()

        html_params['info'] = info
        html_params['shortinfo'] = shortinfo

        # Determine the title of the popup based on what information is being shown
        if pl.get_boolean_attrib(element, 'show-help-text',
                                 SHOW_HELP_TEXT_DEFAULT):
            html_params['popup_title'] = 'Number'
        else:
            html_params['popup_title'] = 'Correct Answer'

        # Enable or disable the popup
        if pl.get_boolean_attrib(element, 'show-help-text',
                                 SHOW_HELP_TEXT_DEFAULT) or show_correct:
            html_params['show_info'] = True
        html_params[
            'display_append_span'] = 'questionmark' in html_params or suffix

        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-number-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)

            html_params['suffix'] = suffix
            html_params['a_sub'] = '{:.12g}'.format(a_sub)
        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)

        # Add true answer to be able to display it in the submitted answer panel
        ans_true = None
        if pl.get_boolean_attrib(element, 'show-correct-answer',
                                 SHOW_CORRECT_ANSWER_DEFAULT):
            ans_true = format_true_ans(element, data, name)
        if ans_true is not None:
            html_params['a_tru'] = ans_true

        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-number-input.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()
    elif data['panel'] == 'answer':
        ans_true = format_true_ans(element, data, name)
        if ans_true is not None:
            html_params = {
                'answer': True,
                'label': label,
                'a_tru': ans_true,
                'suffix': suffix
            }
            with open('pl-number-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, element_index, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers_name')
    label = pl.get_string_attrib(element, 'label', None)
    suffix = pl.get_string_attrib(element, 'suffix', None)
    display = pl.get_string_attrib(element, 'display', 'inline')

    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)
            info_params = {
                'format': True,
                'relabs': True,
                'rtol': rtol,
                'atol': atol
            }
        elif comparison == 'sigfig':
            digits = pl.get_integer_attrib(element, 'digits', 2)
            info_params = {
                'format': True,
                'sigfig': True,
                'digits': digits,
                'comparison_eps': 0.51 * (10**-(digits - 1))
            }
        elif comparison == 'decdig':
            digits = pl.get_integer_attrib(element, 'digits', 2)
            info_params = {
                'format': True,
                'decdig': True,
                'digits': 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_number_input.mustache', 'r', encoding='utf-8') as f:
            info = chevron.render(f, info_params).strip()
        with open('pl_number_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,
            'suffix': suffix,
            '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 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_number_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)

            html_params['suffix'] = suffix
            html_params['a_sub'] = '{:.12g}'.format(a_sub)
        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_number_input.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()
    elif data['panel'] == 'answer':
        a_tru = pl.from_json(data['correct_answers'].get(name, None))
        if a_tru is not None:

            # 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
                a_tru = '{:.12g}'.format(a_tru)
            elif comparison == 'sigfig':
                digits = pl.get_integer_attrib(element, 'digits', 2)
                a_tru = pl.string_from_number_sigfig(a_tru, digits=digits)
            elif comparison == 'decdig':
                digits = pl.get_integer_attrib(element, 'digits', 2)
                a_tru = '{:.{ndigits}f}'.format(a_tru, ndigits=digits)
            else:
                raise ValueError(
                    'method of comparison "%s" is not valid (must be "relabs", "sigfig", or "decdig")'
                    % comparison)

            # FIXME: render correctly with respect to method of comparison
            html_params = {
                'answer': True,
                'label': label,
                'a_tru': a_tru,
                'suffix': suffix
            }
            with open('pl_number_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
Esempio n. 28
0
def get_objects(element, data):
    obj_list = []

    for child in element:
        is_stl = (child.tag in ['pl-threejs-stl', 'pl_threejs_stl'])
        is_txt = (child.tag in ['pl-threejs-txt', 'pl_threejs_txt'])
        if not (is_stl or is_txt):
            continue

        # Type-specific check and get (stl)
        if is_stl:
            # Attributes
            pl.check_attribs(child,
                             required_attribs=['file-name'],
                             optional_attribs=[
                                 'file-directory', 'frame', 'color',
                                 'position', 'orientation', 'format', 'scale',
                                 'opacity'
                             ])
            # - file-name (and file-directory)
            file_url = get_file_url(child, data)
            # - type
            object_type = 'stl'
            # - object
            specific = {'type': object_type, 'file_url': file_url}

        # Type-specific check and get (txt)
        if is_txt:
            # Attributes
            pl.check_attribs(child,
                             required_attribs=[],
                             optional_attribs=[
                                 'frame', 'color', 'position', 'orientation',
                                 'format', 'scale', 'opacity'
                             ])
            # - text
            text = pl.inner_html(child)
            # - type
            object_type = 'txt'
            # - object
            specific = {'type': object_type, 'text': text}

        # Common
        # - frame
        frame = pl.get_string_attrib(child, 'frame', 'body')
        if frame not in ['body', 'space']:
            raise Exception(
                '"frame" must be either "body" or "space": {:s}'.format(frame))
        if frame == 'body':
            default_color = '#e84a27'
            default_opacity = 0.7
        else:
            default_color = '#13294b'
            default_opacity = 0.4
        # - color
        color = pl.get_color_attrib(child, 'color', default_color)
        # - opacity
        opacity = pl.get_float_attrib(child, 'opacity', default_opacity)
        # - position
        p = pl.get_string_attrib(child, 'position', '[0, 0, 0]')
        try:
            position = np.array(json.loads(p), dtype=np.float64)
            if position.shape == (3, ):
                position = position.tolist()
            else:
                raise ValueError()
        except Exception:
            raise Exception(
                'attribute "position" must have format [x, y, z]: {:s}'.format(
                    p))
        # - orientation (and format)
        orientation = get_orientation(child, 'orientation', 'format')
        # - scale
        scale = pl.get_float_attrib(child, 'scale', 1.0)

        common = {
            'frame': frame,
            'color': color,
            'opacity': opacity,
            'position': position,
            'quaternion': orientation,
            'scale': scale
        }

        obj = {**specific, **common}
        obj_list.append(obj)

    return obj_list
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