Exemplo n.º 1
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    script_name = pl.get_string_attrib(element, 'script-name', None)

    with open(os.path.join(data['options']['question_path'],
                           script_name)) as f:
        script = f.read()

    width = pl.get_string_attrib(element, 'width', '500')
    height = pl.get_string_attrib(element, 'height', '300')

    params_names = pl.get_string_attrib(element, 'param-names', None)
    if params_names is None:
        client_params = {}
    else:
        params_names = params_names.split(sep=',')
        client_params = {key: data['params'][key] for key in params_names}

    html_params = {
        'script': script,
        'width': width,
        'height': height,
        'client_params': client_params,
        'uuid': pl.get_uuid(),
    }

    with open('pl-prairiedraw-figure.mustache') as f:
        html = chevron.render(f, html_params).strip()

    return html
Exemplo n.º 2
0
def render(element_html, data):
    if data['panel'] != 'question':
        return ''

    element = lxml.html.fragment_fromstring(element_html)
    uuid = pl.get_uuid()
    raw_file_names = pl.get_string_attrib(element, 'file-names', '')
    file_names = get_file_names_as_array(raw_file_names)
    file_names_json = json.dumps(file_names, allow_nan=False)
    answer_name = get_answer_name(raw_file_names)

    html_params = {'name': answer_name, 'file_names': file_names_json, 'uuid': uuid}

    files = data['submitted_answers'].get('_files', None)
    if files is not None:
        # Filter out any files not part of this element's file_names
        filtered_files = [x for x in files if x.get('name', '') in file_names]
        html_params['has_files'] = True
        html_params['files'] = json.dumps(filtered_files, allow_nan=False)
    else:
        html_params['has_files'] = False

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

    return html
Exemplo n.º 3
0
def render(element_html, data):
    if data['panel'] != 'question':
        return ''

    element = lxml.html.fragment_fromstring(element_html)
    uuid = pl.get_uuid()
    raw_file_names = pl.get_string_attrib(element, 'file-names', '')
    file_names = get_file_names_as_array(raw_file_names)
    file_names_json = json.dumps(file_names, allow_nan=False)
    answer_name = get_answer_name(raw_file_names)

    html_params = {
        'name': answer_name,
        'file_names': file_names_json,
        'uuid': uuid
    }

    files = data['submitted_answers'].get('_files', None)
    if files is not None:
        # Filter out any files not part of this element's file_names
        filtered_files = [x for x in files if x.get('name', '') in file_names]
        html_params['has_files'] = True
        html_params['files'] = json.dumps(filtered_files, allow_nan=False)
    else:
        html_params['has_files'] = False

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

    return html
Exemplo n.º 4
0
def render(element_html, data):
    # Grab the name of the element (name of the hidden input tag), and generate a unique UUID
    # Each element on the page has its own UUID to prevent the JavaScript of other elements from interfering
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    uuid = pl.get_uuid()

    if data['panel'] == 'question':
        html_params = {
            'question': True,
            'number': data['params'][name],
            'answers_name': name,
            'image_url':
            data['options']['client_files_element_url'] + '/block_i.png',
            'uuid': uuid
        }
    elif data['panel'] == 'submission':
        feedback = data['partial_scores'][name].get('feedback', None)
        html_params = {
            'submission': True,
            'submitted': data["raw_submitted_answers"][name],
            'feedback': feedback
        }
    elif data['panel'] == 'answer':
        html_params = {
            'answer': True,
            'correct': data["correct_answers"][name]
        }

    with open('clickable-image.mustache', 'r') as f:
        return chevron.render(f, html_params).strip()
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    script_name = pl.get_string_attrib(element, 'script-name', None)

    with open(os.path.join(data['options']['question_path'], script_name)) as f:
        script = f.read()

    width = pl.get_string_attrib(element, 'width', '500')
    height = pl.get_string_attrib(element, 'height', '300')

    params_names = pl.get_string_attrib(element, 'param-names', None)
    if params_names is None:
        client_params = {}
    else:
        params_names = params_names.split(sep=',')
        client_params = {key: data['params'][key] for key in params_names}

    html_params = {
        'script': script,
        'width': width,
        'height': height,
        'client_params': client_params,
        'uuid': pl.get_uuid(),
    }

    with open('pl-prairiedraw-figure.mustache') as f:
        html = chevron.render(f, html_params).strip()

    return html
