def test(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') weight = pl.get_integer_attrib(element, 'weight', 1) allow_blank = pl.get_string_attrib(element, 'allow-blank', False) # Get correct answer a_tru = data['correct_answers'][name] # If correct answer is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) a_tru = pl.from_json(a_tru) if allow_blank: # no invalid answer implemented when allow-blank="true" result = random.choices(['correct', 'incorrect'], [5, 5])[0] else: result = random.choices(['correct', 'incorrect', 'invalid'], [5, 5, 1])[0] if result == 'correct': data['raw_submitted_answers'][name] = a_tru data['partial_scores'][name] = {'score': 1, 'weight': weight} elif result == 'incorrect': data['raw_submitted_answers'][name] = a_tru + str((random.randint(1, 11) * random.choice([-1, 1]))) data['partial_scores'][name] = {'score': 0, 'weight': weight} elif result == 'invalid': data['raw_submitted_answers'][name] = '' data['format_errors'][name] = 'invalid' else: raise Exception('invalid result: %s' % result)
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = [] optional_attribs = [ 'source-file-name', 'submitted-file-name', 'contents', 'language' ] pl.check_attribs(element, required_attribs, optional_attribs) source_file_name = pl.get_string_attrib(element, 'source-file-name', SOURCE_FILE_NAME_DEFAULT) submitted_file_name = pl.get_string_attrib(element, 'submitted-file-name', SUBMITTED_FILE_NAME_DEFAULT) contents = pl.get_string_attrib(element, 'contents', CONTENTS_DEFAULT) language = pl.get_string_attrib(element, 'language', LANGUAGE_DEFAULT) if (source_file_name is not None and (submitted_file_name is not None or contents is not None)) or ( submitted_file_name is not None and contents is not None): raise Exception( 'Only one of the attributes "source-file-name", "submitted-file-name" and "contents" can be used.' ) if language not in ['html', 'markdown']: raise Exception( 'Attribute "language" must be either "html" or "markdown".')
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) force_text = pl.get_boolean_attrib(element, 'text', TEXT_DEFAULT) varname = pl.get_string_attrib(element, 'params-name') if varname not in data['params']: raise Exception('Could not find {} in params!'.format(varname)) var_out = data['params'][varname] html = '' var_type = 'text' # determine the type of variable to render if isinstance(var_out, dict) and '_type' in var_out: if not force_text: var_type = var_out['_type'] var_out = pl.from_json(var_out) # render the output variable if var_type == 'dataframe': html += var_out.to_html( classes=['pl-python-variable-table'] ) + '<p class="pl-python-variable-table-dimensions">{} rows x {} columns</p><br>'.format( str(var_out.shape[0]), str(var_out.shape[1])) else: no_highlight = pl.get_boolean_attrib(element, 'no-highlight', NO_HIGHLIGHT_DEFAULT) prefix = pl.get_string_attrib(element, 'prefix', PREFIX_DEFAULT) suffix = pl.get_string_attrib(element, 'suffix', SUFFIX_DEFAULT) text = prefix + repr(var_out) + suffix html += '<pl-code language="python" no-highlight="{}">{}</pl-code>'.format( no_highlight, text) return html
def parse(element_html, data): element = lxml.html.fragment_fromstring(element_html) answer_name = pl.get_string_attrib(element, 'answers-name') answer_raw_name = answer_name + '-input' student_answer = '' if answer_raw_name in data['raw_submitted_answers']: student_answer = data['raw_submitted_answers'][answer_raw_name] if student_answer is None or student_answer == '': data['format_errors'][answer_name] = 'No answer was submitted.' return grading_mode = pl.get_string_attrib(element, 'grading-method', GRADING_METHOD_DEFAULT) student_answer = json.loads(student_answer) correct_answers = data['correct_answers'][answer_name] if grading_mode == 'ranking': for answer in student_answer: search = next((item for item in correct_answers if item['inner_html'] == answer['inner_html']), None) answer['ranking'] = search[ 'ranking'] if search is not None else -1 # wrong answers have no ranking elif grading_mode == 'dag': for answer in student_answer: search = next((item for item in correct_answers if item['inner_html'] == answer['inner_html']), None) answer['tag'] = search['tag'] if search is not None else None if pl.get_string_attrib(element, 'grading-method', 'ordered') == 'external': for html_tags in element: if html_tags.tag == 'pl-answer': pl.check_attribs(html_tags, required_attribs=[], optional_attribs=[]) file_name = pl.get_string_attrib(element, 'file-name', FILE_NAME_DEFAULT) answer_code = '' for index, answer in enumerate(student_answer): indent = int(answer['indent']) answer_code += (' ' * indent) + answer['inner_html'] + '\n' if len(answer_code) == 0: data['format_errors']['_files'] = 'The submitted file was empty.' else: data['submitted_answers']['_files'] = [{ 'name': file_name, 'contents': base64.b64encode(answer_code.encode('utf-8')).decode('utf-8') }] data['submitted_answers'][answer_name] = student_answer if answer_raw_name in data['submitted_answers']: del data['submitted_answers'][answer_raw_name]
def parse(element_html, element_index, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers_name') variables = get_variables_list( pl.get_string_attrib(element, 'variables', None)) # Get submitted answer or return parse_error if it does not exist a_sub = data['submitted_answers'].get(name, None) if not a_sub: data['format_errors'][name] = 'No submitted answer.' data['submitted_answers'][name] = None return data try: # Replace '^' with '**' wherever it appears. In MATLAB, either can be used # for exponentiation. In python, only the latter can be used. a_sub = a_sub.replace('^', '**') # Convert submitted answer safely to sympy a_sub = convert_string_to_sympy(a_sub, variables) # Store result as a string. data['submitted_answers'][name] = str(a_sub) except: data['format_errors'][name] = 'Invalid format.' data['submitted_answers'][name] = None return data return data
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) script_name = pl.get_string_attrib(element, 'script-name', None) with open(os.path.join(data['options']['question_path'], script_name)) as f: script = f.read() width = pl.get_string_attrib(element, 'width', '500') height = pl.get_string_attrib(element, 'height', '300') params_names = pl.get_string_attrib(element, 'param-names', None) if params_names is None: client_params = {} else: params_names = params_names.split(sep=',') client_params = {key: data['params'][key] for key in params_names} html_params = { 'script': script, 'width': width, 'height': height, 'client_params': client_params, 'uuid': pl.get_uuid(), } with open('pl-prairiedraw-figure.mustache') as f: html = chevron.render(f, html_params).strip() return html
def parse(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') # Get allow-blank option allow_blank = pl.get_string_attrib(element, 'allow-blank', ALLOW_BLANK_DEFAULT) normalize_to_ascii = pl.get_boolean_attrib(element, 'normalize-to-ascii', NORMALIZE_TO_ASCII_DEFAULT) # Get submitted answer or return parse_error if it does not exist a_sub = data['submitted_answers'].get(name, None) if a_sub is None: data['format_errors'][name] = 'No submitted answer.' data['submitted_answers'][name] = None return if normalize_to_ascii: a_sub = unidecode(a_sub) data['submitted_answers'][name] = a_sub if not a_sub and not allow_blank: data['format_errors'][ name] = 'Invalid format. The submitted answer was left blank.' data['submitted_answers'][name] = None else: data['submitted_answers'][name] = pl.to_json(a_sub)
def grade(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') # Get weight weight = pl.get_integer_attrib(element, 'weight', WEIGHT_DEFAULT) # Get remove-spaces option remove_spaces = pl.get_string_attrib(element, 'remove-spaces', REMOVE_SPACES_DEFAULT) # Get remove-leading-trailing option remove_leading_trailing = pl.get_string_attrib( element, 'remove-leading-trailing', REMOVE_LEADING_TRAILING_DEFAULT) # Get string case sensitivity option ignore_case = pl.get_string_attrib(element, 'ignore-case', IGNORE_CASE_DEFAULT) # Get true answer (if it does not exist, create no grade - leave it # up to the question code) a_tru = pl.from_json(data['correct_answers'].get(name, None)) if a_tru is None: return # explicitly cast the true answer to a string, to handle the case where the answer might be a number or some other type a_tru = str(a_tru) # Get submitted answer (if it does not exist, score is zero) a_sub = data['submitted_answers'].get(name, None) if a_sub is None: data['partial_scores'][name] = {'score': 0, 'weight': weight} return # If submitted answer is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) a_sub = pl.from_json(a_sub) # explicitly cast the submitted answer to a string a_sub = str(a_sub) # Remove the leading and trailing characters if (remove_leading_trailing): a_sub = a_sub.strip() a_tru = a_tru.strip() # Remove the blank spaces between characters if (remove_spaces): a_sub = ''.join(a_sub.split()) a_tru = ''.join(a_tru.split()) # Modify string case for submission and true answer to be lower. if (ignore_case): a_sub = a_sub.lower() a_tru = a_tru.lower() if a_tru == a_sub: data['partial_scores'][name] = {'score': 1, 'weight': weight} else: data['partial_scores'][name] = {'score': 0, 'weight': weight}
def test(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') weight = pl.get_integer_attrib(element, 'weight', 1) allow_blank = pl.get_string_attrib(element, 'allow-blank', False) # Get correct answer a_tru = data['correct_answers'][name] # If correct answer is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) a_tru = pl.from_json(a_tru) if allow_blank: # no invalid answer implemented when allow-blank="true" result = random.choices(['correct', 'incorrect'], [5, 5])[0] else: result = random.choices(['correct', 'incorrect', 'invalid'], [5, 5, 1])[0] if result == 'correct': data['raw_submitted_answers'][name] = a_tru data['partial_scores'][name] = {'score': 1, 'weight': weight} elif result == 'incorrect': data['raw_submitted_answers'][name] = a_tru + str( (random.randint(1, 11) * random.choice([-1, 1]))) data['partial_scores'][name] = {'score': 0, 'weight': weight} elif result == 'invalid': data['raw_submitted_answers'][name] = '' data['format_errors'][name] = 'invalid' else: raise Exception('invalid result: %s' % result)
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) script_name = pl.get_string_attrib(element, 'script-name', None) with open(os.path.join(data['options']['question_path'], script_name)) as f: script = f.read() width = pl.get_string_attrib(element, 'width', '500') height = pl.get_string_attrib(element, 'height', '300') params_names = pl.get_string_attrib(element, 'param-names', None) if params_names is None: client_params = {} else: params_names = params_names.split(sep=',') client_params = {key: data['params'][key] for key in params_names} html_params = { 'script': script, 'width': width, 'height': height, 'client_params': client_params, 'uuid': pl.get_uuid(), } with open('pl-prairiedraw-figure.mustache') as f: html = chevron.render(f, html_params).strip() return html
def grade(element_html, element_index, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers_name') # Get weight weight = pl.get_integer_attrib(element, 'weight', 1) # Get true answer (if it does not exist, create no grade - leave it # up to the question code) a_tru = data['correct_answers'].get(name, None) if a_tru is None: return data # Get submitted answer (if it does not exist, score is zero) a_sub = data['submitted_answers'].get(name, None) if a_sub is None: data['partial_scores'][name] = {'score': 0, 'weight': weight} return data # Parse both correct and submitted answer (will throw an error on fail). variables = get_variables_list( pl.get_string_attrib(element, 'variables', None)) if isinstance(a_tru, str): a_tru = convert_string_to_sympy(a_tru, variables) a_sub = convert_string_to_sympy(a_sub, variables) # Check equality correct = a_tru.equals(a_sub) if correct: data['partial_scores'][name] = {'score': 1, 'weight': weight} else: data['partial_scores'][name] = {'score': 0, 'weight': weight} return data
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = [] optional_attribs = [ 'language', 'no-highlight', 'source-file-name', 'prevent-select', 'highlight-lines', 'highlight-lines-color' ] pl.check_attribs(element, required_attribs, optional_attribs) language = pl.get_string_attrib(element, 'language', LANGUAGE_DEFAULT) if language is not None: lexer = get_lexer_by_name(language) if lexer is None: allowed_languages = map(lambda tup: tup[1][0], pygments.lexers.get_all_lexers()) raise Exception( f'Unknown language: "{language}". Must be one of {", ".join(allowed_languages)}' ) source_file_name = pl.get_string_attrib(element, 'source-file-name', SOURCE_FILE_NAME_DEFAULT) if source_file_name is not None: if element.text is not None and not str(element.text).isspace(): raise Exception( 'Existing code cannot be added inside html element when "source-file-name" attribute is used.' ) highlight_lines = pl.get_string_attrib(element, 'highlight-lines', HIGHLIGHT_LINES_DEFAULT) if highlight_lines is not None: if parse_highlight_lines(highlight_lines) is None: raise Exception( 'Could not parse highlight-lines attribute; check your syntax')
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 prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['answers-name'] optional_attribs = [ 'weight', 'correct-answer', 'label', 'suffix', 'display', 'size', 'show-help-text', 'base', 'allow-blank', 'blank-value' ] pl.check_attribs(element, required_attribs, optional_attribs) name = pl.get_string_attrib(element, 'answers-name') base = pl.get_integer_attrib(element, 'base', BASE_DEFAULT) if base != 0 and (base < 2 or base > 36): raise Exception('Base must be either 0, or between 2 and 36') correct_answer = pl.get_string_attrib(element, 'correct-answer', CORRECT_ANSWER_DEFAULT) if correct_answer is not None: if name in data['correct_answers']: raise Exception('duplicate correct_answers variable name: %s' % name) # Test conversion, but leave as string so proper value is shown on answer panel if pl.string_to_integer(correct_answer, base) is None: raise Exception('correct answer is not a valid input: %s' % name) data['correct_answers'][name] = correct_answer
def grade(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') weight = pl.get_integer_attrib(element, 'weight', WEIGHT_DEFAULT) partial_credit = pl.get_boolean_attrib(element, 'partial-credit', PARTIAL_CREDIT_DEFAULT) number_answers = len(data['params'][name]) partial_credit_method = pl.get_string_attrib(element, 'partial-credit-method', PARTIAL_CREDIT_METHOD_DEFAULT) submitted_keys = data['submitted_answers'].get(name, []) correct_answer_list = data['correct_answers'].get(name, []) correct_keys = [answer['key'] for answer in correct_answer_list] submittedSet = set(submitted_keys) correctSet = set(correct_keys) score = 0 if not partial_credit and submittedSet == correctSet: score = 1 elif partial_credit: if partial_credit_method == 'PC': if submittedSet == correctSet: score = 1 else: n_correct_answers = len(correctSet) - len(correctSet - submittedSet) points = n_correct_answers - len(submittedSet - correctSet) score = max(0, points / len(correctSet)) else: # this is the default EDC method number_wrong = len(submittedSet - correctSet) + len(correctSet - submittedSet) score = 1 - 1.0 * number_wrong / number_answers data['partial_scores'][name] = {'score': score, 'weight': weight}
def test(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') weight = pl.get_integer_attrib(element, 'weight', WEIGHT_DEFAULT) allow_blank = pl.get_string_attrib(element, 'allow-blank', ALLOW_BLANK_DEFAULT) # Get correct answer a_tru = data['correct_answers'][name] # If correct answer is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) a_tru = pl.from_json(a_tru) result = data['test_type'] if result == 'invalid' and allow_blank: # We can't have an invalid submission with allow_blank, so just test correct result = 'correct' if result == 'correct': data['raw_submitted_answers'][name] = a_tru data['partial_scores'][name] = {'score': 1, 'weight': weight} elif result == 'incorrect': data['raw_submitted_answers'][name] = a_tru + str( (random.randint(1, 11) * random.choice([-1, 1]))) data['partial_scores'][name] = {'score': 0, 'weight': weight} elif result == 'invalid': data['raw_submitted_answers'][name] = '' data['format_errors'][name] = 'invalid' else: raise Exception('invalid result: %s' % result)
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['file-name'] optional_attribs = [ 'ace-mode', 'ace-theme', 'editor-config-function', 'source-file-name', 'min-lines', 'max-lines', 'auto-resize', 'preview', 'focus', 'directory', 'normalize-to-ascii' ] pl.check_attribs(element, required_attribs, optional_attribs) source_file_name = pl.get_string_attrib(element, 'source-file-name', SOURCE_FILE_NAME_DEFAULT) file_name = pl.get_string_attrib(element, 'file-name') if '_required_file_names' not in data['params']: data['params']['_required_file_names'] = [] elif file_name in data['params']['_required_file_names']: raise Exception( 'There is more than one file editor with the same file name.') data['params']['_required_file_names'].append(file_name) if source_file_name is not None: if element.text is not None and not str(element.text).isspace(): raise Exception( 'Existing code cannot be added inside html element when "source-file-name" attribute is used.' )
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) # Get the number of digits to output digits = pl.get_integer_attrib(element, 'digits', 2) # Get the presentation type presentation_type = pl.get_string_attrib(element, 'presentation-type', 'f') var_name = pl.get_string_attrib(element, 'params-name') # Get value of variable, raising exception if variable does not exist var_data = data['params'].get(var_name, None) if var_data is None: raise Exception('No value in data["params"] for variable %s in pl-matrix-latex element' % var_name) # If the variable is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) var_data = pl.from_json(var_data) if not np.isscalar(var_data): var_data = np.array(var_data) # Check if numpy array type is numeric (integer, float or complex) if np.issubdtype(var_data.dtype, np.number): # Check shape of variable if var_data.ndim != 2: raise Exception('Value in data["params"] for variable %s in pl-matrix-latex element must be 2D array or scalar' % var_name) else: raise Exception('Value in data["params"] for variable %s in pl-matrix-latex element must be numeric' % var_name) # Create string for latex matrix format html = pl.latex_from_2darray(var_data, presentation_type=presentation_type, digits=digits) return html
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) num_backgrounds = 0 for child in element: if isinstance(child, lxml.html.HtmlComment): continue if child.tag == 'pl-location': pl.check_attribs(child, required_attribs=[], optional_attribs=['left', 'right', 'top', 'bottom', 'valign', 'halign']) if ('left' not in child.attrib and 'right' not in child.attrib) or ('left' in child.attrib and 'right' in child.attrib): raise ValueError('pl-location requires exactly one of "left" or "right" attributes.') if ('top' not in child.attrib and 'bottom' not in child.attrib) or ('top' in child.attrib and 'bottom' in child.attrib): raise ValueError('pl-location requires exactly one of "top" or "bottom" attributes.') valign = pl.get_string_attrib(child, 'valign', VALIGN_DEFAULT) if valign not in VALIGN_VALUES: raise ValueError(f'Unknown vertical alignment "{valign}"') halign = pl.get_string_attrib(child, 'halign', HALIGN_DEFAULT) if halign not in HALIGN_VALUES: raise ValueError(f'Unknown horizontal alignment "{halign}"') elif child.tag == 'pl-background': pl.check_attribs(child, required_attribs=[], optional_attribs=[]) num_backgrounds += 1 else: raise ValueError(f'Unknown tag "{child.tag}" found as child of pl-overlay') if num_backgrounds == 0: pl.check_attribs(element, required_attribs=['width', 'height'], optional_attribs=['clip']) elif num_backgrounds == 1: pl.check_attribs(element, required_attribs=[], optional_attribs=['clip', 'width', 'height']) else: raise ValueError(f'pl-overlay can have at most one <pl-background> child, found {num_backgrounds}.')
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['file-name'] optional_attribs = [ 'quill-theme', 'source-file-name', 'directory', 'placeholder', 'format', 'markdown-shortcuts' ] pl.check_attribs(element, required_attribs, optional_attribs) source_file_name = pl.get_string_attrib(element, 'source-file-name', SOURCE_FILE_NAME_DEFAULT) output_format = pl.get_string_attrib(element, 'format', FORMAT_DEFAULT) element_text = element_inner_html(element) file_name = pl.get_string_attrib(element, 'file-name') if '_required_file_names' not in data['params']: data['params']['_required_file_names'] = [] elif file_name in data['params']['_required_file_names']: raise Exception( 'There is more than one file editor with the same file name.') data['params']['_required_file_names'].append(file_name) if source_file_name is not None: if element_text and not str(element_text).isspace(): raise Exception( 'Existing text cannot be added inside rich-text element when "source-file-name" attribute is used.' + element_text) if output_format not in ('html', 'markdown'): raise Exception( f'Invalid output format "{output_format}". Must be either "html" or "markdown".' )
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) file_name = pl.get_string_attrib(element, 'file-name', '') answer_name = get_answer_name(file_name) quill_theme = pl.get_string_attrib(element, 'quill-theme', QUILL_THEME_DEFAULT) placeholder = pl.get_string_attrib(element, 'placeholder', PLACEHOLDER_DEFAULT) uuid = pl.get_uuid() source_file_name = pl.get_string_attrib(element, 'source-file-name', SOURCE_FILE_NAME_DEFAULT) directory = pl.get_string_attrib(element, 'directory', DIRECTORY_DEFAULT) element_text = element_inner_html(element) if data['panel'] == 'question' or data['panel'] == 'submission': html_params = { 'name': answer_name, 'file_name': file_name, 'quill_theme': quill_theme, 'placeholder': placeholder, 'editor_uuid': uuid, 'question': data['panel'] == 'question', 'submission': data['panel'] == 'submission', 'read_only': 'true' if data['panel'] == 'submission' else 'false' } if source_file_name is not None: if directory == 'serverFilesCourse': directory = data['options']['server_files_course_path'] elif directory == 'clientFilesCourse': directory = data['options']['client_files_course_path'] else: directory = os.path.join(data['options']['question_path'], directory) file_path = os.path.join(directory, source_file_name) text_display = open(file_path).read() else: if element_text is not None: text_display = str(element_text) else: text_display = '' html_params['original_file_contents'] = base64.b64encode(text_display.encode('UTF-8').strip()).decode() submitted_file_contents = data['submitted_answers'].get(answer_name, None) if submitted_file_contents: html_params['current_file_contents'] = submitted_file_contents else: html_params['current_file_contents'] = html_params['original_file_contents'] html_params['question'] = data['panel'] == 'question' with open('pl-rich-text-editor.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() elif data['panel'] == 'answer': html = '' else: raise Exception('Invalid panel type: ' + data['panel']) return html
def test(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') weight = pl.get_integer_attrib(element, 'weight', WEIGHT_DEFAULT) partial_credit = pl.get_boolean_attrib(element, 'partial-credit', PARTIAL_CREDIT_DEFAULT) partial_credit_method = pl.get_string_attrib(element, 'partial-credit-method', PARTIAL_CREDIT_METHOD_DEFAULT) correct_answer_list = data['correct_answers'].get(name, []) correct_keys = [answer['key'] for answer in correct_answer_list] number_answers = len(data['params'][name]) all_keys = [pl.index2key(i) for i in range(number_answers)] result = data['test_type'] if result == 'correct': if len(correct_keys) == 1: data['raw_submitted_answers'][name] = correct_keys[0] elif len(correct_keys) > 1: data['raw_submitted_answers'][name] = correct_keys else: pass # no raw_submitted_answer if no correct keys data['partial_scores'][name] = {'score': 1, 'weight': weight} elif result == 'incorrect': while True: # select answer keys at random ans = [k for k in all_keys if random.choice([True, False])] # break and use this choice if it isn't correct if (len(ans) >= 1): if set(ans) != set(correct_keys): if not pl.get_boolean_attrib(element, 'detailed-help-text', DETAILED_HELP_TEXT_DEFAULT): break else: min_correct = pl.get_integer_attrib(element, 'min-correct', 1) max_correct = pl.get_integer_attrib(element, 'max-correct', len(correct_answer_list)) if len(ans) <= max_correct and len(ans) >= min_correct: break if partial_credit: if partial_credit_method == 'PC': if set(ans) == set(correct_keys): score = 1 else: n_correct_answers = len(set(correct_keys)) - len(set(correct_keys) - set(ans)) points = n_correct_answers - len(set(ans) - set(correct_keys)) score = max(0, points / len(set(correct_keys))) else: # this is the EDC method number_wrong = len(set(ans) - set(correct_keys)) + len(set(correct_keys) - set(ans)) score = 1 - 1.0 * number_wrong / number_answers else: score = 0 data['raw_submitted_answers'][name] = ans data['partial_scores'][name] = {'score': score, 'weight': weight} elif result == 'invalid': # FIXME: add more invalid examples data['raw_submitted_answers'][name] = None data['format_errors'][name] = 'You must select at least one option.' else: raise Exception('invalid result: %s' % result)
def grade(element_html, element_index, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers_name') # Get weight weight = pl.get_integer_attrib(element, 'weight', 1) # Get true answer (if it does not exist, create no grade - leave it # up to the question code) a_tru = data['correct_answers'].get(name, None) if a_tru is None: return data # Convert true answer to numpy a_tru = np.array(a_tru) # Throw an error if true answer is not a 2D numpy array if a_tru.ndim != 2: raise ValueError('true answer must be a 2D array') # Get submitted answer (if it does not exist, score is zero) a_sub = data['submitted_answers'].get(name, None) if a_sub is None: data['partial_scores'][name] = {'score': 0, 'weight': weight} return data # Convert submitted answer to numpy a_sub = np.array(a_sub) # If true and submitted answers have different shapes, score is zero if not (a_sub.shape == a_tru.shape): data['partial_scores'][name] = {'score': 0, 'weight': weight} return data # Get method of comparison, with relabs as default comparison = pl.get_string_attrib(element, 'comparison', 'relabs') # Compare submitted answer with true answer if comparison == 'relabs': rtol = pl.get_float_attrib(element, 'rtol', 1e-5) atol = pl.get_float_attrib(element, 'atol', 1e-8) correct = pl.is_correct_ndarray2D_ra(a_sub, a_tru, rtol, atol) elif comparison == 'sigfig': digits = pl.get_integer_attrib(element, 'digits', 2) eps_digits = pl.get_integer_attrib(element, 'eps_digits', 3) correct = pl.is_correct_ndarray2D_sf(a_sub, a_tru, digits, eps_digits) elif comparison == 'decdig': digits = pl.get_integer_attrib(element, 'digits', 2) eps_digits = pl.get_integer_attrib(element, 'eps_digits', 3) correct = pl.is_correct_ndarray2D_dd(a_sub, a_tru, digits, eps_digits) else: raise ValueError('method of comparison "%s" is not valid' % comparison) if correct: data['partial_scores'][name] = {'score': 1, 'weight': weight} else: data['partial_scores'][name] = {'score': 0, 'weight': weight} return data
def grade(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') # Get weight weight = pl.get_integer_attrib(element, 'weight', WEIGHT_DEFAULT) # Get true answer (if it does not exist, create no grade - leave it # up to the question code) a_tru = pl.from_json(data['correct_answers'].get(name, None)) if a_tru is None: return # Wrap true answer in ndarray (if it already is one, this does nothing) a_tru = np.array(a_tru) # Throw an error if true answer is not a 2D numpy array if a_tru.ndim != 2: raise ValueError('true answer must be a 2D array') # Get submitted answer (if it does not exist, score is zero) a_sub = data['submitted_answers'].get(name, None) if a_sub is None: data['partial_scores'][name] = {'score': 0, 'weight': weight} return # If submitted answer is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) a_sub = pl.from_json(a_sub) # Wrap submitted answer in an ndarray (if it's already one, this does nothing) a_sub = np.array(a_sub) # If true and submitted answers have different shapes, score is zero if not (a_sub.shape == a_tru.shape): data['partial_scores'][name] = {'score': 0, 'weight': weight} return # Get method of comparison, with relabs as default comparison = pl.get_string_attrib(element, 'comparison', COMPARISON_DEFAULT) # Compare submitted answer with true answer if comparison == 'relabs': rtol = pl.get_float_attrib(element, 'rtol', RTOL_DEFAULT) atol = pl.get_float_attrib(element, 'atol', ATOL_DEFAULT) correct = pl.is_correct_ndarray2D_ra(a_sub, a_tru, rtol, atol) elif comparison == 'sigfig': digits = pl.get_integer_attrib(element, 'digits', DIGITS_DEFAULT) correct = pl.is_correct_ndarray2D_sf(a_sub, a_tru, digits) elif comparison == 'decdig': digits = pl.get_integer_attrib(element, 'digits', DIGITS_DEFAULT) correct = pl.is_correct_ndarray2D_dd(a_sub, a_tru, digits) else: raise ValueError('method of comparison "%s" is not valid' % comparison) if correct: data['partial_scores'][name] = {'score': 1, 'weight': weight} else: data['partial_scores'][name] = {'score': 0, 'weight': weight}
def grade(element_html, data): element = lxml.html.fragment_fromstring(element_html) answer_name = pl.get_string_attrib(element, 'answer-name') # Check if this element is intended to produce a grade will_be_graded = pl.get_boolean_attrib(element, 'grade', True) if not will_be_graded: return # Get weight weight = pl.get_integer_attrib(element, 'weight', 1) # Get submitted answer (the "state") state = data['submitted_answers'].get(answer_name, None) if state is None: # This might happen. It means that, somehow, the hidden input element # did not get populated with the PLThreeJS state. The student is not at # fault, so we'll return nothing - don't grade. return # Get correct answer (if none, don't grade) a = data['correct_answers'].get(answer_name, None) if a is None: return # Get submitted position (as np.array([x, y, z])) p_sub = np.array(state['body_position']) # Get submitted orientation (as Quaternion - first, roll [x,y,z,w] to [w,x,y,z]) q_sub = pyquaternion.Quaternion(np.roll(state['body_quaternion'], 1)) # Get format of correct answer f = pl.get_string_attrib(element, 'answer-pose-format', 'rpy') # Get correct position (as np.array([x, y, z])) and orientation (as Quaternion) p_tru, q_tru = parse_correct_answer(f, a) # Find distance between submitted position and correct position error_in_translation = np.linalg.norm(p_sub - p_tru) # Find smallest angle of rotation between submitted orientation and correct orientation error_in_rotation = np.abs((q_tru.inverse * q_sub).degrees) # Get tolerances tol_translation = pl.get_float_attrib(element, 'tol-translation', 0.5) tol_rotation = pl.get_float_attrib(element, 'tol-rotation', 5) if (tol_translation <= 0): raise Exception('tol_translation must be a positive real number: {:g}'.format(tol_translation)) if (tol_rotation <= 0): raise Exception('tol_rotation must be a positive real number (angle in degrees): {:g}'.format(tol_rotation)) # Check if angle is no greater than tolerance if ((error_in_rotation <= tol_rotation) and (error_in_translation <= tol_translation)): data['partial_scores'][answer_name] = {'score': 1, 'weight': weight, 'feedback': {'error_in_rotation': error_in_rotation, 'error_in_translation': error_in_translation}} else: data['partial_scores'][answer_name] = {'score': 0, 'weight': weight, 'feedback': {'error_in_rotation': error_in_rotation, 'error_in_translation': error_in_translation}}
def test(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') weight = pl.get_integer_attrib(element, 'weight', WEIGHT_DEFAULT) # Get correct answer a_tru = data['correct_answers'][name] # If correct answer is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) a_tru = pl.from_json(a_tru) result = random.choices(['correct', 'incorrect', 'invalid'], [5, 5, 1])[0] if result == 'correct': data['raw_submitted_answers'][name] = str(a_tru) data['partial_scores'][name] = {'score': 1, 'weight': weight} elif result == 'incorrect': data['partial_scores'][name] = {'score': 0, 'weight': weight} # Get method of comparison, with relabs as default comparison = pl.get_string_attrib(element, 'comparison', COMPARISON_DEFAULT) if comparison == 'relabs': rtol = pl.get_float_attrib(element, 'rtol', RTOL_DEFAULT) atol = pl.get_float_attrib(element, 'atol', ATOL_DEFAULT) # Get max error according to numpy.allclose() eps = np.absolute(a_tru) * rtol + atol eps += random.uniform(1, 10) answer = a_tru + eps * random.choice([-1, 1]) elif comparison == 'sigfig': digits = pl.get_integer_attrib(element, 'digits', DIGITS_DEFAULT) # Get max error according to pl.is_correct_scalar_sf() if (a_tru == 0): n = digits - 1 else: n = -int(np.floor(np.log10(np.abs(a_tru)))) + (digits - 1) eps = 0.51 * (10**-n) eps += random.uniform(1, 10) answer = a_tru + eps * random.choice([-1, 1]) elif comparison == 'decdig': digits = pl.get_integer_attrib(element, 'digits', DIGITS_DEFAULT) # Get max error according to pl.is_correct_scalar_dd() eps = 0.51 * (10**-digits) eps += random.uniform(1, 10) answer = a_tru + eps * random.choice([-1, 1]) else: raise ValueError('method of comparison "%s" is not valid' % comparison) data['raw_submitted_answers'][name] = str(answer) elif result == 'invalid': # FIXME: add more invalid expressions, make text of format_errors # correct, and randomize data['raw_submitted_answers'][name] = '1 + 2' data['format_errors'][name] = 'invalid' else: raise Exception('invalid result: %s' % result)
def grade(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') # Get weight weight = pl.get_integer_attrib(element, 'weight', 1) # Get true answer (if it does not exist, create no grade - leave it # up to the question code) a_tru = pl.from_json(data['correct_answers'].get(name, None)) if a_tru is None: return # Wrap true answer in ndarray (if it already is one, this does nothing) a_tru = np.array(a_tru) # Throw an error if true answer is not a 2D numpy array if a_tru.ndim != 2: raise ValueError('true answer must be a 2D array') # Get submitted answer (if it does not exist, score is zero) a_sub = data['submitted_answers'].get(name, None) if a_sub is None: data['partial_scores'][name] = {'score': 0, 'weight': weight} return # If submitted answer is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) a_sub = pl.from_json(a_sub) # Wrap submitted answer in an ndarray (if it's already one, this does nothing) a_sub = np.array(a_sub) # If true and submitted answers have different shapes, score is zero if not (a_sub.shape == a_tru.shape): data['partial_scores'][name] = {'score': 0, 'weight': weight} return # Get method of comparison, with relabs as default comparison = pl.get_string_attrib(element, 'comparison', 'relabs') # Compare submitted answer with true answer if comparison == 'relabs': rtol = pl.get_float_attrib(element, 'rtol', 1e-2) atol = pl.get_float_attrib(element, 'atol', 1e-8) correct = pl.is_correct_ndarray2D_ra(a_sub, a_tru, rtol, atol) elif comparison == 'sigfig': digits = pl.get_integer_attrib(element, 'digits', 2) correct = pl.is_correct_ndarray2D_sf(a_sub, a_tru, digits) elif comparison == 'decdig': digits = pl.get_integer_attrib(element, 'digits', 2) correct = pl.is_correct_ndarray2D_dd(a_sub, a_tru, digits) else: raise ValueError('method of comparison "%s" is not valid' % comparison) if correct: data['partial_scores'][name] = {'score': 1, 'weight': weight} else: data['partial_scores'][name] = {'score': 0, 'weight': weight}
def graphviz_from_adj_matrix(element, data): # Get matrix attributes engine = pl.get_string_attrib(element, 'engine', ENGINE_DEFAULT) input_param = pl.get_string_attrib(element, 'params-name-matrix', PARAMS_NAME_MATRIX_DEFAULT) input_label = pl.get_string_attrib(element, 'params-name-labels', PARAMS_NAME_LABELS_DEFAULT) mat = np.array(pl.from_json(data['params'][input_param])) show_weights = pl.get_boolean_attrib(element, 'weights', WEIGHTS_DEFAULT) # by default display weights for stochastic matrices digits = pl.get_integer_attrib(element, 'weights-digits', WEIGHTS_DIGITS_DEFAULT) # if displaying weights how many digits to round to presentation_type = pl.get_string_attrib(element, 'weights-presentation-type', WEIGHTS_PRESENTATION_TYPE_DEFAULT).lower() label = None if input_label is not None: label = np.array(pl.from_json(data['params'][input_label])) # Sanity checking if (mat.shape[0] != mat.shape[1]): raise Exception(f'Non-square adjacency matrix of size ({mat.shape[0]}, {mat.shape[1]}) given as input.') if label is not None: mat_label = label if (mat_label.shape[0] != mat.shape[0]): raise Exception(f'Dimension of the label ({mat_label.shape[0]}) is not consistent with the dimension of the matrix ({mat.shape[0]})') else: mat_label = range(mat.shape[1]) # Auto detect showing weights if any of the weights are not 1 or 0 if show_weights is None: all_ones = True for x in mat.flatten(): if x != 1 and x != 0: all_ones = False show_weights = not all_ones # Create pygraphviz graph representation G = pygraphviz.AGraph(directed=True) for node in mat_label: G.add_node(node) for i, out_node in enumerate(mat_label): for j, in_node in enumerate(mat_label): x = mat[j, i] if (x > 0): if (show_weights): G.add_edge(out_node, in_node, label=pl.string_from_2darray(x, presentation_type=presentation_type, digits=digits)) else: G.add_edge(out_node, in_node) G.layout(engine) return G.string()
def test(element_html, data): element = lxml.html.fragment_fromstring(element_html) grading_mode = pl.get_string_attrib(element, 'grading-method', 'ordered') answer_name = pl.get_string_attrib(element, 'answers-name') answer_name_field = answer_name + '-input' weight = pl.get_integer_attrib(element, 'weight', WEIGHT_DEFAULT) feedback_type = pl.get_string_attrib(element, 'feedback', FEEDBACK_DEFAULT) # Right now invalid input must mean an empty response. Because user input is only # through drag and drop, there is no other way for their to be invalid input. This # may change in the future if we have nested input boxes (like faded parsons' problems). if data['test_type'] == 'invalid': data['raw_submitted_answers'][answer_name_field] = json.dumps([]) data['format_errors'][answer_name] = 'No answer was submitted.' # TODO grading modes 'unordered,' 'dag,' and 'ranking' allow multiple different possible # correct answers, we should check them at random instead of just the provided solution elif data['test_type'] == 'correct': answer = filter_multiple_from_array( data['correct_answers'][answer_name], ['inner_html', 'indent', 'uuid']) data['raw_submitted_answers'][answer_name_field] = json.dumps(answer) data['partial_scores'][answer_name] = { 'score': 1, 'weight': weight, 'feedback': '', 'first_wrong': -1 } # TODO: The only wrong answer being tested is the correct answer with the first # block mising. We should instead do a random selection of correct and incorrect blocks. elif data['test_type'] == 'incorrect': answer = filter_multiple_from_array( data['correct_answers'][answer_name], ['inner_html', 'indent', 'uuid']) answer.pop(0) score = float(len(answer)) / (len(answer) + 1) if grading_mode == 'unordered' else 0 first_wrong = 0 if grading_mode == 'dag' else -1 feedback = DAG_FIRST_WRONG_FEEDBACK['wrong-at-block'].format( 1 ) if grading_mode == 'dag' and feedback_type == 'first-wrong' else '' data['raw_submitted_answers'][answer_name_field] = json.dumps(answer) data['partial_scores'][answer_name] = { 'score': score, 'weight': weight, 'feedback': feedback, 'first_wrong': first_wrong } else: raise Exception('invalid result: %s' % data['test_type'])
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 render(element_html, data): if data['panel'] != 'question': return '' element = lxml.html.fragment_fromstring(element_html) file_name = pl.get_string_attrib(element, 'file-name', '') answer_name = get_answer_name(file_name) editor_config_function = pl.get_string_attrib(element, 'editor-config-function', None) ace_mode = pl.get_string_attrib(element, 'ace-mode', None) ace_theme = pl.get_string_attrib(element, 'ace-theme', None) uuid = pl.get_uuid() source_file_name = pl.get_string_attrib(element, 'source-file-name', None) html_params = { 'name': answer_name, 'file_name': file_name, 'ace_mode': ace_mode, 'ace_theme': ace_theme, 'editor_config_function': editor_config_function, 'uuid': uuid } if source_file_name is not None: file_path = os.path.join(data['options']['question_path'], source_file_name) text_display = open(file_path).read() else: if element.text is not None: text_display = str(element.text) else: text_display = '' html_params['original_file_contents'] = base64.b64encode( text_display.encode('UTF-8').strip()).decode() submitted_file_contents = data['submitted_answers'].get(answer_name, None) if submitted_file_contents: html_params['current_file_contents'] = submitted_file_contents else: html_params['current_file_contents'] = html_params[ 'original_file_contents'] if data['panel'] == 'question': html_params['question'] = True with open('pl-file-editor.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() else: html = '' return html
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['answers-name'] optional_attribs = ['weight', 'correct-answer', 'label', 'suffix', 'display', 'remove-leading-trailing', 'remove-spaces', 'allow-blank', 'placeholder'] pl.check_attribs(element, required_attribs, optional_attribs) name = pl.get_string_attrib(element, 'answers-name') correct_answer = pl.get_string_attrib(element, 'correct-answer', None) if correct_answer is not None: if name in data['correct_answers']: raise Exception('duplicate correct_answers variable name: %s' % name) data['correct_answers'][name] = correct_answer
def prepare(element_html, element_index, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['answers_name'] optional_attribs = ['weight', 'correct_answer', 'variables'] pl.check_attribs(element, required_attribs, optional_attribs) name = pl.get_string_attrib(element, 'answers_name') correct_answer = pl.get_string_attrib(element, 'correct_answer', None) if correct_answer is not None: if name in data['correct_answers']: raise Exception('duplicate correct_answers variable name: %s' % name) data['correct_answers'][name] = correct_answer
def prepare(element_html, element_index, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['answers-name'] optional_attribs = ['weight', 'correct-answer', 'label', 'suffix', 'display', 'remove-leading-trailing', 'remove-spaces', 'allow-blank'] pl.check_attribs(element, required_attribs, optional_attribs) name = pl.get_string_attrib(element, 'answers-name') correct_answer = pl.get_string_attrib(element, 'correct-answer', None) if correct_answer is not None: if name in data['correct_answers']: raise Exception('duplicate correct_answers variable name: %s' % name) data['correct_answers'][name] = correct_answer
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 get_orientation(element, name_orientation, name_format): s = pl.get_string_attrib(element, name_orientation, None) if s is None: return [0, 0, 0, 1] f = pl.get_string_attrib(element, name_format, 'rpy') if f == 'rpy': try: rpy = np.array(json.loads(s), dtype=np.float64) if rpy.shape == (3,): qx = pyquaternion.Quaternion(axis=[1, 0, 0], degrees=rpy[0]) qy = pyquaternion.Quaternion(axis=[0, 1, 0], degrees=rpy[1]) qz = pyquaternion.Quaternion(axis=[0, 0, 1], degrees=rpy[2]) return np.roll((qx * qy * qz).elements, -1).tolist() else: raise ValueError() except Exception: raise Exception('attribute "{:s}" with format "{:s}" must be a set of roll, pitch, yaw angles in degrees with format "[roll, pitch, yaw]": {:s}'.format(name_orientation, name_format, s)) elif f == 'quaternion': try: q = np.array(json.loads(s), dtype=np.float64) if (q.shape == (4,)) and np.allclose(np.linalg.norm(q), 1.0): return q.tolist() else: raise ValueError() except Exception: raise Exception('attribute "{:s}" with format "{:s}" must be a unit quaternion with format "[x, y, z, w]": {:s}'.format(name_orientation, name_format, s)) elif f == 'matrix': try: R = np.array(json.loads(s), dtype=np.float64) return np.roll(pyquaternion.Quaternion(matrix=R).elements, -1).tolist() except Exception: raise Exception('attribute "{:s}" with format "{:s}" must be a 3x3 rotation matrix with format "[[ ... ], [ ... ], [ ... ]]": {:s}'.format(name_orientation, name_format, s)) elif f == 'axisangle': try: q = np.array(json.loads(s), dtype=np.float64) if (q.shape == (4,)): axis = q[0:3] angle = q[3] if np.allclose(np.linalg.norm(axis), 1.0): return np.roll(pyquaternion.Quaternion(axis=axis, degrees=angle).elements, -1).tolist() else: raise ValueError() else: raise ValueError() except Exception: raise Exception('attribute "{:s}" with format "{:s}" must have format "[x, y, z, angle]" where (x, y, z) are the components of a unit vector and where the angle is in degrees: {:s}'.format(name_orientation, name_format, s)) else: raise Exception('attribute "{:s}" must be "rpy", "quaternion", "matrix", or "axisangle": {:s}'.format(name_format, f))
def test(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') weight = pl.get_integer_attrib(element, 'weight', 1) correct_key = data['correct_answers'].get(name, {'key': None}).get('key', None) if correct_key is None: raise Exception('could not determine correct_key') number_answers = len(data['params'][name]) all_keys = [chr(ord('a') + i) for i in range(number_answers)] incorrect_keys = list(set(all_keys) - set([correct_key])) result = random.choices(['correct', 'incorrect', 'invalid'], [5, 5, 1])[0] if result == 'correct': data['raw_submitted_answers'][name] = data['correct_answers'][name]['key'] data['partial_scores'][name] = {'score': 1, 'weight': weight} elif result == 'incorrect': if len(incorrect_keys) > 0: data['raw_submitted_answers'][name] = random.choice(incorrect_keys) data['partial_scores'][name] = {'score': 0, 'weight': weight} else: # actually an invalid submission data['raw_submitted_answers'][name] = '0' data['format_errors'][name] = 'INVALID choice' elif result == 'invalid': data['raw_submitted_answers'][name] = '0' data['format_errors'][name] = 'INVALID choice' # FIXME: add more invalid choices else: raise Exception('invalid result: %s' % result)
def parse(element_html, data): # By convention, this function returns at the first error found element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') allow_complex = pl.get_boolean_attrib(element, 'allow-complex', False) # Get submitted answer or return parse_error if it does not exist a_sub = data['submitted_answers'].get(name, None) if a_sub is None: data['format_errors'][name] = 'No submitted answer.' data['submitted_answers'][name] = None return # Convert submitted answer to numpy array (return parse_error on failure) (a_sub_parsed, info) = pl.string_to_2darray(a_sub, allow_complex=allow_complex) if a_sub_parsed is None: data['format_errors'][name] = info['format_error'] data['submitted_answers'][name] = None return # Replace submitted answer with numpy array data['submitted_answers'][name] = pl.to_json(a_sub_parsed) # Store format type if '_pl_matrix_input_format' not in data['submitted_answers']: data['submitted_answers']['_pl_matrix_input_format'] = {} data['submitted_answers']['_pl_matrix_input_format'][name] = info['format_type']
def render(element_html, data): if data['panel'] != 'question': return '' element = lxml.html.fragment_fromstring(element_html) uuid = pl.get_uuid() raw_file_names = pl.get_string_attrib(element, 'file-names', '') file_names = get_file_names_as_array(raw_file_names) file_names_json = json.dumps(file_names, allow_nan=False) answer_name = get_answer_name(raw_file_names) html_params = {'name': answer_name, 'file_names': file_names_json, 'uuid': uuid} files = data['submitted_answers'].get('_files', None) if files is not None: # Filter out any files not part of this element's file_names filtered_files = [x for x in files if x.get('name', '') in file_names] html_params['has_files'] = True html_params['files'] = json.dumps(filtered_files, allow_nan=False) else: html_params['has_files'] = False with open('pl-file-upload.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() return html
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 grade(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') # Get weight weight = pl.get_integer_attrib(element, 'weight', 1) # Get true answer (if it does not exist, create no grade - leave it # up to the question code) a_tru = pl.from_json(data['correct_answers'].get(name, None)) if a_tru is None: return # Get submitted answer (if it does not exist, score is zero) a_sub = data['submitted_answers'].get(name, None) if a_sub is None: data['partial_scores'][name] = {'score': 0, 'weight': weight} return # If submitted answer is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) a_sub = pl.from_json(a_sub) # Cast both submitted and true answers as integers. a_tru = int(a_tru) a_sub = int(a_sub) if a_tru == a_sub: data['partial_scores'][name] = {'score': 1, 'weight': weight} else: data['partial_scores'][name] = {'score': 0, 'weight': weight}
def test(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') weight = pl.get_integer_attrib(element, 'weight', 1) result = random.choices(['correct', 'incorrect', 'invalid'], [5, 5, 1])[0] if result == 'correct': data['raw_submitted_answers'][name] = str(pl.from_json(data['correct_answers'][name])) data['partial_scores'][name] = {'score': 1, 'weight': weight} elif result == 'incorrect': data['raw_submitted_answers'][name] = str(pl.from_json(data['correct_answers'][name])) + ' + {:d}'.format(random.randint(1, 100)) data['partial_scores'][name] = {'score': 0, 'weight': weight} elif result == 'invalid': invalid_type = random.choice(['float', 'complex', 'expression', 'function', 'variable', 'syntax', 'escape', 'comment']) if invalid_type == 'float': data['raw_submitted_answers'][name] = 'x + 1.234' s = 'Your answer contains the floating-point number ' + str(1.234) + '. ' s += 'All numbers must be expressed as integers (or ratios of integers). ' s += '<br><br><pre>' + phs.point_to_error('x + 1.234', 4) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'complex': data['raw_submitted_answers'][name] = 'x + (1+2j)' s = 'Your answer contains the complex number ' + str(2j) + '. ' s += 'All numbers must be expressed as integers (or ratios of integers). ' s += '<br><br><pre>' + phs.point_to_error('x + (1+2j)', 7) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'expression': data['raw_submitted_answers'][name] = '1 and 0' s = 'Your answer has an invalid expression. ' s += '<br><br><pre>' + phs.point_to_error('1 and 0', 0) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'function': data['raw_submitted_answers'][name] = 'atan(x)' s = 'Your answer calls an invalid function "' + 'atan' + '". ' s += '<br><br><pre>' + phs.point_to_error('atan(x)', 0) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'variable': data['raw_submitted_answers'][name] = 'x + y' s = 'Your answer refers to an invalid variable "' + 'y' + '". ' s += '<br><br><pre>' + phs.point_to_error('x + y', 4) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'syntax': data['raw_submitted_answers'][name] = 'x +* 1' s = 'Your answer has a syntax error. ' s += '<br><br><pre>' + phs.point_to_error('x +* 1', 4) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'escape': data['raw_submitted_answers'][name] = 'x + 1\\n' s = 'Your answer must not contain the character "\\". ' s += '<br><br><pre>' + phs.point_to_error('x + 1\\n', 5) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'comment': data['raw_submitted_answers'][name] = 'x # some text' s = 'Your answer must not contain the character "#". ' s += '<br><br><pre>' + phs.point_to_error('x # some text', 2) + '</pre>' data['format_errors'][name] = s else: raise Exception('invalid invalid_type: %s' % invalid_type) else: raise Exception('invalid result: %s' % result)
def test(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') weight = pl.get_integer_attrib(element, 'weight', 1) # Get correct answer a_tru = data['correct_answers'][name] # If correct answer is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) a_tru = pl.from_json(a_tru) result = random.choices(['correct', 'incorrect', 'invalid'], [5, 5, 1])[0] if result == 'correct': data['raw_submitted_answers'][name] = str(a_tru) data['partial_scores'][name] = {'score': 1, 'weight': weight} elif result == 'incorrect': data['raw_submitted_answers'][name] = str(a_tru + (random.randint(1, 11) * random.choice([-1, 1]))) data['partial_scores'][name] = {'score': 0, 'weight': weight} elif result == 'invalid': # FIXME: add more invalid expressions, make text of format_errors # correct, and randomize if random.choice([True, False]): data['raw_submitted_answers'][name] = '1 + 2' else: data['raw_submitted_answers'][name] = '3.4' data['format_errors'][name] = 'invalid' else: raise Exception('invalid result: %s' % result)
def parse(element_html, data): element = lxml.html.fragment_fromstring(element_html) raw_file_names = pl.get_string_attrib(element, 'file-names', '') required_file_names = get_file_names_as_array(raw_file_names) answer_name = get_answer_name(raw_file_names) # Get submitted answer or return parse_error if it does not exist files = data['submitted_answers'].get(answer_name, None) if not files: add_format_error(data, 'No submitted answer for file upload.') return try: parsed_files = json.loads(files) except ValueError: add_format_error(data, 'Could not parse submitted files.') # Filter out any files that were not listed in file_names parsed_files = [x for x in parsed_files if x.get('name', '') in required_file_names] if data['submitted_answers'].get('_files', None) is None: data['submitted_answers']['_files'] = parsed_files elif isinstance(data['submitted_answers'].get('_files', None), list): data['submitted_answers']['_files'].extend(parsed_files) else: add_format_error(data, '_files was present but was not an array.') # Validate that all required files are present if parsed_files is not None: submitted_file_names = [x.get('name', '') for x in parsed_files] missing_files = [x for x in required_file_names if x not in submitted_file_names] if len(missing_files) > 0: add_format_error(data, 'The following required files were missing: ' + ', '.join(missing_files))
def render(element_html, data): if data['panel'] != 'question': return '' element = lxml.html.fragment_fromstring(element_html) file_name = pl.get_string_attrib(element, 'file-name', '') answer_name = get_answer_name(file_name) editor_config_function = pl.get_string_attrib(element, 'editor-config-function', None) ace_mode = pl.get_string_attrib(element, 'ace-mode', None) ace_theme = pl.get_string_attrib(element, 'ace-theme', None) uuid = pl.get_uuid() source_file_name = pl.get_string_attrib(element, 'source-file-name', None) html_params = { 'name': answer_name, 'file_name': file_name, 'ace_mode': ace_mode, 'ace_theme': ace_theme, 'editor_config_function': editor_config_function, 'uuid': uuid } if source_file_name is not None: file_path = os.path.join(data['options']['question_path'], source_file_name) text_display = open(file_path).read() else: if element.text is not None: text_display = str(element.text) else: text_display = '' html_params['original_file_contents'] = base64.b64encode(text_display.encode('UTF-8').strip()).decode() submitted_file_contents = data['submitted_answers'].get(answer_name, None) if submitted_file_contents: html_params['current_file_contents'] = submitted_file_contents else: html_params['current_file_contents'] = html_params['original_file_contents'] if data['panel'] == 'question': html_params['question'] = True with open('pl-file-editor.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() else: html = '' return html
def parse(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') # Get allow-blank option allow_blank = pl.get_string_attrib(element, 'allow-blank', False) # Get submitted answer or return parse_error if it does not exist a_sub = data['submitted_answers'].get(name, None) if a_sub is None: data['format_errors'][name] = 'No submitted answer.' data['submitted_answers'][name] = None return if not a_sub and not allow_blank: data['format_errors'][name] = 'Invalid format. The submitted answer was left blank.' data['submitted_answers'][name] = None else: data['submitted_answers'][name] = pl.to_json(a_sub)
def get_file_url(element, data): # Get file name or raise exception if one does not exist file_name = pl.get_string_attrib(element, 'file-name') # Get directory (default is clientFilesQuestion) file_directory = pl.get_string_attrib(element, 'file-directory', 'clientFilesQuestion') # Get base url, which depends on the directory if file_directory == 'clientFilesQuestion': base_url = data['options']['client_files_question_url'] elif file_directory == 'clientFilesCourse': base_url = data['options']['client_files_course_url'] else: raise ValueError('file-directory "{}" is not valid (must be "clientFilesQuestion" or "clientFilesCourse")'.format(file_directory)) # Get full url file_url = os.path.join(base_url, file_name) return file_url
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['file-names'] optional_attribs = [] pl.check_attribs(element, required_attribs, optional_attribs) if '_required_file_names' not in data['params']: data['params']['_required_file_names'] = [] file_names = get_file_names_as_array(pl.get_string_attrib(element, 'file-names')) data['params']['_required_file_names'].extend(file_names)
def test(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') weight = pl.get_integer_attrib(element, 'weight', 1) allow_partial_credit = pl.get_boolean_attrib(element, 'allow-partial-credit', False) # Get correct answer a_tru = data['correct_answers'][name] # If correct answer is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) a_tru = pl.from_json(a_tru) # Wrap true answer in ndarray (if it already is one, this does nothing) a_tru = np.array(a_tru) # Throw an error if true answer is not a 2D numpy array if a_tru.ndim != 2: raise ValueError('true answer must be a 2D array') else: m, n = np.shape(a_tru) result = random.choices(['correct', 'incorrect', 'incorrect'], [5, 5, 1])[0] number_of_correct = 0 feedback = {} for i in range(m): for j in range(n): each_entry_name = name + str(n * i + j + 1) if result == 'correct': data['raw_submitted_answers'][each_entry_name] = str(a_tru[i, j]) number_of_correct += 1 feedback.update({each_entry_name: 'correct'}) elif result == 'incorrect': data['raw_submitted_answers'][each_entry_name] = str(a_tru[i, j] + (random.uniform(1, 10) * random.choice([-1, 1]))) feedback.update({each_entry_name: 'incorrect'}) elif result == 'invalid': if random.choice([True, False]): data['raw_submitted_answers'][each_entry_name] = '1,2' data['format_errors'][each_entry_name] = '(Invalid format)' else: data['raw_submitted_answers'][name] = '' data['format_errors'][each_entry_name] = '(Invalid blank entry)' else: raise Exception('invalid result: %s' % result) if result == 'invalid': data['format_errors'][name] = 'At least one of the entries has invalid format (empty entries or not a double precision floating point number)' if number_of_correct == m * n: data['partial_scores'][name] = {'score': 1, 'weight': weight} else: if not allow_partial_credit: score_value = 0 else: score_value = number_of_correct / (m * n) data['partial_scores'][name] = {'score': score_value, 'weight': weight, 'feedback': feedback}
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) # Get file name or raise exception if one does not exist file_name = pl.get_string_attrib(element, 'file-name') # Get type (default is static) file_type = pl.get_string_attrib(element, 'type', 'static') # Get directory (default is clientFilesQuestion) file_directory = pl.get_string_attrib(element, 'directory', 'clientFilesQuestion') # Get label (default is file_name) file_label = pl.get_string_attrib(element, 'label', file_name) # Get whether to force a download or open in-browser force_download = pl.get_boolean_attrib(element, 'force-download', True) # Get base url, which depends on the type and directory if file_type == 'static': if file_directory == 'clientFilesQuestion': base_url = data['options']['client_files_question_url'] elif file_directory == 'clientFilesCourse': base_url = data['options']['client_files_course_url'] else: raise ValueError('directory "{}" is not valid for type "{}" (must be "clientFilesQuestion" or "clientFilesCourse")'.format(file_directory, file_type)) elif file_type == 'dynamic': if pl.has_attrib(element, 'directory'): raise ValueError('no directory ("{}") can be provided for type "{}"'.format(file_directory, file_type)) else: base_url = data['options']['client_files_question_dynamic_url'] else: raise ValueError('type "{}" is not valid (must be "static" or "dynamic")'.format(file_type)) # Get full url file_url = os.path.join(base_url, file_name) # Create and return html if force_download: return '<a href="' + file_url + '" download>' + file_label + '</a>' else: return '<a href="' + file_url + '" target="_blank">' + file_label + '</a>'
def grade(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') variables = get_variables_list(pl.get_string_attrib(element, 'variables', None)) allow_complex = pl.get_boolean_attrib(element, 'allow-complex', False) weight = pl.get_integer_attrib(element, 'weight', 1) # Get true answer (if it does not exist, create no grade - leave it # up to the question code) a_tru = data['correct_answers'].get(name, None) if a_tru is None: return # Get submitted answer (if it does not exist, score is zero) a_sub = data['submitted_answers'].get(name, None) if a_sub is None: data['partial_scores'][name] = {'score': 0, 'weight': weight} return # Parse true answer if isinstance(a_tru, str): # this is so instructors can specify the true answer simply as a string a_tru = phs.convert_string_to_sympy(a_tru, variables, allow_complex=allow_complex) else: a_tru = phs.json_to_sympy(a_tru, allow_complex=allow_complex) # Parse submitted answer if isinstance(a_sub, str): # this is for backward-compatibility a_sub = phs.convert_string_to_sympy(a_sub, variables, allow_complex=allow_complex) else: a_sub = phs.json_to_sympy(a_sub, allow_complex=allow_complex) # Check equality correct = a_tru.equals(a_sub) if correct: data['partial_scores'][name] = {'score': 1, 'weight': weight} else: data['partial_scores'][name] = {'score': 0, 'weight': weight}
def grade(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') # Get weight weight = pl.get_integer_attrib(element, 'weight', 1) # Get remove-spaces option remove_spaces = pl.get_string_attrib(element, 'remove-spaces', False) # Get remove-leading-trailing option remove_leading_trailing = pl.get_string_attrib(element, 'remove-leading-trailing', False) # Get true answer (if it does not exist, create no grade - leave it # up to the question code) a_tru = pl.from_json(data['correct_answers'].get(name, None)) if a_tru is None: return # Get submitted answer (if it does not exist, score is zero) a_sub = data['submitted_answers'].get(name, None) if a_sub is None: data['partial_scores'][name] = {'score': 0, 'weight': weight} return # If submitted answer is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) a_sub = pl.from_json(a_sub) # Remove the leading and trailing characters if (remove_leading_trailing): a_sub = a_sub.strip() # Remove the blank spaces between characters if (remove_spaces): a_sub = a_sub.replace(' ', '') if a_tru == a_sub: data['partial_scores'][name] = {'score': 1, 'weight': weight} else: data['partial_scores'][name] = {'score': 0, 'weight': weight}
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) # Get file name or raise exception if one does not exist file_name = pl.get_string_attrib(element, 'file-name') # Get type (default is static) file_type = pl.get_string_attrib(element, 'type', 'static') # Get directory (default is clientFilesQuestion) file_directory = pl.get_string_attrib(element, 'directory', 'clientFilesQuestion') # Get base url, which depends on the type and directory if file_type == 'static': if file_directory == 'clientFilesQuestion': base_url = data['options']['client_files_question_url'] elif file_directory == 'clientFilesCourse': base_url = data['options']['client_files_course_url'] else: raise ValueError('directory "{}" is not valid for type "{}" (must be "clientFilesQuestion" or "clientFilesCourse")'.format(file_directory, file_type)) elif file_type == 'dynamic': if pl.has_attrib(element, 'directory'): raise ValueError('no directory ("{}") can be provided for type "{}"'.format(file_directory, file_type)) else: base_url = data['options']['client_files_question_dynamic_url'] else: raise ValueError('type "{}" is not valid (must be "static" or "dynamic")'.format(file_type)) # Get full url file_url = os.path.join(base_url, file_name) # Get width (optional) width = pl.get_string_attrib(element, 'width', None) # Create and return html html_params = {'src': file_url, 'width': width} with open('pl-figure.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() return html
def grade(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') weight = pl.get_integer_attrib(element, 'weight', 1) submitted_key = data['submitted_answers'].get(name, None) correct_key = data['correct_answers'].get(name, {'key': None}).get('key', None) score = 0 if (submitted_key is not None and submitted_key == correct_key): score = 1 data['partial_scores'][name] = {'score': score, 'weight': weight}
def parse(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') # Get true answer a_tru = pl.from_json(data['correct_answers'].get(name, None)) if a_tru is None: return a_tru = np.array(a_tru) if a_tru.ndim != 2: raise ValueError('true answer must be a 2D array') else: m, n = np.shape(a_tru) A = np.empty([m, n]) # Create an array for the submitted answer to be stored in data['submitted_answer'][name] # used for display in the answer and submission panels # Also creates invalid error messages invalid_format = False for i in range(m): for j in range(n): each_entry_name = name + str(n * i + j + 1) a_sub = data['submitted_answers'].get(each_entry_name, None) if a_sub is None: data['submitted_answers'][each_entry_name] = None data['format_errors'][each_entry_name] = '(No submitted answer)' invalid_format = True elif not a_sub: data['submitted_answers'][each_entry_name] = None data['format_errors'][each_entry_name] = '(Invalid blank entry)' invalid_format = True else: a_sub_parsed = pl.string_to_number(a_sub, allow_complex=False) if a_sub_parsed is None: data['submitted_answers'][each_entry_name] = None data['format_errors'][each_entry_name] = '(Invalid format)' invalid_format = True elif not np.isfinite(a_sub_parsed): data['submitted_answers'][each_entry_name] = None data['format_errors'][each_entry_name] = '(Invalid format - not finite)' invalid_format = True else: data['submitted_answers'][each_entry_name] = pl.to_json(a_sub_parsed) A[i, j] = a_sub_parsed if invalid_format: data['format_errors'][name] = 'At least one of the entries has invalid format (empty entries or not a double precision floating point number)' data['submitted_answers'][name] = None else: data['submitted_answers'][name] = pl.to_json(A)
def parse(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') submitted_key = data['submitted_answers'].get(name, None) all_keys = [a['key'] for a in data['params'][name]] if submitted_key is None: data['format_errors'][name] = 'No submitted answer.' return if submitted_key not in all_keys: data['format_errors'][name] = 'INVALID choice: ' + submitted_key # FIXME: escape submitted_key return
def get_position(element, name_position, default=[0, 0, 0], must_be_nonzero=False): s = pl.get_string_attrib(element, name_position, None) if s is None: return default try: p = np.array(json.loads(s), dtype=np.float64) if p.shape == (3,): if must_be_nonzero and np.allclose(p, np.array([0, 0, 0])): raise ValueError('attribute "{:s}" must be non-zero'.format(name_position)) else: return p.tolist() else: raise ValueError() except Exception: raise Exception('attribute "{:s}" must have format "[x, y, z]": {:s}'.format(name_position, s))
def test(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') weight = pl.get_integer_attrib(element, 'weight', 1) # Get correct answer a_tru = data['correct_answers'][name] # If correct answer is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) a_tru = pl.from_json(a_tru) # Wrap true answer in ndarray (if it already is one, this does nothing) a_tru = np.array(a_tru) result = random.choices(['correct', 'incorrect', 'invalid'], [5, 5, 1])[0] if random.choice([True, False]): # matlab if result == 'correct': data['raw_submitted_answers'][name] = pl.numpy_to_matlab(a_tru, ndigits=12, wtype='g') data['partial_scores'][name] = {'score': 1, 'weight': weight} elif result == 'incorrect': data['raw_submitted_answers'][name] = pl.numpy_to_matlab(a_tru + (random.uniform(1, 10) * random.choice([-1, 1])), ndigits=12, wtype='g') data['partial_scores'][name] = {'score': 0, 'weight': weight} elif result == 'invalid': # FIXME: add more invalid expressions, make text of format_errors # correct, and randomize data['raw_submitted_answers'][name] = '[1, 2, 3]' data['format_errors'][name] = 'invalid' else: raise Exception('invalid result: %s' % result) else: # python if result == 'correct': data['raw_submitted_answers'][name] = str(np.array(a_tru).tolist()) data['partial_scores'][name] = {'score': 1, 'weight': weight} elif result == 'incorrect': data['raw_submitted_answers'][name] = str((a_tru + (random.uniform(1, 10) * random.choice([-1, 1]))).tolist()) data['partial_scores'][name] = {'score': 0, 'weight': weight} elif result == 'invalid': # FIXME: add more invalid expressions, make text of format_errors # correct, and randomize data['raw_submitted_answers'][name] = '[[1, 2, 3], [4, 5]]' data['format_errors'][name] = 'invalid' else: raise Exception('invalid result: %s' % result)