def generate(el, data): return { 'left': pl.get_float_attrib(el, 'x', 20), 'top': pl.get_float_attrib(el, 'y', 20), 'angle': pl.get_float_attrib(el, 'angle', 0), 'image_url': path.join(data['clientFilesUrl'], 'logo.png') }
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 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, 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', 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 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): # Get the name of the element and the weight for this answer element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') weight = pl.get_float_attrib(element, 'weight', 1.0) # Get the number of submitted clicks and the correct number of clicks submitted_answer = data["submitted_answers"][name] correct_answer = data["correct_answers"][name] score = 0.0 feedback = None # Grade the actual number of clicks if submitted_answer == correct_answer: score = 1.0 elif submitted_answer == correct_answer - 1: score = 0.75 feedback = 'Your number was one too small.' elif submitted_answer == correct_answer + 1: score = 0.5 feedback = 'Your number was one too large.' else: score = 0 feedback = "You didn't click on the image the correct number of times" # Put the score, weight, and feedback into the data object data['partial_scores'][name] = { 'score': score, 'weight': weight, 'feedback': feedback }
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 # 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_scalar_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_scalar_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_scalar_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 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', 'comparison', 'rtol', 'atol', 'digits', 'allow-complex'] pl.check_attribs(element, required_attribs, optional_attribs) name = pl.get_string_attrib(element, 'answers-name') correct_answer = pl.get_float_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 = ['answers-name'] optional_attribs = ['weight', 'correct-answer', 'label', 'suffix', 'display', 'comparison', 'rtol', 'atol', 'digits', 'allow-complex', 'show-help-text', 'size', 'show-correct-answer', 'show-placeholder', 'allow-fractions', 'allow-blank', 'blank-value', 'custom-format'] pl.check_attribs(element, required_attribs, optional_attribs) name = pl.get_string_attrib(element, 'answers-name') correct_answer = pl.get_float_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 custom_format = pl.get_string_attrib(element, 'custom-format', None) if custom_format is not None: try: _ = ('{:' + custom_format + '}').format(0) except ValueError: raise Exception('invalid custom format: %s' % custom_format) from None
def render(element_html, element_index, 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 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-5) atol = pl.get_float_attrib(element, 'atol', 1e-8) info_params = { 'format': True, 'relabs': True, 'rtol': rtol, 'atol': atol } elif comparison == 'sigfig': digits = pl.get_integer_attrib(element, 'digits', 2) info_params = {'format': True, 'sigfig': True, 'digits': digits} elif comparison == 'decdig': digits = pl.get_integer_attrib(element, 'digits', 2) info_params = {'format': True, 'decdig': True, 'digits': digits} else: raise ValueError( 'method of comparison "%s" is not valid (must be "relabs", "sigfig", or "decdig")' % comparison) with open('pl_matrix_input.mustache', 'r') as f: info = chevron.render(f, info_params).strip() with open('pl_matrix_input.mustache', 'r') 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 } if raw_submitted_answer is not None: html_params['raw_submitted_answer'] = escape(raw_submitted_answer) with open('pl_matrix_input.mustache', 'r') 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 } if parse_error is None: a_sub = np.array(data['submitted_answers'][name]) html_params['a_sub'] = pl.numpy_to_matlab(a_sub, ndigits=12, wtype='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) with open('pl_matrix_input.mustache', 'r') 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 = 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-5) atol = pl.get_float_attrib(element, 'atol', 1e-8) # FIXME: render correctly with respect to rtol and atol a_tru = pl.numpy_to_matlab(a_tru, ndigits=12, wtype='g') elif comparison == 'sigfig': digits = pl.get_integer_attrib(element, 'digits', 2) a_tru = pl.numpy_to_matlab_sf(a_tru, ndigits=digits) elif comparison == 'decdig': digits = pl.get_integer_attrib(element, 'digits', 2) a_tru = pl.numpy_to_matlab(a_tru, ndigits=digits, wtype='f') else: raise ValueError( 'method of comparison "%s" is not valid (must be "relabs", "sigfig", or "decdig")' % comparison) # FIXME: render correctly with respect to method of comparison html_params = {'answer': True, 'label': label, 'a_tru': a_tru} with open('pl_matrix_input.mustache', 'r') as f: html = chevron.render(f, html_params).strip() else: html = '' else: raise Exception('Invalid panel type: %s' % data['panel']) 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=['score', 'correct', 'feedback']) correct = pl.get_boolean_attrib(child, 'correct', False) child_html = pl.inner_html(child) child_feedback = pl.get_string_attrib(child, 'feedback', FEEDBACK_DEFAULT) default_score = SCORE_CORRECT_DEFAULT if correct else SCORE_INCORRECT_DEFAULT score = pl.get_float_attrib(child, 'score', default_score) if not (SCORE_INCORRECT_DEFAULT <= score <= SCORE_CORRECT_DEFAULT): raise Exception( f'Score {score} is invalid, must be in the range [0.0, 1.0].' ) if correct and score != SCORE_CORRECT_DEFAULT: raise Exception('Correct answers must give full credit.') answer_tuple = (index, correct, child_html, child_feedback, score) if correct: correct_answers.append(answer_tuple) else: incorrect_answers.append(answer_tuple) index += 1 file_path = pl.get_string_attrib(element, 'external-json', EXTERNAL_JSON_DEFAULT) if file_path is not EXTERNAL_JSON_DEFAULT: correct_attrib = pl.get_string_attrib( element, 'external-json-correct-key', EXTERNAL_JSON_CORRECT_KEY_DEFAULT) incorrect_attrib = pl.get_string_attrib( element, 'external-json-incorrect-key', EXTERNAL_JSON_INCORRECT_KEY_DEFAULT) if pathlib.PurePath(file_path).is_absolute(): json_file = file_path else: json_file = pathlib.PurePath( data['options']['question_path']).joinpath(file_path) try: with open(json_file, mode='r', encoding='utf-8') as f: obj = json.load(f) for text in obj.get(correct_attrib, []): correct_answers.append( (index, True, text, None, SCORE_CORRECT_DEFAULT)) index += 1 for text in obj.get(incorrect_attrib, []): incorrect_answers.append( (index, False, text, None, SCORE_INCORRECT_DEFAULT)) index += 1 except FileNotFoundError: raise Exception( f'JSON answer file: "{json_file}" could not be found') return correct_answers, incorrect_answers
def 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', '') preview_mode = not pl.get_boolean_attrib( element, 'gradable', defaults.element_defaults['gradable']) with open('pl-drawing.mustache') as f: template = f.read() btn_markup = '' init = [] load_extensions(data) for el in element: if el.tag is lxml.etree.Comment: continue elif el.tag == 'pl-controls' and not preview_mode: btn_markup = render_controls(template, el) elif el.tag == 'pl-drawing-initial': init, _ = render_drawing_items(el) draw_error_box = pl.get_boolean_attrib( el, 'draw-error-box', defaults.element_defaults['draw-error-box']) for obj in init: obj['graded'] = False obj['drawErrorBox'] = draw_error_box if 'objectDrawErrorBox' in obj: if obj['objectDrawErrorBox'] is not None: obj['drawErrorBox'] = obj['objectDrawErrorBox'] grid_size = pl.get_integer_attrib(element, 'grid-size', defaults.element_defaults['grid-size']) tol = pl.get_float_attrib(element, 'tol', grid_size / 2) angle_tol = pl.get_float_attrib(element, 'angle-tol', defaults.element_defaults['angle-tol']) tol_percent = round(tol / grid_size, 2) if grid_size != 0 else 1 js_options = { 'snap_to_grid': pl.get_boolean_attrib(element, 'snap-to-grid', defaults.element_defaults['snap-to-grid']), 'grid_size': grid_size, 'editable': (data['panel'] == 'question' and not preview_mode), 'base_url': data['options']['base_url'], 'client_files': '/pl/static/elements/pl-drawing/clientFilesElement/', 'element_client_files': data['options']['client_files_extensions_url'], 'render_scale': pl.get_float_attrib(element, 'render-scale', defaults.element_defaults['render-scale']), 'width': pl.get_string_attrib(element, 'width', defaults.element_defaults['width']), 'height': pl.get_string_attrib(element, 'height', defaults.element_defaults['height']) } show_btn = data['panel'] == 'question' and not preview_mode if tol == grid_size / 2: message_default = 'The expected tolerance is 1/2 square grid for position and ' + str( angle_tol) + ' degrees for angle.' else: message_default = 'The expected tolerance is ' + str( tol_percent) + ' square grid for position and ' + str( angle_tol) + ' degrees for angle.' html_params = { 'uuid': pl.get_uuid(), 'width': pl.get_string_attrib(element, 'width', defaults.element_defaults['width']), 'height': pl.get_string_attrib(element, 'height', defaults.element_defaults['height']), 'options_json': json.dumps(js_options), 'show_buttons': show_btn, 'name': name, 'render_element': True, 'btn_markup': btn_markup, 'show_tolerance': show_btn and pl.get_boolean_attrib( element, 'show-tolerance-hint', defaults.element_defaults['show-tolerance-hint']), 'tolerance': pl.get_string_attrib(element, 'tolerance-hint', message_default), } if data['panel'] == 'answer' and pl.get_boolean_attrib( element, 'hide-answer-panel', True): return '' if preview_mode: html_params['input_answer'] = json.dumps(init) else: if data['panel'] == 'answer' and name in data['correct_answers']: html_params['input_answer'] = json.dumps( data['correct_answers'][name]) else: sub = [] if name in data['submitted_answers']: sub = data['submitted_answers'][name] items = union_drawing_items(init, sub) html_params['input_answer'] = json.dumps(items) # Grading feedback if data['panel'] == 'submission': parse_error = data['format_errors'].get(name, None) html_params['parse_error'] = parse_error return chevron.render(template, html_params).strip()
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=BODY_POSITION_DEFAULT) body_orientation = get_orientation(element, 'body-orientation', BODY_ORIENTATION_DEFAULT) camera_position = get_position(element, 'camera-position', default=CAMERA_POSITION_DEFAULT, must_be_nonzero=True) body_cantranslate = pl.get_boolean_attrib(element, 'body-cantranslate', BODY_CANTRANSLATE_DEFAULT) body_canrotate = pl.get_boolean_attrib(element, 'body-canrotate', BODY_CANROTATE_DEFAULT) camera_canmove = pl.get_boolean_attrib(element, 'camera-canmove', CAMERA_CANMOVE_DEFAULT) text_pose_format = pl.get_string_attrib(element, 'text-pose-format', TEXT_POSE_FORMAT_DEFAULT) 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', GRADE_DEFAULT) show_pose = pl.get_boolean_attrib(element, 'show-pose-in-question', SHOW_POSE_IN_QUESTION_DEFAULT) # 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', GRADE_DEFAULT) if not will_be_graded: return '' show_pose = pl.get_boolean_attrib( element, 'show-pose-in-submitted-answer', SHOW_POSE_IN_SUBMITTED_ANSWER_DEFAULT) # 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', GRADE_DEFAULT) if not will_be_graded: return '' show_pose = pl.get_boolean_attrib(element, 'show-pose-in-correct-answer', SHOW_POSE_IN_CORRECT_ANSWER_DEFAULT) # 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', ANSWER_POSE_FORMAT_DEFAULT) 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 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) width = pl.get_float_attrib(element, 'width', None) height = pl.get_float_attrib(element, 'height', None) background = None # Assign layer index in order children are defined # Later defined elements will be placed on top of earlier ones locations = [] z_index = 0 for child in element: # Ignore comments if isinstance(child, lxml.html.HtmlComment): continue # Don't do any special processing for backgrounds if child.tag == 'pl-background': background = pl.inner_html(child) continue # Otherwise, continue as normal valign = pl.get_string_attrib(child, 'valign', VALIGN_DEFAULT) halign = pl.get_string_attrib(child, 'halign', HALIGN_DEFAULT) left = pl.get_float_attrib(child, 'left', None) right = pl.get_float_attrib(child, 'right', None) top = pl.get_float_attrib(child, 'top', None) bottom = pl.get_float_attrib(child, 'bottom', None) # We allow both left/right and top/bottom but only set top and left # so we don't have to worry about all the alignment possibilities if left is not None: x = left elif right is not None: x = width - right if top is not None: y = top elif bottom is not None: y = height - bottom hoff = ALIGNMENT_TO_PERC[halign] voff = ALIGNMENT_TO_PERC[valign] transform = f'translate({hoff}, {voff})' style = f'top: {y}px; left: {x}px; transform: {transform}; z-index: {z_index}' obj = { 'html': pl.inner_html(child), 'outer_style': style, } locations.append(obj) z_index += 1 html_params = { 'width': width, 'height': height, 'locations': locations, 'background': background, 'clip': pl.get_boolean_attrib(element, 'clip', CLIP_DEFAULT) } with open('pl-overlay.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() return html
def 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, element_index, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers_name') label = pl.get_string_attrib(element, 'label', None) suffix = pl.get_string_attrib(element, 'suffix', None) display = pl.get_string_attrib(element, 'display', 'inline') 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-5) atol = pl.get_float_attrib(element, 'atol', 1e-8) info_params = { 'format': True, 'relabs': True, 'rtol': rtol, 'atol': atol } elif comparison == 'sigfig': digits = pl.get_integer_attrib(element, 'digits', 2) info_params = {'format': True, 'sigfig': True, 'digits': digits} elif comparison == 'decdig': digits = pl.get_integer_attrib(element, 'digits', 2) info_params = {'format': True, 'decdig': True, 'digits': digits} else: raise ValueError( 'method of comparison "%s" is not valid (must be "relabs", "sigfig", or "decdig")' % comparison) with open('pl_number_input.mustache', 'r') as f: info = chevron.render(f, info_params).strip() with open('pl_number_input.mustache', 'r') 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, 'suffix': suffix, 'editable': editable, 'info': info, 'shortinfo': shortinfo } 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: 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", "block", or "display")' % display) if raw_submitted_answer is not None: html_params['raw_submitted_answer'] = escape(raw_submitted_answer) with open('pl_number_input.mustache', 'r') 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 } if parse_error is None: a_sub = data['submitted_answers'][name] html_params['suffix'] = suffix html_params['a_sub'] = '{:.12g}'.format(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: raise ValueError('invalid score' + score) with open('pl_number_input.mustache', 'r') 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: # Get comparison parameters comparison = pl.get_string_attrib(element, 'comparison', 'relabs') if comparison == 'relabs': rtol = pl.get_float_attrib(element, 'rtol', 1e-5) atol = pl.get_float_attrib(element, 'atol', 1e-8) # FIXME: render correctly with respect to rtol and atol a_tru = '{:.12g}'.format(a_tru) elif comparison == 'sigfig': digits = pl.get_integer_attrib(element, 'digits', 2) a_tru = to_precision.to_precision(a_tru, digits) elif comparison == 'decdig': digits = pl.get_integer_attrib(element, 'digits', 2) a_tru = '{:.{ndigits}f}'.format(a_tru, ndigits=digits) else: raise ValueError( 'method of comparison "%s" is not valid (must be "relabs", "sigfig", or "decdig")' % comparison) # FIXME: render correctly with respect to method of comparison html_params = { 'answer': True, 'label': label, 'a_tru': a_tru, 'suffix': suffix } with open('pl_number_input.mustache', 'r') 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) 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 grade(element_html, data): element = lxml.html.fragment_fromstring(element_html) prev = not pl.get_boolean_attrib(element, 'gradable', defaults.element_defaults['gradable']) if prev: return grid_size = pl.get_integer_attrib(element, 'grid-size', defaults.element_defaults['grid-size']) tol = pl.get_float_attrib(element, 'tol', grid_size / 2) angtol = pl.get_float_attrib(element, 'angle-tol', defaults.element_defaults['angle-tol']) disregard_extra_elements = pl.get_boolean_attrib( element, 'disregard-extra-elements', defaults.element_defaults['disregard-extra-elements']) name = pl.get_string_attrib(element, 'answers-name', defaults.element_defaults['answers-name']) student = data['submitted_answers'][name] reference = data['correct_answers'][name] if not isinstance(student, list) or len(student) == 0: data['format_errors'][name] = 'No submitted answer.' return data matches = {} # If a reference object is matched to a student object num_correct = 0 # number correct num_total_ref = 0 num_total_st = 0 num_optional = 0 # Ensure that each reference object matches one and only one # student object. Ungraded elements are skipped. # num_total_ref is the total number of objects that are expected to be graded # this disregards optional objects and objects that don't have a grading function for ref_element in reference: if elements.is_gradable( ref_element['gradingName']) and ref_element['graded']: matches[ref_element['id']] = False if 'optional_grading' in ref_element and ref_element[ 'optional_grading']: continue num_total_ref += 1 # Loop through and check everything for element in student: if 'gradingName' not in element or not elements.is_gradable( element['gradingName'] ) or 'graded' not in element or not element['graded']: continue # total number of objects inserted by students (using buttons) # this will disregard the initial objects placed by question authors num_total_st += 1 for ref_element in reference: if not elements.is_gradable( ref_element['gradingName'] ) or not ref_element['graded'] or element[ 'gradingName'] != ref_element['gradingName']: # Skip if the reference element is not gradable continue if not disregard_extra_elements and matches[ref_element['id']]: # Skip if this object has already been matched continue if elements.grade(ref_element, element, element['gradingName'], tol, angtol): if ('optional_grading' in ref_element and ref_element['optional_grading']) or ( disregard_extra_elements and matches[ref_element['id']]): # It's optional but correct, so the score should not be affected # Or, it's a duplicate and we're okay with that. num_optional += 1 else: # if correct but optional, it does not add on number of correct answers, # but the object will not be considered as extra num_correct += 1 matches[ref_element['id']] = True break extra_not_optional = num_total_st - (num_optional + num_correct) if num_total_ref == 0: score = 1 else: percent_correct = num_correct / num_total_ref if percent_correct == 1: # if all the expected objects are matched # penalize extra objects score = max(0, percent_correct - extra_not_optional / num_total_ref) else: score = percent_correct data['partial_scores'][name] = { 'score': score, 'weight': 1, 'feedback': { 'correct': (score == 1), 'missing': {}, 'matches': matches } } return data
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
def get_objects(element, data): obj_list = [] for child in element: is_stl = (child.tag in ['pl-threejs-stl', 'pl_threejs_stl']) is_txt = (child.tag in ['pl-threejs-txt', 'pl_threejs_txt']) if not (is_stl or is_txt): continue # Type-specific check and get (stl) if is_stl: # Attributes pl.check_attribs(child, required_attribs=['file-name'], optional_attribs=['file-directory', 'frame', 'color', 'position', 'orientation', 'format', 'scale', 'opacity']) # - file-name (and file-directory) file_url = get_file_url(child, data) # - type object_type = 'stl' # - object specific = { 'type': object_type, 'file_url': file_url } # Type-specific check and get (txt) if is_txt: # Attributes pl.check_attribs(child, required_attribs=[], optional_attribs=['frame', 'color', 'position', 'orientation', 'format', 'scale', 'opacity']) # - text text = pl.inner_html(child) # - type object_type = 'txt' # - object specific = { 'type': object_type, 'text': text } # Common # - frame frame = pl.get_string_attrib(child, 'frame', 'body') if frame not in ['body', 'space']: raise Exception('"frame" must be either "body" or "space": {:s}'.format(frame)) if frame == 'body': default_color = '#e84a27' default_opacity = 0.7 else: default_color = '#13294b' default_opacity = 0.4 # - color color = pl.get_color_attrib(child, 'color', default_color) # - opacity opacity = pl.get_float_attrib(child, 'opacity', default_opacity) # - position p = pl.get_string_attrib(child, 'position', '[0, 0, 0]') try: position = np.array(json.loads(p), dtype=np.float64) if position.shape == (3,): position = position.tolist() else: raise ValueError() except Exception: raise Exception('attribute "position" must have format [x, y, z]: {:s}'.format(p)) # - orientation (and format) orientation = get_orientation(child, 'orientation', 'format') # - scale scale = pl.get_float_attrib(child, 'scale', 1.0) common = { 'frame': frame, 'color': color, 'opacity': opacity, 'position': position, 'quaternion': orientation, 'scale': scale } obj = {**specific, **common} obj_list.append(obj) return obj_list
def 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 # 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 np.float64, because... # # If the method of comparison is relabs (i.e., using relative and # absolute tolerance) then np.allclose is applied to check if the # submitted and true answers are the same. If either answer is an # integer outside the range of int64... # # https://docs.scipy.org/doc/numpy-1.13.0/user/basics.types.html # # ...then numpy throws this error: # # TypeError: ufunc 'isfinite' not supported for the input types, and # the inputs could not be safely coerced to any supported types # according to the casting rule ''safe'' # # Casting as np.float64 avoids this error. This is reasonable in any case, # because <pl-number-input> accepts double-precision floats, not ints. # if np.iscomplexobj(a_sub) or np.iscomplexobj(a_tru): a_sub = np.complex128(a_sub) a_tru = np.complex128(a_tru) else: a_sub = np.float64(a_sub) a_tru = np.float64(a_tru) # 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_scalar_ra(a_sub, a_tru, rtol, atol) elif comparison == 'sigfig': digits = pl.get_integer_attrib(element, 'digits', DIGITS_DEFAULT) correct = pl.is_correct_scalar_sf(a_sub, a_tru, digits) elif comparison == 'decdig': digits = pl.get_integer_attrib(element, 'digits', DIGITS_DEFAULT) correct = pl.is_correct_scalar_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 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', LABEL_DEFAULT) 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', COMPARISON_DEFAULT) if comparison == 'relabs': rtol = pl.get_float_attrib(element, 'rtol', RTOL_DEFAULT) atol = pl.get_float_attrib(element, 'atol', ATOL_DEFAULT) 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', DIGITS_DEFAULT) 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', DIGITS_DEFAULT) 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', ALLOW_COMPLEX_DEFAULT) 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, 'show_info': pl.get_boolean_attrib(element, 'show-help-text', SHOW_HELP_TEXT_DEFAULT), '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'] = pl.escape_unicode_string( 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 and name in data['submitted_answers']: # 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') elif name not in data['submitted_answers']: html_params['missing_input'] = True html_params['parse_error'] = None else: raw_submitted_answer = data['raw_submitted_answers'].get( name, None) if raw_submitted_answer is not None: html_params['raw_submitted_answer'] = pl.escape_unicode_string( 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) html_params['error'] = html_params['parse_error'] or html_params.get( 'missing_input', False) 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', COMPARISON_DEFAULT) if comparison == 'relabs': rtol = pl.get_float_attrib(element, 'rtol', RTOL_DEFAULT) atol = pl.get_float_attrib(element, 'atol', ATOL_DEFAULT) # 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', DIGITS_DEFAULT) 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', DIGITS_DEFAULT) 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) suffix = pl.get_string_attrib(element, 'suffix', None) display = pl.get_string_attrib(element, 'display', DISPLAY_DEFAULT) if data['panel'] == 'question': editable = data['editable'] raw_submitted_answer = data['raw_submitted_answers'].get(name, None) html_params = { 'question': True, 'name': name, 'label': label, 'suffix': suffix, 'editable': editable, 'size': pl.get_integer_attrib(element, 'size', SIZE_DEFAULT), '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) # Get comparison parameters and info strings 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) 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', DIGITS_DEFAULT) 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', DIGITS_DEFAULT) 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) # Update parameters for the info popup show_correct = 'correct' in html_params and pl.get_boolean_attrib( element, 'show-correct-answer', SHOW_CORRECT_ANSWER_DEFAULT) info_params['allow_complex'] = pl.get_boolean_attrib( element, 'allow-complex', ALLOW_COMPLEX_DEFAULT) info_params['show_info'] = pl.get_boolean_attrib( element, 'show-help-text', SHOW_HELP_TEXT_DEFAULT) info_params['show_correct'] = show_correct # Find the true answer to be able to display it in the info popup ans_true = None if pl.get_boolean_attrib(element, 'show-correct-answer', SHOW_CORRECT_ANSWER_DEFAULT): ans_true = format_true_ans(element, data, name) if ans_true is not None: info_params['a_tru'] = ans_true with open('pl-number-input.mustache', 'r', encoding='utf-8') as f: info = chevron.render(f, info_params).strip() with open('pl-number-input.mustache', 'r', encoding='utf-8') as f: info_params.pop('format', None) # Within mustache, the shortformat generates the shortinfo that is used as a placeholder inside of the numeric entry. # Here we opt to not generate the value, hence the placeholder is empty. info_params['shortformat'] = pl.get_boolean_attrib( element, 'show-placeholder', SHOW_PLACEHOLDER_DEFAULT) shortinfo = chevron.render(f, info_params).strip() html_params['info'] = info html_params['shortinfo'] = shortinfo # Determine the title of the popup based on what information is being shown if pl.get_boolean_attrib(element, 'show-help-text', SHOW_HELP_TEXT_DEFAULT): html_params['popup_title'] = 'Number' else: html_params['popup_title'] = 'Correct Answer' # Enable or disable the popup if pl.get_boolean_attrib(element, 'show-help-text', SHOW_HELP_TEXT_DEFAULT) or show_correct: html_params['show_info'] = True html_params[ 'display_append_span'] = 'questionmark' in html_params or suffix 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-number-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) html_params['suffix'] = suffix html_params['a_sub'] = '{:.12g}'.format(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) # Add true answer to be able to display it in the submitted answer panel ans_true = None if pl.get_boolean_attrib(element, 'show-correct-answer', SHOW_CORRECT_ANSWER_DEFAULT): ans_true = format_true_ans(element, data, name) if ans_true is not None: html_params['a_tru'] = ans_true 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-number-input.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() elif data['panel'] == 'answer': ans_true = format_true_ans(element, data, name) if ans_true is not None: html_params = { 'answer': True, 'label': label, 'a_tru': ans_true, 'suffix': suffix } with open('pl-number-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, element_index, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers_name') label = pl.get_string_attrib(element, 'label', None) suffix = pl.get_string_attrib(element, 'suffix', None) display = pl.get_string_attrib(element, 'display', 'inline') 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) info_params = { 'format': True, 'relabs': True, 'rtol': rtol, 'atol': atol } elif comparison == 'sigfig': digits = pl.get_integer_attrib(element, 'digits', 2) info_params = { 'format': True, 'sigfig': True, 'digits': digits, 'comparison_eps': 0.51 * (10**-(digits - 1)) } elif comparison == 'decdig': digits = pl.get_integer_attrib(element, 'digits', 2) info_params = { 'format': True, 'decdig': True, 'digits': 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_number_input.mustache', 'r', encoding='utf-8') as f: info = chevron.render(f, info_params).strip() with open('pl_number_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, 'suffix': suffix, '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 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_number_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) html_params['suffix'] = suffix html_params['a_sub'] = '{:.12g}'.format(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) with open('pl_number_input.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() elif data['panel'] == 'answer': a_tru = pl.from_json(data['correct_answers'].get(name, None)) if a_tru is not None: # 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 a_tru = '{:.12g}'.format(a_tru) elif comparison == 'sigfig': digits = pl.get_integer_attrib(element, 'digits', 2) a_tru = pl.string_from_number_sigfig(a_tru, digits=digits) elif comparison == 'decdig': digits = pl.get_integer_attrib(element, 'digits', 2) a_tru = '{:.{ndigits}f}'.format(a_tru, ndigits=digits) else: raise ValueError( 'method of comparison "%s" is not valid (must be "relabs", "sigfig", or "decdig")' % comparison) # FIXME: render correctly with respect to method of comparison html_params = { 'answer': True, 'label': label, 'a_tru': a_tru, 'suffix': suffix } with open('pl_number_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 get_objects(element, data): obj_list = [] for child in element: is_stl = (child.tag in ['pl-threejs-stl', 'pl_threejs_stl']) is_txt = (child.tag in ['pl-threejs-txt', 'pl_threejs_txt']) if not (is_stl or is_txt): continue # Type-specific check and get (stl) if is_stl: # Attributes pl.check_attribs(child, required_attribs=['file-name'], optional_attribs=[ 'file-directory', 'frame', 'color', 'position', 'orientation', 'format', 'scale', 'opacity' ]) # - file-name (and file-directory) file_url = get_file_url(child, data) # - type object_type = 'stl' # - object specific = {'type': object_type, 'file_url': file_url} # Type-specific check and get (txt) if is_txt: # Attributes pl.check_attribs(child, required_attribs=[], optional_attribs=[ 'frame', 'color', 'position', 'orientation', 'format', 'scale', 'opacity' ]) # - text text = pl.inner_html(child) # - type object_type = 'txt' # - object specific = {'type': object_type, 'text': text} # Common # - frame frame = pl.get_string_attrib(child, 'frame', 'body') if frame not in ['body', 'space']: raise Exception( '"frame" must be either "body" or "space": {:s}'.format(frame)) if frame == 'body': default_color = '#e84a27' default_opacity = 0.7 else: default_color = '#13294b' default_opacity = 0.4 # - color color = pl.get_color_attrib(child, 'color', default_color) # - opacity opacity = pl.get_float_attrib(child, 'opacity', default_opacity) # - position p = pl.get_string_attrib(child, 'position', '[0, 0, 0]') try: position = np.array(json.loads(p), dtype=np.float64) if position.shape == (3, ): position = position.tolist() else: raise ValueError() except Exception: raise Exception( 'attribute "position" must have format [x, y, z]: {:s}'.format( p)) # - orientation (and format) orientation = get_orientation(child, 'orientation', 'format') # - scale scale = pl.get_float_attrib(child, 'scale', 1.0) common = { 'frame': frame, 'color': color, 'opacity': opacity, 'position': position, 'quaternion': orientation, 'scale': scale } obj = {**specific, **common} obj_list.append(obj) return obj_list
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) # 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