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 prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['answers-name'] optional_attribs = ['weight', 'correct-answer', 'label', 'suffix', 'display'] pl.check_attribs(element, required_attribs, optional_attribs) name = pl.get_string_attrib(element, 'answers-name') correct_answer = pl.get_integer_attrib(element, 'correct-answer', None) if correct_answer is not None: if name in data['correct_answers']: raise Exception('duplicate correct_answers variable name: %s' % name) data['correct_answers'][name] = correct_answer
def 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.')
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')
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = [ 'answer_name', # key for 'submitted_answers' and 'true_answers' ] optional_attribs = [ 'body-position', # [x, y, z] 'body-orientation', # [x, y, z, w] or [roll, pitch, yaw] or rotation matrix (3x3 ndarray) or exponential coordinates [wx, wy, wz] 'camera-position', # [x, y, z] - camera is z up and points at origin of space frame 'body-cantranslate', # true (default) or false 'body-canrotate', # true (default) or false 'camera-canmove', # true (default) or false 'body-pose-format', # 'rpy' (default), 'quaternion', 'matrix', 'axisangle' 'answer-pose-format', # 'rpy' (default), 'quaternion', 'matrix', 'axisangle' 'text-pose-format', # 'matrix' (default), 'quaternion', 'homogeneous' 'show-pose-in-question', # true (default) or false 'show-pose-in-correct-answer', # true (default) or false 'show-pose-in-submitted-answer', # true (default) or false 'tol-translation', # 0.5 (default : float > 0) 'tol-rotation', # 5 (default : float > 0) 'grade' # true (default) or false ] pl.check_attribs(element, required_attribs=required_attribs, optional_attribs=optional_attribs)
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', 'size', 'show-help-text' ] 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', CORRECT_ANSWER_DEFAULT) 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', IMAGINARY_UNIT_FOR_DISPLAY_DEFAULT) if not (imaginary_unit == 'i' or imaginary_unit == 'j'): raise Exception('imaginary-unit-for-display must be either i or j')
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' ] 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.' )
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 data['correct_answers'][name] = correct_answer if correct_answer is None: correct_answer = pl.from_json(data['correct_answers'].get(name, None)) if correct_answer is not None: try: if not isinstance(correct_answer, int): correct_answer = int(correct_answer, base) except Exception: raise Exception('correct answer is not a valid input: %s' % name) if correct_answer > 2**53 - 1 or correct_answer < -((2**53) - 1): raise Exception( 'correct answer must be between -9007199254740991 and +9007199254740991 (that is, between -(2^53 - 1) and +(2^53 - 1)).' )
def prepare_tag(html_tags, index, group=None): if html_tags.tag != 'pl-answer': raise Exception('Any html tags nested inside <pl-order-blocks> must be <pl-answer> or <pl-block-group>. \ Any html tags nested inside <pl-block-group> must be <pl-answer>') if grading_method == 'external': pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct']) elif grading_method == 'unordered': pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct', 'indent']) elif grading_method in ['ranking', 'ordered']: pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct', 'ranking', 'indent']) elif grading_method == 'dag': pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct', 'tag', 'depends', 'comment', 'indent']) is_correct = pl.get_boolean_attrib(html_tags, 'correct', PL_ANSWER_CORRECT_DEFAULT) answer_indent = pl.get_integer_attrib(html_tags, 'indent', None) inner_html = pl.inner_html(html_tags) ranking = pl.get_integer_attrib(html_tags, 'ranking', -1) tag = pl.get_string_attrib(html_tags, 'tag', None) if grading_method == 'ranking': tag = str(index) depends = pl.get_string_attrib(html_tags, 'depends', '') depends = depends.strip().split(',') if depends else [] if check_indentation is False and answer_indent is not None: raise Exception('<pl-answer> should not specify indentation if indentation is disabled.') answer_data_dict = {'inner_html': inner_html, 'indent': answer_indent, 'ranking': ranking, 'index': index, 'tag': tag, # set by HTML with DAG grader, set internally for ranking grader 'depends': depends, # only used with DAG grader 'group': group # only used with DAG grader } if is_correct: correct_answers.append(answer_data_dict) else: incorrect_answers.append(answer_data_dict)
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) digits = pl.get_integer_attrib(element, 'digits', 2) show_matlab = pl.get_boolean_attrib(element, 'show-matlab', True) show_mathematica = pl.get_boolean_attrib(element, 'show-mathematica', True) show_python = pl.get_boolean_attrib(element, 'show-python', True) default_tab = pl.get_string_attrib(element, 'default-tab', 'matlab') tab_list = ['matlab', 'mathematica', 'python'] if default_tab not in tab_list: raise Exception(f'invalid default-tab: {default_tab}') # Setting the default tab displayed_tab = [show_matlab, show_mathematica, show_python] if not any(displayed_tab): raise Exception('All tabs have been hidden from display. At least one tab must be shown.') default_tab_index = tab_list.index(default_tab) # If not displayed, make first visible tab the default if not displayed_tab[default_tab_index]: first_display = displayed_tab.index(True) default_tab = tab_list[first_display] default_tab_index = tab_list.index(default_tab) # Active tab should be the default tab default_tab_list = [False, False, False] default_tab_list[default_tab_index] = True [active_tab_matlab, active_tab_mathematica, active_tab_python] = default_tab_list # Process parameter data matlab_data = '' mathematica_data = '' python_data = 'import numpy as np\n\n' for child in element: if child.tag == 'variable': # Raise exception if variable does not have a name pl.check_attribs(child, required_attribs=['params-name'], optional_attribs=['comment', 'digits']) # Get name of variable var_name = pl.get_string_attrib(child, 'params-name') # Get value of variable, raising exception if variable does not exist var_data = data['params'].get(var_name, None) if var_data is None: raise Exception('No value in data["params"] for variable %s in pl-variable-output element' % var_name) # If the variable is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) var_data = pl.from_json(var_data) # Get comment, if it exists var_matlab_comment = '' var_mathematica_comment = '' var_python_comment = '' if pl.has_attrib(child, 'comment'): var_comment = pl.get_string_attrib(child, 'comment') var_matlab_comment = f' % {var_comment}' var_mathematica_comment = f' (* {var_comment} *)' var_python_comment = f' # {var_comment}' # Get digit for child, if it exists if not pl.has_attrib(child, 'digits'): var_digits = digits else: var_digits = pl.get_string_attrib(child, 'digits') # Assembling Python array formatting if np.isscalar(var_data): prefix = '' suffix = '' else: # Wrap the variable in an ndarray (if it's already one, this does nothing) var_data = np.array(var_data) # Check shape of variable if var_data.ndim > 2: raise Exception('Value in data["params"] for variable %s in pl-variable-output element must be a scalar, a vector, or a 2D array' % var_name) # Create prefix/suffix so python string is np.array( ... ) prefix = 'np.array(' suffix = ')' # Mathematica reserved letters: C D E I K N O mathematica_reserved = ['C', 'D', 'E', 'I', 'K', 'N', 'O'] if pl.inner_html(child) in mathematica_reserved: mathematica_suffix = 'm' else: mathematica_suffix = '' # Create string for matlab and python format var_name_disp = pl.inner_html(child) var_matlab_data = pl.string_from_numpy(var_data, language='matlab', digits=var_digits) var_mathematica = pl.string_from_numpy(var_data, language='mathematica', digits=var_digits) var_python_data = pl.string_from_numpy(var_data, language='python', digits=var_digits) matlab_data += f'{var_name_disp} = {var_matlab_data};{var_matlab_comment}\n' mathematica_data += f'{var_name_disp}{mathematica_suffix} = {var_mathematica};{var_mathematica_comment}\n' python_data += f'{var_name_disp} = {prefix}{var_python_data}{suffix}{var_python_comment}\n' html_params = { 'active_tab_matlab': active_tab_matlab, 'active_tab_mathematica': active_tab_mathematica, 'active_tab_python': active_tab_python, 'show_matlab': show_matlab, 'show_mathematica': show_mathematica, 'show_python': show_python, 'matlab_data': matlab_data, 'mathematica_data': mathematica_data, 'python_data': python_data, 'uuid': pl.get_uuid() } with open('pl-variable-output.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() return html
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) pl.check_attribs( element, required_attribs=['params-name'], optional_attribs=['text', 'no-highlight', 'prefix', 'suffix'])
def parse(element_html, data): element = lxml.html.fragment_fromstring(element_html) answer_name = pl.get_string_attrib(element, 'answers-name') temp = answer_name temp += '-input' # the answer_name textfields that raw-submitted-answer reads from # have '-input' appended to their name attribute student_answer_temp = '' if temp in data['raw_submitted_answers']: student_answer_temp = data['raw_submitted_answers'][temp] if student_answer_temp is None: data['format_errors'][answer_name] = 'NULL was submitted as an answer.' return elif student_answer_temp == '': data['format_errors'][answer_name] = 'No answer was submitted.' return student_answer = [] student_answer_indent = [] grading_mode = pl.get_string_attrib(element, 'grading-method', GRADING_METHOD_DEFAULT) student_answer_ranking = ['Question grading_mode is not "ranking"'] student_answer_temp = json.loads(student_answer_temp) student_answer = student_answer_temp['answers'] student_answer_indent = student_answer_temp['answer_indent'] if grading_mode.lower() == 'ranking': student_answer_ranking = [] pl_drag_drop_element = lxml.html.fragment_fromstring(element_html) for answer in student_answer: e = pl_drag_drop_element.xpath(f'.//pl-answer[text()="{answer}"]') is_correct = pl.get_boolean_attrib( e[0], 'correct', PL_ANSWER_CORRECT_DEFAULT) # default correctness to True if is_correct: ranking = pl.get_integer_attrib(e[0], 'ranking', 0) else: ranking = -1 # wrong answers have no ranking student_answer_ranking.append(ranking) 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(student_answer_indent[index]) answer_code += (' ' * indent) + answer + '\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_submission_ordering': student_answer_ranking, 'student_raw_submission': student_answer, 'student_answer_indent': student_answer_indent } if temp in data['submitted_answers']: del data['submitted_answers'][temp]
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) pl.check_attribs(element, required_attribs=[], optional_attribs=['engine', 'params-name-matrix', 'weights', 'weights-digits', 'weights-presentation-type', 'params-name-labels'])
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['answers-name'] optional_attribs = ['weight', 'label', 'comparison', 'rtol', 'atol', 'digits', 'allow-complex'] pl.check_attribs(element, required_attribs, optional_attribs)
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = [] optional_attribs = [] pl.check_attribs(element, required_attribs, optional_attribs)
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['params-name'] optional_attribs = ['digits', 'presentation-type'] pl.check_attribs(element, required_attribs, optional_attribs)
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) pl.check_attribs(element, required_attribs=['file-name'], optional_attribs=['type', 'directory', 'label'])
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) check_attributes_rec(element) w_button = None prev = not pl.get_boolean_attrib(element, 'gradable', defaults.element_defaults['gradable']) load_extensions(data) # Some preparation for elements with grading componenet if not prev: name = pl.get_string_attrib(element, 'answers-name', None) if name is None: raise Exception( 'answers-name is required if gradable mode is enabled') n_id = 0 n_control_elements = 0 answer_child = None initial_child = None for child in element: # Get all the objects in pl-drawing-answer if child.tag == 'pl-drawing-answer': if answer_child is not None: raise Exception( 'You should have only one pl-drawing-answer inside a pl-drawing.' ) draw_error_box = pl.get_boolean_attrib( child, 'draw-error-box', defaults.element_defaults['draw-error-box']) answer_child = child # Get all the objects in pl-drawing-initial if child.tag == 'pl-drawing-initial': if initial_child is not None: raise Exception( 'You should have only one pl-drawing-initial inside a pl-drawing.' ) initial_child = child # Get the width of the vector defined in the pl-drawing-button for pl-vector if child.tag == 'pl-controls': n_control_elements += 1 for groups in child: if groups.tag == 'pl-controls-group': for buttons in groups: if buttons.tag == 'pl-drawing-button': type_name = buttons.attrib.get('type', None) if type_name == 'pl-arc-vector-CCW': type_name = 'pl-arc-vector' elif type_name == 'pl-arc-vector-CW': type_name = 'pl-arc-vector' type_attribs = elements.get_attributes( type_name) if elements.should_validate_attributes( type_name): pl.check_attribs( buttons, required_attribs=['type'], optional_attribs=type_attribs) if buttons.attrib['type'] == 'pl-vector': if 'width' in buttons.attrib: w_button = buttons.attrib['width'] else: w_button = None if answer_child is None: raise Exception( 'You do not have any "pl-drawing-answer" inside pl-drawing where gradable=True. You should either specify the "pl-drawing-answer" if you want to grade objects, or make gradable=False' ) # Generate these in order so that answer elements are displayed on top of initial elements init = None if initial_child is not None: init, n_id = render_drawing_items(initial_child, n_id) ans, n_id = render_drawing_items(answer_child, n_id) # Makes sure that all objects in pl-drawing-answer are graded # and all the objects in pl-drawing--initial are not graded for obj in ans: obj['graded'] = True obj['drawErrorBox'] = draw_error_box if 'objectDrawErrorBox' in obj: if obj['objectDrawErrorBox'] is not None: obj['drawErrorBox'] = obj['objectDrawErrorBox'] # Check to see if consistent width for pl-vector is used for correct answer # and submitted answers that are added using the buttons if obj['gradingName'] == 'vector': if (w_button is None and obj['width'] == defaults.drawing_defaults['force-width'] ) or obj['width'] == float(w_button): continue else: raise Exception( 'Width is not consistent! pl-vector in pl-drawing-answers needs to have the same width of pl-vector in pl-drawing-button.' ) # Combines all the objects in pl-drawing-answers and pl-drawing-initial # and saves in correct_answers if init is not None: for obj in init: obj['graded'] = False data['correct_answers'][name] = union_drawing_items(init, ans) else: data['correct_answers'][name] = ans
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) pl.check_attribs(element, required_attribs=['file-name'], optional_attribs=['type', 'directory', 'label', 'force-download'])
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 prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['answers-name'] optional_attribs = [ 'fixed-order', 'number-statements', 'number-options', 'none-of-the-above', 'blank', 'counter-type' ] pl.check_attribs(element, required_attribs, optional_attribs) name = pl.get_string_attrib(element, 'answers-name') options, statements = categorize_matches(element, data) # Choose and randomize the options and statements. Each can be in a fixed order. fixed_statements_order = pl.get_boolean_attrib( element, 'fixed-order', FIXED_STATEMENTS_ORDER_DEFAULT) number_statements = pl.get_integer_attrib(element, 'number-statements', len(statements)) number_options = pl.get_integer_attrib(element, 'number-options', len(options)) nota = pl.get_boolean_attrib(element, 'none-of-the-above', NOTA_DEFAULT) # Organize the list of statements to use. if fixed_statements_order: if number_statements < len(statements): # Take a random sampling, but maintain the original order of the statements. indices = random.sample(range(len(statements)), number_statements) statements = [statements[i] for i in sorted(indices)] # Otherwise, just use all the statements as-is. else: # Shuffle or sample the statements. if number_statements < len(statements): statements = random.sample(statements, number_statements) else: random.shuffle(statements) # Organize the list of options to use. # First, select all the options associated with the chosen statements. needed_options_keys = set((s['match'] for s in statements)) needed_options, distractors = partition( options, lambda opt: opt['name'] in needed_options_keys) if len(needed_options) < number_options: # The limit is set above the # of options needed to match the chosen statements. # Add distractor options; and None of the Above if needed. more_needed = number_options - len(needed_options) if more_needed >= len(distractors): # Add all distractors. needed_options.extend(distractors) # Add NOTA if that's still not enough. if more_needed > len(distractors): nota = True else: # Add a sample of the distractors. needed_options.extend(random.sample(distractors, more_needed)) options = needed_options random.shuffle(options) elif len(needed_options) > number_options: # The limit is set below the # of options needed. # Add None of the Above to compensate. options = random.sample(needed_options, number_options) nota = True else: # The number of needed options matches the total options. options = needed_options random.shuffle(options) if nota: options.append({ 'index': len(options), 'name': '__nota__', 'html': 'None of the above' }) # Build the options to display to the student. chosen_option_names = [] display_options = [] for (i, opt) in enumerate(options): keyed_option = {'key': opt['name'], 'html': opt['html']} display_options.append(keyed_option) chosen_option_names.append(opt['name']) # Build the statements to display to the student. display_statements = [] correct_matches = [] for (i, statement) in enumerate(statements): # Check if the matched option was removed from the display_options to make room for # none-of-the-above option. if nota and statement['match'] not in chosen_option_names: match_index = len(options) - 1 else: match_index = chosen_option_names.index(statement['match']) keyed_statement = { 'key': str(i), 'html': statement['html'], 'match': statement['match'] } display_statements.append(keyed_statement) correct_matches.append(match_index) data['params'][name] = (display_statements, display_options) data['correct_answers'][name] = correct_matches
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = [] optional_attribs = ['question', 'submission', 'answer'] pl.check_attribs(element, required_attribs, optional_attribs)
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['answers-name'] optional_attribs = ['weight', 'label', 'comparison', 'rtol', 'atol', 'digits', 'allow-complex'] pl.check_attribs(element, required_attribs, optional_attribs)
def prepare(element_html, data): if not use_pl_variable_score: return element = lxml.html.fragment_fromstring(element_html) pl.check_attribs(element, required_attribs=['answers-name'], optional_attribs=[])
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) answer_name = pl.get_string_attrib(element, 'answers-name') required_attribs = ['answers-name'] optional_attribs = [ 'source-blocks-order', 'grading-method', 'indentation', 'source-header', 'solution-header', 'file-name', 'solution-placement', 'max-incorrect', 'min-incorrect', 'weight', 'inline', 'max-indent', 'feedback', 'partial-credit' ] pl.check_attribs(element, required_attribs=required_attribs, optional_attribs=optional_attribs) check_indentation = pl.get_boolean_attrib(element, 'indentation', INDENTION_DEFAULT) grading_method = pl.get_string_attrib(element, 'grading-method', GRADING_METHOD_DEFAULT) feedback_type = pl.get_string_attrib(element, 'feedback', FEEDBACK_DEFAULT) if grading_method in ['dag', 'ranking']: partial_credit_type = pl.get_string_attrib(element, 'partial-credit', 'lcs') if partial_credit_type not in ['none', 'lcs']: raise Exception('partial credit type "' + partial_credit_type + '" is not available with the "' + grading_method + '" grading-method.') elif pl.get_string_attrib(element, 'partial-credit', None) is not None: raise Exception( 'You may only specify partial credit options in the DAG and ranking grading modes.' ) accepted_grading_method = [ 'ordered', 'unordered', 'ranking', 'dag', 'external' ] if grading_method not in accepted_grading_method: raise Exception( 'The grading-method attribute must be one of the following: ' + ', '.join(accepted_grading_method)) if (grading_method not in ['dag', 'ranking'] and feedback_type != 'none') or \ (grading_method in ['dag', 'ranking'] and feedback_type not in ['none', 'first-wrong']): raise Exception('feedback type "' + feedback_type + '" is not available with the "' + grading_method + '" grading-method.') correct_answers = [] incorrect_answers = [] def prepare_tag(html_tags, index, group_info={ 'tag': None, 'depends': None }): if html_tags.tag != 'pl-answer': raise Exception( 'Any html tags nested inside <pl-order-blocks> must be <pl-answer> or <pl-block-group>. \ Any html tags nested inside <pl-block-group> must be <pl-answer>' ) if grading_method == 'external': pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct']) elif grading_method in ['unordered', 'ordered']: pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct', 'indent']) elif grading_method == 'ranking': pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct', 'ranking', 'indent']) elif grading_method == 'dag': pl.check_attribs(html_tags, required_attribs=[], optional_attribs=[ 'correct', 'tag', 'depends', 'comment', 'indent' ]) is_correct = pl.get_boolean_attrib(html_tags, 'correct', PL_ANSWER_CORRECT_DEFAULT) answer_indent = pl.get_integer_attrib(html_tags, 'indent', None) inner_html = pl.inner_html(html_tags) ranking = pl.get_integer_attrib(html_tags, 'ranking', -1) tag, depends = get_graph_info(html_tags) if grading_method == 'ranking': tag = str(index) if check_indentation is False and answer_indent is not None: raise Exception( '<pl-answer> should not specify indentation if indentation is disabled.' ) answer_data_dict = { 'inner_html': inner_html, 'indent': answer_indent, 'ranking': ranking, 'index': index, 'tag': tag, # set by HTML with DAG grader, set internally for ranking grader 'depends': depends, # only used with DAG grader 'group_info': group_info # only used with DAG grader } if is_correct: correct_answers.append(answer_data_dict) else: incorrect_answers.append(answer_data_dict) index = 0 for html_tags in element: # iterate through the html tags inside pl-order-blocks if html_tags.tag is etree.Comment: continue elif html_tags.tag == 'pl-block-group': if grading_method != 'dag': raise Exception( 'Block groups only supported in the "dag" grading mode.') group_tag, group_depends = get_graph_info(html_tags) for grouped_tag in html_tags: if html_tags.tag is etree.Comment: continue else: prepare_tag(grouped_tag, index, { 'tag': group_tag, 'depends': group_depends }) index += 1 else: prepare_tag(html_tags, index) index += 1 if grading_method != 'external' and len(correct_answers) == 0: raise Exception( 'There are no correct answers specified for this question.') all_incorrect_answers = len(incorrect_answers) max_incorrect = pl.get_integer_attrib(element, 'max-incorrect', all_incorrect_answers) min_incorrect = pl.get_integer_attrib(element, 'min-incorrect', all_incorrect_answers) if min_incorrect > len(incorrect_answers) or max_incorrect > len( incorrect_answers): raise Exception( 'The min-incorrect or max-incorrect attribute may not exceed the number of incorrect <pl-answers>.' ) if min_incorrect > max_incorrect: raise Exception( 'The attribute min-incorrect must be smaller than max-incorrect.') incorrect_answers_count = random.randint(min_incorrect, max_incorrect) sampled_correct_answers = correct_answers sampled_incorrect_answers = random.sample(incorrect_answers, incorrect_answers_count) mcq_options = sampled_correct_answers + sampled_incorrect_answers source_blocks_order = pl.get_string_attrib(element, 'source-blocks-order', SOURCE_BLOCKS_ORDER_DEFAULT) if source_blocks_order == 'random': random.shuffle(mcq_options) elif source_blocks_order == 'ordered': mcq_options.sort(key=lambda a: a['index']) else: raise Exception( 'The specified option for the "source-blocks-order" attribute is invalid.' ) for option in mcq_options: option['uuid'] = pl.get_uuid() data['params'][answer_name] = mcq_options data['correct_answers'][answer_name] = correct_answers # if the order of the blocks in the HTML is a correct solution, leave it unchanged, but if it # isn't we need to change it into a solution before displaying it as such data_copy = deepcopy(data) data_copy['submitted_answers'] = {answer_name: correct_answers} data_copy['partial_scores'] = {} grade(element_html, data_copy) if data_copy['partial_scores'][answer_name]['score'] != 1: data['correct_answers'][answer_name] = solve_problem( correct_answers, grading_method)
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = [] optional_attribs = [] pl.check_attribs(element, required_attribs, optional_attribs)
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 prepare(element_html, element_index, data): element = lxml.html.fragment_fromstring(element_html) pl.check_attribs(element, required_attribs=['file_name'], optional_attribs=['width', 'type', 'directory']) return data
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) digits = pl.get_integer_attrib(element, 'digits', DIGITS_DEFAULT) matlab_data = '' python_data = 'import numpy as np\n\n' for child in element: if child.tag == 'variable': # Raise exception of variable does not have a name pl.check_attribs(child, required_attribs=['params-name'], optional_attribs=[]) # Get name of variable var_name = pl.get_string_attrib(child, 'params-name') # Get value of variable, raising exception if variable does not exist var_data = data['params'].get(var_name, None) if var_data is None: raise Exception( 'No value in data["params"] for variable %s in pl-matrix-output element' % var_name) # If the variable is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) var_data = pl.from_json(var_data) if np.isscalar(var_data): prefix = '' suffix = '' else: # Wrap the variable in an ndarray (if it's already one, this does nothing) var_data = np.array(var_data) # Check shape of variable if var_data.ndim != 2: raise Exception( 'Value in data["params"] for variable %s in pl-matrix-output element must be a scalar or a 2D array' % var_name) # Create prefix/suffix so python string is np.array( ... ) prefix = 'np.array(' suffix = ')' # Create string for matlab and python format matlab_data += pl.inner_html( child) + ' = ' + pl.string_from_2darray( var_data, language='matlab', digits=digits) + ';\n' python_data += pl.inner_html( child) + ' = ' + prefix + pl.string_from_2darray( var_data, language='python', digits=digits) + suffix + '\n' html_params = { 'default_is_matlab': True, 'matlab_data': matlab_data, 'python_data': python_data, 'uuid': pl.get_uuid() } with open('pl-matrix-output.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() return html
def 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 = None if answer_raw_name in data['raw_submitted_answers']: student_answer = data['raw_submitted_answers'][answer_raw_name] student_answer = json.loads(student_answer) 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) 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]
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 prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['answers-name'] optional_attribs = ['weight', 'number-answers', 'fixed-order', 'inline', 'hide-letter-keys', 'none-of-the-above', 'none-of-the-above-feedback', 'all-of-the-above', 'all-of-the-above-feedback', 'external-json', 'external-json-correct-key', 'external-json-incorrect-key'] pl.check_attribs(element, required_attribs, optional_attribs) name = pl.get_string_attrib(element, 'answers-name') correct_answers, incorrect_answers = categorize_options(element, data) len_correct = len(correct_answers) len_incorrect = len(incorrect_answers) len_total = len_correct + len_incorrect enable_nota = pl.get_boolean_attrib(element, 'none-of-the-above', NONE_OF_THE_ABOVE_DEFAULT) enable_aota = pl.get_boolean_attrib(element, 'all-of-the-above', ALL_OF_THE_ABOVE_DEFAULT) nota_correct = False aota_correct = False if enable_nota or enable_aota: prob_space = len_correct + enable_nota + enable_aota rand_int = random.randint(1, prob_space) # Either 'None of the above' or 'All of the above' is correct # with probability 1/(number_correct + enable-nota + enable-aota). # However, if len_correct is 0, nota_correct is guaranteed to be True. # Thus, if no correct option is provided, 'None of the above' will always # be correct, and 'All of the above' always incorrect nota_correct = enable_nota and (rand_int == 1 or len_correct == 0) # 'All of the above' will always be correct when no incorrect option is # provided, while still never both True aota_correct = enable_aota and (rand_int == 2 or len_incorrect == 0) and not nota_correct if len_correct < 1 and not enable_nota: # This means the code needs to handle the special case when len_correct == 0 raise Exception('pl-multiple-choice element must have at least 1 correct answer or set none-of-the-above') if enable_aota and len_correct < 2: # To prevent confusion on the client side raise Exception('pl-multiple-choice element must have at least 2 correct answers when all-of-the-above is set') # 1. Pick the choice(s) to display number_answers = pl.get_integer_attrib(element, 'number-answers', None) # determine if user provides number-answers set_num_answers = True if number_answers is None: set_num_answers = False number_answers = len_total + enable_nota + enable_aota # figure out how many choice(s) to choose from the *provided* choices, # excluding 'none-of-the-above' and 'all-of-the-above' number_answers -= (enable_nota + enable_aota) expected_num_answers = number_answers if enable_aota: # min number if 'All of the above' is correct number_answers = min(len_correct, number_answers) # raise exception when the *provided* number-answers can't be satisfied if set_num_answers and number_answers < expected_num_answers: raise Exception(f'Not enough correct choices for all-of-the-above. Need {expected_num_answers - number_answers} more') if enable_nota: # if nota correct number_answers = min(len_incorrect, number_answers) # raise exception when the *provided* number-answers can't be satisfied if set_num_answers and number_answers < expected_num_answers: raise Exception(f'Not enough incorrect choices for none-of-the-above. Need {expected_num_answers - number_answers} more') # this is the case for # - 'All of the above' is incorrect # - 'None of the above' is incorrect # - nota and aota disabled number_answers = min(min(1, len_correct) + len_incorrect, number_answers) if aota_correct: # when 'All of the above' is correct, we choose all from correct # and none from incorrect number_correct = number_answers number_incorrect = 0 elif nota_correct: # when 'None of the above' is correct, we choose all from incorrect # and none from correct number_correct = 0 number_incorrect = number_answers else: # PROOF: by the above probability, if len_correct == 0, then nota_correct # conversely; if not nota_correct, then len_correct != 0. Since len_correct # is none negative, this means len_correct >= 1. number_correct = 1 number_incorrect = max(0, number_answers - number_correct) if not (0 <= number_incorrect <= len_incorrect): raise Exception('INTERNAL ERROR: number_incorrect: (%d, %d, %d)' % (number_incorrect, len_incorrect, number_answers)) # 2. Sample correct and incorrect choices sampled_correct = random.sample(correct_answers, number_correct) sampled_incorrect = random.sample(incorrect_answers, number_incorrect) sampled_answers = sampled_correct + sampled_incorrect random.shuffle(sampled_answers) # 3. Modify sampled choices fixed_order = pl.get_boolean_attrib(element, 'fixed-order', FIXED_ORDER_DEFAULT) if fixed_order: # we can't simply skip the shuffle because we already broke the original # order by separating into correct/incorrect lists sampled_answers.sort(key=lambda a: a[0]) # sort by stored original index inline = pl.get_boolean_attrib(element, 'inline', INLINE_DEFAULT) if enable_aota: if inline: aota_text = 'All of these' else: aota_text = 'All of the above' # Add 'All of the above' option after shuffling aota_feedback = pl.get_string_attrib(element, 'all-of-the-above-feedback', FEEDBACK_DEFAULT) sampled_answers.append((len_total, aota_correct, aota_text, aota_feedback)) if enable_nota: if inline: nota_text = 'None of these' else: nota_text = 'None of the above' # Add 'None of the above' option after shuffling nota_feedback = pl.get_string_attrib(element, 'none-of-the-above-feedback', FEEDBACK_DEFAULT) sampled_answers.append((len_total + 1, nota_correct, nota_text, nota_feedback)) # 4. Write to data # Because 'All of the above' is below all the correct choice(s) when it's # true, the variable correct_answer will save it as correct, and # overwriting previous choice(s) display_answers = [] correct_answer = None for (i, (index, correct, html, feedback)) in enumerate(sampled_answers): keyed_answer = {'key': pl.index2key(i), 'html': html, 'feedback': feedback} 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 prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) pl.check_attribs(element, [], [])
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) pl.check_attribs(element, required_attribs=['answers-name'], optional_attribs=[ 'source-blocks-order', 'grading-method', 'indentation', 'source-header', 'solution-header', 'file-name', 'solution-placement', 'max-incorrect', 'min-incorrect', 'weight' ]) answer_name = pl.get_string_attrib(element, 'answers-name') mcq_options = [] html_ordering = [] correct_answers = [] correct_answers_indent = [] correct_answers_ranking = [] incorrect_answers = [] check_indentation = pl.get_boolean_attrib(element, 'indentation', INDENTION_DEFAULT) grading_method = pl.get_string_attrib(element, 'grading-method', GRADING_METHOD_DEFAULT) accepted_grading_method = ['ordered', 'unordered', 'ranking', 'external'] if grading_method not in accepted_grading_method: raise Exception( 'The grading-method attribute must be one of the following: ' + accepted_grading_method) for html_tags in element: # iterate through the tags inside pl-order-blocks, should be <pl-answer> tags if html_tags.tag == 'pl-answer': if grading_method == 'external': pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct']) else: pl.check_attribs( html_tags, required_attribs=[], optional_attribs=['correct', 'ranking', 'indent']) is_correct = pl.get_boolean_attrib(html_tags, 'correct', PL_ANSWER_CORRECT_DEFAULT) if check_indentation is False: try: answer_indent = pl.get_string_attrib(html_tags, 'indent') except Exception: answer_indent = -1 else: raise Exception( '<pl-answer> should not specify indentation if indentation is disabled.' ) else: answer_indent = pl.get_integer_attrib( html_tags, 'indent', PL_ANSWER_INDENT_DEFAULT ) # get answer indent, and default to -1 (indent level ignored) if is_correct is True: # add option to the correct answer array, along with the correct required indent if pl.get_string_attrib(html_tags, 'ranking', '') != '': ranking = pl.get_string_attrib(html_tags, 'ranking') try: ranking = int(ranking) - 1 except ValueError: raise Exception( 'Ranking specified in <pl-answer> is not a number.' ) correct_answers_ranking.append(ranking) correct_answers.append(html_tags.text) correct_answers_indent.append(answer_indent) else: incorrect_answers.append(html_tags.text) html_ordering.append(html_tags.text) else: raise Exception( 'Tags nested inside <pl-order-blocks> must be <pl-answer>.') if pl.get_string_attrib(element, 'grading-method', GRADING_METHOD_DEFAULT ) != 'external' and len(correct_answers) == 0: raise Exception( 'There are no correct answers specified for this question.') if (correct_answers_ranking != sorted(correct_answers_ranking)): # sort correct answers by indices specified in corect_answers_ranking correct_answers = [ x for _, x in sorted(zip(correct_answers_ranking, correct_answers)) ] min_incorrect = pl.get_integer_attrib(element, 'min-incorrect', MIN_INCORRECT_DEFAULT) max_incorrect = pl.get_integer_attrib(element, 'max-incorrect', MAX_INCORRECT_DEFAULT) if ((min_incorrect is None) & (max_incorrect is None)): mcq_options = correct_answers + incorrect_answers else: # Setting default for min-correct and checking for correct interval if min_incorrect is None: min_incorrect = 1 else: if min_incorrect > len(incorrect_answers): raise Exception( 'min-incorrect must be less than or equal to the number of incorrect <pl-answers>.' ) # Setting default for max-correct and checking for correct interval if max_incorrect is None: max_incorrect = len(incorrect_answers) else: if max_incorrect > len(incorrect_answers): raise Exception( 'max-incorrect must be less than or equal to the number of incorrect <pl-answers>.' ) if min_incorrect > max_incorrect: raise Exception( 'min-incorrect must be smaller than max-incorrect.') incorrect_answers_count = random.randint(min_incorrect, max_incorrect) mcq_options = correct_answers + random.sample(incorrect_answers, incorrect_answers_count) source_blocks_order = pl.get_string_attrib(element, 'source-blocks-order', SOURCE_BLOCKS_ORDER_DEFAULT) if source_blocks_order == 'random': random.shuffle(mcq_options) elif source_blocks_order == 'ordered': mcq_options = html_ordering else: raise Exception( 'the selected option for "source-blocks-order" does not exist.') data['params'][answer_name] = mcq_options data['correct_answers'][answer_name] = { 'correct_answers': correct_answers, 'correct_answers_indent': correct_answers_indent }
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 = ['script-name'] optional_attribs = ['param-names', 'width', 'height'] pl.check_attribs(element, required_attribs, optional_attribs) return data
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) pl.check_attribs(element, required_attribs=['file-name'], optional_attribs=['width', 'type', 'directory'])
def prepare(element_html, element_index, data): element = lxml.html.fragment_fromstring(element_html) pl.check_attribs(element, required_attribs=[], optional_attribs=['digits'])
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = [] optional_attribs = ['digits', 'default-tab', 'show-matlab', 'show-mathematica', 'show-python'] pl.check_attribs(element, required_attribs, optional_attribs)
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 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) answer_name = pl.get_string_attrib(element, 'answers-name') required_attribs = ['answers-name'] optional_attribs = [ 'source-blocks-order', 'grading-method', 'indentation', 'source-header', 'solution-header', 'file-name', 'solution-placement', 'max-incorrect', 'min-incorrect', 'weight', 'inline', 'max-indent', 'feedback' ] pl.check_attribs(element, required_attribs=required_attribs, optional_attribs=optional_attribs) check_indentation = pl.get_boolean_attrib(element, 'indentation', INDENTION_DEFAULT) grading_method = pl.get_string_attrib(element, 'grading-method', GRADING_METHOD_DEFAULT) feedback_type = pl.get_string_attrib(element, 'feedback', FEEDBACK_DEFAULT) accepted_grading_method = [ 'ordered', 'unordered', 'ranking', 'dag', 'external' ] if grading_method not in accepted_grading_method: raise Exception( 'The grading-method attribute must be one of the following: ' + accepted_grading_method) if (grading_method != 'dag' and feedback_type != 'none') or \ (grading_method == 'dag' and feedback_type not in ['none', 'first-wrong']): raise Exception('feedback type "' + feedback_type + '" is not available with the "' + grading_method + '" grading-method.') correct_answers = [] incorrect_answers = [] def prepare_tag(html_tags, index, group=None): if html_tags.tag != 'pl-answer': raise Exception( 'Any html tags nested inside <pl-order-blocks> must be <pl-answer> or <pl-block-group>. \ Any html tags nested inside <pl-block-group> must be <pl-answer>' ) if grading_method == 'external': pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct']) elif grading_method == 'unordered': pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct', 'indent']) elif grading_method in ['ranking', 'ordered']: pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct', 'ranking', 'indent']) elif grading_method == 'dag': pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct', 'tag', 'depends']) is_correct = pl.get_boolean_attrib(html_tags, 'correct', PL_ANSWER_CORRECT_DEFAULT) answer_indent = pl.get_integer_attrib(html_tags, 'indent', None) inner_html = pl.inner_html(html_tags) ranking = pl.get_integer_attrib(html_tags, 'ranking', -1) tag = pl.get_string_attrib(html_tags, 'tag', None) depends = pl.get_string_attrib(html_tags, 'depends', '') depends = depends.strip().split(',') if depends else [] if check_indentation is False and answer_indent is not None: raise Exception( '<pl-answer> should not specify indentation if indentation is disabled.' ) answer_data_dict = { 'inner_html': inner_html, 'indent': answer_indent, 'ranking': ranking, 'index': index, 'tag': tag, # only used with DAG grader 'depends': depends, # only used with DAG grader 'group': group # only used with DAG grader } if is_correct: correct_answers.append(answer_data_dict) else: incorrect_answers.append(answer_data_dict) index = 0 group_counter = 0 for html_tags in element: # iterate through the html tags inside pl-order-blocks if html_tags.tag is lxml.etree.Comment: continue elif html_tags.tag == 'pl-block-group': if grading_method != 'dag': raise Exception( 'Block groups only supported in the "dag" grading mode.') group_counter += 1 for grouped_tag in html_tags: if html_tags.tag is lxml.etree.Comment: continue else: prepare_tag(grouped_tag, index, group_counter) index += 1 else: prepare_tag(html_tags, index) index += 1 if pl.get_string_attrib(element, 'grading-method', GRADING_METHOD_DEFAULT ) != 'external' and len(correct_answers) == 0: raise Exception( 'There are no correct answers specified for this question.') all_incorrect_answers = len(incorrect_answers) max_incorrect = pl.get_integer_attrib(element, 'max-incorrect', all_incorrect_answers) min_incorrect = pl.get_integer_attrib(element, 'min-incorrect', all_incorrect_answers) if min_incorrect > len(incorrect_answers) or max_incorrect > len( incorrect_answers): raise Exception( 'The min-incorrect or max-incorrect attribute may not exceed the number of incorrect <pl-answers>.' ) if min_incorrect > max_incorrect: raise Exception( 'The attribute min-incorrect must be smaller than max-incorrect.') incorrect_answers_count = random.randint(min_incorrect, max_incorrect) sampled_correct_answers = correct_answers sampled_incorrect_answers = random.sample(incorrect_answers, incorrect_answers_count) mcq_options = sampled_correct_answers + sampled_incorrect_answers source_blocks_order = pl.get_string_attrib(element, 'source-blocks-order', SOURCE_BLOCKS_ORDER_DEFAULT) if source_blocks_order == 'random': random.shuffle(mcq_options) elif source_blocks_order == 'ordered': mcq_options.sort(key=lambda a: a['index']) else: raise Exception( 'The specified option for the "source-blocks-order" attribute is invalid.' ) # data['params'][answer_name] = filter_keys_from_array(mcq_options, 'inner_html') for option in mcq_options: option['uuid'] = pl.get_uuid() data['params'][answer_name] = mcq_options data['correct_answers'][answer_name] = correct_answers
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['script-name'] optional_attribs = ['param-names', 'width', 'height'] pl.check_attribs(element, required_attribs, optional_attribs) return data