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 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 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 render(element_html, data): element = lxml.html.fragment_fromstring(element_html) hide_in_question = pl.get_boolean_attrib(element, 'question', QUESTION_DEFAULT) hide_in_submission = pl.get_boolean_attrib(element, 'submission', SUBMISSION_DEFAULT) hide_in_answer = pl.get_boolean_attrib(element, 'answer', ANSWER_DEFAULT) if (data['panel'] == 'question' and not hide_in_question) \ or (data['panel'] == 'submission' and not hide_in_submission) \ or (data['panel'] == 'answer' and not hide_in_answer): element = lxml.html.fragment_fromstring(element_html) return pl.inner_html(element) else: return ''
def 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]] correct_answer_list = data['correct_answers'].get(name, []) # Check that at least one option was selected if submitted_key is None: data['format_errors'][name] = 'You must select at least one option.' return # Check that the selected options are a subset of the valid options # FIXME: raise ValueError instead of treating as parse error? submitted_key_set = set(submitted_key) all_keys_set = set(all_keys) if not submitted_key_set.issubset(all_keys_set): one_bad_key = submitted_key_set.difference(all_keys_set).pop() # FIXME: escape one_bad_key data['format_errors'][name] = 'You selected an invalid option: {:s}'.format(str(one_bad_key)) return # Check that the number of submitted answers is in range when 'detailed_help_text="true"' if pl.get_boolean_attrib(element, 'detailed-help-text', DETAILED_HELP_TEXT_DEFAULT): min_correct = pl.get_integer_attrib(element, 'min-correct', 1) max_correct = pl.get_integer_attrib(element, 'max-correct', len(correct_answer_list)) n_submitted = len(submitted_key) if n_submitted > max_correct or n_submitted < min_correct: data['format_errors'][name] = 'You must select between <b>%d</b> and <b>%d</b> options.' % (min_correct, max_correct) return
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 parse(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') allow_complex = pl.get_boolean_attrib(element, 'allow-complex', ALLOW_COMPLEX_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 # Convert to float or complex try: a_sub_parsed = pl.string_to_number(a_sub, allow_complex=allow_complex) if a_sub_parsed is None: raise ValueError('invalid submitted answer (wrong type)') if not np.isfinite(a_sub_parsed): raise ValueError('invalid submitted answer (not finite)') data['submitted_answers'][name] = pl.to_json(a_sub_parsed) except Exception: if allow_complex: data['format_errors'][ name] = 'Invalid format. The submitted answer could not be interpreted as a double-precision floating-point or complex number.' else: data['format_errors'][ name] = 'Invalid format. The submitted answer could not be interpreted as a double-precision floating-point number.' data['submitted_answers'][name] = None
def render(element_html, element_index, data): element = lxml.html.fragment_fromstring(element_html) language = pl.get_string_attrib(element, 'language', None) no_highlight = pl.get_boolean_attrib(element, 'no-highlight', False) specify_language = (language is not None) and (not no_highlight) # Strip a single leading newline from the code, if present. This # avoids having spurious newlines because of HTML like: # # <pl-code> # some_code # </pl-code> # # which technically starts with a newline, but we probably # don't want a blank line at the start of the code block. code = pl.inner_html(element) if len(code) > 1 and code[0] == '\r' and code[1] == '\n': code = code[2:] elif len(code) > 0 and (code[0] == '\n' or code[0] == '\r'): code = code[1:] html_params = { 'specify_language': specify_language, 'language': language, 'no_highlight': no_highlight, 'code': code, } with open('pl-code.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() return html
def render_drawing_items(elem, curid=0, defaults={}): # Convert a set of drawing items defined as html elements into an array of # objects that can be sent to mechanicsObjects.js # Some helpers to get attributes from elements. If there is no default argument passed in, # it is assumed that the attribute must be present or else an error will be raised. If a # default is passed, the attribute is optional. objects = [] for el in elem: if el.tag is lxml.etree.Comment: continue elif el.tag == 'pl-drawing-group': if pl.get_boolean_attrib(el, 'visible', True): curid += 1 raw, _ = render_drawing_items(el, curid, {'groupid': curid}) objs = raw curid += len(objs) objects.extend(objs) else: obj = elements.generate(el, el.tag, defaults) if obj is not None: obj['id'] = curid objects.append(obj) curid += 1 else: warnings.warn('No known tag type: ' + el.tag) return (objects, curid)
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) display_statements, _ = data['params'][name] number_statements = len(display_statements) submitted_answers = data['submitted_answers'] correct_answers = data['correct_answers'].get(name, []) # Count the number of answers that are correct. num_correct = 0 for i in range(number_statements): expected_html_name = get_form_name(name, i) student_answer = int(submitted_answers.get(expected_html_name, -1)) correct_answer = correct_answers[i] if student_answer == correct_answer: num_correct += 1 score = 0 if not partial_credit and num_correct == number_statements: score = 1 elif partial_credit: # EDC grading score = num_correct / number_statements data['partial_scores'][name] = {'score': score, 'weight': weight}
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', ALLOW_COMPLEX_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] = get_format_string('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] = get_format_string(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 parse(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) 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 file_contents = data['submitted_answers'].get(answer_name, None) if not file_contents: add_format_error(data, 'No submitted answer for {0}'.format(file_name)) return if normalize_to_ascii: try: decoded_contents = base64.b64decode(file_contents).decode('utf-8') normalized = unidecode(decoded_contents) file_contents = base64.b64encode(normalized.encode('UTF-8').strip()).decode() data['submitted_answers'][answer_name] = file_contents except UnicodeError: add_format_error(data, 'Submitted answer is not a valid UTF-8 string.') if data['submitted_answers'].get('_files', None) is None: data['submitted_answers']['_files'] = [] data['submitted_answers']['_files'].append({ 'name': file_name, 'contents': file_contents }) elif isinstance(data['submitted_answers'].get('_files', None), list): data['submitted_answers']['_files'].append({ 'name': file_name, 'contents': file_contents }) else: add_format_error(data, '_files was present but was not an array.')
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 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 render(element_html, data): element = lxml.html.fragment_fromstring(element_html) language = pl.get_string_attrib(element, 'language', None) no_highlight = pl.get_boolean_attrib(element, 'no-highlight', False) specify_language = (language is not None) and (not no_highlight) source_file_name = pl.get_string_attrib(element, 'source-file-name', None) prevent_select = pl.get_boolean_attrib(element, 'prevent-select', False) if source_file_name is not None: base_path = data['options']['question_path'] file_path = os.path.join(base_path, source_file_name) if not os.path.exists(file_path): raise Exception(f'Unknown file path: "{file_path}".') f = open(file_path, 'r') code = '' for line in f.readlines(): code += line code = code[:-1] f.close() else: # Strip a single leading newline from the code, if present. This # avoids having spurious newlines because of HTML like: # # <pl-code> # some_code # </pl-code> # # which technically starts with a newline, but we probably # don't want a blank line at the start of the code block. code = pl.inner_html(element) if len(code) > 1 and code[0] == '\r' and code[1] == '\n': code = code[2:] elif len(code) > 0 and (code[0] == '\n' or code[0] == '\r'): code = code[1:] html_params = { 'specify_language': specify_language, 'language': language, 'no_highlight': no_highlight, 'code': code, 'prevent_select': prevent_select, } with open('pl-code.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() return html
def parse(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') allow_complex = pl.get_boolean_attrib(element, 'allow-complex', ALLOW_COMPLEX_DEFAULT) allow_fractions = pl.get_boolean_attrib(element, 'allow-fractions', ALLOW_FRACTIONS_DEFAULT) allow_blank = pl.get_boolean_attrib(element, 'allow-blank', ALLOW_BLANK_DEFAULT) blank_value = pl.get_string_attrib(element, 'blank-value', str(BLANK_VALUE_DEFAULT)) a_sub = data['submitted_answers'].get(name, None) if allow_blank and a_sub is not None and a_sub.strip() == '': a_sub = blank_value value, newdata = pl.string_fraction_to_number(a_sub, allow_fractions, allow_complex) if value is not None: data['submitted_answers'][name] = newdata['submitted_answers'] else: data['format_errors'][name] = get_format_string(allow_complex, allow_fractions, newdata['format_errors']) data['submitted_answers'][name] = None
def parse(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') allow_complex = pl.get_boolean_attrib(element, 'allow-complex', ALLOW_COMPLEX_DEFAULT) allow_fractions = pl.get_boolean_attrib(element, 'allow-fractions', ALLOW_FRACTIONS_DEFAULT) a_sub = data['submitted_answers'].get(name, None) value, newdata = pl.string_fraction_to_number(a_sub, allow_fractions, allow_complex) if value is not None: data['submitted_answers'][name] = newdata['submitted_answers'] else: data['format_errors'][name] = get_format_string( allow_complex, allow_fractions, newdata['format_errors']) data['submitted_answers'][name] = None
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', 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 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_partial_credit = pl.get_boolean_attrib(element, 'allow-partial-credit', ALLOW_PARTIAL_CREDIT_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) # 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 = data['test_type'] 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 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 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 parse(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') base = pl.get_integer_attrib(element, 'base', BASE_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 a_sub.strip() == '': if pl.get_boolean_attrib(element, 'allow-blank', ALLOW_BLANK_DEFAULT): a_sub = pl.get_integer_attrib(element, 'blank-value', BLANK_VALUE_DEFAULT) else: opts = { 'format_error': True, 'format_error_message': 'the submitted answer was blank.', 'base': base, 'default_base': base == BASE_DEFAULT or base == 0, 'zero_base': base == 0 } with open('pl-integer-input.mustache', 'r', encoding='utf-8') as f: format_str = chevron.render(f, opts).strip() data['format_errors'][name] = format_str data['submitted_answers'][name] = None return # Convert to integer try: a_sub_parsed = pl.string_to_integer(str(a_sub), base) if a_sub_parsed is None: raise ValueError('invalid submitted answer (wrong type)') if a_sub_parsed > 2**53 - 1 or a_sub_parsed < -((2**53) - 1): data['format_errors'][ name] = 'correct answer must be between -9007199254740991 and +9007199254740991 (that is, between -(2^53 - 1) and +(2^53 - 1)).' data['submitted_answers'][name] = pl.to_json(a_sub_parsed) except Exception: with open('pl-integer-input.mustache', 'r', encoding='utf-8') as f: format_str = chevron.render( f, { 'format_error': True, 'base': base, 'default_base': base == BASE_DEFAULT or base == 0, 'zero_base': base == 0 }).strip() data['format_errors'][name] = format_str data['submitted_answers'][name] = None
def get_solution(element, data): solution = [] for child in element: if child.tag in ['pl-answer']: pl.check_attribs(child, required_attribs=[], optional_attribs=['correct']) is_correct = pl.get_boolean_attrib(child, 'correct', False) child_html = pl.inner_html(child).strip() if is_correct: solution.append(child_html) if len(solution) > 1: raise Exception('Multiple correct answers were set') return solution[0]
def parse(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') allow_fractions = pl.get_boolean_attrib(element, 'allow-fractions', ALLOW_FRACTIONS_DEFAULT) # 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) value, newdata = pl.string_fraction_to_number(a_sub, allow_fractions, allow_complex=False) if value is not None: A[i, j] = value data['submitted_answers'][each_entry_name] = newdata[ 'submitted_answers'] else: invalid_format = True data['format_errors'][each_entry_name] = newdata[ 'format_errors'] data['submitted_answers'][each_entry_name] = None if invalid_format: with open('pl-matrix-component-input.mustache', 'r', encoding='utf-8') as f: data['format_errors'][name] = chevron.render( f, { 'format_error': True, 'allow_fractions': allow_fractions }).strip() data['submitted_answers'][name] = None else: data['submitted_answers'][name] = pl.to_json(A)
def render(element_html, element_index, data): element = lxml.html.fragment_fromstring(element_html) name = element.get('answers_name') answers = data['params'].get(name, []) inline = pl.get_boolean_attrib(element, 'inline', False) submitted_key = data['submitted_answers'].get(name, None) if data['panel'] == 'question': editable = data['editable'] html = '' for answer in answers: item = ' <label' + (' class="radio-inline"' if inline else '') + '>\n' \ + ' <input type="radio"' \ + ' name="' + name + '" value="' + answer['key'] + '"' \ + ('' if editable else ' disabled') \ + (' checked ' if (submitted_key == answer['key']) else '') \ + ' />\n' \ + ' (' + answer['key'] + ') ' + answer['html'] + '\n' \ + ' </label>\n' if not inline: item = '<div class="radio">\n' + item + '</div>\n' html += item if inline: html = '<p>\n' + html + '</p>\n' elif data['panel'] == 'submission': # FIXME: handle parse errors? if submitted_key is None: html = 'No submitted answer' else: submitted_html = next( (a['html'] for a in answers if a['key'] == submitted_key), None) if submitted_html is None: html = 'ERROR: Invalid submitted value selected: %s' % submitted_key # FIXME: escape submitted_key else: html = '(%s) %s' % (submitted_key, submitted_html) elif data['panel'] == 'answer': correct_answer = data['correct_answers'].get(name, None) if correct_answer is None: html = 'ERROR: No true answer' else: html = '(%s) %s' % (correct_answer['key'], correct_answer['html']) else: raise Exception('Invalid panel type: %s' % data['panel']) return html
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 graphviz_from_matrix(mat, label, engine, element): # Get the matrix specific attributes 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() # Sanity checking if (mat.shape[0] != mat.shape[1]): raise Exception('Non-square adjacency matrix of size (%s,%s) given as input.' % (mat.shape[0], mat.shape[1])) if label is not None: mat_label = label if (mat_label.shape[0] != mat.shape[0]): raise Exception('Dimension of the label is not consistent with the dimension of matrix' % (mat_label.shape[0], 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 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', TYPE_DEFAULT) # Get directory (default is clientFilesQuestion) file_directory = pl.get_string_attrib(element, 'directory', DIRECTORY_DEFAULT) # Get inline (default is false) inline = pl.get_boolean_attrib(element, 'inline', INLINE_DEFAULT) # Get alternate-text text (default is PrairieLearn Image) alt_text = pl.get_string_attrib(element, 'alt', ALT_TEXT_DEFAULT) # 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', WIDTH_DEFAULT) # Create and return html html_params = {'src': file_url, 'width': width, 'inline': inline, 'alt': alt_text} with open('pl-figure.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() return html
def categorize_options(element, data): """Get provided correct and incorrect answers""" correct_answers = [] incorrect_answers = [] index = 0 for child in element: if child.tag in ['pl-answer', 'pl_answer']: pl.check_attribs(child, required_attribs=[], optional_attribs=['correct']) correct = pl.get_boolean_attrib(child, 'correct', False) child_html = pl.inner_html(child) answer_tuple = (index, correct, child_html) if correct: correct_answers.append(answer_tuple) else: incorrect_answers.append(answer_tuple) index += 1 file_path = pl.get_string_attrib(element, 'external-json', EXTERNAL_JSON_DEFAULT) if file_path is not EXTERNAL_JSON_DEFAULT: correct_attrib = pl.get_string_attrib( element, 'external-json-correct-key', EXTERNAL_JSON_CORRECT_KEY_DEFAULT) incorrect_attrib = pl.get_string_attrib( element, 'external-json-incorrect-key', EXTERNAL_JSON_INCORRECT_KEY_DEFAULT) if pathlib.PurePath(file_path).is_absolute(): json_file = file_path else: json_file = pathlib.PurePath( data['options']['question_path']).joinpath(file_path) try: with open(json_file, mode='r', encoding='utf-8') as f: obj = json.load(f) for text in obj.get(correct_attrib, []): correct_answers.append((index, True, text)) index += 1 for text in obj.get(incorrect_attrib, []): incorrect_answers.append((index, False, text)) index += 1 except FileNotFoundError: raise Exception( f'JSON answer file: "{json_file}" could not be found') return correct_answers, incorrect_answers
def categorize_options(element): """Get provided correct and incorrect answers""" correct_answers = [] incorrect_answers = [] index = 0 for child in element: if child.tag in ['pl-answer', 'pl_answer']: pl.check_attribs(child, required_attribs=[], optional_attribs=['correct']) correct = pl.get_boolean_attrib(child, 'correct', False) child_html = pl.inner_html(child) answer_tuple = (index, correct, child_html) if correct: correct_answers.append(answer_tuple) else: incorrect_answers.append(answer_tuple) index += 1 return correct_answers, incorrect_answers
def 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', VARIABLES_DEFAULT)) allow_complex = pl.get_boolean_attrib(element, 'allow-complex', ALLOW_COMPLEX_DEFAULT) 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 = 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 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 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) required_attribs = ['answers-name'] optional_attribs = ['weight', 'number-answers', 'fixed-order', 'inline'] pl.check_attribs(element, required_attribs, optional_attribs) name = pl.get_string_attrib(element, 'answers-name') correct_answers = [] incorrect_answers = [] index = 0 for child in element: if child.tag in ['pl-answer', 'pl_answer']: pl.check_attribs(child, required_attribs=[], optional_attribs=['correct']) correct = pl.get_boolean_attrib(child, 'correct', False) child_html = pl.inner_html(child) answer_tuple = (index, correct, child_html) if correct: correct_answers.append(answer_tuple) else: incorrect_answers.append(answer_tuple) index += 1 len_correct = len(correct_answers) len_incorrect = len(incorrect_answers) len_total = len_correct + len_incorrect if len_correct < 1: raise Exception('pl-multiple-choice element must have at least one correct answer') number_answers = pl.get_integer_attrib(element, 'number-answers', len_total) number_answers = max(1, min(1 + len_incorrect, number_answers)) number_correct = 1 number_incorrect = number_answers - number_correct if not (0 <= number_incorrect <= len_incorrect): raise Exception('INTERNAL ERROR: number_incorrect: (%d, %d, %d)' % (number_incorrect, len_incorrect, number_answers)) sampled_correct = random.sample(correct_answers, number_correct) sampled_incorrect = random.sample(incorrect_answers, number_incorrect) sampled_answers = sampled_correct + sampled_incorrect random.shuffle(sampled_answers) fixed_order = pl.get_boolean_attrib(element, 'fixed-order', False) if fixed_order: # we can't simply skip the shuffle because we already broke the original # order by separating into correct/incorrect lists sampled_answers.sort(key=lambda a: a[0]) # sort by stored original index display_answers = [] correct_answer = None for (i, (index, correct, html)) in enumerate(sampled_answers): keyed_answer = {'key': chr(ord('a') + i), 'html': html} display_answers.append(keyed_answer) if correct: correct_answer = keyed_answer if name in data['params']: raise Exception('duplicate params variable name: %s' % name) if name in data['correct_answers']: raise Exception('duplicate correct_answers variable name: %s' % name) data['params'][name] = display_answers data['correct_answers'][name] = correct_answer
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') answers = data['params'].get(name, []) inline = pl.get_boolean_attrib(element, 'inline', False) submitted_key = data['submitted_answers'].get(name, None) correct_key = data['correct_answers'].get(name, {'key': None}).get('key', None) if data['panel'] == 'question': editable = data['editable'] partial_score = data['partial_scores'].get(name, {'score': None}) score = partial_score.get('score', None) html = '' for answer in answers: item = '<input class="form-check-input" type="radio"' \ + ' name="' + name + '" value="' + answer['key'] + '"' \ + ('' if editable else ' disabled') \ + (' checked ' if (submitted_key == answer['key']) else '') \ + f' id="{name}-{answer["key"]}"' \ + ' />\n' \ + f'<label class="form-check-label" for="{name}-{answer["key"]}">\n' \ + ' (' + answer['key'] + ') ' + answer['html'] + '\n' if score is not None: if submitted_key == answer['key']: if correct_key == answer['key']: item = item + '<span class="badge badge-success"><i class="fa fa-check" aria-hidden="true"></i></span>' else: item = item + '<span class="badge badge-danger"><i class="fa fa-times" aria-hidden="true"></i></span>' item += '</label>\n' item = f'<div class="form-check {"form-check-inline" if inline else ""}">\n' + item + '</div>\n' html += item if inline: html = '<span>\n' + html + '</span>\n' if score is not None: try: score = float(score) if score >= 1: html = html + ' <span class="badge badge-success"><i class="fa fa-check" aria-hidden="true"></i> 100%</span>' elif score > 0: html = html + ' <span class="badge badge-warning"><i class="fa fa-circle-o" aria-hidden="true"></i> {:d}%</span>'.format(math.floor(score * 100)) else: html = html + ' <span class="badge badge-danger"><i class="fa fa-times" aria-hidden="true"></i> 0%</span>' except Exception: raise ValueError('invalid score' + score) elif data['panel'] == 'submission': # FIXME: handle parse errors? if submitted_key is None: html = 'No submitted answer' else: submitted_html = next((a['html'] for a in answers if a['key'] == submitted_key), None) if submitted_html is None: html = 'ERROR: Invalid submitted value selected: %s' % submitted_key # FIXME: escape submitted_key else: html = '(%s) %s' % (submitted_key, submitted_html) partial_score = data['partial_scores'].get(name, {'score': None}) score = partial_score.get('score', None) if score is not None: try: score = float(score) if score >= 1: html = html + ' <span class="badge badge-success"><i class="fa fa-check" aria-hidden="true"></i> 100%</span>' elif score > 0: html = html + ' <span class="badge badge-warning"><i class="fa fa-circle-o" aria-hidden="true"></i> {:d}%</span>'.format(math.floor(score * 100)) else: html = html + ' <span class="badge badge-danger"><i class="fa fa-times" aria-hidden="true"></i> 0%</span>' except Exception: raise ValueError('invalid score' + score) elif data['panel'] == 'answer': correct_answer = data['correct_answers'].get(name, None) if correct_answer is None: html = 'ERROR: No true answer' else: html = '(%s) %s' % (correct_answer['key'], correct_answer['html']) else: raise Exception('Invalid panel type: %s' % data['panel']) return html
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') label = pl.get_string_attrib(element, 'label', None) if '_pl_matrix_input_format' in data['submitted_answers']: format_type = data['submitted_answers']['_pl_matrix_input_format'].get(name, 'matlab') else: format_type = 'matlab' if data['panel'] == 'question': editable = data['editable'] raw_submitted_answer = data['raw_submitted_answers'].get(name, None) # Get comparison parameters and info strings comparison = pl.get_string_attrib(element, 'comparison', 'relabs') if comparison == 'relabs': rtol = pl.get_float_attrib(element, 'rtol', 1e-2) atol = pl.get_float_attrib(element, 'atol', 1e-8) if (rtol < 0): raise ValueError('Attribute rtol = {:g} must be non-negative'.format(rtol)) if (atol < 0): raise ValueError('Attribute atol = {:g} must be non-negative'.format(atol)) info_params = {'format': True, 'relabs': True, 'rtol': '{:g}'.format(rtol), 'atol': '{:g}'.format(atol)} elif comparison == 'sigfig': digits = pl.get_integer_attrib(element, 'digits', 2) if (digits < 0): raise ValueError('Attribute digits = {:d} must be non-negative'.format(digits)) info_params = {'format': True, 'sigfig': True, 'digits': '{:d}'.format(digits), 'comparison_eps': 0.51 * (10**-(digits - 1))} elif comparison == 'decdig': digits = pl.get_integer_attrib(element, 'digits', 2) if (digits < 0): raise ValueError('Attribute digits = {:d} must be non-negative'.format(digits)) info_params = {'format': True, 'decdig': True, 'digits': '{:d}'.format(digits), 'comparison_eps': 0.51 * (10**-(digits - 0))} else: raise ValueError('method of comparison "%s" is not valid (must be "relabs", "sigfig", or "decdig")' % comparison) info_params['allow_complex'] = pl.get_boolean_attrib(element, 'allow-complex', False) with open('pl-matrix-input.mustache', 'r', encoding='utf-8') as f: info = chevron.render(f, info_params).strip() with open('pl-matrix-input.mustache', 'r', encoding='utf-8') as f: info_params.pop('format', None) info_params['shortformat'] = True shortinfo = chevron.render(f, info_params).strip() html_params = { 'question': True, 'name': name, 'label': label, 'editable': editable, 'info': info, 'shortinfo': shortinfo, 'uuid': pl.get_uuid() } partial_score = data['partial_scores'].get(name, {'score': None}) score = partial_score.get('score', None) if score is not None: try: score = float(score) if score >= 1: html_params['correct'] = True elif score > 0: html_params['partial'] = math.floor(score * 100) else: html_params['incorrect'] = True except Exception: raise ValueError('invalid score' + score) if raw_submitted_answer is not None: html_params['raw_submitted_answer'] = escape(raw_submitted_answer) with open('pl-matrix-input.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() elif data['panel'] == 'submission': parse_error = data['format_errors'].get(name, None) html_params = { 'submission': True, 'label': label, 'parse_error': parse_error, 'uuid': pl.get_uuid() } if parse_error is None: # Get submitted answer, raising an exception if it does not exist a_sub = data['submitted_answers'].get(name, None) if a_sub is None: raise Exception('submitted answer is None') # If answer is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) a_sub = pl.from_json(a_sub) # Wrap answer in an ndarray (if it's already one, this does nothing) a_sub = np.array(a_sub) # Format answer as a string html_params['a_sub'] = pl.string_from_2darray(a_sub, language=format_type, digits=12, presentation_type='g') else: raw_submitted_answer = data['raw_submitted_answers'].get(name, None) if raw_submitted_answer is not None: html_params['raw_submitted_answer'] = escape(raw_submitted_answer) partial_score = data['partial_scores'].get(name, {'score': None}) score = partial_score.get('score', None) if score is not None: try: score = float(score) if score >= 1: html_params['correct'] = True elif score > 0: html_params['partial'] = math.floor(score * 100) else: html_params['incorrect'] = True except Exception: raise ValueError('invalid score' + score) with open('pl-matrix-input.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() elif data['panel'] == 'answer': # Get true answer - do nothing if it does not exist a_tru = pl.from_json(data['correct_answers'].get(name, None)) if a_tru is not None: a_tru = np.array(a_tru) # Get comparison parameters comparison = pl.get_string_attrib(element, 'comparison', 'relabs') if comparison == 'relabs': rtol = pl.get_float_attrib(element, 'rtol', 1e-2) atol = pl.get_float_attrib(element, 'atol', 1e-8) # FIXME: render correctly with respect to rtol and atol matlab_data = pl.string_from_2darray(a_tru, language='matlab', digits=12, presentation_type='g') python_data = pl.string_from_2darray(a_tru, language='python', digits=12, presentation_type='g') elif comparison == 'sigfig': digits = pl.get_integer_attrib(element, 'digits', 2) matlab_data = pl.string_from_2darray(a_tru, language='matlab', digits=digits, presentation_type='sigfig') python_data = pl.string_from_2darray(a_tru, language='python', digits=digits, presentation_type='sigfig') elif comparison == 'decdig': digits = pl.get_integer_attrib(element, 'digits', 2) matlab_data = pl.string_from_2darray(a_tru, language='matlab', digits=digits, presentation_type='f') python_data = pl.string_from_2darray(a_tru, language='python', digits=digits, presentation_type='f') else: raise ValueError('method of comparison "%s" is not valid (must be "relabs", "sigfig", or "decdig")' % comparison) html_params = { 'answer': True, 'label': label, 'matlab_data': matlab_data, 'python_data': python_data, 'uuid': pl.get_uuid() } if format_type == 'matlab': html_params['default_is_matlab'] = True else: html_params['default_is_python'] = True with open('pl-matrix-input.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() else: html = '' else: raise Exception('Invalid panel type: %s' % data['panel']) return html
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') label = pl.get_string_attrib(element, 'label', None) variables_string = pl.get_string_attrib(element, 'variables', None) variables = get_variables_list(variables_string) display = pl.get_string_attrib(element, 'display', 'inline') allow_complex = pl.get_boolean_attrib(element, 'allow-complex', False) imaginary_unit = pl.get_string_attrib(element, 'imaginary-unit-for-display', 'i') if data['panel'] == 'question': editable = data['editable'] raw_submitted_answer = data['raw_submitted_answers'].get(name, None) operators = ', '.join(['cos', 'sin', 'tan', 'exp', 'log', 'sqrt', '( )', '+', '-', '*', '/', '^', '**']) constants = ', '.join(['pi, e']) info_params = { 'format': True, 'variables': variables_string, 'operators': operators, 'constants': constants, 'allow_complex': allow_complex, } with open('pl-symbolic-input.mustache', 'r', encoding='utf-8') as f: info = chevron.render(f, info_params).strip() with open('pl-symbolic-input.mustache', 'r', encoding='utf-8') as f: info_params.pop('format', None) info_params['shortformat'] = True shortinfo = chevron.render(f, info_params).strip() html_params = { 'question': True, 'name': name, 'label': label, 'editable': editable, 'info': info, 'shortinfo': shortinfo, 'uuid': pl.get_uuid(), 'allow_complex': allow_complex, } partial_score = data['partial_scores'].get(name, {'score': None}) score = partial_score.get('score', None) if score is not None: try: score = float(score) if score >= 1: html_params['correct'] = True elif score > 0: html_params['partial'] = math.floor(score * 100) else: html_params['incorrect'] = True except Exception: raise ValueError('invalid score' + score) if display == 'inline': html_params['inline'] = True elif display == 'block': html_params['block'] = True else: raise ValueError('method of display "%s" is not valid (must be "inline" or "block")' % display) if raw_submitted_answer is not None: html_params['raw_submitted_answer'] = escape(raw_submitted_answer) with open('pl-symbolic-input.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() elif data['panel'] == 'submission': parse_error = data['format_errors'].get(name, None) html_params = { 'submission': True, 'label': label, 'parse_error': parse_error, 'uuid': pl.get_uuid() } if parse_error is None: a_sub = data['submitted_answers'][name] if isinstance(a_sub, str): # this is for backward-compatibility a_sub = phs.convert_string_to_sympy(a_sub, variables, allow_complex=allow_complex) else: a_sub = phs.json_to_sympy(a_sub, allow_complex=allow_complex) a_sub = a_sub.subs(sympy.I, sympy.Symbol(imaginary_unit)) html_params['a_sub'] = sympy.latex(a_sub) else: raw_submitted_answer = data['raw_submitted_answers'].get(name, None) if raw_submitted_answer is not None: html_params['raw_submitted_answer'] = escape(raw_submitted_answer) partial_score = data['partial_scores'].get(name, {'score': None}) score = partial_score.get('score', None) if score is not None: try: score = float(score) if score >= 1: html_params['correct'] = True elif score > 0: html_params['partial'] = math.floor(score * 100) else: html_params['incorrect'] = True except Exception: raise ValueError('invalid score' + score) if display == 'inline': html_params['inline'] = True elif display == 'block': html_params['block'] = True else: raise ValueError('method of display "%s" is not valid (must be "inline" or "block")' % display) with open('pl-symbolic-input.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() elif data['panel'] == 'answer': a_tru = data['correct_answers'].get(name, None) if a_tru is not None: if isinstance(a_tru, str): # this is so instructors can specify the true answer simply as a string a_tru = phs.convert_string_to_sympy(a_tru, variables, allow_complex=allow_complex) else: a_tru = phs.json_to_sympy(a_tru, allow_complex=allow_complex) a_tru = a_tru.subs(sympy.I, sympy.Symbol(imaginary_unit)) html_params = { 'answer': True, 'label': label, 'a_tru': sympy.latex(a_tru) } with open('pl-symbolic-input.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() else: html = '' else: raise Exception('Invalid panel type: %s' % data['panel']) return html
def parse(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) imaginary_unit = pl.get_string_attrib(element, 'imaginary-unit-for-display', 'i') # 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 # Parse the submitted answer and put the result in a string 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('^', '**') # Strip whitespace a_sub = a_sub.strip() # Convert safely to sympy a_sub_parsed = phs.convert_string_to_sympy(a_sub, variables, allow_complex=allow_complex) # If complex numbers are not allowed, raise error if expression has the imaginary unit if (not allow_complex) and (a_sub_parsed.has(sympy.I)): a_sub_parsed = a_sub_parsed.subs(sympy.I, sympy.Symbol(imaginary_unit)) s = 'Your answer was simplified to this, which contains a complex number (denoted ${:s}$): $${:s}$$'.format(imaginary_unit, sympy.latex(a_sub_parsed)) data['format_errors'][name] = s data['submitted_answers'][name] = None return # Store result as json. a_sub_json = phs.sympy_to_json(a_sub_parsed, allow_complex=allow_complex) except phs.HasFloatError as err: s = 'Your answer contains the floating-point number ' + str(err.n) + '. ' s += 'All numbers must be expressed as integers (or ratios of integers). ' s += '<br><br><pre>' + phs.point_to_error(a_sub, err.offset) + '</pre>' data['format_errors'][name] = s data['submitted_answers'][name] = None return except phs.HasComplexError as err: s = 'Your answer contains the complex number ' + str(err.n) + '. ' s += 'All numbers must be expressed as integers (or ratios of integers). ' if allow_complex: s += 'To include a complex number in your expression, write it as the product of an integer with the imaginary unit <code>i</code> or <code>j</code>. ' s += '<br><br><pre>' + phs.point_to_error(a_sub, err.offset) + '</pre>' data['format_errors'][name] = s data['submitted_answers'][name] = None return except phs.HasInvalidExpressionError as err: s = 'Your answer has an invalid expression. ' s += '<br><br><pre>' + phs.point_to_error(a_sub, err.offset) + '</pre>' data['format_errors'][name] = s data['submitted_answers'][name] = None return except phs.HasInvalidFunctionError as err: s = 'Your answer calls an invalid function "' + err.text + '". ' s += '<br><br><pre>' + phs.point_to_error(a_sub, err.offset) + '</pre>' data['format_errors'][name] = s data['submitted_answers'][name] = None return except phs.HasInvalidVariableError as err: s = 'Your answer refers to an invalid variable "' + err.text + '". ' s += '<br><br><pre>' + phs.point_to_error(a_sub, err.offset) + '</pre>' data['format_errors'][name] = s data['submitted_answers'][name] = None return except phs.HasParseError as err: s = 'Your answer has a syntax error. ' s += '<br><br><pre>' + phs.point_to_error(a_sub, err.offset) + '</pre>' data['format_errors'][name] = s data['submitted_answers'][name] = None return except phs.HasEscapeError as err: s = 'Your answer must not contain the character "\\". ' s += '<br><br><pre>' + phs.point_to_error(a_sub, err.offset) + '</pre>' data['format_errors'][name] = s data['submitted_answers'][name] = None return except phs.HasCommentError as err: s = 'Your answer must not contain the character "#". ' s += '<br><br><pre>' + phs.point_to_error(a_sub, err.offset) + '</pre>' data['format_errors'][name] = s data['submitted_answers'][name] = None return except Exception: data['format_errors'][name] = 'Invalid format.' data['submitted_answers'][name] = None return # Make sure we can parse the json again try: # Convert safely to sympy phs.json_to_sympy(a_sub_json, allow_complex=allow_complex) # Finally, store the result data['submitted_answers'][name] = a_sub_json except Exception: s = 'Your answer was simplified to this, which contains an invalid expression: $${:s}$$'.format(sympy.latex(a_sub_parsed)) data['format_errors'][name] = s data['submitted_answers'][name] = None
def grade(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') allow_partial_credit = pl.get_boolean_attrib(element, 'allow-partial-credit', False) # Get weight weight = pl.get_integer_attrib(element, 'weight', 1) # Get method of comparison, with relabs as default comparison = pl.get_string_attrib(element, 'comparison', 'relabs') if comparison == 'relabs': rtol = pl.get_float_attrib(element, 'rtol', 1e-2) atol = pl.get_float_attrib(element, 'atol', 1e-8) elif comparison == 'sigfig': digits = pl.get_integer_attrib(element, 'digits', 2) elif comparison == 'decdig': digits = pl.get_integer_attrib(element, 'digits', 2) else: raise ValueError('method of comparison "%s" is not valid' % comparison) # 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') else: m, n = np.shape(a_tru) number_of_correct = 0 feedback = {} 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) # Get submitted answer (if it does not exist, score is zero) 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) # Compare submitted answer with true answer if comparison == 'relabs': correct = pl.is_correct_scalar_ra(a_sub, a_tru[i, j], rtol, atol) elif comparison == 'sigfig': correct = pl.is_correct_scalar_sf(a_sub, a_tru[i, j], digits) elif comparison == 'decdig': correct = pl.is_correct_scalar_dd(a_sub, a_tru[i, j], digits) if correct: number_of_correct += 1 feedback.update({each_entry_name: 'correct'}) else: feedback.update({each_entry_name: 'incorrect'}) 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) answer_name = pl.get_string_attrib(element, 'answer-name') uuid = pl.get_uuid() body_position = get_position(element, 'body-position', default=[0, 0, 0]) body_orientation = get_orientation(element, 'body-orientation', 'body-pose-format') camera_position = get_position(element, 'camera-position', default=[5, 2, 2], must_be_nonzero=True) body_cantranslate = pl.get_boolean_attrib(element, 'body-cantranslate', True) body_canrotate = pl.get_boolean_attrib(element, 'body-canrotate', True) camera_canmove = pl.get_boolean_attrib(element, 'camera-canmove', True) text_pose_format = pl.get_string_attrib(element, 'text-pose-format', 'matrix') if text_pose_format not in ['matrix', 'quaternion', 'homogeneous']: raise Exception('attribute "text-pose-format" must be either "matrix", "quaternion", or homogeneous') objects = get_objects(element, data) if data['panel'] == 'question': will_be_graded = pl.get_boolean_attrib(element, 'grade', True) show_pose = pl.get_boolean_attrib(element, 'show-pose-in-question', True) # Restore pose of body and camera, if available - otherwise use values # from attributes (note that restored pose will also have camera_orientation, # which we currently ignore because the camera is always z up and looking # at the origin of the space frame). # # Be careful. It's possible that data['submitted_answers'][answer_name] # exists but is None (due to some other error). So we need to use None # as the default and to check if the result - either from the existing # value or the default value - is None. pose_default = { 'body_quaternion': body_orientation, 'body_position': body_position, 'camera_position': camera_position } pose = data['submitted_answers'].get(answer_name, None) if pose is None: pose = pose_default # These are passed as arguments to PLThreeJS constructor in client code options = { 'uuid': uuid, 'pose': dict_to_b64(pose), 'pose_default': dict_to_b64(pose_default), 'body_cantranslate': body_cantranslate, 'body_canrotate': body_canrotate, 'camera_canmove': camera_canmove, 'text_pose_format': text_pose_format, 'objects': objects } # These are used for templating html_params = { 'question': True, 'uuid': uuid, 'answer_name': answer_name, 'show_bodybuttons': body_cantranslate or body_canrotate, 'show_toggle': body_cantranslate and body_canrotate, 'show_reset': body_cantranslate or body_canrotate or camera_canmove, 'show_pose': show_pose, 'show_instructions': will_be_graded, 'tol_translation': '{:.2f}'.format(pl.get_float_attrib(element, 'tol-translation', 0.5)), 'tol_rotation': '{:.1f}'.format(pl.get_float_attrib(element, 'tol-rotation', 5)), 'default_is_python': True, 'options': json.dumps(options, allow_nan=False) } with open('pl-threejs.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() elif data['panel'] == 'submission': will_be_graded = pl.get_boolean_attrib(element, 'grade', True) if not will_be_graded: return '' show_pose = pl.get_boolean_attrib(element, 'show-pose-in-submitted-answer', True) # Get submitted answer pose = data['submitted_answers'].get(answer_name) # These are passed as arguments to PLThreeJS constructor in client code options = { 'uuid': uuid, 'pose': dict_to_b64(pose), 'body_cantranslate': False, 'body_canrotate': False, 'camera_canmove': False, 'text_pose_format': text_pose_format, 'objects': objects } # These are used for templating html_params = { 'submission': True, 'uuid': uuid, 'answer_name': answer_name, 'show_bodybuttons': False, 'show_toggle': False, 'show_pose': show_pose, 'default_is_python': True, 'options': json.dumps(options, allow_nan=False) } partial_score = data['partial_scores'].get(answer_name, None) if partial_score is not None: html_params['error_in_translation'] = str(np.abs(np.round(partial_score['feedback']['error_in_translation'], 2))) html_params['error_in_rotation'] = str(np.abs(np.round(partial_score['feedback']['error_in_rotation'], 1))) html_params['show_feedback'] = True score = partial_score.get('score', None) if score is not None: try: score = float(score) if score >= 1: html_params['correct'] = True elif score > 0: html_params['partial'] = math.floor(score * 100) else: html_params['incorrect'] = True except Exception: raise ValueError('invalid score' + score) with open('pl-threejs.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() elif data['panel'] == 'answer': will_be_graded = pl.get_boolean_attrib(element, 'grade', True) if not will_be_graded: return '' show_pose = pl.get_boolean_attrib(element, 'show-pose-in-correct-answer', True) # Get submitted answer pose = data['submitted_answers'].get(answer_name, None) if pose is None: # If we are here, an error has occurred. Replace pose with its default. # (Only pose['camera_position'] is actually used.) pose = { 'body_quaternion': body_orientation, 'body_position': body_position, 'camera_position': camera_position } # Get correct answer a = data['correct_answers'].get(answer_name, None) if a is None: return '' # Convert correct answer to Quaternion, then to [x, y, z, w] f = pl.get_string_attrib(element, 'answer-pose-format', 'rpy') p, q = parse_correct_answer(f, a) p = p.tolist() q = np.roll(q.elements, -1).tolist() # Replace body pose with correct answer pose['body_position'] = p pose['body_quaternion'] = q # These are passed as arguments to PLThreeJS constructor in client code options = { 'uuid': uuid, 'pose': dict_to_b64(pose), 'body_cantranslate': False, 'body_canrotate': False, 'camera_canmove': False, 'text_pose_format': text_pose_format, 'objects': objects } # These are used for templating html_params = { 'answer': True, 'uuid': uuid, 'answer_name': answer_name, 'show_bodybuttons': False, 'show_toggle': False, 'show_pose': show_pose, 'default_is_python': True, 'options': json.dumps(options, allow_nan=False) } with open('pl-threejs.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() else: raise Exception('Invalid panel type: %s' % data['panel']) return html
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) # get the name of the element, in this case, the name of the array name = pl.get_string_attrib(element, 'answers-name') label = pl.get_string_attrib(element, 'label', None) allow_partial_credit = pl.get_boolean_attrib(element, 'allow-partial-credit', False) allow_feedback = pl.get_boolean_attrib(element, 'allow-feedback', allow_partial_credit) if data['panel'] == 'question': editable = data['editable'] # Get true answer a_tru = pl.from_json(data['correct_answers'].get(name, None)) if a_tru is None: raise Exception('No value in data["correct_answers"] for variable %s in pl-matrix-component-input element' % name) else: if np.isscalar(a_tru): raise Exception('Value in data["correct_answers"] for variable %s in pl-matrix-component-input element cannot be a scalar.' % name) else: a_tru = np.array(a_tru) if a_tru.ndim != 2: raise Exception('Value in data["correct_answers"] for variable %s in pl-matrix-component-input element must be a 2D array.' % name) else: m, n = np.shape(a_tru) input_array = createTableForHTMLDisplay(m, n, name, label, data, 'input') # Get comparison parameters and info strings comparison = pl.get_string_attrib(element, 'comparison', 'relabs') if comparison == 'relabs': rtol = pl.get_float_attrib(element, 'rtol', 1e-2) atol = pl.get_float_attrib(element, 'atol', 1e-8) if (rtol < 0): raise ValueError('Attribute rtol = {:g} must be non-negative'.format(rtol)) if (atol < 0): raise ValueError('Attribute atol = {:g} must be non-negative'.format(atol)) info_params = {'format': True, 'relabs': True, 'rtol': '{:g}'.format(rtol), 'atol': '{:g}'.format(atol)} elif comparison == 'sigfig': digits = pl.get_integer_attrib(element, 'digits', 2) if (digits < 0): raise ValueError('Attribute digits = {:d} must be non-negative'.format(digits)) info_params = {'format': True, 'sigfig': True, 'digits': '{:d}'.format(digits), 'comparison_eps': 0.51 * (10**-(digits - 1))} elif comparison == 'decdig': digits = pl.get_integer_attrib(element, 'digits', 2) if (digits < 0): raise ValueError('Attribute digits = {:d} must be non-negative'.format(digits)) info_params = {'format': True, 'decdig': True, 'digits': '{:d}'.format(digits), 'comparison_eps': 0.51 * (10**-(digits - 0))} else: raise ValueError('method of comparison "%s" is not valid (must be "relabs", "sigfig", or "decdig")' % comparison) with open('pl-matrix-component-input.mustache', 'r', encoding='utf-8') as f: info = chevron.render(f, info_params).strip() with open('pl-matrix-component-input.mustache', 'r', encoding='utf-8') as f: info_params.pop('format', None) info_params['shortformat'] = True shortinfo = chevron.render(f, info_params).strip() html_params = { 'question': True, 'name': name, 'label': label, 'editable': editable, 'info': info, 'shortinfo': shortinfo, 'input_array': input_array, 'inline': True, 'uuid': pl.get_uuid() } partial_score = data['partial_scores'].get(name, {'score': None}) score = partial_score.get('score', None) if score is not None: try: score = float(score) if score >= 1: html_params['correct'] = True elif score > 0: html_params['partial'] = math.floor(score * 100) else: html_params['incorrect'] = True except Exception: raise ValueError('invalid score' + score) with open('pl-matrix-component-input.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() elif data['panel'] == 'submission': parse_error = data['format_errors'].get(name, None) html_params = { 'submission': True, 'label': label, 'parse_error': parse_error, 'uuid': pl.get_uuid() } a_tru = pl.from_json(data['correct_answers'].get(name, None)) m, n = np.shape(a_tru) partial_score = data['partial_scores'].get(name, {'score': None}) score = partial_score.get('score', None) if score is not None: try: score = float(score) if score >= 1: html_params['correct'] = True elif score > 0: html_params['partial'] = math.floor(score * 100) else: html_params['incorrect'] = True except Exception: raise ValueError('invalid score' + score) if parse_error is None: # Get submitted answer, raising an exception if it does not exist a_sub = data['submitted_answers'].get(name, None) if a_sub is None: raise Exception('submitted answer is None') # If answer is in a format generated by pl.to_json, convert it back to a standard type (otherwise, do nothing) a_sub = pl.from_json(a_sub) # Wrap answer in an ndarray (if it's already one, this does nothing) a_sub = np.array(a_sub) # Format submitted answer as a latex string sub_latex = '$' + pl.latex_from_2darray(a_sub, presentation_type='g', digits=12) + '$' # When allowing feedback, display submitted answers using html table sub_html_table = createTableForHTMLDisplay(m, n, name, label, data, 'output-feedback') if allow_feedback and score is not None: if score < 1: html_params['a_sub_feedback'] = sub_html_table else: html_params['a_sub'] = sub_latex else: html_params['a_sub'] = sub_latex else: # create html table to show submitted answer when there is an invalid format html_params['raw_submitted_answer'] = createTableForHTMLDisplay(m, n, name, label, data, 'output-invalid') with open('pl-matrix-component-input.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() elif data['panel'] == 'answer': # Get true answer - do nothing if it does not exist a_tru = pl.from_json(data['correct_answers'].get(name, None)) if a_tru is not None: a_tru = np.array(a_tru) # Get comparison parameters and create the display data comparison = pl.get_string_attrib(element, 'comparison', 'relabs') if comparison == 'relabs': rtol = pl.get_float_attrib(element, 'rtol', 1e-2) atol = pl.get_float_attrib(element, 'atol', 1e-8) # FIXME: render correctly with respect to rtol and atol latex_data = '$' + pl.latex_from_2darray(a_tru, presentation_type='g', digits=12) + '$' elif comparison == 'sigfig': digits = pl.get_integer_attrib(element, 'digits', 2) latex_data = '$' + pl.latex_from_2darray(a_tru, presentation_type='sigfig', digits=digits) + '$' elif comparison == 'decdig': digits = pl.get_integer_attrib(element, 'digits', 2) latex_data = '$' + pl.latex_from_2darray(a_tru, presentation_type='f', digits=digits) + '$' else: raise ValueError('method of comparison "%s" is not valid (must be "relabs", "sigfig", or "decdig")' % comparison) html_params = { 'answer': True, 'label': label, 'latex_data': latex_data, 'uuid': pl.get_uuid() } with open('pl-matrix-component-input.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() else: html = '' else: raise Exception('Invalid panel type: %s' % data['panel']) return html