def render(element_html, data):

    element = lxml.html.fragment_fromstring(element_html)
    file_name = pl.get_string_attrib(element, 'file-name', '')
    answer_name = get_answer_name(file_name)
    quill_theme = pl.get_string_attrib(element, 'quill-theme', QUILL_THEME_DEFAULT)
    placeholder = pl.get_string_attrib(element, 'placeholder', PLACEHOLDER_DEFAULT)
    uuid = pl.get_uuid()
    source_file_name = pl.get_string_attrib(element, 'source-file-name', SOURCE_FILE_NAME_DEFAULT)
    directory = pl.get_string_attrib(element, 'directory', DIRECTORY_DEFAULT)
    element_text = element_inner_html(element)

    if data['panel'] == 'question' or data['panel'] == 'submission':

        html_params = {
            'name': answer_name,
            'file_name': file_name,
            'quill_theme': quill_theme,
            'placeholder': placeholder,
            'editor_uuid': uuid,
            'question': data['panel'] == 'question',
            'submission': data['panel'] == 'submission',
            'read_only': 'true' if data['panel'] == 'submission' else 'false'
        }

        if source_file_name is not None:
            if directory == 'serverFilesCourse':
                directory = data['options']['server_files_course_path']
            elif directory == 'clientFilesCourse':
                directory = data['options']['client_files_course_path']
            else:
                directory = os.path.join(data['options']['question_path'], directory)
            file_path = os.path.join(directory, source_file_name)
            text_display = open(file_path).read()
        else:
            if element_text is not None:
                text_display = str(element_text)
            else:
                text_display = ''

        html_params['original_file_contents'] = base64.b64encode(text_display.encode('UTF-8').strip()).decode()

        submitted_file_contents = data['submitted_answers'].get(answer_name, None)
        if submitted_file_contents:
            html_params['current_file_contents'] = submitted_file_contents
        else:
            html_params['current_file_contents'] = html_params['original_file_contents']

        html_params['question'] = data['panel'] == 'question'
        with open('pl-rich-text-editor.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()

    elif data['panel'] == 'answer':
        html = ''
    else:
        raise Exception('Invalid panel type: ' + data['panel'])

    return html
def render(element_html, element_index, data):
    if data['panel'] == 'submission':
        html_params = {'submission': True, 'graded': True, 'uuid': pl.get_uuid()}

        feedback = data['feedback']
        html_params['graded'] = bool(feedback)
        grading_succeeded = bool(feedback.get('succeeded', None))
        html_params['grading_succeeded'] = grading_succeeded
        if not grading_succeeded:
            html_params['message'] = feedback.get('message', None)
        else:
            results = feedback.get('results', None)
            if grading_succeeded and results:
                html_params['succeeded'] = bool(results.get('succeeded', None))
                html_params['score'] = format(results.get('score', 0) * 100, '.2f').rstrip('0').rstrip('.')
                html_params['achieved_max_points'] = (results.get('score', 0) >= 1.0)
                html_params['results_color'] = '#4CAF50' if (results.get('score', 0) >= 1.0) else '#F44336'
                html_params['has_message'] = bool(results.get('message', False))
                html_params['message'] = results.get('message', None)
                html_params['has_output'] = bool(results.get('output', False))
                html_params['output'] = results.get('output', None)
                html_params['has_message_or_output'] = bool(html_params['has_message'] or html_params['has_output'])

                results_tests = results.get('tests', None)
                html_params['has_tests'] = bool(results.get('tests', None))
                if results_tests:
                    html_params['points'] = sum(test['points'] for test in results_tests)
                    html_params['max_points'] = sum(test['max_points'] for test in results_tests)

                    # We need to build a new tests array to massage data a bit
                    tests = []
                    for index, results_test in enumerate(results_tests):
                        test = {}
                        test['index'] = index
                        test['name'] = results_test.get('name', '')
                        test['has_message'] = bool(results_test.get('message', None))
                        test['message'] = results_test.get('message', None)
                        test['has_output'] = bool(results_test.get('output', None))
                        test['output'] = results_test.get('output', None)
                        test['max_points'] = results_test.get('max_points')
                        test['points'] = results_test.get('points')
                        correct = test['max_points'] == test['points']
                        test['results_color'] = '#4CAF50' if correct else '#F44336'
                        test['results_icon'] = 'glyphicon-ok' if correct else 'glyphicon-remove'
                        test['has_description'] = bool(results_test.get('description', None))
                        test['description'] = results_test.get('description', None)

                        tests.append(test)

                    html_params['tests'] = tests

        with open('pl_external_grader_results.mustache', 'r') as f:
            html = chevron.render(f, html_params).strip()
    else:
        html = ''

    return html
Exemplo n.º 8
0
def render(element_html, data):
    if data['panel'] != 'question':
        return ''

    element = lxml.html.fragment_fromstring(element_html)
    file_name = pl.get_string_attrib(element, 'file-name', '')
    answer_name = get_answer_name(file_name)
    editor_config_function = pl.get_string_attrib(element,
                                                  'editor-config-function',
                                                  None)
    ace_mode = pl.get_string_attrib(element, 'ace-mode', None)
    ace_theme = pl.get_string_attrib(element, 'ace-theme', None)
    uuid = pl.get_uuid()
    source_file_name = pl.get_string_attrib(element, 'source-file-name', None)

    html_params = {
        'name': answer_name,
        'file_name': file_name,
        'ace_mode': ace_mode,
        'ace_theme': ace_theme,
        'editor_config_function': editor_config_function,
        'uuid': uuid
    }

    if source_file_name is not None:
        file_path = os.path.join(data['options']['question_path'],
                                 source_file_name)
        text_display = open(file_path).read()
    else:
        if element.text is not None:
            text_display = str(element.text)
        else:
            text_display = ''

    html_params['original_file_contents'] = base64.b64encode(
        text_display.encode('UTF-8').strip()).decode()

    submitted_file_contents = data['submitted_answers'].get(answer_name, None)
    if submitted_file_contents:
        html_params['current_file_contents'] = submitted_file_contents
    else:
        html_params['current_file_contents'] = html_params[
            'original_file_contents']

    if data['panel'] == 'question':
        html_params['question'] = True
        with open('pl-file-editor.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()
    else:
        html = ''

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

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

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

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

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

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

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

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

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

    return html
Exemplo n.º 10
0
def render(element_html, data):
    if data['panel'] != 'question':
        return ''

    element = lxml.html.fragment_fromstring(element_html)
    file_name = pl.get_string_attrib(element, 'file-name', '')
    answer_name = get_answer_name(file_name)
    editor_config_function = pl.get_string_attrib(element, 'editor-config-function', None)
    ace_mode = pl.get_string_attrib(element, 'ace-mode', None)
    ace_theme = pl.get_string_attrib(element, 'ace-theme', None)
    uuid = pl.get_uuid()
    source_file_name = pl.get_string_attrib(element, 'source-file-name', None)

    html_params = {
        'name': answer_name,
        'file_name': file_name,
        'ace_mode': ace_mode,
        'ace_theme': ace_theme,
        'editor_config_function': editor_config_function,
        'uuid': uuid
    }

    if source_file_name is not None:
        file_path = os.path.join(data['options']['question_path'], source_file_name)
        text_display = open(file_path).read()
    else:
        if element.text is not None:
            text_display = str(element.text)
        else:
            text_display = ''

    html_params['original_file_contents'] = base64.b64encode(text_display.encode('UTF-8').strip()).decode()

    submitted_file_contents = data['submitted_answers'].get(answer_name, None)
    if submitted_file_contents:
        html_params['current_file_contents'] = submitted_file_contents
    else:
        html_params['current_file_contents'] = html_params['original_file_contents']

    if data['panel'] == 'question':
        html_params['question'] = True
        with open('pl-file-editor.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()
    else:
        html = ''

    return html
Exemplo n.º 11
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    engine = pl.get_string_attrib(element, 'engine', 'dot')

    # Read the contents of this element as the data to render
    # we dump the string to json to ensure that newlines are
    # properly encoded
    graphviz_data = json.dumps(str(element.text))

    html_params = {
        'uuid': pl.get_uuid(),
        'workerURL': '/node_modules/viz.js/full.render.js',
        'data': graphviz_data,
        'engine': engine,
    }

    with open('pl-graphviz-render.mustache') as f:
        html = chevron.render(f, html_params).strip()

    return html
Exemplo n.º 12
0
def render(element_html, data):
    # Get attribs

    element = lxml.html.fragment_fromstring(element_html)
    engine = pl.get_string_attrib(element, 'engine', ENGINE_DEFAULT)
    input_param = pl.get_string_attrib(element, 'params-name-matrix',
                                       PARAMS_NAME_MATRIX_DEFAULT)
    input_label = pl.get_string_attrib(element, 'params-name-labels',
                                       PARAMS_NAME_LABELS_DEFAULT)

    if len(str(element.text)) == 0 and input_param is None:
        raise Exception(
            'No graph source given! Must either define graph in HTML or provide source in params.'
        )

    graphviz_data = None

    if input_param is not None:
        mat = np.array(pl.from_json(data['params'][input_param]))
        label = None
        if input_label is not None:
            label = np.array(pl.from_json(data['params'][input_label]))
        graphviz_data = json.dumps(
            graphviz_from_matrix(mat, label, engine, element))
    else:
        # Read the contents of this element as the data to render
        # we dump the string to json to ensure that newlines are
        # properly encoded
        graphviz_data = json.dumps(str(element.text))

    html_params = {
        'uuid': pl.get_uuid(),
        'workerURL': '/node_modules/viz.js/full.render.js',
        'data': graphviz_data,
        'engine': engine,
    }

    with open('pl-graph.mustache') as f:
        html = chevron.render(f, html_params).strip()

    return html
def render(element_html, element_index, data):
    if data['panel'] != 'question':
        return ''

    element = lxml.html.fragment_fromstring(element_html)
    file_name = pl.get_string_attrib(element, 'file_name', '')
    answer_name = get_answer_name(file_name)
    editor_config_function = pl.get_string_attrib(element,
                                                  'editor_config_function',
                                                  None)
    ace_mode = pl.get_string_attrib(element, 'ace_mode', None)
    ace_theme = pl.get_string_attrib(element, 'ace_theme', None)
    uuid = pl.get_uuid()

    html_params = {
        'name': answer_name,
        'file_name': file_name,
        'ace_mode': ace_mode,
        'ace_theme': ace_theme,
        'editor_config_function': editor_config_function,
        'uuid': uuid
    }

    html_params['original_file_contents'] = base64.b64encode(
        str(element.text).encode('UTF-8').strip() or '').decode()

    submitted_file_contents = data['submitted_answers'].get(answer_name, None)
    if submitted_file_contents:
        html_params['current_file_contents'] = submitted_file_contents
    else:
        html_params['current_file_contents'] = html_params[
            'original_file_contents']

    if data['panel'] == 'question':
        html_params['question'] = True
        with open('pl_file_editor.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()
    else:
        html = ''

    return html
Exemplo n.º 14
0
def render(element_html, data):
    if data['panel'] != 'submission':
        return ''

    html_params = {'uuid': pl.get_uuid()}

    # Fetch the list of required files for this question
    required_file_names = data['params'].get('_required_file_names', [])
    html_params['required_files'] = required_file_names

    # Fetch any submitted files
    submitted_files = data['submitted_answers'].get('_files', [])

    # Pass through format errors from the file input elements
    html_params['errors'] = data['format_errors'].get('_files', [])

    # Decode and reshape files into a useful form
    if len(submitted_files) > 0:
        files = []
        for idx, file in enumerate(submitted_files):
            b64contents = file['contents'] or ''
            try:
                contents = base64.b64decode(b64contents).decode()
            except UnicodeDecodeError:
                contents = 'Unable to decode file.'
            files.append({
                'name': file['name'],
                'contents': contents,
                'contentsb64': b64contents,
                'index': idx
            })
        html_params['has_files'] = True
        html_params['files'] = files
    else:
        html_params['has_files'] = False

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

    return html
Exemplo n.º 15
0
def render(element_html, data):
    # Load all extensions
    extensions = pl.load_all_extensions(data)
    for extension in extensions.values():
        backends = extension.backends
        for name, backend in backends.items():
            matrix_backends[name] = backend

    # Get attribs
    element = lxml.html.fragment_fromstring(element_html)
    engine = pl.get_string_attrib(element, 'engine', ENGINE_DEFAULT)
    input_param = pl.get_string_attrib(element, 'params-name-matrix', PARAMS_NAME_MATRIX_DEFAULT)
    input_type = pl.get_string_attrib(element, 'params-type', PARAMS_TYPE_DEFAULT)

    if len(str(element.text)) == 0 and input_param is None:
        raise Exception('No graph source given! Must either define graph in HTML or provide source in params.')

    if input_param is not None:
        if input_type in matrix_backends:
            graphviz_data = json.dumps(matrix_backends[input_type](element, data))
        else:
            raise Exception(f'Unknown graph type "{input_type}".')
    else:
        # Read the contents of this element as the data to render
        # we dump the string to json to ensure that newlines are
        # properly encoded
        graphviz_data = json.dumps(str(element.text))

    html_params = {
        'uuid': pl.get_uuid(),
        'workerURL': '/node_modules/viz.js/full.render.js',
        'data': graphviz_data,
        'engine': engine,
    }

    with open('pl-graph.mustache') as f:
        html = chevron.render(f, html_params).strip()

    return html
Exemplo n.º 16
0
def render(element_html, data):
    if data['panel'] != 'submission':
        return ''

    html_params = {'uuid': pl.get_uuid()}

    # Fetch the list of required files for this question
    required_file_names = data['params'].get('_required_file_names', [])
    html_params['required_files'] = required_file_names

    # Fetch any submitted files
    submitted_files = data['submitted_answers'].get('_files', [])

    # Pass through format errors from the file input elements
    html_params['errors'] = data['format_errors'].get('_files', [])

    # Decode and reshape files into a useful form
    if len(submitted_files) > 0:
        files = []
        for idx, file in enumerate(submitted_files):
            try:
                contents = base64.b64decode(file['contents'] or '').decode()
            except UnicodeDecodeError:
                contents = 'Unable to decode file.'
            files.append({
                'name': file['name'],
                'contents': contents,
                'index': idx
            })
        html_params['has_files'] = True
        html_params['files'] = files
    else:
        html_params['has_files'] = False

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

    return html
Exemplo n.º 17
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)
    variables_string = pl.get_string_attrib(element, 'variables', None)
    variables = get_variables_list(variables_string)
    display = pl.get_string_attrib(element, 'display', 'inline')
    allow_complex = pl.get_boolean_attrib(element, 'allow-complex', False)
    imaginary_unit = pl.get_string_attrib(element, 'imaginary-unit-for-display', 'i')

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

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

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

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

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

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

    elif data['panel'] == 'submission':
        parse_error = data['format_errors'].get(name, None)
        html_params = {
            'submission': True,
            'label': label,
            'parse_error': parse_error,
            'uuid': pl.get_uuid()
        }
        if parse_error is None:
            a_sub = data['submitted_answers'][name]
            if isinstance(a_sub, str):
                # this is for backward-compatibility
                a_sub = phs.convert_string_to_sympy(a_sub, variables, allow_complex=allow_complex)
            else:
                a_sub = phs.json_to_sympy(a_sub, allow_complex=allow_complex)
            a_sub = a_sub.subs(sympy.I, sympy.Symbol(imaginary_unit))
            html_params['a_sub'] = sympy.latex(a_sub)
        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)

        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)

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

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

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

    return html
Exemplo n.º 18
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')

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

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

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

        # Set up the templating for each answer
        answerset = []
        for answer in answers:
            answer_html = {
                'key': answer['key'],
                'checked': (submitted_key == answer['key']),
                'html': answer['html'],
                'display_score_badge': display_score and submitted_key == answer['key'],
                'display_feedback': submitted_key == answer['key'] and feedback is not None,
                'feedback': feedback
            }
            if answer_html['display_score_badge']:
                answer_html['correct'] = (correct_key == answer['key'])
                answer_html['incorrect'] = (correct_key != answer['key'])
            answerset.append(answer_html)

        html_params = {
            'question': True,
            'inline': inline,
            'name': name,
            'editable': editable,
            'display_score_badge': display_score,
            'answers': answerset,
            'hide_letter_keys': pl.get_boolean_attrib(element, 'hide-letter-keys', HIDE_LETTER_KEYS_DEFAULT)
        }

        # Display the score badge if necessary
        if display_score:
            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-multiple-choice.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,
            'parse_error': parse_error,
            'uuid': pl.get_uuid(),
            'hide_letter_keys': pl.get_boolean_attrib(element, 'hide-letter-keys', HIDE_LETTER_KEYS_DEFAULT)
        }

        if parse_error is None:
            submitted_answer = next(filter(lambda a: a['key'] == submitted_key, answers), None)
            html_params['key'] = submitted_key
            html_params['answer'] = submitted_answer

            partial_score = data['partial_scores'].get(name, {'score': None})
            feedback = partial_score.get('feedback', None)
            score = partial_score.get('score', None)
            if score is not None:
                html_params['display_score_badge'] = True
                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['display_feedback'] = feedback is not None
            html_params['feedback'] = feedback

        with open('pl-multiple-choice.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()
    elif data['panel'] == 'answer':
        correct_answer = data['correct_answers'].get(name, None)

        if correct_answer is None:
            raise ValueError('No true answer.')
        else:
            html_params = {
                'answer': True,
                'answers': correct_answer,
                'key': correct_answer['key'],
                'html': correct_answer['html'],
                'inline': inline,
                'hide_letter_keys': pl.get_boolean_attrib(element, 'hide-letter-keys', HIDE_LETTER_KEYS_DEFAULT)
            }
            with open('pl-multiple-choice.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 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
Exemplo n.º 20
0
def prepare(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    answer_name = pl.get_string_attrib(element, 'answers-name')

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

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

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

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

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

    correct_answers = []
    incorrect_answers = []

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

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

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

        tag = pl.get_string_attrib(html_tags, 'tag', None)
        depends = pl.get_string_attrib(html_tags, 'depends', '')
        depends = depends.strip().split(',') if depends else []

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

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

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

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

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

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

    incorrect_answers_count = random.randint(min_incorrect, max_incorrect)

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

    mcq_options = sampled_correct_answers + sampled_incorrect_answers

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

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

    data['params'][answer_name] = mcq_options
    data['correct_answers'][answer_name] = correct_answers
Exemplo n.º 21
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    partial_credit = pl.get_boolean_attrib(element, 'partial-credit', PARTIAL_CREDIT_DEFAULT)
    partial_credit_method = pl.get_string_attrib(element, 'partial-credit-method', PARTIAL_CREDIT_METHOD_DEFAULT)
    hide_score_badge = pl.get_boolean_attrib(element, 'hide-score-badge', HIDE_SCORE_BADGE_DEFAULT)

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

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

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

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

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

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

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

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

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

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

        html_params = {
            'question': True,
            'name': name,
            'editable': editable,
            'uuid': pl.get_uuid(),
            'info': info,
            'answers': answerset,
            'inline': inline,
            'hide_letter_keys': pl.get_boolean_attrib(element, 'hide-letter-keys', HIDE_LETTER_KEYS_DEFAULT)
        }

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

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

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

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

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

            html_params = {
                'submission': True,
                'display_score_badge': (score is not None),
                'answers': answers,
                'inline': inline,
                'hide_letter_keys': pl.get_boolean_attrib(element, 'hide-letter-keys', HIDE_LETTER_KEYS_DEFAULT)
            }

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

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

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

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

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

    return html
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
Exemplo n.º 23
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    label = pl.get_string_attrib(element, 'label', LABEL_DEFAULT)
    variables_string = pl.get_string_attrib(element, 'variables',
                                            VARIABLES_DEFAULT)
    variables = get_variables_list(variables_string)
    display = pl.get_string_attrib(element, 'display', DISPLAY_DEFAULT)
    allow_complex = pl.get_boolean_attrib(element, 'allow-complex',
                                          ALLOW_COMPLEX_DEFAULT)
    imaginary_unit = pl.get_string_attrib(element,
                                          'imaginary-unit-for-display',
                                          IMAGINARY_UNIT_FOR_DISPLAY_DEFAULT)
    size = pl.get_integer_attrib(element, 'size', SIZE_DEFAULT)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return html
Exemplo n.º 24
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    answer_name = pl.get_string_attrib(element, 'answers-name')
    grading_method = pl.get_string_attrib(element, 'grading-method',
                                          GRADING_METHOD_DEFAULT)

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

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

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

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

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

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

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

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

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

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

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

    elif data['panel'] == 'submission':
        if grading_method == 'external':
            return ''  # external grader is responsible for displaying results screen

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

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

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

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

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

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

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

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

        html_params = {
            'true_answer': True,
            'question_solution': question_solution,
            'ordering_message': ordering_message,
            'indentation_message': indentation_message
        }
        with open('pl-order-blocks.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params)
        return html

    else:
        raise Exception('Invalid panel type')
Exemplo n.º 25
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    digits = pl.get_integer_attrib(element, 'digits', 2)
    show_matlab = pl.get_boolean_attrib(element, 'show-matlab', True)
    show_mathematica = pl.get_boolean_attrib(element, 'show-mathematica', True)
    show_python = pl.get_boolean_attrib(element, 'show-python', True)
    default_tab = pl.get_string_attrib(element, 'default-tab', 'matlab')

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return html
Exemplo 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', LABEL_DEFAULT)
    suffix = pl.get_string_attrib(element, 'suffix', SUFFIX_DEFAULT)
    display = pl.get_string_attrib(element, 'display', DISPLAY_DEFAULT)
    remove_leading_trailing = pl.get_string_attrib(
        element, 'remove-leading-trailing', REMOVE_LEADING_TRAILING_DEFAULT)
    remove_spaces = pl.get_string_attrib(element, 'remove-spaces',
                                         REMOVE_SPACES_DEFAULT)
    placeholder = pl.get_string_attrib(element, 'placeholder',
                                       PLACEHOLDER_DEFAULT)

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

        if remove_leading_trailing:
            if remove_spaces:
                space_hint = 'All spaces will be removed from your answer.'
            else:
                space_hint = 'Leading and trailing spaces will be removed from your answer.'
        else:
            if remove_spaces:
                space_hint = 'All spaces between text will be removed but leading and trailing spaces will be left as part of your answer.'
            else:
                space_hint = 'Leading and trailing spaces will be left as part of your answer.'

        # Get info strings
        info_params = {'format': True, 'space_hint': space_hint}
        with open('pl-string-input.mustache', 'r', encoding='utf-8') as f:
            template = f.read()
            info = chevron.render(template, info_params).strip()
            info_params.pop('format', None)

        html_params = {
            'question':
            True,
            'name':
            name,
            'label':
            label,
            'suffix':
            suffix,
            'remove-leading-trailing':
            remove_leading_trailing,
            'remove-spaces':
            remove_spaces,
            'editable':
            editable,
            'info':
            info,
            'placeholder':
            placeholder,
            'size':
            pl.get_integer_attrib(element, 'size', SIZE_DEFAULT),
            '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)

        html_params['display_append_span'] = html_params['show_info'] 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-string-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)
            a_sub = pl.escape_unicode_string(a_sub)

            html_params['suffix'] = suffix
            html_params['a_sub'] = a_sub
        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-string-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:
            html_params = {
                'answer': True,
                'label': label,
                'a_tru': a_tru,
                'suffix': suffix
            }
            with open('pl-string-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
Exemplo n.º 27
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', 'inline')

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

        # Get info strings
        info_params = {'format': True}
        with open('pl-integer-input.mustache', 'r', encoding='utf-8') as f:
            info = chevron.render(f, info_params).strip()
        with open('pl-integer-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-integer-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'] = '{:d}'.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-integer-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:
            html_params = {'answer': True, 'label': label, 'a_tru': '{:d}'.format(a_tru), 'suffix': suffix}
            with open('pl-integer-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
Exemplo n.º 28
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
def render(element_html, data):
    if data['panel'] == 'submission':
        html_params = {'submission': True, 'graded': True, 'uuid': pl.get_uuid()}

        feedback = data['feedback']
        html_params['graded'] = bool(feedback)
        grading_succeeded = bool(feedback.get('succeeded', None))
        html_params['grading_succeeded'] = grading_succeeded
        if not grading_succeeded:
            html_params['message'] = feedback.get('message', None)
        else:
            results = feedback.get('results', None)
            if grading_succeeded and results:
                html_params['succeeded'] = bool(results.get('succeeded', None))
                html_params['score'] = format(results.get('score', 0) * 100, '.2f').rstrip('0').rstrip('.')
                html_params['achieved_max_points'] = (results.get('score', 0) >= 1.0)
                html_params['results_color'] = '#4CAF50' if (results.get('score', 0) >= 1.0) else '#F44336'
                html_params['has_message'] = bool(results.get('message', False))
                html_params['message'] = results.get('message', None)
                html_params['has_output'] = bool(results.get('output', False))
                html_params['output'] = results.get('output', None)
                html_params['has_message_or_output'] = bool(html_params['has_message'] or html_params['has_output'])

                results_tests = results.get('tests', None)
                html_params['has_tests'] = bool(results.get('tests', None))
                if results_tests:
                    # Let's not assume that people give us a valid array of tests
                    # If any test is missing either points or max_points, we'll
                    # disable detailed scores for all questions
                    tests_missing_points = False
                    for test in results_tests:
                        if test.get('points', None) is None:
                            tests_missing_points = True
                        if test.get('max_points', None) is None:
                            tests_missing_points = True
                    html_params['tests_missing_points'] = tests_missing_points

                    if not tests_missing_points:
                        html_params['points'] = sum(test['points'] for test in results_tests)
                        html_params['max_points'] = sum(test['max_points'] for test in results_tests)

                    # We need to build a new tests array to massage data a bit
                    tests = []
                    for index, results_test in enumerate(results_tests):
                        test = {}
                        test['index'] = index
                        test['name'] = results_test.get('name', '')
                        test['has_message'] = bool(results_test.get('message', None))
                        test['message'] = results_test.get('message', None)
                        test['has_output'] = bool(results_test.get('output', None))
                        test['output'] = results_test.get('output', None)
                        test['has_description'] = bool(results_test.get('description', None))
                        test['description'] = results_test.get('description', None)
                        if not tests_missing_points:
                            test['max_points'] = results_test.get('max_points')
                            test['points'] = results_test.get('points')
                            correct = test['max_points'] == test['points']
                            test['results_color'] = '#4CAF50' if correct else '#F44336'
                            test['results_icon'] = 'fa-check' if correct else 'fa-times'
                        tests.append(test)

                    html_params['tests'] = tests

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

    return html
Exemplo n.º 30
0
def render(element_html, data):
    if data['panel'] != 'question':
        return ''

    element = lxml.html.fragment_fromstring(element_html)
    file_name = pl.get_string_attrib(element, 'file-name', '')
    answer_name = get_answer_name(file_name)
    editor_config_function = pl.get_string_attrib(element,
                                                  'editor-config-function',
                                                  None)
    ace_mode = pl.get_string_attrib(element, 'ace-mode', None)
    ace_theme = pl.get_string_attrib(element, 'ace-theme', None)
    uuid = pl.get_uuid()
    source_file_name = pl.get_string_attrib(element, 'source-file-name', None)
    min_lines = pl.get_integer_attrib(element, 'min-lines', None)
    max_lines = pl.get_integer_attrib(element, 'max-lines', None)
    auto_resize = pl.get_string_attrib(element, 'auto-resize', 'false')

    # If auto_resize is set but min_lines isn't, the height of the
    # file editor area will be set to 1 line. Thus, we need to set
    # a default of about 18 lines to match an editor window without
    # the auto resizing enabled.
    if min_lines is None and auto_resize == 'true':
        min_lines = 18

    html_params = {
        'name': answer_name,
        'file_name': file_name,
        'ace_mode': ace_mode,
        'ace_theme': ace_theme,
        'editor_config_function': editor_config_function,
        'min_lines': min_lines,
        'max_lines': max_lines,
        'auto_resize': auto_resize,
        'uuid': uuid
    }

    if source_file_name is not None:
        file_path = os.path.join(data['options']['question_path'],
                                 source_file_name)
        text_display = open(file_path).read()
    else:
        if element.text is not None:
            text_display = str(element.text)
        else:
            text_display = ''

    html_params['original_file_contents'] = base64.b64encode(
        text_display.encode('UTF-8').strip()).decode()

    submitted_file_contents = data['submitted_answers'].get(answer_name, None)
    if submitted_file_contents:
        html_params['current_file_contents'] = submitted_file_contents
    else:
        html_params['current_file_contents'] = html_params[
            'original_file_contents']

    if data['panel'] == 'question':
        html_params['question'] = True
        with open('pl-file-editor.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()
    else:
        html = ''

    return html
Exemplo n.º 31
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    answer_name = pl.get_string_attrib(element, 'answers-name')

    if data['panel'] == 'question':
        mcq_options = []  # stores MCQ options
        student_previous_submission = []
        submission_indent = []

        for html_tags in element:
            if html_tags.tag == 'pl-answer':
                mcq_options.append(
                    html_tags.text
                )  # store the original specified ordering of all the MCQ options

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

        student_submission_dict_list = []

        mcq_options = data['params'][answer_name]

        if answer_name in data['submitted_answers']:
            student_previous_submission = data['submitted_answers'][
                answer_name]['student_raw_submission']
            mcq_options = list(
                set(mcq_options) - set(student_previous_submission))

        for index, mcq_options_text in enumerate(student_previous_submission):
            # render the answers column (restore the student submission)
            submission_indent = data['submitted_answers'][answer_name][
                'student_answer_indent'][index]
            submission_indent = (int(submission_indent) * 50) + 10
            temp = {'text': mcq_options_text, 'indent': submission_indent}
            student_submission_dict_list.append(dict(temp))

        dropzone_layout = pl.get_string_attrib(element, 'solution-placement',
                                               SOLUTION_PLACEMENT_DEFAULT)

        check_indentation = pl.get_boolean_attrib(element, 'indentation',
                                                  INDENTION_DEFAULT)

        html_params = {
            'question':
            True,
            'answer_name':
            answer_name,
            'options':
            mcq_options,
            'source-header':
            source_header,
            'solution-header':
            solution_header,
            'submission_dict':
            student_submission_dict_list,
            'dropzone_layout':
            'pl-order-blocks-bottom'
            if dropzone_layout == 'bottom' else 'pl-order-blocks-right',
            'check_indentation':
            'enableIndentation' if check_indentation is True else None
        }

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

    elif data['panel'] == 'submission':
        if pl.get_string_attrib(element, 'grading-method',
                                'ordered') == 'external':
            return ''
        # render the submission panel
        uuid = pl.get_uuid()
        student_submission = ''
        color = 'badge-danger'
        score = 0
        feedback = None

        if answer_name in data['submitted_answers']:
            student_submission = data['submitted_answers'][answer_name][
                'student_raw_submission']
        if answer_name in data['partial_scores']:
            color = render_html_color(
                data['partial_scores'][answer_name]['score'])
            score = data['partial_scores'][answer_name]['score'] * 100
            feedback = data['partial_scores'][answer_name]['feedback']

        html_params = {
            'submission': True,
            'uuid': uuid,
            'parse-error': data['format_errors'].get(answer_name, None),
            'student_submission': pretty_print(student_submission),
            'color': color,
            'score': score,
            'perfect_score': True if score == 100 else None,
            'feedback': feedback
        }

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

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

        grading_mode = pl.get_string_attrib(element, 'grading-method',
                                            'ordered')
        grading_mode = 'in any order' if grading_mode == 'unordered' else 'in the specified order'

        check_indentation = pl.get_boolean_attrib(element, 'indentation',
                                                  INDENTION_DEFAULT)
        check_indentation = ', with correct indentation' if check_indentation is True else None

        if answer_name in data['correct_answers']:
            html_params = {
                'true_answer':
                True,
                'question_solution':
                pretty_print(
                    data['correct_answers'][answer_name]['correct_answers']),
                'grading_mode':
                grading_mode,
                'check_indentation':
                check_indentation
            }
            with open('pl-order-blocks.mustache', 'r', encoding='utf-8') as f:
                html = chevron.render(f, html_params)
            return html
        else:
            return ''
Exemplo n.º 32
0
def render(element_html, data):
    if data['panel'] != 'question':
        return ''

    element = lxml.html.fragment_fromstring(element_html)
    file_name = pl.get_string_attrib(element, 'file-name', '')
    answer_name = get_answer_name(file_name)
    editor_config_function = pl.get_string_attrib(
        element, 'editor-config-function', EDITOR_CONFIG_FUNCTION_DEFAULT)
    ace_mode = pl.get_string_attrib(element, 'ace-mode', ACE_MODE_DEFAULT)
    ace_theme = pl.get_string_attrib(element, 'ace-theme', ACE_THEME_DEFAULT)
    uuid = pl.get_uuid()
    source_file_name = pl.get_string_attrib(element, 'source-file-name',
                                            SOURCE_FILE_NAME_DEFAULT)
    min_lines = pl.get_integer_attrib(element, 'min-lines', MIN_LINES_DEFAULT)
    max_lines = pl.get_integer_attrib(element, 'max-lines', MAX_LINES_DEFAULT)
    auto_resize = pl.get_boolean_attrib(element, 'auto-resize',
                                        AUTO_RESIZE_DEFAULT)
    preview = pl.get_string_attrib(element, 'preview', PREVIEW_DEFAULT)
    focus = pl.get_boolean_attrib(element, 'focus', FOCUS_DEFAULT)

    # stringify boolean attributes (needed when written to html_params)
    auto_resize = 'true' if auto_resize else 'false'
    focus = 'true' if focus else 'false'

    # If auto_resize is set but min_lines isn't, the height of the
    # file editor area will be set to 1 line. Thus, we need to set
    # a default of about 18 lines to match an editor window without
    # the auto resizing enabled.
    if min_lines is None and auto_resize == 'true':
        min_lines = 18

    html_params = {
        'name': answer_name,
        'file_name': file_name,
        'ace_mode': ace_mode,
        'ace_theme': ace_theme,
        'editor_config_function': editor_config_function,
        'min_lines': min_lines,
        'max_lines': max_lines,
        'auto_resize': auto_resize,
        'preview': preview,
        'uuid': uuid,
        'focus': focus
    }

    if source_file_name is not None:
        file_path = os.path.join(data['options']['question_path'],
                                 source_file_name)
        text_display = open(file_path).read()
    else:
        if element.text is not None:
            text_display = str(element.text)
        else:
            text_display = ''

    html_params['original_file_contents'] = base64.b64encode(
        text_display.encode('UTF-8').strip()).decode()

    submitted_file_contents = data['submitted_answers'].get(answer_name, None)
    if submitted_file_contents:
        html_params['current_file_contents'] = submitted_file_contents
    else:
        html_params['current_file_contents'] = html_params[
            'original_file_contents']

    if data['panel'] == 'question':
        html_params['question'] = True
        with open('pl-file-editor.mustache', 'r', encoding='utf-8') as f:
            html = chevron.render(f, html_params).strip()
    else:
        html = ''

    return html
Exemplo n.º 33
0
def get_graph_info(html_tags):
    tag = pl.get_string_attrib(html_tags, 'tag', pl.get_uuid()).strip()
    depends = pl.get_string_attrib(html_tags, 'depends', '')
    depends = [tag.strip() for tag in depends.split(',')] if depends else []
    return tag, depends
Exemplo n.º 34
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)
    variables_string = pl.get_string_attrib(element, 'variables', None)
    variables = get_variables_list(variables_string)
    display = pl.get_string_attrib(element, 'display', 'inline')
    allow_complex = pl.get_boolean_attrib(element, 'allow-complex', False)
    imaginary_unit = pl.get_string_attrib(element, 'imaginary-unit-for-display', 'i')

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

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

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

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

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

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

    elif data['panel'] == 'submission':
        parse_error = data['format_errors'].get(name, None)
        html_params = {
            'submission': True,
            'label': label,
            'parse_error': parse_error,
            'uuid': pl.get_uuid()
        }
        if parse_error is None:
            a_sub = data['submitted_answers'][name]
            if isinstance(a_sub, str):
                # this is for backward-compatibility
                a_sub = phs.convert_string_to_sympy(a_sub, variables, allow_complex=allow_complex)
            else:
                a_sub = phs.json_to_sympy(a_sub, allow_complex=allow_complex)
            a_sub = a_sub.subs(sympy.I, sympy.Symbol(imaginary_unit))
            html_params['a_sub'] = sympy.latex(a_sub)
        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)

        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)

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

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

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

    return html
Exemplo n.º 35
0
def prepare(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    answer_name = pl.get_string_attrib(element, 'answers-name')

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

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

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

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

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

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

    correct_answers = []
    incorrect_answers = []

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

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

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

        tag, depends = get_graph_info(html_tags)
        if grading_method == 'ranking':
            tag = str(index)

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

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

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

            group_tag, group_depends = get_graph_info(html_tags)
            for grouped_tag in html_tags:
                if html_tags.tag is etree.Comment:
                    continue
                else:
                    prepare_tag(grouped_tag, index, {
                        'tag': group_tag,
                        'depends': group_depends
                    })
                    index += 1
        else:
            prepare_tag(html_tags, index)
            index += 1

    if grading_method != 'external' and len(correct_answers) == 0:
        raise Exception(
            'There are no correct answers specified for this question.')

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

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

    incorrect_answers_count = random.randint(min_incorrect, max_incorrect)

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

    mcq_options = sampled_correct_answers + sampled_incorrect_answers

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

    for option in mcq_options:
        option['uuid'] = pl.get_uuid()

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

    # if the order of the blocks in the HTML is a correct solution, leave it unchanged, but if it
    # isn't we need to change it into a solution before displaying it as such
    data_copy = deepcopy(data)
    data_copy['submitted_answers'] = {answer_name: correct_answers}
    data_copy['partial_scores'] = {}
    grade(element_html, data_copy)
    if data_copy['partial_scores'][answer_name]['score'] != 1:
        data['correct_answers'][answer_name] = solve_problem(
            correct_answers, grading_method)
Exemplo n.º 36
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
Exemplo n.º 37
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
Exemplo n.º 38
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', 'inline')
    remove_leading_trailing = pl.get_string_attrib(element,
                                                   'remove-leading-trailing',
                                                   False)
    remove_spaces = pl.get_string_attrib(element, 'remove-spaces', False)
    placeholder = pl.get_string_attrib(element, 'placeholder', None)

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

        # Get info strings
        info_params = {'format': True}
        with open('pl-string-input.mustache', 'r', encoding='utf-8') as f:
            template = f.read()
            info = chevron.render(template, info_params).strip()
            info_params.pop('format', None)

        html_params = {
            'question': True,
            'name': name,
            'label': label,
            'suffix': suffix,
            'remove-leading-trailing': remove_leading_trailing,
            'remove-spaces': remove_spaces,
            'editable': editable,
            'info': info,
            'placeholder': placeholder,
            '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-string-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'] = 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-string-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:
            html_params = {
                'answer': True,
                'label': label,
                'a_tru': a_tru,
                'suffix': suffix
            }
            with open('pl-string-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
Exemplo n.º 39
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
Exemplo n.º 40
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
Exemplo n.º 41
0
def render(element_html, data):
    if data['panel'] == 'submission':
        html_params = {
            'submission': True,
            'graded': True,
            'uuid': pl.get_uuid()
        }

        feedback = data['feedback']
        html_params['graded'] = bool(feedback)
        grading_succeeded = bool(feedback.get('succeeded', None))
        html_params['grading_succeeded'] = grading_succeeded
        if not grading_succeeded:
            html_params['message'] = feedback.get('message', None)
        else:
            results = feedback.get('results', None)
            if grading_succeeded and results:
                html_params['succeeded'] = bool(results.get('succeeded', None))
                html_params['score'] = format(
                    results.get('score', 0) * 100,
                    '.2f').rstrip('0').rstrip('.')
                html_params['achieved_max_points'] = (results.get('score', 0)
                                                      >= 1.0)
                html_params['results_color'] = '#4CAF50' if (
                    results.get('score', 0) >= 1.0) else '#F44336'
                html_params['has_message'] = bool(results.get(
                    'message', False))
                html_params['message'] = results.get('message', None)
                html_params['has_output'] = bool(results.get('output', False))
                html_params['output'] = results.get('output', None)
                html_params['has_message_or_output'] = bool(
                    html_params['has_message'] or html_params['has_output'])

                results_tests = results.get('tests', None)
                html_params['has_tests'] = bool(results.get('tests', None))
                if results_tests:
                    # Let's not assume that people give us a valid array of tests
                    # If any test is missing either points or max_points, we'll
                    # disable detailed scores for all questions
                    tests_missing_points = False
                    for test in results_tests:
                        if test.get('points', None) is None:
                            tests_missing_points = True
                        if test.get('max_points', None) is None:
                            tests_missing_points = True
                    html_params['tests_missing_points'] = tests_missing_points

                    if not tests_missing_points:
                        html_params['points'] = sum(test['points']
                                                    for test in results_tests)
                        html_params['max_points'] = sum(
                            test['max_points'] for test in results_tests)

                    # We need to build a new tests array to massage data a bit
                    tests = []
                    for index, results_test in enumerate(results_tests):
                        test = {}
                        test['index'] = index
                        test['name'] = results_test.get('name', '')
                        test['has_message'] = bool(
                            results_test.get('message', None))
                        test['message'] = results_test.get('message', None)
                        test['has_output'] = bool(
                            results_test.get('output', None))
                        test['output'] = results_test.get('output', None)
                        test['has_description'] = bool(
                            results_test.get('description', None))
                        test['description'] = results_test.get(
                            'description', None)
                        if not tests_missing_points:
                            test['max_points'] = results_test.get('max_points')
                            test['points'] = results_test.get('points')
                            correct = test['max_points'] == test['points']
                            test[
                                'results_color'] = '#4CAF50' if correct else '#F44336'
                            test[
                                'results_icon'] = 'fa-check' if correct else 'fa-times'
                        tests.append(test)

                    html_params['tests'] = tests

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

    return html