def render(element_html, element_index, data): element = lxml.html.fragment_fromstring(element_html) digits = pl.get_integer_attrib(element, 'digits', 2) matlab_data = '' python_data = 'import numpy as np\n\n' for child in element: if child.tag == 'variable': # Raise exception of variable does not have a name pl.check_attribs(child, required_attribs=['params_name'], optional_attribs=[]) # Get name of variable var_name = pl.get_string_attrib(child, 'params_name') # Get value of variable, raising exception if variable does not exist var_data = data['params'].get(var_name, None) if var_data is None: raise Exception('No value in data["params"] for variable %s in pl_matrix_output element' % var_name) # If the variable is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) var_data = pl.from_json(var_data) if np.isscalar(var_data): prefix = '' suffix = '' else: # Wrap the variable in an ndarray (if it's already one, this does nothing) var_data = np.array(var_data) # Check shape of variable if var_data.ndim != 2: raise Exception('Value in data["params"] for variable %s in pl_matrix_output element must be a scalar or a 2D array' % var_name) # Create prefix/suffix so python string is np.array( ... ) prefix = 'np.array(' suffix = ')' # Create string for matlab and python format matlab_data += pl.inner_html(child) + ' = ' + pl.string_from_2darray(var_data, language='matlab', digits=digits) + ';\n' python_data += pl.inner_html(child) + ' = ' + prefix + pl.string_from_2darray(var_data, language='python', digits=digits) + suffix + '\n' html_params = { 'default_is_matlab': True, 'matlab_data': matlab_data, 'python_data': python_data, 'element_index': element_index, 'uuid': pl.get_uuid() } with open('pl_matrix_output.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() return html
def parse(element_html, data): element = lxml.html.fragment_fromstring(element_html) answers_name = pl.get_string_attrib(element, 'answers-name') answer = data['submitted_answers'].get(answers_name, None) # Blank option should be available, but cause format error when submitted. valid_options = [' '] for child in element: if child.tag in ['pl-answer']: pl.check_attribs(child, required_attribs=[], optional_attribs=['correct']) child_html = pl.inner_html(child).strip() valid_options.append(child_html) if answer is BLANK_ANSWER: data['format_errors'][ answers_name] = 'The submitted answer was left blank.' if answer is None: data['format_errors'][answers_name] = 'No answer was submitted.' if answer not in valid_options: data['format_errors'][answers_name] = 'Invalid option submitted.'
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
def render(element_html, element_index, data): element = lxml.html.fragment_fromstring(element_html) language = pl.get_string_attrib(element, 'language', None) no_highlight = pl.get_boolean_attrib(element, 'no-highlight', False) specify_language = (language is not None) and (not no_highlight) # 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:] html_params = { 'specify_language': specify_language, 'language': language, 'no_highlight': no_highlight, 'code': code, } with open('pl-code.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() return html
def get_options(element, data): options = [] for child in element: if child.tag in ['pl-answer']: pl.check_attribs(child, required_attribs=[], optional_attribs=['correct']) child_html = pl.inner_html(child).strip() options.append(child_html) return options
def categorize_matches(element, data): """Get provided statements and options from the pl-matching element""" options = {} statements = [] index = 0 # Sort the elements so that pl-options come first. children = element[:] children.sort(key=lambda child: child.tag) def make_option(name, html): nonlocal index option = {'index': index, 'name': name, 'html': html} index += 1 return option for child in children: if child.tag in ['pl-option', 'pl_option']: pl.check_attribs(child, required_attribs=[], optional_attribs=['name']) child_html = pl.inner_html(child) option_name = pl.get_string_attrib(child, 'name', child_html) # An option object has: index of appearance in the pl-matching element; # the name attribute; and the html content. option = make_option(option_name, child_html) options[option_name] = option elif child.tag in ['pl-statement', 'pl_statement']: pl.check_attribs(child, required_attribs=['match'], optional_attribs=[]) child_html = pl.inner_html(child) match_name = pl.get_string_attrib(child, 'match') if match_name not in options: new_option = make_option(match_name, match_name) options[match_name] = new_option # A statement object has: the name attribute of the correct matching option; and # the html content. statement = {'match': match_name, 'html': child_html} statements.append(statement) return list(options.values()), statements
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)
def get_options(element, data): answers_name = pl.get_string_attrib(element, 'answers-name') submitted_answer = data.get('submitted_answers', {}).get(answers_name, None) options = [] for child in element: if child.tag in ['pl-answer']: pl.check_attribs(child, required_attribs=[], optional_attribs=['correct']) child_html = pl.inner_html(child).strip() options.append({ 'value': child_html, 'selected': (child_html == submitted_answer) }) return options
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) hide_in_question = pl.get_boolean_attrib(element, 'question', QUESTION_DEFAULT) hide_in_submission = pl.get_boolean_attrib(element, 'submission', SUBMISSION_DEFAULT) hide_in_answer = pl.get_boolean_attrib(element, 'answer', ANSWER_DEFAULT) if (data['panel'] == 'question' and not hide_in_question) \ or (data['panel'] == 'submission' and not hide_in_submission) \ or (data['panel'] == 'answer' and not hide_in_answer): element = lxml.html.fragment_fromstring(element_html) return pl.inner_html(element) else: return ''
def get_solution(element, data): solution = [] for child in element: if child.tag in ['pl-answer']: pl.check_attribs(child, required_attribs=[], optional_attribs=['correct']) is_correct = pl.get_boolean_attrib(child, 'correct', False) child_html = pl.inner_html(child).strip() if is_correct: solution.append(child_html) if len(solution) > 1: raise Exception('Multiple correct answers were set') return solution[0]
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) language = pl.get_string_attrib(element, 'language', None) no_highlight = pl.get_boolean_attrib(element, 'no-highlight', False) specify_language = (language is not None) and (not no_highlight) source_file_name = pl.get_string_attrib(element, 'source-file-name', None) prevent_select = pl.get_boolean_attrib(element, 'prevent-select', False) 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() 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:] 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
def categorize_options(element, data): """Get provided correct and incorrect answers""" correct_answers = [] incorrect_answers = [] index = 0 for child in element: if child.tag in ['pl-answer', 'pl_answer']: pl.check_attribs(child, required_attribs=[], optional_attribs=['correct']) correct = pl.get_boolean_attrib(child, 'correct', False) child_html = pl.inner_html(child) answer_tuple = (index, correct, child_html) if correct: correct_answers.append(answer_tuple) else: incorrect_answers.append(answer_tuple) index += 1 file_path = pl.get_string_attrib(element, 'external-json', EXTERNAL_JSON_DEFAULT) if file_path is not EXTERNAL_JSON_DEFAULT: correct_attrib = pl.get_string_attrib( element, 'external-json-correct-key', EXTERNAL_JSON_CORRECT_KEY_DEFAULT) incorrect_attrib = pl.get_string_attrib( element, 'external-json-incorrect-key', EXTERNAL_JSON_INCORRECT_KEY_DEFAULT) if pathlib.PurePath(file_path).is_absolute(): json_file = file_path else: json_file = pathlib.PurePath( data['options']['question_path']).joinpath(file_path) try: with open(json_file, mode='r', encoding='utf-8') as f: obj = json.load(f) for text in obj.get(correct_attrib, []): correct_answers.append((index, True, text)) index += 1 for text in obj.get(incorrect_attrib, []): incorrect_answers.append((index, False, text)) index += 1 except FileNotFoundError: raise Exception( f'JSON answer file: "{json_file}" could not be found') return correct_answers, incorrect_answers
def categorize_options(element): """Get provided correct and incorrect answers""" correct_answers = [] incorrect_answers = [] index = 0 for child in element: if child.tag in ['pl-answer', 'pl_answer']: pl.check_attribs(child, required_attribs=[], optional_attribs=['correct']) correct = pl.get_boolean_attrib(child, 'correct', False) child_html = pl.inner_html(child) answer_tuple = (index, correct, child_html) if correct: correct_answers.append(answer_tuple) else: incorrect_answers.append(answer_tuple) index += 1 return correct_answers, incorrect_answers
def render(element_html, element_index, data): element = lxml.html.fragment_fromstring(element_html) digits = pl.get_integer_attrib(element, 'digits', 2) html = '<pre>\n% Data in MATLAB format\n' for child in element: if child.tag == 'variable': pl.check_attribs(child, required_attribs=['params_name'], optional_attribs=[]) var_name = pl.get_string_attrib(child, 'params_name') var_data = data['params'].get(var_name, None) if var_data is None: raise Exception( 'No value in data["params"] for variable %s in matrix_output element' % var_name) html += pl.inner_html(child) \ + ' = ' \ + pl.numpy_to_matlab((var_data if np.isscalar(var_data) else np.array(var_data)), ndigits=digits) + ';' \ + '\n' html += '</pre>' return html
def get_objects(element, data): obj_list = [] for child in element: is_stl = (child.tag in ['pl-threejs-stl', 'pl_threejs_stl']) is_txt = (child.tag in ['pl-threejs-txt', 'pl_threejs_txt']) if not (is_stl or is_txt): continue # Type-specific check and get (stl) if is_stl: # Attributes pl.check_attribs(child, required_attribs=['file-name'], optional_attribs=[ 'file-directory', 'frame', 'color', 'position', 'orientation', 'format', 'scale', 'opacity' ]) # - file-name (and file-directory) file_url = get_file_url(child, data) # - type object_type = 'stl' # - object specific = {'type': object_type, 'file_url': file_url} # Type-specific check and get (txt) if is_txt: # Attributes pl.check_attribs(child, required_attribs=[], optional_attribs=[ 'frame', 'color', 'position', 'orientation', 'format', 'scale', 'opacity' ]) # - text text = pl.inner_html(child) # - type object_type = 'txt' # - object specific = {'type': object_type, 'text': text} # Common # - frame frame = pl.get_string_attrib(child, 'frame', 'body') if frame not in ['body', 'space']: raise Exception( '"frame" must be either "body" or "space": {:s}'.format(frame)) if frame == 'body': default_color = '#e84a27' default_opacity = 0.7 else: default_color = '#13294b' default_opacity = 0.4 # - color color = pl.get_color_attrib(child, 'color', default_color) # - opacity opacity = pl.get_float_attrib(child, 'opacity', default_opacity) # - position p = pl.get_string_attrib(child, 'position', '[0, 0, 0]') try: position = np.array(json.loads(p), dtype=np.float64) if position.shape == (3, ): position = position.tolist() else: raise ValueError() except Exception: raise Exception( 'attribute "position" must have format [x, y, z]: {:s}'.format( p)) # - orientation (and format) orientation = get_orientation(child, 'orientation', 'format') # - scale scale = pl.get_float_attrib(child, 'scale', 1.0) common = { 'frame': frame, 'color': color, 'opacity': opacity, 'position': position, 'quaternion': orientation, 'scale': scale } obj = {**specific, **common} obj_list.append(obj) return obj_list
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['answers-name'] optional_attribs = ['weight', 'number-answers', 'min-correct', 'max-correct', 'fixed-order', 'inline', 'hide-answer-panel', 'hide-help-text', 'detailed-help-text', 'partial-credit', 'partial-credit-method', 'hide-letter-keys', 'hide-score-badge'] pl.check_attribs(element, required_attribs, optional_attribs) name = pl.get_string_attrib(element, 'answers-name') partial_credit = pl.get_boolean_attrib(element, 'partial-credit', PARTIAL_CREDIT_DEFAULT) partial_credit_method = pl.get_string_attrib(element, 'partial-credit-method', None) if not partial_credit and partial_credit_method is not None: raise Exception('Cannot specify partial-credit-method if partial-credit is not enabled') correct_answers = [] incorrect_answers = [] index = 0 for child in element: if child.tag in ['pl-answer', 'pl_answer']: pl.check_attribs(child, required_attribs=[], optional_attribs=['correct']) correct = pl.get_boolean_attrib(child, 'correct', False) child_html = pl.inner_html(child) answer_tuple = (index, correct, child_html) if correct: correct_answers.append(answer_tuple) else: incorrect_answers.append(answer_tuple) index += 1 len_correct = len(correct_answers) len_incorrect = len(incorrect_answers) len_total = len_correct + len_incorrect if len_correct == 0: raise ValueError('At least one option must be true.') number_answers = pl.get_integer_attrib(element, 'number-answers', len_total) min_correct = pl.get_integer_attrib(element, 'min-correct', 1) max_correct = pl.get_integer_attrib(element, 'max-correct', len(correct_answers)) if min_correct < 1: raise ValueError('The attribute min-correct is {:d} but must be at least 1'.format(min_correct)) # FIXME: why enforce a maximum number of options? max_answers = 26 # will not display more than 26 checkbox answers number_answers = max(0, min(len_total, min(max_answers, number_answers))) min_correct = min(len_correct, min(number_answers, max(0, max(number_answers - len_incorrect, min_correct)))) max_correct = min(len_correct, min(number_answers, max(min_correct, max_correct))) if not (0 <= min_correct <= max_correct <= len_correct): raise ValueError('INTERNAL ERROR: correct number: (%d, %d, %d, %d)' % (min_correct, max_correct, len_correct, len_incorrect)) min_incorrect = number_answers - max_correct max_incorrect = number_answers - min_correct if not (0 <= min_incorrect <= max_incorrect <= len_incorrect): raise ValueError('INTERNAL ERROR: incorrect number: (%d, %d, %d, %d)' % (min_incorrect, max_incorrect, len_incorrect, len_correct)) number_correct = random.randint(min_correct, max_correct) number_incorrect = number_answers - number_correct sampled_correct = random.sample(correct_answers, number_correct) sampled_incorrect = random.sample(incorrect_answers, number_incorrect) sampled_answers = sampled_correct + sampled_incorrect random.shuffle(sampled_answers) fixed_order = pl.get_boolean_attrib(element, 'fixed-order', FIXED_ORDER_DEFAULT) if fixed_order: # we can't simply skip the shuffle because we already broke the original # order by separating into correct/incorrect lists sampled_answers.sort(key=lambda a: a[0]) # sort by stored original index display_answers = [] correct_answer_list = [] for (i, (index, correct, html)) in enumerate(sampled_answers): keyed_answer = {'key': pl.index2key(i), 'html': html} display_answers.append(keyed_answer) if correct: correct_answer_list.append(keyed_answer) if name in data['params']: raise Exception('duplicate params variable name: %s' % name) if name in data['correct_answers']: raise Exception('duplicate correct_answers variable name: %s' % name) data['params'][name] = display_answers data['correct_answers'][name] = correct_answer_list
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['answers-name'] optional_attribs = ['weight', 'number-answers', 'fixed-order', 'inline'] pl.check_attribs(element, required_attribs, optional_attribs) name = pl.get_string_attrib(element, 'answers-name') correct_answers = [] incorrect_answers = [] index = 0 for child in element: if child.tag in ['pl-answer', 'pl_answer']: pl.check_attribs(child, required_attribs=[], optional_attribs=['correct']) correct = pl.get_boolean_attrib(child, 'correct', False) child_html = pl.inner_html(child) answer_tuple = (index, correct, child_html) if correct: correct_answers.append(answer_tuple) else: incorrect_answers.append(answer_tuple) index += 1 len_correct = len(correct_answers) len_incorrect = len(incorrect_answers) len_total = len_correct + len_incorrect if len_correct < 1: raise Exception('pl-multiple-choice element must have at least one correct answer') number_answers = pl.get_integer_attrib(element, 'number-answers', len_total) number_answers = max(1, min(1 + len_incorrect, number_answers)) number_correct = 1 number_incorrect = number_answers - number_correct if not (0 <= number_incorrect <= len_incorrect): raise Exception('INTERNAL ERROR: number_incorrect: (%d, %d, %d)' % (number_incorrect, len_incorrect, number_answers)) sampled_correct = random.sample(correct_answers, number_correct) sampled_incorrect = random.sample(incorrect_answers, number_incorrect) sampled_answers = sampled_correct + sampled_incorrect random.shuffle(sampled_answers) fixed_order = pl.get_boolean_attrib(element, 'fixed-order', False) if fixed_order: # we can't simply skip the shuffle because we already broke the original # order by separating into correct/incorrect lists sampled_answers.sort(key=lambda a: a[0]) # sort by stored original index display_answers = [] correct_answer = None for (i, (index, correct, html)) in enumerate(sampled_answers): keyed_answer = {'key': chr(ord('a') + i), 'html': html} display_answers.append(keyed_answer) if correct: correct_answer = keyed_answer if name in data['params']: raise Exception('duplicate params variable name: %s' % name) if name in data['correct_answers']: raise Exception('duplicate correct_answers variable name: %s' % name) data['params'][name] = display_answers data['correct_answers'][name] = correct_answer
def render(element_html, data): if data['manual_grading']: element = lxml.html.fragment_fromstring(element_html) return pl.inner_html(element) else: return ''
def prepare(element_html, element_index, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['answers_name'] optional_attribs = ['weight', 'number_answers', 'min_correct', 'max_correct', 'fixed_order', 'inline', 'hide_help_text', 'detailed_help_text'] pl.check_attribs(element, required_attribs, optional_attribs) name = pl.get_string_attrib(element, 'answers_name') correct_answers = [] incorrect_answers = [] index = 0 for child in element: if child.tag == 'pl_answer': pl.check_attribs(child, required_attribs=[], optional_attribs=['correct']) correct = pl.get_boolean_attrib(child, 'correct', False) child_html = pl.inner_html(child) answer_tuple = (index, correct, child_html) if correct: correct_answers.append(answer_tuple) else: incorrect_answers.append(answer_tuple) index += 1 len_correct = len(correct_answers) len_incorrect = len(incorrect_answers) len_total = len_correct + len_incorrect number_answers = pl.get_integer_attrib(element, 'number_answers', len_total) min_correct = pl.get_integer_attrib(element, 'min_correct', 0) max_correct = pl.get_integer_attrib(element, 'max_correct', len(correct_answers)) number_answers = max(0, min(len_total, min(26, number_answers))) min_correct = min(len_correct, min(number_answers, max(0, max(number_answers - len_incorrect, min_correct)))) max_correct = min(len_correct, min(number_answers, max(min_correct, max_correct))) if not (0 <= min_correct <= max_correct <= len_correct): raise Exception('INTERNAL ERROR: correct number: (%d, %d, %d, %d)' % (min_correct, max_correct, len_correct, len_incorrect)) min_incorrect = number_answers - max_correct max_incorrect = number_answers - min_correct if not (0 <= min_incorrect <= max_incorrect <= len_incorrect): raise Exception('INTERNAL ERROR: incorrect number: (%d, %d, %d, %d)' % (min_incorrect, max_incorrect, len_incorrect, len_correct)) number_correct = random.randint(min_correct, max_correct) number_incorrect = number_answers - number_correct sampled_correct = random.sample(correct_answers, number_correct) sampled_incorrect = random.sample(incorrect_answers, number_incorrect) sampled_answers = sampled_correct + sampled_incorrect random.shuffle(sampled_answers) fixed_order = pl.get_boolean_attrib(element, 'fixed_order', False) if fixed_order: # we can't simply skip the shuffle because we already broke the original # order by separating into correct/incorrect lists sampled_answers.sort(key=lambda a: a[0]) # sort by stored original index display_answers = [] correct_answer_list = [] for (i, (index, correct, html)) in enumerate(sampled_answers): keyed_answer = {'key': chr(ord('a') + i), 'html': html} display_answers.append(keyed_answer) if correct: correct_answer_list.append(keyed_answer) if name in data['params']: raise Exception('duplicate params variable name: %s' % name) if name in data['correct_answers']: raise Exception('duplicate correct_answers variable name: %s' % name) data['params'][name] = display_answers data['correct_answers'][name] = correct_answer_list
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['answers-name'] optional_attribs = ['weight', 'number-answers', 'fixed-order', 'inline'] pl.check_attribs(element, required_attribs, optional_attribs) name = pl.get_string_attrib(element, 'answers-name') correct_answers = [] incorrect_answers = [] index = 0 for child in element: if child.tag in ['pl-answer', 'pl_answer']: pl.check_attribs(child, required_attribs=[], optional_attribs=['correct']) correct = pl.get_boolean_attrib(child, 'correct', False) child_html = pl.inner_html(child) answer_tuple = (index, correct, child_html) if correct: correct_answers.append(answer_tuple) else: incorrect_answers.append(answer_tuple) index += 1 len_correct = len(correct_answers) len_incorrect = len(incorrect_answers) len_total = len_correct + len_incorrect if len_correct < 1: raise Exception('pl-multiple-choice element must have at least one correct answer') number_answers = pl.get_integer_attrib(element, 'number-answers', len_total) number_answers = max(1, min(1 + len_incorrect, number_answers)) number_correct = 1 number_incorrect = number_answers - number_correct if not (0 <= number_incorrect <= len_incorrect): raise Exception('INTERNAL ERROR: number_incorrect: (%d, %d, %d)' % (number_incorrect, len_incorrect, number_answers)) sampled_correct = random.sample(correct_answers, number_correct) sampled_incorrect = random.sample(incorrect_answers, number_incorrect) sampled_answers = sampled_correct + sampled_incorrect random.shuffle(sampled_answers) fixed_order = pl.get_boolean_attrib(element, 'fixed-order', False) if fixed_order: # we can't simply skip the shuffle because we already broke the original # order by separating into correct/incorrect lists sampled_answers.sort(key=lambda a: a[0]) # sort by stored original index display_answers = [] correct_answer = None for (i, (index, correct, html)) in enumerate(sampled_answers): keyed_answer = {'key': chr(ord('a') + i), 'html': html} display_answers.append(keyed_answer) if correct: correct_answer = keyed_answer if name in data['params']: raise Exception('duplicate params variable name: %s' % name) if name in data['correct_answers']: raise Exception('duplicate correct_answers variable name: %s' % name) data['params'][name] = display_answers data['correct_answers'][name] = correct_answer
def render(element_html, element_index, data): if data['panel'] == 'submission': element = lxml.html.fragment_fromstring(element_html) return pl.inner_html(element) else: return ''
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) width = pl.get_float_attrib(element, 'width', None) height = pl.get_float_attrib(element, 'height', None) background = None # Assign layer index in order children are defined # Later defined elements will be placed on top of earlier ones locations = [] z_index = 0 for child in element: # Ignore comments if isinstance(child, lxml.html.HtmlComment): continue # Don't do any special processing for backgrounds if child.tag == 'pl-background': background = pl.inner_html(child) continue # Otherwise, continue as normal valign = pl.get_string_attrib(child, 'valign', VALIGN_DEFAULT) halign = pl.get_string_attrib(child, 'halign', HALIGN_DEFAULT) left = pl.get_float_attrib(child, 'left', None) right = pl.get_float_attrib(child, 'right', None) top = pl.get_float_attrib(child, 'top', None) bottom = pl.get_float_attrib(child, 'bottom', None) # We allow both left/right and top/bottom but only set top and left # so we don't have to worry about all the alignment possibilities if left is not None: x = left elif right is not None: x = width - right if top is not None: y = top elif bottom is not None: y = height - bottom hoff = ALIGNMENT_TO_PERC[halign] voff = ALIGNMENT_TO_PERC[valign] transform = f'translate({hoff}, {voff})' style = f'top: {y}px; left: {x}px; transform: {transform}; z-index: {z_index}' obj = { 'html': pl.inner_html(child), 'outer_style': style, } locations.append(obj) z_index += 1 html_params = { 'width': width, 'height': height, 'locations': locations, 'background': background, 'clip': pl.get_boolean_attrib(element, 'clip', CLIP_DEFAULT) } with open('pl-overlay.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() return html
def render(element_html, data): if data['panel'] == 'answer': element = lxml.html.fragment_fromstring(element_html) return pl.inner_html(element) else: return ''
def test_inner_html(): e = lxml.html.fragment_fromstring('<div>test</div>') assert pl.inner_html(e) == 'test' e = lxml.html.fragment_fromstring('<div>test>test</div>') assert pl.inner_html(e) == 'test>test'
def categorize_options(element, data): """Get provided correct and incorrect answers""" correct_answers = [] incorrect_answers = [] index = 0 for child in element: if child.tag in ['pl-answer', 'pl_answer']: pl.check_attribs(child, required_attribs=[], optional_attribs=['score', 'correct', 'feedback']) correct = pl.get_boolean_attrib(child, 'correct', False) child_html = pl.inner_html(child) child_feedback = pl.get_string_attrib(child, 'feedback', FEEDBACK_DEFAULT) default_score = SCORE_CORRECT_DEFAULT if correct else SCORE_INCORRECT_DEFAULT score = pl.get_float_attrib(child, 'score', default_score) if not (SCORE_INCORRECT_DEFAULT <= score <= SCORE_CORRECT_DEFAULT): raise Exception( f'Score {score} is invalid, must be in the range [0.0, 1.0].' ) if correct and score != SCORE_CORRECT_DEFAULT: raise Exception('Correct answers must give full credit.') answer_tuple = (index, correct, child_html, child_feedback, score) if correct: correct_answers.append(answer_tuple) else: incorrect_answers.append(answer_tuple) index += 1 file_path = pl.get_string_attrib(element, 'external-json', EXTERNAL_JSON_DEFAULT) if file_path is not EXTERNAL_JSON_DEFAULT: correct_attrib = pl.get_string_attrib( element, 'external-json-correct-key', EXTERNAL_JSON_CORRECT_KEY_DEFAULT) incorrect_attrib = pl.get_string_attrib( element, 'external-json-incorrect-key', EXTERNAL_JSON_INCORRECT_KEY_DEFAULT) if pathlib.PurePath(file_path).is_absolute(): json_file = file_path else: json_file = pathlib.PurePath( data['options']['question_path']).joinpath(file_path) try: with open(json_file, mode='r', encoding='utf-8') as f: obj = json.load(f) for text in obj.get(correct_attrib, []): correct_answers.append( (index, True, text, None, SCORE_CORRECT_DEFAULT)) index += 1 for text in obj.get(incorrect_attrib, []): incorrect_answers.append( (index, False, text, None, SCORE_INCORRECT_DEFAULT)) index += 1 except FileNotFoundError: raise Exception( f'JSON answer file: "{json_file}" could not be found') return correct_answers, incorrect_answers
def get_objects(element, data): obj_list = [] for child in element: is_stl = (child.tag in ['pl-threejs-stl', 'pl_threejs_stl']) is_txt = (child.tag in ['pl-threejs-txt', 'pl_threejs_txt']) if not (is_stl or is_txt): continue # Type-specific check and get (stl) if is_stl: # Attributes pl.check_attribs(child, required_attribs=['file-name'], optional_attribs=['file-directory', 'frame', 'color', 'position', 'orientation', 'format', 'scale', 'opacity']) # - file-name (and file-directory) file_url = get_file_url(child, data) # - type object_type = 'stl' # - object specific = { 'type': object_type, 'file_url': file_url } # Type-specific check and get (txt) if is_txt: # Attributes pl.check_attribs(child, required_attribs=[], optional_attribs=['frame', 'color', 'position', 'orientation', 'format', 'scale', 'opacity']) # - text text = pl.inner_html(child) # - type object_type = 'txt' # - object specific = { 'type': object_type, 'text': text } # Common # - frame frame = pl.get_string_attrib(child, 'frame', 'body') if frame not in ['body', 'space']: raise Exception('"frame" must be either "body" or "space": {:s}'.format(frame)) if frame == 'body': default_color = '#e84a27' default_opacity = 0.7 else: default_color = '#13294b' default_opacity = 0.4 # - color color = pl.get_color_attrib(child, 'color', default_color) # - opacity opacity = pl.get_float_attrib(child, 'opacity', default_opacity) # - position p = pl.get_string_attrib(child, 'position', '[0, 0, 0]') try: position = np.array(json.loads(p), dtype=np.float64) if position.shape == (3,): position = position.tolist() else: raise ValueError() except Exception: raise Exception('attribute "position" must have format [x, y, z]: {:s}'.format(p)) # - orientation (and format) orientation = get_orientation(child, 'orientation', 'format') # - scale scale = pl.get_float_attrib(child, 'scale', 1.0) common = { 'frame': frame, 'color': color, 'opacity': opacity, 'position': position, 'quaternion': orientation, 'scale': scale } obj = {**specific, **common} obj_list.append(obj) return obj_list
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) digits = pl.get_integer_attrib(element, 'digits', 2) show_matlab = pl.get_boolean_attrib(element, 'show-matlab', True) show_mathematica = pl.get_boolean_attrib(element, 'show-mathematica', True) show_python = pl.get_boolean_attrib(element, 'show-python', True) default_tab = pl.get_string_attrib(element, 'default-tab', 'matlab') tab_list = ['matlab', 'mathematica', 'python'] if default_tab not in tab_list: raise Exception(f'invalid default-tab: {default_tab}') # Setting the default tab displayed_tab = [show_matlab, show_mathematica, show_python] if not any(displayed_tab): raise Exception('All tabs have been hidden from display. At least one tab must be shown.') default_tab_index = tab_list.index(default_tab) # If not displayed, make first visible tab the default if not displayed_tab[default_tab_index]: first_display = displayed_tab.index(True) default_tab = tab_list[first_display] default_tab_index = tab_list.index(default_tab) # Active tab should be the default tab default_tab_list = [False, False, False] default_tab_list[default_tab_index] = True [active_tab_matlab, active_tab_mathematica, active_tab_python] = default_tab_list # Process parameter data matlab_data = '' mathematica_data = '' python_data = 'import numpy as np\n\n' for child in element: if child.tag == 'variable': # Raise exception if variable does not have a name pl.check_attribs(child, required_attribs=['params-name'], optional_attribs=['comment', 'digits']) # Get name of variable var_name = pl.get_string_attrib(child, 'params-name') # Get value of variable, raising exception if variable does not exist var_data = data['params'].get(var_name, None) if var_data is None: raise Exception('No value in data["params"] for variable %s in pl-variable-output element' % var_name) # If the variable is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) var_data = pl.from_json(var_data) # Get comment, if it exists var_matlab_comment = '' var_mathematica_comment = '' var_python_comment = '' if pl.has_attrib(child, 'comment'): var_comment = pl.get_string_attrib(child, 'comment') var_matlab_comment = f' % {var_comment}' var_mathematica_comment = f' (* {var_comment} *)' var_python_comment = f' # {var_comment}' # Get digit for child, if it exists if not pl.has_attrib(child, 'digits'): var_digits = digits else: var_digits = pl.get_string_attrib(child, 'digits') # Assembling Python array formatting if np.isscalar(var_data): prefix = '' suffix = '' else: # Wrap the variable in an ndarray (if it's already one, this does nothing) var_data = np.array(var_data) # Check shape of variable if var_data.ndim > 2: raise Exception('Value in data["params"] for variable %s in pl-variable-output element must be a scalar, a vector, or a 2D array' % var_name) # Create prefix/suffix so python string is np.array( ... ) prefix = 'np.array(' suffix = ')' # Mathematica reserved letters: C D E I K N O mathematica_reserved = ['C', 'D', 'E', 'I', 'K', 'N', 'O'] if pl.inner_html(child) in mathematica_reserved: mathematica_suffix = 'm' else: mathematica_suffix = '' # Create string for matlab and python format var_name_disp = pl.inner_html(child) var_matlab_data = pl.string_from_numpy(var_data, language='matlab', digits=var_digits) var_mathematica = pl.string_from_numpy(var_data, language='mathematica', digits=var_digits) var_python_data = pl.string_from_numpy(var_data, language='python', digits=var_digits) matlab_data += f'{var_name_disp} = {var_matlab_data};{var_matlab_comment}\n' mathematica_data += f'{var_name_disp}{mathematica_suffix} = {var_mathematica};{var_mathematica_comment}\n' python_data += f'{var_name_disp} = {prefix}{var_python_data}{suffix}{var_python_comment}\n' html_params = { 'active_tab_matlab': active_tab_matlab, 'active_tab_mathematica': active_tab_mathematica, 'active_tab_python': active_tab_python, 'show_matlab': show_matlab, 'show_mathematica': show_mathematica, 'show_python': show_python, 'matlab_data': matlab_data, 'mathematica_data': mathematica_data, 'python_data': python_data, 'uuid': pl.get_uuid() } with open('pl-variable-output.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() return html
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) digits = pl.get_integer_attrib(element, 'digits', DIGITS_DEFAULT) show_matlab = pl.get_boolean_attrib(element, 'show-matlab', SHOW_MATLAB_DEFAULT) show_mathematica = pl.get_boolean_attrib(element, 'show-mathematica', SHOW_MATHEMATICA_DEFAULT) show_python = pl.get_boolean_attrib(element, 'show-python', SHOW_PYTHON_DEFAULT) default_tab = pl.get_string_attrib(element, 'default-tab', DEFAULT_TAB_DEFAULT) tab_list = ['matlab', 'mathematica', 'python'] if default_tab not in tab_list: raise Exception(f'invalid default-tab: {default_tab}') # Setting the default tab displayed_tab = [show_matlab, show_mathematica, show_python] if not any(displayed_tab): raise Exception( 'All tabs have been hidden from display. At least one tab must be shown.' ) default_tab_index = tab_list.index(default_tab) # If not displayed, make first visible tab the default if not displayed_tab[default_tab_index]: first_display = displayed_tab.index(True) default_tab = tab_list[first_display] default_tab_index = tab_list.index(default_tab) # Active tab should be the default tab default_tab_list = [False, False, False] default_tab_list[default_tab_index] = True [active_tab_matlab, active_tab_mathematica, active_tab_python] = default_tab_list # Process parameter data matlab_data = '' mathematica_data = '' python_data = 'import numpy as np\n\n' for child in element: if child.tag == 'variable': # Raise exception if variable does not have a name pl.check_attribs(child, required_attribs=['params-name'], optional_attribs=['comment', 'digits']) # Get name of variable var_name = pl.get_string_attrib(child, 'params-name') # Get value of variable, raising exception if variable does not exist var_data = data['params'].get(var_name, None) if var_data is None: raise Exception( 'No value in data["params"] for variable %s in pl-variable-output element' % var_name) # If the variable is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) var_data = pl.from_json(var_data) # Get comment, if it exists var_matlab_comment = '' var_mathematica_comment = '' var_python_comment = '' if pl.has_attrib(child, 'comment'): var_comment = pl.get_string_attrib(child, 'comment') var_matlab_comment = f' % {var_comment}' var_mathematica_comment = f' (* {var_comment} *)' var_python_comment = f' # {var_comment}' # Get digit for child, if it exists if not pl.has_attrib(child, 'digits'): var_digits = digits else: var_digits = pl.get_string_attrib(child, 'digits') # Assembling Python array formatting if np.isscalar(var_data): prefix = '' suffix = '' else: # Wrap the variable in an ndarray (if it's already one, this does nothing) var_data = np.array(var_data) # Check shape of variable if var_data.ndim > 2: raise Exception( 'Value in data["params"] for variable %s in pl-variable-output element must be a scalar, a vector, or a 2D array' % var_name) # Create prefix/suffix so python string is np.array( ... ) prefix = 'np.array(' suffix = ')' # Mathematica reserved letters: C D E I K N O mathematica_reserved = ['C', 'D', 'E', 'I', 'K', 'N', 'O'] if pl.inner_html(child) in mathematica_reserved: mathematica_suffix = 'm' else: mathematica_suffix = '' # Create string for matlab and python format var_name_disp = pl.inner_html(child) var_matlab_data = pl.string_from_numpy(var_data, language='matlab', digits=var_digits) var_mathematica = pl.string_from_numpy(var_data, language='mathematica', digits=var_digits) var_python_data = pl.string_from_numpy(var_data, language='python', digits=var_digits) matlab_data += f'{var_name_disp} = {var_matlab_data};{var_matlab_comment}\n' mathematica_data += f'{var_name_disp}{mathematica_suffix} = {var_mathematica};{var_mathematica_comment}\n' python_data += f'{var_name_disp} = {prefix}{var_python_data}{suffix}{var_python_comment}\n' html_params = { 'active_tab_matlab': active_tab_matlab, 'active_tab_mathematica': active_tab_mathematica, 'active_tab_python': active_tab_python, 'show_matlab': show_matlab, 'show_mathematica': show_mathematica, 'show_python': show_python, 'matlab_data': matlab_data, 'mathematica_data': mathematica_data, 'python_data': python_data, 'uuid': pl.get_uuid() } with open('pl-variable-output.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() return html
def 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 # Chop off ending newlines if code[:-2] == '\r\n': code = code[:-2] if code[:-1] == '\n': 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 specify_language: lexer = get_lexer_by_name(language) else: lexer = NoHighlightingLexer() formatter_opts = { 'style': 'friendly', 'cssclass': 'mb-2 rounded', 'prestyles': 'padding: 0.5rem; margin-bottom: 0px', 'noclasses': True } if highlight_lines is not None: formatter_opts['hl_lines'] = parse_highlight_lines(highlight_lines) formatter_opts['hl_color'] = highlight_lines_color formatter = HighlightingHtmlFormatter(**formatter_opts) code = pygments.highlight(unescape(code), lexer, formatter) html_params = { '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
def test_inner_html(self): e = lxml.html.fragment_fromstring('<div>test</div>') self.assertEqual(pl.inner_html(e), 'test') e = lxml.html.fragment_fromstring('<div>test>test</div>') self.assertEqual(pl.inner_html(e), 'test>test')
def render(element_html, data): if data['panel'] == 'submission': element = lxml.html.fragment_fromstring(element_html) return pl.inner_html(element) else: return ''