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

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

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

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

    if result == 'correct':
        data['raw_submitted_answers'][name] = a_tru
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    elif result == 'incorrect':
        data['raw_submitted_answers'][name] = a_tru + str((random.randint(1, 11) * random.choice([-1, 1])))
        data['partial_scores'][name] = {'score': 0, 'weight': weight}
    elif result == 'invalid':
        data['raw_submitted_answers'][name] = ''
        data['format_errors'][name] = 'invalid'
    else:
        raise Exception('invalid result: %s' % result)
Exemplo n.º 2
0
def prepare(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    required_attribs = []
    optional_attribs = [
        'source-file-name', 'submitted-file-name', 'contents', 'language'
    ]
    pl.check_attribs(element, required_attribs, optional_attribs)

    source_file_name = pl.get_string_attrib(element, 'source-file-name',
                                            SOURCE_FILE_NAME_DEFAULT)
    submitted_file_name = pl.get_string_attrib(element, 'submitted-file-name',
                                               SUBMITTED_FILE_NAME_DEFAULT)
    contents = pl.get_string_attrib(element, 'contents', CONTENTS_DEFAULT)
    language = pl.get_string_attrib(element, 'language', LANGUAGE_DEFAULT)

    if (source_file_name is not None and
        (submitted_file_name is not None or contents is not None)) or (
            submitted_file_name is not None and contents is not None):
        raise Exception(
            'Only one of the attributes "source-file-name", "submitted-file-name" and "contents" can be used.'
        )

    if language not in ['html', 'markdown']:
        raise Exception(
            'Attribute "language" must be either "html" or "markdown".')
Exemplo n.º 3
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    force_text = pl.get_boolean_attrib(element, 'text', TEXT_DEFAULT)
    varname = pl.get_string_attrib(element, 'params-name')

    if varname not in data['params']:
        raise Exception('Could not find {} in params!'.format(varname))

    var_out = data['params'][varname]
    html = ''
    var_type = 'text'

    # determine the type of variable to render
    if isinstance(var_out, dict) and '_type' in var_out:
        if not force_text:
            var_type = var_out['_type']
        var_out = pl.from_json(var_out)

    # render the output variable
    if var_type == 'dataframe':
        html += var_out.to_html(
            classes=['pl-python-variable-table']
        ) + '<p class="pl-python-variable-table-dimensions">{} rows x {} columns</p><br>'.format(
            str(var_out.shape[0]), str(var_out.shape[1]))
    else:
        no_highlight = pl.get_boolean_attrib(element, 'no-highlight',
                                             NO_HIGHLIGHT_DEFAULT)
        prefix = pl.get_string_attrib(element, 'prefix', PREFIX_DEFAULT)
        suffix = pl.get_string_attrib(element, 'suffix', SUFFIX_DEFAULT)

        text = prefix + repr(var_out) + suffix
        html += '<pl-code language="python" no-highlight="{}">{}</pl-code>'.format(
            no_highlight, text)

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

    answer_raw_name = answer_name + '-input'
    student_answer = ''

    if answer_raw_name in data['raw_submitted_answers']:
        student_answer = data['raw_submitted_answers'][answer_raw_name]
    if student_answer is None or student_answer == '':
        data['format_errors'][answer_name] = 'No answer was submitted.'
        return

    grading_mode = pl.get_string_attrib(element, 'grading-method',
                                        GRADING_METHOD_DEFAULT)
    student_answer = json.loads(student_answer)
    correct_answers = data['correct_answers'][answer_name]

    if grading_mode == 'ranking':
        for answer in student_answer:
            search = next((item for item in correct_answers
                           if item['inner_html'] == answer['inner_html']),
                          None)
            answer['ranking'] = search[
                'ranking'] if search is not None else -1  # wrong answers have no ranking
    elif grading_mode == 'dag':
        for answer in student_answer:
            search = next((item for item in correct_answers
                           if item['inner_html'] == answer['inner_html']),
                          None)
            answer['tag'] = search['tag'] if search is not None else None

    if pl.get_string_attrib(element, 'grading-method',
                            'ordered') == 'external':
        for html_tags in element:
            if html_tags.tag == 'pl-answer':
                pl.check_attribs(html_tags,
                                 required_attribs=[],
                                 optional_attribs=[])
        file_name = pl.get_string_attrib(element, 'file-name',
                                         FILE_NAME_DEFAULT)

        answer_code = ''
        for index, answer in enumerate(student_answer):
            indent = int(answer['indent'])
            answer_code += ('    ' * indent) + answer['inner_html'] + '\n'

        if len(answer_code) == 0:
            data['format_errors']['_files'] = 'The submitted file was empty.'
        else:
            data['submitted_answers']['_files'] = [{
                'name':
                file_name,
                'contents':
                base64.b64encode(answer_code.encode('utf-8')).decode('utf-8')
            }]

    data['submitted_answers'][answer_name] = student_answer
    if answer_raw_name in data['submitted_answers']:
        del data['submitted_answers'][answer_raw_name]
Exemplo n.º 5
0
def parse(element_html, element_index, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers_name')
    variables = get_variables_list(
        pl.get_string_attrib(element, 'variables', None))

    # Get submitted answer or return parse_error if it does not exist
    a_sub = data['submitted_answers'].get(name, None)
    if not a_sub:
        data['format_errors'][name] = 'No submitted answer.'
        data['submitted_answers'][name] = None
        return data

    try:
        # Replace '^' with '**' wherever it appears. In MATLAB, either can be used
        # for exponentiation. In python, only the latter can be used.
        a_sub = a_sub.replace('^', '**')

        # Convert submitted answer safely to sympy
        a_sub = convert_string_to_sympy(a_sub, variables)

        # Store result as a string.
        data['submitted_answers'][name] = str(a_sub)
    except:
        data['format_errors'][name] = 'Invalid format.'
        data['submitted_answers'][name] = None
        return data

    return data
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.º 7
0
def parse(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    # Get allow-blank option
    allow_blank = pl.get_string_attrib(element, 'allow-blank',
                                       ALLOW_BLANK_DEFAULT)
    normalize_to_ascii = pl.get_boolean_attrib(element, 'normalize-to-ascii',
                                               NORMALIZE_TO_ASCII_DEFAULT)

    # Get submitted answer or return parse_error if it does not exist
    a_sub = data['submitted_answers'].get(name, None)
    if a_sub is None:
        data['format_errors'][name] = 'No submitted answer.'
        data['submitted_answers'][name] = None
        return

    if normalize_to_ascii:
        a_sub = unidecode(a_sub)
        data['submitted_answers'][name] = a_sub

    if not a_sub and not allow_blank:
        data['format_errors'][
            name] = 'Invalid format. The submitted answer was left blank.'
        data['submitted_answers'][name] = None
    else:
        data['submitted_answers'][name] = pl.to_json(a_sub)
Exemplo n.º 8
0
def grade(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')

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

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

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

    # Get string case sensitivity option
    ignore_case = pl.get_string_attrib(element, 'ignore-case',
                                       IGNORE_CASE_DEFAULT)

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

    # explicitly cast the true answer to a string, to handle the case where the answer might be a number or some other type
    a_tru = str(a_tru)

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

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

    # explicitly cast the submitted answer to a string
    a_sub = str(a_sub)

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

    # Remove the blank spaces between characters
    if (remove_spaces):
        a_sub = ''.join(a_sub.split())
        a_tru = ''.join(a_tru.split())

    # Modify string case for submission and true answer to be lower.
    if (ignore_case):
        a_sub = a_sub.lower()
        a_tru = a_tru.lower()

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

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

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

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

    if result == 'correct':
        data['raw_submitted_answers'][name] = a_tru
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    elif result == 'incorrect':
        data['raw_submitted_answers'][name] = a_tru + str(
            (random.randint(1, 11) * random.choice([-1, 1])))
        data['partial_scores'][name] = {'score': 0, 'weight': weight}
    elif result == 'invalid':
        data['raw_submitted_answers'][name] = ''
        data['format_errors'][name] = 'invalid'
    else:
        raise Exception('invalid result: %s' % result)
Exemplo n.º 10
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.º 11
0
def grade(element_html, element_index, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers_name')

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

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

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

    # Parse both correct and submitted answer (will throw an error on fail).
    variables = get_variables_list(
        pl.get_string_attrib(element, 'variables', None))
    if isinstance(a_tru, str):
        a_tru = convert_string_to_sympy(a_tru, variables)
    a_sub = convert_string_to_sympy(a_sub, variables)

    # Check equality
    correct = a_tru.equals(a_sub)

    if correct:
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    else:
        data['partial_scores'][name] = {'score': 0, 'weight': weight}

    return data
Exemplo n.º 12
0
def prepare(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    required_attribs = []
    optional_attribs = [
        'language', 'no-highlight', 'source-file-name', 'prevent-select',
        'highlight-lines', 'highlight-lines-color'
    ]
    pl.check_attribs(element, required_attribs, optional_attribs)

    language = pl.get_string_attrib(element, 'language', LANGUAGE_DEFAULT)
    if language is not None:
        lexer = get_lexer_by_name(language)
        if lexer is None:
            allowed_languages = map(lambda tup: tup[1][0],
                                    pygments.lexers.get_all_lexers())
            raise Exception(
                f'Unknown language: "{language}". Must be one of {", ".join(allowed_languages)}'
            )

    source_file_name = pl.get_string_attrib(element, 'source-file-name',
                                            SOURCE_FILE_NAME_DEFAULT)
    if source_file_name is not None:
        if element.text is not None and not str(element.text).isspace():
            raise Exception(
                'Existing code cannot be added inside html element when "source-file-name" attribute is used.'
            )

    highlight_lines = pl.get_string_attrib(element, 'highlight-lines',
                                           HIGHLIGHT_LINES_DEFAULT)
    if highlight_lines is not None:
        if parse_highlight_lines(highlight_lines) is None:
            raise Exception(
                'Could not parse highlight-lines attribute; check your syntax')
Exemplo n.º 13
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    language = pl.get_string_attrib(element, 'language', LANGUAGE_DEFAULT)
    no_highlight = pl.get_boolean_attrib(element, 'no-highlight',
                                         NO_HIGHLIGHT_DEFAULT)
    specify_language = (language is not None) and (not no_highlight)
    source_file_name = pl.get_string_attrib(element, 'source-file-name',
                                            SOURCE_FILE_NAME_DEFAULT)
    prevent_select = pl.get_boolean_attrib(element, 'prevent-select',
                                           PREVENT_SELECT_DEFAULT)
    highlight_lines = pl.get_string_attrib(element, 'highlight-lines',
                                           HIGHLIGHT_LINES_DEFAULT)
    highlight_lines_color = pl.get_string_attrib(
        element, 'highlight-lines-color', HIGHLIGHT_LINES_COLOR_DEFAULT)

    if source_file_name is not None:
        base_path = data['options']['question_path']
        file_path = os.path.join(base_path, source_file_name)
        if not os.path.exists(file_path):
            raise Exception(f'Unknown file path: "{file_path}".')
        f = open(file_path, 'r')
        code = ''
        for line in f.readlines():
            code += line
        code = code[:-1]
        f.close()
        # Automatically escape code in file source (important for: html/xml).
        code = escape(code)
    else:
        # Strip a single leading newline from the code, if present. This
        # avoids having spurious newlines because of HTML like:
        #
        # <pl-code>
        # some_code
        # </pl-code>
        #
        # which technically starts with a newline, but we probably
        # don't want a blank line at the start of the code block.
        code = pl.inner_html(element)
        if len(code) > 1 and code[0] == '\r' and code[1] == '\n':
            code = code[2:]
        elif len(code) > 0 and (code[0] == '\n' or code[0] == '\r'):
            code = code[1:]

    if highlight_lines is not None:
        code = highlight_lines_in_code(code, highlight_lines,
                                       highlight_lines_color)

    html_params = {
        'specify_language': specify_language,
        'language': language,
        'no_highlight': no_highlight,
        'code': code,
        'prevent_select': prevent_select,
    }

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

    return html
Exemplo n.º 14
0
def prepare(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    required_attribs = ['answers-name']
    optional_attribs = [
        'weight', 'correct-answer', 'label', 'suffix', 'display', 'size',
        'show-help-text', 'base', 'allow-blank', 'blank-value'
    ]

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

    if base != 0 and (base < 2 or base > 36):
        raise Exception('Base must be either 0, or between 2 and 36')

    correct_answer = pl.get_string_attrib(element, 'correct-answer',
                                          CORRECT_ANSWER_DEFAULT)
    if correct_answer is not None:
        if name in data['correct_answers']:
            raise Exception('duplicate correct_answers variable name: %s' %
                            name)
        # Test conversion, but leave as string so proper value is shown on answer panel
        if pl.string_to_integer(correct_answer, base) is None:
            raise Exception('correct answer is not a valid input: %s' % name)
        data['correct_answers'][name] = correct_answer
Exemplo n.º 15
0
def grade(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    weight = pl.get_integer_attrib(element, 'weight', WEIGHT_DEFAULT)
    partial_credit = pl.get_boolean_attrib(element, 'partial-credit', PARTIAL_CREDIT_DEFAULT)
    number_answers = len(data['params'][name])
    partial_credit_method = pl.get_string_attrib(element, 'partial-credit-method', PARTIAL_CREDIT_METHOD_DEFAULT)

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

    submittedSet = set(submitted_keys)
    correctSet = set(correct_keys)

    score = 0
    if not partial_credit and submittedSet == correctSet:
        score = 1
    elif partial_credit:
        if partial_credit_method == 'PC':
            if submittedSet == correctSet:
                score = 1
            else:
                n_correct_answers = len(correctSet) - len(correctSet - submittedSet)
                points = n_correct_answers - len(submittedSet - correctSet)
                score = max(0, points / len(correctSet))
        else:  # this is the default EDC method
            number_wrong = len(submittedSet - correctSet) + len(correctSet - submittedSet)
            score = 1 - 1.0 * number_wrong / number_answers

    data['partial_scores'][name] = {'score': score, 'weight': weight}
Exemplo n.º 16
0
def test(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    weight = pl.get_integer_attrib(element, 'weight', WEIGHT_DEFAULT)
    allow_blank = pl.get_string_attrib(element, 'allow-blank',
                                       ALLOW_BLANK_DEFAULT)

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

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

    result = data['test_type']
    if result == 'invalid' and allow_blank:
        # We can't have an invalid submission with allow_blank, so just test correct
        result = 'correct'

    if result == 'correct':
        data['raw_submitted_answers'][name] = a_tru
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    elif result == 'incorrect':
        data['raw_submitted_answers'][name] = a_tru + str(
            (random.randint(1, 11) * random.choice([-1, 1])))
        data['partial_scores'][name] = {'score': 0, 'weight': weight}
    elif result == 'invalid':
        data['raw_submitted_answers'][name] = ''
        data['format_errors'][name] = 'invalid'
    else:
        raise Exception('invalid result: %s' % result)
Exemplo n.º 17
0
def prepare(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    required_attribs = ['file-name']
    optional_attribs = [
        'ace-mode', 'ace-theme', 'editor-config-function', 'source-file-name',
        'min-lines', 'max-lines', 'auto-resize', 'preview', 'focus',
        'directory', 'normalize-to-ascii'
    ]
    pl.check_attribs(element, required_attribs, optional_attribs)
    source_file_name = pl.get_string_attrib(element, 'source-file-name',
                                            SOURCE_FILE_NAME_DEFAULT)

    file_name = pl.get_string_attrib(element, 'file-name')
    if '_required_file_names' not in data['params']:
        data['params']['_required_file_names'] = []
    elif file_name in data['params']['_required_file_names']:
        raise Exception(
            'There is more than one file editor with the same file name.')
    data['params']['_required_file_names'].append(file_name)

    if source_file_name is not None:
        if element.text is not None and not str(element.text).isspace():
            raise Exception(
                'Existing code cannot be added inside html element when "source-file-name" attribute is used.'
            )
Exemplo n.º 18
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)

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

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

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

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

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

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

    return html
Exemplo n.º 19
0
def prepare(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    num_backgrounds = 0
    for child in element:
        if isinstance(child, lxml.html.HtmlComment):
            continue

        if child.tag == 'pl-location':
            pl.check_attribs(child, required_attribs=[], optional_attribs=['left', 'right', 'top', 'bottom', 'valign', 'halign'])

            if ('left' not in child.attrib and 'right' not in child.attrib) or ('left' in child.attrib and 'right' in child.attrib):
                raise ValueError('pl-location requires exactly one of "left" or "right" attributes.')

            if ('top' not in child.attrib and 'bottom' not in child.attrib) or ('top' in child.attrib and 'bottom' in child.attrib):
                raise ValueError('pl-location requires exactly one of "top" or "bottom" attributes.')

            valign = pl.get_string_attrib(child, 'valign', VALIGN_DEFAULT)
            if valign not in VALIGN_VALUES:
                raise ValueError(f'Unknown vertical alignment "{valign}"')

            halign = pl.get_string_attrib(child, 'halign', HALIGN_DEFAULT)
            if halign not in HALIGN_VALUES:
                raise ValueError(f'Unknown horizontal alignment "{halign}"')
        elif child.tag == 'pl-background':
            pl.check_attribs(child, required_attribs=[], optional_attribs=[])
            num_backgrounds += 1
        else:
            raise ValueError(f'Unknown tag "{child.tag}" found as child of pl-overlay')

    if num_backgrounds == 0:
        pl.check_attribs(element, required_attribs=['width', 'height'], optional_attribs=['clip'])
    elif num_backgrounds == 1:
        pl.check_attribs(element, required_attribs=[], optional_attribs=['clip', 'width', 'height'])
    else:
        raise ValueError(f'pl-overlay can have at most one <pl-background> child, found {num_backgrounds}.')
Exemplo n.º 20
0
def prepare(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    required_attribs = ['file-name']
    optional_attribs = [
        'quill-theme', 'source-file-name', 'directory', 'placeholder',
        'format', 'markdown-shortcuts'
    ]
    pl.check_attribs(element, required_attribs, optional_attribs)
    source_file_name = pl.get_string_attrib(element, 'source-file-name',
                                            SOURCE_FILE_NAME_DEFAULT)
    output_format = pl.get_string_attrib(element, 'format', FORMAT_DEFAULT)
    element_text = element_inner_html(element)

    file_name = pl.get_string_attrib(element, 'file-name')
    if '_required_file_names' not in data['params']:
        data['params']['_required_file_names'] = []
    elif file_name in data['params']['_required_file_names']:
        raise Exception(
            'There is more than one file editor with the same file name.')
    data['params']['_required_file_names'].append(file_name)

    if source_file_name is not None:
        if element_text and not str(element_text).isspace():
            raise Exception(
                'Existing text cannot be added inside rich-text element when "source-file-name" attribute is used.'
                + element_text)

    if output_format not in ('html', 'markdown'):
        raise Exception(
            f'Invalid output format "{output_format}". Must be either "html" or "markdown".'
        )
Exemplo n.º 21
0
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
Exemplo n.º 22
0
def test(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    weight = pl.get_integer_attrib(element, 'weight', WEIGHT_DEFAULT)
    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)

    correct_answer_list = data['correct_answers'].get(name, [])
    correct_keys = [answer['key'] for answer in correct_answer_list]
    number_answers = len(data['params'][name])
    all_keys = [pl.index2key(i) for i in range(number_answers)]

    result = data['test_type']

    if result == 'correct':
        if len(correct_keys) == 1:
            data['raw_submitted_answers'][name] = correct_keys[0]
        elif len(correct_keys) > 1:
            data['raw_submitted_answers'][name] = correct_keys
        else:
            pass  # no raw_submitted_answer if no correct keys
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    elif result == 'incorrect':
        while True:
            # select answer keys at random
            ans = [k for k in all_keys if random.choice([True, False])]
            # break and use this choice if it isn't correct
            if (len(ans) >= 1):
                if set(ans) != set(correct_keys):
                    if not pl.get_boolean_attrib(element, 'detailed-help-text', DETAILED_HELP_TEXT_DEFAULT):
                        break
                    else:
                        min_correct = pl.get_integer_attrib(element, 'min-correct', 1)
                        max_correct = pl.get_integer_attrib(element, 'max-correct', len(correct_answer_list))
                        if len(ans) <= max_correct and len(ans) >= min_correct:
                            break
        if partial_credit:
            if partial_credit_method == 'PC':
                if set(ans) == set(correct_keys):
                    score = 1
                else:
                    n_correct_answers = len(set(correct_keys)) - len(set(correct_keys) - set(ans))
                    points = n_correct_answers - len(set(ans) - set(correct_keys))
                    score = max(0, points / len(set(correct_keys)))
            else:  # this is the EDC method
                number_wrong = len(set(ans) - set(correct_keys)) + len(set(correct_keys) - set(ans))
                score = 1 - 1.0 * number_wrong / number_answers
        else:
            score = 0
        data['raw_submitted_answers'][name] = ans
        data['partial_scores'][name] = {'score': score, 'weight': weight}
    elif result == 'invalid':
        # FIXME: add more invalid examples
        data['raw_submitted_answers'][name] = None
        data['format_errors'][name] = 'You must select at least one option.'
    else:
        raise Exception('invalid result: %s' % result)
Exemplo n.º 23
0
def grade(element_html, element_index, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers_name')

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

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

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

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

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

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

    if correct:
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    else:
        data['partial_scores'][name] = {'score': 0, 'weight': weight}

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

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

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

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

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

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

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

    if correct:
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    else:
        data['partial_scores'][name] = {'score': 0, 'weight': weight}
Exemplo n.º 25
0
def grade(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    answer_name = pl.get_string_attrib(element, 'answer-name')

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    if correct:
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    else:
        data['partial_scores'][name] = {'score': 0, 'weight': weight}
Exemplo n.º 28
0
def graphviz_from_adj_matrix(element, data):
    # Get matrix attributes

    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)
    mat = np.array(pl.from_json(data['params'][input_param]))
    show_weights = pl.get_boolean_attrib(element, 'weights', WEIGHTS_DEFAULT)  # by default display weights for stochastic matrices
    digits = pl.get_integer_attrib(element, 'weights-digits', WEIGHTS_DIGITS_DEFAULT)  # if displaying weights how many digits to round to
    presentation_type = pl.get_string_attrib(element, 'weights-presentation-type', WEIGHTS_PRESENTATION_TYPE_DEFAULT).lower()

    label = None
    if input_label is not None:
        label = np.array(pl.from_json(data['params'][input_label]))

    # Sanity checking

    if (mat.shape[0] != mat.shape[1]):
        raise Exception(f'Non-square adjacency matrix of size ({mat.shape[0]}, {mat.shape[1]}) given as input.')

    if label is not None:
        mat_label = label
        if (mat_label.shape[0] != mat.shape[0]):
            raise Exception(f'Dimension of the label ({mat_label.shape[0]}) is not consistent with the dimension of the matrix ({mat.shape[0]})')
    else:
        mat_label = range(mat.shape[1])

    # Auto detect showing weights if any of the weights are not 1 or 0

    if show_weights is None:
        all_ones = True
        for x in mat.flatten():
            if x != 1 and x != 0:
                all_ones = False
        show_weights = not all_ones

    # Create pygraphviz graph representation

    G = pygraphviz.AGraph(directed=True)

    for node in mat_label:
        G.add_node(node)

    for i, out_node in enumerate(mat_label):
        for j, in_node in enumerate(mat_label):
            x = mat[j, i]
            if (x > 0):
                if (show_weights):
                    G.add_edge(out_node, in_node, label=pl.string_from_2darray(x, presentation_type=presentation_type, digits=digits))
                else:
                    G.add_edge(out_node, in_node)

    G.layout(engine)
    return G.string()
Exemplo n.º 29
0
def test(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    grading_mode = pl.get_string_attrib(element, 'grading-method', 'ordered')
    answer_name = pl.get_string_attrib(element, 'answers-name')
    answer_name_field = answer_name + '-input'
    weight = pl.get_integer_attrib(element, 'weight', WEIGHT_DEFAULT)
    feedback_type = pl.get_string_attrib(element, 'feedback', FEEDBACK_DEFAULT)

    # Right now invalid input must mean an empty response. Because user input is only
    # through drag and drop, there is no other way for their to be invalid input. This
    # may change in the future if we have nested input boxes (like faded parsons' problems).
    if data['test_type'] == 'invalid':
        data['raw_submitted_answers'][answer_name_field] = json.dumps([])
        data['format_errors'][answer_name] = 'No answer was submitted.'

    # TODO grading modes 'unordered,' 'dag,' and 'ranking' allow multiple different possible
    # correct answers, we should check them at random instead of just the provided solution
    elif data['test_type'] == 'correct':
        answer = filter_multiple_from_array(
            data['correct_answers'][answer_name],
            ['inner_html', 'indent', 'uuid'])
        data['raw_submitted_answers'][answer_name_field] = json.dumps(answer)
        data['partial_scores'][answer_name] = {
            'score': 1,
            'weight': weight,
            'feedback': '',
            'first_wrong': -1
        }

    # TODO: The only wrong answer being tested is the correct answer with the first
    # block mising. We should instead do a random selection of correct and incorrect blocks.
    elif data['test_type'] == 'incorrect':
        answer = filter_multiple_from_array(
            data['correct_answers'][answer_name],
            ['inner_html', 'indent', 'uuid'])
        answer.pop(0)
        score = float(len(answer)) / (len(answer) +
                                      1) if grading_mode == 'unordered' else 0
        first_wrong = 0 if grading_mode == 'dag' else -1
        feedback = DAG_FIRST_WRONG_FEEDBACK['wrong-at-block'].format(
            1
        ) if grading_mode == 'dag' and feedback_type == 'first-wrong' else ''
        data['raw_submitted_answers'][answer_name_field] = json.dumps(answer)
        data['partial_scores'][answer_name] = {
            'score': score,
            'weight': weight,
            'feedback': feedback,
            'first_wrong': first_wrong
        }

    else:
        raise Exception('invalid result: %s' % data['test_type'])
Exemplo n.º 30
0
    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)
Exemplo n.º 31
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.º 32
0
def prepare(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    required_attribs = ['answers-name']
    optional_attribs = ['weight', 'correct-answer', 'label', 'suffix', 'display', 'remove-leading-trailing', 'remove-spaces', 'allow-blank', 'placeholder']
    pl.check_attribs(element, required_attribs, optional_attribs)

    name = pl.get_string_attrib(element, 'answers-name')
    correct_answer = pl.get_string_attrib(element, 'correct-answer', None)

    if correct_answer is not None:
        if name in data['correct_answers']:
            raise Exception('duplicate correct_answers variable name: %s' % name)
        data['correct_answers'][name] = correct_answer
def prepare(element_html, element_index, data):
    element = lxml.html.fragment_fromstring(element_html)
    required_attribs = ['answers_name']
    optional_attribs = ['weight', 'correct_answer', 'variables']
    pl.check_attribs(element, required_attribs, optional_attribs)
    name = pl.get_string_attrib(element, 'answers_name')

    correct_answer = pl.get_string_attrib(element, 'correct_answer', None)
    if correct_answer is not None:
        if name in data['correct_answers']:
            raise Exception('duplicate correct_answers variable name: %s' %
                            name)
        data['correct_answers'][name] = correct_answer
Exemplo n.º 34
0
def prepare(element_html, element_index, data):
    element = lxml.html.fragment_fromstring(element_html)
    required_attribs = ['answers-name']
    optional_attribs = ['weight', 'correct-answer', 'label', 'suffix', 'display', 'remove-leading-trailing', 'remove-spaces', 'allow-blank']
    pl.check_attribs(element, required_attribs, optional_attribs)

    name = pl.get_string_attrib(element, 'answers-name')
    correct_answer = pl.get_string_attrib(element, 'correct-answer', None)

    if correct_answer is not None:
        if name in data['correct_answers']:
            raise Exception('duplicate correct_answers variable name: %s' % name)
        data['correct_answers'][name] = correct_answer
Exemplo n.º 35
0
def prepare(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    required_attribs = ['file-name']
    optional_attribs = ['ace-mode', 'ace-theme', 'editor-config-function', 'source-file-name']
    pl.check_attribs(element, required_attribs, optional_attribs)
    source_file_name = pl.get_string_attrib(element, 'source-file-name', None)

    if '_required_file_names' not in data['params']:
        data['params']['_required_file_names'] = []
    data['params']['_required_file_names'].append(pl.get_string_attrib(element, 'file-name'))

    if source_file_name is not None:
        if element.text is not None and not str(element.text).isspace():
            raise Exception('Existing code cannot be added inside html element when "source-file-name" attribute is used.')
Exemplo n.º 36
0
def get_orientation(element, name_orientation, name_format):
    s = pl.get_string_attrib(element, name_orientation, None)
    if s is None:
        return [0, 0, 0, 1]

    f = pl.get_string_attrib(element, name_format, 'rpy')
    if f == 'rpy':
        try:
            rpy = np.array(json.loads(s), dtype=np.float64)
            if rpy.shape == (3,):
                qx = pyquaternion.Quaternion(axis=[1, 0, 0], degrees=rpy[0])
                qy = pyquaternion.Quaternion(axis=[0, 1, 0], degrees=rpy[1])
                qz = pyquaternion.Quaternion(axis=[0, 0, 1], degrees=rpy[2])
                return np.roll((qx * qy * qz).elements, -1).tolist()
            else:
                raise ValueError()
        except Exception:
            raise Exception('attribute "{:s}" with format "{:s}" must be a set of roll, pitch, yaw angles in degrees with format "[roll, pitch, yaw]": {:s}'.format(name_orientation, name_format, s))
    elif f == 'quaternion':
        try:
            q = np.array(json.loads(s), dtype=np.float64)
            if (q.shape == (4,)) and np.allclose(np.linalg.norm(q), 1.0):
                return q.tolist()
            else:
                raise ValueError()
        except Exception:
            raise Exception('attribute "{:s}" with format "{:s}" must be a unit quaternion with format "[x, y, z, w]": {:s}'.format(name_orientation, name_format, s))
    elif f == 'matrix':
        try:
            R = np.array(json.loads(s), dtype=np.float64)
            return np.roll(pyquaternion.Quaternion(matrix=R).elements, -1).tolist()
        except Exception:
            raise Exception('attribute "{:s}" with format "{:s}" must be a 3x3 rotation matrix with format "[[ ... ], [ ... ], [ ... ]]": {:s}'.format(name_orientation, name_format, s))
    elif f == 'axisangle':
        try:
            q = np.array(json.loads(s), dtype=np.float64)
            if (q.shape == (4,)):
                axis = q[0:3]
                angle = q[3]
                if np.allclose(np.linalg.norm(axis), 1.0):
                    return np.roll(pyquaternion.Quaternion(axis=axis, degrees=angle).elements, -1).tolist()
                else:
                    raise ValueError()
            else:
                raise ValueError()
        except Exception:
            raise Exception('attribute "{:s}" with format "{:s}" must have format "[x, y, z, angle]" where (x, y, z) are the components of a unit vector and where the angle is in degrees: {:s}'.format(name_orientation, name_format, s))
    else:
        raise Exception('attribute "{:s}" must be "rpy", "quaternion", "matrix", or "axisangle": {:s}'.format(name_format, f))
Exemplo n.º 37
0
def test(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    weight = pl.get_integer_attrib(element, 'weight', 1)

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

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

        # FIXME: add more invalid choices
    else:
        raise Exception('invalid result: %s' % result)
Exemplo n.º 38
0
def parse(element_html, data):
    # By convention, this function returns at the first error found

    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    allow_complex = pl.get_boolean_attrib(element, 'allow-complex', False)

    # Get submitted answer or return parse_error if it does not exist
    a_sub = data['submitted_answers'].get(name, None)
    if a_sub is None:
        data['format_errors'][name] = 'No submitted answer.'
        data['submitted_answers'][name] = None
        return

    # Convert submitted answer to numpy array (return parse_error on failure)
    (a_sub_parsed, info) = pl.string_to_2darray(a_sub, allow_complex=allow_complex)
    if a_sub_parsed is None:
        data['format_errors'][name] = info['format_error']
        data['submitted_answers'][name] = None
        return

    # Replace submitted answer with numpy array
    data['submitted_answers'][name] = pl.to_json(a_sub_parsed)

    # Store format type
    if '_pl_matrix_input_format' not in data['submitted_answers']:
        data['submitted_answers']['_pl_matrix_input_format'] = {}
    data['submitted_answers']['_pl_matrix_input_format'][name] = info['format_type']
Exemplo n.º 39
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.º 40
0
def prepare(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    required_attribs = ['answers-name']
    optional_attribs = ['weight', 'correct-answer', 'variables', 'label', 'display', 'allow-complex', 'imaginary-unit-for-display']
    pl.check_attribs(element, required_attribs, optional_attribs)
    name = pl.get_string_attrib(element, 'answers-name')

    correct_answer = pl.get_string_attrib(element, 'correct-answer', None)
    if correct_answer is not None:
        if name in data['correct-answers']:
            raise Exception('duplicate correct-answers variable name: %s' % name)
        data['correct-answers'][name] = correct_answer

    imaginary_unit = pl.get_string_attrib(element, 'imaginary-unit-for-display', 'i')
    if not (imaginary_unit == 'i' or imaginary_unit == 'j'):
        raise Exception('imaginary-unit-for-display must be either i or j')
Exemplo n.º 41
0
def grade(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')

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

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

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

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

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

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

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

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

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

    # Get submitted answer or return parse_error if it does not exist
    files = data['submitted_answers'].get(answer_name, None)
    if not files:
        add_format_error(data, 'No submitted answer for file upload.')
        return

    try:
        parsed_files = json.loads(files)
    except ValueError:
        add_format_error(data, 'Could not parse submitted files.')

    # Filter out any files that were not listed in file_names
    parsed_files = [x for x in parsed_files if x.get('name', '') in required_file_names]

    if data['submitted_answers'].get('_files', None) is None:
        data['submitted_answers']['_files'] = parsed_files
    elif isinstance(data['submitted_answers'].get('_files', None), list):
        data['submitted_answers']['_files'].extend(parsed_files)
    else:
        add_format_error(data, '_files was present but was not an array.')

    # Validate that all required files are present
    if parsed_files is not None:
        submitted_file_names = [x.get('name', '') for x in parsed_files]
        missing_files = [x for x in required_file_names if x not in submitted_file_names]

        if len(missing_files) > 0:
            add_format_error(data, 'The following required files were missing: ' + ', '.join(missing_files))
Exemplo n.º 45
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.º 46
0
def parse(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    # Get allow-blank option
    allow_blank = pl.get_string_attrib(element, 'allow-blank', False)

    # Get submitted answer or return parse_error if it does not exist
    a_sub = data['submitted_answers'].get(name, None)
    if a_sub is None:
        data['format_errors'][name] = 'No submitted answer.'
        data['submitted_answers'][name] = None
        return

    if not a_sub and not allow_blank:
        data['format_errors'][name] = 'Invalid format. The submitted answer was left blank.'
        data['submitted_answers'][name] = None
    else:
        data['submitted_answers'][name] = pl.to_json(a_sub)
Exemplo n.º 47
0
def get_file_url(element, data):
    # Get file name or raise exception if one does not exist
    file_name = pl.get_string_attrib(element, 'file-name')

    # Get directory (default is clientFilesQuestion)
    file_directory = pl.get_string_attrib(element, 'file-directory', 'clientFilesQuestion')

    # Get base url, which depends on the directory
    if file_directory == 'clientFilesQuestion':
        base_url = data['options']['client_files_question_url']
    elif file_directory == 'clientFilesCourse':
        base_url = data['options']['client_files_course_url']
    else:
        raise ValueError('file-directory "{}" is not valid (must be "clientFilesQuestion" or "clientFilesCourse")'.format(file_directory))

    # Get full url
    file_url = os.path.join(base_url, file_name)

    return file_url
Exemplo n.º 48
0
def prepare(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    required_attribs = ['file-names']
    optional_attribs = []
    pl.check_attribs(element, required_attribs, optional_attribs)

    if '_required_file_names' not in data['params']:
        data['params']['_required_file_names'] = []
    file_names = get_file_names_as_array(pl.get_string_attrib(element, 'file-names'))
    data['params']['_required_file_names'].extend(file_names)
def test(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    weight = pl.get_integer_attrib(element, 'weight', 1)
    allow_partial_credit = pl.get_boolean_attrib(element, 'allow-partial-credit', False)

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

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

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

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

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

    if number_of_correct == m * n:
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    else:
        if not allow_partial_credit:
            score_value = 0
        else:
            score_value = number_of_correct / (m * n)
        data['partial_scores'][name] = {'score': score_value, 'weight': weight, 'feedback': feedback}
Exemplo n.º 50
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)

    # Get file name or raise exception if one does not exist
    file_name = pl.get_string_attrib(element, 'file-name')

    # Get type (default is static)
    file_type = pl.get_string_attrib(element, 'type', 'static')

    # Get directory (default is clientFilesQuestion)
    file_directory = pl.get_string_attrib(element, 'directory', 'clientFilesQuestion')

    # Get label (default is file_name)
    file_label = pl.get_string_attrib(element, 'label', file_name)

    # Get whether to force a download or open in-browser
    force_download = pl.get_boolean_attrib(element, 'force-download', True)

    # Get base url, which depends on the type and directory
    if file_type == 'static':
        if file_directory == 'clientFilesQuestion':
            base_url = data['options']['client_files_question_url']
        elif file_directory == 'clientFilesCourse':
            base_url = data['options']['client_files_course_url']
        else:
            raise ValueError('directory "{}" is not valid for type "{}" (must be "clientFilesQuestion" or "clientFilesCourse")'.format(file_directory, file_type))
    elif file_type == 'dynamic':
        if pl.has_attrib(element, 'directory'):
            raise ValueError('no directory ("{}") can be provided for type "{}"'.format(file_directory, file_type))
        else:
            base_url = data['options']['client_files_question_dynamic_url']
    else:
        raise ValueError('type "{}" is not valid (must be "static" or "dynamic")'.format(file_type))

    # Get full url
    file_url = os.path.join(base_url, file_name)

    # Create and return html
    if force_download:
        return '<a href="' + file_url + '" download>' + file_label + '</a>'
    else:
        return '<a href="' + file_url + '" target="_blank">' + file_label + '</a>'
Exemplo n.º 51
0
def grade(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    variables = get_variables_list(pl.get_string_attrib(element, 'variables', None))
    allow_complex = pl.get_boolean_attrib(element, 'allow-complex', False)
    weight = pl.get_integer_attrib(element, 'weight', 1)

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

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

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

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

    # Check equality
    correct = a_tru.equals(a_sub)

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

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

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

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

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

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

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

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

    if a_tru == a_sub:
        data['partial_scores'][name] = {'score': 1, 'weight': weight}
    else:
        data['partial_scores'][name] = {'score': 0, 'weight': weight}
Exemplo n.º 53
0
def render(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)

    # Get file name or raise exception if one does not exist
    file_name = pl.get_string_attrib(element, 'file-name')

    # Get type (default is static)
    file_type = pl.get_string_attrib(element, 'type', 'static')

    # Get directory (default is clientFilesQuestion)
    file_directory = pl.get_string_attrib(element, 'directory', 'clientFilesQuestion')

    # Get base url, which depends on the type and directory
    if file_type == 'static':
        if file_directory == 'clientFilesQuestion':
            base_url = data['options']['client_files_question_url']
        elif file_directory == 'clientFilesCourse':
            base_url = data['options']['client_files_course_url']
        else:
            raise ValueError('directory "{}" is not valid for type "{}" (must be "clientFilesQuestion" or "clientFilesCourse")'.format(file_directory, file_type))
    elif file_type == 'dynamic':
        if pl.has_attrib(element, 'directory'):
            raise ValueError('no directory ("{}") can be provided for type "{}"'.format(file_directory, file_type))
        else:
            base_url = data['options']['client_files_question_dynamic_url']
    else:
        raise ValueError('type "{}" is not valid (must be "static" or "dynamic")'.format(file_type))

    # Get full url
    file_url = os.path.join(base_url, file_name)

    # Get width (optional)
    width = pl.get_string_attrib(element, 'width', None)

    # Create and return html
    html_params = {'src': file_url, 'width': width}
    with open('pl-figure.mustache', 'r', encoding='utf-8') as f:
        html = chevron.render(f, html_params).strip()

    return html
Exemplo n.º 54
0
def grade(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    weight = pl.get_integer_attrib(element, 'weight', 1)

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

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

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

    # Get true answer
    a_tru = pl.from_json(data['correct_answers'].get(name, None))
    if a_tru is None:
        return
    a_tru = np.array(a_tru)
    if a_tru.ndim != 2:
        raise ValueError('true answer must be a 2D array')
    else:
        m, n = np.shape(a_tru)
        A = np.empty([m, n])

    # Create an array for the submitted answer to be stored in data['submitted_answer'][name]
    # used for display in the answer and submission panels
    # Also creates invalid error messages
    invalid_format = False
    for i in range(m):
        for j in range(n):
            each_entry_name = name + str(n * i + j + 1)
            a_sub = data['submitted_answers'].get(each_entry_name, None)
            if a_sub is None:
                data['submitted_answers'][each_entry_name] = None
                data['format_errors'][each_entry_name] = '(No submitted answer)'
                invalid_format = True
            elif not a_sub:
                data['submitted_answers'][each_entry_name] = None
                data['format_errors'][each_entry_name] = '(Invalid blank entry)'
                invalid_format = True
            else:
                a_sub_parsed = pl.string_to_number(a_sub, allow_complex=False)
                if a_sub_parsed is None:
                    data['submitted_answers'][each_entry_name] = None
                    data['format_errors'][each_entry_name] = '(Invalid format)'
                    invalid_format = True
                elif not np.isfinite(a_sub_parsed):
                    data['submitted_answers'][each_entry_name] = None
                    data['format_errors'][each_entry_name] = '(Invalid format - not finite)'
                    invalid_format = True
                else:
                    data['submitted_answers'][each_entry_name] = pl.to_json(a_sub_parsed)
                    A[i, j] = a_sub_parsed

    if invalid_format:
        data['format_errors'][name] = 'At least one of the entries has invalid format (empty entries or not a double precision floating point number)'
        data['submitted_answers'][name] = None
    else:
        data['submitted_answers'][name] = pl.to_json(A)
Exemplo n.º 56
0
def parse(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')

    submitted_key = data['submitted_answers'].get(name, None)
    all_keys = [a['key'] for a in data['params'][name]]

    if submitted_key is None:
        data['format_errors'][name] = 'No submitted answer.'
        return

    if submitted_key not in all_keys:
        data['format_errors'][name] = 'INVALID choice: ' + submitted_key  # FIXME: escape submitted_key
        return
Exemplo n.º 57
0
def get_position(element, name_position, default=[0, 0, 0], must_be_nonzero=False):
    s = pl.get_string_attrib(element, name_position, None)
    if s is None:
        return default
    try:
        p = np.array(json.loads(s), dtype=np.float64)
        if p.shape == (3,):
            if must_be_nonzero and np.allclose(p, np.array([0, 0, 0])):
                raise ValueError('attribute "{:s}" must be non-zero'.format(name_position))
            else:
                return p.tolist()
        else:
            raise ValueError()
    except Exception:
        raise Exception('attribute "{:s}" must have format "[x, y, z]": {:s}'.format(name_position, s))
Exemplo n.º 58
0
def test(element_html, data):
    element = lxml.html.fragment_fromstring(element_html)
    name = pl.get_string_attrib(element, 'answers-name')
    weight = pl.get_integer_attrib(element, 'weight', 1)

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

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

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

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